├── .devcontainer
├── devcontainer.json
└── on_create.sh
├── .github
├── actions
│ └── prepare-environment
│ │ └── action.yml
├── pull_request_template.md
└── workflows
│ └── CI.yml
├── .gitignore
├── CONTRIBUTING.md
├── COPYING
├── Makefile
├── Pipfile
├── Pipfile.lock
├── README.md
├── build_helper
└── flatpak-pip-generator.py
├── data
├── icons
│ ├── hicolor
│ │ ├── scalable
│ │ │ └── apps
│ │ │ │ └── io.github.jorchube.monitorets.svg
│ │ └── symbolic
│ │ │ └── apps
│ │ │ └── io.github.jorchube.monitorets-symbolic.svg
│ └── meson.build
├── io.github.jorchube.monitorets.appdata.xml.in
├── io.github.jorchube.monitorets.desktop.in
├── io.github.jorchube.monitorets.gschema.xml
└── meson.build
├── docs
└── index.md
├── imgs
├── 2.png
├── 4.png
├── adaptable.png
├── configurable.png
├── dark_large.png
├── images.xcf
├── layouts.png
├── layouts_large.png
├── light_window.png
├── logo.svg
├── logo_old.svg
├── main.png
├── preferences_appearance.png
├── preferences_large.png
├── preferences_monitors.png
├── preferences_monitors_1.png
├── preferences_monitors_2.png
└── themeable.png
├── io.github.jorchube.monitorets.json
├── meson.build
├── po
├── LINGUAS
├── POTFILES
├── de.po
├── es.po
├── fr.po
├── meson.build
├── monitorets.pot
├── nl.po
└── tr.po
├── pypi-dependencies.json
└── src
├── __init__.py
├── controller.py
├── discover_temperature_monitors.py
├── event_broker.py
├── events.py
├── gtk
├── help-overlay.ui
├── icons
│ ├── dark.png
│ ├── horizontal.png
│ ├── layout_toggle_icons.xcf
│ ├── light.png
│ ├── system.png
│ ├── theme_toggle_icons.xcf
│ └── vertical.png
├── main-menu-model.ui
├── preferences-page-appearance.ui
├── preferences-page-monitors.ui
├── preferences-window.ui
├── rename-monitor-popover.ui
├── single-window.ui
├── tips-window.ui
└── ui.cmb
├── layout.py
├── main.py
├── meson.build
├── monitor_descriptors.py
├── monitor_redraw_frequency_seconds_values.py
├── monitor_type.py
├── monitorets.gresource.xml
├── monitorets.in
├── monitors
├── __init__.py
├── cpu_monitor.py
├── cpu_per_core_monitor.py
├── cpu_pressure_monitor.py
├── downlink_monitor.py
├── gpu_monitor.py
├── home_usage_monitor.py
├── io_pressure_monitor.py
├── memory_monitor.py
├── memory_pressure_monitor.py
├── monitor.py
├── root_usage_monitor.py
├── swap_monitor.py
├── temperature_monitor.py
└── uplink_monitor.py
├── network_monitor_scale_manager.py
├── preference_keys.py
├── preferences.py
├── samplers
├── __init__.py
├── cpu_per_core_sampler.py
├── cpu_pressure_sampler.py
├── cpu_sampler.py
├── delta_sampler.py
├── disk_usage_sampler.py
├── downlink_sampler.py
├── gpu_sampler.py
├── io_pressure_sampler.py
├── memory_pressure_sampler.py
├── memory_sampler.py
├── pressure_sampler.py
├── sample.py
├── sampler.py
├── swap_sampler.py
├── temperature_sensor_sampler.py
└── uplink_sampler.py
├── temperature.py
├── temperature_sensors
└── temperature_sensor_descriptor.py
├── tests
├── __init__.py
├── conftest.py
├── test_delta_sampler.py
├── test_event_broker.py
├── test_monitor.py
├── test_preferences.py
├── test_pressure_sampler.py
└── test_sampler.py
├── theme.py
├── theming.py
├── translatable_strings
├── __init__.py
├── monitor_title.py
├── preference_toggle_description.py
├── preference_toggle_label.py
├── preference_toggle_section_name.py
├── redraw_frequency.py
└── tips.py
├── translators.py
├── ui
├── __init__.py
├── colors.py
├── graph_area.py
├── graph_redraw_tick_manager.py
├── headerbar_wrapper.py
├── monitor_title_overlay.py
├── monitor_widgets
│ ├── cpu_monitor_widget.py
│ ├── cpu_per_core_monitor_widget.py
│ ├── cpu_pressure_monitor_widget.py
│ ├── downlink_monitor_widget.py
│ ├── gpu_monitor_widget.py
│ ├── home_usage_monitor_widget.py
│ ├── io_pressure_monitor_widget.py
│ ├── memory_monitor_widget.py
│ ├── memory_pressure_monitor_widget.py
│ ├── monitor_widget.py
│ ├── overlapping_values_monitor_widget.py
│ ├── root_usage_monitor_widget.py
│ ├── swap_monitor_widget.py
│ ├── temperature_sensor_monitor_widget.py
│ └── uplink_monitor_widget.py
├── overlapping_graphs_area.py
├── popover_menu.py
├── preference_switch.py
├── preferences
│ ├── monitor_preference_row.py
│ ├── preferences_page_appearance.py
│ ├── preferences_page_monitors.py
│ ├── preferences_window.py
│ ├── redraw_frequency_toggle_widget.py
│ ├── rename_monitor_popover.py
│ └── temperature_units_toggle_widget.py
├── relative_graph_area.py
├── single_window.py
├── tips_window.py
└── window_layout_manager.py
├── units.py
└── window_geometry.py
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gnome-44-python-devel",
3 | "image": "quay.io/gnome_infrastructure/gnome-runtime-images:gnome-44",
4 | "shutdownAction": "stopContainer",
5 | "postCreateCommand": "bash .devcontainer/on_create.sh",
6 | "remoteUser": "root",
7 | "customizations": {
8 | "vscode": {
9 | "extensions": [
10 | "ms-python.python",
11 | "ms-python.vscode-pylance",
12 | "ms-python.isort"
13 | ]
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.devcontainer/on_create.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 |
5 | dnf -y install vim make python3-devel python3-gobject libadwaita-devel gtk4-devel gobject-introspection-devel
6 | pip install pipenv
7 |
8 | echo ". /usr/share/git-core/contrib/completion/git-prompt.sh" >> ~/.bashrc
9 | echo "export PS1='\[\e[01;34m\]\w\[\e[01;35m\]\$(__git_ps1)\n\[\e[00;01m\]$\[\e[00m\] '" >> ~/.bashrc
10 |
--------------------------------------------------------------------------------
/.github/actions/prepare-environment/action.yml:
--------------------------------------------------------------------------------
1 | name: 'Prepare Environment'
2 | runs:
3 | using: "composite"
4 | steps:
5 | - name: install python devel packages
6 | run: sudo apt install -y python3-all-dev gobject-introspection libgirepository1.0-dev
7 | shell: bash
8 |
9 | - name: install pipenv
10 | run: pip install pipenv
11 | shell: bash
12 |
13 | - name: Install dependencies
14 | run: make install-dev-requirements
15 | shell: bash
16 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
3 |
4 | ## What?
5 |
6 |
7 | ## Why?
8 |
9 |
10 | ## How could a reviewer see this new change in action?
11 |
12 |
--------------------------------------------------------------------------------
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
1 | name: Merge Checks
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | jobs:
10 | unit-tests:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: actions/setup-python@v4
15 | with:
16 | python-version: '3.10'
17 |
18 | - uses: ./.github/actions/prepare-environment
19 |
20 | - name: Run tests
21 | run: make test
22 |
23 | lint-rules:
24 | runs-on: ubuntu-latest
25 | steps:
26 | - uses: actions/checkout@v3
27 | - uses: actions/setup-python@v4
28 | with:
29 | python-version: '3.10'
30 |
31 | - uses: ./.github/actions/prepare-environment
32 |
33 | - name: Check linting
34 | run: make check-linting
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | venv
2 | **/*.pyc
3 | .vscode
4 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 |
4 | Hello!
5 |
6 | Are you considering contributing to this project? Then thank you very much! :)
7 |
8 |
9 | ## Contributing changes
10 |
11 | Before you start working on a new feature or a bug fix it would be a good idea to open an issue explaining what you want to do.
12 |
13 | By doing so we start a discussion to flesh out the idea and reach a common ground on the best strategy to follow. In the end this will speed up the process a lot and the change will make its way into a new release faster.
14 |
15 |
16 | ### Setting up the environment
17 |
18 | Monitorets is written in *Python* and it uses *Pipenv* to manage its dependencies.
19 |
20 | There is a Makefile providing several convenient targets:
21 |
22 | * To setup a python virtual environment with all the dependencies:
23 |
24 | ```
25 | make install-dev-requirements
26 | ```
27 |
28 | * To enter the new environment:
29 |
30 | ```
31 | make dev-shell
32 | ```
33 |
34 | * To run the unit tests:
35 |
36 | ```
37 | make tests
38 | ```
39 |
40 | * To check the linting rules:
41 |
42 | ```
43 | make check-linting
44 | ```
45 |
46 | * To apply automatically the linting rules:
47 |
48 | ```
49 | make linting
50 | ```
51 |
52 |
53 | For every PR there are checks ensuring that the unit tests are passing and that the linting is correct, so it is a good idea to ensure the tests are passing and to apply the linting rules before opening a PR.
54 |
55 |
56 | ### Running your changes
57 |
58 | The easiest way to build and run Monitorets locally is to use [Gnome Builder](https://wiki.gnome.org/Apps/Builder): You just need to open the project and hit the *run* button.
59 |
60 |
61 | ## Opening issues
62 |
63 |
64 | ### Have you found a bug?
65 |
66 | Try to explain as best as you can the nature of the bug.
67 |
68 | If the application is behaving in an unexpected manner try to describe the steps necessary to reproduce this behavior and also explain what did you expect to happen instead.
69 |
70 | An image (and a video!) is worth a thousand words: Screenshots or screencasts showing the bug are also very helpful!
71 |
72 |
73 |
74 | ### Do you have a feature request?
75 |
76 | Try to explain your request as clearly as possible. Providing examples (mockups, references to other applications, etc) will make it easier to understand your request.
77 |
78 |
79 |
80 | ## Translators
81 |
82 | Translations are ***very*** welcome!
83 |
84 |
85 | If you submit a translation you may consider adding yourself to the [list of translators](https://github.com/jorchube/monitorets/blob/master/src/translators.py). By doing so your name will appear in the credits of the application on *Menu* --> *About* --> *Credits*.
86 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SOURCE_DIR=src
2 | REQUIREMENTS_TXT=requirements.txt
3 |
4 | install-dev-requirements:
5 | pipenv sync --dev --verbose
6 |
7 | generate-python-gtk-symbols: install-dev-requirements
8 | pipenv run gengir --gtk 4
9 |
10 | setup-dev-environment: install-dev-requirements generate-python-gtk-symbols
11 |
12 | dev-shell:
13 | pipenv shell
14 |
15 | generate-pot-file:
16 | meson setup build
17 | ninja -C build monitorets-pot
18 |
19 | test:
20 | pipenv run pytest
21 |
22 | check-linting:
23 | pipenv run black --check $(SOURCE_DIR)
24 |
25 | linting:
26 | pipenv run black $(SOURCE_DIR)
27 |
28 | _generate-requirements-txt:
29 | pipenv requirements > $(REQUIREMENTS_TXT)
30 |
31 | _delete-requirements-txt:
32 | rm $(REQUIREMENTS_TXT)
33 |
34 | _generate-dependencies-json-file:
35 | python ./build_helper/flatpak-pip-generator.py --requirements-file=$(REQUIREMENTS_TXT) --output pypi-dependencies
36 |
37 | update-dependencies-manifest: _generate-requirements-txt _generate-dependencies-json-file _delete-requirements-txt
38 |
39 | validate-app-data:
40 | flatpak run --env=G_DEBUG=fatal-criticals --command=appstream-util org.flatpak.Builder validate data/io.github.jorchube.monitorets.appdata.xml.in
41 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | psutil = "==5.9.3"
8 | xdg = "==5.1.1"
9 |
10 | [dev-packages]
11 | pytest = "*"
12 | requirements-parser = "*"
13 | black = "*"
14 | pygobject = "*"
15 | gengir = "*"
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Monitorets
2 |
3 |
4 |
5 |
6 |
7 | **Monitorets** is a small utility application offering a simple and quick view at the usage of several of your computer resources. Almost like an applet or a widget for your Linux desktop.
8 |
9 |
10 |
11 |
12 |
13 | ### Flexible:
14 |
15 | Select between *horizontal* and *vertical* layout. Choose Light or Dark theme.
16 |
17 |
18 |
19 |
20 |
21 | ### Configurable:
22 |
23 | Choose which resources you want to have visible:
24 | * Cpu
25 | * Gpu \[1\]
26 | * Memory
27 | * Swap
28 | * Network downlink traffic
29 | * Network uplink traffic
30 | * Home folder ( **~** ) space
31 | * Root ( **/** ) space
32 | * CPU, Memory and I/O pressure
33 | * Temperature sensors
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | ### Get it now:
44 |
45 | You can download the latest version from flathub. Click on the banner below:
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | You can also install it using the command line with the following commands:
54 |
55 | ```
56 | flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
57 | flatpak install io.github.jorchube.monitorets
58 | ```
59 |
60 | ---
61 |
62 | \[1\] GPU monitoring is an experimental feature that may not work at all depending on your GPU vendor and drivers.
63 |
--------------------------------------------------------------------------------
/data/icons/hicolor/symbolic/apps/io.github.jorchube.monitorets-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/data/icons/meson.build:
--------------------------------------------------------------------------------
1 | application_id = 'io.github.jorchube.monitorets'
2 |
3 | scalable_dir = join_paths('hicolor', 'scalable', 'apps')
4 | install_data(
5 | join_paths(scalable_dir, ('@0@.svg').format(application_id)),
6 | install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir)
7 | )
8 |
9 | symbolic_dir = join_paths('hicolor', 'symbolic', 'apps')
10 | install_data(
11 | join_paths(symbolic_dir, ('@0@-symbolic.svg').format(application_id)),
12 | install_dir: join_paths(get_option('datadir'), 'icons', symbolic_dir)
13 | )
14 |
--------------------------------------------------------------------------------
/data/io.github.jorchube.monitorets.appdata.xml.in:
--------------------------------------------------------------------------------
1 |
2 |
3 | io.github.jorchube.monitorets
4 | Monitorets
5 | Have always at a glance the usage of system resources
6 | CC0-1.0
7 | GPL-3.0-or-later
8 | Jordi Chulia
9 | https://github.com/jorchube/monitorets/
10 | https://github.com/jorchube/monitorets/issues
11 | https://github.com/jorchube/monitorets/po/
12 | https://github.com/jorchube/monitorets/
13 |
14 |
15 |
16 |
17 | Have always at a glance the usage of system resources
18 |
19 | Monitorets is a small utility application offering a simple and quick view at the usage of several of your computer resources. Almost like an applet or a widget for your desktop.
20 |
21 | Available monitors:
22 |
23 | CPU
24 | GPU (experimental support)
25 | Memory
26 | Swap
27 | Network downlink traffic
28 | Network uplink traffic
29 | Home folder space
30 | Root space
31 | CPU, Memory and I/O pressure
32 | Temperature sensors
33 |
34 |
35 |
36 |
37 |
38 | https://raw.githubusercontent.com/jorchube/monitorets/master/imgs/main.png
39 |
40 |
41 | https://raw.githubusercontent.com/jorchube/monitorets/master/imgs/layouts.png
42 |
43 |
44 | https://raw.githubusercontent.com/jorchube/monitorets/master/imgs/preferences_appearance.png
45 |
46 |
47 | https://raw.githubusercontent.com/jorchube/monitorets/master/imgs/preferences_monitors_1.png
48 |
49 |
50 | https://raw.githubusercontent.com/jorchube/monitorets/master/imgs/preferences_monitors_2.png
51 |
52 |
53 |
54 | io.github.jorchube.monitorets.desktop
55 |
56 |
57 | monitorets
58 |
59 |
60 |
61 |
62 |
63 |
64 | Added Turkish translation
65 | Added German translation
66 |
67 |
68 |
69 |
70 |
71 |
72 | Added CPU, Memory and I/O pressure monitors
73 |
74 |
75 |
76 |
77 |
78 |
79 | Fix misalignment of monitor titles
80 |
81 |
82 |
83 |
84 |
85 |
86 | Added missing filesystem permissions
87 |
88 |
89 |
90 |
91 |
92 |
93 | Added support for arbitrarily large monitor size
94 | Added support to arrange monitors in a grid layout
95 | Added support to select between Celsius and Fahrenheit units for the temperature sensors
96 | Added support to configure the redraw rate of the monitor graphs
97 | Added a keyboard shortcut to open the preferences window (Kudos to github.com/gregorni/)
98 | Added french translation (Kudos to github.com/rene-coty/)
99 |
100 |
101 |
102 |
103 |
104 |
105 | Added support to show the current value of a monitor
106 | Remember window size on close
107 | Use the same scaling for network uplink and downlink monitors to avoid confusion
108 |
109 |
110 |
111 |
112 |
113 |
114 | Fixed a bug preventing the preferences window from opening (Thanks to @jrdmellow)
115 |
116 |
117 |
118 |
119 |
120 |
121 | Added support for custom monitor names
122 | Added smooth graph drawing
123 |
124 |
125 |
126 |
127 |
128 |
129 | Added Swap memory monitor
130 | Refactored dropdown menu and preferences
131 |
132 |
133 |
134 |
135 |
136 |
137 | Added Dutch language support
138 |
139 |
140 |
141 |
142 |
143 |
144 | Added internationalization support
145 |
146 |
147 |
148 |
149 |
150 |
151 | Restored missing network permission needed for the network monitors
152 |
153 |
154 |
155 |
156 |
157 |
158 | Autodiscover temperature sensors and expose them as temperature monitors
159 | Ellipsize monitor names when do not fit the available space
160 | Add monitor groups for same-kind monitors in the preferences popover
161 |
162 |
163 |
164 |
165 |
166 |
167 | Added network downlink and uplink traffic monitors
168 | Some cosmetic changes have been done
169 |
170 |
171 |
172 |
173 |
174 |
175 | Added home and root folder usage monitors
176 | Added layout selector
177 | Added Tips section
178 | Improve close and preferences buttons visibility when shown
179 |
180 |
181 |
182 |
183 |
184 | Under the hood tweaks
185 |
186 |
187 |
188 |
189 | Aesthetic:
190 | - Reworked graph drawing
191 | Internal:
192 | - Decoupled several events that can trigger a graph redraw
193 |
194 |
195 |
196 |
197 | First release
198 |
199 |
200 |
201 |
202 |
203 |
--------------------------------------------------------------------------------
/data/io.github.jorchube.monitorets.desktop.in:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | # Translators: Do not translate the application name
3 | Name=Monitorets
4 | Exec=monitorets
5 | Icon=io.github.jorchube.monitorets
6 | Terminal=false
7 | Type=Application
8 | Categories=System;Utility;GNOME;GTK;
9 | StartupNotify=true
10 | Keywords=monitor;system monitor;System Monitor;Monitor;
11 |
--------------------------------------------------------------------------------
/data/io.github.jorchube.monitorets.gschema.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/data/meson.build:
--------------------------------------------------------------------------------
1 | desktop_file = i18n.merge_file(
2 | input: 'io.github.jorchube.monitorets.desktop.in',
3 | output: 'io.github.jorchube.monitorets.desktop',
4 | type: 'desktop',
5 | po_dir: '../po',
6 | install: true,
7 | install_dir: join_paths(get_option('datadir'), 'applications')
8 | )
9 |
10 | desktop_utils = find_program('desktop-file-validate', required: false)
11 | if desktop_utils.found()
12 | test('Validate desktop file', desktop_utils, args: [desktop_file])
13 | endif
14 |
15 | appstream_file = i18n.merge_file(
16 | input: 'io.github.jorchube.monitorets.appdata.xml.in',
17 | output: 'io.github.jorchube.monitorets.appdata.xml',
18 | po_dir: '../po',
19 | install: true,
20 | install_dir: join_paths(get_option('datadir'), 'appdata')
21 | )
22 |
23 | appstream_util = find_program('appstream-util', required: false)
24 | if appstream_util.found()
25 | test('Validate appstream file', appstream_util, args: ['validate', appstream_file])
26 | endif
27 |
28 | install_data('io.github.jorchube.monitorets.gschema.xml',
29 | install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')
30 | )
31 |
32 | compile_schemas = find_program('glib-compile-schemas', required: false)
33 | if compile_schemas.found()
34 | test('Validate schema file',
35 | compile_schemas,
36 | args: ['--strict', '--dry-run', meson.current_source_dir()])
37 | endif
38 |
39 | subdir('icons')
40 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | **Monitorets** is a small utility application offering a simple and quick view at the usage of several of your computer resources. Almost like an applet or a widget for your Linux desktop.
6 |
7 |
8 |
9 |
10 |
11 | ### Flexible:
12 |
13 | Select between *horizontal* and *vertical* layout. Choose Light or Dark theme.
14 |
15 |
16 |
17 |
18 |
19 | ### Configurable:
20 |
21 | Choose which resources you want to have visible:
22 | * Cpu
23 | * Gpu \[1\]
24 | * Memory
25 | * Swap
26 | * Network downlink traffic
27 | * Network uplink traffic
28 | * Home folder ( **~** ) space
29 | * Root ( **/** ) space
30 | * CPU, Memory and I/O pressure
31 | * Temperature sensors
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | ### Get it now:
42 |
43 | You can download the latest version from flathub. Click on the banner below:
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | You can also install it using the command line with the following commands:
52 |
53 | ```
54 | flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
55 | flatpak install io.github.jorchube.monitorets
56 | ```
57 |
58 | ---
59 |
60 | [Code](https://github.com/jorchube/monitorets)
61 |
62 | [Issues](https://github.com/jorchube/monitorets/issues)
63 |
64 | ---
65 |
66 | \[1\] GPU monitoring is an experimental feature that may not work at all depending on your GPU vendor and drivers.
67 |
--------------------------------------------------------------------------------
/imgs/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/2.png
--------------------------------------------------------------------------------
/imgs/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/4.png
--------------------------------------------------------------------------------
/imgs/adaptable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/adaptable.png
--------------------------------------------------------------------------------
/imgs/configurable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/configurable.png
--------------------------------------------------------------------------------
/imgs/dark_large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/dark_large.png
--------------------------------------------------------------------------------
/imgs/images.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/images.xcf
--------------------------------------------------------------------------------
/imgs/layouts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/layouts.png
--------------------------------------------------------------------------------
/imgs/layouts_large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/layouts_large.png
--------------------------------------------------------------------------------
/imgs/light_window.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/light_window.png
--------------------------------------------------------------------------------
/imgs/main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/main.png
--------------------------------------------------------------------------------
/imgs/preferences_appearance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/preferences_appearance.png
--------------------------------------------------------------------------------
/imgs/preferences_large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/preferences_large.png
--------------------------------------------------------------------------------
/imgs/preferences_monitors.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/preferences_monitors.png
--------------------------------------------------------------------------------
/imgs/preferences_monitors_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/preferences_monitors_1.png
--------------------------------------------------------------------------------
/imgs/preferences_monitors_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/preferences_monitors_2.png
--------------------------------------------------------------------------------
/imgs/themeable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/imgs/themeable.png
--------------------------------------------------------------------------------
/io.github.jorchube.monitorets.json:
--------------------------------------------------------------------------------
1 | {
2 | "app-id" : "io.github.jorchube.monitorets",
3 | "runtime" : "org.gnome.Platform",
4 | "runtime-version" : "44",
5 | "sdk" : "org.gnome.Sdk",
6 | "command" : "monitorets",
7 | "finish-args" : [
8 | "--share=ipc",
9 | "--socket=fallback-x11",
10 | "--device=dri",
11 | "--socket=wayland",
12 | "--filesystem=host:ro",
13 | "--share=network"
14 | ],
15 | "cleanup" : [
16 | "/include",
17 | "/lib/pkgconfig",
18 | "/man",
19 | "/share/doc",
20 | "/share/gtk-doc",
21 | "/share/man",
22 | "/share/pkgconfig",
23 | "*.la",
24 | "*.a"
25 | ],
26 | "modules" : [
27 | "pypi-dependencies.json",
28 | {
29 | "name" : "monitorets",
30 | "builddir" : true,
31 | "buildsystem" : "meson",
32 | "sources" : [
33 | {
34 | "type" : "dir",
35 | "path" : "."
36 | }
37 | ]
38 | }
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/meson.build:
--------------------------------------------------------------------------------
1 | project('monitorets',
2 | meson_version: '>= 0.59.0',
3 | default_options: [ 'warning_level=2', 'werror=false', ],
4 | )
5 |
6 | i18n = import('i18n')
7 | gnome = import('gnome')
8 |
9 |
10 |
11 | subdir('data')
12 | subdir('src')
13 | subdir('po')
14 |
15 | gnome.post_install(
16 | glib_compile_schemas: true,
17 | gtk_update_icon_cache: true,
18 | update_desktop_database: true,
19 | )
20 |
--------------------------------------------------------------------------------
/po/LINGUAS:
--------------------------------------------------------------------------------
1 | # please keep this list sorted alphabetically
2 | de
3 | es
4 | fr
5 | nl
6 | tr
7 |
--------------------------------------------------------------------------------
/po/POTFILES:
--------------------------------------------------------------------------------
1 | data/io.github.jorchube.monitorets.appdata.xml.in
2 | data/io.github.jorchube.monitorets.desktop.in
3 | data/io.github.jorchube.monitorets.gschema.xml
4 | src/gtk/help-overlay.ui
5 | src/gtk/main-menu-model.ui
6 | src/gtk/preferences-page-appearance.ui
7 | src/gtk/preferences-page-monitors.ui
8 | src/gtk/preferences-window.ui
9 | src/gtk/rename-monitor-popover.ui
10 | src/translatable_strings/monitor_title.py
11 | src/translatable_strings/preference_toggle_description.py
12 | src/translatable_strings/preference_toggle_label.py
13 | src/translatable_strings/preference_toggle_section_name.py
14 | src/translatable_strings/redraw_frequency.py
15 | src/translatable_strings/tips.py
16 |
--------------------------------------------------------------------------------
/po/meson.build:
--------------------------------------------------------------------------------
1 | i18n.gettext('monitorets', preset: 'glib')
2 |
--------------------------------------------------------------------------------
/po/monitorets.pot:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the monitorets package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: monitorets\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2023-10-14 21:13+0300\n"
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 | "Last-Translator: FULL NAME \n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: \n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 |
20 | #: data/io.github.jorchube.monitorets.appdata.xml.in:5
21 | #: data/io.github.jorchube.monitorets.appdata.xml.in:17
22 | msgid "Have always at a glance the usage of system resources"
23 | msgstr ""
24 |
25 | #: data/io.github.jorchube.monitorets.appdata.xml.in:19
26 | msgid ""
27 | "Monitorets is a small utility application offering a simple and quick view "
28 | "at the usage of several of your computer resources. Almost like an applet or "
29 | "a widget for your desktop."
30 | msgstr ""
31 |
32 | #: data/io.github.jorchube.monitorets.appdata.xml.in:21
33 | msgid "Available monitors:"
34 | msgstr ""
35 |
36 | #: data/io.github.jorchube.monitorets.appdata.xml.in:23
37 | #: src/translatable_strings/monitor_title.py:3
38 | #: src/translatable_strings/preference_toggle_label.py:3
39 | #: src/translatable_strings/preference_toggle_section_name.py:3
40 | msgid "CPU"
41 | msgstr ""
42 |
43 | #: data/io.github.jorchube.monitorets.appdata.xml.in:24
44 | msgid "GPU (experimental support)"
45 | msgstr ""
46 |
47 | #: data/io.github.jorchube.monitorets.appdata.xml.in:25
48 | #: src/translatable_strings/monitor_title.py:5
49 | #: src/translatable_strings/preference_toggle_label.py:6
50 | #: src/translatable_strings/preference_toggle_section_name.py:5
51 | msgid "Memory"
52 | msgstr ""
53 |
54 | #: data/io.github.jorchube.monitorets.appdata.xml.in:26
55 | #: src/translatable_strings/monitor_title.py:6
56 | #: src/translatable_strings/preference_toggle_label.py:7
57 | msgid "Swap"
58 | msgstr ""
59 |
60 | #: data/io.github.jorchube.monitorets.appdata.xml.in:27
61 | msgid "Network downlink traffic"
62 | msgstr ""
63 |
64 | #: data/io.github.jorchube.monitorets.appdata.xml.in:28
65 | msgid "Network uplink traffic"
66 | msgstr ""
67 |
68 | #: data/io.github.jorchube.monitorets.appdata.xml.in:29
69 | msgid "Home folder space"
70 | msgstr ""
71 |
72 | #: data/io.github.jorchube.monitorets.appdata.xml.in:30
73 | msgid "Root space"
74 | msgstr ""
75 |
76 | #: data/io.github.jorchube.monitorets.appdata.xml.in:31
77 | msgid "CPU, Memory and I/O pressure"
78 | msgstr ""
79 |
80 | #: data/io.github.jorchube.monitorets.appdata.xml.in:32
81 | msgid "Temperature sensors"
82 | msgstr ""
83 |
84 | #: data/io.github.jorchube.monitorets.appdata.xml.in:64
85 | msgid "Added CPU, Memory and I/O pressure monitors"
86 | msgstr ""
87 |
88 | #. Translators: Do not translate the application name
89 | #: data/io.github.jorchube.monitorets.desktop.in:4
90 | msgid "Monitorets"
91 | msgstr ""
92 |
93 | #: data/io.github.jorchube.monitorets.desktop.in:11
94 | msgid "monitor;system monitor;System Monitor;Monitor;"
95 | msgstr ""
96 |
97 | #: src/gtk/help-overlay.ui:11
98 | msgctxt "shortcut window"
99 | msgid "General"
100 | msgstr ""
101 |
102 | #: src/gtk/help-overlay.ui:14
103 | msgctxt "shortcut window"
104 | msgid "Show Shortcuts"
105 | msgstr ""
106 |
107 | #: src/gtk/help-overlay.ui:20
108 | msgctxt "shortcut window"
109 | msgid "Quit"
110 | msgstr ""
111 |
112 | #: src/gtk/main-menu-model.ui:6
113 | msgid "Preferences"
114 | msgstr ""
115 |
116 | #: src/gtk/main-menu-model.ui:12 src/translatable_strings/tips.py:3
117 | msgid "Tips"
118 | msgstr ""
119 |
120 | #: src/gtk/main-menu-model.ui:16
121 | msgid "Keyboard Shortcuts"
122 | msgstr ""
123 |
124 | #: src/gtk/main-menu-model.ui:20
125 | msgid "About"
126 | msgstr ""
127 |
128 | #: src/gtk/main-menu-model.ui:22
129 | msgid "Quit"
130 | msgstr ""
131 |
132 | #: src/gtk/preferences-page-appearance.ui:9
133 | msgctxt "monitorets"
134 | msgid "Appearance"
135 | msgstr ""
136 |
137 | #: src/gtk/preferences-page-appearance.ui:12
138 | msgctxt "monitorets"
139 | msgid "Choose between light, dark or the global system theme."
140 | msgstr ""
141 |
142 | #: src/gtk/preferences-page-appearance.ui:14
143 | msgctxt "monitorets"
144 | msgid "Theme"
145 | msgstr ""
146 |
147 | #: src/gtk/preferences-page-appearance.ui:52
148 | msgctxt "monitorets"
149 | msgid "System"
150 | msgstr ""
151 |
152 | #: src/gtk/preferences-page-appearance.ui:84
153 | msgctxt "monitorets"
154 | msgid "Light"
155 | msgstr ""
156 |
157 | #: src/gtk/preferences-page-appearance.ui:117
158 | msgctxt "monitorets"
159 | msgid "Dark"
160 | msgstr ""
161 |
162 | #: src/gtk/preferences-page-appearance.ui:134
163 | msgctxt "monitorets"
164 | msgid "Choose how to organize the enabled resource monitors."
165 | msgstr ""
166 |
167 | #: src/gtk/preferences-page-appearance.ui:135
168 | msgctxt "monitorets"
169 | msgid "Layout"
170 | msgstr ""
171 |
172 | #: src/gtk/preferences-page-appearance.ui:138
173 | msgctxt "monitorets"
174 | msgid "Monitors stacked in a single column."
175 | msgstr ""
176 |
177 | #: src/gtk/preferences-page-appearance.ui:139
178 | msgid "Vertical"
179 | msgstr ""
180 |
181 | #: src/gtk/preferences-page-appearance.ui:144
182 | msgctxt "monitorets"
183 | msgid "Monitors aligned in a single row."
184 | msgstr ""
185 |
186 | #: src/gtk/preferences-page-appearance.ui:145
187 | msgid "Horizontal"
188 | msgstr ""
189 |
190 | #: src/gtk/preferences-page-appearance.ui:150
191 | msgctxt "monitorets"
192 | msgid "Monitors aligned in a grid."
193 | msgstr ""
194 |
195 | #: src/gtk/preferences-page-appearance.ui:151
196 | msgid "Grid"
197 | msgstr ""
198 |
199 | #: src/gtk/preferences-page-appearance.ui:158
200 | msgctxt "preferences_page_appearance"
201 | msgid "Graphs"
202 | msgstr ""
203 |
204 | #: src/gtk/preferences-page-appearance.ui:161
205 | msgctxt "preferences_page_appearance"
206 | msgid "Draw smooth graphs"
207 | msgstr ""
208 |
209 | #: src/gtk/preferences-page-appearance.ui:166
210 | msgctxt "preferences_page_appearance"
211 | msgid "Show current value"
212 | msgstr ""
213 |
214 | #: src/gtk/preferences-page-appearance.ui:171
215 | msgctxt "preferences_appearance"
216 | msgid "Temperature units"
217 | msgstr ""
218 |
219 | #: src/gtk/preferences-page-appearance.ui:176
220 | msgctxt "preferences_appearance"
221 | msgid ""
222 | "Higher means a smoother redraw but also a slight increase in the resources "
223 | "used by the application itself."
224 | msgstr ""
225 |
226 | #: src/gtk/preferences-page-appearance.ui:177
227 | msgctxt "preferences_appearance"
228 | msgid "Redraw frequency"
229 | msgstr ""
230 |
231 | #: src/gtk/preferences-page-monitors.ui:8
232 | msgctxt "monitorets"
233 | msgid "Monitors"
234 | msgstr ""
235 |
236 | #: src/gtk/preferences-page-monitors.ui:11
237 | msgctxt "monitors_preference_group"
238 | msgid "CPU"
239 | msgstr ""
240 |
241 | #: src/gtk/preferences-page-monitors.ui:16
242 | msgctxt "monitors_preference_group"
243 | msgid "GPU"
244 | msgstr ""
245 |
246 | #: src/gtk/preferences-page-monitors.ui:21
247 | msgctxt "monitors_preference_group"
248 | msgid "Memory"
249 | msgstr ""
250 |
251 | #: src/gtk/preferences-page-monitors.ui:26
252 | msgctxt "monitors_preference_group"
253 | msgid "Network"
254 | msgstr ""
255 |
256 | #: src/gtk/preferences-page-monitors.ui:31
257 | msgctxt "monitors_preference_group"
258 | msgid "Disk usage"
259 | msgstr ""
260 |
261 | #: src/gtk/preferences-page-monitors.ui:36
262 | msgctxt "monitors_preference_group"
263 | msgid ""
264 | "Pressure stall quantifies resource scarcity between processes. The more "
265 | "processes are stalled waiting for a resource (because it is being used by "
266 | "something else) the higher the pressure over that resource will be."
267 | msgstr ""
268 |
269 | #: src/gtk/preferences-page-monitors.ui:37
270 | msgctxt "monitors_preference_group"
271 | msgid "Pressure"
272 | msgstr ""
273 |
274 | #: src/gtk/preferences-page-monitors.ui:42
275 | msgctxt "monitorets"
276 | msgid ""
277 | "These are all the temperature sensors that Monitorets has been able to "
278 | "discover in your setup."
279 | msgstr ""
280 |
281 | #: src/gtk/preferences-page-monitors.ui:43
282 | msgctxt "monitors_preference_group"
283 | msgid "Temperature sensors"
284 | msgstr ""
285 |
286 | #: src/gtk/rename-monitor-popover.ui:20
287 | msgctxt "rename_monitor"
288 | msgid "Rename monitor"
289 | msgstr ""
290 |
291 | #: src/gtk/rename-monitor-popover.ui:29
292 | msgctxt "rename_monitor"
293 | msgid "Leaving this text empty will restore the original name."
294 | msgstr ""
295 |
296 | #: src/gtk/rename-monitor-popover.ui:38
297 | msgctxt "rename_monitor"
298 | msgid "Rename"
299 | msgstr ""
300 |
301 | #: src/translatable_strings/monitor_title.py:4
302 | #: src/translatable_strings/preference_toggle_label.py:5
303 | #: src/translatable_strings/preference_toggle_section_name.py:4
304 | msgid "GPU"
305 | msgstr ""
306 |
307 | #: src/translatable_strings/monitor_title.py:7
308 | msgid "Network 🠅"
309 | msgstr ""
310 |
311 | #: src/translatable_strings/monitor_title.py:8
312 | msgid "Network 🠇"
313 | msgstr ""
314 |
315 | #: src/translatable_strings/monitor_title.py:11
316 | msgid "Temp"
317 | msgstr ""
318 |
319 | #: src/translatable_strings/monitor_title.py:12
320 | msgid "CPU Pressure"
321 | msgstr ""
322 |
323 | #: src/translatable_strings/monitor_title.py:13
324 | msgid "Memory Pressure"
325 | msgstr ""
326 |
327 | #: src/translatable_strings/monitor_title.py:14
328 | msgid "I/O Pressure"
329 | msgstr ""
330 |
331 | #: src/translatable_strings/preference_toggle_description.py:3
332 | msgid "Shown as"
333 | msgstr ""
334 |
335 | #: src/translatable_strings/preference_toggle_description.py:6
336 | msgid ""
337 | "The monitor will draw as many graphs as cores are present in the system."
338 | msgstr ""
339 |
340 | #: src/translatable_strings/preference_toggle_description.py:8
341 | msgid "Experimental. This is only known to work on some AMD GPUs currently."
342 | msgstr ""
343 |
344 | #: src/translatable_strings/preference_toggle_label.py:4
345 | msgid "CPU per core"
346 | msgstr ""
347 |
348 | #: src/translatable_strings/preference_toggle_label.py:8
349 | msgid "Downlink"
350 | msgstr ""
351 |
352 | #: src/translatable_strings/preference_toggle_label.py:9
353 | msgid "Uplink"
354 | msgstr ""
355 |
356 | #: src/translatable_strings/preference_toggle_label.py:10
357 | msgid "Home folder usage"
358 | msgstr ""
359 |
360 | #: src/translatable_strings/preference_toggle_label.py:11
361 | msgid "Root folder usage"
362 | msgstr ""
363 |
364 | #: src/translatable_strings/preference_toggle_label.py:12
365 | #: src/translatable_strings/preference_toggle_section_name.py:8
366 | msgid "Temperature"
367 | msgstr ""
368 |
369 | #: src/translatable_strings/preference_toggle_label.py:13
370 | msgid "CPU pressure"
371 | msgstr ""
372 |
373 | #: src/translatable_strings/preference_toggle_label.py:14
374 | msgid "Memory pressure"
375 | msgstr ""
376 |
377 | #: src/translatable_strings/preference_toggle_label.py:15
378 | msgid "I/O pressure"
379 | msgstr ""
380 |
381 | #: src/translatable_strings/preference_toggle_section_name.py:6
382 | msgid "Network"
383 | msgstr ""
384 |
385 | #: src/translatable_strings/preference_toggle_section_name.py:7
386 | msgid "Disk usage"
387 | msgstr ""
388 |
389 | #: src/translatable_strings/preference_toggle_section_name.py:9
390 | msgid "Pressure"
391 | msgstr ""
392 |
393 | #: src/translatable_strings/redraw_frequency.py:4
394 | msgid "Very High"
395 | msgstr ""
396 |
397 | #: src/translatable_strings/redraw_frequency.py:5
398 | msgid "High"
399 | msgstr ""
400 |
401 | #: src/translatable_strings/redraw_frequency.py:6
402 | msgid "Low"
403 | msgstr ""
404 |
405 | #: src/translatable_strings/redraw_frequency.py:7
406 | msgid "Very Low"
407 | msgstr ""
408 |
409 | #: src/translatable_strings/tips.py:4
410 | msgid "Always on Top"
411 | msgstr ""
412 |
413 | #: src/translatable_strings/tips.py:6
414 | msgid ""
415 | "You can make the window stay on top of any other window: Press Alt+Space or right click with your mouse in the "
417 | "window titlebar to bring the window menu, then select Always on Top ."
419 | msgstr ""
420 |
--------------------------------------------------------------------------------
/pypi-dependencies.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pypi-dependencies",
3 | "buildsystem": "simple",
4 | "build-commands": [],
5 | "modules": [
6 | {
7 | "name": "python3-psutil",
8 | "buildsystem": "simple",
9 | "build-commands": [
10 | "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"psutil==5.9.3\" --no-build-isolation"
11 | ],
12 | "sources": [
13 | {
14 | "type": "file",
15 | "url": "https://files.pythonhosted.org/packages/de/eb/1c01a34c86ee3b058c556e407ce5b07cb7d186ebe47b3e69d6f152ca5cc5/psutil-5.9.3.tar.gz",
16 | "sha256": "7ccfcdfea4fc4b0a02ca2c31de7fcd186beb9cff8207800e14ab66f79c773af6"
17 | }
18 | ]
19 | },
20 | {
21 | "name": "python3-xdg",
22 | "buildsystem": "simple",
23 | "build-commands": [
24 | "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"xdg==5.1.1\" --no-build-isolation"
25 | ],
26 | "sources": [
27 | {
28 | "type": "file",
29 | "url": "https://files.pythonhosted.org/packages/ea/09/4a0f30aada49e142b94bbb232c023abcbc6ced7e2a9776533fb14977e9db/xdg-5.1.1-py3-none-any.whl",
30 | "sha256": "865a7b56ed1d4cd2fce2ead1eddf97360843619757f473cd90b75f1817ca541d"
31 | }
32 | ]
33 | }
34 | ]
35 | }
--------------------------------------------------------------------------------
/src/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/src/__init__.py
--------------------------------------------------------------------------------
/src/controller.py:
--------------------------------------------------------------------------------
1 | from gi.repository import GObject
2 | import traceback
3 | from . import events
4 | from .event_broker import EventBroker
5 | from .preferences import Preferences
6 | from .preference_keys import PreferenceKeys
7 | from .theming import Theming
8 | from .monitor_descriptors import monitor_descriptor_list
9 | from .network_monitor_scale_manager import NetworkMonitorScaleManager
10 | from .ui.window_layout_manager import WindowLayoutManager
11 |
12 |
13 | class Controller:
14 | _PREFERENCES_NEEDING_MONITORS_RESTART = [
15 | PreferenceKeys.SMOOTH_GRAPH,
16 | PreferenceKeys.REDRAW_FREQUENCY_SECONDS,
17 | ]
18 |
19 | @classmethod
20 | def initialize(self, application):
21 | self._application = application
22 |
23 | EventBroker.initialize()
24 | Preferences.initialize()
25 | Preferences.load()
26 | Theming.initialize()
27 | NetworkMonitorScaleManager.initialize()
28 | WindowLayoutManager.initialize()
29 |
30 | EventBroker.subscribe(events.PREFERENCES_CHANGED, self._on_preference_changed)
31 | EventBroker.subscribe(events.MONITOR_ENABLED, self._handle_on_monitor_enabled)
32 | EventBroker.subscribe(events.MONITOR_DISABLED, self._handle_on_monitor_disabled)
33 |
34 | self._available_monitors = self._build_available_monitors_dict()
35 | self._enabled_monitors = dict()
36 |
37 | @classmethod
38 | def show_monitors(self):
39 | for descriptor in monitor_descriptor_list:
40 | if Preferences.get(descriptor["enabled_preference_key"]):
41 | EventBroker.notify(events.MONITOR_ENABLED, descriptor["type"])
42 |
43 | @classmethod
44 | def stop_all_monitors(self):
45 | for monitor in self._enabled_monitors.values():
46 | if monitor is not None:
47 | monitor.stop()
48 |
49 | @classmethod
50 | def _restart_monitors(self):
51 | for descriptor in monitor_descriptor_list:
52 | EventBroker.notify(events.MONITOR_DISABLED, descriptor["type"])
53 |
54 | self.show_monitors()
55 |
56 | @classmethod
57 | def _on_preference_changed(self, preference_key, value):
58 | if preference_key in self._PREFERENCES_NEEDING_MONITORS_RESTART:
59 | self._restart_monitors()
60 | return
61 |
62 | for descriptor in monitor_descriptor_list:
63 | if preference_key == descriptor["enabled_preference_key"]:
64 | self._on_monitor_enabled_changed(descriptor["type"], value)
65 | return
66 |
67 | @classmethod
68 | def _on_monitor_enabled_changed(self, monitor_type, enabled):
69 | if enabled:
70 | event = events.MONITOR_ENABLED
71 | else:
72 | event = events.MONITOR_DISABLED
73 |
74 | EventBroker.notify(event, monitor_type)
75 |
76 | @classmethod
77 | def _handle_on_monitor_enabled(self, type):
78 | GObject.idle_add(self._on_monitor_enabled, type)
79 |
80 | @classmethod
81 | def _handle_on_monitor_disabled(self, type):
82 | GObject.idle_add(self._on_monitor_disabled, type)
83 |
84 | @classmethod
85 | def _on_monitor_enabled(self, type):
86 | try:
87 | self._enable_monitor(type)
88 | except Exception as e:
89 | print(f"Exception: {e}")
90 | traceback.print_exc()
91 |
92 | @classmethod
93 | def _enable_monitor(self, type):
94 | if self._enabled_monitors.get(type) is not None:
95 | print(f"[Warning] {type} monitor is already enabled")
96 | return
97 |
98 | monitor = self._available_monitors[type]()
99 | self._enabled_monitors[type] = monitor
100 | monitor.start()
101 | WindowLayoutManager.add_monitor(monitor)
102 |
103 | @classmethod
104 | def _on_monitor_disabled(self, type):
105 | self._disable_monitor(type)
106 |
107 | @classmethod
108 | def _disable_monitor(self, type):
109 | monitor = self._enabled_monitors.get(type)
110 | if monitor is None:
111 | print(f"[Warning] {type} monitor is already disabled")
112 | return
113 |
114 | self._enabled_monitors[type] = None
115 | WindowLayoutManager.remove_monitor(monitor)
116 | monitor.stop()
117 |
118 | @classmethod
119 | def _build_available_monitors_dict(self):
120 | monitors_dict = {}
121 | for descriptor in monitor_descriptor_list:
122 | monitors_dict[descriptor["type"]] = descriptor["monitor_class"]
123 |
124 | return monitors_dict
125 |
--------------------------------------------------------------------------------
/src/discover_temperature_monitors.py:
--------------------------------------------------------------------------------
1 | import psutil
2 |
3 | from .temperature_sensors.temperature_sensor_descriptor import (
4 | TemperatureSensorDescriptor,
5 | )
6 | from .ui.monitor_widgets.temperature_sensor_monitor_widget import (
7 | TemperatureSensorMonitorWidget,
8 | )
9 | from .monitor_descriptors import register_monitor_descriptor
10 | from .preferences import Preferences
11 | from .translatable_strings import (
12 | preference_toggle_label,
13 | preference_toggle_section_name,
14 | )
15 |
16 |
17 | def _get_sensor_descriptors():
18 | sensors = psutil.sensors_temperatures()
19 | if not sensors:
20 | return list()
21 |
22 | sensor_descriptor_list = list()
23 | for hardware_name, hardware_sensor_list in sensors.items():
24 | for hardware_sensor in hardware_sensor_list:
25 | hardware_sensor_name = hardware_sensor.label
26 | descriptor = TemperatureSensorDescriptor(
27 | hardware_name, hardware_sensor_name
28 | )
29 | sensor_descriptor_list.append(descriptor)
30 |
31 | return sensor_descriptor_list
32 |
33 |
34 | def _build_monitor_descriptor(sensor_descriptor):
35 | sensor_id = (
36 | f"{sensor_descriptor.hardware_name}-{sensor_descriptor.hardware_sensor_name}"
37 | )
38 | monitor_type = f"temperature_sensor_{sensor_id}"
39 | enabled_preference_key = f"temp_monitor.{sensor_id}.enabled"
40 | widget_constructor = lambda: TemperatureSensorMonitorWidget(
41 | monitor_type, sensor_descriptor
42 | )
43 | _preference_toggle_label = f"{sensor_id}"
44 | _preference_toggle_section_name = preference_toggle_section_name.TEMPERATURE
45 |
46 | return {
47 | "type": monitor_type,
48 | "enabled_preference_key": enabled_preference_key,
49 | "monitor_class": widget_constructor,
50 | "preference_toggle_label": _preference_toggle_label,
51 | "preference_toggle_description": None,
52 | "preference_toggle_section_name": _preference_toggle_section_name,
53 | "default_order": None,
54 | }
55 |
56 |
57 | def execute():
58 | sensor_descriptor_list = _get_sensor_descriptors()
59 |
60 | for sensor_descriptor in sensor_descriptor_list:
61 | monitor_descriptor = _build_monitor_descriptor(sensor_descriptor)
62 | register_monitor_descriptor(monitor_descriptor)
63 | Preferences.register_preference_key_default(
64 | monitor_descriptor["enabled_preference_key"], False
65 | )
66 |
--------------------------------------------------------------------------------
/src/event_broker.py:
--------------------------------------------------------------------------------
1 | from concurrent.futures import ThreadPoolExecutor
2 |
3 |
4 | class EventBroker:
5 | _NUMBER_OF_WORKERS = 4
6 |
7 | _subscriptions = dict()
8 | _thread_pool_executor = None
9 |
10 | @classmethod
11 | def subscribe(cls, event, subscription):
12 | if event not in cls._subscriptions:
13 | cls._subscriptions[event] = set()
14 |
15 | cls._subscriptions[event].add(subscription)
16 |
17 | @classmethod
18 | def notify(cls, event, *args, **kwargs):
19 | print(f"[Event] {event} {args} {kwargs}")
20 | if event not in cls._subscriptions:
21 | return
22 |
23 | for subscription in cls._subscriptions[event]:
24 | cls._execute_in_thread(subscription, *args, **kwargs)
25 |
26 | @classmethod
27 | def initialize(cls):
28 | cls._thread_pool_executor = ThreadPoolExecutor(
29 | max_workers=cls._NUMBER_OF_WORKERS
30 | )
31 |
32 | @classmethod
33 | def _execute_in_thread(cls, call, *args, **kwargs):
34 | cls._thread_pool_executor.submit(call, *args, **kwargs)
35 |
--------------------------------------------------------------------------------
/src/events.py:
--------------------------------------------------------------------------------
1 | PREFERENCES_CHANGED = "preferences changed"
2 |
3 | MONITOR_ENABLED = "monitor enabled"
4 | MONITOR_DISABLED = "monitor disabled"
5 |
6 | MONITOR_RENAMED = "monitor renamed"
7 |
8 | UPLINK_NETWORK_MONITOR_NEW_REFERENCE_VALUE_PROPOSAL = (
9 | "uplink network monitor new reference value proposal"
10 | )
11 | DOWNLINK_NETWORK_MONITOR_NEW_REFERENCE_VALUE_PROPOSAL = (
12 | "downlink network monitor new reference value proposal"
13 | )
14 | NETWORK_MONITOR_NEW_REFERENCE_VALUE = "uplink network monitor new reference value"
15 |
16 | ABOUT_DIALOG_TRIGGERED = "about dialog triggered"
17 | TIPS_DIALOG_TRIGGERED = "tips dialog triggered"
18 |
19 | CLOSE_APPLICATION_REQUESTED = "close application requested"
20 |
--------------------------------------------------------------------------------
/src/gtk/help-overlay.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | True
5 |
6 |
7 | shortcuts
8 | 10
9 |
10 |
11 | General
12 |
13 |
14 | Show Shortcuts
15 | win.show-help-overlay
16 |
17 |
18 |
19 |
20 | Quit
21 | app.quit
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/gtk/icons/dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/src/gtk/icons/dark.png
--------------------------------------------------------------------------------
/src/gtk/icons/horizontal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/src/gtk/icons/horizontal.png
--------------------------------------------------------------------------------
/src/gtk/icons/layout_toggle_icons.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/src/gtk/icons/layout_toggle_icons.xcf
--------------------------------------------------------------------------------
/src/gtk/icons/light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/src/gtk/icons/light.png
--------------------------------------------------------------------------------
/src/gtk/icons/system.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/src/gtk/icons/system.png
--------------------------------------------------------------------------------
/src/gtk/icons/theme_toggle_icons.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/src/gtk/icons/theme_toggle_icons.xcf
--------------------------------------------------------------------------------
/src/gtk/icons/vertical.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/src/gtk/icons/vertical.png
--------------------------------------------------------------------------------
/src/gtk/main-menu-model.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
Preferences
7 | app.preferences
8 |
9 |
10 |
11 | -
12 |
Tips
13 | app.tips
14 |
15 | -
16 |
Keyboard Shortcuts
17 | win.show-help-overlay
18 |
19 | -
20 |
About
21 | app.about
22 |
23 |
24 |
25 | -
26 |
Quit
27 | app.quit
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/gtk/preferences-page-appearance.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | preferences-desktop-appearance-symbolic
8 | Appearance
9 | Appearance
10 |
11 |
12 | Choose between light, dark or the global system theme.
13 | 15
14 | Theme
15 |
16 |
17 | card
18 |
19 |
20 |
21 |
22 | True
23 | 10
24 | 10
25 | 10
26 | 10
27 | 10
28 |
29 |
30 |
31 |
32 | vertical
33 | 10
34 |
35 |
36 | True
37 |
38 |
39 | 48
40 |
41 |
42 |
43 |
44 | 128
45 |
46 |
47 |
48 |
49 |
50 |
51 | body
52 | System
53 |
54 |
55 |
56 |
57 | flat
58 |
59 |
60 |
61 |
62 |
63 |
64 | vertical
65 | 10
66 |
67 |
68 | True
69 |
70 |
71 | 48
72 |
73 |
74 |
75 |
76 | 128
77 |
78 |
79 |
80 |
81 |
82 |
83 | body
84 | Light
85 |
86 |
87 |
88 |
89 | flat
90 | _system_theme_toggle_button
91 |
92 |
93 |
94 |
95 |
96 |
97 | vertical
98 | 10
99 |
100 |
101 | True
102 |
103 |
104 | 48
105 |
106 |
107 |
108 |
109 | 128
110 |
111 |
112 |
113 |
114 |
115 |
116 | body
117 | Dark
118 |
119 |
120 |
121 |
122 | flat
123 | _system_theme_toggle_button
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | Choose how to organize the enabled resource monitors.
135 | Layout
136 |
137 |
138 | Monitors stacked in a single column.
139 | Vertical
140 |
141 |
142 |
143 |
144 | Monitors aligned in a single row.
145 | Horizontal
146 |
147 |
148 |
149 |
150 | Monitors aligned in a grid.
151 | Grid
152 |
153 |
154 |
155 |
156 |
157 |
158 | Graphs
159 |
160 |
161 | Draw smooth graphs
162 |
163 |
164 |
165 |
166 | Show current value
167 |
168 |
169 |
170 |
171 | Temperature units
172 |
173 |
174 |
175 |
176 | Higher means a smoother redraw but also a slight increase in the resources used by the application itself.
177 | Redraw frequency
178 |
179 |
180 |
181 |
182 |
183 |
184 |
--------------------------------------------------------------------------------
/src/gtk/preferences-page-monitors.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | tablet-symbolic
7 | Monitors
8 | Monitors
9 |
10 |
11 | CPU
12 |
13 |
14 |
15 |
16 | GPU
17 |
18 |
19 |
20 |
21 | Memory
22 |
23 |
24 |
25 |
26 | Network
27 |
28 |
29 |
30 |
31 | Disk usage
32 |
33 |
34 |
35 |
36 | Pressure stall quantifies resource scarcity between processes. The more processes are stalled waiting for a resource (because it is being used by something else) the higher the pressure over that resource will be.
37 | Pressure
38 |
39 |
40 |
41 |
42 | These are all the temperature sensors that Monitorets has been able to discover in your setup.
43 | Temperature sensors
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/gtk/preferences-window.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 645
7 | 680
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/gtk/rename-monitor-popover.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 20
12 | 20
13 | 20
14 | 20
15 | vertical
16 | 15
17 |
18 |
19 | title-4
20 | Rename monitor
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | caption
29 | Leaving this text empty will restore the original name.
30 |
31 |
32 |
33 |
34 | end
35 |
36 |
37 | suggested-action
38 | Rename
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/gtk/single-window.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 40
8 | 180
9 |
10 |
11 |
12 |
13 | 10
14 | 10
15 | 10
16 | 10
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/gtk/tips-window.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | True
8 |
9 |
10 | vertical
11 |
12 |
16 |
17 |
18 |
19 | center
20 | True
21 | 25
22 | 25
23 | 25
24 | 25
25 | vertical
26 | 5
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/layout.py:
--------------------------------------------------------------------------------
1 | class Layout:
2 | HORIZONTAL = "horizontal"
3 | VERTICAL = "vertical"
4 | GRID = "grid"
5 |
--------------------------------------------------------------------------------
/src/main.py:
--------------------------------------------------------------------------------
1 | # main.py
2 | #
3 | # Copyright 2022 Jordi Chulia
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | #
18 | # SPDX-License-Identifier: GPL-3.0-or-later
19 |
20 | import sys
21 | import gi
22 |
23 | gi.require_version("Gtk", "4.0")
24 | gi.require_version("Adw", "1")
25 |
26 | from gi.repository import Gio, Adw
27 | from .controller import Controller
28 | from .ui.preferences.preferences_window import PreferencesWindow
29 | from .ui.single_window import SingleWindow
30 | from .ui.tips_window import TipsWindow
31 | from . import discover_temperature_monitors
32 | from .translators import translators_credits
33 | from . import events
34 | from .event_broker import EventBroker
35 |
36 |
37 | class MonitorApplication(Adw.Application):
38 | """The main application singleton class."""
39 |
40 | def __init__(self):
41 | super().__init__(
42 | application_id="io.github.jorchube.monitorets",
43 | flags=Gio.ApplicationFlags.FLAGS_NONE,
44 | resource_base_path="/io/github/jorchube/monitorets",
45 | )
46 |
47 | self.window = None
48 |
49 | self.create_action("quit", self.on_quit, ["q"])
50 | self.create_action("about", self.on_about_action)
51 | self.create_action("tips", self.on_tips_action)
52 | self.create_action(
53 | "preferences", self.on_preferences_action, ["comma"]
54 | )
55 |
56 | self._discover_dynamic_monitors()
57 |
58 | Controller.initialize(application=self)
59 |
60 | EventBroker.subscribe(events.CLOSE_APPLICATION_REQUESTED, self.on_quit)
61 |
62 | def do_activate(self):
63 | """Called when the application is activated.
64 |
65 | We raise the application's main window, creating it if
66 | necessary.
67 | """
68 |
69 | self.window = SingleWindow(application=self)
70 | self.window.present()
71 | Controller.show_monitors()
72 |
73 | def on_about_action(self, widget, _):
74 | """Callback for the app.about action."""
75 | about = Adw.AboutWindow(
76 | transient_for=self.props.active_window,
77 | application_name="Monitorets",
78 | application_icon="io.github.jorchube.monitorets",
79 | developer_name="Jordi Chulia",
80 | version="0.10.0",
81 | developers=["Jordi Chulia"],
82 | copyright="© 2022 Jordi Chulia",
83 | translator_credits=translators_credits.strip(),
84 | )
85 | about.present()
86 |
87 | def on_tips_action(self, widget, _):
88 | tips_window = TipsWindow(transient_for=self.props.active_window)
89 | tips_window.present()
90 |
91 | def on_quit(self, *args, **kwargs):
92 | Controller.stop_all_monitors()
93 | for window in self.get_windows():
94 | window.close()
95 | self.quit()
96 |
97 | def on_preferences_action(self, widget, _):
98 | """Callback for the app.preferences action."""
99 | print("app.preferences action activated")
100 | preferences_window = PreferencesWindow(transient_for=self.props.active_window)
101 | preferences_window.present()
102 |
103 | def create_action(self, name, callback, shortcuts=None):
104 | """Add an application action.
105 |
106 | Args:
107 | name: the name of the action
108 | callback: the function to be called when the action is
109 | activated
110 | shortcuts: an optional list of accelerators
111 | """
112 | action = Gio.SimpleAction.new(name, None)
113 | action.connect("activate", callback)
114 | self.add_action(action)
115 | if shortcuts:
116 | self.set_accels_for_action(f"app.{name}", shortcuts)
117 |
118 | def _discover_dynamic_monitors(self):
119 | discover_temperature_monitors.execute()
120 |
121 |
122 | def main(version):
123 | """The application's entry point."""
124 | app = MonitorApplication()
125 | return app.run(sys.argv)
126 |
--------------------------------------------------------------------------------
/src/meson.build:
--------------------------------------------------------------------------------
1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
2 | moduledir = join_paths(pkgdatadir, 'monitorets')
3 | gnome = import('gnome')
4 |
5 | gnome.compile_resources('monitorets',
6 | 'monitorets.gresource.xml',
7 | gresource_bundle: true,
8 | install: true,
9 | install_dir: pkgdatadir,
10 | )
11 |
12 | python = import('python')
13 |
14 | conf = configuration_data()
15 | conf.set('PYTHON', python.find_installation('python3').path())
16 | conf.set('VERSION', meson.project_version())
17 | conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir')))
18 | conf.set('pkgdatadir', pkgdatadir)
19 |
20 | configure_file(
21 | input: 'monitorets.in',
22 | output: 'monitorets',
23 | configuration: conf,
24 | install: true,
25 | install_dir: get_option('bindir')
26 | )
27 |
28 | monitorets_sources = [
29 | '__init__.py',
30 | 'main.py',
31 | 'event_broker.py',
32 | 'events.py',
33 | 'monitor_type.py',
34 | 'preferences.py',
35 | 'preference_keys.py',
36 | 'controller.py',
37 | 'theming.py',
38 | 'theme.py',
39 | 'layout.py',
40 | 'monitor_descriptors.py',
41 | 'discover_temperature_monitors.py',
42 | 'translators.py',
43 | 'units.py',
44 | 'network_monitor_scale_manager.py',
45 | 'window_geometry.py',
46 | 'temperature.py',
47 | 'monitor_redraw_frequency_seconds_values.py',
48 | ]
49 |
50 | install_data(monitorets_sources, install_dir: moduledir)
51 |
52 | install_subdir('translatable_strings', install_dir: moduledir)
53 | install_subdir('ui', install_dir: moduledir)
54 | install_subdir('samplers', install_dir: moduledir)
55 | install_subdir('monitors', install_dir: moduledir)
56 | install_subdir('temperature_sensors', install_dir: moduledir)
57 |
--------------------------------------------------------------------------------
/src/monitor_descriptors.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 |
3 | from .monitor_type import MonitorType
4 | from .preference_keys import PreferenceKeys
5 | from .ui.monitor_widgets.cpu_monitor_widget import CpuMonitorWidget
6 | from .ui.monitor_widgets.cpu_per_core_monitor_widget import CpuPerCoreMonitorWidget
7 | from .ui.monitor_widgets.gpu_monitor_widget import GpuMonitorWidget
8 | from .ui.monitor_widgets.memory_monitor_widget import MemoryMonitorWidget
9 | from .ui.monitor_widgets.swap_monitor_widget import SwapMonitorWidget
10 | from .ui.monitor_widgets.downlink_monitor_widget import DownlinkMonitorWidget
11 | from .ui.monitor_widgets.uplink_monitor_widget import UplinkMonitorWidget
12 | from .ui.monitor_widgets.home_usage_monitor_widget import HomeUsageMonitorWidget
13 | from .ui.monitor_widgets.root_usage_monitor_widget import RootUsageMonitorWidget
14 | from .ui.monitor_widgets.cpu_pressure_monitor_widget import CpuPressureMonitorWidget
15 | from .ui.monitor_widgets.memory_pressure_monitor_widget import (
16 | MemoryPressureMonitorWidget,
17 | )
18 | from .ui.monitor_widgets.io_pressure_monitor_widget import IOPressureMonitorWidget
19 | from .translatable_strings import (
20 | preference_toggle_label,
21 | preference_toggle_description,
22 | preference_toggle_section_name,
23 | )
24 |
25 |
26 | monitor_descriptor_list = [
27 | {
28 | "type": MonitorType.CPU,
29 | "enabled_preference_key": PreferenceKeys.CPU_MONITOR_ENABLED,
30 | "monitor_class": CpuMonitorWidget,
31 | "preference_toggle_label": preference_toggle_label.CPU,
32 | "preference_toggle_description": None,
33 | "preference_toggle_section_name": preference_toggle_section_name.CPU,
34 | "default_order": 1,
35 | },
36 | {
37 | "type": MonitorType.CPU_PER_CORE,
38 | "enabled_preference_key": PreferenceKeys.CPU_PER_CORE_MONITOR_ENABLED,
39 | "monitor_class": CpuPerCoreMonitorWidget,
40 | "preference_toggle_label": preference_toggle_label.CPU_PER_CORE,
41 | "preference_toggle_description": preference_toggle_description.CPU_PER_CORE,
42 | "preference_toggle_section_name": preference_toggle_section_name.CPU,
43 | "default_order": 2,
44 | },
45 | {
46 | "type": MonitorType.GPU,
47 | "enabled_preference_key": PreferenceKeys.GPU_MONITOR_ENABLED,
48 | "monitor_class": GpuMonitorWidget,
49 | "preference_toggle_label": preference_toggle_label.GPU,
50 | "preference_toggle_description": preference_toggle_description.GPU,
51 | "preference_toggle_section_name": preference_toggle_section_name.GPU,
52 | "default_order": 3,
53 | },
54 | {
55 | "type": MonitorType.Memory,
56 | "enabled_preference_key": PreferenceKeys.MEMORY_MONITOR_ENABLED,
57 | "monitor_class": MemoryMonitorWidget,
58 | "preference_toggle_label": preference_toggle_label.MEMORY,
59 | "preference_toggle_description": None,
60 | "preference_toggle_section_name": preference_toggle_section_name.MEMORY,
61 | "default_order": 4,
62 | },
63 | {
64 | "type": MonitorType.Swap,
65 | "enabled_preference_key": PreferenceKeys.SWAP_MONITOR_ENABLED,
66 | "monitor_class": SwapMonitorWidget,
67 | "preference_toggle_label": preference_toggle_label.SWAP,
68 | "preference_toggle_description": None,
69 | "preference_toggle_section_name": preference_toggle_section_name.MEMORY,
70 | "default_order": 5,
71 | },
72 | {
73 | "type": MonitorType.Downlink,
74 | "enabled_preference_key": PreferenceKeys.DOWNLINK_MONITOR_ENABLED,
75 | "monitor_class": DownlinkMonitorWidget,
76 | "preference_toggle_label": preference_toggle_label.DOWNLINK,
77 | "preference_toggle_description": None,
78 | "preference_toggle_section_name": preference_toggle_section_name.NETWORK,
79 | "default_order": 6,
80 | },
81 | {
82 | "type": MonitorType.Uplink,
83 | "enabled_preference_key": PreferenceKeys.UPLINK_MONITOR_ENABLED,
84 | "monitor_class": UplinkMonitorWidget,
85 | "preference_toggle_label": preference_toggle_label.UPLINK,
86 | "preference_toggle_description": None,
87 | "preference_toggle_section_name": preference_toggle_section_name.NETWORK,
88 | "default_order": 7,
89 | },
90 | {
91 | "type": MonitorType.Home_usage,
92 | "enabled_preference_key": PreferenceKeys.HOME_USAGE_MONITOR_ENABLED,
93 | "monitor_class": HomeUsageMonitorWidget,
94 | "preference_toggle_label": preference_toggle_label.HOME_FOLDER_USAGE,
95 | "preference_toggle_description": None,
96 | "preference_toggle_section_name": preference_toggle_section_name.DISK_USAGE,
97 | "default_order": 8,
98 | },
99 | {
100 | "type": MonitorType.Root_usage,
101 | "enabled_preference_key": PreferenceKeys.ROOT_USAGE_MONITOR_ENABLED,
102 | "monitor_class": RootUsageMonitorWidget,
103 | "preference_toggle_label": preference_toggle_label.ROOT_FOLDER_USAGE,
104 | "preference_toggle_description": None,
105 | "preference_toggle_section_name": preference_toggle_section_name.DISK_USAGE,
106 | "default_order": 9,
107 | },
108 | {
109 | "type": MonitorType.CPU_PRESSURE,
110 | "enabled_preference_key": PreferenceKeys.CPU_PRESSURE_MONITOR_ENABLED,
111 | "monitor_class": CpuPressureMonitorWidget,
112 | "preference_toggle_label": preference_toggle_label.CPU_PRESSURE,
113 | "preference_toggle_description": None,
114 | "preference_toggle_section_name": preference_toggle_section_name.PRESSURE,
115 | "default_order": 10,
116 | },
117 | {
118 | "type": MonitorType.MEMORY_PRESSURE,
119 | "enabled_preference_key": PreferenceKeys.MEMORY_PRESSURE_MONITOR_ENABLED,
120 | "monitor_class": MemoryPressureMonitorWidget,
121 | "preference_toggle_label": preference_toggle_label.MEMORY_PRESSURE,
122 | "preference_toggle_description": None,
123 | "preference_toggle_section_name": preference_toggle_section_name.PRESSURE,
124 | "default_order": 11,
125 | },
126 | {
127 | "type": MonitorType.IO_PRESSURE,
128 | "enabled_preference_key": PreferenceKeys.IO_PRESSURE_MONITOR_ENABLED,
129 | "monitor_class": IOPressureMonitorWidget,
130 | "preference_toggle_label": preference_toggle_label.IO_PRESSURE,
131 | "preference_toggle_description": None,
132 | "preference_toggle_section_name": preference_toggle_section_name.PRESSURE,
133 | "default_order": 12,
134 | },
135 | ]
136 |
137 |
138 | def get_monitor_descriptors_grouped_by_preference_toggle_section():
139 | grouped_descriptors = {"toplevel": list(), "section": defaultdict(list)}
140 |
141 | grouped_descriptors = defaultdict(list)
142 |
143 | for descriptor in monitor_descriptor_list:
144 | grouped_descriptors[descriptor["preference_toggle_section_name"]].append(
145 | descriptor
146 | )
147 |
148 | return grouped_descriptors
149 |
150 |
151 | def get_ordering_dict():
152 | ordering = dict()
153 | for descriptor in monitor_descriptor_list:
154 | ordering[descriptor["type"]] = descriptor["default_order"]
155 |
156 | return ordering
157 |
158 |
159 | def register_monitor_descriptor(new_descriptor):
160 | new_descriptor["default_order"] = len(monitor_descriptor_list) + 1
161 | monitor_descriptor_list.append(new_descriptor)
162 |
--------------------------------------------------------------------------------
/src/monitor_redraw_frequency_seconds_values.py:
--------------------------------------------------------------------------------
1 | VERY_HIGH = 0.05
2 | HIGH = 0.1
3 | LOW = 0.5
4 | VERY_LOW = 1
5 |
--------------------------------------------------------------------------------
/src/monitor_type.py:
--------------------------------------------------------------------------------
1 | class MonitorType:
2 | CPU = "cpu"
3 | CPU_PER_CORE = "cpu per core"
4 | GPU = "gpu"
5 | Memory = "memory"
6 | Swap = "swap"
7 | Downlink = "downlink"
8 | Uplink = "uplink"
9 | Home_usage = "home usage"
10 | Root_usage = "root usage"
11 | CPU_PRESSURE = "cpu pressure"
12 | MEMORY_PRESSURE = "memory pressure"
13 | IO_PRESSURE = "io pressure"
14 |
--------------------------------------------------------------------------------
/src/monitorets.gresource.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | gtk/help-overlay.ui
5 | gtk/single-window.ui
6 | gtk/tips-window.ui
7 | gtk/preferences-window.ui
8 | gtk/preferences-page-appearance.ui
9 | gtk/preferences-page-monitors.ui
10 | gtk/rename-monitor-popover.ui
11 | gtk/main-menu-model.ui
12 | gtk/icons/system.png
13 | gtk/icons/light.png
14 | gtk/icons/dark.png
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/monitorets.in:
--------------------------------------------------------------------------------
1 | #!@PYTHON@
2 |
3 | # monitorets.in
4 | #
5 | # Copyright 2022 Jordi Chulia
6 | #
7 | # This program is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with this program. If not, see .
19 | #
20 | # SPDX-License-Identifier: GPL-3.0-or-later
21 |
22 | import os
23 | import sys
24 | import signal
25 | import locale
26 | import gettext
27 |
28 | VERSION = '@VERSION@'
29 | pkgdatadir = '@pkgdatadir@'
30 | localedir = '@localedir@'
31 |
32 | sys.path.insert(1, pkgdatadir)
33 | signal.signal(signal.SIGINT, signal.SIG_DFL)
34 | locale.bindtextdomain('monitorets', localedir)
35 | locale.textdomain('monitorets')
36 | gettext.install('monitorets', localedir)
37 | gettext.bindtextdomain('monitorets', localedir)
38 | gettext.textdomain('monitorets')
39 |
40 | if __name__ == '__main__':
41 | import gi
42 |
43 | from gi.repository import Gio
44 | resource = Gio.Resource.load(os.path.join(pkgdatadir, 'monitorets.gresource'))
45 | resource._register()
46 |
47 | from monitorets import main
48 | sys.exit(main.main(VERSION))
49 |
--------------------------------------------------------------------------------
/src/monitors/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/src/monitors/__init__.py
--------------------------------------------------------------------------------
/src/monitors/cpu_monitor.py:
--------------------------------------------------------------------------------
1 | from .monitor import Monitor
2 | from ..samplers.cpu_sampler import CpuSampler
3 |
4 |
5 | class CpuMonitor(Monitor):
6 | def __init__(self):
7 | sampler = CpuSampler()
8 | super().__init__(sampler)
9 |
--------------------------------------------------------------------------------
/src/monitors/cpu_per_core_monitor.py:
--------------------------------------------------------------------------------
1 | from .monitor import Monitor
2 | from ..samplers.cpu_per_core_sampler import CpuPerCoreSampler
3 |
4 |
5 | class CpuPerCoreMonitor(Monitor):
6 | def __init__(self):
7 | sampler = CpuPerCoreSampler()
8 | super().__init__(sampler)
9 |
10 | def _report_values(self):
11 | values = list(zip(*self._graph_values))
12 | self._new_values_callback(values, self._last_readable_value)
13 |
--------------------------------------------------------------------------------
/src/monitors/cpu_pressure_monitor.py:
--------------------------------------------------------------------------------
1 | from .monitor import Monitor
2 | from ..samplers.cpu_pressure_sampler import CpuPressureSampler
3 |
4 |
5 | class CpuPressureMonitor(Monitor):
6 | def __init__(self):
7 | sampler = CpuPressureSampler()
8 | super().__init__(sampler)
9 |
--------------------------------------------------------------------------------
/src/monitors/downlink_monitor.py:
--------------------------------------------------------------------------------
1 | from .monitor import Monitor
2 | from ..samplers.downlink_sampler import DownlinkSampler
3 |
4 |
5 | class DownlinkMonitor(Monitor):
6 | def __init__(self):
7 | sampler = DownlinkSampler()
8 | super().__init__(sampler)
9 |
--------------------------------------------------------------------------------
/src/monitors/gpu_monitor.py:
--------------------------------------------------------------------------------
1 | from .monitor import Monitor
2 | from ..samplers.gpu_sampler import GpuSampler
3 |
4 |
5 | class GpuMonitor(Monitor):
6 | def __init__(self):
7 | sampler = GpuSampler("/sys/class/drm/card0/device/gpu_busy_percent")
8 | super().__init__(sampler)
9 |
--------------------------------------------------------------------------------
/src/monitors/home_usage_monitor.py:
--------------------------------------------------------------------------------
1 | from .monitor import Monitor
2 | from pathlib import Path
3 | from ..samplers.disk_usage_sampler import DiskUsageSampler
4 |
5 |
6 | class HomeUsageMonitor(Monitor):
7 | def __init__(self):
8 | sampler = DiskUsageSampler(self._home_path())
9 | super().__init__(sampler)
10 |
11 | def _home_path(self):
12 | return Path.home().absolute().as_posix()
13 |
--------------------------------------------------------------------------------
/src/monitors/io_pressure_monitor.py:
--------------------------------------------------------------------------------
1 | from .monitor import Monitor
2 | from ..samplers.io_pressure_sampler import IOPressureSampler
3 |
4 |
5 | class IOPressureMonitor(Monitor):
6 | def __init__(self):
7 | sampler = IOPressureSampler()
8 | super().__init__(sampler)
9 |
--------------------------------------------------------------------------------
/src/monitors/memory_monitor.py:
--------------------------------------------------------------------------------
1 | from .monitor import Monitor
2 | from ..samplers.memory_sampler import MemorySampler
3 |
4 |
5 | class MemoryMonitor(Monitor):
6 | def __init__(self):
7 | sampler = MemorySampler()
8 | super().__init__(sampler)
9 |
--------------------------------------------------------------------------------
/src/monitors/memory_pressure_monitor.py:
--------------------------------------------------------------------------------
1 | from .monitor import Monitor
2 | from ..samplers.memory_pressure_sampler import MemoryPressureSampler
3 |
4 |
5 | class MemoryPressureMonitor(Monitor):
6 | def __init__(self):
7 | sampler = MemoryPressureSampler()
8 | super().__init__(sampler)
9 |
--------------------------------------------------------------------------------
/src/monitors/monitor.py:
--------------------------------------------------------------------------------
1 | class Monitor:
2 | _DEFAULT_MAX_NUMBER_OF_VALUES_STORED = 55
3 | _EXTRA_BUFFER_OF_STORED_SAMPLES = 2
4 |
5 | def __init__(self, sampler):
6 | self._sampler = sampler
7 | self._sampler.install_new_sample_callback(self._new_sample)
8 | self._new_values_callback = None
9 | self._graph_values = []
10 | self._max_values_stored = self._DEFAULT_MAX_NUMBER_OF_VALUES_STORED
11 |
12 | def start(self):
13 | self._sampler.start()
14 |
15 | def stop(self):
16 | self._sampler.stop()
17 |
18 | def install_new_values_callback(self, cb):
19 | self._new_values_callback = cb
20 |
21 | def set_max_number_of_stored_samples(self, number):
22 | self._max_values_stored = number + self._EXTRA_BUFFER_OF_STORED_SAMPLES
23 |
24 | def _new_sample(self, sample):
25 | self._last_readable_value = sample.label_value
26 | self._graph_values.insert(0, sample.to_plot)
27 | if self._new_values_callback:
28 | self._report_values()
29 |
30 | if self._has_reached_max_values_stored():
31 | self._free_old_values()
32 |
33 | def _report_values(self):
34 | self._new_values_callback(self._graph_values, self._last_readable_value)
35 |
36 | def _has_reached_max_values_stored(self):
37 | return len(self._graph_values) > self._max_values_stored
38 |
39 | def _free_old_values(self):
40 | self._graph_values = self._graph_values[: self._max_values_stored]
41 |
--------------------------------------------------------------------------------
/src/monitors/root_usage_monitor.py:
--------------------------------------------------------------------------------
1 | from .monitor import Monitor
2 | from ..samplers.disk_usage_sampler import DiskUsageSampler
3 |
4 |
5 | class RootUsageMonitor(Monitor):
6 | def __init__(self):
7 | sampler = DiskUsageSampler("/")
8 | super().__init__(sampler)
9 |
--------------------------------------------------------------------------------
/src/monitors/swap_monitor.py:
--------------------------------------------------------------------------------
1 | from .monitor import Monitor
2 | from ..samplers.swap_sampler import SwapSampler
3 |
4 |
5 | class SwapMonitor(Monitor):
6 | def __init__(self):
7 | sampler = SwapSampler()
8 | super().__init__(sampler)
9 |
--------------------------------------------------------------------------------
/src/monitors/temperature_monitor.py:
--------------------------------------------------------------------------------
1 | from .monitor import Monitor
2 | from ..samplers.temperature_sensor_sampler import TemperatureSensorSampler
3 | from ..event_broker import EventBroker
4 | from .. import events
5 | from ..preference_keys import PreferenceKeys
6 | from ..temperature import FAHRENHEIT
7 | from ..preferences import Preferences
8 |
9 |
10 | class TemperatureMonitor(Monitor):
11 | def __init__(self, temperature_sensor_descriptor):
12 | sampler = TemperatureSensorSampler(temperature_sensor_descriptor)
13 | super().__init__(sampler)
14 |
15 | temperature_units = Preferences.get(PreferenceKeys.TEMPERATURE_UNITS)
16 | self._set_temperature_units(temperature_units)
17 | EventBroker.subscribe(events.PREFERENCES_CHANGED, self._on_preference_changed)
18 |
19 | def _on_preference_changed(self, preference_key, value):
20 | if preference_key == PreferenceKeys.TEMPERATURE_UNITS:
21 | self._set_temperature_units(value)
22 |
23 | def _set_temperature_units(self, units):
24 | if units == FAHRENHEIT:
25 | self._sampler.set_fahrenheit()
26 | else:
27 | self._sampler.set_celsius()
28 |
--------------------------------------------------------------------------------
/src/monitors/uplink_monitor.py:
--------------------------------------------------------------------------------
1 | from .monitor import Monitor
2 | from ..samplers.uplink_sampler import UplinkSampler
3 |
4 |
5 | class UplinkMonitor(Monitor):
6 | def __init__(self):
7 | sampler = UplinkSampler()
8 | super().__init__(sampler)
9 |
--------------------------------------------------------------------------------
/src/network_monitor_scale_manager.py:
--------------------------------------------------------------------------------
1 | from .event_broker import EventBroker
2 | from . import events
3 | from .monitor_type import MonitorType
4 | from .preferences import Preferences
5 | from .preference_keys import PreferenceKeys
6 |
7 |
8 | class NetworkMonitorScaleManager:
9 | @classmethod
10 | def initialize(self):
11 | self._current_value = 0
12 | self._current_downlink_value = 0
13 | self._current_uplink_value = 0
14 |
15 | EventBroker.subscribe(events.MONITOR_ENABLED, self._on_monitor_enabled)
16 | EventBroker.subscribe(events.MONITOR_DISABLED, self._on_monitor_disabled)
17 | EventBroker.subscribe(
18 | events.DOWNLINK_NETWORK_MONITOR_NEW_REFERENCE_VALUE_PROPOSAL,
19 | self._new_downlink_monitor_value,
20 | )
21 | EventBroker.subscribe(
22 | events.UPLINK_NETWORK_MONITOR_NEW_REFERENCE_VALUE_PROPOSAL,
23 | self._new_uplink_monitor_value,
24 | )
25 |
26 | @classmethod
27 | def _new_downlink_monitor_value(self, value):
28 | self._current_downlink_value = value
29 | self._new_value_received()
30 |
31 | @classmethod
32 | def _new_uplink_monitor_value(self, value):
33 | self._current_uplink_value = value
34 | self._new_value_received()
35 |
36 | @classmethod
37 | def _new_value_received(self):
38 | candidate_value = max(self._current_downlink_value, self._current_uplink_value)
39 |
40 | if candidate_value != self._current_value:
41 | self._current_value = candidate_value
42 | EventBroker.notify(
43 | events.NETWORK_MONITOR_NEW_REFERENCE_VALUE, self._current_value
44 | )
45 |
46 | @classmethod
47 | def _on_monitor_enabled(self, monitor):
48 | if monitor in [MonitorType.Uplink, MonitorType.Downlink]:
49 | self._refresh_use_shared_scaling_preference_value()
50 |
51 | @classmethod
52 | def _on_monitor_disabled(self, monitor):
53 | if monitor in [MonitorType.Uplink, MonitorType.Downlink]:
54 | self._refresh_use_shared_scaling_preference_value()
55 |
56 | @classmethod
57 | def _refresh_use_shared_scaling_preference_value(self):
58 | if (
59 | Preferences.get(PreferenceKeys.UPLINK_MONITOR_ENABLED) is False
60 | or Preferences.get(PreferenceKeys.DOWNLINK_MONITOR_ENABLED) is False
61 | ):
62 | Preferences.set(
63 | PreferenceKeys.UNIFIED_SCALE_FOR_NETWORK_MONITORS_ENABLED, False
64 | )
65 | return
66 |
67 | Preferences.set(PreferenceKeys.UNIFIED_SCALE_FOR_NETWORK_MONITORS_ENABLED, True)
68 |
--------------------------------------------------------------------------------
/src/preference_keys.py:
--------------------------------------------------------------------------------
1 | class PreferenceKeys:
2 | THEME = "general.theme"
3 | LAYOUT = "general.layout"
4 | SMOOTH_GRAPH = "general.smooth_graph"
5 | SHOW_CURRENT_VALUE = "general.show_current_value"
6 | WINDOW_GEOMETRY = "general.window_geometry"
7 | TEMPERATURE_UNITS = "general.temperature_units"
8 | REDRAW_FREQUENCY_SECONDS = "general.redraw_frequency_seconds"
9 | CPU_MONITOR_ENABLED = "cpu_monitor.enabled"
10 | CPU_PER_CORE_MONITOR_ENABLED = "cpu_per_core_monitor.enabled"
11 | GPU_MONITOR_ENABLED = "gpu_monitor.enabled"
12 | MEMORY_MONITOR_ENABLED = "memory_monitor.enabled"
13 | SWAP_MONITOR_ENABLED = "swap_monitor.enabled"
14 | DOWNLINK_MONITOR_ENABLED = "downlink_monitor.enabled"
15 | UPLINK_MONITOR_ENABLED = "uplink_monitor.enabled"
16 | HOME_USAGE_MONITOR_ENABLED = "home_usage_monitor.enabled"
17 | ROOT_USAGE_MONITOR_ENABLED = "root_usage_monitor.enabled"
18 | CPU_PRESSURE_MONITOR_ENABLED = "cpu_pressure_monitor.enabled"
19 | MEMORY_PRESSURE_MONITOR_ENABLED = "memory_pressure_monitor.enabled"
20 | IO_PRESSURE_MONITOR_ENABLED = "io_pressure_monitor.enabled"
21 | UNIFIED_SCALE_FOR_NETWORK_MONITORS_ENABLED = "network.unified_scale.enabled"
22 |
--------------------------------------------------------------------------------
/src/preferences.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | from xdg import xdg_config_home
4 |
5 | from .event_broker import EventBroker
6 | from . import events
7 | from .preference_keys import PreferenceKeys
8 | from .theme import Theme
9 | from .layout import Layout
10 | from .window_geometry import WindowGeometry
11 | from .temperature import CELSIUS
12 | from . import monitor_redraw_frequency_seconds_values
13 |
14 |
15 | class Preferences:
16 | _folder_name = "io.github.jorchube.monitorets"
17 | _file_name = "preferences.json"
18 |
19 | _default_preferences = {
20 | PreferenceKeys.THEME: Theme.SYSTEM,
21 | PreferenceKeys.LAYOUT: Layout.VERTICAL,
22 | PreferenceKeys.SMOOTH_GRAPH: True,
23 | PreferenceKeys.SHOW_CURRENT_VALUE: False,
24 | PreferenceKeys.TEMPERATURE_UNITS: CELSIUS,
25 | PreferenceKeys.REDRAW_FREQUENCY_SECONDS: monitor_redraw_frequency_seconds_values.HIGH,
26 | PreferenceKeys.WINDOW_GEOMETRY: WindowGeometry(width=180, height=40).as_dict(),
27 | PreferenceKeys.CPU_MONITOR_ENABLED: True,
28 | PreferenceKeys.CPU_PER_CORE_MONITOR_ENABLED: False,
29 | PreferenceKeys.GPU_MONITOR_ENABLED: False,
30 | PreferenceKeys.MEMORY_MONITOR_ENABLED: True,
31 | PreferenceKeys.SWAP_MONITOR_ENABLED: False,
32 | PreferenceKeys.DOWNLINK_MONITOR_ENABLED: False,
33 | PreferenceKeys.UPLINK_MONITOR_ENABLED: False,
34 | PreferenceKeys.HOME_USAGE_MONITOR_ENABLED: False,
35 | PreferenceKeys.ROOT_USAGE_MONITOR_ENABLED: False,
36 | PreferenceKeys.CPU_PRESSURE_MONITOR_ENABLED: False,
37 | PreferenceKeys.MEMORY_PRESSURE_MONITOR_ENABLED: False,
38 | PreferenceKeys.IO_PRESSURE_MONITOR_ENABLED: False,
39 | PreferenceKeys.UNIFIED_SCALE_FOR_NETWORK_MONITORS_ENABLED: False,
40 | "gpu_monitor.sampling_frequency_seconds": 0.1,
41 | "cpu_monitor.sampling_frequency_seconds": 0.1,
42 | "memory_monitor.sampling_frequency_seconds": 0.1,
43 | "custom_name": {},
44 | }
45 |
46 | _preferences = dict()
47 | _custom_key_handler = dict()
48 |
49 | @classmethod
50 | def initialize(self):
51 | self._custom_key_handler[PreferenceKeys.WINDOW_GEOMETRY] = {
52 | "set": self._set_window_geometry,
53 | "get": self._get_window_geometry,
54 | }
55 |
56 | @classmethod
57 | def get(self, preference_path):
58 | if preference_path in self._custom_key_handler:
59 | return self._custom_key_handler[preference_path]["get"]()
60 |
61 | return self._default_get_handler(preference_path)
62 |
63 | @classmethod
64 | def set(self, preference_path, value):
65 | if preference_path in self._custom_key_handler:
66 | self._custom_key_handler[preference_path]["set"](value)
67 | return
68 |
69 | self._default_set_handler(preference_path, value)
70 |
71 | @classmethod
72 | def _default_set_handler(self, preference_path, value):
73 | self._preferences[preference_path] = value
74 | self._persist_preferences()
75 | EventBroker.notify(events.PREFERENCES_CHANGED, preference_path, value)
76 |
77 | @classmethod
78 | def _default_get_handler(self, preference_path):
79 | return self._preferences[preference_path]
80 |
81 | @classmethod
82 | def get_custom_name(self, monitor_type):
83 | return self._preferences["custom_name"].get(monitor_type)
84 |
85 | @classmethod
86 | def set_custom_name(self, monitor_type, name):
87 | self._preferences["custom_name"][monitor_type] = name
88 | self._persist_preferences()
89 | EventBroker.notify(events.MONITOR_RENAMED, monitor_type, name)
90 |
91 | @classmethod
92 | def _persist_preferences(self):
93 | file_path = self._build_file_path()
94 | self._write_preferences(self._preferences, file_path)
95 |
96 | @classmethod
97 | def load(self):
98 | file_path = self._build_file_path()
99 | if not self._file_exists(file_path):
100 | self._write_preferences(self._default_preferences, file_path)
101 |
102 | self._preferences = self._default_preferences | self._read_preferences(
103 | file_path
104 | )
105 |
106 | self._migrate_deprecated_adaptive_layout_value()
107 |
108 | @classmethod
109 | def _read_preferences(self, file_path):
110 | json_content = self._read_file(file_path)
111 | return json.loads(json_content)
112 |
113 | @classmethod
114 | def _write_preferences(self, preferences, file_path):
115 | json_default_preferences = json.dumps(preferences)
116 | self._write_file(file_path, json_default_preferences)
117 |
118 | @classmethod
119 | def _file_exists(self, file_path):
120 | return file_path.exists()
121 |
122 | @classmethod
123 | def _read_file(self, file_path):
124 | return file_path.read_text()
125 |
126 | @classmethod
127 | def _write_file(self, file_path, content):
128 | os.makedirs(file_path.parent, exist_ok=True)
129 | file_path.write_text(content)
130 |
131 | @classmethod
132 | def _build_file_path(self):
133 | base = xdg_config_home()
134 | full_path = base / self._folder_name / self._file_name
135 | return full_path
136 |
137 | @classmethod
138 | def register_preference_key_default(self, key, default_value):
139 | self._default_preferences[key] = default_value
140 |
141 | @classmethod
142 | def _migrate_deprecated_adaptive_layout_value(self):
143 | if Preferences.get(PreferenceKeys.LAYOUT) == "adaptive":
144 | Preferences.set(PreferenceKeys.LAYOUT, Layout.VERTICAL)
145 |
146 | @classmethod
147 | def _set_window_geometry(self, window_geometry):
148 | self._default_set_handler(
149 | PreferenceKeys.WINDOW_GEOMETRY, window_geometry.as_dict()
150 | )
151 |
152 | @classmethod
153 | def _get_window_geometry(self):
154 | window_geometry_dict = self._default_get_handler(PreferenceKeys.WINDOW_GEOMETRY)
155 | return WindowGeometry.from_dict(window_geometry_dict)
156 |
--------------------------------------------------------------------------------
/src/samplers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/src/samplers/__init__.py
--------------------------------------------------------------------------------
/src/samplers/cpu_per_core_sampler.py:
--------------------------------------------------------------------------------
1 | import psutil
2 | from statistics import mean
3 |
4 | from .sampler import Sampler
5 | from .sample import Sample
6 |
7 |
8 | class CpuPerCoreSampler(Sampler):
9 | def __init__(self, *args, **kwargs):
10 | super().__init__(*args, **kwargs)
11 |
12 | def _get_sample(self):
13 | value_list = psutil.cpu_percent(percpu=True)
14 |
15 | int_values = list(map(int, value_list))
16 | sample = Sample(
17 | to_plot=int_values, single_value=int(mean(int_values)), units="%"
18 | )
19 |
20 | return sample
21 |
--------------------------------------------------------------------------------
/src/samplers/cpu_pressure_sampler.py:
--------------------------------------------------------------------------------
1 | from .pressure_sampler import PressureSampler
2 |
3 |
4 | class CpuPressureSampler(PressureSampler):
5 | def __init__(self, *args, **kwargs):
6 | super().__init__(pressure_file_path="/proc/pressure/cpu", *args, **kwargs)
7 |
--------------------------------------------------------------------------------
/src/samplers/cpu_sampler.py:
--------------------------------------------------------------------------------
1 | import psutil
2 |
3 | from .sampler import Sampler
4 | from .sample import Sample
5 |
6 |
7 | class CpuSampler(Sampler):
8 | def __init__(self, *args, **kwargs):
9 | super().__init__(*args, **kwargs)
10 |
11 | def _get_sample(self):
12 | value = int(psutil.cpu_percent())
13 | sample = Sample(to_plot=value, single_value=value, units="%")
14 | return sample
15 |
--------------------------------------------------------------------------------
/src/samplers/delta_sampler.py:
--------------------------------------------------------------------------------
1 | from .sampler import Sampler
2 |
3 |
4 | class DeltaSampler(Sampler):
5 | def __init__(self, sampling_frequency_hz=1):
6 | super().__init__(sampling_frequency_hz)
7 | self._previous_value = None
8 |
9 | def process_sample(self, value):
10 | if self._previous_value == None:
11 | self._previous_value = value
12 | return 0
13 |
14 | delta_value = value - self._previous_value
15 | self._previous_value = value
16 |
17 | return delta_value
18 |
--------------------------------------------------------------------------------
/src/samplers/disk_usage_sampler.py:
--------------------------------------------------------------------------------
1 | import psutil
2 |
3 | from .. import units
4 | from .sampler import Sampler
5 | from .sample import Sample
6 |
7 |
8 | class DiskUsageSampler(Sampler):
9 | def __init__(self, path, *args, **kwargs):
10 | super().__init__(*args, **kwargs)
11 | self._path = path
12 |
13 | def _get_sample(self):
14 | usage = psutil.disk_usage(self._path)
15 |
16 | value_percent = usage.percent
17 | single_value = units.convert(usage.used, units.Byte, units.GiB)
18 |
19 | sample = Sample(
20 | to_plot=value_percent,
21 | single_value=round(single_value),
22 | units=units.GiB.unit,
23 | )
24 |
25 | return sample
26 |
--------------------------------------------------------------------------------
/src/samplers/downlink_sampler.py:
--------------------------------------------------------------------------------
1 | import psutil
2 |
3 | from .. import units
4 | from .delta_sampler import DeltaSampler
5 | from .sampler import Sampler
6 | from .sample import Sample
7 |
8 |
9 | class DownlinkSampler(Sampler):
10 | def __init__(self, *args, **kwargs):
11 | super().__init__(*args, **kwargs)
12 | self._delta_sampler = DeltaSampler()
13 |
14 | def _get_sample(self):
15 | per_nic_counters = psutil.net_io_counters(pernic=True)
16 |
17 | counter = 0
18 | for key, value in per_nic_counters.items():
19 | if key != "lo":
20 | counter += value.bytes_recv
21 |
22 | value = int(self._delta_sampler.process_sample(counter))
23 | single_value, unit = self._get_single_value_and_unit(value)
24 |
25 | sample = Sample(
26 | to_plot=value, single_value=round(single_value), units=f"{unit}/s"
27 | )
28 |
29 | return sample
30 |
31 | def _get_single_value_and_unit(self, value):
32 | _units = units.Byte
33 | if value > units.KiB.value:
34 | _units = units.KiB
35 | if value > units.MiB.value:
36 | _units = units.MiB
37 | if value > units.GiB.value:
38 | _units = units.GiB
39 |
40 | return units.convert(value, units.Byte, _units), _units.unit
41 |
--------------------------------------------------------------------------------
/src/samplers/gpu_sampler.py:
--------------------------------------------------------------------------------
1 | from io import SEEK_SET
2 |
3 | from .sampler import Sampler
4 | from .sample import Sample
5 |
6 |
7 | class GpuSampler(Sampler):
8 | def __init__(self, file, *args, **kwargs):
9 | super().__init__(*args, **kwargs)
10 | self._path = file
11 | self._file_handle = open(self._path, "r")
12 |
13 | def _get_sample(self):
14 | value = int(self._read_file(self._file_handle))
15 |
16 | sample = Sample(to_plot=value, single_value=value, units="%")
17 |
18 | return sample
19 |
20 | def _read_file(self, file_handle):
21 | file_handle.seek(0, SEEK_SET)
22 | return file_handle.readline()
23 |
--------------------------------------------------------------------------------
/src/samplers/io_pressure_sampler.py:
--------------------------------------------------------------------------------
1 | from .pressure_sampler import PressureSampler
2 |
3 |
4 | class IOPressureSampler(PressureSampler):
5 | def __init__(self, *args, **kwargs):
6 | super().__init__(pressure_file_path="/proc/pressure/io", *args, **kwargs)
7 |
--------------------------------------------------------------------------------
/src/samplers/memory_pressure_sampler.py:
--------------------------------------------------------------------------------
1 | from .pressure_sampler import PressureSampler
2 |
3 |
4 | class MemoryPressureSampler(PressureSampler):
5 | def __init__(self, *args, **kwargs):
6 | super().__init__(pressure_file_path="/proc/pressure/memory", *args, **kwargs)
7 |
--------------------------------------------------------------------------------
/src/samplers/memory_sampler.py:
--------------------------------------------------------------------------------
1 | import psutil
2 |
3 | from .. import units
4 | from .sampler import Sampler
5 | from .sample import Sample
6 |
7 |
8 | class MemorySampler(Sampler):
9 | def __init__(self, *args, **kwargs):
10 | super().__init__(*args, **kwargs)
11 |
12 | def _get_sample(self):
13 | vmem = psutil.virtual_memory()
14 |
15 | available = vmem.available
16 | total = vmem.total
17 | used = total - available
18 |
19 | percent_value = int((used / total) * 100)
20 | single_value = units.convert(used, units.Byte, units.GiB)
21 |
22 | sample = Sample(
23 | to_plot=percent_value,
24 | single_value=round(single_value, 1),
25 | units=units.GiB.unit,
26 | )
27 |
28 | return sample
29 |
--------------------------------------------------------------------------------
/src/samplers/pressure_sampler.py:
--------------------------------------------------------------------------------
1 | from io import SEEK_SET
2 | import re
3 | from .sampler import Sampler
4 | from .sample import Sample
5 |
6 |
7 | class PressureSampler(Sampler):
8 | def __init__(self, pressure_file_path, *args, **kwargs):
9 | super().__init__(*args, **kwargs)
10 | self._file_handle = open(pressure_file_path, "r")
11 | self._regex = re.compile(".*avg10=(\d+\.\d\d).*")
12 |
13 | def _get_sample(self):
14 | line = self._read_line_from_file(self._file_handle)
15 |
16 | value = int(self._get_avg10_value(line))
17 |
18 | sample = Sample(to_plot=value, single_value=value, units="%")
19 |
20 | return sample
21 |
22 | def _read_line_from_file(self, file_handle):
23 | file_handle.seek(0, SEEK_SET)
24 | return file_handle.readline()
25 |
26 | def _get_avg10_value(self, line):
27 | matches = self._regex.match(line)
28 | return float(matches.group(1))
29 |
--------------------------------------------------------------------------------
/src/samplers/sample.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 |
4 | @dataclass
5 | class Sample:
6 | to_plot: int | list
7 | single_value: int | float
8 | units: str
9 |
10 | @property
11 | def label_value(self):
12 | return f"{self.single_value} {self.units}"
13 |
--------------------------------------------------------------------------------
/src/samplers/sampler.py:
--------------------------------------------------------------------------------
1 | from threading import Thread
2 | from time import sleep
3 |
4 |
5 | class Sampler:
6 | def __init__(self, sampling_frequency_hz=1):
7 | self._sample_callback = None
8 | self._sampling_frequency_seconds = 1 / sampling_frequency_hz
9 | self._task = None
10 | self._is_running = False
11 |
12 | def install_new_sample_callback(self, callback):
13 | self._sample_callback = callback
14 |
15 | def start(self):
16 | self._is_running = True
17 | self._task = Thread(target=self._sample_forever)
18 | self._task.daemon = True
19 | self._task.start()
20 |
21 | def stop(self):
22 | self._is_running = False
23 |
24 | def _sample_forever(self):
25 | while self._is_running:
26 | self._sample()
27 | sleep(self._sampling_frequency_seconds)
28 |
29 | def _sample(self):
30 | value = self._get_sample()
31 | self._sample_callback(value)
32 |
33 | def _get_sample(self):
34 | raise NotImplementedError
35 |
--------------------------------------------------------------------------------
/src/samplers/swap_sampler.py:
--------------------------------------------------------------------------------
1 | import psutil
2 |
3 | from .. import units
4 | from .sampler import Sampler
5 | from .sample import Sample
6 |
7 |
8 | class SwapSampler(Sampler):
9 | def __init__(self, *args, **kwargs):
10 | super().__init__(*args, **kwargs)
11 |
12 | def _get_sample(self):
13 | swap = psutil.swap_memory()
14 |
15 | percent_value = int(swap.percent)
16 | single_value = units.convert(swap.used, units.Byte, units.GiB)
17 |
18 | sample = Sample(
19 | to_plot=percent_value,
20 | single_value=round(single_value, 1),
21 | units=units.GiB.unit,
22 | )
23 |
24 | return sample
25 |
--------------------------------------------------------------------------------
/src/samplers/temperature_sensor_sampler.py:
--------------------------------------------------------------------------------
1 | import psutil
2 |
3 | from .sampler import Sampler
4 | from .sample import Sample
5 |
6 |
7 | class TemperatureSensorSampler(Sampler):
8 | _MAX_CELSIUS = 100
9 | _MAX_FAHRENHEIT = 212
10 |
11 | def __init__(self, temperature_sensor_descriptor, *args, **kwargs):
12 | super().__init__(*args, **kwargs)
13 | self._sensor_descriptor = temperature_sensor_descriptor
14 | self._fahrenheit = False
15 |
16 | def set_celsius(self):
17 | self._fahrenheit = False
18 |
19 | def set_fahrenheit(self):
20 | self._fahrenheit = True
21 |
22 | def _get_sample(self):
23 | discovered_hardware = psutil.sensors_temperatures()
24 | sensor_list = discovered_hardware[self._sensor_descriptor.hardware_name]
25 |
26 | for sensor in sensor_list:
27 | if sensor.label == self._sensor_descriptor.hardware_sensor_name:
28 | sample = self._get_sample_from_sensor(sensor)
29 | return sample
30 |
31 | return Sample(to_plot=0, single_value=0, units="-")
32 |
33 | def _get_sample_from_sensor(self, sensor):
34 | max_default = self._MAX_CELSIUS
35 | units = self._get_units()
36 |
37 | current_temp = sensor.current
38 | max_temp = sensor.high if sensor.high else max_default
39 | temp_as_percent = self._get_temp_as_percent(current_temp, max_temp)
40 |
41 | single_value = current_temp
42 | if self._fahrenheit:
43 | single_value = self._celsius_to_fahrenheit(current_temp)
44 |
45 | sample = Sample(
46 | to_plot=int(temp_as_percent), single_value=round(single_value), units=units
47 | )
48 |
49 | return sample
50 |
51 | def _get_units(self):
52 | return "℉" if self._fahrenheit else "℃"
53 |
54 | def _get_temp_as_percent(self, current_temp, max_temp):
55 | return (current_temp * 100) / max_temp
56 |
57 | def _celsius_to_fahrenheit(self, celsius):
58 | return (celsius * 1.8) + 32
59 |
--------------------------------------------------------------------------------
/src/samplers/uplink_sampler.py:
--------------------------------------------------------------------------------
1 | import psutil
2 |
3 | from .. import units
4 | from .delta_sampler import DeltaSampler
5 | from .sampler import Sampler
6 | from .sample import Sample
7 |
8 |
9 | class UplinkSampler(Sampler):
10 | def __init__(self, *args, **kwargs):
11 | super().__init__(*args, **kwargs)
12 | self._delta_sampler = DeltaSampler()
13 |
14 | def _get_sample(self):
15 | per_nic_counters = psutil.net_io_counters(pernic=True)
16 |
17 | counter = 0
18 | for key, value in per_nic_counters.items():
19 | if key != "lo":
20 | counter += value.bytes_sent
21 |
22 | value = int(self._delta_sampler.process_sample(counter))
23 | single_value, unit = self._get_single_value_and_unit(value)
24 |
25 | sample = Sample(
26 | to_plot=value, single_value=round(single_value), units=f"{unit}/s"
27 | )
28 |
29 | return sample
30 |
31 | def _get_single_value_and_unit(self, value):
32 | _units = units.Byte
33 | if value > units.KiB.value:
34 | _units = units.KiB
35 | if value > units.MiB.value:
36 | _units = units.MiB
37 | if value > units.GiB.value:
38 | _units = units.GiB
39 |
40 | return units.convert(value, units.Byte, _units), _units.unit
41 |
--------------------------------------------------------------------------------
/src/temperature.py:
--------------------------------------------------------------------------------
1 | CELSIUS = "celsius"
2 | FAHRENHEIT = "fahrenheit"
3 |
--------------------------------------------------------------------------------
/src/temperature_sensors/temperature_sensor_descriptor.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 |
4 | @dataclass
5 | class TemperatureSensorDescriptor:
6 | hardware_name: str
7 | hardware_sensor_name: str
8 |
--------------------------------------------------------------------------------
/src/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/src/tests/__init__.py
--------------------------------------------------------------------------------
/src/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from src.event_broker import EventBroker
4 |
5 |
6 | class EventWaiter:
7 | _called = False
8 |
9 | def __init__(self, event):
10 | EventBroker.subscribe(event, self)
11 |
12 | def __call__(self, *args, **kwargs):
13 | self._called = True
14 |
15 | def wait_for_event(self, seconds=1):
16 | limit = time.time() + seconds
17 |
18 | while time.time() < limit and self._called is False:
19 | time.sleep(0.1)
20 |
21 | self._wait_for_other_subscribers()
22 |
23 | return self._called
24 |
25 | def _wait_for_other_subscribers(self):
26 | time.sleep(0.05)
27 |
--------------------------------------------------------------------------------
/src/tests/test_delta_sampler.py:
--------------------------------------------------------------------------------
1 | from ..samplers.delta_sampler import DeltaSampler
2 |
3 |
4 | class TestDeltaSampler:
5 | def test_it_returns_zero_on_first_sample(self):
6 | delta_sampler = DeltaSampler()
7 |
8 | value = delta_sampler.process_sample(1)
9 |
10 | assert value == 0
11 |
12 | def test_it_returns_delta_values_on_subsequent_calls_after_first_one(self):
13 | delta_sampler = DeltaSampler()
14 |
15 | delta_sampler.process_sample(1)
16 |
17 | assert delta_sampler.process_sample(2) == 1
18 | assert delta_sampler.process_sample(5) == 3
19 | assert delta_sampler.process_sample(10) == 5
20 | assert delta_sampler.process_sample(20) == 10
21 |
--------------------------------------------------------------------------------
/src/tests/test_event_broker.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from unittest.mock import Mock, call
3 | from ..event_broker import EventBroker
4 | from .conftest import EventWaiter
5 |
6 |
7 | class TestEventBroker:
8 | @pytest.fixture
9 | def mock_subscription(self):
10 | return Mock()
11 |
12 | @pytest.fixture
13 | def mock_subscription_2(self):
14 | return Mock()
15 |
16 | def test_it_notifies_event_to_a_subscription(
17 | self, mock_subscription, mock_subscription_2
18 | ):
19 | waiter = EventWaiter("some event")
20 |
21 | EventBroker.initialize()
22 | EventBroker.subscribe("some event", mock_subscription)
23 | EventBroker.subscribe("a different event", mock_subscription_2)
24 |
25 | EventBroker.notify("some event")
26 |
27 | assert waiter.wait_for_event()
28 |
29 | mock_subscription.assert_called_once()
30 |
31 | def test_it_notifies_event_to_many_subscriptions(
32 | self, mock_subscription, mock_subscription_2
33 | ):
34 | waiter = EventWaiter("another event")
35 |
36 | EventBroker.initialize()
37 | EventBroker.subscribe("another event", mock_subscription)
38 | EventBroker.subscribe("another event", mock_subscription_2)
39 |
40 | EventBroker.notify("another event")
41 |
42 | assert waiter.wait_for_event()
43 |
44 | mock_subscription.assert_called_once()
45 | mock_subscription_2.assert_called_once()
46 |
47 | def test_it_notifies_many_events_to_a_subscription(self, mock_subscription):
48 | waiter1 = EventWaiter("some event")
49 | waiter2 = EventWaiter("a different event")
50 |
51 | EventBroker.initialize()
52 | EventBroker.subscribe("some event", mock_subscription)
53 | EventBroker.subscribe("a different event", mock_subscription)
54 |
55 | EventBroker.notify("some event")
56 | EventBroker.notify("a different event")
57 |
58 | assert waiter1.wait_for_event()
59 | assert waiter2.wait_for_event()
60 |
61 | mock_subscription.assert_has_calls(
62 | [
63 | call(),
64 | call(),
65 | ]
66 | )
67 |
68 | def test_it_notifies_event_with_extra_data_to_a_subscription(
69 | self, mock_subscription
70 | ):
71 | waiter = EventWaiter("some event")
72 |
73 | EventBroker.initialize()
74 | EventBroker.subscribe("some event", mock_subscription)
75 |
76 | EventBroker.notify(
77 | "some event",
78 | "positional arg 1",
79 | "positional arg 2",
80 | named_arg_1=1,
81 | named_arg_2=2,
82 | )
83 |
84 | assert waiter.wait_for_event()
85 |
86 | mock_subscription.assert_called_once_with(
87 | "positional arg 1", "positional arg 2", named_arg_1=1, named_arg_2=2
88 | )
89 |
--------------------------------------------------------------------------------
/src/tests/test_monitor.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from ..monitors.monitor import Monitor
4 | from ..samplers.sampler import Sampler
5 | from ..samplers.sample import Sample
6 |
7 |
8 | class TestMonitor:
9 | @pytest.fixture
10 | def samples(self):
11 | return [
12 | Sample(to_plot=1, single_value=1, units="unit"),
13 | Sample(to_plot=3, single_value=3, units="unit"),
14 | Sample(to_plot=5, single_value=5, units="unit"),
15 | Sample(to_plot=10, single_value=10, units="unit"),
16 | Sample(to_plot=20, single_value=20, units="unit"),
17 | ]
18 |
19 | @pytest.fixture
20 | def test_sampler(self, samples):
21 | class _TestSampler(Sampler):
22 | def __init__(self, sampling_frequency_hz=1):
23 | self.samples = samples
24 | self.samples_index = 0
25 | super().__init__(sampling_frequency_hz)
26 |
27 | def take_sample(self):
28 | sample = self.samples[self.samples_index]
29 | self.samples_index += 1
30 | self._sample_callback(sample)
31 |
32 | return _TestSampler()
33 |
34 | @pytest.fixture
35 | def test_monitor(self, test_sampler):
36 | class _TestMonitor(Monitor):
37 | def __init__(self):
38 | self._sampler = test_sampler
39 | super().__init__(self._sampler)
40 | self._sample = None
41 |
42 | def trigger_new_sample(self):
43 | self._sampler.take_sample()
44 |
45 | def get_values(self):
46 | return self._graph_values
47 |
48 | return _TestMonitor()
49 |
50 | def test_monitor_stores_one_sample_from_sampler(self, test_monitor):
51 | test_monitor.trigger_new_sample()
52 |
53 | values = test_monitor.get_values()
54 |
55 | assert values == [1]
56 |
57 | def test_monitor_stores_many_samples_from_sampler(self, test_monitor):
58 | test_monitor.trigger_new_sample()
59 | test_monitor.trigger_new_sample()
60 | test_monitor.trigger_new_sample()
61 |
62 | values = test_monitor.get_values()
63 |
64 | assert values == [5, 3, 1]
65 |
66 | def test_monitor_discards_samples_when_max_is_reached(self, test_monitor):
67 | test_monitor._EXTRA_BUFFER_OF_STORED_SAMPLES = 0
68 | test_monitor.set_max_number_of_stored_samples(2)
69 | test_monitor.trigger_new_sample()
70 | test_monitor.trigger_new_sample()
71 | test_monitor.trigger_new_sample()
72 | test_monitor.trigger_new_sample()
73 | test_monitor.trigger_new_sample()
74 |
75 | values = test_monitor.get_values()
76 |
77 | assert values == [20, 10]
78 |
--------------------------------------------------------------------------------
/src/tests/test_preferences.py:
--------------------------------------------------------------------------------
1 | from unittest import mock
2 | import pytest
3 |
4 | from ..preferences import Preferences
5 | from ..event_broker import EventBroker
6 | from .. import events
7 | from time import sleep
8 |
9 |
10 | class TestPreferences:
11 | @pytest.fixture
12 | def settings_content(self):
13 | return """{
14 | "cpu_monitor.enabled": true,
15 | "gpu_monitor.enabled": false,
16 | "memory_monitor.enabled": true
17 | }
18 | """
19 |
20 | @pytest.fixture(autouse=True)
21 | def default_preferences(self):
22 | Preferences._default_preferences = {
23 | "general.layout": "vertical",
24 | "cpu_monitor.enabled": True,
25 | "gpu_monitor.enabled": True,
26 | "memory_monitor.enabled": True,
27 | "custom_name": {},
28 | }
29 |
30 | @pytest.fixture
31 | def mock_file_exists(self):
32 | with mock.patch("src.preferences.Preferences._file_exists") as mock_exists:
33 | mock_exists.return_value = True
34 | yield mock_exists
35 |
36 | @pytest.fixture
37 | def mock_file_does_not_exists(self):
38 | with mock.patch("src.preferences.Preferences._file_exists") as mock_exists:
39 | mock_exists.return_value = False
40 | yield mock_exists
41 |
42 | @pytest.fixture
43 | def mock_read_file(self, settings_content):
44 | with mock.patch("src.preferences.Preferences._read_file") as mock_read:
45 | mock_read.return_value = settings_content
46 | yield mock_read
47 |
48 | @pytest.fixture
49 | def mock_write_file(self):
50 | with mock.patch("src.preferences.Preferences._write_file") as mock_write:
51 | yield mock_write
52 |
53 | @pytest.fixture
54 | def settings_content_with_adaptive_layout(self):
55 | return """{
56 | "general.layout": "adaptive",
57 | "cpu_monitor.enabled": true,
58 | "gpu_monitor.enabled": false,
59 | "memory_monitor.enabled": true
60 | }
61 | """
62 |
63 | @pytest.fixture
64 | def mock_read_file_with_adaptive_layout(
65 | self, settings_content_with_adaptive_layout
66 | ):
67 | with mock.patch("src.preferences.Preferences._read_file") as mock_read:
68 | mock_read.return_value = settings_content_with_adaptive_layout
69 | yield mock_read
70 |
71 | @pytest.mark.usefixtures("mock_file_exists", "mock_read_file")
72 | def test_it_loads_preferences_from_file_when_requested_to_load_them(self):
73 | Preferences.load()
74 |
75 | assert Preferences.get("cpu_monitor.enabled") is True
76 | assert Preferences.get("gpu_monitor.enabled") is False
77 | assert Preferences.get("memory_monitor.enabled") is True
78 |
79 | @pytest.mark.usefixtures("mock_read_file", "mock_file_does_not_exists")
80 | def test_it_writes_default_preferences_when_requested_to_load_them_and_do_not_exist(
81 | self, mock_write_file
82 | ):
83 | Preferences.load()
84 | mock_write_file.assert_called_once_with(
85 | mock.ANY,
86 | '{"general.layout": "vertical", "cpu_monitor.enabled": true, "gpu_monitor.enabled": true, "memory_monitor.enabled": true, "custom_name": {}}',
87 | )
88 |
89 | @pytest.mark.usefixtures("mock_read_file", "mock_file_exists", "mock_write_file")
90 | def test_it_changes_preference_when_a_preference_has_changed(self):
91 | Preferences.load()
92 |
93 | Preferences.set("memory_monitor.enabled", False)
94 |
95 | assert Preferences.get("memory_monitor.enabled") is False
96 |
97 | @pytest.mark.usefixtures("mock_read_file", "mock_file_exists")
98 | def test_it_persists_preferences_when_a_preference_has_changed(
99 | self, mock_write_file
100 | ):
101 | Preferences.load()
102 |
103 | Preferences.set("memory_monitor.enabled", False)
104 |
105 | mock_write_file.assert_called_once_with(
106 | mock.ANY,
107 | '{"general.layout": "vertical", "cpu_monitor.enabled": true, "gpu_monitor.enabled": false, "memory_monitor.enabled": false, "custom_name": {}}',
108 | )
109 |
110 | @pytest.mark.usefixtures("mock_file_exists", "mock_read_file", "mock_write_file")
111 | def test_it_notifies_preferences_changed_when_a_preference_has_changed(self):
112 | mock_subscription = mock.MagicMock()
113 | EventBroker.initialize()
114 | EventBroker.subscribe(events.PREFERENCES_CHANGED, mock_subscription)
115 | Preferences.load()
116 |
117 | Preferences.set("memory_monitor.enabled", False)
118 |
119 | retries = 5
120 | while mock_subscription.call_count == 0 and retries > 0:
121 | sleep(0.1)
122 | retries = retries - 1
123 |
124 | mock_subscription.assert_called_once_with("memory_monitor.enabled", False)
125 |
126 | @pytest.mark.usefixtures("mock_file_exists", "mock_read_file", "mock_write_file")
127 | def test_it_adds_new_fields_when_default_preferences_has_more_fields_than_persisted_preferences(
128 | self,
129 | ):
130 | Preferences._default_preferences["new.key"] = "new value"
131 |
132 | Preferences.load()
133 |
134 | assert Preferences.get("new.key") == "new value"
135 | assert Preferences.get("cpu_monitor.enabled") is True
136 | assert Preferences.get("gpu_monitor.enabled") is False
137 | assert Preferences.get("memory_monitor.enabled") is True
138 |
139 | @pytest.mark.usefixtures("mock_file_exists", "mock_read_file", "mock_write_file")
140 | def test_it_returns_None_when_there_is_no_custom_name_set_for_a_monitor_type(self):
141 | Preferences.load()
142 |
143 | assert Preferences.get_custom_name("a monitor type") == None
144 |
145 | @pytest.mark.usefixtures("mock_file_exists", "mock_read_file", "mock_write_file")
146 | def test_it_returns_custom_name_when_there_is_custom_name_set_for_a_monitor_type(
147 | self,
148 | ):
149 | Preferences._default_preferences["custom_name"]["a monitor type"] = "new value"
150 |
151 | Preferences.load()
152 |
153 | assert Preferences.get_custom_name("a monitor type") == "new value"
154 |
155 | @pytest.mark.usefixtures("mock_file_exists", "mock_read_file")
156 | def test_it_sets_and_returns_custom_name_for_a_monitor_type(self, mock_write_file):
157 | Preferences.load()
158 |
159 | Preferences.set_custom_name("a monitor type", "Custom name")
160 |
161 | assert Preferences.get_custom_name("a monitor type") == "Custom name"
162 | mock_write_file.assert_called_once_with(
163 | mock.ANY,
164 | '{"general.layout": "vertical", "cpu_monitor.enabled": true, "gpu_monitor.enabled": false, "memory_monitor.enabled": true, "custom_name": {"a monitor type": "Custom name"}}',
165 | )
166 |
167 | @pytest.mark.usefixtures("mock_file_exists", "mock_read_file", "mock_write_file")
168 | def test_it_notifies_when_a_custom_name_changes(self):
169 | mock_subscription = mock.MagicMock()
170 | EventBroker.initialize()
171 | EventBroker.subscribe(events.MONITOR_RENAMED, mock_subscription)
172 | Preferences.load()
173 |
174 | Preferences.set_custom_name("a monitor type", "Custom name")
175 |
176 | retries = 5
177 | while mock_subscription.call_count == 0 and retries > 0:
178 | sleep(0.1)
179 | retries = retries - 1
180 |
181 | mock_subscription.assert_called_once_with("a monitor type", "Custom name")
182 |
183 | @pytest.mark.usefixtures(
184 | "mock_file_exists", "mock_read_file_with_adaptive_layout", "mock_write_file"
185 | )
186 | def test_migrates_deprecated_adaptive_layout_preference(self):
187 | Preferences.load()
188 |
189 | assert Preferences.get("general.layout") == "vertical"
190 |
191 | @pytest.mark.usefixtures("mock_file_exists", "mock_read_file", "mock_write_file")
192 | def test_uses_custom_handler_to_get_a_preference(self):
193 | custom_get = mock.MagicMock()
194 | custom_get.return_value = "value"
195 | Preferences._custom_key_handler["a key"] = {"get": custom_get}
196 |
197 | assert Preferences.get("a key") == "value"
198 | assert Preferences.get("cpu_monitor.enabled") == True
199 |
200 | @pytest.mark.usefixtures("mock_file_exists", "mock_read_file", "mock_write_file")
201 | def test_uses_custom_handler_to_set_a_preference(self):
202 | custom_set = mock.MagicMock()
203 | Preferences._custom_key_handler["a key"] = {"set": custom_set}
204 |
205 | Preferences.set("a key", "value")
206 |
207 | custom_set.assert_called_once_with("value")
208 |
--------------------------------------------------------------------------------
/src/tests/test_pressure_sampler.py:
--------------------------------------------------------------------------------
1 | from unittest import mock
2 | import pytest
3 | from ..samplers.pressure_sampler import PressureSampler
4 |
5 |
6 | class TestPressureSampler:
7 | @pytest.fixture
8 | def file_handle_mock(self):
9 | m = mock.Mock()
10 | m.readline.return_value = (
11 | """some avg10=17.71 avg60=0.00 avg300=1.03 total=154824318"""
12 | )
13 | return m
14 |
15 | @pytest.fixture
16 | def mock_open(self, file_handle_mock):
17 | with mock.patch("builtins.open") as m:
18 | m.return_value = file_handle_mock
19 | yield m
20 |
21 | def test_it_returns_avg10_value_for_some_row(self, mock_open):
22 | delta_sampler = PressureSampler("test_file")
23 |
24 | sample = delta_sampler._get_sample()
25 |
26 | mock_open.assert_called_once_with("test_file", "r")
27 | assert sample.units == "%"
28 | assert sample.single_value == 17
29 | assert sample.to_plot == 17
30 |
--------------------------------------------------------------------------------
/src/tests/test_sampler.py:
--------------------------------------------------------------------------------
1 | from ..samplers.sampler import Sampler
2 |
3 | from time import sleep
4 |
5 |
6 | class TestSampler:
7 | class _TestSampler(Sampler):
8 | def __init__(self):
9 | self._samples_gotten = 0
10 | super().__init__(sampling_frequency_hz=20)
11 |
12 | def _get_sample(self):
13 | self._samples_gotten = self._samples_gotten + 1
14 | return self._samples_gotten
15 |
16 | def test_it_samples_values_until_stopped(self):
17 | samples = []
18 |
19 | def mock_new_sample_callback(value):
20 | samples.append(value)
21 |
22 | test_sampler = self._TestSampler()
23 | test_sampler.install_new_sample_callback(mock_new_sample_callback)
24 | test_sampler.start()
25 |
26 | while len(samples) < 3:
27 | sleep(0.1)
28 |
29 | test_sampler.stop()
30 |
31 | assert [1, 2, 3] == samples[:3]
32 |
--------------------------------------------------------------------------------
/src/theme.py:
--------------------------------------------------------------------------------
1 | class Theme:
2 | SYSTEM = "system"
3 | DARK = "dark"
4 | LIGHT = "light"
5 |
--------------------------------------------------------------------------------
/src/theming.py:
--------------------------------------------------------------------------------
1 | from gi.repository import Adw, GObject
2 |
3 | from .preferences import Preferences
4 | from .preference_keys import PreferenceKeys
5 | from .event_broker import EventBroker
6 | from . import events
7 | from .theme import Theme
8 |
9 |
10 | class Theming:
11 | _color_scheme_map = {
12 | Theme.SYSTEM: Adw.ColorScheme.DEFAULT,
13 | Theme.DARK: Adw.ColorScheme.FORCE_DARK,
14 | Theme.LIGHT: Adw.ColorScheme.FORCE_LIGHT,
15 | }
16 |
17 | @classmethod
18 | def initialize(self):
19 | self._manager = Adw.StyleManager.get_default()
20 | self._refresh_theme_from_preferences()
21 |
22 | EventBroker.subscribe(events.PREFERENCES_CHANGED, self._on_preferences_changed)
23 |
24 | @classmethod
25 | def _refresh_theme_from_preferences(self):
26 | theme = Preferences.get("general.theme")
27 | color_scheme = self._color_scheme_map[theme]
28 | GObject.idle_add(self._manager.set_color_scheme, color_scheme)
29 |
30 | @classmethod
31 | def _on_preferences_changed(self, preference_key, value):
32 | if preference_key == PreferenceKeys.THEME:
33 | self._refresh_theme_from_preferences()
34 |
--------------------------------------------------------------------------------
/src/translatable_strings/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/src/translatable_strings/__init__.py
--------------------------------------------------------------------------------
/src/translatable_strings/monitor_title.py:
--------------------------------------------------------------------------------
1 | from gettext import gettext as _
2 |
3 | CPU = _("CPU")
4 | GPU = _("GPU")
5 | MEMORY = _("Memory")
6 | SWAP = _("Swap")
7 | UPLINK = _("Network 🠅")
8 | DOWNLINK = _("Network 🠇")
9 | HOME_USAGE = "~"
10 | ROOT_USAGE = "/"
11 | TEMPERATURE = _("Temp")
12 | CPU_PRESSURE = _("CPU Pressure")
13 | MEMORY_PRESSURE = _("Memory Pressure")
14 | IO_PRESSURE = _("I/O Pressure")
15 |
--------------------------------------------------------------------------------
/src/translatable_strings/preference_toggle_description.py:
--------------------------------------------------------------------------------
1 | from gettext import gettext as _
2 |
3 | SHOWN_AS = _("Shown as")
4 |
5 | CPU_PER_CORE = _(
6 | "The monitor will draw as many graphs as cores are present in the system."
7 | )
8 | GPU = _("Experimental. This is only known to work on some AMD GPUs currently.")
9 |
--------------------------------------------------------------------------------
/src/translatable_strings/preference_toggle_label.py:
--------------------------------------------------------------------------------
1 | from gettext import gettext as _
2 |
3 | CPU = _("CPU")
4 | CPU_PER_CORE = _("CPU per core")
5 | GPU = _("GPU")
6 | MEMORY = _("Memory")
7 | SWAP = _("Swap")
8 | DOWNLINK = _("Downlink")
9 | UPLINK = _("Uplink")
10 | HOME_FOLDER_USAGE = _("Home folder usage")
11 | ROOT_FOLDER_USAGE = _("Root folder usage")
12 | TEMPERATURE = _("Temperature")
13 | CPU_PRESSURE = _("CPU pressure")
14 | MEMORY_PRESSURE = _("Memory pressure")
15 | IO_PRESSURE = _("I/O pressure")
16 |
--------------------------------------------------------------------------------
/src/translatable_strings/preference_toggle_section_name.py:
--------------------------------------------------------------------------------
1 | from gettext import gettext as _
2 |
3 | CPU = _("CPU")
4 | GPU = _("GPU")
5 | MEMORY = _("Memory")
6 | NETWORK = _("Network")
7 | DISK_USAGE = _("Disk usage")
8 | TEMPERATURE = _("Temperature")
9 | PRESSURE = _("Pressure")
10 |
--------------------------------------------------------------------------------
/src/translatable_strings/redraw_frequency.py:
--------------------------------------------------------------------------------
1 | from gettext import gettext as _
2 |
3 |
4 | VERY_HIGH = _("Very High")
5 | HIGH = _("High")
6 | LOW = _("Low")
7 | VERY_LOW = _("Very Low")
8 |
--------------------------------------------------------------------------------
/src/translatable_strings/tips.py:
--------------------------------------------------------------------------------
1 | from gettext import gettext as _
2 |
3 | WINDOW_TITLE = _("Tips")
4 | ALWAYS_ON_TOP_TITLE = _("Always on Top")
5 | ALWAYS_ON_TOP_BODY = _(
6 | "You can make the window stay on top of any other window: Press Alt+Space or right click with your mouse in the window titlebar to bring the window menu, then select Always on Top ."
7 | )
8 |
--------------------------------------------------------------------------------
/src/translators.py:
--------------------------------------------------------------------------------
1 | translators_credits = """
2 | Heimen Stoffels
3 | Irénée Thirion
4 | Jordi Chulia
5 | Philipp Kiemle
6 | Sabri Ünal
7 | """
8 |
--------------------------------------------------------------------------------
/src/ui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jorchube/monitorets/503ff6694f75d8435257590bd7a1fd48188197c3/src/ui/__init__.py
--------------------------------------------------------------------------------
/src/ui/colors.py:
--------------------------------------------------------------------------------
1 | class RED:
2 | HTML = "ff5050"
3 | RGB = (1, 0.2, 0.2)
4 |
5 |
6 | class GREEN:
7 | HTML = "00cc00"
8 | RGB = (0, 0.8, 0)
9 |
10 |
11 | class BROWN:
12 | HTML = "b07000"
13 | RGB = (0.61, 0.38, 0)
14 |
15 |
16 | class BLUE:
17 | HTML = "0080ff"
18 | RGB = (0, 0.5, 1)
19 |
20 |
21 | class ORANGE:
22 | HTML = "ff8000"
23 | RGB = (1, 0.5, 0)
24 |
25 |
26 | class PURPLE:
27 | HTML = "9f30ff"
28 | RGB = (0.7, 0.2, 1)
29 |
30 |
31 | class YELLOW:
32 | HTML = "bbbb00"
33 | RGB = (0.73, 0.73, 0)
34 |
--------------------------------------------------------------------------------
/src/ui/graph_area.py:
--------------------------------------------------------------------------------
1 | import math
2 | import cairo
3 | from gi.repository import Gtk, GObject
4 |
5 |
6 | class GraphArea:
7 | _LINE_WIDTH = 0.2
8 | _ALPHA_FILL = 0.2
9 | _MASK_CORNER_RADIUS = 12
10 | _DEFAULT_WIDTH_PER_SAMPLE = 10
11 |
12 | def __init__(self, color, redraw_frequency_seconds, smooth_graph=False):
13 | self._color = color.RGB
14 | self._redraw_frequency_seconds = redraw_frequency_seconds
15 | self._width_per_sample = None
16 | self.set_width_per_sample(self._DEFAULT_WIDTH_PER_SAMPLE)
17 | self._drawing_area = self._build_drawing_area()
18 | self._drawing_area.set_draw_func(self._draw_func, None)
19 | self._values = None
20 | self._current_x_step_offset = 0
21 | self._draw_smooth_graph = smooth_graph
22 |
23 | def set_width_per_sample(self, value):
24 | self._width_per_sample = value
25 | self._x_step_per_tick = self._width_per_sample * self._redraw_frequency_seconds
26 |
27 | def set_new_values(self, values):
28 | self._values = values
29 | self._current_x_step_offset = self._width_per_sample
30 |
31 | def get_drawing_area_widget(self):
32 | return self._drawing_area
33 |
34 | def redraw_tick(self):
35 | GObject.idle_add(self._redraw)
36 | self._current_x_step_offset -= self._x_step_per_tick
37 |
38 | def _build_drawing_area(self):
39 | drawing_area = Gtk.DrawingArea()
40 | drawing_area.set_hexpand(True)
41 | drawing_area.set_vexpand(True)
42 |
43 | return drawing_area
44 |
45 | def _redraw(self):
46 | self._drawing_area.queue_draw()
47 |
48 | def _draw_func(self, gtk_drawing_area, context, width, height, user_data):
49 | if self._values is None:
50 | return
51 | values = self._values
52 |
53 | self._draw_values_fill(context, values, width, height)
54 | self._draw_values_ouline(context, values, width, height)
55 | self._apply_mask(context, width, height)
56 |
57 | def _draw_values_fill(self, context, values, width, height):
58 | context.new_path()
59 | context.set_line_join(cairo.LINE_JOIN_ROUND)
60 | context.set_line_cap(cairo.LINE_CAP_ROUND)
61 |
62 | self._draw_values_shape(context, values, width, height, close=True)
63 |
64 | context.set_source_rgba(*self._color, self._ALPHA_FILL)
65 | context.fill()
66 |
67 | def _draw_values_ouline(self, context, values, width, height):
68 | context.new_path()
69 | context.set_line_join(cairo.LINE_JOIN_ROUND)
70 | context.set_line_cap(cairo.LINE_CAP_ROUND)
71 |
72 | self._draw_values_shape(context, values, width, height)
73 |
74 | context.set_line_width(self._LINE_WIDTH)
75 | context.set_source_rgba(*self._color, 1)
76 | context.stroke()
77 |
78 | def _draw_values_shape(self, context, values, width, height, close=False):
79 | if self._draw_smooth_graph is True:
80 | self._smooth_draw_values_shape(context, values, width, height, close)
81 | else:
82 | self._fast_draw_values_shape(context, values, width, height, close)
83 |
84 | def _fast_draw_values_shape(self, context, values, width, height, close=False):
85 | order = 0
86 |
87 | for value in values:
88 | x, y = self._value_point(width, height, value, order)
89 | context.line_to(x, y)
90 | order += 1
91 |
92 | if close:
93 | context.line_to(x, height)
94 | context.line_to(width, height)
95 | context.close_path()
96 |
97 | def _value_point(self, width, height, value, order):
98 | x = width - (order * self._width_per_sample) + self._current_x_step_offset
99 | y = height - (height * (value / 100.0))
100 |
101 | return x, y
102 |
103 | def _smooth_draw_values_shape(self, context, values, width, height, close=False):
104 | order = 0
105 |
106 | x, _ = self._smooth_value_point(width, height, values[0], order)
107 |
108 | for i in range(len(values) - 1):
109 | v0 = values[i]
110 | v3 = values[i + 1]
111 |
112 | x0, y0 = self._smooth_value_point(width, height, v0, order)
113 | x3, y3 = self._smooth_value_point(width, height, v3, order + 1)
114 |
115 | mid_x = (x0 + x3) / 2
116 | x1, y1 = mid_x, y0
117 | x2, y2 = mid_x, y3
118 |
119 | context.curve_to(x1, y1, x2, y2, x3, y3)
120 | order += 1
121 | x = x3
122 |
123 | if close:
124 | context.line_to(x, height)
125 | context.line_to(width, height)
126 | context.close_path()
127 |
128 | def _smooth_value_point(self, width, height, value, order):
129 | x = width - ((order - 1) * self._width_per_sample) + self._current_x_step_offset
130 | y = height - (height * (value / 100.0))
131 |
132 | return x, y
133 |
134 | def _apply_mask(self, context, width, height):
135 | context.set_operator(cairo.OPERATOR_DEST_IN)
136 | context.new_path()
137 | self._rectangle_path_with_corner_radius(
138 | context, width, height, self._MASK_CORNER_RADIUS
139 | )
140 | context.close_path()
141 | context.fill()
142 |
143 | def _rectangle_path_with_corner_radius(self, context, width, height, radius):
144 | context.new_path()
145 |
146 | context.line_to(width, height - radius)
147 | context.arc(width - radius, height - radius, radius, 0, math.pi / 2)
148 | context.line_to(radius, height)
149 | context.arc(radius, height - radius, radius, math.pi / 2, math.pi)
150 | context.line_to(0, radius)
151 | context.arc(radius, radius, radius, math.pi, (3 / 2) * math.pi)
152 | context.line_to(width - radius, 0)
153 | context.arc(width - radius, radius, radius, (3 / 2) * math.pi, 0)
154 |
--------------------------------------------------------------------------------
/src/ui/graph_redraw_tick_manager.py:
--------------------------------------------------------------------------------
1 | from threading import Timer
2 | from resource import *
3 |
4 |
5 | class GraphRedrawTickManager:
6 | _TICK_FREQUENCY_SECONDS = 0.1
7 |
8 | def __init__(self, redraw_callback, tick_frequency_seconds=_TICK_FREQUENCY_SECONDS):
9 | self._tick_frequency_seconds = tick_frequency_seconds
10 | self._redraw_callback = redraw_callback
11 | self._timer = None
12 | self._stop = False
13 |
14 | def start(self):
15 | self._arm_timer()
16 |
17 | def stop(self):
18 | self._stop = True
19 |
20 | def _tick(self):
21 | self._redraw_callback()
22 |
23 | def _arm_timer(self):
24 | if self._stop:
25 | return
26 | self._timer = Timer(self._tick_frequency_seconds, self._redraw_and_rearm)
27 | self._timer.start()
28 |
29 | def _redraw_and_rearm(self):
30 | self._tick()
31 | self._arm_timer()
32 |
--------------------------------------------------------------------------------
/src/ui/headerbar_wrapper.py:
--------------------------------------------------------------------------------
1 | from gi.repository import Adw, Gtk
2 |
3 | from ..event_broker import EventBroker
4 | from .. import events
5 |
6 |
7 | class HeaderBarWrapper:
8 | def __init__(self, parent_window):
9 | self._parent_window = parent_window
10 | self._headerbar = self._build_headerbar()
11 |
12 | self._set_not_focused()
13 |
14 | @property
15 | def root_widget(self):
16 | return self._headerbar
17 |
18 | def on_mouse_enter(self):
19 | self._set_focused()
20 |
21 | def on_mouse_exit(self):
22 | self._set_not_focused()
23 |
24 | def _set_not_focused(self):
25 | self._headerbar.set_opacity(0)
26 |
27 | def _set_focused(self):
28 | self._headerbar.set_opacity(1)
29 |
30 | def _build_headerbar(self):
31 | headerbar = Adw.HeaderBar()
32 | headerbar.set_vexpand(True)
33 | headerbar.add_css_class("flat")
34 | headerbar.set_title_widget(Gtk.Label())
35 | headerbar.set_decoration_layout(":")
36 |
37 | close_button = self._build_close_button()
38 | menu_button = self._build_menu_button()
39 |
40 | headerbar.pack_start(self._build_headerbar_button_box(menu_button))
41 | headerbar.pack_end(self._build_headerbar_button_box(close_button))
42 |
43 | return headerbar
44 |
45 | def _build_headerbar_button_box(self, button):
46 | control_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
47 | control_box.set_valign(Gtk.Align.START)
48 | control_box.append(button)
49 |
50 | return control_box
51 |
52 | def _close_button_clicked(self, *args, **kwargs):
53 | EventBroker.notify(events.CLOSE_APPLICATION_REQUESTED)
54 |
55 | def _build_close_button(self):
56 | button = Gtk.Button()
57 | button.set_icon_name("window-close")
58 | button.add_css_class("circular")
59 | button.add_css_class("raised")
60 | button.connect("clicked", self._close_button_clicked)
61 |
62 | return button
63 |
64 | def _build_menu_button(self):
65 | button = Gtk.MenuButton()
66 | button.set_icon_name("open-menu-symbolic")
67 | button.add_css_class("circular")
68 | button.add_css_class("raised")
69 |
70 | builder = Gtk.Builder.new_from_resource(
71 | "/io/github/jorchube/monitorets/gtk/main-menu-model.ui"
72 | )
73 | menu = builder.get_object("main_menu")
74 |
75 | popover = Gtk.PopoverMenu.new_from_model(menu)
76 |
77 | button.set_popover(popover)
78 |
79 | return button
80 |
--------------------------------------------------------------------------------
/src/ui/monitor_title_overlay.py:
--------------------------------------------------------------------------------
1 | from gi.repository import Adw, Gtk, Pango, GObject
2 |
3 |
4 | class MonitorTitleOverlay(Adw.Bin):
5 | _SMALL_VIEW_SIZE_LIMIT = 100
6 | _BIG_VIEW_SIZE_LIMIT = 200
7 |
8 | def __init__(self, html_color_code):
9 | super().__init__()
10 |
11 | self._html_color_code = html_color_code
12 |
13 | self._huge_view = _HugeMonitorTitleOverlayView(self._html_color_code)
14 | self._big_view = _BigMonitorTitleOverlayView(self._html_color_code)
15 | self._small_view = _SmallMonitorTitleOverlayView(self._html_color_code)
16 |
17 | self._squeezer = Adw.Squeezer()
18 | self._squeezer.set_transition_duration(250)
19 | self._squeezer.set_transition_type(Adw.SqueezerTransitionType.CROSSFADE)
20 | self._squeezer_page_big = self._squeezer.add(self._big_view)
21 | self._squeezer_page_small = self._squeezer.add(self._small_view)
22 | self._squeezer_page_huge = self._squeezer.add(self._huge_view)
23 |
24 | self.set_child(self._squeezer)
25 |
26 | self._paintable = Gtk.WidgetPaintable()
27 | self._paintable.set_widget(self)
28 | self._paintable.connect("invalidate-size", self._on_size_changed)
29 |
30 | self._refresh_visible_view()
31 |
32 | def set_title(self, title):
33 | self._huge_view.set_title(title)
34 | self._big_view.set_title(title)
35 | self._small_view.set_title(title)
36 |
37 | def set_value(self, value):
38 | self._huge_view.set_value(value)
39 | self._big_view.set_value(value)
40 | self._small_view.set_value(value)
41 |
42 | def _on_size_changed(self, paintable):
43 | self._refresh_visible_view()
44 |
45 | def _refresh_visible_view(self):
46 | width = self.get_width()
47 | height = self.get_height()
48 |
49 | if height < self._SMALL_VIEW_SIZE_LIMIT or width < self._SMALL_VIEW_SIZE_LIMIT:
50 | self._squeezer_page_huge.set_enabled(False)
51 | self._squeezer_page_big.set_enabled(False)
52 | self._squeezer_page_small.set_enabled(True)
53 | return
54 |
55 | if height < self._BIG_VIEW_SIZE_LIMIT or width < self._BIG_VIEW_SIZE_LIMIT:
56 | self._squeezer_page_huge.set_enabled(False)
57 | self._squeezer_page_small.set_enabled(False)
58 | self._squeezer_page_big.set_enabled(True)
59 | return
60 |
61 | self._squeezer_page_small.set_enabled(False)
62 | self._squeezer_page_big.set_enabled(False)
63 | self._squeezer_page_huge.set_enabled(True)
64 |
65 |
66 | class _MonitorTitleOverlayView(Gtk.Box):
67 | def __init__(self, html_color_code):
68 | super().__init__(orientation=Gtk.Orientation.VERTICAL)
69 |
70 | self._html_color_code = html_color_code
71 | self._padding_top_label = self._build_padding_top_label()
72 | self._title_label = self._build_title_label()
73 | self._value_label = self._build_value_label()
74 |
75 | self.set_valign(Gtk.Align.CENTER)
76 | self.append(self._padding_top_label)
77 | self.append(self._title_label)
78 | self.append(self._value_label)
79 |
80 | def _build_title_label(self):
81 | label = Gtk.Label()
82 | label.set_margin_start(10)
83 | label.set_margin_end(10)
84 | label.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
85 | return label
86 |
87 | def _build_value_label(self):
88 | label = Gtk.Label()
89 | return label
90 |
91 | def _build_padding_top_label(self):
92 | label = Gtk.Label()
93 | return label
94 |
95 | def set_title(self, title):
96 | markup = f"{title} "
97 | GObject.idle_add(self._title_label.set_markup, markup)
98 |
99 | def set_value(self, value):
100 | value_as_str = value if value is not None else ""
101 | markup = f"{value_as_str} "
102 | GObject.idle_add(self._value_label.set_markup, markup)
103 | padding_markup = (
104 | f" "
105 | )
106 | GObject.idle_add(self._padding_top_label.set_markup, padding_markup)
107 |
108 | def _title_size(self):
109 | raise NotImplementedError
110 |
111 | def _title_weight(self):
112 | raise NotImplementedError
113 |
114 | def _value_size(self):
115 | raise NotImplementedError
116 |
117 | def _value_weight(self):
118 | raise NotImplementedError
119 |
120 |
121 | class _SmallMonitorTitleOverlayView(_MonitorTitleOverlayView):
122 | def _title_size(self):
123 | return "medium"
124 |
125 | def _title_weight(self):
126 | return "bold"
127 |
128 | def _value_size(self):
129 | return "small"
130 |
131 | def _value_weight(self):
132 | return "bold"
133 |
134 |
135 | class _BigMonitorTitleOverlayView(_MonitorTitleOverlayView):
136 | def _title_size(self):
137 | return "large"
138 |
139 | def _title_weight(self):
140 | return "bold"
141 |
142 | def _value_size(self):
143 | return "medium"
144 |
145 | def _value_weight(self):
146 | return "bold"
147 |
148 |
149 | class _HugeMonitorTitleOverlayView(_MonitorTitleOverlayView):
150 | def _title_size(self):
151 | return "xx-large"
152 |
153 | def _title_weight(self):
154 | return "ultrabold"
155 |
156 | def _value_size(self):
157 | return "large"
158 |
159 | def _value_weight(self):
160 | return "bold"
161 |
--------------------------------------------------------------------------------
/src/ui/monitor_widgets/cpu_monitor_widget.py:
--------------------------------------------------------------------------------
1 | from .monitor_widget import MonitorWidget
2 | from ...monitors.cpu_monitor import CpuMonitor
3 | from .. import colors
4 | from ...translatable_strings import monitor_title
5 | from ...monitor_type import MonitorType
6 |
7 |
8 | class CpuMonitorWidget(MonitorWidget):
9 | def __init__(self, *args, **kwargs):
10 | self._type = MonitorType.CPU
11 | self._title = monitor_title.CPU
12 | self._color = colors.BLUE
13 | self._monitor = CpuMonitor()
14 |
15 | super().__init__(
16 | self._monitor, self._type, self._title, self._color, *args, **kwargs
17 | )
18 |
--------------------------------------------------------------------------------
/src/ui/monitor_widgets/cpu_per_core_monitor_widget.py:
--------------------------------------------------------------------------------
1 | from .overlapping_values_monitor_widget import OverlappingGraphsMonitorWidget
2 | from ...monitors.cpu_per_core_monitor import CpuPerCoreMonitor
3 | from .. import colors
4 | from ...translatable_strings import monitor_title
5 | from ...monitor_type import MonitorType
6 |
7 |
8 | class CpuPerCoreMonitorWidget(OverlappingGraphsMonitorWidget):
9 | def __init__(self, *args, **kwargs):
10 | self._type = MonitorType.CPU_PER_CORE
11 | self._title = monitor_title.CPU
12 | self._color = colors.BLUE
13 | self._monitor = CpuPerCoreMonitor()
14 |
15 | super().__init__(
16 | self._monitor, self._type, self._title, self._color, *args, **kwargs
17 | )
18 |
--------------------------------------------------------------------------------
/src/ui/monitor_widgets/cpu_pressure_monitor_widget.py:
--------------------------------------------------------------------------------
1 | from .monitor_widget import MonitorWidget
2 | from ...monitors.cpu_pressure_monitor import CpuPressureMonitor
3 | from .. import colors
4 | from ...translatable_strings import monitor_title
5 | from ...monitor_type import MonitorType
6 |
7 |
8 | class CpuPressureMonitorWidget(MonitorWidget):
9 | def __init__(self, *args, **kwargs):
10 | self._type = MonitorType.CPU_PRESSURE
11 | self._title = monitor_title.CPU_PRESSURE
12 | self._color = colors.BLUE
13 | self._monitor = CpuPressureMonitor()
14 |
15 | super().__init__(
16 | self._monitor, self._type, self._title, self._color, *args, **kwargs
17 | )
18 |
--------------------------------------------------------------------------------
/src/ui/monitor_widgets/downlink_monitor_widget.py:
--------------------------------------------------------------------------------
1 | from .monitor_widget import MonitorWidget
2 | from ...monitors.downlink_monitor import DownlinkMonitor
3 | from ..relative_graph_area import RelativeGraphArea
4 | from .. import colors
5 | from ...translatable_strings import monitor_title
6 | from ...monitor_type import MonitorType
7 | from ...event_broker import EventBroker
8 | from ... import events
9 | from ...preferences import Preferences
10 | from ...preference_keys import PreferenceKeys
11 |
12 |
13 | class DownlinkMonitorWidget(MonitorWidget):
14 | def __init__(self, *args, **kwargs):
15 | self._type = MonitorType.Downlink
16 | self._title = monitor_title.DOWNLINK
17 | self._color = colors.BLUE
18 | self._monitor = DownlinkMonitor()
19 | self._relative_graph_area = None
20 | self._use_unified_network_scale = Preferences.get(
21 | PreferenceKeys.UNIFIED_SCALE_FOR_NETWORK_MONITORS_ENABLED
22 | )
23 |
24 | EventBroker.subscribe(events.PREFERENCES_CHANGED, self._on_preference_changed)
25 | EventBroker.subscribe(
26 | events.NETWORK_MONITOR_NEW_REFERENCE_VALUE, self._set_new_reference_value
27 | )
28 |
29 | super().__init__(
30 | self._monitor, self._type, self._title, self._color, *args, **kwargs
31 | )
32 |
33 | def _graph_area_instance(self, color, redraw_freq_seconds, draw_smooth_graph):
34 | self._relative_graph_area = RelativeGraphArea(
35 | color,
36 | redraw_freq_seconds,
37 | draw_smooth_graph,
38 | new_reference_value_callback=self._new_reference_value,
39 | )
40 | return self._relative_graph_area
41 |
42 | def _new_reference_value(self, value):
43 | if self._use_unified_network_scale:
44 | EventBroker.notify(
45 | events.DOWNLINK_NETWORK_MONITOR_NEW_REFERENCE_VALUE_PROPOSAL, value
46 | )
47 | else:
48 | self._relative_graph_area.set_reference_value(value)
49 |
50 | def _set_new_reference_value(self, value):
51 | self._relative_graph_area.set_reference_value(value)
52 |
53 | def _on_preference_changed(self, key, value):
54 | if key == PreferenceKeys.UNIFIED_SCALE_FOR_NETWORK_MONITORS_ENABLED:
55 | self._use_unified_network_scale = value
56 | return
57 |
58 | super()._on_preference_changed(key, value)
59 |
--------------------------------------------------------------------------------
/src/ui/monitor_widgets/gpu_monitor_widget.py:
--------------------------------------------------------------------------------
1 | from .monitor_widget import MonitorWidget
2 | from ...monitors.gpu_monitor import GpuMonitor
3 | from .. import colors
4 | from ...translatable_strings import monitor_title
5 | from ...monitor_type import MonitorType
6 |
7 |
8 | class GpuMonitorWidget(MonitorWidget):
9 | def __init__(self, *args, **kwargs):
10 | self._type = MonitorType.GPU
11 | self._title = monitor_title.GPU
12 | self._color = colors.GREEN
13 | self._monitor = GpuMonitor()
14 |
15 | super().__init__(
16 | self._monitor, self._type, self._title, self._color, *args, **kwargs
17 | )
18 |
--------------------------------------------------------------------------------
/src/ui/monitor_widgets/home_usage_monitor_widget.py:
--------------------------------------------------------------------------------
1 | from .monitor_widget import MonitorWidget
2 | from ...monitors.home_usage_monitor import HomeUsageMonitor
3 | from .. import colors
4 | from ...translatable_strings import monitor_title
5 | from ...monitor_type import MonitorType
6 |
7 |
8 | class HomeUsageMonitorWidget(MonitorWidget):
9 | def __init__(self, *args, **kwargs):
10 | self._type = MonitorType.Home_usage
11 | self._title = monitor_title.HOME_USAGE
12 | self._color = colors.PURPLE
13 | self._monitor = HomeUsageMonitor()
14 |
15 | super().__init__(
16 | self._monitor, self._type, self._title, self._color, *args, **kwargs
17 | )
18 |
--------------------------------------------------------------------------------
/src/ui/monitor_widgets/io_pressure_monitor_widget.py:
--------------------------------------------------------------------------------
1 | from .monitor_widget import MonitorWidget
2 | from ...monitors.io_pressure_monitor import IOPressureMonitor
3 | from .. import colors
4 | from ...translatable_strings import monitor_title
5 | from ...monitor_type import MonitorType
6 |
7 |
8 | class IOPressureMonitorWidget(MonitorWidget):
9 | def __init__(self, *args, **kwargs):
10 | self._type = MonitorType.IO_PRESSURE
11 | self._title = monitor_title.IO_PRESSURE
12 | self._color = colors.YELLOW
13 | self._monitor = IOPressureMonitor()
14 |
15 | super().__init__(
16 | self._monitor, self._type, self._title, self._color, *args, **kwargs
17 | )
18 |
--------------------------------------------------------------------------------
/src/ui/monitor_widgets/memory_monitor_widget.py:
--------------------------------------------------------------------------------
1 | from .monitor_widget import MonitorWidget
2 | from ...monitors.memory_monitor import MemoryMonitor
3 | from .. import colors
4 | from ...translatable_strings import monitor_title
5 | from ...monitor_type import MonitorType
6 |
7 |
8 | class MemoryMonitorWidget(MonitorWidget):
9 | def __init__(self, *args, **kwargs):
10 | self._type = MonitorType.Memory
11 | self._title = monitor_title.MEMORY
12 | self._color = colors.ORANGE
13 | self._monitor = MemoryMonitor()
14 |
15 | super().__init__(
16 | self._monitor, self._type, self._title, self._color, *args, **kwargs
17 | )
18 |
--------------------------------------------------------------------------------
/src/ui/monitor_widgets/memory_pressure_monitor_widget.py:
--------------------------------------------------------------------------------
1 | from .monitor_widget import MonitorWidget
2 | from ...monitors.memory_pressure_monitor import MemoryPressureMonitor
3 | from .. import colors
4 | from ...translatable_strings import monitor_title
5 | from ...monitor_type import MonitorType
6 |
7 |
8 | class MemoryPressureMonitorWidget(MonitorWidget):
9 | def __init__(self, *args, **kwargs):
10 | self._type = MonitorType.MEMORY_PRESSURE
11 | self._title = monitor_title.MEMORY_PRESSURE
12 | self._color = colors.ORANGE
13 | self._monitor = MemoryPressureMonitor()
14 |
15 | super().__init__(
16 | self._monitor, self._type, self._title, self._color, *args, **kwargs
17 | )
18 |
--------------------------------------------------------------------------------
/src/ui/monitor_widgets/monitor_widget.py:
--------------------------------------------------------------------------------
1 | import math
2 | from gi.repository import Adw, Gtk, Pango, GObject
3 | from ..graph_area import GraphArea
4 | from ..graph_redraw_tick_manager import GraphRedrawTickManager
5 | from ...preferences import Preferences
6 | from ...preference_keys import PreferenceKeys
7 | from ...event_broker import EventBroker
8 | from ... import events
9 | from ..monitor_title_overlay import MonitorTitleOverlay
10 |
11 |
12 | class MonitorWidget(Adw.Bin):
13 | _REDRAW_FREQUENCY_SECONDS = 0.1
14 | _WIDTH_PER_SAMPLE = 10
15 |
16 | def __init__(
17 | self,
18 | monitor,
19 | type,
20 | title,
21 | color=None,
22 | redraw_freq_seconds=_REDRAW_FREQUENCY_SECONDS,
23 | *args,
24 | **kwargs,
25 | ):
26 | super().__init__(*args, **kwargs)
27 | self._type = type
28 | self._color = color
29 | self._monitor = monitor
30 | self._title = title
31 | self._show_current_value_label = Preferences.get(
32 | PreferenceKeys.SHOW_CURRENT_VALUE
33 | )
34 | redraw_freq_seconds = Preferences.get(PreferenceKeys.REDRAW_FREQUENCY_SECONDS)
35 | draw_smooth_graph = Preferences.get(PreferenceKeys.SMOOTH_GRAPH)
36 | self._graph_area = self._graph_area_instance(
37 | self._color, redraw_freq_seconds, draw_smooth_graph
38 | )
39 | self._graph_area.set_width_per_sample(self._WIDTH_PER_SAMPLE)
40 |
41 | self.set_size_request(120, 65)
42 |
43 | self._redraw_manager = GraphRedrawTickManager(self._tick, redraw_freq_seconds)
44 |
45 | self._overlay_bin = Adw.Bin()
46 | self._overlay_bin.add_css_class("card")
47 | self._overlay = Gtk.Overlay()
48 |
49 | self.set_child(self._overlay_bin)
50 | self._overlay_bin.set_child(self._overlay)
51 |
52 | self._overlay.set_child(self._graph_area.get_drawing_area_widget())
53 |
54 | self._monitor_title_overlay = MonitorTitleOverlay(self._color.HTML)
55 | self._overlay.add_overlay(self._monitor_title_overlay)
56 | self._refresh_title()
57 |
58 | self._setup_graph_area_callback()
59 |
60 | EventBroker.subscribe(events.MONITOR_RENAMED, self._on_monitor_renamed)
61 | EventBroker.subscribe(events.PREFERENCES_CHANGED, self._on_preference_changed)
62 |
63 | self._paintable = Gtk.WidgetPaintable()
64 | self._paintable.set_widget(self)
65 | self._paintable.connect("invalidate-size", self._on_size_changed)
66 |
67 | @property
68 | def type(self):
69 | return self._type
70 |
71 | def _graph_area_instance(self, color, redraw_freq_seconds, draw_smooth_graph):
72 | return GraphArea(color, redraw_freq_seconds, smooth_graph=draw_smooth_graph)
73 |
74 | def _on_monitor_renamed(self, monitor_type, name):
75 | if self._type == monitor_type:
76 | if name is None:
77 | self._set_title(self._title)
78 | else:
79 | self._set_title(name)
80 |
81 | def _on_preference_changed(self, key, value):
82 | if key == PreferenceKeys.SHOW_CURRENT_VALUE:
83 | self._on_show_current_value_changed(value)
84 |
85 | def _on_show_current_value_changed(self, new_value):
86 | self._show_current_value_label = new_value
87 |
88 | def start(self):
89 | self._monitor.start()
90 | self._redraw_manager.start()
91 |
92 | def stop(self):
93 | self._monitor.stop()
94 | self._redraw_manager.stop()
95 |
96 | def _set_value_label(self, value):
97 | self._monitor_title_overlay.set_value(value)
98 |
99 | def _refresh_title(self):
100 | custom_name = Preferences.get_custom_name(self._type)
101 | if custom_name:
102 | self._set_title(custom_name)
103 | else:
104 | self._set_title(self._title)
105 |
106 | def _set_title(self, title):
107 | self._monitor_title_overlay.set_title(title)
108 |
109 | def _tick(self):
110 | self._graph_area.redraw_tick()
111 |
112 | def _setup_graph_area_callback(self):
113 | self._monitor.install_new_values_callback(self._new_values)
114 |
115 | def _new_values(self, values, readable_value=None):
116 | if self._show_current_value_label:
117 | self._set_value_label(readable_value)
118 | else:
119 | self._set_value_label(None)
120 |
121 | self._graph_area.set_new_values(values)
122 |
123 | def _on_size_changed(self, paintable):
124 | new_width = self.get_width()
125 | self._set_max_stored_samples_for_width(new_width)
126 |
127 | def _set_max_stored_samples_for_width(self, width):
128 | num_needed_samples = self._calculate_needed_samples_for_width(width)
129 | self._monitor.set_max_number_of_stored_samples(num_needed_samples)
130 |
131 | def _calculate_needed_samples_for_width(self, width):
132 | num_samples = math.ceil(width / self._WIDTH_PER_SAMPLE)
133 | return num_samples
134 |
--------------------------------------------------------------------------------
/src/ui/monitor_widgets/overlapping_values_monitor_widget.py:
--------------------------------------------------------------------------------
1 | from .monitor_widget import MonitorWidget
2 | from ..overlapping_graphs_area import OverlappingGraphsArea
3 |
4 |
5 | class OverlappingGraphsMonitorWidget(MonitorWidget):
6 | def _graph_area_instance(self, color, redraw_freq_seconds, draw_smooth_graph):
7 | return OverlappingGraphsArea(color, redraw_freq_seconds, draw_smooth_graph)
8 |
--------------------------------------------------------------------------------
/src/ui/monitor_widgets/root_usage_monitor_widget.py:
--------------------------------------------------------------------------------
1 | from .monitor_widget import MonitorWidget
2 | from ...monitors.root_usage_monitor import RootUsageMonitor
3 | from .. import colors
4 | from ...translatable_strings import monitor_title
5 | from ...monitor_type import MonitorType
6 |
7 |
8 | class RootUsageMonitorWidget(MonitorWidget):
9 | def __init__(self, *args, **kwargs):
10 | self._type = MonitorType.Root_usage
11 | self._title = monitor_title.ROOT_USAGE
12 | self._color = colors.PURPLE
13 | self._monitor = RootUsageMonitor()
14 |
15 | super().__init__(
16 | self._monitor, self._type, self._title, self._color, *args, **kwargs
17 | )
18 |
--------------------------------------------------------------------------------
/src/ui/monitor_widgets/swap_monitor_widget.py:
--------------------------------------------------------------------------------
1 | from .monitor_widget import MonitorWidget
2 | from ...monitors.swap_monitor import SwapMonitor
3 | from .. import colors
4 | from ...translatable_strings import monitor_title
5 | from ...monitor_type import MonitorType
6 |
7 |
8 | class SwapMonitorWidget(MonitorWidget):
9 | def __init__(self, *args, **kwargs):
10 | self._type = MonitorType.Swap
11 | self._title = monitor_title.SWAP
12 | self._color = colors.PURPLE
13 | self._monitor = SwapMonitor()
14 |
15 | super().__init__(
16 | self._monitor, self._type, self._title, self._color, *args, **kwargs
17 | )
18 |
--------------------------------------------------------------------------------
/src/ui/monitor_widgets/temperature_sensor_monitor_widget.py:
--------------------------------------------------------------------------------
1 | from .monitor_widget import MonitorWidget
2 | from ...monitors.temperature_monitor import TemperatureMonitor
3 | from .. import colors
4 |
5 |
6 | class TemperatureSensorMonitorWidget(MonitorWidget):
7 | def __init__(self, monitor_type, temperature_sensor_descriptor, *args, **kwargs):
8 | name = f"{temperature_sensor_descriptor.hardware_name}-{temperature_sensor_descriptor.hardware_sensor_name}"
9 | self._type = monitor_type
10 | self._title = f"🌡{name}"
11 | self._color = colors.BROWN
12 | self._monitor = TemperatureMonitor(temperature_sensor_descriptor)
13 |
14 | super().__init__(
15 | self._monitor, self._type, self._title, self._color, *args, **kwargs
16 | )
17 |
--------------------------------------------------------------------------------
/src/ui/monitor_widgets/uplink_monitor_widget.py:
--------------------------------------------------------------------------------
1 | from .monitor_widget import MonitorWidget
2 | from ...monitors.uplink_monitor import UplinkMonitor
3 | from ..relative_graph_area import RelativeGraphArea
4 | from .. import colors
5 | from ...translatable_strings import monitor_title
6 | from ...monitor_type import MonitorType
7 | from ...event_broker import EventBroker
8 | from ... import events
9 | from ...preferences import Preferences
10 | from ...preference_keys import PreferenceKeys
11 |
12 |
13 | class UplinkMonitorWidget(MonitorWidget):
14 | def __init__(self, *args, **kwargs):
15 | self._type = MonitorType.Uplink
16 | self._title = monitor_title.UPLINK
17 | self._color = colors.RED
18 | self._monitor = UplinkMonitor()
19 | self._relative_graph_area = None
20 | self._use_unified_network_scale = Preferences.get(
21 | PreferenceKeys.UNIFIED_SCALE_FOR_NETWORK_MONITORS_ENABLED
22 | )
23 |
24 | EventBroker.subscribe(events.PREFERENCES_CHANGED, self._on_preference_changed)
25 | EventBroker.subscribe(
26 | events.NETWORK_MONITOR_NEW_REFERENCE_VALUE, self._set_new_reference_value
27 | )
28 |
29 | super().__init__(
30 | self._monitor, self._type, self._title, self._color, *args, **kwargs
31 | )
32 |
33 | def _graph_area_instance(self, color, redraw_freq_seconds, draw_smooth_graph):
34 | self._relative_graph_area = RelativeGraphArea(
35 | color,
36 | redraw_freq_seconds,
37 | draw_smooth_graph,
38 | new_reference_value_callback=self._new_reference_value,
39 | )
40 | return self._relative_graph_area
41 |
42 | def _new_reference_value(self, value):
43 | if self._use_unified_network_scale:
44 | EventBroker.notify(
45 | events.UPLINK_NETWORK_MONITOR_NEW_REFERENCE_VALUE_PROPOSAL, value
46 | )
47 | else:
48 | self._relative_graph_area.set_reference_value(value)
49 |
50 | def _set_new_reference_value(self, value):
51 | self._relative_graph_area.set_reference_value(value)
52 |
53 | def _on_preference_changed(self, key, value):
54 | if key == PreferenceKeys.UNIFIED_SCALE_FOR_NETWORK_MONITORS_ENABLED:
55 | self._use_unified_network_scale = value
56 | return
57 |
58 | super()._on_preference_changed(key, value)
59 |
--------------------------------------------------------------------------------
/src/ui/overlapping_graphs_area.py:
--------------------------------------------------------------------------------
1 | from .graph_area import GraphArea
2 |
3 |
4 | class OverlappingGraphsArea(GraphArea):
5 | def __init__(self, color, redraw_frequency_seconds, draw_smooth_graph):
6 | super().__init__(
7 | color, redraw_frequency_seconds, smooth_graph=draw_smooth_graph
8 | )
9 | self._ALPHA_FILL = None
10 |
11 | def _draw_func(self, gtk_drawing_area, context, width, height, user_data):
12 | if self._values is None:
13 | return
14 |
15 | if self._ALPHA_FILL is None:
16 | self._ALPHA_FILL = (super()._ALPHA_FILL / len(self._values)) * 2.0
17 |
18 | values_lists = self._values
19 |
20 | for values in values_lists:
21 | self._draw_values_fill(context, values, width, height)
22 | self._draw_values_ouline(context, values, width, height)
23 |
24 | self._apply_mask(context, width, height)
25 |
--------------------------------------------------------------------------------
/src/ui/popover_menu.py:
--------------------------------------------------------------------------------
1 | from gi.repository import Adw
2 | from gi.repository import Gtk
3 |
4 |
5 | @Gtk.Template(resource_path="/io/github/jorchube/monitorets/gtk/popover-menu.ui")
6 | class PopoverMenu(Gtk.Popover):
7 | __gtype_name__ = "PopoverMenu"
8 |
9 | _preferences_button = Gtk.Template.Child()
10 |
--------------------------------------------------------------------------------
/src/ui/preference_switch.py:
--------------------------------------------------------------------------------
1 | from gi.repository import Gtk
2 |
3 | from ..preferences import Preferences
4 |
5 |
6 | class PreferenceSwitch(Gtk.Switch):
7 | def __init__(self, preference_key, *args, **kwargs):
8 | super().__init__(*args, **kwargs)
9 | self.set_valign(Gtk.Align.CENTER)
10 | self._preference_key = preference_key
11 |
12 | is_active = Preferences.get(self._preference_key)
13 | self.set_active(is_active)
14 |
15 | self.connect("state-set", self._on_state_changed)
16 |
17 | def _on_state_changed(self, emitting_widget, enabled):
18 | is_active = self.get_active()
19 | Preferences.set(self._preference_key, is_active)
20 |
--------------------------------------------------------------------------------
/src/ui/preferences/monitor_preference_row.py:
--------------------------------------------------------------------------------
1 | from gi.repository import Adw, Gtk
2 | from .rename_monitor_popover import RenameMonitorPopover
3 | from ..preference_switch import PreferenceSwitch
4 | from ...preferences import Preferences
5 | from ...translatable_strings import preference_toggle_description
6 |
7 |
8 | class MonitorPreferenceRow(Adw.ActionRow):
9 | def __init__(self, monitor_type, title, preference_key, subtitle=None):
10 | super().__init__()
11 |
12 | self._monitor_type = monitor_type
13 |
14 | self.set_title(title)
15 |
16 | self._custom_name_label = self._create_custom_name_label()
17 | self.add_suffix(self._custom_name_label)
18 |
19 | self._rename_popover = RenameMonitorPopover(self._on_rename)
20 |
21 | self._edit_button = self._create_edit_button()
22 | self.add_suffix(self._edit_button)
23 |
24 | switch = PreferenceSwitch(preference_key)
25 | self.add_suffix(switch)
26 | self.set_activatable_widget(switch)
27 |
28 | custom_name = Preferences.get_custom_name(self._monitor_type)
29 | if custom_name:
30 | self._set_custom_name(custom_name, persist=False)
31 |
32 | if subtitle is not None:
33 | self.set_subtitle(subtitle)
34 |
35 | self._install_motion_event_controller()
36 |
37 | def _on_rename(self, new_name):
38 | if new_name:
39 | self._set_custom_name(new_name)
40 | else:
41 | self._reset_name()
42 |
43 | def _create_custom_name_label(self):
44 | label = Gtk.Label()
45 | label.add_css_class("dim-label")
46 | label.add_css_class("caption-heading")
47 | label.set_valign(Gtk.Align.CENTER)
48 | label.set_xalign(0)
49 |
50 | return label
51 |
52 | def _set_custom_name(self, custom_name, persist=True):
53 | self._custom_name = custom_name
54 | self._custom_name_label.set_size_request(120, -1)
55 | self._custom_name_label.set_markup(
56 | f'{preference_toggle_description.SHOWN_AS}:\n {custom_name} '
57 | )
58 |
59 | self._rename_popover.set_text(custom_name)
60 |
61 | if persist is True:
62 | Preferences.set_custom_name(self._monitor_type, custom_name)
63 |
64 | def _reset_name(self):
65 | self._custom_name = None
66 | self._custom_name_label.set_size_request(-1, -1)
67 | self._custom_name_label.set_label("")
68 | Preferences.set_custom_name(self._monitor_type, None)
69 |
70 | def _create_edit_button(self):
71 | edit_button = Gtk.MenuButton()
72 | edit_button.set_icon_name("document-edit-symbolic")
73 | edit_button.add_css_class("flat")
74 | edit_button.set_valign(Gtk.Align.CENTER)
75 | edit_button.set_opacity(0)
76 | edit_button.set_popover(self._rename_popover)
77 | return edit_button
78 |
79 | def _install_motion_event_controller(self):
80 | controller = Gtk.EventControllerMotion()
81 | controller.connect("enter", self._on_mouse_enter)
82 | controller.connect("leave", self._on_mouse_leave)
83 | self.add_controller(controller)
84 |
85 | def _on_mouse_enter(self, motion_controller, x, y):
86 | self._edit_button.set_opacity(1)
87 |
88 | def _on_mouse_leave(self, motion_controller):
89 | self._edit_button.set_opacity(0)
90 |
--------------------------------------------------------------------------------
/src/ui/preferences/preferences_page_appearance.py:
--------------------------------------------------------------------------------
1 | from gi.repository import Adw, Gtk
2 |
3 | from ..preference_switch import PreferenceSwitch
4 | from ...preferences import Preferences
5 | from ...preference_keys import PreferenceKeys
6 | from ...theme import Theme
7 | from ...layout import Layout
8 | from .temperature_units_toggle_widget import TemperatureUnitsToggleWidget
9 | from .redraw_frequency_toggle_widget import RedrawFrequencyToggleWidget
10 |
11 |
12 | @Gtk.Template(
13 | resource_path="/io/github/jorchube/monitorets/gtk/preferences-page-appearance.ui"
14 | )
15 | class PreferencesPageAppearance(Adw.PreferencesPage):
16 | __gtype_name__ = "PreferencesPageAppearance"
17 |
18 | _system_theme_toggle_button = Gtk.Template.Child()
19 | _light_theme_toggle_button = Gtk.Template.Child()
20 | _dark_theme_toggle_button = Gtk.Template.Child()
21 |
22 | _system_theme_toggle_button_image_big = Gtk.Template.Child()
23 | _system_theme_toggle_button_image_small = Gtk.Template.Child()
24 | _light_theme_toggle_button_image_big = Gtk.Template.Child()
25 | _light_theme_toggle_button_image_small = Gtk.Template.Child()
26 | _dark_theme_toggle_button_image_big = Gtk.Template.Child()
27 | _dark_theme_toggle_button_image_small = Gtk.Template.Child()
28 |
29 | _system_theme_toggle_image_squeezer = Gtk.Template.Child()
30 |
31 | _horizontal_layout_action_row = Gtk.Template.Child()
32 | _vertical_layout_action_row = Gtk.Template.Child()
33 | _grid_layout_action_row = Gtk.Template.Child()
34 |
35 | _smooth_graphs_action_row = Gtk.Template.Child()
36 | _show_current_value_action_row = Gtk.Template.Child()
37 | _temperature_units_action_row = Gtk.Template.Child()
38 | _redraw_frequency_action_row = Gtk.Template.Child()
39 |
40 | def __init__(self, *args, **kwargs):
41 | super().__init__(*args, **kwargs)
42 |
43 | self._vertical_check_button = Gtk.CheckButton()
44 | self._horizontal_check_button = Gtk.CheckButton()
45 | self._grid_check_button = Gtk.CheckButton()
46 | self._init_toggles()
47 |
48 | theme = Preferences.get(PreferenceKeys.THEME)
49 | self._set_active_toggle_for_theme(theme)
50 |
51 | layout = Preferences.get(PreferenceKeys.LAYOUT)
52 | self._set_active_toggle_for_layout(layout)
53 |
54 | self._system_theme_toggle_button.connect(
55 | "clicked", self._on_system_theme_button_clicked
56 | )
57 | self._light_theme_toggle_button.connect(
58 | "clicked", self._on_light_theme_button_clicked
59 | )
60 | self._dark_theme_toggle_button.connect(
61 | "clicked", self._on_dark_theme_button_clicked
62 | )
63 |
64 | self._vertical_check_button.connect(
65 | "toggled", self._on_vertical_check_button_toggled
66 | )
67 | self._horizontal_check_button.connect(
68 | "toggled", self._on_horizontal_check_button_toggled
69 | )
70 | self._grid_check_button.connect("toggled", self._on_grid_check_button_toggled)
71 |
72 | def _init_toggles(self):
73 | self._system_theme_toggle_button_image_big.set_from_resource(
74 | "/io/github/jorchube/monitorets/gtk/icons/system.png"
75 | )
76 | self._system_theme_toggle_button_image_small.set_from_resource(
77 | "/io/github/jorchube/monitorets/gtk/icons/system.png"
78 | )
79 |
80 | self._light_theme_toggle_button_image_big.set_from_resource(
81 | "/io/github/jorchube/monitorets/gtk/icons/light.png"
82 | )
83 | self._light_theme_toggle_button_image_small.set_from_resource(
84 | "/io/github/jorchube/monitorets/gtk/icons/light.png"
85 | )
86 |
87 | self._dark_theme_toggle_button_image_big.set_from_resource(
88 | "/io/github/jorchube/monitorets/gtk/icons/dark.png"
89 | )
90 | self._dark_theme_toggle_button_image_small.set_from_resource(
91 | "/io/github/jorchube/monitorets/gtk/icons/dark.png"
92 | )
93 |
94 | self._vertical_layout_action_row.add_prefix(self._vertical_check_button)
95 | self._vertical_layout_action_row.set_activatable_widget(
96 | self._vertical_check_button
97 | )
98 |
99 | self._horizontal_check_button.set_group(self._vertical_check_button)
100 | self._horizontal_layout_action_row.add_prefix(self._horizontal_check_button)
101 | self._horizontal_layout_action_row.set_activatable_widget(
102 | self._horizontal_check_button
103 | )
104 |
105 | self._grid_check_button.set_group(self._vertical_check_button)
106 | self._grid_layout_action_row.add_prefix(self._grid_check_button)
107 | self._grid_layout_action_row.set_activatable_widget(self._grid_check_button)
108 |
109 | smooth_graph_switch = PreferenceSwitch(PreferenceKeys.SMOOTH_GRAPH)
110 | self._smooth_graphs_action_row.add_suffix(smooth_graph_switch)
111 | self._smooth_graphs_action_row.set_activatable_widget(smooth_graph_switch)
112 |
113 | show_current_value_switch = PreferenceSwitch(PreferenceKeys.SHOW_CURRENT_VALUE)
114 | self._show_current_value_action_row.add_suffix(show_current_value_switch)
115 | self._show_current_value_action_row.set_activatable_widget(
116 | show_current_value_switch
117 | )
118 |
119 | self._temperature_units_action_row.add_suffix(TemperatureUnitsToggleWidget())
120 |
121 | self._redraw_frequency_action_row.add_suffix(RedrawFrequencyToggleWidget())
122 |
123 | def _on_system_theme_button_clicked(self, user_data):
124 | Preferences.set(PreferenceKeys.THEME, Theme.SYSTEM)
125 |
126 | def _on_light_theme_button_clicked(self, user_data):
127 | Preferences.set(PreferenceKeys.THEME, Theme.LIGHT)
128 |
129 | def _on_dark_theme_button_clicked(self, user_data):
130 | Preferences.set(PreferenceKeys.THEME, Theme.DARK)
131 |
132 | def _set_active_toggle_for_theme(self, theme):
133 | theme_to_toggle_button_map = {
134 | Theme.SYSTEM: self._system_theme_toggle_button,
135 | Theme.LIGHT: self._light_theme_toggle_button,
136 | Theme.DARK: self._dark_theme_toggle_button,
137 | }
138 | theme_to_toggle_button_map[theme].set_active(True)
139 |
140 | def _set_active_toggle_for_layout(self, layout):
141 | layout_to_toggle_button_map = {
142 | Layout.HORIZONTAL: self._horizontal_check_button,
143 | Layout.VERTICAL: self._vertical_check_button,
144 | Layout.GRID: self._grid_check_button,
145 | }
146 | layout_to_toggle_button_map[layout].set_active(True)
147 |
148 | def _on_vertical_check_button_toggled(self, toggle_button):
149 | if toggle_button.get_active():
150 | Preferences.set(PreferenceKeys.LAYOUT, Layout.VERTICAL)
151 |
152 | def _on_horizontal_check_button_toggled(self, toggle_button):
153 | if toggle_button.get_active():
154 | Preferences.set(PreferenceKeys.LAYOUT, Layout.HORIZONTAL)
155 |
156 | def _on_grid_check_button_toggled(self, toggle_button):
157 | if toggle_button.get_active():
158 | Preferences.set(PreferenceKeys.LAYOUT, Layout.GRID)
159 |
--------------------------------------------------------------------------------
/src/ui/preferences/preferences_page_monitors.py:
--------------------------------------------------------------------------------
1 | from gi.repository import Adw, Gtk
2 |
3 | from ...translatable_strings import preference_toggle_section_name
4 | from .monitor_preference_row import MonitorPreferenceRow
5 | from ...monitor_descriptors import (
6 | get_monitor_descriptors_grouped_by_preference_toggle_section,
7 | )
8 |
9 |
10 | @Gtk.Template(
11 | resource_path="/io/github/jorchube/monitorets/gtk/preferences-page-monitors.ui"
12 | )
13 | class PreferencesPageMonitors(Adw.PreferencesPage):
14 | __gtype_name__ = "PreferencesPageMonitors"
15 |
16 | _cpu_preferences_group = Gtk.Template.Child()
17 | _gpu_preferences_group = Gtk.Template.Child()
18 | _memory_preferences_group = Gtk.Template.Child()
19 | _network_preferences_group = Gtk.Template.Child()
20 | _disk_preferences_group = Gtk.Template.Child()
21 | _pressure_preferences_group = Gtk.Template.Child()
22 | _temperature_preferences_group = Gtk.Template.Child()
23 |
24 | def __init__(self, *args, **kwargs):
25 | super().__init__(*args, **kwargs)
26 |
27 | self._add_toggles()
28 |
29 | def _add_toggles(self):
30 | descriptors = get_monitor_descriptors_grouped_by_preference_toggle_section()
31 |
32 | for descriptor in descriptors[preference_toggle_section_name.CPU]:
33 | self._add_toggle_to_group(descriptor, self._cpu_preferences_group)
34 |
35 | for descriptor in descriptors[preference_toggle_section_name.GPU]:
36 | self._add_toggle_to_group(descriptor, self._gpu_preferences_group)
37 |
38 | for descriptor in descriptors[preference_toggle_section_name.MEMORY]:
39 | self._add_toggle_to_group(descriptor, self._memory_preferences_group)
40 |
41 | for descriptor in descriptors[preference_toggle_section_name.NETWORK]:
42 | self._add_toggle_to_group(descriptor, self._network_preferences_group)
43 |
44 | for descriptor in descriptors[preference_toggle_section_name.DISK_USAGE]:
45 | self._add_toggle_to_group(descriptor, self._disk_preferences_group)
46 |
47 | for descriptor in descriptors[preference_toggle_section_name.PRESSURE]:
48 | self._add_toggle_to_group(descriptor, self._pressure_preferences_group)
49 |
50 | for descriptor in descriptors[preference_toggle_section_name.TEMPERATURE]:
51 | self._add_toggle_to_group(descriptor, self._temperature_preferences_group)
52 |
53 | def _add_toggle_to_group(self, monitor_descriptor, group):
54 | action_row = self._build_toggle_action_row(monitor_descriptor)
55 | group.add(action_row)
56 |
57 | def _build_toggle_action_row(self, monitor_descriptor):
58 | monitor_type = monitor_descriptor["type"]
59 | label = monitor_descriptor["preference_toggle_label"]
60 | enabled_preference_key = monitor_descriptor["enabled_preference_key"]
61 | description = monitor_descriptor.get("preference_toggle_description")
62 |
63 | return MonitorPreferenceRow(
64 | monitor_type, label, enabled_preference_key, subtitle=description
65 | )
66 |
--------------------------------------------------------------------------------
/src/ui/preferences/preferences_window.py:
--------------------------------------------------------------------------------
1 | from gi.repository import Adw, Gtk
2 |
3 | from .preferences_page_appearance import PreferencesPageAppearance
4 | from .preferences_page_monitors import PreferencesPageMonitors
5 |
6 |
7 | @Gtk.Template(resource_path="/io/github/jorchube/monitorets/gtk/preferences-window.ui")
8 | class PreferencesWindow(Adw.PreferencesWindow):
9 | __gtype_name__ = "PreferencesWindow"
10 |
11 | def __init__(self, *args, **kwargs):
12 | super().__init__(*args, **kwargs)
13 | self.set_modal(True)
14 |
15 | self.add(PreferencesPageAppearance())
16 | self.add(PreferencesPageMonitors())
17 |
--------------------------------------------------------------------------------
/src/ui/preferences/redraw_frequency_toggle_widget.py:
--------------------------------------------------------------------------------
1 | from gi.repository import Adw, Gtk
2 | from ...preferences import Preferences
3 | from ...preference_keys import PreferenceKeys
4 | from ...translatable_strings import redraw_frequency as redraw_frequency_labels
5 | from ... import monitor_redraw_frequency_seconds_values as redraw_frequency_values
6 |
7 |
8 | class RedrawFrequencyToggleWidget(Adw.Bin):
9 | def __init__(self):
10 | super().__init__()
11 | self.set_margin_top(10)
12 | self.set_margin_bottom(10)
13 |
14 | self._options_map = {
15 | 0: {
16 | "value": redraw_frequency_values.VERY_HIGH,
17 | "label": redraw_frequency_labels.VERY_HIGH,
18 | },
19 | 1: {
20 | "value": redraw_frequency_values.HIGH,
21 | "label": redraw_frequency_labels.HIGH,
22 | },
23 | 2: {
24 | "value": redraw_frequency_values.LOW,
25 | "label": redraw_frequency_labels.LOW,
26 | },
27 | 3: {
28 | "value": redraw_frequency_values.VERY_LOW,
29 | "label": redraw_frequency_labels.VERY_LOW,
30 | },
31 | }
32 |
33 | combo_box = Gtk.DropDown.new_from_strings(self._get_dropdown_options())
34 | self.set_child(combo_box)
35 | self._mark_current_selected_item(combo_box)
36 |
37 | combo_box.connect("notify::selected", self._on_selected_item)
38 |
39 | def _mark_current_selected_item(self, combo_box):
40 | current_value = Preferences.get(PreferenceKeys.REDRAW_FREQUENCY_SECONDS)
41 | current_index = self._get_index_for_frequency(current_value)
42 | combo_box.set_selected(current_index)
43 |
44 | def _on_selected_item(self, dropdown, _):
45 | index = dropdown.get_selected()
46 | redraw_frequency = self._get_frequency_for_index(index)
47 | Preferences.set(PreferenceKeys.REDRAW_FREQUENCY_SECONDS, redraw_frequency)
48 |
49 | def _get_dropdown_options(self):
50 | return [
51 | self._options_map[0]["label"],
52 | self._options_map[1]["label"],
53 | self._options_map[2]["label"],
54 | self._options_map[3]["label"],
55 | ]
56 |
57 | def _get_frequency_for_index(self, index):
58 | return self._options_map[index]["value"]
59 |
60 | def _get_index_for_frequency(self, value):
61 | for index in range(len(self._options_map)):
62 | if self._options_map[index]["value"] == value:
63 | return index
64 |
--------------------------------------------------------------------------------
/src/ui/preferences/rename_monitor_popover.py:
--------------------------------------------------------------------------------
1 | from gi.repository import Adw, Gtk
2 |
3 |
4 | @Gtk.Template(
5 | resource_path="/io/github/jorchube/monitorets/gtk/rename-monitor-popover.ui"
6 | )
7 | class RenameMonitorPopover(Gtk.Popover):
8 | __gtype_name__ = "RenameMonitorPopover"
9 |
10 | _text_entry = Gtk.Template.Child()
11 | _rename_button = Gtk.Template.Child()
12 |
13 | def __init__(self, rename_callback):
14 | super().__init__()
15 | self._rename_button.connect("clicked", self._on_rename_clicked)
16 | self._text_entry.connect("activate", self._on_enter_pressed_on_text_entry)
17 | self._rename_callback = rename_callback
18 | self._current_name = None
19 |
20 | def set_text(self, text):
21 | if text:
22 | self._text_entry.get_buffer().set_text(text, len(text))
23 |
24 | def _on_rename_clicked(self, user_data):
25 | self._apply_rename()
26 |
27 | def _on_enter_pressed_on_text_entry(self, user_data):
28 | self._apply_rename()
29 |
30 | def _apply_rename(self):
31 | text = self._text_entry.get_buffer().get_text().strip()
32 | self._rename_callback(text)
33 | self.popdown()
34 |
--------------------------------------------------------------------------------
/src/ui/preferences/temperature_units_toggle_widget.py:
--------------------------------------------------------------------------------
1 | from gi.repository import Adw, Gtk
2 | from ...preferences import Preferences
3 | from ...preference_keys import PreferenceKeys
4 | from ...temperature import CELSIUS, FAHRENHEIT
5 |
6 |
7 | class TemperatureUnitsToggleWidget(Adw.Bin):
8 | def __init__(self):
9 | super().__init__()
10 | self.set_margin_top(10)
11 | self.set_margin_bottom(10)
12 |
13 | self._fahrenheit_toggle = Gtk.ToggleButton(label="℉")
14 | self._celsius_toggle = Gtk.ToggleButton(label="℃")
15 | self._celsius_toggle.set_group(self._fahrenheit_toggle)
16 |
17 | buttons_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
18 | buttons_container.add_css_class("linked")
19 | buttons_container.append(self._celsius_toggle)
20 | buttons_container.append(self._fahrenheit_toggle)
21 | self.set_child(buttons_container)
22 |
23 | self._set_current_active_toggle()
24 | self._celsius_toggle.connect("toggled", self._on_celsius_toggled)
25 | self._fahrenheit_toggle.connect("toggled", self._on_fahrenheit_toggled)
26 |
27 | def _set_current_active_toggle(self):
28 | current_units = Preferences.get(PreferenceKeys.TEMPERATURE_UNITS)
29 | self._celsius_toggle.set_active(current_units == CELSIUS)
30 | self._fahrenheit_toggle.set_active(current_units == FAHRENHEIT)
31 |
32 | def _on_celsius_toggled(self, *_):
33 | if self._celsius_toggle.get_active():
34 | Preferences.set(PreferenceKeys.TEMPERATURE_UNITS, CELSIUS)
35 |
36 | def _on_fahrenheit_toggled(self, *_):
37 | if self._fahrenheit_toggle.get_active():
38 | Preferences.set(PreferenceKeys.TEMPERATURE_UNITS, FAHRENHEIT)
39 |
--------------------------------------------------------------------------------
/src/ui/relative_graph_area.py:
--------------------------------------------------------------------------------
1 | from .graph_area import GraphArea
2 |
3 |
4 | class RelativeGraphArea(GraphArea):
5 | def __init__(
6 | self,
7 | color,
8 | redraw_frequency_seconds,
9 | draw_smooth_graph,
10 | new_reference_value_callback,
11 | ):
12 | self._minimum_reference_value = 1000
13 | self._reference_value = self._minimum_reference_value
14 | self._own_reference_value = self._minimum_reference_value
15 | self._new_reference_value_callback = new_reference_value_callback
16 | super().__init__(color, redraw_frequency_seconds, draw_smooth_graph)
17 |
18 | def set_new_values(self, values):
19 | normalized_values = self._normalize_values(values)
20 | return super().set_new_values(normalized_values)
21 |
22 | def _normalize_values(self, values):
23 | self._refresh_reference_value(values)
24 |
25 | normalized_values = []
26 |
27 | for value in values:
28 | normalized_value = self._calculate_normalized_value(value)
29 | normalized_values.append(normalized_value)
30 |
31 | return normalized_values
32 |
33 | def _calculate_normalized_value(self, value):
34 | if self._reference_value == 0:
35 | return 0
36 |
37 | return int(value * 100 / self._reference_value)
38 |
39 | def _refresh_reference_value(self, values):
40 | previous_own_reference_value = self._own_reference_value
41 | self._own_reference_value = max(max(values), self._minimum_reference_value)
42 |
43 | if previous_own_reference_value != self._own_reference_value:
44 | self._new_reference_value_callback(self._own_reference_value)
45 |
46 | def set_reference_value(self, value):
47 | self._reference_value = value
48 |
--------------------------------------------------------------------------------
/src/ui/single_window.py:
--------------------------------------------------------------------------------
1 | # window.py
2 | #
3 | # Copyright 2022 Jordi Chulia
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | #
18 | # SPDX-License-Identifier: GPL-3.0-or-later
19 |
20 | from gi.repository import Adw
21 | from gi.repository import Gtk
22 |
23 | from .headerbar_wrapper import HeaderBarWrapper
24 | from .window_layout_manager import WindowLayoutManager
25 | from ..preferences import Preferences
26 | from ..preference_keys import PreferenceKeys
27 | from ..window_geometry import WindowGeometry
28 |
29 |
30 | @Gtk.Template(resource_path="/io/github/jorchube/monitorets/gtk/single-window.ui")
31 | class SingleWindow(Adw.ApplicationWindow):
32 | __gtype_name__ = "SingleWindow"
33 |
34 | _overlay = Gtk.Template.Child()
35 | _monitors_container_bin = Gtk.Template.Child()
36 |
37 | def __init__(self, *args, **kwargs):
38 | super().__init__(*args, **kwargs)
39 |
40 | window_geometry = Preferences.get(PreferenceKeys.WINDOW_GEOMETRY)
41 | self.set_default_size(window_geometry.width, window_geometry.height)
42 |
43 | self._headerbar_wrapper = HeaderBarWrapper(parent_window=self)
44 | self._overlay.add_overlay(self._headerbar_wrapper.root_widget)
45 | self._monitors_container_bin.set_child(
46 | WindowLayoutManager.get_container_widget()
47 | )
48 |
49 | self.connect("close-request", self._close_request)
50 | self._install_motion_event_controller()
51 |
52 | def _install_motion_event_controller(self):
53 | controller = Gtk.EventControllerMotion()
54 | controller.connect("enter", self._on_mouse_enter)
55 | controller.connect("leave", self._on_mouse_leave)
56 | self._overlay.add_controller(controller)
57 |
58 | def _on_mouse_enter(self, motion_controller, x, y):
59 | self._headerbar_wrapper.on_mouse_enter()
60 |
61 | def _on_mouse_leave(self, motion_controller):
62 | self._headerbar_wrapper.on_mouse_exit()
63 |
64 | def _close_request(self, user_data):
65 | self._persist_window_geometry()
66 |
67 | def _persist_window_geometry(self):
68 | window_geometry = WindowGeometry(
69 | width=self.get_width(), height=self.get_height()
70 | )
71 | Preferences.set(PreferenceKeys.WINDOW_GEOMETRY, window_geometry)
72 |
--------------------------------------------------------------------------------
/src/ui/tips_window.py:
--------------------------------------------------------------------------------
1 | from gi.repository import Adw
2 | from gi.repository import Gtk
3 |
4 | from ..translatable_strings import tips
5 |
6 |
7 | @Gtk.Template(resource_path="/io/github/jorchube/monitorets/gtk/tips-window.ui")
8 | class TipsWindow(Adw.Window):
9 | __gtype_name__ = "TipsWindow"
10 |
11 | _headerbar = Gtk.Template.Child()
12 | _tips_box = Gtk.Template.Child()
13 |
14 | def __init__(self, *args, **kwargs):
15 | super().__init__(*args, **kwargs)
16 |
17 | title_label = Gtk.Label()
18 | title_label.set_markup(f'{tips.WINDOW_TITLE} ')
19 | self._headerbar.set_title_widget(title_label)
20 |
21 | tip = self._new_tip_content(tips.ALWAYS_ON_TOP_TITLE, tips.ALWAYS_ON_TOP_BODY)
22 |
23 | self._tips_box.append(tip)
24 |
25 | self.set_default_size(400, 20)
26 |
27 | def _new_tip_content(self, title, description):
28 | title_label = self._build_title_label(title)
29 | description_label = self._build_description_label(description)
30 |
31 | box = Gtk.Box()
32 | box.set_halign(Gtk.Align.START)
33 | box.add_css_class("card")
34 | box.set_orientation(Gtk.Orientation.VERTICAL)
35 | box.append(title_label)
36 | box.append(description_label)
37 |
38 | return box
39 |
40 | def _build_title_label(self, title):
41 | label = Gtk.Label(label=title)
42 | label.set_margin_top(10)
43 | label.set_margin_bottom(5)
44 | label.set_margin_start(20)
45 | label.set_margin_end(20)
46 | label.add_css_class("heading")
47 |
48 | return label
49 |
50 | def _build_description_label(self, description):
51 | label = Gtk.Label()
52 | label.set_markup(description)
53 | label.set_margin_top(5)
54 | label.set_margin_bottom(20)
55 | label.set_margin_start(20)
56 | label.set_margin_end(20)
57 | label.set_wrap(True)
58 |
59 | return label
60 |
--------------------------------------------------------------------------------
/src/ui/window_layout_manager.py:
--------------------------------------------------------------------------------
1 | from gi.repository import Gtk
2 | from ..event_broker import EventBroker
3 | from .. import events
4 | from ..preferences import Preferences
5 | from ..preference_keys import PreferenceKeys
6 | from ..layout import Layout
7 | from .. import monitor_descriptors
8 | from math import ceil
9 |
10 |
11 | class WindowLayoutManager:
12 | @classmethod
13 | def initialize(self):
14 | self._monitors_flow_box = Gtk.FlowBox()
15 | self._monitors_flow_box.set_row_spacing(5)
16 | self._monitors_flow_box.set_column_spacing(5)
17 | self._num_monitors = 0
18 |
19 | self._layout_selected_callbacks = {
20 | Layout.HORIZONTAL: self._horizontal_layout_selected,
21 | Layout.VERTICAL: self._vertical_layout_selected,
22 | Layout.GRID: self._grid_layout_selected,
23 | }
24 |
25 | EventBroker.subscribe(events.PREFERENCES_CHANGED, self._on_preferences_changed)
26 |
27 | self._refresh_layout_from_preferences()
28 | self._monitors_flow_box.set_sort_func(self._sort_function, None, None)
29 |
30 | @classmethod
31 | def add_monitor(self, monitor):
32 | self._monitors_flow_box.append(monitor)
33 | self._num_monitors += 1
34 | self._refresh_grid_row_limit()
35 |
36 | @classmethod
37 | def remove_monitor(self, monitor):
38 | self._monitors_flow_box.remove(monitor)
39 | self._num_monitors -= 1
40 | self._refresh_grid_row_limit()
41 |
42 | @classmethod
43 | def _refresh_grid_row_limit(self):
44 | layout = Preferences.get(PreferenceKeys.LAYOUT)
45 | if layout in [Layout.HORIZONTAL, Layout.VERTICAL]:
46 | self._set_grid_row_limit(1)
47 | return
48 | self._set_grid_row_limit(ceil(self._num_monitors / 3))
49 |
50 | @classmethod
51 | def _set_grid_row_limit(self, value):
52 | self._monitors_flow_box.set_max_children_per_line(value)
53 | self._monitors_flow_box.set_min_children_per_line(value)
54 |
55 | @classmethod
56 | def get_container_widget(self):
57 | return self._monitors_flow_box
58 |
59 | @classmethod
60 | def _refresh_layout_from_preferences(self):
61 | layout = Preferences.get(PreferenceKeys.LAYOUT)
62 | self._layout_selected_callbacks[layout]()
63 |
64 | @classmethod
65 | def _on_preferences_changed(self, preference_key, value):
66 | if preference_key == PreferenceKeys.LAYOUT:
67 | self._refresh_layout_from_preferences()
68 |
69 | @classmethod
70 | def _horizontal_layout_selected(self):
71 | self._monitors_flow_box.set_orientation(Gtk.Orientation.VERTICAL)
72 | self._refresh_grid_row_limit()
73 |
74 | @classmethod
75 | def _vertical_layout_selected(self):
76 | self._monitors_flow_box.set_orientation(Gtk.Orientation.HORIZONTAL)
77 | self._refresh_grid_row_limit()
78 |
79 | @classmethod
80 | def _grid_layout_selected(self):
81 | self._monitors_flow_box.set_orientation(Gtk.Orientation.HORIZONTAL)
82 | self._refresh_grid_row_limit()
83 |
84 | @classmethod
85 | def _sort_function(self, flow_box_child_1, flow_box_child_2, *_):
86 | monitor_1 = flow_box_child_1.get_child()
87 | monitor_2 = flow_box_child_2.get_child()
88 |
89 | ordering_dict = monitor_descriptors.get_ordering_dict()
90 | monitor_1_order = ordering_dict[monitor_1.type]
91 | monitor_2_order = ordering_dict[monitor_2.type]
92 | return monitor_1_order - monitor_2_order
93 |
--------------------------------------------------------------------------------
/src/units.py:
--------------------------------------------------------------------------------
1 | class GiB:
2 | value = 1024 * 1024 * 1024
3 | unit = "GiB"
4 |
5 |
6 | class MiB:
7 | value = 1024 * 1024
8 | unit = "MiB"
9 |
10 |
11 | class KiB:
12 | value = 1024
13 | unit = "KiB"
14 |
15 |
16 | class Byte:
17 | value = 1
18 | unit = "B"
19 |
20 |
21 | def convert(value, from_units, to_units):
22 | return value * (from_units.value / to_units.value)
23 |
--------------------------------------------------------------------------------
/src/window_geometry.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, asdict
2 |
3 |
4 | @dataclass
5 | class WindowGeometry:
6 | width: int
7 | height: int
8 |
9 | def as_dict(self):
10 | return asdict(self)
11 |
12 | @classmethod
13 | def from_dict(self, a_dict):
14 | return WindowGeometry(**a_dict)
15 |
--------------------------------------------------------------------------------