├── .github ├── ISSUE_TEMPLATE │ ├── 1-bug.yaml │ ├── 2-feature.yaml │ ├── 3-docs.yaml │ ├── 4-nix.yaml │ └── config.yml └── workflows │ ├── _build_docs.yaml │ ├── _deploy_docs.yaml │ ├── latest_docs.yaml │ ├── pr_docs.yaml │ ├── release.yaml │ ├── release_docs.yaml │ ├── stable_docs.yaml │ └── static.yaml ├── .gitignore ├── LICENSE ├── README.md ├── bin └── ignis ├── dev.txt ├── docs ├── Makefile ├── _ext │ └── ignis_directives.py ├── _static │ ├── css │ │ └── custom.css │ ├── images │ │ └── user_guide │ │ │ ├── boxes.png │ │ │ └── hello_world.png │ └── switcher.json ├── api │ ├── app.rst │ ├── base_service.rst │ ├── base_widget.rst │ ├── client.rst │ ├── connection_manager.rst │ ├── dbus.rst │ ├── dbus_menu.rst │ ├── deprecation.rst │ ├── exceptions.rst │ ├── gobject.rst │ ├── index.rst │ ├── menu_model.rst │ ├── options.rst │ ├── options_manager.rst │ ├── services │ │ ├── applications.rst │ │ ├── audio.rst │ │ ├── backlight.rst │ │ ├── bluetooth.rst │ │ ├── fetch.rst │ │ ├── hyprland.rst │ │ ├── index.rst │ │ ├── mpris.rst │ │ ├── network.rst │ │ ├── niri.rst │ │ ├── notifications.rst │ │ ├── recorder.rst │ │ ├── system_tray.rst │ │ ├── systemd.rst │ │ ├── upower.rst │ │ └── wallpaper.rst │ ├── toplevel.rst │ ├── utils │ │ ├── FileMonitor.rst │ │ ├── Poll.rst │ │ ├── Timeout.rst │ │ ├── debounce.rst │ │ ├── file.rst │ │ ├── icon.rst │ │ ├── index.rst │ │ ├── misc.rst │ │ ├── monitor.rst │ │ ├── pixbuf.rst │ │ ├── sass.rst │ │ ├── sh.rst │ │ ├── socket.rst │ │ ├── str_cases.rst │ │ ├── thread.rst │ │ └── version.rst │ ├── variable.rst │ └── widgets │ │ ├── Arrow.rst │ │ ├── ArrowButton.rst │ │ ├── Box.rst │ │ ├── Button.rst │ │ ├── Calendar.rst │ │ ├── CenterBox.rst │ │ ├── CheckButton.rst │ │ ├── DropDown.rst │ │ ├── Entry.rst │ │ ├── EventBox.rst │ │ ├── FileChooserButton.rst │ │ ├── FileDialog.rst │ │ ├── FileFilter.rst │ │ ├── Grid.rst │ │ ├── HeaderBar.rst │ │ ├── Icon.rst │ │ ├── Label.rst │ │ ├── ListBox.rst │ │ ├── ListBoxRow.rst │ │ ├── Overlay.rst │ │ ├── Picture.rst │ │ ├── PopoverMenu.rst │ │ ├── RegularWindow.rst │ │ ├── Revealer.rst │ │ ├── RevealerWindow.rst │ │ ├── Scale.rst │ │ ├── Scroll.rst │ │ ├── Separator.rst │ │ ├── SpinButton.rst │ │ ├── Stack.rst │ │ ├── Switch.rst │ │ ├── ToggleButton.rst │ │ ├── Window.rst │ │ └── index.rst ├── conf.py ├── dev │ ├── code_style.rst │ ├── creating_service.rst │ ├── documentation.rst │ ├── env.rst │ ├── gobject.rst │ ├── index.rst │ └── subclassing_widgets.rst ├── examples │ ├── code_snippets.rst │ ├── configurations.rst │ └── index.rst ├── index.rst ├── requirements.txt └── user │ ├── async.rst │ ├── cli.rst │ ├── dynamic_content.rst │ ├── expanding_functionality.rst │ ├── faq.rst │ ├── first_widgets.rst │ ├── index.rst │ ├── installation.rst │ ├── nix.rst │ ├── options.rst │ ├── styling.rst │ ├── troubleshooting.rst │ └── using_classes.rst ├── examples └── bar │ ├── README.md │ ├── config.py │ ├── simple-bar.png │ └── style.scss ├── flake.lock ├── flake.nix ├── ignis ├── __commit__.py.in ├── __init__.py ├── app.py ├── base_service.py ├── base_widget.py ├── cli.py ├── client.py ├── connection_manager.py ├── dbus.py ├── dbus │ ├── com.canonical.dbusmenu.xml │ ├── com.github.linkfrg.ignis.xml │ ├── org.freedesktop.DBus.Peer.xml │ ├── org.freedesktop.DBus.xml │ ├── org.freedesktop.Notifications.xml │ ├── org.freedesktop.UPower.Device.xml │ ├── org.freedesktop.UPower.xml │ ├── org.freedesktop.login1.Manager.xml │ ├── org.freedesktop.login1.Session.xml │ ├── org.freedesktop.portal.Request.xml │ ├── org.freedesktop.portal.ScreenCast.xml │ ├── org.freedesktop.systemd1.Manager.xml │ ├── org.freedesktop.systemd1.Unit.xml │ ├── org.kde.StatusNotifierItem.xml │ ├── org.kde.StatusNotifierWatcher.xml │ ├── org.mpris.MediaPlayer2.Player.xml │ └── org.mpris.MediaPlayer2.xml ├── dbus_menu.py ├── deprecation.py ├── exceptions.py ├── gobject.py ├── log_utils.py ├── main.py ├── menu_model.py ├── options.py ├── options_manager.py ├── py.typed ├── services │ ├── __init__.py │ ├── applications │ │ ├── __init__.py │ │ ├── action.py │ │ ├── application.py │ │ └── service.py │ ├── audio │ │ ├── __init__.py │ │ ├── _imports.py │ │ ├── constants.py │ │ ├── service.py │ │ └── stream.py │ ├── backlight │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── device.py │ │ ├── service.py │ │ └── util.py │ ├── bluetooth │ │ ├── __init__.py │ │ ├── _imports.py │ │ ├── constants.py │ │ ├── device.py │ │ └── service.py │ ├── fetch │ │ ├── __init__.py │ │ └── service.py │ ├── hyprland │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── keyboard.py │ │ ├── monitor.py │ │ ├── service.py │ │ ├── window.py │ │ └── workspace.py │ ├── mpris │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── player.py │ │ ├── service.py │ │ └── util.py │ ├── network │ │ ├── __init__.py │ │ ├── _imports.py │ │ ├── access_point.py │ │ ├── constants.py │ │ ├── ethernet.py │ │ ├── ethernet_device.py │ │ ├── service.py │ │ ├── util.py │ │ ├── vpn.py │ │ ├── wifi.py │ │ ├── wifi_connect_dialog.py │ │ └── wifi_device.py │ ├── niri │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── keyboard.py │ │ ├── service.py │ │ ├── window.py │ │ └── workspace.py │ ├── notifications │ │ ├── __init__.py │ │ ├── action.py │ │ ├── constants.py │ │ ├── notification.py │ │ └── service.py │ ├── recorder │ │ ├── __init__.py │ │ ├── _imports.py │ │ ├── constants.py │ │ ├── service.py │ │ ├── session.py │ │ └── util.py │ ├── system_tray │ │ ├── __init__.py │ │ ├── item.py │ │ └── service.py │ ├── systemd │ │ ├── __init__.py │ │ ├── service.py │ │ └── unit.py │ ├── upower │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── device.py │ │ └── service.py │ └── wallpaper │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── service.py │ │ └── window.py ├── utils │ ├── __init__.py │ ├── debounce.py │ ├── file.py │ ├── file_monitor.py │ ├── icon.py │ ├── misc.py │ ├── monitor.py │ ├── pixbuf.py │ ├── poll.py │ ├── sass.py │ ├── shell.py │ ├── socket.py │ ├── str_cases.py │ ├── thread.py │ ├── timeout.py │ └── version.py ├── variable.py └── widgets │ ├── __init__.py │ ├── arrow.py │ ├── arrow_button.py │ ├── box.py │ ├── button.py │ ├── calendar.py │ ├── centerbox.py │ ├── check_button.py │ ├── dropdown.py │ ├── entry.py │ ├── eventbox.py │ ├── file_chooser_button.py │ ├── file_dialog.py │ ├── file_filter.py │ ├── grid.py │ ├── headerbar.py │ ├── icon.py │ ├── label.py │ ├── listbox.py │ ├── listboxrow.py │ ├── overlay.py │ ├── picture.py │ ├── popover_menu.py │ ├── regular_window.py │ ├── revealer.py │ ├── revealer_window.py │ ├── scale.py │ ├── scroll.py │ ├── separator.py │ ├── spin_button.py │ ├── stack.py │ ├── stack_page.py │ ├── stack_switcher.py │ ├── switch.py │ ├── toggle_button.py │ └── window.py ├── meson.build ├── meson_options.txt ├── nix ├── default.nix └── version.nix ├── pyproject.toml ├── requirements.txt ├── subprojects └── gvc.wrap └── tools └── get_version.py /.github/ISSUE_TEMPLATE/1-bug.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Something is not working as expected 3 | labels: ["bug"] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: System Information 8 | description: | 9 | Paste the output of `ignis systeminfo` below. 10 | 11 | value: " 12 | ``` 13 | 14 | 15 | 16 | ```" 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | attributes: 22 | label: Description 23 | description: "Provide a clear and concise description of the issue you encountered" 24 | validations: 25 | required: true 26 | 27 | - type: textarea 28 | attributes: 29 | label: How to reproduce 30 | description: "List the steps to reproduce the issue. Be as detailed as possible to help replicate the problem" 31 | validations: 32 | required: true 33 | 34 | - type: textarea 35 | attributes: 36 | label: Additional Information 37 | description: | 38 | Anything that can help (Logs, Images, Videos, Configs, etc.). Please avoid pasting large text directly. 39 | Logs can be found at `$XDG_STATE_HOME/ignis/ignis.log` (`~/.local/state/ignis/ignis.log`). 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature or improvement 3 | labels: ["enhancement"] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Feature Description 8 | description: "Clearly describe the feature or improvement you'd like to see" 9 | validations: 10 | required: true 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-docs.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | description: Suggest improvements or additions to the documentation 3 | labels: ["documentation"] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Description 8 | description: "Clearly describe your suggestion for improving the documentation" 9 | validations: 10 | required: true 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/4-nix.yaml: -------------------------------------------------------------------------------- 1 | name: Nix/NixOS 2 | description: Related to the Nix package manager or NixOS 3 | labels: ["nix"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please note that I (linkfrg) don't maintain the Nix flake for Ignis, any help or pull request would be highly appreciated. 9 | 10 | --- 11 | 12 | - type: textarea 13 | attributes: 14 | label: Description 15 | description: "Describe the issue" 16 | validations: 17 | required: true 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Question 3 | url: https://github.com/ignis-sh/ignis/discussions 4 | about: To ask a question, please use Github Discussions -------------------------------------------------------------------------------- /.github/workflows/_build_docs.yaml: -------------------------------------------------------------------------------- 1 | name: Build Documentation 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | DOC_TAG: 7 | required: true 8 | type: string 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: '3.12' 22 | 23 | - name: Install Python dependencies 24 | run: | 25 | pip install -r docs/requirements.txt 26 | 27 | - name: Build documentation 28 | run: | 29 | export DOC_TAG=${{ inputs.DOC_TAG }} 30 | cd docs 31 | make html 32 | 33 | - name: Upload Documentation Artifact 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: documentation-${{ inputs.DOC_TAG }} 37 | path: docs/_build/html 38 | retention-days: 7 39 | -------------------------------------------------------------------------------- /.github/workflows/_deploy_docs.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy Documentation 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | DOC_TAG: 7 | required: true 8 | type: string 9 | 10 | jobs: 11 | build: 12 | uses: ./.github/workflows/_build_docs.yaml 13 | with: 14 | DOC_TAG: ${{ inputs.DOC_TAG }} 15 | 16 | deploy: 17 | runs-on: ubuntu-latest 18 | needs: build 19 | 20 | steps: 21 | - name: Download Documentation Artifact 22 | uses: actions/download-artifact@v4 23 | with: 24 | name: documentation-${{ inputs.DOC_TAG }} 25 | path: docs/_build/html 26 | 27 | - name: Deploy to GitHub Pages 28 | uses: peaceiris/actions-gh-pages@v4 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | publish_dir: docs/_build/html 32 | destination_dir: ${{ inputs.DOC_TAG }} 33 | allow_empty_commit: true 34 | force_orphan: false 35 | keep_files: true 36 | -------------------------------------------------------------------------------- /.github/workflows/latest_docs.yaml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "ignis/**" 9 | - "docs/**" 10 | - ".github/workflows/latest_docs.yaml" 11 | - ".github/workflows/_build_docs.yaml" 12 | - ".github/workflows/_deploy_docs.yaml" 13 | workflow_dispatch: 14 | 15 | jobs: 16 | build: 17 | uses: ./.github/workflows/_deploy_docs.yaml 18 | with: 19 | DOC_TAG: "latest" -------------------------------------------------------------------------------- /.github/workflows/pr_docs.yaml: -------------------------------------------------------------------------------- 1 | name: Build PR Documentation 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - "ignis/**" 7 | - "docs/**" 8 | - ".github/workflows/pr_docs.yaml" 9 | - ".github/workflows/_build_docs.yaml" 10 | branches: 11 | - '*' 12 | 13 | jobs: 14 | build: 15 | uses: ./.github/workflows/_build_docs.yaml 16 | with: 17 | DOC_TAG: "latest" 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release Tarball 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | inputs: 8 | TAG_NAME: 9 | description: "Release tag name" 10 | type: "string" 11 | required: true 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | env: 17 | RELEASE_TAG: ${{ github.event.release.tag_name || github.event.inputs.TAG_NAME }} 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | with: 23 | ref: ${{ env.RELEASE_TAG }} 24 | 25 | - name: Install meson 26 | run: | 27 | sudo apt update 28 | sudo apt install -y meson 29 | 30 | - name: Download subprojects 31 | run: | 32 | meson subprojects download 33 | 34 | - name: Create tarball 35 | run: | 36 | cd .. 37 | tar -czf ignis.tar.gz ignis 38 | 39 | - name: Upload tarball to release 40 | uses: svenstaro/upload-release-action@v2 41 | with: 42 | file: ../ignis.tar.gz 43 | asset_name: ignis-${{ env.RELEASE_TAG }}.tar.gz 44 | tag: ${{ env.RELEASE_TAG }} 45 | repo_token: ${{ secrets.GITHUB_TOKEN }} 46 | overwrite: true 47 | -------------------------------------------------------------------------------- /.github/workflows/release_docs.yaml: -------------------------------------------------------------------------------- 1 | name: Build Release Documentation 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | uses: ./.github/workflows/_deploy_docs.yaml 10 | with: 11 | DOC_TAG: ${{ github.ref_name }} 12 | -------------------------------------------------------------------------------- /.github/workflows/stable_docs.yaml: -------------------------------------------------------------------------------- 1 | name: Build Stable Documentation 2 | 3 | on: 4 | 5 | push: 6 | branches: 7 | - stable-docs 8 | paths: 9 | - "ignis/**" 10 | - "docs/**" 11 | - ".github/workflows/stable_docs.yaml" 12 | - ".github/workflows/_build_docs.yaml" 13 | - ".github/workflows/_deploy_docs.yaml" 14 | workflow_dispatch: 15 | 16 | jobs: 17 | build: 18 | uses: ./.github/workflows/_deploy_docs.yaml 19 | with: 20 | DOC_TAG: "stable" 21 | -------------------------------------------------------------------------------- /.github/workflows/static.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | paths: 6 | - "ignis/**" 7 | - "examples/**" 8 | - ".github/workflows/static.yaml" 9 | pull_request: 10 | paths: 11 | - "ignis/**" 12 | - "examples/**" 13 | - ".github/workflows/static.yaml" 14 | 15 | name: Static Analysis 16 | jobs: 17 | mypy-check: 18 | name: Mypy 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up Python 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: "3.12" 26 | 27 | - name: Install system dependencies 28 | run: | 29 | sudo apt update 30 | sudo apt install libcairo2-dev 31 | 32 | - name: Install Python dependencies 33 | run: | 34 | python -m pip install --upgrade pip 35 | pip install -r dev.txt 36 | grep -vE "PyGObject|setuptools" requirements.txt | pip install -r /dev/stdin 37 | 38 | - name: Run mypy analysis 39 | run: | 40 | mypy 41 | 42 | ruff-check: 43 | name: Ruff 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - name: Set up Python 48 | uses: actions/setup-python@v5 49 | with: 50 | python-version: "3.12" 51 | - name: Install Python dependencies 52 | run: | 53 | python -m pip install --upgrade pip 54 | pip install ruff 55 | 56 | - name: Check code 57 | run: ruff check 58 | 59 | - name: Check format 60 | run: ruff format --check 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | venv 3 | .venv 4 | dist 5 | docs/_build 6 | generated 7 | build 8 | .vscode/* 9 | !.vscode/settings.json 10 | ignis/__lib_dir__.py 11 | ignis/__commit__.py 12 | subprojects/gvc 13 | 14 | .idea/* 15 | docs/tmp 16 | .artifacts 17 | result -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ignis 2 | 3 | [![docs](https://github.com/ignis-sh/ignis/actions/workflows/latest_docs.yaml/badge.svg)](https://github.com/ignis-sh/ignis/actions/workflows/latest_docs.yaml) 4 | [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) 5 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 6 | [![Linting: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 7 | 8 | A widget framework for building desktop shells, written and configurable in Python. 9 | 10 | - Easy to use 11 | - GTK4-based 12 | - Batteries Included (a lot of built-in Services and Utilities!) 13 | - Flexible work with widgets 14 | 15 | > [!NOTE] 16 | > Ignis is mostly stable, but still a work in progress. 17 | > The API is a subject to change. 18 | > 19 | > The breaking changes tracker is available in [#60](https://github.com/ignis-sh/ignis/issues/60) 20 | 21 | ## Getting started 22 | See the [Documentation](https://ignis-sh.github.io/ignis) 23 | 24 | ## Supported Desktops 25 | - wlroots-based Wayland compositors (e.g., __Sway__) 26 | - __Hyprland__ 27 | - Smithay based compositors (e.g., __COSMIC__) 28 | - __KDE Plasma__ on wayland 29 | 30 | ...and all other compositors that implement the Layer Shell protocol. 31 | 32 | Ignis __is not supported__ on: 33 | - GNOME Wayland 34 | - X11 35 | 36 | ...because they don't support the Layer Shell protocol. 37 | 38 | ## Examples 39 | * A simple bar, see [examples](./examples/bar) 40 | ![simple-bar](./examples/bar/simple-bar.png) 41 | 42 | * [My own configuration](https://github.com/linkfrg/dotfiles/) 43 | ![My own configuration](https://github.com/linkfrg/dotfiles/blob/main/assets/1.png?raw=true) 44 | 45 | ## Contributing 46 | Check out the [Developer Guide](https://ignis-sh.github.io/ignis/latest/dev/index.html) 47 | 48 | ## Licensing 49 | Ignis is licensed under the __LGPL-2.1-or-later__. 50 | 51 | ## Special Thanks 52 | [AGS](https://github.com/aylur/ags) - for inspiration 53 | -------------------------------------------------------------------------------- /bin/ignis: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | if __name__ == "__main__": 4 | from ignis.main import main 5 | main() 6 | -------------------------------------------------------------------------------- /dev.txt: -------------------------------------------------------------------------------- 1 | PyGObject-stubs==2.12.0 --config-settings=config=Gtk4,Gdk4 2 | git+https://github.com/ignis-sh/gi-stubs-extra.git 3 | mypy>=1.11.2 4 | ruff>=0.6.2 -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= "--fail-on-warning" 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_ext/ignis_directives.py: -------------------------------------------------------------------------------- 1 | from sphinx.application import Sphinx 2 | from sphinx.util.typing import ExtensionMetadata 3 | from sphinx.ext.autodoc import PropertyDocumenter, AttributeDocumenter 4 | from sphinx.domains.python import PyProperty 5 | from docutils import nodes 6 | from docutils.statemachine import StringList 7 | from typing import Any 8 | from sphinx import addnodes 9 | from sphinx.ext.autodoc.mock import mock 10 | 11 | 12 | def create_prefix(name: str) -> list[nodes.Node]: 13 | prefix: list[nodes.Node] = [] 14 | prefix.append(nodes.Text(name)) 15 | prefix.append(addnodes.desc_sig_space()) 16 | return prefix 17 | 18 | 19 | class GPropertyDocumenter(PropertyDocumenter): 20 | objtype = "gproperty" 21 | directivetype = "gproperty" 22 | priority = PropertyDocumenter.priority + 1 23 | 24 | @classmethod 25 | def can_document_member( 26 | cls, member: Any, membername: str, isattr: bool, parent: Any 27 | ) -> bool: 28 | with mock(["gi"]): 29 | from ignis.gobject import IgnisProperty 30 | 31 | return isinstance(member, IgnisProperty) 32 | 33 | def add_content( 34 | self, 35 | more_content: StringList | None, 36 | ) -> None: 37 | source_name = self.get_sourcename() 38 | obj = self.object 39 | 40 | if obj.fset: 41 | self.add_line("- read-write", source_name) 42 | else: 43 | self.add_line("- read-only", source_name) 44 | 45 | self.add_line("", source_name) 46 | 47 | super().add_content(more_content) 48 | 49 | 50 | class SignalDocumenter(AttributeDocumenter): 51 | objtype = "signal" 52 | directivetype = "signal" 53 | priority = AttributeDocumenter.priority + 1 54 | 55 | @classmethod 56 | def can_document_member( 57 | cls, member: Any, membername: str, isattr: bool, parent: Any 58 | ) -> bool: 59 | with mock(["gi"]): 60 | from ignis.gobject import IgnisSignal 61 | return isinstance(member, IgnisSignal) 62 | 63 | 64 | class PropertyDirective(PyProperty): 65 | def get_signature_prefix(self, sig: str) -> list[nodes.Node]: 66 | return create_prefix("gproperty") 67 | 68 | 69 | class SignalDirective(PyProperty): 70 | def get_signature_prefix(self, sig: str) -> list[nodes.Node]: 71 | return create_prefix("signal") 72 | 73 | 74 | def setup(app: Sphinx) -> ExtensionMetadata: 75 | app.setup_extension("sphinx.ext.autodoc") 76 | 77 | app.add_autodocumenter(GPropertyDocumenter) 78 | app.add_autodocumenter(SignalDocumenter) 79 | app.add_directive_to_domain("py", "gproperty", PropertyDirective) 80 | app.add_directive_to_domain("py", "signal", SignalDirective) 81 | return { 82 | "version": "1", 83 | "parallel_read_safe": True, 84 | } 85 | -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap'); 2 | 3 | html { 4 | --pst-font-family-base: "JetBrains Mono", var(--pst-font-family-base-system); 5 | --pst-font-family-heading: "JetBrains Mono", var(--pst-font-family-base-system); 6 | --pst-font-family-monospace: "JetBrains Mono", var(--pst-font-family-monospace-system); 7 | } 8 | 9 | a { 10 | text-decoration: none; 11 | } 12 | 13 | pre, code { 14 | font-family: "JetBrains Mono"; 15 | line-height: 1.5; 16 | padding: 10px; 17 | box-sizing: border-box; 18 | overflow: auto; 19 | } 20 | 21 | pre { 22 | font-size: 1rem; 23 | } 24 | 25 | 26 | html[data-theme=dark] .bd-content img:not(.only-dark) { 27 | background: transparent; 28 | border-radius: 0; 29 | filter: none; 30 | } 31 | 32 | html[data-theme="light"] { 33 | --pst-color-primary: #35618e; 34 | --pst-color-secondary: #535f70; 35 | --pst-color-danger: #ba1a1a; 36 | --pst-color-background: #ffffff; 37 | --pst-color-on-background: #f2f3f9; 38 | --pst-color-surface: #f8f9ff; 39 | --pst-color-on-surface: #e6e8ee; 40 | } 41 | 42 | html[data-theme="dark"] { 43 | --pst-color-primary: #a0cafd; 44 | --pst-color-secondary: #bac8db; 45 | --pst-color-danger: #ffb4ab; 46 | --pst-color-background: #0b0e13; 47 | --pst-color-on-background: #191c20; 48 | --pst-color-surface: #101418; 49 | --pst-color-on-surface: #272a2f; 50 | } 51 | -------------------------------------------------------------------------------- /docs/_static/images/user_guide/boxes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/ignis/8a94285fd9234fe457796440f806b7556b59e31f/docs/_static/images/user_guide/boxes.png -------------------------------------------------------------------------------- /docs/_static/images/user_guide/hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/ignis/8a94285fd9234fe457796440f806b7556b59e31f/docs/_static/images/user_guide/hello_world.png -------------------------------------------------------------------------------- /docs/_static/switcher.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "dev", 4 | "url": "https://ignis-sh.github.io/ignis/latest/" 5 | }, 6 | { 7 | "name": "v0.5.1 (stable)", 8 | "version": "v0.5.1", 9 | "url": "https://ignis-sh.github.io/ignis/stable/", 10 | "preferred": true 11 | }, 12 | { 13 | "name": "v0.5", 14 | "version": "v0.5", 15 | "url": "https://ignis-sh.github.io/ignis/v0.5/" 16 | }, 17 | { 18 | "name": "v0.4", 19 | "version": "v0.4", 20 | "url": "https://ignis-sh.github.io/ignis/v0.4/" 21 | }, 22 | { 23 | "version": "v0.3", 24 | "url": "https://ignis-sh.github.io/ignis/v0.3/" 25 | }, 26 | { 27 | "version": "v0.2", 28 | "url": "https://ignis-sh.github.io/ignis/v0.2/" 29 | } 30 | ] -------------------------------------------------------------------------------- /docs/api/app.rst: -------------------------------------------------------------------------------- 1 | Application 2 | ------------- 3 | 4 | .. autoclass:: ignis.app.IgnisApp 5 | :members: -------------------------------------------------------------------------------- /docs/api/base_service.rst: -------------------------------------------------------------------------------- 1 | BaseService 2 | ============= 3 | 4 | .. autoclass:: ignis.base_service.BaseService 5 | :members: -------------------------------------------------------------------------------- /docs/api/base_widget.rst: -------------------------------------------------------------------------------- 1 | BaseWidget 2 | ============ 3 | 4 | .. autoclass:: ignis.base_widget.BaseWidget 5 | :members: -------------------------------------------------------------------------------- /docs/api/client.rst: -------------------------------------------------------------------------------- 1 | Client 2 | ------------- 3 | 4 | .. autoclass:: ignis.client.IgnisClient 5 | :members: -------------------------------------------------------------------------------- /docs/api/connection_manager.rst: -------------------------------------------------------------------------------- 1 | Connection Manager 2 | ================== 3 | 4 | .. autoclass:: ignis.connection_manager.ConnectionManager 5 | :members: 6 | 7 | .. autoclass:: ignis.connection_manager.DBusConnectionManager 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/api/dbus.rst: -------------------------------------------------------------------------------- 1 | D-Bus 2 | ============ 3 | 4 | There are several classes that can help you interact with D-Bus. 5 | 6 | .. autoclass:: ignis.dbus.DBusService 7 | :members: 8 | 9 | .. autoclass:: ignis.dbus.DBusProxy 10 | :members: 11 | -------------------------------------------------------------------------------- /docs/api/dbus_menu.rst: -------------------------------------------------------------------------------- 1 | D-Bus menu 2 | ============ 3 | 4 | .. autoclass:: ignis.dbus_menu.DBusMenu 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/deprecation.rst: -------------------------------------------------------------------------------- 1 | Deprecation 2 | =========== 3 | 4 | .. automodule:: ignis.deprecation 5 | :members: -------------------------------------------------------------------------------- /docs/api/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | =============== 3 | 4 | Here is a list of Ignis exceptions. They may be raised by various components in the case of an error. 5 | 6 | To access them, use the ``ignis.exceptions`` module. 7 | 8 | .. code-block:: python 9 | 10 | from ignis.exceptions import SomeException 11 | 12 | .. automodule:: ignis.exceptions 13 | :members: -------------------------------------------------------------------------------- /docs/api/gobject.rst: -------------------------------------------------------------------------------- 1 | GObject 2 | ------------- 3 | 4 | .. autoclass:: ignis.gobject.IgnisGObject 5 | :members: 6 | 7 | .. autoclass:: ignis.gobject.Binding 8 | :members: 9 | 10 | .. autoclass:: ignis.gobject.IgnisProperty 11 | :members: 12 | 13 | .. autoclass:: ignis.gobject.IgnisSignal 14 | :members: 15 | 16 | .. autoclass:: ignis.gobject.DataGObject 17 | :members: -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | API reference 2 | ================= 3 | 4 | This reference manual details functions, modules, and objects included in Ignis, describing what they are and what they do. 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | toplevel 10 | app 11 | gobject 12 | variable 13 | client 14 | options 15 | options_manager 16 | exceptions 17 | dbus 18 | dbus_menu 19 | base_widget 20 | base_service 21 | deprecation 22 | connection_manager 23 | menu_model 24 | widgets/index 25 | services/index 26 | utils/index 27 | -------------------------------------------------------------------------------- /docs/api/menu_model.rst: -------------------------------------------------------------------------------- 1 | Menu Model 2 | ========== 3 | 4 | .. autoclass:: ignis.menu_model.IgnisMenuModel 5 | :members: 6 | 7 | .. autoclass:: ignis.menu_model.IgnisMenuItem 8 | :members: 9 | 10 | .. autoclass:: ignis.menu_model.IgnisMenuSeparator 11 | :members: -------------------------------------------------------------------------------- /docs/api/options.rst: -------------------------------------------------------------------------------- 1 | Options 2 | ========== 3 | 4 | .. autoclass:: ignis.options.Options 5 | :members: -------------------------------------------------------------------------------- /docs/api/options_manager.rst: -------------------------------------------------------------------------------- 1 | Options Manager 2 | =============== 3 | 4 | .. autoclass:: ignis.options_manager.OptionsManager 5 | :members: 6 | 7 | .. autoclass:: ignis.options_manager.OptionsGroup 8 | :members: 9 | 10 | .. autoclass:: ignis.options_manager.TrackedList 11 | :members: -------------------------------------------------------------------------------- /docs/api/services/applications.rst: -------------------------------------------------------------------------------- 1 | Applications 2 | ================= 3 | 4 | .. autoclass:: ignis.services.applications.ApplicationsService 5 | :members: 6 | 7 | .. autoclass:: ignis.services.applications.Application 8 | :members: 9 | 10 | .. autoclass:: ignis.services.applications.ApplicationAction 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/api/services/audio.rst: -------------------------------------------------------------------------------- 1 | Audio 2 | =========== 3 | 4 | .. autoclass:: ignis.services.audio.AudioService 5 | :members: 6 | 7 | .. autoclass:: ignis.services.audio.Stream 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/api/services/backlight.rst: -------------------------------------------------------------------------------- 1 | Backlight 2 | ============== 3 | 4 | .. autoclass:: ignis.services.backlight.BacklightService 5 | :members: 6 | 7 | .. autoclass:: ignis.services.backlight.BacklightDevice 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/api/services/bluetooth.rst: -------------------------------------------------------------------------------- 1 | Bluetooth 2 | ========== 3 | 4 | .. autoclass:: ignis.services.bluetooth.BluetoothService 5 | :members: 6 | 7 | .. autoclass:: ignis.services.bluetooth.BluetoothDevice 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/api/services/fetch.rst: -------------------------------------------------------------------------------- 1 | Fetch 2 | =========== 3 | 4 | .. autoclass:: ignis.services.fetch.FetchService 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/services/hyprland.rst: -------------------------------------------------------------------------------- 1 | Hyprland 2 | ================= 3 | 4 | .. autoclass:: ignis.services.hyprland.HyprlandService 5 | :members: 6 | 7 | .. autoclass:: ignis.services.hyprland.HyprlandWorkspace 8 | :members: 9 | 10 | .. autoclass:: ignis.services.hyprland.HyprlandWindow 11 | :members: 12 | 13 | .. autoclass:: ignis.services.hyprland.HyprlandKeyboard 14 | :members: 15 | 16 | .. autoclass:: ignis.services.hyprland.HyprlandMonitor 17 | :members: 18 | -------------------------------------------------------------------------------- /docs/api/services/index.rst: -------------------------------------------------------------------------------- 1 | Services 2 | ========== 3 | There is a list of built-in services that provide additional functionality to build various components of your desktop. 4 | 5 | To access a service, import it and call the ``.get_default()`` method. 6 | 7 | .. code-block:: python 8 | 9 | from ignis.services.audio import AudioService 10 | 11 | audio = AudioService.get_default() 12 | 13 | Built-in services 14 | ----------------- 15 | 16 | .. hint:: 17 | If the service you need is not here, you can make your own. 18 | 19 | See `Creating Service <../../dev/creating_service.html>`_ for more info. 20 | 21 | .. toctree:: 22 | :glob: 23 | :maxdepth: 1 24 | 25 | ./* 26 | -------------------------------------------------------------------------------- /docs/api/services/mpris.rst: -------------------------------------------------------------------------------- 1 | MPRIS (media) 2 | ================= 3 | 4 | .. autoclass:: ignis.services.mpris.MprisService 5 | :members: 6 | 7 | .. autoclass:: ignis.services.mpris.MprisPlayer 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/api/services/network.rst: -------------------------------------------------------------------------------- 1 | Network 2 | ================= 3 | 4 | .. autoclass:: ignis.services.network.NetworkService 5 | :members: 6 | 7 | .. autoclass:: ignis.services.network.Wifi 8 | :members: 9 | 10 | .. autoclass:: ignis.services.network.Ethernet 11 | :members: 12 | 13 | .. autoclass:: ignis.services.network.Vpn 14 | :members: 15 | 16 | .. autoclass:: ignis.services.network.WifiDevice 17 | :members: 18 | 19 | .. autoclass:: ignis.services.network.EthernetDevice 20 | :members: 21 | 22 | .. autoclass:: ignis.services.network.WifiAccessPoint 23 | :members: 24 | 25 | .. autoclass:: ignis.services.network.VpnConnection 26 | :members: 27 | -------------------------------------------------------------------------------- /docs/api/services/niri.rst: -------------------------------------------------------------------------------- 1 | Niri 2 | ================= 3 | 4 | .. autoclass:: ignis.services.niri.NiriService 5 | :members: 6 | 7 | .. autoclass:: ignis.services.niri.NiriKeyboardLayouts 8 | :members: 9 | 10 | .. autoclass:: ignis.services.niri.NiriWindow 11 | :members: 12 | 13 | .. autoclass:: ignis.services.niri.NiriWorkspace 14 | :members: 15 | -------------------------------------------------------------------------------- /docs/api/services/notifications.rst: -------------------------------------------------------------------------------- 1 | Notifications 2 | ================= 3 | 4 | .. autoclass:: ignis.services.notifications.NotificationService 5 | :members: 6 | 7 | .. autoclass:: ignis.services.notifications.Notification 8 | :members: 9 | 10 | .. autoclass:: ignis.services.notifications.NotificationAction 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/api/services/recorder.rst: -------------------------------------------------------------------------------- 1 | Recorder 2 | ================= 3 | 4 | .. autoclass:: ignis.services.recorder.RecorderService 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/services/system_tray.rst: -------------------------------------------------------------------------------- 1 | System Tray 2 | ================= 3 | 4 | .. autoclass:: ignis.services.system_tray.SystemTrayService 5 | :members: 6 | 7 | .. autoclass:: ignis.services.system_tray.SystemTrayItem 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/api/services/systemd.rst: -------------------------------------------------------------------------------- 1 | Systemd 2 | ================= 3 | 4 | .. autoclass:: ignis.services.systemd.SystemdService 5 | :members: 6 | 7 | .. autoclass:: ignis.services.systemd.SystemdUnit 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/api/services/upower.rst: -------------------------------------------------------------------------------- 1 | UPower 2 | ======== 3 | 4 | .. autoclass:: ignis.services.upower.UPowerService 5 | :members: 6 | 7 | .. autoclass:: ignis.services.upower.UPowerDevice 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/api/services/wallpaper.rst: -------------------------------------------------------------------------------- 1 | Wallpaper 2 | ================= 3 | 4 | .. autoclass:: ignis.services.wallpaper.WallpaperService 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/toplevel.rst: -------------------------------------------------------------------------------- 1 | Toplevel package 2 | ================ 3 | 4 | This is the documentation for the toplevel ``ignis`` package. 5 | 6 | .. automodule:: ignis 7 | :members: -------------------------------------------------------------------------------- /docs/api/utils/FileMonitor.rst: -------------------------------------------------------------------------------- 1 | FileMonitor 2 | =========== 3 | 4 | .. autoclass:: ignis.utils.Utils.FileMonitor 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/utils/Poll.rst: -------------------------------------------------------------------------------- 1 | Poll 2 | ==== 3 | 4 | .. autoclass:: ignis.utils.Utils.Poll 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/utils/Timeout.rst: -------------------------------------------------------------------------------- 1 | Timeout 2 | ======= 3 | 4 | .. autoclass:: ignis.utils.Utils.Timeout 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/utils/debounce.rst: -------------------------------------------------------------------------------- 1 | Debounce 2 | ======== 3 | 4 | .. autoclass:: ignis.utils.Utils.DebounceTask 5 | 6 | .. autofunction:: ignis.utils.Utils.debounce 7 | -------------------------------------------------------------------------------- /docs/api/utils/file.rst: -------------------------------------------------------------------------------- 1 | File 2 | ==== 3 | 4 | .. autofunction:: ignis.utils.Utils.read_file 5 | 6 | .. autofunction:: ignis.utils.Utils.read_file_async 7 | 8 | .. autofunction:: ignis.utils.Utils.write_file 9 | 10 | .. autofunction:: ignis.utils.Utils.write_file_async -------------------------------------------------------------------------------- /docs/api/utils/icon.rst: -------------------------------------------------------------------------------- 1 | Icon 2 | ==== 3 | 4 | .. autofunction:: ignis.utils.Utils.get_file_icon_name 5 | 6 | .. autofunction:: ignis.utils.Utils.get_paintable 7 | 8 | .. autofunction:: ignis.utils.Utils.get_app_icon_name 9 | -------------------------------------------------------------------------------- /docs/api/utils/index.rst: -------------------------------------------------------------------------------- 1 | Utils 2 | ===== 3 | 4 | There is a list of utilities—useful tools that can help you. 5 | 6 | Use the universal ``Utils`` class to access them: 7 | 8 | .. code-block:: python 9 | 10 | from ignis.utils import Utils 11 | 12 | Utils.NAME() 13 | 14 | .. toctree:: 15 | :glob: 16 | :maxdepth: 1 17 | 18 | ./* -------------------------------------------------------------------------------- /docs/api/utils/misc.rst: -------------------------------------------------------------------------------- 1 | Misc 2 | ==== 3 | 4 | .. autofunction:: ignis.utils.Utils.get_current_dir 5 | 6 | .. autofunction:: ignis.utils.Utils.load_interface_xml -------------------------------------------------------------------------------- /docs/api/utils/monitor.rst: -------------------------------------------------------------------------------- 1 | Monitor 2 | ======= 3 | 4 | .. autofunction:: ignis.utils.Utils.get_monitor 5 | 6 | .. autofunction:: ignis.utils.Utils.get_monitors 7 | 8 | .. autofunction:: ignis.utils.Utils.get_n_monitors -------------------------------------------------------------------------------- /docs/api/utils/pixbuf.rst: -------------------------------------------------------------------------------- 1 | Pixbuf 2 | ====== 3 | 4 | .. autofunction:: ignis.utils.Utils.crop_pixbuf 5 | 6 | .. autofunction:: ignis.utils.Utils.scale_pixbuf -------------------------------------------------------------------------------- /docs/api/utils/sass.rst: -------------------------------------------------------------------------------- 1 | Sass 2 | ==== 3 | 4 | .. autofunction:: ignis.utils.Utils.sass_compile 5 | -------------------------------------------------------------------------------- /docs/api/utils/sh.rst: -------------------------------------------------------------------------------- 1 | Shell Commands 2 | ============== 3 | 4 | .. autofunction:: ignis.utils.Utils.exec_sh 5 | 6 | .. autofunction:: ignis.utils.Utils.exec_sh_async 7 | 8 | .. autoclass:: ignis.utils.Utils.AsyncCompletedProcess 9 | :members: -------------------------------------------------------------------------------- /docs/api/utils/socket.rst: -------------------------------------------------------------------------------- 1 | Socket 2 | ====== 3 | 4 | .. autofunction:: ignis.utils.Utils.listen_socket 5 | 6 | .. autofunction:: ignis.utils.Utils.send_socket 7 | -------------------------------------------------------------------------------- /docs/api/utils/str_cases.rst: -------------------------------------------------------------------------------- 1 | String Cases 2 | ============ 3 | 4 | .. autofunction:: ignis.utils.Utils.snake_to_pascal 5 | 6 | .. autofunction:: ignis.utils.Utils.pascal_to_snake -------------------------------------------------------------------------------- /docs/api/utils/thread.rst: -------------------------------------------------------------------------------- 1 | Thread 2 | ====== 3 | 4 | .. autofunction:: ignis.utils.Utils.thread 5 | 6 | .. autofunction:: ignis.utils.Utils.run_in_thread 7 | 8 | .. autoclass:: ignis.utils.Utils.ThreadTask 9 | :members: -------------------------------------------------------------------------------- /docs/api/utils/version.rst: -------------------------------------------------------------------------------- 1 | Version 2 | ======= 3 | 4 | .. autofunction:: ignis.utils.Utils.get_ignis_version 5 | 6 | .. autofunction:: ignis.utils.Utils.get_ignis_commit 7 | 8 | .. autofunction:: ignis.utils.Utils.get_ignis_branch 9 | 10 | .. autofunction:: ignis.utils.Utils.get_ignis_commit_msg -------------------------------------------------------------------------------- /docs/api/variable.rst: -------------------------------------------------------------------------------- 1 | Variable 2 | ========= 3 | 4 | .. autoclass:: ignis.variable.Variable 5 | :members: -------------------------------------------------------------------------------- /docs/api/widgets/Arrow.rst: -------------------------------------------------------------------------------- 1 | Arrow 2 | ----- 3 | 4 | .. autoclass:: ignis.widgets.Widget.Arrow 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/ArrowButton.rst: -------------------------------------------------------------------------------- 1 | ArrowButton 2 | ----------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.ArrowButton 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/Box.rst: -------------------------------------------------------------------------------- 1 | Box 2 | --- 3 | 4 | .. autoclass:: ignis.widgets.Widget.Box 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/Button.rst: -------------------------------------------------------------------------------- 1 | Button 2 | ------ 3 | 4 | .. autoclass:: ignis.widgets.Widget.Button 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/Calendar.rst: -------------------------------------------------------------------------------- 1 | Calendar 2 | -------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.Calendar 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/CenterBox.rst: -------------------------------------------------------------------------------- 1 | CenterBox 2 | --------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.CenterBox 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/CheckButton.rst: -------------------------------------------------------------------------------- 1 | CheckButton 2 | ----------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.CheckButton 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/DropDown.rst: -------------------------------------------------------------------------------- 1 | DropDown 2 | -------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.DropDown 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/Entry.rst: -------------------------------------------------------------------------------- 1 | Entry 2 | ----- 3 | 4 | .. autoclass:: ignis.widgets.Widget.Entry 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/EventBox.rst: -------------------------------------------------------------------------------- 1 | EventBox 2 | -------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.EventBox 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/FileChooserButton.rst: -------------------------------------------------------------------------------- 1 | FileChooserButton 2 | ----------------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.FileChooserButton 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/FileDialog.rst: -------------------------------------------------------------------------------- 1 | FileDialog 2 | ---------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.FileDialog 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/FileFilter.rst: -------------------------------------------------------------------------------- 1 | FileFilter 2 | ---------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.FileFilter 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/Grid.rst: -------------------------------------------------------------------------------- 1 | Grid 2 | ---- 3 | 4 | .. autoclass:: ignis.widgets.Widget.Grid 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/HeaderBar.rst: -------------------------------------------------------------------------------- 1 | HeaderBar 2 | --------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.HeaderBar 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/Icon.rst: -------------------------------------------------------------------------------- 1 | Icon 2 | ---- 3 | 4 | .. autoclass:: ignis.widgets.Widget.Icon 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/Label.rst: -------------------------------------------------------------------------------- 1 | Label 2 | ----- 3 | 4 | .. autoclass:: ignis.widgets.Widget.Label 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/ListBox.rst: -------------------------------------------------------------------------------- 1 | ListBox 2 | ------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.ListBox 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/ListBoxRow.rst: -------------------------------------------------------------------------------- 1 | ListBoxRow 2 | ---------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.ListBoxRow 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/Overlay.rst: -------------------------------------------------------------------------------- 1 | Overlay 2 | ------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.Overlay 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/Picture.rst: -------------------------------------------------------------------------------- 1 | Picture 2 | ------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.Picture 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/PopoverMenu.rst: -------------------------------------------------------------------------------- 1 | PopoverMenu 2 | ----------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.PopoverMenu 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/RegularWindow.rst: -------------------------------------------------------------------------------- 1 | RegularWindow 2 | ------------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.RegularWindow 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/Revealer.rst: -------------------------------------------------------------------------------- 1 | Revealer 2 | -------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.Revealer 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/RevealerWindow.rst: -------------------------------------------------------------------------------- 1 | RevealerWindow 2 | -------------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.RevealerWindow 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/Scale.rst: -------------------------------------------------------------------------------- 1 | Scale 2 | ----- 3 | 4 | .. autoclass:: ignis.widgets.Widget.Scale 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/Scroll.rst: -------------------------------------------------------------------------------- 1 | Scroll 2 | ------ 3 | 4 | .. autoclass:: ignis.widgets.Widget.Scroll 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/Separator.rst: -------------------------------------------------------------------------------- 1 | Separator 2 | --------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.Separator 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/SpinButton.rst: -------------------------------------------------------------------------------- 1 | SpinButton 2 | ---------- 3 | 4 | .. autoclass:: ignis.widgets.Widget.SpinButton 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/Stack.rst: -------------------------------------------------------------------------------- 1 | Stack 2 | ======= 3 | 4 | .. autoclass:: ignis.widgets.Widget.Stack 5 | :members: 6 | 7 | .. autoclass:: ignis.widgets.Widget.StackPage 8 | :members: 9 | 10 | .. autoclass:: ignis.widgets.Widget.StackSwitcher 11 | :members: -------------------------------------------------------------------------------- /docs/api/widgets/Switch.rst: -------------------------------------------------------------------------------- 1 | Switch 2 | ------ 3 | 4 | .. autoclass:: ignis.widgets.Widget.Switch 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/ToggleButton.rst: -------------------------------------------------------------------------------- 1 | ToggleButton 2 | ------------ 3 | 4 | .. autoclass:: ignis.widgets.Widget.ToggleButton 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/widgets/Window.rst: -------------------------------------------------------------------------------- 1 | Window 2 | ------ 3 | 4 | .. autoclass:: ignis.widgets.Widget.Window 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/dev/code_style.rst: -------------------------------------------------------------------------------- 1 | Code Style Guidelines 2 | ======================== 3 | 4 | Code Formatting 5 | ------------------- 6 | 7 | All code must be formatted using `Ruff `_. 8 | 9 | Type checking 10 | ----------------- 11 | 12 | The use of type hints is encouraged, 13 | and all code must be checked using `mypy `_. 14 | 15 | Commit messages 16 | ---------------- 17 | 18 | Commit messages should follow the `Conventional Commits `_. 19 | 20 | Versioning 21 | ---------------- 22 | 23 | Ignis follows `Semantic Versioning `_. 24 | 25 | Definitions and Naming 26 | ----------------------- 27 | 28 | Use the following naming conventions for different purposes: 29 | 30 | - ``snake_case``: Functions, variables, GObject properties (except D-Bus methods/properties) 31 | - ``SCREAMING_SNAKE_CASE``: Constants 32 | - ``PascalCase``: Classes, D-Bus methods/properties 33 | - ``kebab-case``: GObject signals -------------------------------------------------------------------------------- /docs/dev/env.rst: -------------------------------------------------------------------------------- 1 | Setting up a Development Environment 2 | ===================================== 3 | 4 | This guide with walk you through process of setting up a Development Environment for working on Ignis. 5 | 6 | Source 7 | ------ 8 | 9 | Firstly, you have to grab the Ignis sources: 10 | 11 | .. code-block:: bash 12 | 13 | # replace with the actual URL of your fork (if needed) 14 | git clone https://github.com/ignis-sh/ignis.git 15 | cd ignis 16 | 17 | Virtual Environment 18 | ------------------- 19 | 20 | It's always a good practice to work within a Python virtual environment. 21 | 22 | .. code-block:: bash 23 | 24 | python -m venv venv 25 | source venv/bin/activate # for fish: . venv/bin/activate.fish 26 | 27 | Editable install 28 | ---------------- 29 | 30 | Ignis is build with Meson and meson-python. 31 | In order to support editable installs, Meson-python, Meson, and Ninja should be installed in the virtual environment. 32 | 33 | .. code-block:: python 34 | 35 | pip install meson-python meson ninja 36 | 37 | Now, install Ignis in the local virtual environment with the ``--no-build-isolation`` and ``-e`` options for an editable install. 38 | 39 | .. code-block:: bash 40 | 41 | pip install --no-build-isolation -e . 42 | 43 | Additionally, you can install useful development tools by running: 44 | 45 | .. code-block:: bash 46 | 47 | pip install -r dev.txt 48 | 49 | Done! 50 | 51 | You can now edit the ``ignis`` directory at the root of the repository, 52 | and the changes will be applied without the need to reinstall Ignis. 53 | -------------------------------------------------------------------------------- /docs/dev/gobject.rst: -------------------------------------------------------------------------------- 1 | GObject 2 | ============ 3 | 4 | All GObjects in Ignis should inherit from the :class:`~ignis.gobject.IgnisGObject` class, 5 | which provides additional functionality and thread-safe signal operations. 6 | 7 | All other stuff (properties, signals) follow the standard PyGObject way. 8 | Use :class:`~ignis.gobject.IgnisProperty` and :class:`~ignis.gobject.IgnisSignal` decorators to define properties and signals respectively. 9 | -------------------------------------------------------------------------------- /docs/dev/index.rst: -------------------------------------------------------------------------------- 1 | Developer Guide 2 | ================== 3 | 4 | .. warning:: 5 | If you plan to open a Pull Request to the Ignis repository, 6 | please follow the `latest `_ version of the developer documentation. 7 | 8 | Ignis is a modular and extensible framework that allows you to easily make modifications to the source code. 9 | 10 | This guide will help you with this. 11 | 12 | .. hint:: 13 | Knowledge of GTK and GObject will be very helpful for hacking. 14 | Here are some useful resources to learn more about GTK, GObject, and PyGObject: 15 | 16 | - `PyGObject Homepage `_ 17 | - `PyGObject User Guide `_ 18 | - `GObject Guide `_ 19 | - `PyGObject API Reference `_ 20 | 21 | .. toctree:: 22 | :maxdepth: 1 23 | 24 | env 25 | gobject 26 | subclassing_widgets 27 | creating_service 28 | documentation 29 | code_style 30 | -------------------------------------------------------------------------------- /docs/dev/subclassing_widgets.rst: -------------------------------------------------------------------------------- 1 | Subclassing Widgets 2 | ======================= 3 | 4 | All widgets should inherit from the :class:`~ignis.base_widget.BaseWidget` class. 5 | 6 | Widget Class Template 7 | ----------------------- 8 | 9 | Here is the template for the widget that you want to override. 10 | Replace ``WIDGET_NAME`` with the actual name of the widget. 11 | 12 | .. code-block:: python 13 | 14 | from gi.repository import Gtk 15 | from ignis.base_widget import BaseWidget 16 | from ignis.gobject import IgnisProperty 17 | 18 | 19 | class WIDGET_NAME(Gtk.WIDGET_NAME, BaseWidget): 20 | __gtype_name__ = "IgnisWIDGET_NAME" 21 | __gproperties__ = {**BaseWidget.gproperties} # this need to inherit properties from BaseWidget 22 | 23 | def __init__(self, **kwargs): # accept keyword arguments 24 | Gtk.Label.__init__(self) 25 | # if you want to override enums, do it BEFORE BaseWidget.__init__(self, **kwargs) 26 | # otherwise, the property will be set before it is overridden. 27 | self.override_enum("SOME_PROPERTY", SOME_ENUM) 28 | self._custom_property = None # define protected/private variables for your custom properties BEFORE BaseWidget.__init__(self, **kwargs) 29 | BaseWidget.__init__(self, **kwargs) # this sets all properties transferred to kwargs 30 | 31 | @IgnisProperty 32 | def custom_property(self) -> bool: 33 | return self._custom_property 34 | 35 | @custom_property.setter 36 | def custom_property(self, value: bool) -> None: 37 | self._custom_property = value 38 | -------------------------------------------------------------------------------- /docs/examples/configurations.rst: -------------------------------------------------------------------------------- 1 | Configurations 2 | ================= 3 | 4 | This page contains some configurations that you can use as an example. 5 | 6 | `A simple bar `_ 7 | --------------------------------------------------------------------------------- 8 | .. image:: https://github.com/ignis-sh/ignis/blob/main/examples/bar/simple-bar.png?raw=true 9 | 10 | `My own configuration `_ 11 | --------------------------------------------------------------------------------- 12 | .. image:: https://github.com/linkfrg/dotfiles/blob/main/assets/1.png?raw=true -------------------------------------------------------------------------------- /docs/examples/index.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ============= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | configurations 8 | code_snippets 9 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Ignis 2 | ========= 3 | 4 | **Useful links**: 5 | `Installation `__ | 6 | `Source Repository `__ | 7 | `Issue Tracker `__ 8 | 9 | A widget framework for building desktop shells, written and configurable in Python. 10 | 11 | .. grid:: 1 1 2 2 12 | :gutter: 2 3 4 4 13 | 14 | .. grid-item-card:: 15 | :text-align: left 16 | 17 | :material-regular:`widgets;1.5em` Widgets 18 | ^^^ 19 | 20 | See a list of built-in `Widgets `_ 21 | 22 | .. grid-item-card:: 23 | :text-align: left 24 | 25 | :material-regular:`design_services;1.5em` Services 26 | ^^^ 27 | 28 | See a list of built-in `Services `_ 29 | 30 | .. grid-item-card:: 31 | :text-align: left 32 | 33 | :material-regular:`settings;1.5em` Utils 34 | ^^^ 35 | 36 | See a list of built-in `Utils `_ 37 | 38 | .. grid-item-card:: 39 | :text-align: left 40 | 41 | :material-regular:`menu_book;1.5em` Examples 42 | ^^^ 43 | 44 | See examples `Examples `_ 45 | 46 | .. toctree:: 47 | :maxdepth: 1 48 | :hidden: 49 | 50 | user/index 51 | api/index 52 | dev/index 53 | examples/index 54 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==8.1.0 2 | pydata-sphinx-theme>=0.15.4 3 | sphinx_design>=0.6.1 4 | sphinx_copybutton>=0.5.2 5 | sphinx-autodoc-typehints>=2.5.0 6 | sphinxcontrib-typer>=0.5.1 -------------------------------------------------------------------------------- /docs/user/async.rst: -------------------------------------------------------------------------------- 1 | Asynchronous Programming 2 | ======================== 3 | 4 | Some I/O-bound and high-latency tasks can block the main thread and make your application unresponsive. 5 | To work around this, it's better to run these tasks independently of the main program flow. 6 | Asynchronous programming allows you to wait until a task is finished in the background while keeping your application responsive. 7 | 8 | Ignis (and PyGObject in general) integrates with Python's :mod:`asyncio` module and provides convenient ``async/await`` syntax support. 9 | 10 | Asynchronous functions in the Ignis documentation are prefixed with the ``async`` word before their name. 11 | 12 | Calling Asynchronous Functions 13 | ------------------------------- 14 | 15 | There are two common cases. 16 | 17 | 1. From a synchronous function 18 | 19 | Use :func:`asyncio.create_task` to schedule execution. 20 | 21 | .. code-block:: python 22 | 23 | import asyncio 24 | from ignis.utils import Utils 25 | 26 | asyncio.create_task(Utils.exec_sh_async("notify-send 'asynchrony!'")) 27 | 28 | 2. From another asynchronous function 29 | 30 | Use the ``await`` keyword to wait for execution. 31 | 32 | .. code-block:: python 33 | 34 | import asyncio 35 | from ignis.utils import Utils 36 | 37 | async def some_func() -> None: 38 | await Utils.exec_sh_async("notify-send 'asynchrony!'") 39 | 40 | # you still need create_task() because some_func is async 41 | asyncio.create_task(some_func()) 42 | 43 | 44 | Choosing the right approach 45 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 46 | 47 | The best approach depends on your needs: 48 | 49 | - If you need to retrieve the function's output and/or control execution order, define a custom async function and use await. 50 | - Otherwise, if execution timing is not critical, use :func:`asyncio.create_task` to run the task in the background. 51 | 52 | Cancelling tasks 53 | ---------------- 54 | 55 | Since :func:`asyncio.create_task` schedules execution for the future, you can cancel a task if needed. 56 | 57 | .. code-block:: python 58 | 59 | task = asyncio.create_task(some_func()) 60 | # cancel task 61 | task.cancel() 62 | 63 | .. seealso:: 64 | The following resources may be useful to you: 65 | 66 | - `PyGObject Asynchronous Guide `_ 67 | - :mod:`asyncio` documentation -------------------------------------------------------------------------------- /docs/user/cli.rst: -------------------------------------------------------------------------------- 1 | CLI 2 | ============== 3 | 4 | Run ``ignis --help`` to view the CLI usage help message. 5 | 6 | .. typer:: ignis.cli.cli_app 7 | :prog: ignis 8 | :width: 80 9 | :preferred: svg 10 | :show-nested: 11 | :make-sections: 12 | :theme: monokai -------------------------------------------------------------------------------- /docs/user/expanding_functionality.rst: -------------------------------------------------------------------------------- 1 | Expanding functionality 2 | ======================== 3 | 4 | Most of the features you need should be available in the `API Reference <../api/index.html>`_. 5 | However, if the feature you are looking for is not there, this page may help you. 6 | 7 | Since Ignis uses Python, you can take advantage of its many benefits, features, and modules to expand functionality, including the use of native PyGObject. 8 | 9 | For a more detailed understanding of how Ignis works and to help you add extra features, it is highly recommended to read the following resources: 10 | 11 | - `PyGObject User Guide `_ 12 | - `GObject Guide `_ 13 | - `PyGObject API Reference `_ 14 | 15 | .. hint:: 16 | The `Developer Guide <../dev/index.html>`_ will also be very helpful. 17 | -------------------------------------------------------------------------------- /docs/user/faq.rst: -------------------------------------------------------------------------------- 1 | FAQ 2 | ======== 3 | 4 | Custom SVG icons 5 | ------------------ 6 | 7 | See :func:`~ignis.app.IgnisApp.add_icons`. -------------------------------------------------------------------------------- /docs/user/index.rst: -------------------------------------------------------------------------------- 1 | User guide 2 | ==================== 3 | 4 | This guide is an overview and explains the basics. 5 | 6 | Widgets, services, utils and more details can be found in `API reference <../api/index.html>`_. 7 | 8 | Get started 9 | --------------- 10 | 11 | .. toctree:: 12 | :maxdepth: 1 13 | 14 | installation 15 | first_widgets 16 | using_classes 17 | dynamic_content 18 | styling 19 | async 20 | cli 21 | options 22 | expanding_functionality 23 | nix 24 | troubleshooting 25 | faq 26 | -------------------------------------------------------------------------------- /docs/user/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Arch Linux 5 | ----------- 6 | 7 | Install the package from AUR. 8 | 9 | .. code-block:: bash 10 | 11 | paru -S python-ignis 12 | 13 | For the latest (git) version of Ignis install ``python-ignis-git``. 14 | 15 | .. code-block:: bash 16 | 17 | paru -S python-ignis-git 18 | 19 | Nix 20 | --- 21 | 22 | Read more on the `Nix page `_. 23 | 24 | Void Linux 25 | ---------- 26 | 27 | An ``xbps-src`` template is available in a `third party repository `_ 28 | with `prebuilt packages `_ too. 29 | 30 | Add this repository by creating ``ignis.conf`` at ``/etc/xbps.d/``: 31 | 32 | .. tab-set:: 33 | 34 | .. tab-item:: /etc/xbps.d/ignis.conf 35 | 36 | .. code-block:: 37 | 38 | repository=https://raw.githubusercontent.com/binarylinuxx/ignis-xbps-src/x86_64-glibc/pkgs 39 | 40 | Then, you can install Ignis as a usual package using ``xbps-install``. 41 | 42 | .. code-block:: bash 43 | 44 | sudo xbps-install -S ignis 45 | 46 | Pip 47 | ---- 48 | 49 | Pip is the standard package manager for Python. 50 | You can install Ignis directly from the Git repository using Pip. 51 | 52 | .. hint:: 53 | 54 | You can do this in a Python virtual environment. 55 | Create and activate one with the following commands: 56 | 57 | .. code-block:: bash 58 | 59 | python -m venv venv 60 | source venv/bin/activate # for fish: . venv/bin/activate.fish 61 | 62 | To install the latest (Git) version of Ignis: 63 | 64 | .. code-block:: bash 65 | 66 | pip install git+https://github.com/ignis-sh/ignis.git 67 | 68 | To install a specific version (e.g., ``v0.5``): 69 | 70 | .. code-block:: bash 71 | 72 | # replace "TAG" with the desired Git tag 73 | pip install git+https://github.com/ignis-sh/ignis.git@TAG 74 | 75 | .. seealso:: 76 | 77 | For advanced usage, you can `set up a development environment <../dev/env.html>`_ and install Ignis in editable mode. 78 | This allows you to easily switch between commits, versions, branches, or pull requests using `git`, without having to reinstall Ignis. 79 | 80 | Building from source 81 | --------------------- 82 | 83 | **Dependencies:** 84 | 85 | - ninja 86 | - meson 87 | - gtk4 88 | - gtk4-layer-shell 89 | - glib-mkenums (glib2-devel) 90 | - pygobject >= 3.50.0 91 | - pycairo 92 | - python-typer 93 | - python-loguru 94 | - libpulse (if using PipeWire, install ``pipewire-pulse``) 95 | 96 | .. code-block:: bash 97 | 98 | git clone https://github.com/ignis-sh/ignis.git 99 | cd ignis 100 | meson setup build --prefix=/usr 101 | meson compile -C build 102 | meson install -C build 103 | 104 | 105 | Running 106 | -------- 107 | 108 | .. code-block:: bash 109 | 110 | ignis init 111 | -------------------------------------------------------------------------------- /docs/user/options.rst: -------------------------------------------------------------------------------- 1 | Options 2 | =========== 3 | 4 | Some services provide `options` - user-specific settings that allow you to customize their behavior to suit your needs. 5 | 6 | For a complete list of available options, refer to :class:`~ignis.options.Options`. 7 | There is also detailed documentation about using them. 8 | 9 | 10 | User Options 11 | --------------- 12 | 13 | You can define your own options! 14 | 15 | Just follow the structure described in :class:`~ignis.options.OptionsManager`. -------------------------------------------------------------------------------- /docs/user/styling.rst: -------------------------------------------------------------------------------- 1 | Styling 2 | ======== 3 | Ignis supports both CSS and SCSS. 4 | 5 | Using CSS/SCSS file 6 | -------------------- 7 | 8 | To get started, add the following to your config: 9 | 10 | .. code-block:: python 11 | 12 | from ignis.app import IgnisApp 13 | 14 | app = IgnisApp.get_default() 15 | 16 | app.apply_css("PATH/TO/CSS_FILE") 17 | 18 | For example, we will use ``~/.config/ignis/style.scss``: 19 | 20 | .. code-block:: python 21 | 22 | import os 23 | from ignis.app import IgnisApp 24 | 25 | app = IgnisApp.get_default() 26 | 27 | app.apply_css(os.path.expanduser("~/.config/ignis/style.scss")) 28 | 29 | Now, you can add a CSS class to any widget and style it in the CSS file. 30 | To add CSS classes to a widget, use the ``css_classes`` property. 31 | 32 | .. code-block:: python 33 | 34 | Widget.Label( 35 | label="hello", 36 | css_classes=["my-label"] 37 | ) 38 | 39 | In style.scss: 40 | 41 | .. code-block:: css 42 | 43 | .my-label { 44 | background-color: red; 45 | } 46 | 47 | Using the ``style`` property 48 | ------------------------------ 49 | 50 | .. warning:: 51 | The ``style`` property does not support SCSS features, only CSS. 52 | 53 | .. code-block:: python 54 | 55 | Widget.Label( 56 | label="hello", 57 | style="background-color: black;" 58 | ) -------------------------------------------------------------------------------- /docs/user/troubleshooting.rst: -------------------------------------------------------------------------------- 1 | Troubleshooting 2 | ================ 3 | 4 | Cannot view children of ``Widget.Scroll`` 5 | ------------------------------------------ 6 | 7 | Set ``min-height: 100px;`` or ``min-width: 100px;`` to your ``Widget.Scroll`` in CSS. 8 | Where ``100px`` you can set any value. 9 | 10 | Alternatively, set ``vexpand=True`` or ``hexpand=True`` to your ``Widget.Scroll``. 11 | 12 | Render issues or Unresponsive UI after wake up from suspend 13 | ----------------------------------------------------------- 14 | 15 | If you use Nvidia, see this: `Preserve video memory after suspend `_. 16 | -------------------------------------------------------------------------------- /docs/user/using_classes.rst: -------------------------------------------------------------------------------- 1 | Using Classes 2 | ============= 3 | 4 | While functions are easier to use (especially for beginners in Python), 5 | classes are more suitable for initializing new objects. 6 | 7 | This approach has several benefits, such as better-structured code 8 | and the ability to use ``self``, which is especially useful for complex widgets. 9 | 10 | .. code-block:: python 11 | 12 | from ignis.widgets import Widget 13 | 14 | 15 | class Bar(Widget.Window): # inheriting from Widget.Window 16 | __gtype_name__ = "MyBar" # optional, this will change the widget's display name in the GTK inspector. 17 | 18 | def __init__(self, monitor: int): 19 | button1 = Widget.Button( 20 | child=Widget.Label(label="Click me!"), 21 | on_click=lambda x: print("you clicked the button 1"), 22 | ) 23 | button2 = Widget.Button( 24 | child=Widget.Label(label="Close window"), 25 | on_click=lambda x: self.set_visible(False), # you can use "self" - the window object itself 26 | ) 27 | button3 = Widget.Button( 28 | child=Widget.Label(label="Custom function on self"), 29 | on_click=lambda x: self.some_func(), 30 | ) 31 | 32 | super().__init__( # calling the constructor of the parent class (Widget.Window) 33 | namespace=f"some-window-{monitor}", 34 | monitor=monitor, 35 | anchor=["left", "top", "right"], 36 | child=Widget.Box( 37 | spacing=10, 38 | child=[ 39 | Widget.Label(label="This window created using a custom class!"), 40 | button1, 41 | button2, 42 | button3, 43 | ], 44 | ), 45 | ) 46 | 47 | def some_func(self) -> None: 48 | print("Custom function on self!") 49 | 50 | # initialize 51 | Bar(0) 52 | 53 | In fact, you can use both classes and functions. 54 | Using classes instead of functions is not mandatory but is recommended. 55 | 56 | .. seealso:: 57 | For advanced usage, you can override methods, add custom properties, and define signals. 58 | Knowledge of Python OOP and PyGObject is encouraged. -------------------------------------------------------------------------------- /examples/bar/README.md: -------------------------------------------------------------------------------- 1 | # Simple bar 2 | 3 | 4 | 5 | This example demonstrates a simple bar. 6 | Place ``config.py`` and ``style.css`` in the ``~/.config/ignis/`` directory. 7 | 8 | > [!WARNING] 9 | > Requires the latest development version of Ignis (for Arch Linux install ``ignis-git``). 10 | > 11 | > Or, use the example bar for the latest stable Ignis version: [v0.5.1](https://github.com/ignis-sh/ignis/tree/v0.5.1/examples/bar) 12 | 13 | **Dependencies:** 14 | ``` 15 | dart-sass 16 | ``` 17 | 18 | 19 | > [!NOTE] 20 | > Currently fully supported compositors are Hyprland and Niri (workspaces, window title 21 | > and keyboard layout). Under other compositors, the bar will not display those elements. 22 | -------------------------------------------------------------------------------- /examples/bar/simple-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/ignis/8a94285fd9234fe457796440f806b7556b59e31f/examples/bar/simple-bar.png -------------------------------------------------------------------------------- /examples/bar/style.scss: -------------------------------------------------------------------------------- 1 | * { 2 | all: unset; 3 | font-weight: bold; 4 | } 5 | 6 | $bg: #1a1112; 7 | $bg-light: #261d1e; 8 | $fg: #f0dedf; 9 | $active: #ffb2bc; 10 | $unactive: #d7c1c3; 11 | 12 | .bar { 13 | padding: 0.4rem 1rem; 14 | background-color: $bg; 15 | color: $fg; 16 | } 17 | 18 | .middle-separator { 19 | background-color: gray; 20 | padding: 0 0.03rem; 21 | } 22 | 23 | .volume-slider trough { 24 | min-width: 100px; 25 | background-color: gray; 26 | min-height: 0.3rem; 27 | border-radius: 1rem; 28 | 29 | highlight { 30 | background-color: $active; 31 | } 32 | 33 | slider { 34 | background-color: $fg; 35 | min-height: 20px; 36 | min-width: 20px; 37 | margin: -8px; 38 | border-radius: 1rem; 39 | } 40 | } 41 | 42 | .workspace { 43 | border-radius: 1rem; 44 | padding: 0 0.55rem; 45 | transition: 0.3s; 46 | } 47 | 48 | .workspace:hover { 49 | background-color: lighten($bg, 10%); 50 | } 51 | 52 | .workspace.active { 53 | background-color: $active; 54 | color: $bg; 55 | } 56 | 57 | .tray-item { 58 | all: unset; 59 | } 60 | 61 | .clock { 62 | font-size: 1.1rem; 63 | margin-right: 0.5rem; 64 | } 65 | 66 | popover.menu { 67 | contents { 68 | background-color: $bg; 69 | padding: 0.35rem; 70 | border-radius: 1rem; 71 | modelbutton { 72 | transition: 0.3s; 73 | border-radius: 0.5rem; 74 | padding: 0.25rem 1rem; 75 | } 76 | modelbutton:hover { 77 | background-color: mix($bg, $unactive, 80%); 78 | } 79 | separator { 80 | background-color: mix($bg, $unactive, 90%); 81 | min-height: 0.1rem; 82 | margin: 0.5rem 0; 83 | } 84 | 85 | label:disabled { 86 | color: $unactive; 87 | } 88 | 89 | arrow { 90 | opacity: 0.3; 91 | 92 | &:hover { background: none; } 93 | 94 | &.left { -gtk-icon-source: -gtk-icontheme("go-previous-symbolic"); } 95 | 96 | &.right { -gtk-icon-source: -gtk-icontheme("go-next-symbolic"); } 97 | } 98 | 99 | } 100 | } -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": [ 6 | "systems" 7 | ] 8 | }, 9 | "locked": { 10 | "lastModified": 1731533236, 11 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 12 | "owner": "numtide", 13 | "repo": "flake-utils", 14 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 15 | "type": "github" 16 | }, 17 | "original": { 18 | "owner": "numtide", 19 | "repo": "flake-utils", 20 | "type": "github" 21 | } 22 | }, 23 | "gvc": { 24 | "flake": false, 25 | "locked": { 26 | "lastModified": 1735384240, 27 | "narHash": "sha256-ikF9EzFlsRH8i4+SVUHETF4Jk1ob2JX1RLsuMdzrQOQ=", 28 | "owner": "ignis-sh", 29 | "repo": "libgnome-volume-control-wheel", 30 | "rev": "2d1cb33dacdae43127bb843a48b159ea7b8925d0", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "ignis-sh", 35 | "repo": "libgnome-volume-control-wheel", 36 | "type": "github" 37 | } 38 | }, 39 | "nixpkgs": { 40 | "locked": { 41 | "lastModified": 1748693115, 42 | "narHash": "sha256-StSrWhklmDuXT93yc3GrTlb0cKSS0agTAxMGjLKAsY8=", 43 | "owner": "NixOS", 44 | "repo": "nixpkgs", 45 | "rev": "910796cabe436259a29a72e8d3f5e180fc6dfacc", 46 | "type": "github" 47 | }, 48 | "original": { 49 | "owner": "NixOS", 50 | "ref": "nixos-unstable", 51 | "repo": "nixpkgs", 52 | "type": "github" 53 | } 54 | }, 55 | "root": { 56 | "inputs": { 57 | "flake-utils": "flake-utils", 58 | "gvc": "gvc", 59 | "nixpkgs": "nixpkgs", 60 | "systems": "systems" 61 | } 62 | }, 63 | "systems": { 64 | "locked": { 65 | "lastModified": 1689347949, 66 | "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", 67 | "owner": "nix-systems", 68 | "repo": "default-linux", 69 | "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", 70 | "type": "github" 71 | }, 72 | "original": { 73 | "owner": "nix-systems", 74 | "repo": "default-linux", 75 | "type": "github" 76 | } 77 | } 78 | }, 79 | "root": "root", 80 | "version": 7 81 | } 82 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A widget framework for building desktop shells, written and configurable in Python"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | 7 | systems.url = "github:nix-systems/default-linux"; 8 | 9 | flake-utils = { 10 | url = "github:numtide/flake-utils"; 11 | inputs.systems.follows = "systems"; 12 | }; 13 | 14 | gvc = { 15 | url = "github:ignis-sh/libgnome-volume-control-wheel"; 16 | flake = false; 17 | }; 18 | }; 19 | 20 | outputs = { self, nixpkgs, flake-utils, gvc, ... }: 21 | flake-utils.lib.eachDefaultSystem (system: 22 | let 23 | pkgs = import nixpkgs { inherit system; }; 24 | version = import ./nix/version.nix { inherit self; }; 25 | in { 26 | packages = rec { 27 | ignis = pkgs.callPackage ./nix { inherit self gvc version; }; 28 | default = ignis; 29 | }; 30 | apps = rec { 31 | ignis = flake-utils.lib.mkApp {drv = self.packages.${system}.ignis;}; 32 | default = ignis; 33 | }; 34 | } 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /ignis/__commit__.py.in: -------------------------------------------------------------------------------- 1 | __commit__ = """@COMMIT@""" 2 | __branch__ = """@BRANCH@""" 3 | __commit_msg__ = """@COMMIT_MSG@""" 4 | -------------------------------------------------------------------------------- /ignis/base_service.py: -------------------------------------------------------------------------------- 1 | from ignis.gobject import IgnisGObject 2 | from typing import TypeVar 3 | 4 | T = TypeVar("T", bound="BaseService") 5 | 6 | 7 | class BaseService(IgnisGObject): 8 | """ 9 | Bases: :class:`~ignis.gobject.IgnisGObject`. 10 | 11 | The base class for all services. 12 | """ 13 | 14 | _instance: T | None = None # type: ignore 15 | 16 | def __init__(self) -> None: 17 | super().__init__() 18 | 19 | @classmethod 20 | def get_default(cls: type[T]) -> T: 21 | """ 22 | Returns the default Service object for this process, creating it if necessary. 23 | """ 24 | if cls._instance is None: 25 | cls._instance = cls() 26 | return cls._instance 27 | -------------------------------------------------------------------------------- /ignis/dbus/com.canonical.dbusmenu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /ignis/dbus/com.github.linkfrg.ignis.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /ignis/dbus/org.freedesktop.DBus.Peer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ignis/dbus/org.freedesktop.Notifications.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /ignis/dbus/org.freedesktop.UPower.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ignis/dbus/org.freedesktop.login1.Manager.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ignis/dbus/org.freedesktop.login1.Session.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ignis/dbus/org.freedesktop.portal.Request.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ignis/dbus/org.freedesktop.portal.ScreenCast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /ignis/dbus/org.kde.StatusNotifierItem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ignis/dbus/org.kde.StatusNotifierWatcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /ignis/dbus/org.mpris.MediaPlayer2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /ignis/deprecation.py: -------------------------------------------------------------------------------- 1 | """Simple deprecation utilities.""" 2 | 3 | from loguru import logger 4 | from collections.abc import Callable 5 | 6 | 7 | def deprecation_warning(message: str) -> None: 8 | """ 9 | Log a warning about the deprecation of a feature or function. 10 | 11 | Args: 12 | message: The message to print. 13 | """ 14 | logger.log("DEPRECATED", message) 15 | 16 | 17 | def deprecated_func(message: str): 18 | """ 19 | A decorator to mark a function as deprecated. 20 | 21 | Args: 22 | message: The message to log. ``{name}`` will be replaced with the function name. 23 | """ 24 | 25 | def decorator(func: Callable): 26 | def wrapper(*args, **kwargs): 27 | deprecation_warning(message.replace("{name}", func.__name__)) 28 | return func(*args, **kwargs) 29 | 30 | return wrapper 31 | 32 | return decorator 33 | 34 | 35 | def deprecated_class(message: str): 36 | """ 37 | A decorator to mark a class as deprecated. 38 | 39 | Args: 40 | message: The message to log. ``{name}`` will be replaced with the class name. 41 | """ 42 | 43 | def decorator(cls): 44 | deprecation_warning(message.replace("{name}", cls.__name__)) 45 | return cls 46 | 47 | return decorator 48 | -------------------------------------------------------------------------------- /ignis/main.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | from ignis.cli import cli_app 3 | 4 | 5 | def set_process_name(name): 6 | libc = ctypes.CDLL("libc.so.6") 7 | libc.prctl(15, ctypes.c_char_p(name.encode()), 0, 0, 0) 8 | 9 | 10 | def main(): 11 | set_process_name("ignis") 12 | cli_app(prog_name="ignis") 13 | 14 | 15 | if __name__ == "__main__": 16 | main() 17 | -------------------------------------------------------------------------------- /ignis/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/ignis/8a94285fd9234fe457796440f806b7556b59e31f/ignis/py.typed -------------------------------------------------------------------------------- /ignis/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkfrg/ignis/8a94285fd9234fe457796440f806b7556b59e31f/ignis/services/__init__.py -------------------------------------------------------------------------------- /ignis/services/applications/__init__.py: -------------------------------------------------------------------------------- 1 | from .service import ApplicationsService 2 | from .application import Application 3 | from .action import ApplicationAction 4 | 5 | __all__ = [ 6 | "ApplicationsService", 7 | "Application", 8 | "ApplicationAction", 9 | ] 10 | -------------------------------------------------------------------------------- /ignis/services/applications/action.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gio # type: ignore 2 | from ignis.gobject import IgnisGObject 3 | from ignis.gobject import IgnisProperty 4 | 5 | 6 | class ApplicationAction(IgnisGObject): 7 | """ 8 | Application action. 9 | """ 10 | 11 | def __init__(self, app: Gio.DesktopAppInfo, action: str): 12 | super().__init__() 13 | 14 | self._app = app 15 | self._action = action 16 | self._name: str = app.get_action_name(action) 17 | 18 | @IgnisProperty 19 | def action(self) -> str: 20 | """ 21 | The ID of the action. 22 | """ 23 | return self._action 24 | 25 | @IgnisProperty 26 | def name(self) -> str: 27 | """ 28 | The human-readable name of the action. 29 | """ 30 | return self._name 31 | 32 | def launch(self) -> None: 33 | """ 34 | Launch this action. 35 | """ 36 | self._app.launch_action(self.action, None) 37 | -------------------------------------------------------------------------------- /ignis/services/applications/service.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gio # type: ignore 2 | from ignis.base_service import BaseService 3 | from .application import Application 4 | from ignis.options import options 5 | from ignis.gobject import IgnisProperty 6 | 7 | 8 | class ApplicationsService(BaseService): 9 | """ 10 | Provides a list of applications installed on the system. 11 | It also allows "pinning" of apps and retrieving a list of pinned applications. 12 | 13 | There are options available for this service: :class:`~ignis.options.Options.Applications`. 14 | 15 | Example usage: 16 | 17 | .. code-block:: python 18 | 19 | from ignis.services.applications import ApplicationsService 20 | 21 | applications = ApplicationsService.get_default() 22 | for i in applications.apps: 23 | print(i.name) 24 | 25 | """ 26 | 27 | def __init__(self): 28 | super().__init__() 29 | self._apps: dict[str, Application] = {} 30 | 31 | self._monitor = Gio.AppInfoMonitor.get() 32 | self._monitor.connect("changed", lambda x: self.__sync()) 33 | 34 | options.applications.connect_option( 35 | "pinned_apps", lambda: self.notify("pinned") 36 | ) 37 | 38 | self.__sync() 39 | 40 | @IgnisProperty 41 | def apps(self) -> list[Application]: 42 | """ 43 | A list of all installed applications. 44 | """ 45 | return sorted(self._apps.values(), key=lambda x: x.name) 46 | 47 | @IgnisProperty 48 | def pinned(self) -> list[Application]: 49 | """ 50 | A list of all pinned applications. 51 | """ 52 | return [ 53 | self._apps.get(name) # type: ignore 54 | for name in options.applications.pinned_apps 55 | if name in self._apps 56 | ] 57 | 58 | def __sync(self) -> None: 59 | self._apps = {} 60 | 61 | for app in Gio.AppInfo.get_all(): 62 | if isinstance(app, Gio.DesktopAppInfo): 63 | self.__add_app(app) 64 | 65 | self.notify("apps") 66 | self.notify("pinned") 67 | 68 | def __add_app(self, app: Gio.DesktopAppInfo) -> None: 69 | if app.get_nodisplay(): 70 | return 71 | 72 | obj = Application(app=app) 73 | 74 | self._apps[obj.id] = obj 75 | 76 | @classmethod 77 | def search( 78 | cls, 79 | apps: list[Application], 80 | query: str, 81 | ) -> list[Application]: 82 | """ 83 | Search applications by a query. 84 | 85 | Args: 86 | apps: A list of applications where to search, e.g., :attr:`~ignis.services.applications.ApplicationsService.apps`. 87 | query: The string to be searched for. 88 | 89 | Returns: 90 | list[Application]: A list of applications filtered by the provided query. 91 | """ 92 | return [ 93 | entry 94 | for result in Gio.DesktopAppInfo.search(query) 95 | for entry in apps 96 | if entry.id in result 97 | ] 98 | -------------------------------------------------------------------------------- /ignis/services/audio/__init__.py: -------------------------------------------------------------------------------- 1 | from .service import AudioService 2 | from .stream import Stream, DefaultStream 3 | from .constants import MICROPHONE_ICON_TEMPLATE, SPEAKER_ICON_TEMPLATE 4 | 5 | __all__ = [ 6 | "AudioService", 7 | "Stream", 8 | "DefaultStream", 9 | "MICROPHONE_ICON_TEMPLATE", 10 | "SPEAKER_ICON_TEMPLATE", 11 | ] 12 | -------------------------------------------------------------------------------- /ignis/services/audio/_imports.py: -------------------------------------------------------------------------------- 1 | import gi 2 | from ignis.exceptions import GvcNotFoundError 3 | from ignis import is_sphinx_build 4 | 5 | try: 6 | if not is_sphinx_build: 7 | gi.require_version("Gvc", "1.0") 8 | from gi.repository import Gvc # type: ignore 9 | except (ImportError, ValueError): 10 | raise GvcNotFoundError() from None 11 | 12 | __all__ = ["Gvc"] 13 | -------------------------------------------------------------------------------- /ignis/services/audio/constants.py: -------------------------------------------------------------------------------- 1 | SPEAKER_ICON_TEMPLATE = "audio-volume-{}-symbolic" 2 | MICROPHONE_ICON_TEMPLATE = "microphone-sensitivity-{}-symbolic" 3 | -------------------------------------------------------------------------------- /ignis/services/backlight/__init__.py: -------------------------------------------------------------------------------- 1 | from .service import BacklightService 2 | from .device import BacklightDevice 3 | from .constants import SYS_BACKLIGHT 4 | 5 | __all__ = [ 6 | "BacklightService", 7 | "BacklightDevice", 8 | "SYS_BACKLIGHT", 9 | ] 10 | -------------------------------------------------------------------------------- /ignis/services/backlight/constants.py: -------------------------------------------------------------------------------- 1 | SYS_BACKLIGHT = "/sys/class/backlight" 2 | -------------------------------------------------------------------------------- /ignis/services/backlight/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ignis.dbus import DBusProxy 3 | from ignis.utils import Utils 4 | 5 | 6 | def get_session_path() -> str: 7 | proxy = DBusProxy.new( 8 | name="org.freedesktop.login1", 9 | object_path="/org/freedesktop/login1", 10 | info=Utils.load_interface_xml("org.freedesktop.login1.Manager"), 11 | interface_name="org.freedesktop.login1.Manager", 12 | bus_type="system", 13 | ) 14 | 15 | session_id = os.getenv("XDG_SESSION_ID") 16 | if session_id is None: 17 | return "" 18 | 19 | session_path = proxy.GetSession("(s)", session_id) 20 | return session_path 21 | -------------------------------------------------------------------------------- /ignis/services/bluetooth/__init__.py: -------------------------------------------------------------------------------- 1 | from .service import BluetoothService 2 | from .device import BluetoothDevice 3 | 4 | __all__ = [ 5 | "BluetoothService", 6 | "BluetoothDevice", 7 | ] 8 | -------------------------------------------------------------------------------- /ignis/services/bluetooth/_imports.py: -------------------------------------------------------------------------------- 1 | import gi 2 | from ignis.exceptions import GnomeBluetoothNotFoundError 3 | from ignis import is_sphinx_build 4 | 5 | try: 6 | if not is_sphinx_build: 7 | gi.require_version("GnomeBluetooth", "3.0") 8 | from gi.repository import GnomeBluetooth # type: ignore 9 | except (ImportError, ValueError): 10 | raise GnomeBluetoothNotFoundError() from None 11 | 12 | __all__ = ["GnomeBluetooth"] 13 | -------------------------------------------------------------------------------- /ignis/services/bluetooth/constants.py: -------------------------------------------------------------------------------- 1 | from ._imports import GnomeBluetooth 2 | 3 | ADAPTER_STATE = { 4 | GnomeBluetooth.AdapterState.ABSENT: "absent", 5 | GnomeBluetooth.AdapterState.ON: "on", 6 | GnomeBluetooth.AdapterState.TURNING_ON: "turning-on", 7 | GnomeBluetooth.AdapterState.TURNING_OFF: "turning-off", 8 | GnomeBluetooth.AdapterState.OFF: "off", 9 | } 10 | -------------------------------------------------------------------------------- /ignis/services/fetch/__init__.py: -------------------------------------------------------------------------------- 1 | from .service import FetchService 2 | 3 | __all__ = ["FetchService"] 4 | -------------------------------------------------------------------------------- /ignis/services/hyprland/__init__.py: -------------------------------------------------------------------------------- 1 | from .service import HyprlandService 2 | from .workspace import HyprlandWorkspace 3 | from .window import HyprlandWindow 4 | from .keyboard import HyprlandKeyboard 5 | from .monitor import HyprlandMonitor 6 | from .constants import HYPR_SOCKET_DIR, HYPRLAND_INSTANCE_SIGNATURE 7 | 8 | __all__ = [ 9 | "HyprlandService", 10 | "HyprlandWorkspace", 11 | "HyprlandWindow", 12 | "HyprlandKeyboard", 13 | "HyprlandMonitor", 14 | "HYPRLAND_INSTANCE_SIGNATURE", 15 | "HYPR_SOCKET_DIR", 16 | ] 17 | -------------------------------------------------------------------------------- /ignis/services/hyprland/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | HYPRLAND_INSTANCE_SIGNATURE = os.getenv("HYPRLAND_INSTANCE_SIGNATURE") 4 | XDG_RUNTIME_DIR = os.getenv("XDG_RUNTIME_DIR") 5 | HYPR_SOCKET_DIR = f"{XDG_RUNTIME_DIR}/hypr/{HYPRLAND_INSTANCE_SIGNATURE}" 6 | -------------------------------------------------------------------------------- /ignis/services/hyprland/workspace.py: -------------------------------------------------------------------------------- 1 | from ignis.gobject import IgnisProperty, IgnisSignal, DataGObject 2 | 3 | MATCH_DICT = { 4 | "monitorID": "monitor_id", 5 | "hasfullscreen": "has_fullscreen", 6 | "lastwindow": "last_window", 7 | "lastwindowtitle": "last_window_title", 8 | "ispersistent": "is_persistent", 9 | } 10 | 11 | 12 | class HyprlandWorkspace(DataGObject): 13 | """ 14 | A workspace. 15 | """ 16 | 17 | def __init__(self, service): 18 | super().__init__(match_dict=MATCH_DICT) 19 | self.__service = service 20 | self._id: int = -1 21 | self._name: str = "" 22 | self._monitor: str = "" 23 | self._monitor_id: int = -1 24 | self._windows: int = -1 25 | self._has_fullscreen: bool = False 26 | self._last_window: str = "" 27 | self._last_window_title: str = "" 28 | self._is_persistent: bool = False 29 | 30 | @IgnisSignal 31 | def destroyed(self): 32 | """ 33 | Emitted when the workspace has been destroyed. 34 | """ 35 | 36 | @IgnisProperty 37 | def id(self) -> int: 38 | """ 39 | The ID of the workspace. 40 | """ 41 | return self._id 42 | 43 | @IgnisProperty 44 | def name(self) -> str: 45 | """ 46 | The name of the workspace. 47 | """ 48 | return self._name 49 | 50 | @IgnisProperty 51 | def monitor(self) -> str: 52 | """ 53 | The monitor on which the workspace is placed. 54 | """ 55 | return self._monitor 56 | 57 | @IgnisProperty 58 | def monitor_id(self) -> int: 59 | """ 60 | The ID of the monitor on which the workspace is placed. 61 | """ 62 | return self._monitor_id 63 | 64 | @IgnisProperty 65 | def windows(self) -> int: 66 | """ 67 | The amount of windows on the workspace. 68 | """ 69 | return self._windows 70 | 71 | @IgnisProperty 72 | def has_fullscreen(self) -> bool: 73 | """ 74 | Whether the workspace has a fullscreen window. 75 | """ 76 | return self._has_fullscreen 77 | 78 | @IgnisProperty 79 | def last_window(self) -> str: 80 | """ 81 | The latest window. 82 | """ 83 | return self._last_window 84 | 85 | @IgnisProperty 86 | def last_window_title(self) -> str: 87 | """ 88 | The latest window title. 89 | """ 90 | return self._last_window_title 91 | 92 | @IgnisProperty 93 | def is_persistent(self) -> bool: 94 | """ 95 | Whether the workspace is persistent. 96 | """ 97 | return self._is_persistent 98 | 99 | def switch_to(self) -> None: 100 | """ 101 | Switch to this workspace. 102 | """ 103 | self.__service.send_command(f"dispatch workspace {self.id}") 104 | -------------------------------------------------------------------------------- /ignis/services/mpris/__init__.py: -------------------------------------------------------------------------------- 1 | from .service import MprisService 2 | from .player import MprisPlayer 3 | from .constants import ART_URL_CACHE_DIR 4 | 5 | __all__ = [ 6 | "MprisService", 7 | "MprisPlayer", 8 | "ART_URL_CACHE_DIR", 9 | ] 10 | -------------------------------------------------------------------------------- /ignis/services/mpris/constants.py: -------------------------------------------------------------------------------- 1 | import ignis 2 | 3 | ART_URL_CACHE_DIR = f"{ignis.CACHE_DIR}/art_url" 4 | -------------------------------------------------------------------------------- /ignis/services/mpris/service.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from ignis.dbus import DBusProxy 3 | from ignis.utils import Utils 4 | from ignis.base_service import BaseService 5 | from ignis.gobject import IgnisProperty, IgnisSignal 6 | from .player import MprisPlayer 7 | 8 | 9 | class MprisService(BaseService): 10 | """ 11 | A service for controlling media players using the MPRIS interface. 12 | 13 | Example usage: 14 | 15 | .. code-block:: python 16 | 17 | from ignis.services.mpris import MprisService 18 | 19 | mpris = MprisService.get_default() 20 | 21 | mpris.connect("player_added", lambda x, player: print(player.desktop_entry, player.title)) 22 | """ 23 | 24 | def __init__(self): 25 | super().__init__() 26 | self._players: dict[str, MprisPlayer] = {} 27 | 28 | self.__dbus = DBusProxy.new( 29 | name="org.freedesktop.DBus", 30 | object_path="/org/freedesktop/DBus", 31 | interface_name="org.freedesktop.DBus", 32 | info=Utils.load_interface_xml("org.freedesktop.DBus"), 33 | ) 34 | 35 | self.__dbus.signal_subscribe( 36 | signal_name="NameOwnerChanged", 37 | callback=lambda *args: asyncio.create_task(self.__init_player(args[5][0])), 38 | ) 39 | 40 | asyncio.create_task(self.__get_players()) 41 | 42 | async def __get_players(self) -> None: 43 | all_names = self.__dbus.ListNames() 44 | for name in all_names: 45 | await self.__init_player(name) 46 | 47 | async def __init_player(self, name: str) -> None: 48 | if ( 49 | name.startswith("org.mpris.MediaPlayer2") 50 | and name not in self._players 51 | and name != "org.mpris.MediaPlayer2.playerctld" 52 | ): 53 | player = await MprisPlayer.new_async(name) 54 | 55 | self._players[name] = player 56 | player.connect("closed", lambda x: self.__remove_player(name)) 57 | self.emit("player_added", player) 58 | self.notify("players") 59 | 60 | def __remove_player(self, name: str) -> None: 61 | if name in self._players: 62 | self._players.pop(name) 63 | self.notify("players") 64 | 65 | @IgnisSignal 66 | def player_added(self, player: MprisPlayer): 67 | """ 68 | Emitted when a player has been added. 69 | 70 | Args: 71 | player: The instance of the player. 72 | """ 73 | pass 74 | 75 | @IgnisProperty 76 | def players(self) -> list[MprisPlayer]: 77 | """ 78 | A list of currently active players. 79 | """ 80 | return list(self._players.values()) 81 | -------------------------------------------------------------------------------- /ignis/services/mpris/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | from urllib.parse import urlparse, unquote 3 | 4 | 5 | def uri_to_unix_path(uri: str) -> str: 6 | parsed = urlparse(uri) 7 | 8 | if parsed.scheme == "file": 9 | return unquote(os.path.basename(parsed.path)) 10 | 11 | elif parsed.scheme in ("http", "https"): 12 | return unquote(os.path.basename(parsed.path)) 13 | 14 | else: 15 | raise ValueError(f"Unsupported URI scheme: {parsed.scheme}") 16 | -------------------------------------------------------------------------------- /ignis/services/network/__init__.py: -------------------------------------------------------------------------------- 1 | from .access_point import WifiAccessPoint, ActiveAccessPoint 2 | from .ethernet_device import EthernetDevice 3 | from .ethernet import Ethernet 4 | from .service import NetworkService 5 | from .wifi_connect_dialog import WifiConnectDialog 6 | from .wifi_device import WifiDevice 7 | from .wifi import Wifi 8 | from .vpn import VpnConnection, Vpn 9 | from .constants import WIFI_ICON_TEMPLATE, STATE 10 | 11 | __all__ = [ 12 | "WifiAccessPoint", 13 | "ActiveAccessPoint", 14 | "EthernetDevice", 15 | "Ethernet", 16 | "NetworkService", 17 | "STATE", 18 | "WifiConnectDialog", 19 | "WifiDevice", 20 | "Wifi", 21 | "VpnConnection", 22 | "Vpn", 23 | "WIFI_ICON_TEMPLATE", 24 | ] 25 | -------------------------------------------------------------------------------- /ignis/services/network/_imports.py: -------------------------------------------------------------------------------- 1 | import gi 2 | from ignis.exceptions import NetworkManagerNotFoundError 3 | from ignis import is_sphinx_build 4 | 5 | try: 6 | if not is_sphinx_build: 7 | gi.require_version("NM", "1.0") 8 | from gi.repository import NM # type: ignore 9 | except (ImportError, ValueError): 10 | raise NetworkManagerNotFoundError() from None 11 | 12 | __all__ = ["NM"] 13 | -------------------------------------------------------------------------------- /ignis/services/network/constants.py: -------------------------------------------------------------------------------- 1 | from ._imports import NM 2 | 3 | WIFI_ICON_TEMPLATE = "network-wireless-signal-{}-symbolic" 4 | 5 | STATE = { 6 | NM.DeviceState.UNKNOWN: "unknown", 7 | NM.DeviceState.UNMANAGED: "unmanaged", 8 | NM.DeviceState.ACTIVATED: "activated", 9 | NM.DeviceState.DEACTIVATING: "deactivating", 10 | NM.DeviceState.FAILED: "failed", 11 | NM.DeviceState.UNAVAILABLE: "unavailable", 12 | NM.DeviceState.DISCONNECTED: "disconnected", 13 | NM.DeviceState.PREPARE: "prepare", 14 | NM.DeviceState.CONFIG: "config", 15 | NM.DeviceState.NEED_AUTH: "need_auth", 16 | NM.DeviceState.IP_CONFIG: "ip_config", 17 | NM.DeviceState.IP_CHECK: "ip_check", 18 | NM.DeviceState.SECONDARIES: "secondaries", 19 | } 20 | -------------------------------------------------------------------------------- /ignis/services/network/service.py: -------------------------------------------------------------------------------- 1 | from ignis.base_service import BaseService 2 | from ignis.gobject import IgnisProperty 3 | from ._imports import NM 4 | from .wifi import Wifi 5 | from .ethernet import Ethernet 6 | from .vpn import Vpn 7 | 8 | 9 | class NetworkService(BaseService): 10 | """ 11 | A Network service. Uses ``NetworkManager``. 12 | """ 13 | 14 | def __init__(self): 15 | super().__init__() 16 | self._client = NM.Client.new(None) 17 | self._wifi = Wifi(self._client) 18 | self._ethernet = Ethernet(self._client) 19 | self._vpn = Vpn(self._client) 20 | 21 | @IgnisProperty 22 | def wifi(self) -> Wifi: 23 | """ 24 | The Wi-Fi object. 25 | """ 26 | return self._wifi 27 | 28 | @IgnisProperty 29 | def ethernet(self) -> Ethernet: 30 | """ 31 | The Ethernet object. 32 | """ 33 | return self._ethernet 34 | 35 | @IgnisProperty 36 | def vpn(self) -> Vpn: 37 | """ 38 | The Vpn object. 39 | """ 40 | return self._vpn 41 | -------------------------------------------------------------------------------- /ignis/services/network/util.py: -------------------------------------------------------------------------------- 1 | from ._imports import NM 2 | 3 | 4 | def check_is_vpn(func): 5 | def wrapper(*args, **kwargs): 6 | connection = args[2] 7 | if ( 8 | connection.get_connection_type() == "vpn" 9 | or connection.get_connection_type() == "wireguard" 10 | ): 11 | func(*args, **kwargs) 12 | 13 | return wrapper 14 | 15 | 16 | def get_wifi_connect_window_name(bssid: str) -> str: 17 | return f"wifi-connect_{bssid}" 18 | 19 | 20 | def filter_connections( 21 | obj: "NM.AccessPoint | NM.Device", connections: list[NM.Connection] 22 | ) -> list[NM.Connection]: 23 | # Filter manually using connection_valid() 24 | # Because the transfer annotation for this function may not work correctly with bindings 25 | # See https://gitlab.gnome.org/GNOME/gobject-introspection/-/issues/305 26 | # E.g.: NM.Device.filter_connections(NM.Client.get_connections()) can cause SIGSEGV 27 | 28 | return [conn for conn in connections if obj.connection_valid(conn)] 29 | -------------------------------------------------------------------------------- /ignis/services/niri/__init__.py: -------------------------------------------------------------------------------- 1 | from .service import NiriService 2 | from .keyboard import NiriKeyboardLayouts 3 | from .window import NiriWindow 4 | from .workspace import NiriWorkspace 5 | from .constants import NIRI_SOCKET 6 | 7 | __all__ = [ 8 | "NiriService", 9 | "NiriKeyboardLayouts", 10 | "NiriWindow", 11 | "NiriWorkspace", 12 | "NIRI_SOCKET", 13 | ] 14 | -------------------------------------------------------------------------------- /ignis/services/niri/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | NIRI_SOCKET = os.getenv("NIRI_SOCKET") 4 | -------------------------------------------------------------------------------- /ignis/services/niri/keyboard.py: -------------------------------------------------------------------------------- 1 | from ignis.gobject import IgnisProperty, DataGObject 2 | 3 | 4 | class NiriKeyboardLayouts(DataGObject): 5 | """ 6 | Configured keyboard layouts. 7 | """ 8 | 9 | def __init__(self, service): 10 | super().__init__() 11 | self.__service = service 12 | self._names: list = [] 13 | self._current_idx: int = -1 14 | 15 | @IgnisProperty 16 | def names(self) -> list: 17 | """ 18 | XKB names of the configured layouts. 19 | """ 20 | return self._names 21 | 22 | @IgnisProperty 23 | def current_idx(self) -> int: 24 | """ 25 | Index of the currently active layout in names. 26 | """ 27 | return self._current_idx 28 | 29 | @IgnisProperty 30 | def current_name(self) -> str: 31 | """ 32 | Name of the currently active layout. 33 | """ 34 | return self._names[self._current_idx] 35 | 36 | def switch_layout(self, layout: str) -> None: 37 | """ 38 | Switch the keyboard layout. 39 | 40 | Args: 41 | layout: The layout to switch to (``Next``, ``Prev`` or a valid id) 42 | """ 43 | cmd = {"Action": {"SwitchLayout": {"layout": layout}}} 44 | self.__service.send_command(cmd) 45 | -------------------------------------------------------------------------------- /ignis/services/niri/window.py: -------------------------------------------------------------------------------- 1 | from ignis.gobject import IgnisProperty, IgnisSignal, DataGObject 2 | 3 | 4 | class NiriWindow(DataGObject): 5 | """ 6 | A window. 7 | """ 8 | 9 | def __init__(self, service): 10 | super().__init__() 11 | 12 | self.__service = service 13 | self._id: int = -1 14 | self._title: str = "" 15 | self._app_id: str = "" 16 | self._pid: int = -1 17 | self._workspace_id: int = -1 18 | self._is_focused: bool = False 19 | self._is_floating: bool = False 20 | 21 | @IgnisSignal 22 | def destroyed(self): 23 | """ 24 | Emitted when the window has been destroyed. 25 | """ 26 | 27 | @IgnisProperty 28 | def id(self) -> int: 29 | """ 30 | The unique ID of the window. 31 | """ 32 | return self._id 33 | 34 | @IgnisProperty 35 | def title(self) -> str: 36 | """ 37 | The title of the window. 38 | """ 39 | return self._title 40 | 41 | @IgnisProperty 42 | def app_id(self) -> str: 43 | """ 44 | Application ID of the window. 45 | """ 46 | return self._app_id 47 | 48 | @IgnisProperty 49 | def pid(self) -> int: 50 | """ 51 | The PID of the window. 52 | """ 53 | return self._pid 54 | 55 | @IgnisProperty 56 | def workspace_id(self) -> int: 57 | """ 58 | The ID of the workspace where the window is placed. 59 | """ 60 | return self._workspace_id 61 | 62 | @IgnisProperty 63 | def is_focused(self) -> bool: 64 | """ 65 | Whether the window is focused. 66 | """ 67 | return self._is_focused 68 | 69 | @IgnisProperty 70 | def is_floating(self) -> bool: # type: ignore 71 | """ 72 | Whether the window is floating. 73 | """ 74 | return self._is_floating 75 | 76 | def close(self) -> None: 77 | """ 78 | Close this window. 79 | """ 80 | cmd = {"Action": {"CloseWindow": {"id": self._id}}} 81 | self.__service.send_command(cmd) 82 | 83 | def focus(self) -> None: 84 | """ 85 | Focus this window. 86 | """ 87 | cmd = {"Action": {"FocusWindow": {"id": self._id}}} 88 | self.__service.send_command(cmd) 89 | 90 | def toggle_fullscreen(self) -> None: 91 | """ 92 | Toggle fullscreen on this window. 93 | """ 94 | cmd = {"Action": {"FullscreenWindow": {"id": self._id}}} 95 | self.__service.send_command(cmd) 96 | 97 | def toggle_floating(self) -> None: 98 | """ 99 | Move the window between the floating and the tiling layout. 100 | """ 101 | cmd = {"Action": {"ToggleWindowFloating": {"id": self._id}}} 102 | self.__service.send_command(cmd) 103 | -------------------------------------------------------------------------------- /ignis/services/niri/workspace.py: -------------------------------------------------------------------------------- 1 | from ignis.gobject import IgnisProperty, IgnisSignal, DataGObject 2 | 3 | 4 | class NiriWorkspace(DataGObject): 5 | """ 6 | A workspace. 7 | """ 8 | 9 | def __init__(self, service): 10 | super().__init__() 11 | self.__service = service 12 | self._id: int = -1 13 | self._idx: int = -1 14 | self._name: str = "" 15 | self._output: str = "" 16 | self._is_active: bool = False 17 | self._is_focused: bool = False 18 | self._active_window_id: int = -1 19 | 20 | @IgnisSignal 21 | def destroyed(self): 22 | """ 23 | Emitted when the workspace has been destroyed. 24 | """ 25 | 26 | @IgnisProperty 27 | def id(self) -> int: 28 | """ 29 | The unique ID of the workspace. 30 | """ 31 | return self._id 32 | 33 | @IgnisProperty 34 | def idx(self) -> int: 35 | """ 36 | The index of the workspace on its monitor. 37 | """ 38 | return self._idx 39 | 40 | @IgnisProperty 41 | def name(self) -> str: 42 | """ 43 | The name of the workspace. 44 | """ 45 | return self._name 46 | 47 | @IgnisProperty 48 | def output(self) -> str: 49 | """ 50 | The name of the output on which the workspace is placed. 51 | """ 52 | return self._output 53 | 54 | @IgnisProperty 55 | def is_active(self) -> bool: 56 | """ 57 | Whether the workspace is currently active on its output. 58 | """ 59 | return self._is_active 60 | 61 | @IgnisProperty 62 | def is_focused(self) -> bool: 63 | """ 64 | Whether the workspace is currently focused. 65 | """ 66 | return self._is_focused 67 | 68 | @IgnisProperty 69 | def active_window_id(self) -> int: 70 | """ 71 | The ID of the active window on this workspace. 72 | """ 73 | return self._active_window_id 74 | 75 | def switch_to(self) -> None: 76 | """ 77 | Switch to this workspace. 78 | """ 79 | cmd = {"Action": {"FocusWorkspace": {"reference": {"Id": self._id}}}} 80 | self.__service.send_command(cmd) 81 | -------------------------------------------------------------------------------- /ignis/services/notifications/__init__.py: -------------------------------------------------------------------------------- 1 | from .action import NotificationAction 2 | from .notification import Notification 3 | from .service import NotificationService 4 | from .constants import ( 5 | NOTIFICATIONS_CACHE_DIR, 6 | NOTIFICATIONS_CACHE_FILE, 7 | NOTIFICATIONS_EMPTY_CACHE_FILE, 8 | NOTIFICATIONS_IMAGE_DATA, 9 | ) 10 | 11 | __all__ = [ 12 | "NotificationAction", 13 | "Notification", 14 | "NotificationService", 15 | "NOTIFICATIONS_CACHE_DIR", 16 | "NOTIFICATIONS_CACHE_FILE", 17 | "NOTIFICATIONS_EMPTY_CACHE_FILE", 18 | "NOTIFICATIONS_IMAGE_DATA", 19 | ] 20 | -------------------------------------------------------------------------------- /ignis/services/notifications/action.py: -------------------------------------------------------------------------------- 1 | from ignis.dbus import DBusService 2 | from gi.repository import GLib # type: ignore 3 | from ignis.gobject import IgnisGObject, IgnisProperty 4 | 5 | 6 | class NotificationAction(IgnisGObject): 7 | """ 8 | A simple object that contains data about a notification action. 9 | """ 10 | 11 | def __init__(self, dbus: DBusService, notification, id: str, label: str): 12 | super().__init__() 13 | self.__dbus = dbus 14 | self.__notification = notification 15 | self._id = id 16 | self._label = label 17 | 18 | @IgnisProperty 19 | def id(self) -> str: 20 | """ 21 | The ID of the action. 22 | """ 23 | return self._id 24 | 25 | @IgnisProperty 26 | def label(self) -> str: 27 | """ 28 | The label of the notification. This one should be displayed to user. 29 | """ 30 | return self._label 31 | 32 | def invoke(self) -> None: 33 | """ 34 | Invoke this action. 35 | """ 36 | self.__dbus.emit_signal( 37 | "ActionInvoked", GLib.Variant("(us)", (self.__notification.id, self.id)) 38 | ) 39 | -------------------------------------------------------------------------------- /ignis/services/notifications/constants.py: -------------------------------------------------------------------------------- 1 | import ignis 2 | 3 | NOTIFICATIONS_CACHE_DIR = f"{ignis.CACHE_DIR}/notifications" 4 | NOTIFICATIONS_CACHE_FILE = f"{NOTIFICATIONS_CACHE_DIR}/notifications.json" 5 | NOTIFICATIONS_IMAGE_DATA = f"{NOTIFICATIONS_CACHE_DIR}/images" 6 | NOTIFICATIONS_EMPTY_CACHE_FILE: dict = {"id": 0, "notifications": []} 7 | -------------------------------------------------------------------------------- /ignis/services/recorder/__init__.py: -------------------------------------------------------------------------------- 1 | from .constants import AUDIO_DEVICE_PIPELINE, MAIN_AUDIO_PIPELINE, PIPELINE_TEMPLATE 2 | from .service import RecorderService 3 | from .session import SessionManager 4 | from .util import gst_inspect 5 | 6 | __all__ = [ 7 | "AUDIO_DEVICE_PIPELINE", 8 | "MAIN_AUDIO_PIPELINE", 9 | "PIPELINE_TEMPLATE", 10 | "RecorderService", 11 | "SessionManager", 12 | "gst_inspect", 13 | ] 14 | -------------------------------------------------------------------------------- /ignis/services/recorder/_imports.py: -------------------------------------------------------------------------------- 1 | import gi 2 | from ignis.exceptions import GstNotFoundError 3 | from ignis import is_sphinx_build 4 | 5 | try: 6 | if not is_sphinx_build: 7 | gi.require_version("Gst", "1.0") 8 | from gi.repository import Gst # type: ignore 9 | except (ImportError, ValueError): 10 | raise GstNotFoundError( 11 | "GStreamer not found! To use the recorder service, install GStreamer." 12 | ) from None 13 | 14 | __all__ = ["Gst"] 15 | -------------------------------------------------------------------------------- /ignis/services/recorder/constants.py: -------------------------------------------------------------------------------- 1 | PIPELINE_TEMPLATE = """ 2 | pipewiresrc path={node_id} do-timestamp=true keepalive-time=1000 resend-last=true ! 3 | videoconvert chroma-mode=none dither=none matrix-mode=output-only n-threads={n_threads} ! 4 | queue ! 5 | x264enc bitrate={bitrate} threads={n_threads} ! 6 | queue ! 7 | h264parse ! 8 | mp4mux fragment-duration=500 fragment-mode=first-moov-then-finalise name=mux ! 9 | filesink location={path} 10 | """ 11 | 12 | MAIN_AUDIO_PIPELINE = """ 13 | pulsesrc device={device} ! 14 | queue ! 15 | audioconvert ! 16 | audioresample ! 17 | audiomixer name=mix ! 18 | queue ! 19 | opusenc ! 20 | mux. 21 | """ 22 | 23 | AUDIO_DEVICE_PIPELINE = """ 24 | pulsesrc device={device} ! 25 | queue ! 26 | audioconvert ! 27 | audioresample ! 28 | mix. 29 | """ 30 | -------------------------------------------------------------------------------- /ignis/services/recorder/util.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | 4 | def gst_inspect(name: str) -> bool: 5 | try: 6 | subprocess.run(["gst-inspect-1.0", "--exists", name], check=True) 7 | return True 8 | except subprocess.CalledProcessError: 9 | return False 10 | -------------------------------------------------------------------------------- /ignis/services/system_tray/__init__.py: -------------------------------------------------------------------------------- 1 | from .item import SystemTrayItem 2 | from .service import SystemTrayService 3 | 4 | __all__ = [ 5 | "SystemTrayItem", 6 | "SystemTrayService", 7 | ] 8 | -------------------------------------------------------------------------------- /ignis/services/systemd/__init__.py: -------------------------------------------------------------------------------- 1 | from .service import SystemdService 2 | from .unit import SystemdUnit 3 | 4 | __all__ = [ 5 | "SystemdService", 6 | "SystemdUnit", 7 | ] 8 | -------------------------------------------------------------------------------- /ignis/services/upower/__init__.py: -------------------------------------------------------------------------------- 1 | from .service import UPowerService 2 | from .device import UPowerDevice 3 | 4 | __all__ = ["UPowerService", "UPowerDevice"] 5 | -------------------------------------------------------------------------------- /ignis/services/upower/constants.py: -------------------------------------------------------------------------------- 1 | DEVICE_KIND = { 2 | None: "unknown", 3 | 0: "unknown", 4 | 1: "line-power", 5 | 2: "battery", 6 | 3: "ups", 7 | 4: "monitor", 8 | 5: "mouse", 9 | 6: "keyboard", 10 | 7: "pda", 11 | 8: "phone", 12 | 9: "media-player", 13 | 10: "tablet", 14 | 11: "computer", 15 | 12: "gaming-input", 16 | 13: "pen", 17 | 14: "touchpad", 18 | 15: "modem", 19 | 16: "network", 20 | 17: "headset", 21 | 18: "speakers", 22 | 19: "headphones", 23 | 20: "video", 24 | 21: "other-audio", 25 | 22: "remote-control", 26 | 23: "printer", 27 | 24: "scanner", 28 | 25: "camera", 29 | 26: "wearable", 30 | 27: "toy", 31 | 28: "bluetooth-generic", 32 | } 33 | 34 | TECHNOLOGY = { 35 | 0: "unknown", 36 | 1: "lithium-ion", 37 | 2: "lithium-polymer", 38 | 3: "lithium-iron-phosphate", 39 | 4: "lead-acid", 40 | 5: "nickel-cadmium", 41 | 6: "nickel-metal-hydride", 42 | } 43 | 44 | 45 | DeviceState = { 46 | "UNKNOWN": 0, 47 | "CHARGING": 1, 48 | "DISCHARGING": 2, 49 | "EMPTY": 3, 50 | "FULLY_CHARGED": 4, 51 | "PENDING_CHARGE": 5, 52 | "PENDING_DISCHARGE": 6, 53 | } 54 | -------------------------------------------------------------------------------- /ignis/services/wallpaper/__init__.py: -------------------------------------------------------------------------------- 1 | from .window import WallpaperLayerWindow 2 | from .service import WallpaperService 3 | from .constants import CACHE_WALLPAPER_PATH 4 | 5 | __all__ = [ 6 | "WallpaperLayerWindow", 7 | "WallpaperService", 8 | "CACHE_WALLPAPER_PATH", 9 | ] 10 | -------------------------------------------------------------------------------- /ignis/services/wallpaper/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ignis 3 | import shutil 4 | from loguru import logger 5 | 6 | 7 | _OLD_CACHE_WALLPAPER_PATH = f"{ignis.CACHE_DIR}/wallpaper" 8 | CACHE_WALLPAPER_PATH = f"{ignis.DATA_DIR}/wallpaper" 9 | 10 | # FIXME: remove after v0.6 release 11 | if not os.path.exists(CACHE_WALLPAPER_PATH) and os.path.exists( 12 | _OLD_CACHE_WALLPAPER_PATH 13 | ): 14 | logger.warning( 15 | f"Copying the cached wallpaper to the new directory: {_OLD_CACHE_WALLPAPER_PATH} -> {CACHE_WALLPAPER_PATH}" 16 | ) 17 | shutil.copy(_OLD_CACHE_WALLPAPER_PATH, CACHE_WALLPAPER_PATH) 18 | -------------------------------------------------------------------------------- /ignis/services/wallpaper/service.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from ignis.utils import Utils 4 | from ignis.base_service import BaseService 5 | from ignis.options import options 6 | from .window import WallpaperLayerWindow 7 | from .constants import CACHE_WALLPAPER_PATH 8 | 9 | 10 | class WallpaperService(BaseService): 11 | """ 12 | A simple service to set the wallpaper. 13 | Supports multiple monitors. 14 | 15 | There are options available for this service: :class:`~ignis.options.Options.Wallpaper`. 16 | 17 | Example usage: 18 | 19 | .. code-block:: python 20 | 21 | .. code-block:: python 22 | 23 | from ignis.services.wallpaper import WallpaperService 24 | from ignis.options import options 25 | 26 | WallpaperService.get_default() # just to initialize it 27 | 28 | options.wallpaper.set_wallpaper_path("path/to/image") 29 | """ 30 | 31 | def __init__(self): 32 | super().__init__() 33 | self._windows: list[WallpaperLayerWindow] = [] 34 | options.wallpaper.connect_option( 35 | "wallpaper_path", lambda: self.__update_wallpaper() 36 | ) 37 | self.__sync() 38 | 39 | def __update_wallpaper(self) -> None: 40 | try: 41 | if options.wallpaper.wallpaper_path is not None: 42 | shutil.copy(options.wallpaper.wallpaper_path, CACHE_WALLPAPER_PATH) 43 | except shutil.SameFileError: 44 | return 45 | 46 | self.__sync() 47 | 48 | def __sync(self) -> None: 49 | for i in self._windows: 50 | i.unrealize() 51 | 52 | if not os.path.isfile(CACHE_WALLPAPER_PATH): 53 | return 54 | 55 | self._windows = [] 56 | 57 | for monitor_id in range(Utils.get_n_monitors()): 58 | gdkmonitor = Utils.get_monitor(monitor_id) 59 | if not gdkmonitor: 60 | return 61 | 62 | geometry = gdkmonitor.get_geometry() 63 | window = WallpaperLayerWindow( 64 | wallpaper_path=CACHE_WALLPAPER_PATH, 65 | gdkmonitor=gdkmonitor, 66 | width=geometry.width, 67 | height=geometry.height, 68 | ) 69 | self._windows.append(window) 70 | -------------------------------------------------------------------------------- /ignis/services/wallpaper/window.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets.picture import Picture 2 | from gi.repository import Gtk, Gdk # type: ignore 3 | from gi.repository import Gtk4LayerShell as GtkLayerShell # type: ignore 4 | from ignis.exceptions import LayerShellNotSupportedError 5 | from ignis.app import IgnisApp 6 | 7 | app = IgnisApp.get_default() 8 | 9 | 10 | class WallpaperLayerWindow(Gtk.Window): 11 | def __init__( 12 | self, wallpaper_path: str, gdkmonitor: Gdk.Monitor, width: int, height: int 13 | ) -> None: 14 | if not GtkLayerShell.is_supported(): 15 | raise LayerShellNotSupportedError() 16 | 17 | Gtk.Window.__init__(self, application=app) 18 | GtkLayerShell.init_for_window(self) 19 | 20 | for anchor in ["LEFT", "RIGHT", "TOP", "BOTTOM"]: 21 | GtkLayerShell.set_anchor(self, getattr(GtkLayerShell.Edge, anchor), True) 22 | 23 | GtkLayerShell.set_exclusive_zone(self, -1) # ignore other layers 24 | 25 | GtkLayerShell.set_namespace( 26 | self, name_space=f"ignis_wallpaper_service_{gdkmonitor.get_model()}" 27 | ) 28 | 29 | GtkLayerShell.set_layer(self, GtkLayerShell.Layer.BACKGROUND) 30 | 31 | GtkLayerShell.set_monitor(self, gdkmonitor) 32 | 33 | self.set_child( 34 | Picture( 35 | image=wallpaper_path, 36 | content_fit="cover", 37 | width=width, 38 | height=height, 39 | ) 40 | ) 41 | 42 | self.set_visible(True) 43 | -------------------------------------------------------------------------------- /ignis/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import TypeAlias 2 | from .debounce import DebounceTask, debounce 3 | from .file_monitor import FileMonitor 4 | from .file import read_file, read_file_async, write_file, write_file_async 5 | from .icon import get_paintable, get_file_icon_name, get_app_icon_name 6 | from .misc import load_interface_xml, get_current_dir 7 | from .monitor import get_monitor, get_n_monitors, get_monitors 8 | from .pixbuf import scale_pixbuf, crop_pixbuf 9 | from .poll import Poll 10 | from .sass import sass_compile 11 | from .shell import exec_sh, exec_sh_async, AsyncCompletedProcess 12 | from .socket import send_socket, listen_socket 13 | from .str_cases import snake_to_pascal, pascal_to_snake 14 | from .thread import thread, run_in_thread, ThreadTask 15 | from .timeout import Timeout 16 | from .version import ( 17 | get_ignis_version, 18 | get_ignis_commit, 19 | get_ignis_branch, 20 | get_ignis_commit_msg, 21 | ) 22 | 23 | 24 | class Utils: 25 | exec_sh = exec_sh 26 | exec_sh_async = exec_sh_async 27 | AsyncCompletedProcess: TypeAlias = AsyncCompletedProcess 28 | load_interface_xml = load_interface_xml 29 | Poll: TypeAlias = Poll 30 | get_monitor = get_monitor 31 | get_n_monitors = get_n_monitors 32 | Timeout: TypeAlias = Timeout 33 | FileMonitor: TypeAlias = FileMonitor 34 | thread = thread 35 | run_in_thread = run_in_thread 36 | sass_compile = sass_compile 37 | get_ignis_version = get_ignis_version 38 | scale_pixbuf = scale_pixbuf 39 | crop_pixbuf = crop_pixbuf 40 | get_paintable = get_paintable 41 | get_file_icon_name = get_file_icon_name 42 | ThreadTask: TypeAlias = ThreadTask 43 | get_ignis_commit = get_ignis_commit 44 | get_current_dir = get_current_dir 45 | get_ignis_branch = get_ignis_branch 46 | get_ignis_commit_msg = get_ignis_commit_msg 47 | send_socket = send_socket 48 | listen_socket = listen_socket 49 | DebounceTask = DebounceTask 50 | debounce = debounce 51 | get_monitors = get_monitors 52 | snake_to_pascal = snake_to_pascal 53 | pascal_to_snake = pascal_to_snake 54 | read_file = read_file 55 | read_file_async = read_file_async 56 | write_file = write_file 57 | write_file_async = write_file_async 58 | get_app_icon_name = get_app_icon_name 59 | -------------------------------------------------------------------------------- /ignis/utils/debounce.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from .timeout import Timeout 3 | 4 | 5 | class DebounceTask: 6 | """ 7 | Delays function calls until a specified time elapses after the most recent call. 8 | 9 | .. hint:: 10 | See the decorator for this class :func:`~ignis.utils.Utils.debounce`. 11 | 12 | Parameters: 13 | ms: The delay time in milliseconds. 14 | target: The function to invoke after the delay. 15 | """ 16 | 17 | def __init__(self, ms: int, target: Callable) -> None: 18 | self._timeout: Timeout | None = None 19 | self._ms = ms 20 | self._target = target 21 | 22 | def run(self, *args, **kwargs): 23 | """ 24 | Run the task. 25 | ``*args`` and ``**kwargs`` will be passed to the ``target`` function. 26 | """ 27 | if self._timeout is not None: 28 | self._timeout.cancel() 29 | self._timeout = Timeout(self._ms, lambda: self._target(*args, **kwargs)) 30 | 31 | 32 | def debounce(ms: int): 33 | """ 34 | A decorator to delay function execution until a set time has passed since the last call. 35 | 36 | This is a convenient wrapper for the :class:`~ignis.utils.Utils.DebounceTask` class. 37 | 38 | Args: 39 | ms: The delay time in milliseconds. 40 | 41 | Example usage: 42 | 43 | .. code-block:: python 44 | 45 | from ignis.utils import Utils 46 | 47 | @Utils.debounce(500) # delay for 500 ms (0.5 s) 48 | def some_func(x) -> None: 49 | print("called!") 50 | 51 | some_func(1) 52 | some_func(2) # only this call will execute 53 | """ 54 | 55 | def decorate_function(func: Callable): 56 | task = DebounceTask(ms, func) 57 | 58 | def wrapper(*args, **kwargs): 59 | task.run(*args, **kwargs) 60 | 61 | return wrapper 62 | 63 | return decorate_function 64 | -------------------------------------------------------------------------------- /ignis/utils/icon.py: -------------------------------------------------------------------------------- 1 | import os 2 | from gi.repository import Gtk, Gdk, Gio # type: ignore 3 | from ignis.exceptions import DisplayNotFoundError 4 | 5 | 6 | def get_paintable( 7 | widget: Gtk.Widget, icon_name: str, size: int 8 | ) -> "Gtk.IconPaintable | None": 9 | """ 10 | Get a ``Gdk.Paintable`` by icon name. 11 | 12 | Args: 13 | widget: The parent widget. 14 | icon_name: The name of the icon to look up. 15 | size: The size of the icon. 16 | 17 | Returns: 18 | The paintable object for the icon or ``None`` if no such icon exists. 19 | """ 20 | display = Gdk.Display.get_default() 21 | if not display: 22 | raise DisplayNotFoundError() 23 | 24 | icon = Gio.ThemedIcon.new(icon_name) 25 | icon_theme = Gtk.IconTheme.get_for_display(display) 26 | return icon_theme.lookup_by_gicon( 27 | icon, 28 | size, 29 | widget.get_scale_factor(), 30 | widget.get_direction(), 31 | Gtk.IconLookupFlags.PRELOAD, 32 | ) 33 | 34 | 35 | def get_file_icon_name(path: str, symbolic: bool = False) -> str | None: 36 | """ 37 | Get a standart icon name for the file or directory. 38 | 39 | Args: 40 | path: The path to the file or directory. 41 | symbolic: Whether the icon should be symbolic. 42 | 43 | Returns: 44 | The name of the icon. ``None`` if the icon with the given name is not found. 45 | """ 46 | file = Gio.File.new_for_path(path) 47 | if not os.path.exists(path): 48 | raise FileNotFoundError(f"No such file or directory: {path}") 49 | info = file.query_info("standard::icon", Gio.FileQueryInfoFlags.NONE) 50 | icon_obj: Gio.ThemedIcon = info.get_icon() # type: ignore 51 | icon_names = icon_obj.get_names() 52 | 53 | if symbolic: 54 | for icon in icon_names: 55 | if icon.endswith("-symbolic"): 56 | return icon 57 | 58 | if len(icon_names) > 0: 59 | return icon_names[0] 60 | 61 | return None 62 | 63 | 64 | def get_app_icon_name(app_id: str) -> str | None: 65 | """ 66 | Get the application icon name by the application ID. 67 | 68 | Args: 69 | app_id: The application ID, without ``.desktop`` extension. 70 | 71 | Returns: 72 | The application icon name, or ``None`` if the application with the given ID doesn't exist or has no icon. 73 | """ 74 | try: 75 | app_info = Gio.DesktopAppInfo.new(app_id + ".desktop") 76 | except TypeError: 77 | return None 78 | 79 | if not app_info: 80 | return None 81 | 82 | return app_info.get_string("Icon") 83 | -------------------------------------------------------------------------------- /ignis/utils/misc.py: -------------------------------------------------------------------------------- 1 | import os 2 | import inspect 3 | from gi.repository import Gio # type: ignore 4 | 5 | 6 | def get_current_dir() -> str: 7 | """ 8 | Returns the directory of the Python file where this function is called. 9 | """ 10 | frame = inspect.stack()[1] 11 | caller_file = frame.filename 12 | return os.path.dirname(os.path.abspath(caller_file)) 13 | 14 | 15 | DBUS_DIR = get_current_dir() + "/../dbus" 16 | 17 | 18 | def load_interface_xml( 19 | interface_name: str | None = None, path: str | None = None, xml: str | None = None 20 | ) -> Gio.DBusInterfaceInfo: 21 | """ 22 | Load interface info from XML. 23 | If you want to load interface info from the path or XML string, you need to provide ``path`` and ``xml`` as keyword arguments respectively. 24 | 25 | Args: 26 | interface_name: The name of the interface. The interface must be stored in the ``ignis/dbus/`` directory in the Ignis sources. 27 | path: The full path to the interface XML. 28 | xml: The XML string. 29 | 30 | Raises: 31 | TypeError: If neither of the arguments is provided. 32 | 33 | Returns: 34 | The interface information. 35 | """ 36 | xml_string: str 37 | 38 | if interface_name: 39 | file_path = f"{DBUS_DIR}/{interface_name}.xml" 40 | with open(file_path) as file: 41 | xml_string = file.read() 42 | elif path: 43 | with open(path) as file: 44 | xml_string = file.read() 45 | elif xml: 46 | xml_string = xml 47 | else: 48 | raise TypeError( 49 | "load_interface_xml() requires at least one positional argument" 50 | ) 51 | 52 | return Gio.DBusNodeInfo.new_for_xml(xml_string).interfaces[0] 53 | -------------------------------------------------------------------------------- /ignis/utils/monitor.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gdk, Gio # type: ignore 2 | from ignis.exceptions import DisplayNotFoundError 3 | 4 | 5 | def get_monitor(monitor_id: int) -> "Gdk.Monitor | None": 6 | """ 7 | Get the ``Gdk.Monitor`` by its ID. 8 | 9 | Args: 10 | monitor_id: The ID of the monitor. 11 | 12 | Returns: 13 | The monitor with the given ID, or ``None`` if no such monitor exists. 14 | """ 15 | display = Gdk.Display.get_default() 16 | if not display: 17 | raise DisplayNotFoundError() 18 | 19 | return display.get_monitors().get_item(monitor_id) # type: ignore 20 | 21 | 22 | def get_monitors() -> Gio.ListModel: 23 | """ 24 | Get a list model of :class:`Gdk.Monitor`. 25 | 26 | Returns: 27 | A list model of :class:`Gdk.Monitor`. 28 | """ 29 | display = Gdk.Display.get_default() 30 | if not display: 31 | raise DisplayNotFoundError() 32 | return display.get_monitors() 33 | 34 | 35 | def get_n_monitors() -> int: 36 | """ 37 | Get the number of monitors. 38 | 39 | Returns: 40 | The number of monitors. 41 | """ 42 | display = Gdk.Display.get_default() 43 | if not display: 44 | raise DisplayNotFoundError() 45 | 46 | return len(display.get_monitors()) 47 | -------------------------------------------------------------------------------- /ignis/utils/pixbuf.py: -------------------------------------------------------------------------------- 1 | from gi.repository import GdkPixbuf # type: ignore 2 | 3 | 4 | def crop_pixbuf(pixbuf: GdkPixbuf.Pixbuf, width: int, height: int) -> GdkPixbuf.Pixbuf: 5 | """ 6 | Crop the ``GdkPixbuf.Pixbuf`` to the given width and height. 7 | 8 | Args: 9 | pixbuf: The source pixbuf. 10 | width: The width to crop to. 11 | height: The height to crop to. 12 | 13 | Returns: 14 | The cropped pixbuf. 15 | """ 16 | img_width = pixbuf.get_width() 17 | img_height = pixbuf.get_height() 18 | 19 | # Calculate the aspect ratios 20 | target_aspect_ratio = width / height 21 | current_aspect_ratio = img_width / img_height 22 | 23 | if current_aspect_ratio > target_aspect_ratio: 24 | # Image is wider than aspect ratio, crop the width 25 | target_width = int(img_height * target_aspect_ratio) 26 | target_height = img_height 27 | else: 28 | # Image is taller than aspect ratio, crop the height 29 | target_width = img_width 30 | target_height = int(img_width / target_aspect_ratio) 31 | 32 | crop_x = (img_width - target_width) // 2 33 | crop_y = (img_height - target_height) // 2 34 | 35 | cropped_image = pixbuf.new_subpixbuf(crop_x, crop_y, target_width, target_height) 36 | return cropped_image 37 | 38 | 39 | def scale_pixbuf( 40 | pixbuf: GdkPixbuf.Pixbuf, width: int, height: int 41 | ) -> "GdkPixbuf.Pixbuf | None": 42 | """ 43 | Scale a ``GdkPixbuf.Pixbuf`` to the given width and height. 44 | 45 | Args: 46 | pixbuf: The source GdkPixbuf.Pixbuf. 47 | width: The target width. 48 | height: The target height. 49 | 50 | Returns: 51 | The scaled GdkPixbuf.Pixbuf or ``None``. 52 | """ 53 | return pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR) 54 | -------------------------------------------------------------------------------- /ignis/utils/poll.py: -------------------------------------------------------------------------------- 1 | from ignis.gobject import IgnisGObject, IgnisProperty, IgnisSignal 2 | from gi.repository import GLib # type: ignore 3 | from typing import Any 4 | from collections.abc import Callable 5 | 6 | 7 | class Poll(IgnisGObject): 8 | """ 9 | Calls a callback every n milliseconds specified by the timeout. 10 | 11 | You can pass arguments to the constructor, and they will be passed to the callback. 12 | 13 | Args: 14 | timeout: The timeout interval in milliseconds. 15 | callback: The function to call when the timeout is reached. The ``self`` will passed as an argument. 16 | *args: Arguments to pass to `callback`. 17 | 18 | Example usage: 19 | 20 | .. code-block:: python 21 | 22 | from ignis.utils import Utils 23 | 24 | # print "Hello" every second 25 | Utils.Poll(timeout=1_000, callback=lambda self: print("Hello")) 26 | """ 27 | 28 | def __init__(self, timeout: int, callback: Callable, *args): 29 | super().__init__() 30 | self._id: int | None = None 31 | self._output: Any = None 32 | 33 | self._timeout = timeout 34 | self._callback = callback 35 | self._args = args 36 | 37 | self.__main() 38 | 39 | @IgnisSignal 40 | def changed(self): 41 | """ 42 | Emitted at each iteration. 43 | """ 44 | 45 | @IgnisProperty 46 | def timeout(self) -> int: 47 | """ 48 | The timeout interval in milliseconds. 49 | """ 50 | return self._timeout 51 | 52 | @timeout.setter 53 | def timeout(self, value: int) -> None: 54 | self._timeout = value 55 | 56 | @IgnisProperty 57 | def callback(self) -> Callable: 58 | """ 59 | The function to call when the timeout is reached. The ``self`` will passed as an argument. 60 | """ 61 | return self._callback 62 | 63 | @callback.setter 64 | def callback(self, value: Callable) -> None: 65 | self._callback = value 66 | 67 | @IgnisProperty 68 | def output(self) -> Any: 69 | """ 70 | The output of the callback. 71 | 72 | .. hint:: 73 | You can use bind() on ``output``. 74 | """ 75 | return self._output 76 | 77 | def __main(self) -> None: 78 | self._output = self._callback(self, *self._args) 79 | self.emit("changed") 80 | self.notify("output") 81 | self._id = GLib.timeout_add(self._timeout, self.__main) 82 | 83 | def cancel(self) -> None: 84 | """ 85 | Cancel polling. 86 | """ 87 | if not self._id: 88 | return 89 | 90 | if GLib.MainContext.default().find_source_by_id(self._id): 91 | GLib.source_remove(self._id) 92 | -------------------------------------------------------------------------------- /ignis/utils/sass.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import subprocess 3 | from typing import Literal 4 | from ignis.exceptions import SassCompilationError, SassNotFoundError 5 | from ignis import TEMP_DIR 6 | 7 | COMPILED_CSS = f"{TEMP_DIR}/compiled.css" 8 | 9 | # resolve Sass compiler paths and pick a default one 10 | # "sass" (dart-sass) is the default, 11 | # "grass" is an API-compatible drop-in replacement 12 | sass_compilers = {} 13 | for cmd in ("sass", "grass"): 14 | path = shutil.which(cmd) 15 | if path: 16 | sass_compilers[cmd] = path 17 | 18 | 19 | def compile_file(path: str, compiler_path: str) -> str: 20 | result = subprocess.run([compiler_path, path, COMPILED_CSS], capture_output=True) 21 | 22 | if result.returncode != 0: 23 | raise SassCompilationError(result.stderr.decode()) 24 | 25 | with open(COMPILED_CSS) as file: 26 | return file.read() 27 | 28 | 29 | def compile_string(string: str, compiler_path: str) -> str: 30 | process = subprocess.Popen( 31 | [compiler_path, "--stdin"], 32 | stdin=subprocess.PIPE, 33 | stdout=subprocess.PIPE, 34 | stderr=subprocess.PIPE, 35 | ) 36 | stdout, stderr = process.communicate(input=string.encode()) 37 | 38 | if process.returncode != 0: 39 | raise SassCompilationError(stderr.decode()) 40 | else: 41 | return stdout.decode() 42 | 43 | 44 | def sass_compile( 45 | path: str | None = None, 46 | string: str | None = None, 47 | compiler: Literal["sass", "grass"] | None = None, 48 | ) -> str: 49 | """ 50 | Compile a SASS/SCSS file or string. 51 | Requires either `Dart Sass `_ 52 | or `Grass `_. 53 | 54 | Args: 55 | path: The path to the SASS/SCSS file. 56 | string: A string with SASS/SCSS style. 57 | compiler: The desired Sass compiler, either ``sass`` or ``grass``. 58 | 59 | Raises: 60 | TypeError: If neither of the arguments is provided. 61 | SassNotFoundError: If no Sass compiler is available. 62 | SassCompilationError: If an error occurred while compiling SASS/SCSS. 63 | """ 64 | if not sass_compilers: 65 | raise SassNotFoundError() 66 | 67 | if compiler and compiler not in sass_compilers: 68 | raise SassNotFoundError() 69 | 70 | if compiler: 71 | compiler_path = sass_compilers[compiler] 72 | else: 73 | compiler_path = next(iter(sass_compilers.values())) 74 | 75 | if string: 76 | return compile_string(string, compiler_path) 77 | 78 | elif path: 79 | return compile_file(path, compiler_path) 80 | 81 | else: 82 | raise TypeError("sass_compile() requires at least one positional argument") 83 | -------------------------------------------------------------------------------- /ignis/utils/shell.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import subprocess 3 | 4 | 5 | def exec_sh(command: str, **kwargs) -> subprocess.CompletedProcess: 6 | """ 7 | Execute a shell (bash) command. 8 | 9 | Args: 10 | command: The command to execute. 11 | 12 | ``**kwargs`` will be passed to ``subprocess.run()``. 13 | 14 | Returns: 15 | The result of the command execution. You can use the ``stdout`` property to get the command's output. 16 | """ 17 | return subprocess.run(command, shell=True, text=True, capture_output=True, **kwargs) 18 | 19 | 20 | class AsyncCompletedProcess: 21 | """ 22 | Completed process object for :func:`~ignis.utils.Utils.exec_sh_async`. 23 | """ 24 | 25 | def __init__(self, stdout: str, stderr: str, returncode: int) -> None: 26 | self._returncode: int = returncode 27 | self._stdout: str = stdout 28 | self._stderr: str = stderr 29 | 30 | @property 31 | def returncode(self) -> int: 32 | """ 33 | The return code of the process. 34 | """ 35 | return self._returncode 36 | 37 | @property 38 | def stdout(self) -> str: 39 | """ 40 | The output of the process. 41 | """ 42 | return self._stdout 43 | 44 | @property 45 | def stderr(self) -> str: 46 | """ 47 | The stderr (errors) of the process. 48 | """ 49 | return self._stderr 50 | 51 | 52 | async def exec_sh_async(command: str) -> AsyncCompletedProcess: 53 | """ 54 | Execute a shell (bash) command asynchronously. 55 | 56 | Args: 57 | command: The command to execute. 58 | on_finished: A function to call when the process is finished. An instance of :class:`~ignis.utils.exec_sh.AsyncCompletedProcess` will be passed to this function. 59 | 60 | Returns: 61 | The instance of ``Gio.Subprocess``. 62 | """ 63 | 64 | process = await asyncio.create_subprocess_shell( 65 | command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE 66 | ) 67 | stdout, stderr = await process.communicate() 68 | returncode = process.returncode 69 | 70 | return AsyncCompletedProcess( 71 | stdout.decode(), stderr.decode(), returncode if returncode is not None else -1 72 | ) 73 | -------------------------------------------------------------------------------- /ignis/utils/socket.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from collections.abc import Generator 3 | from typing import Literal 4 | 5 | 6 | def send_socket( 7 | sock: socket.socket, 8 | message: str, 9 | errors: Literal["strict", "replace", "ignore"] = "strict", 10 | end_char: str | None = None, 11 | ) -> str: 12 | """ 13 | Send a message to the socket. 14 | 15 | Args: 16 | sock: An instance of a socket. 17 | message: The message to send. 18 | errors: The error handling scheme that will be passed to :py:meth:`bytes.decode`. 19 | end_char: The character after which the response is considered fully received (e.g, ``\\n``). 20 | 21 | Returns: 22 | The response from the socket. 23 | """ 24 | sock.send(message.encode()) 25 | 26 | resp = bytearray() 27 | end_bytes = end_char.encode() if end_char is not None else None 28 | 29 | while True: 30 | chunk = sock.recv(8192) 31 | if not chunk: 32 | break 33 | 34 | resp.extend(chunk) 35 | 36 | if end_bytes and resp.endswith(end_bytes): 37 | break 38 | 39 | return resp.decode("utf-8", errors=errors) 40 | 41 | 42 | def listen_socket( 43 | sock: socket.socket, errors: Literal["strict", "replace", "ignore"] = "strict" 44 | ) -> Generator[str, None, None]: 45 | """ 46 | Listen to the socket. 47 | This function is a generator. 48 | 49 | Args: 50 | sock: An instance of a socket. 51 | errors: The error handling scheme that will be passed to :py:meth:`bytes.decode`. 52 | 53 | Returns: 54 | A generator that yields responses from the socket. 55 | 56 | Example usage: 57 | 58 | .. code-block:: python 59 | 60 | with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: 61 | sock.connect("path/to/socket.sock") 62 | 63 | for message in Utils.listen_socket(sock): 64 | print(message) 65 | """ 66 | 67 | buffer = b"" 68 | while True: 69 | new_data = sock.recv(8192) 70 | if not new_data: 71 | break 72 | buffer += new_data 73 | while b"\n" in buffer: 74 | data, buffer = buffer.split(b"\n", 1) 75 | yield data.decode("utf-8", errors=errors) 76 | -------------------------------------------------------------------------------- /ignis/utils/str_cases.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def snake_to_pascal(string: str) -> str: 5 | """ 6 | Convert a `snake_case` string to `PascalCase`. 7 | 8 | Args: 9 | string: the string to convert. 10 | """ 11 | return string.replace("_", " ").title().replace(" ", "") 12 | 13 | 14 | def pascal_to_snake(string: str) -> str: 15 | """ 16 | Convert a `PascalCase` string to `snake_case`. 17 | 18 | Args: 19 | string: the string to convert. 20 | """ 21 | return re.sub(r"(? threading.Thread: 7 | """ 8 | Simply run the given function in a thread. 9 | The provided args and kwargs will be passed to the function. 10 | 11 | Args: 12 | target: The function to run. 13 | 14 | Returns: 15 | The thread in which the function is running. 16 | """ 17 | th = threading.Thread(target=target, args=args, kwargs=kwargs, daemon=True) 18 | th.start() 19 | return th 20 | 21 | 22 | def run_in_thread(func: Callable) -> Callable: 23 | """ 24 | Decorator to run the decorated function in a thread. 25 | """ 26 | 27 | def wrapper(*args, **kwargs): 28 | return thread(func, *args, **kwargs) 29 | 30 | return wrapper 31 | 32 | 33 | class ThreadTask(IgnisGObject): 34 | """ 35 | Execute a function in another thread and call a callback when it's finished. 36 | The output from the function is passed to the callback. 37 | 38 | Parameters: 39 | target: The function to execute in another thread. 40 | callback: The function to call when ``target`` has finished. 41 | """ 42 | 43 | def __init__(self, target: Callable, callback: Callable): 44 | super().__init__() 45 | self._target = target 46 | self._callback = callback 47 | 48 | self.connect("finished", lambda x, result: callback(result)) 49 | 50 | @run_in_thread 51 | def __run(self) -> None: 52 | result = self._target() 53 | self.emit("finished", result) 54 | 55 | @IgnisSignal 56 | def finished(self, output: object): 57 | """ 58 | Args: 59 | output: The output from the function. 60 | """ 61 | 62 | def run(self) -> None: 63 | """ 64 | Run this task. 65 | """ 66 | self.__run() 67 | -------------------------------------------------------------------------------- /ignis/utils/timeout.py: -------------------------------------------------------------------------------- 1 | from gi.repository import GLib # type: ignore 2 | from ignis.gobject import IgnisGObject, IgnisProperty 3 | from collections.abc import Callable 4 | 5 | 6 | class Timeout(IgnisGObject): 7 | """ 8 | Calls a function after a specified time interval. 9 | 10 | Args: 11 | ms: Time in milliseconds. 12 | target: The function to call. 13 | 14 | Example usage: 15 | 16 | .. code-block:: python 17 | 18 | from ignis.utils import Utils 19 | 20 | Utils.Timeout(ms=3000, target=lambda: print("Hello")) 21 | """ 22 | 23 | def __init__(self, ms: int, target: Callable, *args): 24 | super().__init__() 25 | self._ms = ms 26 | self._target = target 27 | 28 | self._id = GLib.timeout_add(ms, target, *args) 29 | 30 | @IgnisProperty 31 | def ms(self) -> int: 32 | """ 33 | Time in milliseconds. 34 | """ 35 | return self._ms 36 | 37 | @IgnisProperty 38 | def target(self) -> Callable: 39 | """ 40 | The function to call. 41 | """ 42 | return self._target 43 | 44 | def cancel(self) -> None: 45 | """ 46 | Cancel the timeout if it is active. 47 | 48 | This method prevents the ``target`` function from being called. 49 | """ 50 | if GLib.MainContext.default().find_source_by_id(self._id): 51 | GLib.source_remove(self._id) 52 | -------------------------------------------------------------------------------- /ignis/utils/version.py: -------------------------------------------------------------------------------- 1 | from ignis import __version__ 2 | 3 | 4 | def get_ignis_version() -> str: 5 | """ 6 | Get the current Ignis version. 7 | 8 | Returns: 9 | The Ignis version. 10 | """ 11 | return __version__ 12 | 13 | 14 | def get_ignis_commit() -> str: 15 | """ 16 | Get the current Ignis commit hash. 17 | 18 | Returns: 19 | The Ignis commit hash. 20 | """ 21 | 22 | try: 23 | from ignis.__commit__ import __commit__ # type: ignore 24 | 25 | return __commit__ 26 | except (ImportError, ValueError): 27 | return "" 28 | 29 | 30 | def get_ignis_branch() -> str: 31 | """ 32 | Get the name of the current Ignis git branch. 33 | 34 | Returns: 35 | The name of the Ignis git branch. 36 | """ 37 | try: 38 | from ignis.__commit__ import __branch__ # type: ignore 39 | 40 | return __branch__ 41 | except (ImportError, ValueError): 42 | return "" 43 | 44 | 45 | def get_ignis_commit_msg() -> str: 46 | """ 47 | Get the message of the latest Ignis commit. 48 | 49 | Returns: 50 | The message of the latest Ignis commit. 51 | """ 52 | try: 53 | from ignis.__commit__ import __commit_msg__ # type: ignore 54 | 55 | return __commit_msg__ 56 | except (ImportError, ValueError): 57 | return "" 58 | -------------------------------------------------------------------------------- /ignis/variable.py: -------------------------------------------------------------------------------- 1 | from .gobject import IgnisGObject, IgnisProperty 2 | from typing import Any 3 | 4 | 5 | class Variable(IgnisGObject): 6 | """ 7 | Bases: :class:`~ignis.gobject.IgnisGObject` 8 | 9 | Simple class that holds a value. 10 | 11 | Args: 12 | value: An initial value to set. 13 | 14 | Example usage: 15 | 16 | .. code-block:: python 17 | 18 | from ignis.variable import Variable 19 | 20 | var = Variable(value=10) 21 | var.connect("notify::value", lambda x, y: print("Value changed!: ", x.value)) 22 | 23 | var.value = 20 24 | """ 25 | 26 | def __init__(self, value: Any = None): 27 | self._value = value 28 | super().__init__() 29 | 30 | @IgnisProperty 31 | def value(self) -> Any: 32 | """ 33 | A value. 34 | """ 35 | return self._value 36 | 37 | @value.setter 38 | def value(self, value: Any) -> None: 39 | self._value = value 40 | -------------------------------------------------------------------------------- /ignis/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import TypeAlias 2 | from .window import Window 3 | from .label import Label 4 | from .button import Button 5 | from .box import Box 6 | from .calendar import Calendar 7 | from .scale import Scale 8 | from .icon import Icon 9 | from .picture import Picture 10 | from .centerbox import CenterBox 11 | from .revealer import Revealer 12 | from .scroll import Scroll 13 | from .entry import Entry 14 | from .switch import Switch 15 | from .separator import Separator 16 | from .toggle_button import ToggleButton 17 | from .regular_window import RegularWindow 18 | from .file_chooser_button import FileChooserButton 19 | from .file_filter import FileFilter 20 | from .file_dialog import FileDialog 21 | from .grid import Grid 22 | from .popover_menu import PopoverMenu 23 | from .eventbox import EventBox 24 | from .headerbar import HeaderBar 25 | from .listboxrow import ListBoxRow 26 | from .listbox import ListBox 27 | from .check_button import CheckButton 28 | from .spin_button import SpinButton 29 | from .dropdown import DropDown 30 | from .overlay import Overlay 31 | from .arrow import Arrow 32 | from .arrow_button import ArrowButton 33 | from .revealer_window import RevealerWindow 34 | from .stack import Stack 35 | from .stack_switcher import StackSwitcher 36 | from .stack_page import StackPage 37 | 38 | 39 | class Widget: 40 | Window: TypeAlias = Window 41 | Label: TypeAlias = Label 42 | Button: TypeAlias = Button 43 | Box: TypeAlias = Box 44 | Calendar: TypeAlias = Calendar 45 | Scale: TypeAlias = Scale 46 | Icon: TypeAlias = Icon 47 | CenterBox: TypeAlias = CenterBox 48 | Revealer: TypeAlias = Revealer 49 | Scroll: TypeAlias = Scroll 50 | Entry: TypeAlias = Entry 51 | Switch: TypeAlias = Switch 52 | Separator: TypeAlias = Separator 53 | ToggleButton: TypeAlias = ToggleButton 54 | RegularWindow: TypeAlias = RegularWindow 55 | FileChooserButton: TypeAlias = FileChooserButton 56 | FileFilter: TypeAlias = FileFilter 57 | Grid: TypeAlias = Grid 58 | PopoverMenu: TypeAlias = PopoverMenu 59 | EventBox: TypeAlias = EventBox 60 | FileDialog: TypeAlias = FileDialog 61 | HeaderBar: TypeAlias = HeaderBar 62 | ListBoxRow: TypeAlias = ListBoxRow 63 | ListBox: TypeAlias = ListBox 64 | Picture: TypeAlias = Picture 65 | CheckButton: TypeAlias = CheckButton 66 | SpinButton: TypeAlias = SpinButton 67 | DropDown: TypeAlias = DropDown 68 | Overlay: TypeAlias = Overlay 69 | Arrow: TypeAlias = Arrow 70 | ArrowButton: TypeAlias = ArrowButton 71 | RevealerWindow: TypeAlias = RevealerWindow 72 | Stack: TypeAlias = Stack 73 | StackSwitcher: TypeAlias = StackSwitcher 74 | StackPage = StackPage 75 | -------------------------------------------------------------------------------- /ignis/widgets/arrow_button.py: -------------------------------------------------------------------------------- 1 | from ignis.widgets.button import Button 2 | from ignis.widgets.arrow import Arrow 3 | from ignis.gobject import IgnisProperty 4 | 5 | 6 | class ArrowButton(Button): 7 | """ 8 | Bases: :class:`~ignis.widgets.button.Button` 9 | 10 | A simple button with an arrow. On click, it will toggle (rotate) the arrow. 11 | 12 | Args: 13 | arrow: An instance of an arrow. 14 | **kwargs: Properties to set. 15 | 16 | .. code-block:: python 17 | 18 | Widget.ArrowButton( 19 | arrow=Widget.Arrow( 20 | ... # Arrow-specific properties go here 21 | ) 22 | ) 23 | """ 24 | 25 | __gtype_name__ = "IgnisArrowButton" 26 | 27 | def __init__(self, arrow: Arrow, **kwargs): 28 | self._arrow = arrow 29 | 30 | super().__init__(child=self._arrow, **kwargs) 31 | self.connect("clicked", lambda x: self._arrow.toggle()) 32 | 33 | @IgnisProperty 34 | def arrow(self) -> Arrow: 35 | """ 36 | An instance of an arrow. 37 | """ 38 | return self._arrow 39 | 40 | def toggle(self) -> None: 41 | """ 42 | Same as :func:`~ignis.widgets.Widget.Arrow.toggle` 43 | """ 44 | self._arrow.toggle() 45 | -------------------------------------------------------------------------------- /ignis/widgets/box.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | from ignis.gobject import IgnisProperty 4 | 5 | 6 | class Box(Gtk.Box, BaseWidget): 7 | """ 8 | Bases: :class:`Gtk.Box`. 9 | 10 | The main layout widget. 11 | 12 | Args: 13 | **kwargs: Properties to set. 14 | 15 | .. hint:: 16 | You can use generators to set children. 17 | 18 | .. code-block:: 19 | 20 | Widget.Box( 21 | child=[Widget.Label(label=str(i)) for i in range(10)] 22 | ) 23 | 24 | .. code-block:: python 25 | 26 | Widget.Box( 27 | child=[Widget.Label(label='heh'), Widget.Label(label='heh2')], 28 | vertical=False, 29 | homogeneous=False, 30 | spacing=52 31 | ) 32 | """ 33 | 34 | __gtype_name__ = "IgnisBox" 35 | __gproperties__ = {**BaseWidget.gproperties} 36 | 37 | def __init__(self, **kwargs): 38 | Gtk.Box.__init__(self) 39 | self._child: list[Gtk.Widget] = [] 40 | BaseWidget.__init__(self, **kwargs) 41 | 42 | @IgnisProperty 43 | def child(self) -> list[Gtk.Widget]: 44 | """ 45 | A list of child widgets. 46 | """ 47 | return self._child 48 | 49 | @child.setter 50 | def child(self, child: list[Gtk.Widget]) -> None: 51 | for c in self._child: 52 | super().remove(c) 53 | 54 | self._child = [] 55 | for c in child: 56 | if c: 57 | self.append(c) 58 | 59 | def append(self, child: Gtk.Widget) -> None: 60 | _orig_unparent = child.unparent 61 | 62 | def unparent_wrapper(*args, **kwargs): 63 | self.remove(child) 64 | _orig_unparent(*args, **kwargs) 65 | child.unparent = _orig_unparent 66 | 67 | child.unparent = unparent_wrapper 68 | self._child.append(child) 69 | super().append(child) 70 | self.notify("child") 71 | 72 | def remove(self, child: Gtk.Widget) -> None: 73 | self._child.remove(child) 74 | super().remove(child) 75 | self.notify("child") 76 | 77 | def prepend(self, child: Gtk.Widget) -> None: 78 | self._child.insert(0, child) 79 | super().prepend(child) 80 | self.notify("child") 81 | 82 | @IgnisProperty 83 | def vertical(self) -> bool: 84 | """ 85 | Whether the box arranges children vertically. 86 | 87 | Default: ``False``. 88 | """ 89 | return self.get_orientation() == Gtk.Orientation.VERTICAL 90 | 91 | @vertical.setter 92 | def vertical(self, value: bool) -> None: 93 | if value: 94 | self.set_property("orientation", Gtk.Orientation.VERTICAL) 95 | else: 96 | self.set_property("orientation", Gtk.Orientation.HORIZONTAL) 97 | -------------------------------------------------------------------------------- /ignis/widgets/calendar.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | 4 | 5 | class Calendar(Gtk.Calendar, BaseWidget): 6 | """ 7 | Bases: :class:`Gtk.Calendar` 8 | 9 | A calendar. 10 | 11 | Args: 12 | **kwargs: Properties to set. 13 | 14 | .. code-block:: python 15 | 16 | Widget.Calendar( 17 | day=1, 18 | month=1, 19 | year=2024, 20 | no_month_change=False, 21 | show_day_names=True, 22 | show_details=True, 23 | show_heading=True, 24 | show_week_numbers=True 25 | ) 26 | 27 | """ 28 | 29 | __gtype_name__ = "IgnisCalendar" 30 | __gproperties__ = {**BaseWidget.gproperties} 31 | 32 | def __init__(self, **kwargs): 33 | Gtk.Calendar.__init__(self) 34 | BaseWidget.__init__(self, **kwargs) 35 | -------------------------------------------------------------------------------- /ignis/widgets/centerbox.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | from ignis.gobject import IgnisProperty 4 | 5 | 6 | class CenterBox(Gtk.CenterBox, BaseWidget): 7 | """ 8 | Bases: :class:`Gtk.CenterBox` 9 | 10 | A box widget that contains three widgets, which are placed at the start, center, and end of the container. 11 | 12 | Args: 13 | **kwargs: Properties to set. 14 | 15 | .. code-block:: python 16 | 17 | Widget.CenterBox( 18 | vertical=False, 19 | start_widget=Widget.Label(label='start'), 20 | center_widget=Widget.Label(label='center'), 21 | end_widget=Widget.Label(label='end'), 22 | ) 23 | """ 24 | 25 | __gtype_name__ = "IgnisCenterBox" 26 | __gproperties__ = {**BaseWidget.gproperties} 27 | 28 | def __init__(self, **kwargs): 29 | Gtk.CenterBox.__init__(self) 30 | BaseWidget.__init__(self, **kwargs) 31 | 32 | @IgnisProperty 33 | def vertical(self) -> bool: 34 | """ 35 | Whether the box arranges children vertically. 36 | 37 | Default: ``False``. 38 | """ 39 | return self.get_orientation() == Gtk.Orientation.VERTICAL 40 | 41 | @vertical.setter 42 | def vertical(self, value: bool) -> None: 43 | if value: 44 | self.set_property("orientation", Gtk.Orientation.VERTICAL) 45 | else: 46 | self.set_property("orientation", Gtk.Orientation.HORIZONTAL) 47 | -------------------------------------------------------------------------------- /ignis/widgets/check_button.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | from collections.abc import Callable 4 | from ignis.gobject import IgnisProperty 5 | 6 | 7 | class CheckButton(Gtk.CheckButton, BaseWidget): 8 | """ 9 | Bases: :class:`Gtk.CheckButton` 10 | 11 | A check button. If ``group`` is set, the check button behaves as a radio button. 12 | 13 | Args: 14 | **kwargs: Properties to set. 15 | 16 | Simple checkbutton: 17 | 18 | .. code-block:: python 19 | 20 | Widget.CheckButton( 21 | label='check button', 22 | active=True, 23 | ) 24 | 25 | Radio button: 26 | 27 | .. code-block:: python 28 | 29 | Widget.CheckButton( 30 | group=Widget.CheckButton(label='radiobutton 1'), 31 | label='radiobutton 2', 32 | active=True, 33 | ) 34 | """ 35 | 36 | __gtype_name__ = "IgnisCheckButton" 37 | __gproperties__ = {**BaseWidget.gproperties} 38 | 39 | def __init__(self, **kwargs): 40 | Gtk.CheckButton.__init__(self) 41 | BaseWidget.__init__(self, **kwargs) 42 | 43 | self._on_toggled: Callable | None = None 44 | 45 | self.connect( 46 | "toggled", 47 | lambda x: self.on_toggled(x, x.active) if self.on_toggled else None, 48 | ) 49 | 50 | @IgnisProperty 51 | def on_toggled(self) -> Callable | None: 52 | """ 53 | The function to call when button is toggled (checked/unchecked). 54 | """ 55 | return self._on_toggled 56 | 57 | @on_toggled.setter 58 | def on_toggled(self, value: Callable) -> None: 59 | self._on_toggled = value 60 | -------------------------------------------------------------------------------- /ignis/widgets/dropdown.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | from collections.abc import Callable 4 | from ignis.gobject import IgnisProperty 5 | 6 | 7 | class DropDown(Gtk.DropDown, BaseWidget): 8 | """ 9 | Bases: :class:`Gtk.DropDown` 10 | 11 | A widget that allows the user to choose an item from a list of options. 12 | 13 | Args: 14 | **kwargs: Properties to set. 15 | 16 | .. code-block:: python 17 | 18 | Widget.DropDown( 19 | items=["option 1", "option 2", "option 3"], 20 | on_selected=lambda x, selected: print(selected) 21 | ) 22 | """ 23 | 24 | __gtype_name__ = "IgnisDropDown" 25 | __gproperties__ = {**BaseWidget.gproperties} 26 | 27 | def __init__(self, **kwargs): 28 | Gtk.DropDown.__init__(self) 29 | self._items: list[str] = [] 30 | self._on_selected: Callable | None = None 31 | BaseWidget.__init__(self, **kwargs) 32 | 33 | self.connect("notify::selected-item", self.__invoke_on_selected) 34 | 35 | @IgnisProperty 36 | def items(self) -> list[str]: 37 | """ 38 | A list of strings that can be selected in the popover. 39 | """ 40 | return self._items 41 | 42 | @items.setter 43 | def items(self, value: list[str]) -> None: 44 | self._items = value 45 | model = Gtk.StringList() 46 | for i in value: 47 | model.append(i) 48 | 49 | self.model = model 50 | 51 | @IgnisProperty 52 | def on_selected(self) -> Callable | None: 53 | """ 54 | The function to call when the user selects an item from the list. 55 | """ 56 | return self._on_selected 57 | 58 | @on_selected.setter 59 | def on_selected(self, value: Callable) -> None: 60 | self._on_selected = value 61 | 62 | def __invoke_on_selected(self, *args) -> None: 63 | if self.on_selected: 64 | self.on_selected(self, self.selected) 65 | 66 | @IgnisProperty 67 | def selected(self) -> str: 68 | """ 69 | The selected string. It is a shortcut for ``self.selected_item.props.string``. 70 | """ 71 | return self.selected_item.props.string 72 | -------------------------------------------------------------------------------- /ignis/widgets/entry.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | from collections.abc import Callable 4 | from ignis.gobject import IgnisProperty 5 | 6 | 7 | class Entry(Gtk.Entry, BaseWidget): # type: ignore 8 | """ 9 | Bases: :class:`Gtk.Entry` 10 | 11 | An input field. To make it work, set the ``kb_mode`` property of the window to ``on_demand`` or ``exclusive``. 12 | 13 | Args: 14 | **kwargs: Properties to set. 15 | 16 | .. code-block:: python 17 | 18 | Widget.Entry( 19 | placeholder="placeholder", 20 | on_accept=lambda x: print(x.text), 21 | on_change=lambda x: print(x.text), 22 | ) 23 | """ 24 | 25 | __gtype_name__ = "IgnisEntry" 26 | __gproperties__ = {**BaseWidget.gproperties} 27 | 28 | def __init__(self, **kwargs): 29 | Gtk.Entry.__init__(self) 30 | self._on_accept: Callable | None = None 31 | self._on_change: Callable | None = None 32 | BaseWidget.__init__(self, **kwargs) 33 | 34 | self.connect( 35 | "activate", lambda x: self.on_accept(x) if self.on_accept else None 36 | ) 37 | self.connect( 38 | "notify::text", lambda x, y: self.on_change(x) if self.on_change else None 39 | ) 40 | 41 | @IgnisProperty 42 | def on_accept(self) -> Callable: 43 | """ 44 | The function that will be called when the user hits the Enter key. 45 | """ 46 | return self._on_accept 47 | 48 | @on_accept.setter 49 | def on_accept(self, value: Callable) -> None: 50 | self._on_accept = value 51 | 52 | @IgnisProperty 53 | def on_change(self) -> Callable: 54 | """ 55 | The function that will be called when the text in the widget is changed (e.g., when the user types something into the entry). 56 | """ 57 | return self._on_change 58 | 59 | @on_change.setter 60 | def on_change(self, value: Callable) -> None: 61 | self._on_change = value 62 | -------------------------------------------------------------------------------- /ignis/widgets/file_filter.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.gobject import IgnisGObject 3 | from ignis.gobject import IgnisProperty 4 | 5 | 6 | class FileFilter(Gtk.FileFilter, IgnisGObject): 7 | """ 8 | Bases: :class:`Gtk.FileFilter` 9 | 10 | .. note:: 11 | This is not a regular widget. 12 | It doesn't support common widget properties and cannot be added as a child to a container. 13 | 14 | A file filter. 15 | Intended for use in :class:`~ignis.widgets.Widget.FileDialog`. 16 | Uses MIME types, `here `_ is a list of common MIME types. 17 | 18 | Args: 19 | mime_types: A list of MIME types. 20 | **kwargs: Properties to set. 21 | 22 | .. code-block :: python 23 | 24 | Widget.FileFilter( 25 | mime_types=["image/jpeg", "image/png"], 26 | default=True, 27 | name="Images JPEG/PNG", 28 | ) 29 | """ 30 | 31 | __gtype_name__ = "IgnisFileFilter" 32 | 33 | def __init__(self, mime_types: list[str], **kwargs): 34 | Gtk.FileFilter.__init__(self) 35 | self._default: bool = False 36 | self._mime_types = mime_types 37 | IgnisGObject.__init__(self, **kwargs) 38 | 39 | for i in mime_types: 40 | self.add_mime_type(i) 41 | 42 | @IgnisProperty 43 | def mime_types(self) -> list[str]: 44 | """ 45 | A list of MIME types. 46 | """ 47 | return self._mime_types 48 | 49 | @IgnisProperty 50 | def default(self) -> bool: 51 | """ 52 | Whether the filter will be selected by default. 53 | """ 54 | return self._default 55 | 56 | @default.setter 57 | def default(self, value: bool) -> None: 58 | self._default = value 59 | -------------------------------------------------------------------------------- /ignis/widgets/grid.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | from ignis.gobject import IgnisProperty 4 | 5 | 6 | class Grid(Gtk.Grid, BaseWidget): 7 | """ 8 | Bases: :class:`Gtk.Grid` 9 | 10 | A container that arranges its child widgets in rows and columns. 11 | 12 | Args: 13 | **kwargs: Properties to set. 14 | 15 | .. code-block:: python 16 | 17 | Widget.Grid( 18 | child=[Widget.Button(label=str(i)), for i in range(100)], 19 | column_num=3 20 | ) 21 | 22 | .. code-block:: python 23 | 24 | Widget.Grid( 25 | child=[Widget.Button(label=str(i)), for i in range(100)], 26 | row_num=3 27 | ) 28 | """ 29 | 30 | __gtype_name__ = "IgnisGrid" 31 | __gproperties__ = {**BaseWidget.gproperties} 32 | 33 | def __init__( 34 | self, column_num: int | None = None, row_num: int | None = None, **kwargs 35 | ): 36 | Gtk.Grid.__init__(self) 37 | self._column_num: int | None = column_num 38 | self._row_num: int | None = row_num 39 | self._child: list[Gtk.Widget] = [] 40 | BaseWidget.__init__(self, **kwargs) 41 | 42 | @IgnisProperty 43 | def column_num(self) -> int: 44 | """ 45 | The number of columns. 46 | """ 47 | return self._column_num 48 | 49 | @column_num.setter 50 | def column_num(self, value: int) -> None: 51 | self._column_num = value 52 | self.__apply() 53 | 54 | @IgnisProperty 55 | def row_num(self) -> int: 56 | """ 57 | The number of rows. This will not take effect if ``column_num`` is specified. 58 | """ 59 | return self._row_num 60 | 61 | @row_num.setter 62 | def row_num(self, value: int) -> None: 63 | self._row_num = value 64 | self.__apply() 65 | 66 | @IgnisProperty 67 | def child(self) -> list[Gtk.Widget]: 68 | """ 69 | A list of child widgets. 70 | """ 71 | return self._child 72 | 73 | @child.setter 74 | def child(self, child: list[Gtk.Widget]) -> None: 75 | for c in self._child: 76 | self.remove(c) 77 | self._child = child 78 | self.__apply() 79 | 80 | def __apply(self) -> None: 81 | if self.column_num: 82 | for i, c in enumerate(self.child): 83 | self.attach(c, i % self.column_num, i // self.column_num, 1, 1) 84 | elif self.row_num: 85 | for i, c in enumerate(self.child): 86 | self.attach(c, i // self.row_num, i % self.row_num, 1, 1) 87 | else: 88 | for c in self.child: 89 | self.attach(c, 1, 1, 1, 1) 90 | -------------------------------------------------------------------------------- /ignis/widgets/headerbar.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | 4 | 5 | class HeaderBar(Gtk.HeaderBar, BaseWidget): 6 | """ 7 | Bases: :class:`Gtk.HeaderBar` 8 | 9 | A custom title bar with decorations like a close button and title. 10 | 11 | Args: 12 | **kwargs: Properties to set. 13 | 14 | .. code-block:: python 15 | 16 | Widget.HeaderBar( 17 | show_title_buttons=True, 18 | ) 19 | """ 20 | 21 | __gtype_name__ = "IgnisHeaderBar" 22 | __gproperties__ = {**BaseWidget.gproperties} 23 | 24 | def __init__(self, **kwargs): 25 | Gtk.HeaderBar.__init__(self) 26 | BaseWidget.__init__(self, **kwargs) 27 | -------------------------------------------------------------------------------- /ignis/widgets/icon.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ignis.base_widget import BaseWidget 3 | from gi.repository import Gtk, GdkPixbuf, Gdk # type: ignore 4 | from ignis.utils import Utils 5 | from ignis.gobject import IgnisProperty 6 | 7 | 8 | class Icon(Gtk.Image, BaseWidget): 9 | """ 10 | Bases: :class:`Gtk.Image` 11 | 12 | A widget that displays images or icons in a 1:1 ratio. 13 | 14 | If you want to display an image at its native aspect ratio, see :class:`~ignis.widgets.picture.Picture`. 15 | 16 | Args: 17 | **kwargs: Properties to set. 18 | 19 | .. code-block:: python 20 | 21 | Widget.Icon( 22 | image='audio-volume-high', 23 | pixel_size=12 24 | ) 25 | 26 | """ 27 | 28 | __gtype_name__ = "IgnisIcon" 29 | __gproperties__ = {**BaseWidget.gproperties} 30 | 31 | def __init__(self, pixel_size: int = -1, **kwargs): 32 | Gtk.Image.__init__(self) 33 | self.pixel_size = pixel_size # this need to set pixel_size BEFORE image 34 | self._image: str | GdkPixbuf.Pixbuf | None = None 35 | BaseWidget.__init__(self, **kwargs) 36 | 37 | @IgnisProperty 38 | def image(self) -> "str | GdkPixbuf.Pixbuf | None": 39 | """ 40 | The icon name, path to the file, or a ``GdkPixbuf.Pixbuf``. 41 | """ 42 | return self._image 43 | 44 | @image.setter 45 | def image(self, value: "str | GdkPixbuf.Pixbuf") -> None: 46 | self._image = value 47 | 48 | pixbuf = None 49 | 50 | if isinstance(value, GdkPixbuf.Pixbuf): 51 | pixbuf = value 52 | elif isinstance(value, str): 53 | if os.path.isfile(value): 54 | pixbuf = GdkPixbuf.Pixbuf.new_from_file(value) 55 | else: 56 | self.set_from_icon_name(value) 57 | return 58 | 59 | if pixbuf is None: 60 | return 61 | 62 | if not self.pixel_size <= 0: 63 | pixbuf = Utils.scale_pixbuf(pixbuf, self.pixel_size, self.pixel_size) 64 | 65 | paintable = Gdk.Texture.new_for_pixbuf(pixbuf) 66 | self.set_from_paintable(paintable) 67 | -------------------------------------------------------------------------------- /ignis/widgets/label.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk, Pango # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | 4 | 5 | class Label(Gtk.Label, BaseWidget): 6 | """ 7 | Bases: :class:`Gtk.Label` 8 | 9 | A widget that displays a small amount of text. 10 | 11 | Overrided properties: 12 | - justify: The alignment of the lines in the text of the label relative to each other. This does NOT affect the alignment of the label within its allocation. 13 | - ellipsize: The preferred place to ellipsize the string. Default: ``none``. 14 | - wrap_mode: If ``wrap`` is set to ``True``, controls how linewrapping is done. Default: ``word``. 15 | 16 | Justify: 17 | - left: The text is placed at the left edge of the label. 18 | - right: The text is placed at the right edge of the label. 19 | - center: The text is placed in the center of the label. 20 | - fill: The text is placed is distributed across the label. 21 | 22 | Ellipsize: 23 | - none: No ellipsization. 24 | - start: Omit characters at the start of the text. 25 | - middle: Omit characters in the middle of the text. 26 | - end: Omit characters at the end of the text. 27 | 28 | Wrap mode: 29 | - word: Wrap lines at word boundaries. 30 | - char: Wrap lines at character boundaries. 31 | - word_char: Wrap lines at word boundaries, but fall back to character boundaries if there is not enough space for a full word. 32 | 33 | Args: 34 | **kwargs: Properties to set. 35 | 36 | .. code-block:: python 37 | 38 | Widget.Label( 39 | label='heh', 40 | use_markup=False, 41 | justify='left', 42 | wrap=True, 43 | wrap_mode='word', 44 | ellipsize='end', 45 | max_width_chars=52 46 | ) 47 | """ 48 | 49 | __gtype_name__ = "IgnisLabel" 50 | __gproperties__ = {**BaseWidget.gproperties} 51 | 52 | def __init__(self, **kwargs): 53 | Gtk.Label.__init__(self) 54 | self.override_enum("justify", Gtk.Justification) 55 | self.override_enum("wrap_mode", Pango.WrapMode) 56 | self.override_enum("ellipsize", Pango.EllipsizeMode) 57 | BaseWidget.__init__(self, **kwargs) 58 | -------------------------------------------------------------------------------- /ignis/widgets/listboxrow.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | from collections.abc import Callable 4 | from ignis.gobject import IgnisProperty 5 | 6 | 7 | class ListBoxRow(Gtk.ListBoxRow, BaseWidget): 8 | """ 9 | Bases: :class:`Gtk.ListBoxRow` 10 | 11 | A row for :class:`~ignis.widgets.listbox.ListBox`. 12 | 13 | Args: 14 | **kwargs: Properties to set. 15 | 16 | .. code-block:: python 17 | 18 | Widget.ListBoxRow( 19 | child=Widget.Label(label="row 1"), 20 | on_activate=lambda x: print("selected row 1"), 21 | selected=True 22 | ) 23 | """ 24 | 25 | __gtype_name__ = "IgnisListBoxRow" 26 | __gproperties__ = {**BaseWidget.gproperties} 27 | 28 | def __init__(self, **kwargs): 29 | Gtk.ListBoxRow.__init__(self) 30 | self._on_activate: Callable | None = None 31 | self._selected: bool = False 32 | BaseWidget.__init__(self, **kwargs) 33 | 34 | @IgnisProperty 35 | def on_activate(self) -> Callable: 36 | """ 37 | The function to call when the user selects the row. 38 | """ 39 | return self._on_activate 40 | 41 | @on_activate.setter 42 | def on_activate(self, value: Callable) -> None: 43 | self._on_activate = value 44 | 45 | @IgnisProperty 46 | def selected(self) -> bool: 47 | """ 48 | Whether the row is selected by default. 49 | """ 50 | return self._selected 51 | 52 | @selected.setter 53 | def selected(self, value: bool) -> None: 54 | self._selected = value 55 | -------------------------------------------------------------------------------- /ignis/widgets/overlay.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | from ignis.gobject import IgnisProperty 4 | 5 | 6 | class Overlay(Gtk.Overlay, BaseWidget): 7 | """ 8 | Bases: :class:`Gtk.Overlay` 9 | 10 | A container that places its children on top of each other. 11 | The ``child`` property is the main child, on which other widgets defined in ``overlays`` will be placed on top. 12 | 13 | Args: 14 | **kwargs: Properties to set. 15 | 16 | .. code-block:: python 17 | 18 | Widget.Overlay( 19 | child=Widget.Label(label="This is the main child"), 20 | overlays=[ 21 | Widget.Label(label="Overlay child 1"), 22 | Widget.Label(label="Overlay child 2"), 23 | Widget.Label(label="Overlay child 3"), 24 | ] 25 | ) 26 | """ 27 | 28 | __gtype_name__ = "IgnisOverlay" 29 | __gproperties__ = {**BaseWidget.gproperties} 30 | 31 | def __init__(self, **kwargs): 32 | Gtk.Overlay.__init__(self) 33 | self._overlays: list[Gtk.Widget] = [] 34 | BaseWidget.__init__(self, **kwargs) 35 | 36 | @IgnisProperty 37 | def overlays(self) -> list[Gtk.Widget]: 38 | """ 39 | A list of overlay widgets. 40 | """ 41 | return self._overlays 42 | 43 | @overlays.setter 44 | def overlays(self, value: list[Gtk.Widget]) -> None: 45 | for i in self._overlays: 46 | self.remove_overlay(i) 47 | 48 | self._overlays = value 49 | 50 | for i in value: 51 | self.add_overlay(i) 52 | -------------------------------------------------------------------------------- /ignis/widgets/popover_menu.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | from ignis.gobject import IgnisProperty 4 | from ignis.menu_model import IgnisMenuModel 5 | 6 | 7 | class PopoverMenu(Gtk.PopoverMenu, BaseWidget): 8 | """ 9 | Bases: :class:`Gtk.PopoverMenu` 10 | 11 | A dropdown menu. 12 | It must be added as a child to a container. 13 | To display it, call the ``popup()`` method. 14 | 15 | .. note:: 16 | The Popover Menu points to the widget to which it was added. 17 | 18 | Args: 19 | **kwargs: Properties to set. 20 | 21 | .. code-block:: python 22 | 23 | from ignis.menu_model import IgnisMenuModel, IgnisMenuItem, IgnisMenuSeparator 24 | 25 | Widget.PopoverMenu( 26 | model=IgnisMenuModel( 27 | IgnisMenuItem( 28 | label="Just item", 29 | on_activate=lambda x: print("item activated!"), 30 | ), 31 | IgnisMenuItem( 32 | label="This is disabled item", 33 | enabled=False, 34 | on_activate=lambda x: print( 35 | "you will not see this message in terminal hehehehehe" 36 | ), 37 | ), 38 | IgnisMenuModel( 39 | *( # unpacking because items must be passed as *args 40 | IgnisMenuItem( 41 | label=str(i), 42 | on_activate=lambda x, i=i: print(f"Clicked on item {i}!"), 43 | ) 44 | for i in range(10) 45 | ), 46 | label="Submenu", # pass label as keyword argument 47 | ), 48 | ), 49 | ) 50 | """ 51 | 52 | __gtype_name__ = "IgnisPopoverMenu" 53 | __gproperties__ = {**BaseWidget.gproperties} 54 | 55 | def __init__(self, **kwargs): 56 | Gtk.PopoverMenu.__init__(self) 57 | self._model: IgnisMenuModel | None = None 58 | BaseWidget.__init__(self, visible=False, **kwargs) 59 | 60 | @IgnisProperty 61 | def model(self) -> IgnisMenuModel | None: 62 | """ 63 | A menu model. 64 | """ 65 | return self._model 66 | 67 | @model.setter 68 | def model(self, value: IgnisMenuModel) -> None: 69 | if self._model: 70 | self._model.clean_gmenu() 71 | 72 | self._model = value 73 | self.set_menu_model(value.gmenu) 74 | 75 | def __del__(self) -> None: 76 | if self._model: 77 | self._model.clean_gmenu() 78 | self._model = None 79 | -------------------------------------------------------------------------------- /ignis/widgets/regular_window.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | from ignis.app import IgnisApp 4 | from ignis.exceptions import WindowNotFoundError 5 | from ignis.gobject import IgnisProperty 6 | 7 | app = IgnisApp.get_default() 8 | 9 | 10 | class RegularWindow(Gtk.Window, BaseWidget): 11 | """ 12 | Bases: :class:`Gtk.Window` 13 | 14 | A standart application window. 15 | 16 | Args: 17 | namespace: The name of the window, used for accessing it from the CLI and :class:`~ignis.app.IgnisApp`. It must be unique. 18 | **kwargs: Properties to set. 19 | 20 | .. code-block:: python 21 | 22 | Widget.RegularWindow( 23 | child=Widget.Label(label="this is regular window"), 24 | title="This is title", 25 | namespace='some-regular-window', 26 | titlebar=Widget.HeaderBar(show_title_buttons=True), 27 | ) 28 | """ 29 | 30 | __gtype_name__ = "IgnisRegularWindow" 31 | __gproperties__ = {**BaseWidget.gproperties} 32 | 33 | def __init__(self, namespace: str, **kwargs): 34 | Gtk.Window.__init__(self) 35 | BaseWidget.__init__(self, **kwargs) 36 | 37 | self._namespace = namespace 38 | 39 | app.add_window(namespace, self) 40 | 41 | self.connect("close-request", self.__on_close_request) 42 | 43 | @IgnisProperty 44 | def namespace(self) -> str: 45 | """ 46 | The name of the window, used for accessing it from the CLI and :class:`~ignis.app.IgnisApp`. 47 | It must be unique. 48 | """ 49 | return self._namespace 50 | 51 | def __remove(self, *args) -> None: 52 | try: 53 | app.remove_window(self.namespace) 54 | except WindowNotFoundError: 55 | pass 56 | 57 | def __on_close_request(self, *args) -> None: 58 | if not self.props.hide_on_close: 59 | self.__remove() 60 | 61 | def destroy(self): 62 | self.__remove() 63 | return super().destroy() 64 | 65 | def unrealize(self): 66 | self.__remove() 67 | return super().unrealize() 68 | -------------------------------------------------------------------------------- /ignis/widgets/revealer.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | 4 | 5 | class Revealer(Gtk.Revealer, BaseWidget): 6 | """ 7 | Bases: :class:`Gtk.Revealer` 8 | 9 | A container that animates the transition of its child from invisible to visible. 10 | 11 | Overrided properties: 12 | - transition_type: The type of transition. Default: ``slide_down``. 13 | 14 | Transition type: 15 | - none: No transition. 16 | - crossfade: Fade in. 17 | - slide_right: Slide in from the left. 18 | - slide_left: Slide in from the right. 19 | - slide_up: Slide in from the bottom. 20 | - slide_down: Slide in from the top. 21 | - swing_right: Floop in from the left 22 | - swing_left: Floop in from the right 23 | - swing_up: Floop in from the bottom 24 | - swing_down: Floop in from the top 25 | 26 | Args: 27 | **kwargs: Properties to set. 28 | 29 | .. code-block:: python 30 | 31 | Widget.Revealer( 32 | child=Widget.Label(label='animation!!!'), 33 | transition_type='slideright', 34 | transition_duration=500, 35 | reveal_child=True, # Whether child is revealed. 36 | ) 37 | """ 38 | 39 | __gtype_name__ = "IgnisRevealer" 40 | __gproperties__ = {**BaseWidget.gproperties} 41 | 42 | def __init__(self, **kwargs): 43 | Gtk.Revealer.__init__(self) 44 | self.override_enum("transition_type", Gtk.RevealerTransitionType) 45 | BaseWidget.__init__(self, **kwargs) 46 | 47 | def toggle(self): 48 | if self.get_reveal_child(): 49 | self.set_reveal_child(False) 50 | else: 51 | self.set_reveal_child(True) 52 | -------------------------------------------------------------------------------- /ignis/widgets/revealer_window.py: -------------------------------------------------------------------------------- 1 | from .window import Window 2 | from .revealer import Revealer 3 | from ignis.utils import Utils 4 | from typing import Any 5 | from ignis.gobject import IgnisProperty 6 | 7 | 8 | class RevealerWindow(Window): 9 | """ 10 | Bases: :class:`~ignis.widgets.Widget.Window` 11 | 12 | A window with animation. 13 | 14 | Args: 15 | revealer: An instance of :class:`~ignis.widgets.Widget.Revealer`. 16 | **kwargs: Properties to set. 17 | 18 | .. warning:: 19 | Do not set ``Widget.Revealer`` as a direct child, 20 | as this can lead to various graphical bugs. 21 | Instead, place `Widget.Revealer` inside a container (e.g., `Widget.Box`) and then set the container as a child. 22 | 23 | Example usage: 24 | 25 | .. code-block:: python 26 | 27 | from ignis.widgets import Widget 28 | 29 | revealer = Widget.Revealer( 30 | transition_type="slide_left", 31 | child=Widget.Button(label="test"), 32 | transition_duration=300, 33 | reveal_child=True, 34 | ) 35 | 36 | box = Widget.Box(child=[revealer]) 37 | 38 | Widget.RevealerWindow( 39 | visible=False, 40 | popup=True, 41 | layer="top", 42 | namespace="revealer-window", 43 | child=box, # do not set Widget.Revealer as a direct child! 44 | revealer=revealer, 45 | ) 46 | 47 | """ 48 | 49 | def __init__(self, revealer: Revealer, **kwargs) -> None: 50 | self._revealer = revealer 51 | super().__init__(**kwargs) 52 | 53 | def set_property(self, prop_name: str, value: Any) -> None: 54 | if prop_name == "visible": 55 | if value: 56 | super().set_property(prop_name, value) 57 | else: 58 | Utils.Timeout( 59 | ms=self._revealer.transition_duration, 60 | target=lambda x=super(): x.set_property(prop_name, value), 61 | ) 62 | self._revealer.reveal_child = value 63 | self.notify("visible") 64 | else: 65 | super().set_property(prop_name, value) 66 | 67 | @IgnisProperty 68 | def visible(self) -> bool: 69 | return self._revealer.reveal_child 70 | 71 | @visible.setter 72 | def visible(self, value: bool) -> None: 73 | super().set_visible(value) 74 | 75 | @IgnisProperty 76 | def revealer(self) -> Revealer: 77 | """ 78 | An instance of :class:`~ignis.widgets.Widget.Revealer`. 79 | """ 80 | return self._revealer 81 | 82 | @revealer.setter 83 | def revealer(self, value: Revealer) -> None: 84 | self._revealer = value 85 | -------------------------------------------------------------------------------- /ignis/widgets/scroll.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | 4 | 5 | class Scroll(Gtk.ScrolledWindow, BaseWidget): 6 | """ 7 | Bases: :class:`Gtk.ScrolledWindow` 8 | 9 | A container that accepts a single child widget and makes it scrollable. 10 | 11 | Args: 12 | **kwargs: Properties to set. 13 | 14 | .. code-block:: python 15 | 16 | Widget.Scroll( 17 | child=Widget.Box( 18 | vertical=True, 19 | child=[Widget.Label(i) for i in range(30)] 20 | ) 21 | ) 22 | """ 23 | 24 | __gtype_name__ = "IgnisScroll" 25 | __gproperties__ = {**BaseWidget.gproperties} 26 | 27 | def __init__(self, **kwargs): 28 | Gtk.ScrolledWindow.__init__(self) 29 | self.override_enum("hscrollbar_policy", Gtk.PolicyType) 30 | self.override_enum("vscrollbar_policy", Gtk.PolicyType) 31 | BaseWidget.__init__(self, **kwargs) 32 | -------------------------------------------------------------------------------- /ignis/widgets/separator.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | from ignis.gobject import IgnisProperty 4 | 5 | 6 | class Separator(Gtk.Separator, BaseWidget): 7 | """ 8 | Bases: :class:`Gtk.Separator` 9 | 10 | A separator widget. 11 | 12 | Args: 13 | **kwargs: Properties to set. 14 | 15 | .. code-block:: python 16 | 17 | Widget.Separator( 18 | vertical=False, 19 | ) 20 | """ 21 | 22 | __gtype_name__ = "IgnisSeparator" 23 | __gproperties__ = {**BaseWidget.gproperties} 24 | 25 | def __init__(self, **kwargs): 26 | Gtk.Separator.__init__(self) 27 | BaseWidget.__init__(self, **kwargs) 28 | 29 | @IgnisProperty 30 | def vertical(self) -> bool: 31 | """ 32 | Whether the separator is vertical. 33 | """ 34 | return self.get_orientation() == Gtk.Orientation.VERTICAL 35 | 36 | @vertical.setter 37 | def vertical(self, value: bool) -> None: 38 | if value: 39 | self.set_property("orientation", Gtk.Orientation.VERTICAL) 40 | else: 41 | self.set_property("orientation", Gtk.Orientation.HORIZONTAL) 42 | -------------------------------------------------------------------------------- /ignis/widgets/spin_button.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | from collections.abc import Callable 4 | from ignis.gobject import IgnisProperty 5 | 6 | 7 | class SpinButton(Gtk.SpinButton, BaseWidget): # type: ignore 8 | """ 9 | Bases: :class:`Gtk.SpinButton` 10 | 11 | A widget that allows the user to increment or decrement the displayed value within a specified range. 12 | 13 | Args: 14 | **kwargs: Properties to set. 15 | 16 | .. code-block:: python 17 | 18 | Widget.SpinButton( 19 | min=0, 20 | max=100, 21 | step=1, 22 | value=50, 23 | on_change=lambda x, value: print(value) 24 | ) 25 | """ 26 | 27 | __gtype_name__ = "IgnisSpinButton" 28 | __gproperties__ = {**BaseWidget.gproperties} 29 | 30 | def __init__(self, min: int | None = None, max: int | None = None, **kwargs): 31 | Gtk.SpinButton.__init__(self) 32 | self._on_change: Callable | None = None 33 | self.adjustment = Gtk.Adjustment( 34 | value=0, lower=0, upper=100, step_increment=1, page_increment=0, page_size=0 35 | ) 36 | self.min = min 37 | self.max = max 38 | BaseWidget.__init__(self, **kwargs) 39 | 40 | self.connect("value-changed", self.__invoke_on_change) 41 | 42 | @IgnisProperty 43 | def value(self) -> float: 44 | """ 45 | The current value. 46 | """ 47 | return super().get_value() 48 | 49 | @value.setter 50 | def value(self, value: float) -> None: 51 | self.adjustment.set_value(value) 52 | 53 | @IgnisProperty 54 | def min(self) -> float: 55 | """ 56 | Minimum value. 57 | """ 58 | return self.adjustment.props.lower 59 | 60 | @min.setter 61 | def min(self, value: float) -> None: 62 | self.adjustment.props.lower = value 63 | 64 | @IgnisProperty 65 | def max(self) -> float: 66 | """ 67 | Maximum value. 68 | """ 69 | return self.adjustment.props.upper 70 | 71 | @max.setter 72 | def max(self, value: float) -> None: 73 | self.adjustment.props.upper = value 74 | 75 | @IgnisProperty 76 | def step(self) -> float: 77 | """ 78 | Step increment. 79 | """ 80 | return self.adjustment.props.step_increment 81 | 82 | @step.setter 83 | def step(self, value: float) -> None: 84 | self.adjustment.props.step_increment = value 85 | 86 | @IgnisProperty 87 | def on_change(self) -> Callable | None: 88 | """ 89 | The function to call when the value changes. 90 | """ 91 | return self._on_change 92 | 93 | @on_change.setter 94 | def on_change(self, value: Callable) -> None: 95 | self._on_change = value 96 | 97 | def __invoke_on_change(self, *args) -> None: 98 | if self.on_change: 99 | self.on_change(self, self.value) 100 | -------------------------------------------------------------------------------- /ignis/widgets/stack.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | from .stack_page import StackPage 4 | from ignis.gobject import IgnisProperty 5 | 6 | 7 | class Stack(Gtk.Stack, BaseWidget): 8 | """ 9 | Bases: :class:`Gtk.Stack` 10 | 11 | Stack is a container which only shows one of its children at a time. 12 | 13 | It does not provide a means for users to change the visible child. 14 | Instead, a separate widget such as :class:`~ignis.widgets.Widget.StackSwitcher` can be used with Stack to provide this functionality. 15 | 16 | Args: 17 | **kwargs: Properties to set. 18 | 19 | Overrided properties: 20 | - transition_type: The type of animation used to transition between pages. Available values: :class:`Gtk.StackTransitionType`. 21 | 22 | .. code-block:: python 23 | 24 | from ignis.widgets import Widget 25 | 26 | stack = Widget.Stack( 27 | child=[ 28 | Widget.StackPage( 29 | title="page 1", child=Widget.Label(label="welcome to page 1!") 30 | ), 31 | Widget.StackPage( 32 | title="page 2", child=Widget.Label(label="welcome to page 2!") 33 | ), 34 | Widget.StackPage( 35 | title="page 3", child=Widget.Label(label="welcome to page 3!") 36 | ), 37 | ] 38 | ) 39 | 40 | Widget.Box( 41 | vertical=True, 42 | # you should add both StackSwitcher and Stack. 43 | child=[Widget.StackSwitcher(stack=stack), stack], 44 | ) 45 | """ 46 | 47 | __gtype_name__ = "IgnisStack" 48 | __gproperties__ = {**BaseWidget.gproperties} 49 | 50 | def __init__(self, **kwargs): 51 | Gtk.Stack.__init__(self) 52 | self.override_enum("transition_type", Gtk.StackTransitionType) 53 | self._child: list[StackPage] = [] 54 | BaseWidget.__init__(self, **kwargs) 55 | 56 | @IgnisProperty 57 | def child(self) -> list[StackPage]: 58 | """ 59 | A list of pages. 60 | """ 61 | return self._child 62 | 63 | @child.setter 64 | def child(self, value: list[StackPage]) -> None: 65 | for i in self._child: 66 | self.remove(i.child) 67 | 68 | for i in value: 69 | self.add_titled(i.child, None, i.title) 70 | -------------------------------------------------------------------------------- /ignis/widgets/stack_page.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.gobject import IgnisGObject 3 | from ignis.gobject import IgnisProperty 4 | 5 | 6 | class StackPage(IgnisGObject): 7 | """ 8 | Bases: :class:`~ignis.gobject.IgnisGObject` 9 | 10 | Intented to use with :class:`~ignis.widgets.Widget.Stack`. 11 | 12 | Args: 13 | title: The title. It will be used by :class:`~ignis.widgets.Widget.StackSwitcher` to display :attr:`child` in a tab bar. 14 | child: The child widget. 15 | 16 | .. warning:: 17 | It is not a widget. 18 | """ 19 | 20 | def __init__(self, title: str, child: Gtk.Widget): 21 | super().__init__() 22 | self._title = title 23 | self._child = child 24 | 25 | @IgnisProperty 26 | def title(self) -> str: 27 | """ 28 | The title. 29 | It will be used by :class:`~ignis.widgets.Widget.StackSwitcher` to display :attr:`child` in a tab bar. 30 | """ 31 | return self._title 32 | 33 | @IgnisProperty 34 | def child(self) -> Gtk.Widget: 35 | """ 36 | The child widget. 37 | """ 38 | return self._child 39 | -------------------------------------------------------------------------------- /ignis/widgets/stack_switcher.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | 4 | 5 | class StackSwitcher(Gtk.StackSwitcher, BaseWidget): 6 | """ 7 | Bases: :class:`Gtk.StackSwitcher` 8 | 9 | The StackSwitcher shows a row of buttons to switch between :class:`~ignis.widgets.Widget.Stack` pages. 10 | 11 | Args: 12 | **kwargs: Properties to set. 13 | """ 14 | 15 | __gtype_name__ = "IgnisStackSwitcher" 16 | __gproperties__ = {**BaseWidget.gproperties} 17 | 18 | def __init__(self, **kwargs): 19 | Gtk.StackSwitcher.__init__(self) 20 | BaseWidget.__init__(self, **kwargs) 21 | -------------------------------------------------------------------------------- /ignis/widgets/switch.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | from typing import Any 4 | from collections.abc import Callable 5 | from ignis.gobject import IgnisProperty 6 | 7 | 8 | class Switch(Gtk.Switch, BaseWidget): 9 | """ 10 | Bases: :class:`Gtk.Switch` 11 | 12 | A switch widget. 13 | 14 | Args: 15 | **kwargs: Properties to set. 16 | 17 | .. code-block:: python 18 | 19 | Widget.Switch( 20 | active=True, 21 | on_change=lambda x, active: print(active), 22 | ) 23 | """ 24 | 25 | __gtype_name__ = "IgnisSwitch" 26 | __gproperties__ = {**BaseWidget.gproperties} 27 | 28 | def __init__(self, **kwargs): 29 | Gtk.Switch.__init__(self) 30 | self._on_change: Callable | None = None 31 | self._can_activate: bool = True 32 | BaseWidget.__init__(self, **kwargs) 33 | 34 | self.connect("state-set", self.__invoke_on_change) 35 | 36 | @IgnisProperty 37 | def on_change(self) -> Callable | None: 38 | """ 39 | The function to call when the position of the switch changes (e.g., when the user toggles the switch). 40 | """ 41 | return self._on_change 42 | 43 | @on_change.setter 44 | def on_change(self, value: Callable) -> None: 45 | self._on_change = value 46 | 47 | def __invoke_on_change(self, *args) -> None: 48 | if self._can_activate and self.on_change: 49 | self.on_change(self, self.active) 50 | 51 | def set_property(self, property_name: str, value: Any) -> None: 52 | if property_name == "active": 53 | self._can_activate = False 54 | super().set_property(property_name, value) 55 | self._can_activate = True 56 | else: 57 | super().set_property(property_name, value) 58 | -------------------------------------------------------------------------------- /ignis/widgets/toggle_button.py: -------------------------------------------------------------------------------- 1 | from gi.repository import Gtk # type: ignore 2 | from ignis.base_widget import BaseWidget 3 | from collections.abc import Callable 4 | from ignis.gobject import IgnisProperty 5 | 6 | 7 | class ToggleButton(Gtk.ToggleButton, BaseWidget): 8 | """ 9 | Bases: :class:`Gtk.ToggleButton` 10 | 11 | A toggle button widget. 12 | 13 | Args: 14 | **kwargs: Properties to set. 15 | 16 | .. code-block:: python 17 | 18 | Widget.ToggleButton( 19 | on_toggled=lambda x, active: print(active) 20 | ) 21 | """ 22 | 23 | __gtype_name__ = "IgnisToggleButton" 24 | __gproperties__ = {**BaseWidget.gproperties} 25 | 26 | def __init__(self, **kwargs) -> None: 27 | Gtk.ToggleButton.__init__(self) 28 | self._on_toggled: Callable | None = None 29 | BaseWidget.__init__(self, **kwargs) 30 | 31 | self.connect( 32 | "toggled", 33 | lambda x: self.on_toggled(self, self.active) if self.on_toggled else None, 34 | ) 35 | 36 | @IgnisProperty 37 | def on_toggled(self) -> Callable | None: 38 | """ 39 | The function to call when the button is toggled by the user. 40 | """ 41 | return self._on_toggled 42 | 43 | @on_toggled.setter 44 | def on_toggled(self, value: Callable | None) -> None: 45 | self._on_toggled = value 46 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'ignis', 3 | license: 'LGPL-2.1-or-later', 4 | version: run_command(['tools/get_version.py'], check: true).stdout().strip(), 5 | default_options: ['warning_level=2', 'werror=false'] 6 | ) 7 | 8 | 9 | # Find Python installation 10 | python = import('python').find_installation(pure: false) 11 | 12 | # Set folders 13 | bindir = get_option('bindir') 14 | pylibdir = python.get_install_dir() 15 | pkginstalldir = join_paths(pylibdir, meson.project_name()) 16 | 17 | # Dependencies 18 | if get_option('dependency_check') 19 | dependency('glib-2.0') 20 | dependency('gobject-introspection-1.0') 21 | dependency('gio-2.0') 22 | dependency('gtk4') 23 | dependency('gtk4-layer-shell-0') 24 | endif 25 | 26 | # gvc 27 | if get_option('build_gvc') 28 | subproject('gvc', 29 | default_options: [ 30 | 'package_name=' + meson.project_name(), 31 | 'static=false', 32 | 'introspection=true', 33 | 'alsa=false' 34 | ] 35 | ) 36 | endif 37 | 38 | # Do installation 39 | install_subdir( 40 | 'ignis', 41 | install_dir: pylibdir, 42 | ) 43 | 44 | config = configuration_data() 45 | 46 | if get_option('COMMITHASH') != '' 47 | commit_hash = get_option('COMMITHASH') 48 | else 49 | commit_hash = run_command('git', 'rev-parse', 'HEAD', check: false).stdout().strip() 50 | endif 51 | 52 | config.set('COMMIT', commit_hash) 53 | config.set('BRANCH', run_command('git', 'branch', '--show-current', check: false).stdout().strip()) 54 | config.set('COMMIT_MSG', run_command('git', 'log', '-1', '--pretty=%B', check: false).stdout().strip().replace('\\', '\\\\').replace('"', '\\"')) 55 | 56 | install_data( 57 | files('bin/ignis'), 58 | install_dir: bindir, 59 | install_mode: 'rwxr-xr-x', 60 | ) 61 | 62 | configure_file( 63 | input: 'ignis/__commit__.py.in', 64 | output: '__commit__.py', 65 | configuration: config, 66 | install_dir: pkginstalldir 67 | ) 68 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option( 2 | 'build_gvc', 3 | type: 'boolean', 4 | value: true, 5 | description: 'Build gnome-volume-control (required for Audio Service).', 6 | ) 7 | 8 | option( 9 | 'dependency_check', 10 | type: 'boolean', 11 | value: true, 12 | description: 'Check required dependencies.', 13 | ) 14 | 15 | option( 16 | 'COMMITHASH', 17 | type: 'string', 18 | value: '', 19 | description: 'NOT PUBLIC OPTION: custom commit hash to configure ignis/__commit__.py.in' 20 | ) 21 | -------------------------------------------------------------------------------- /nix/default.nix: -------------------------------------------------------------------------------- 1 | { self 2 | , lib 3 | , wrapGAppsHook4 4 | , pkg-config 5 | , meson 6 | , ninja 7 | , git 8 | , glib 9 | , gtk4 10 | , gtk4-layer-shell 11 | , gobject-introspection 12 | , librsvg 13 | , dart-sass 14 | , libpulseaudio 15 | , pipewire 16 | , networkmanager 17 | , gnome-bluetooth 18 | , python312Packages 19 | , gst_all_1 20 | , gvc 21 | , extraPackages ? [] 22 | , version ? "git" 23 | }: 24 | let 25 | inherit (lib) 26 | licenses 27 | platforms 28 | ; 29 | inherit (python312Packages) 30 | buildPythonPackage 31 | pygobject3 32 | pycairo 33 | typer 34 | loguru 35 | rich 36 | ; 37 | inherit (gst_all_1) 38 | gstreamer 39 | gst-plugins-base 40 | gst-plugins-good 41 | gst-plugins-bad 42 | gst-plugins-ugly 43 | ; 44 | in buildPythonPackage { 45 | 46 | inherit version; 47 | pname = "ignis"; 48 | src = "${self}"; 49 | 50 | format = "other"; 51 | 52 | nativeBuildInputs = [ 53 | pkg-config 54 | meson 55 | ninja 56 | git 57 | gobject-introspection 58 | wrapGAppsHook4 59 | ]; 60 | 61 | dependencies = extraPackages ++ [ 62 | glib 63 | gtk4 64 | gtk4-layer-shell 65 | gobject-introspection 66 | dart-sass 67 | gstreamer 68 | gst-plugins-base 69 | gst-plugins-good 70 | gst-plugins-bad 71 | gst-plugins-ugly 72 | librsvg 73 | libpulseaudio 74 | pipewire 75 | networkmanager 76 | gnome-bluetooth 77 | 78 | pygobject3 79 | pycairo 80 | typer 81 | loguru 82 | rich 83 | ]; 84 | 85 | patchPhase = '' 86 | mkdir -p ./subprojects/gvc 87 | cp -r ${gvc}/* ./subprojects/gvc 88 | ''; 89 | 90 | mesonFlags = [ 91 | "-DCOMMITHASH=${self.rev or "dirty"}" 92 | ]; 93 | 94 | #? avoid double wrapping. we manually pass args to wrapper 95 | dontWrapGApps = true; 96 | preFixup = '' 97 | makeWrapperArgs+=( 98 | "''${gappsWrapperArgs[@]}" 99 | --set LD_LIBRARY_PATH "$out/lib:${gtk4-layer-shell}/lib:$LD_LIBRARY_PATH" 100 | ) 101 | ''; 102 | 103 | meta = { 104 | description = '' 105 | A widget framework for building desktop shells, 106 | written and configurable in Python. 107 | ''; 108 | homepage = "https://github.com/ignis-sh/ignis"; 109 | changelog = "https://github.com/ignis-sh/ignis/releases"; 110 | license = licenses.lgpl21Plus; 111 | platforms = platforms.linux; 112 | maintainers = [ ]; 113 | mainProgram = "ignis"; 114 | }; 115 | } -------------------------------------------------------------------------------- /nix/version.nix: -------------------------------------------------------------------------------- 1 | { self }: 2 | 3 | let 4 | initPy = builtins.readFile ../ignis/__init__.py; 5 | version = builtins.elemAt (builtins.match ".*__version__ = \"([^\"]*?)\".*" initPy) 0; 6 | date = builtins.substring 0 8 (self.lastModifiedDate or "19700101"); 7 | in 8 | "${version}+date=${date}_${self.shortRev or "dirty"}" 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = 'ignis' 3 | description = 'A widget framework for building desktop shells, written and configurable in Python' 4 | readme = 'README.md' 5 | license = { text = 'LGPL-2.1-or-later' } 6 | authors = [{ name = 'linkfrg' }] 7 | dynamic = ['version'] 8 | requires-python = ">=3.11" 9 | dependencies = [ 10 | "typer>=0.15.2", 11 | "pycairo>=1.26.1", 12 | "PyGObject>=3.50.0", 13 | "loguru>=0.7.2", 14 | "rich>=13.9.4", 15 | ] 16 | 17 | [project.urls] 18 | Homepage = "https://ignis-sh.github.io/ignis" 19 | Documentation = "https://ignis-sh.github.io/ignis" 20 | Repository = "https://github.com/ignis-sh/ignis" 21 | Issues = "https://github.com/ignis-sh/ignis/issues" 22 | 23 | [build-system] 24 | build-backend = 'mesonpy' 25 | requires = ['meson-python', 'setuptools'] 26 | 27 | [tool.mypy] 28 | python_version = "3.10" 29 | packages = ["ignis", "examples"] 30 | exclude = ["venv"] 31 | disable_error_code = [ 32 | "no-redef", # allow variable redefinition (needed for GObject.Property decorator) 33 | "method-assign", # also needed for GObject.Property 34 | ] 35 | check_untyped_defs = true 36 | 37 | [[tool.mypy.overrides]] 38 | module = ["gi.repository.*"] 39 | disable_error_code = ["assignment"] 40 | 41 | [tool.ruff] 42 | include = ["ignis/**/*.py", "examples/**/*.py"] 43 | 44 | [tool.ruff.lint] 45 | select = [ 46 | "F", # pyflakes 47 | "E", # pycodestyle errors 48 | "W", # pycodestyle warnings 49 | "I", # isort 50 | "UP", # pyupgrade 51 | "B", # flake8-bugbear 52 | "C4", # flake8-comprehensions 53 | ] 54 | ignore = [ 55 | "E501", # line too long, handled by black 56 | "B008", # do not perform function calls in argument defaults 57 | "C901", # too complex 58 | "W191", # indentation contains tabs 59 | "I001", # import block is un-sorted or un-formatted 60 | ] 61 | 62 | fixable = ["ALL"] 63 | unfixable = [] 64 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | typer>=0.15.2 2 | pycairo>=1.26.1 3 | PyGObject>=3.50.0 4 | loguru>=0.7.2 5 | rich>=13.9.4 -------------------------------------------------------------------------------- /subprojects/gvc.wrap: -------------------------------------------------------------------------------- 1 | [wrap-git] 2 | url = https://github.com/ignis-sh/libgnome-volume-control-wheel.git 3 | revision = main -------------------------------------------------------------------------------- /tools/get_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Extract version number from __init__.py""" 3 | import os 4 | 5 | init_py = os.path.join(os.path.dirname(__file__), "../ignis/__init__.py") 6 | 7 | data = open(init_py).readlines() 8 | version_line = next(line for line in data if line.startswith("__version__ =")) 9 | 10 | version = version_line.strip().split(" = ")[1].replace('"', "").replace("'", "") 11 | 12 | print(version) 13 | --------------------------------------------------------------------------------