├── .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 | Download on Flathub 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 | Download on Flathub 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 | 183 | 184 | -------------------------------------------------------------------------------- /src/gtk/preferences-page-monitors.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 47 | 48 | -------------------------------------------------------------------------------- /src/gtk/preferences-window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /src/gtk/rename-monitor-popover.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 48 | 49 | -------------------------------------------------------------------------------- /src/gtk/single-window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 22 | 23 | -------------------------------------------------------------------------------- /src/gtk/tips-window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 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 | --------------------------------------------------------------------------------