diff --git a/99-plasma-pstate b/99-plasma-pstate deleted file mode 100644 index 70a42ab..0000000 --- a/99-plasma-pstate +++ /dev/null @@ -1 +0,0 @@ -%sudo ALL=NOPASSWD: /usr/share/plasma/plasmoids/gr.ictpro.jsalatas.plasma.pstate/contents/code/set_prefs.sh diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8257418 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.16) + +project(plasma-pstate) + +find_package(ECM 1.4.0 REQUIRED NO_MODULE) +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) + +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDECompilerSettings NO_POLICY_SCOPE) +include(FeatureSummary) + +find_package(KF5 REQUIRED COMPONENTS + Plasma + I18n +) + +find_package(Qt5 CONFIG REQUIRED COMPONENTS + Quick + Core +) + +add_subdirectory(src) + +feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/Messages.sh b/Messages.sh new file mode 100644 index 0000000..633b987 --- /dev/null +++ b/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_applet_gr.ictpro.jsalatas.plasma.pstate.pot diff --git a/README.md b/README.md index 23910a0..8b6f34f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,5 @@ # Intel P-state and CPUFreq Manager Widget -```diff --Important -``` -as of October 11, 2020, I'm not willing to maintain this widget anymore. I will maintain it based only to my personal needs. I thank you for your support and feedback all this time. Hope the community will take over from here and make the widget even better than it is now. - ## What it is Intel P-state and CPUFreq Manager is a KDE Plasma widget in order to control the frequencies of Intel CPUs and their integrated GPUs for any modern Intel @@ -16,22 +11,22 @@ It can also manage the processor's energy consumption through Energy-Performance Preference (EPP) knob (if supported) or the Energy-Performance Bias (EPB) knob (otherwise). -Intel P-state and CPUFreq Manager Widget +Intel P-state and CPUFreq Manager Widget Furthermore, it allows you to interact with the following vendor specific settings 1. [Dell's Thermal Management Feature](https://www.dell.com/support/manuals/ba/en/babsdt1/dell-command-power-manager-v2.2/userguide_dell/thermal-management?guid=guid-c05d2582-fc07-4e3e-918a-965836d20752&lang=en-us) through [libsmbios library](https://github.com/dell/libsmbios). -Intel P-state and CPUFreq Manager Widget with Dell's Thermal Management Feature +Intel P-state and CPUFreq Manager Widget with Dell's Thermal Management Feature 2. LG Gram laptop [Battery care limit](https://github.com/torvalds/linux/blob/master/Documentation/laptops/lg-laptop.rst#battery-care-limit), [USB charge](https://github.com/torvalds/linux/blob/master/Documentation/laptops/lg-laptop.rst#usb-charge) and [Fan mode](https://github.com/torvalds/linux/blob/master/Documentation/laptops/lg-laptop.rst#fan-mode) features (on kernel 4.20 and higher). -Intel P-state and CPUFreq Manager Widget with LG Laptop Support features +Intel P-state and CPUFreq Manager Widget with LG Laptop Support features 3. [Nvidia PowerMizer Settings](https://www.nvidia.com/object/feature_powermizer.html). -Intel P-state and CPUFreq Manager Widget with Nvidia PowerMizer Settings +Intel P-state and CPUFreq Manager Widget with Nvidia PowerMizer Settings ## What it isn't @@ -58,10 +53,19 @@ First of all you need to be in sudoers' group. After that you can just clone the code and install it using the following commands: ``` -git clone https://github.com/jsalatas/plasma-pstate +git clone https://github.com/frankenfruity/plasma-pstate cd plasma-pstate -sudo ./install.sh +mkdir build +cd build +cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr -DCMAKE_BUILD_TYPE=Release .. +make +make install + +cd .. +cp ./src/plasma_pstate.policy /usr/share/polkit-1/actions/ +chmod 644 /usr/share/polkit-1/actions/plasma_pstate.policy ``` + **Notice:** If your processor doesn't support EPP(ie older generations without HWP), then you need also to install the ``x86_energy_perf_policy`` which (in case of Ubuntu 18.04 distros) is provided by the ``linux-tools`` package and @@ -70,26 +74,38 @@ can be installed using the following command ``` sudo apt install linux-tools-generic linux-tools-`uname -r` ``` -## How to update -Assuming that you have checked out the latest code, you just need to run the following commands as root: -``` -sudo kpackagetool5 -g -t Plasma/Applet -u gr.ictpro.jsalatas.plasma.pstate +## Custom profiles -sudo chmod 755 /usr/share/plasma/plasmoids/gr.ictpro.jsalatas.plasma.pstate/contents/code/set_prefs.sh -``` +A profile applies multiple settings at once. + -## Contributions -Please feel free to clone, hack, and contribute anything you may find useful, -especially in relation to similar to Dell's Thermal Management Feature that -may be available in other hardware platforms. +Click the Edit profiles button to manage profiles. + + -## Issues -The widget uses a shell script that needs to be run as root user (sudo) and which apparently floods your log files with security related messages as described in [issue #16](https://github.com/jsalatas/plasma-pstate/issues/16). +In edit mode, use the toolbar buttons to create or delete a profile. -If you want to get rid of these messages, please see a workaround at + -https://github.com/jsalatas/plasma-pstate/wiki/Too-many-messages-in-system-log +Edit a profile name in the text box. + +Select the settings to be included in a profile. + + + +Colors of the navigation bar are inverted in edit mode. The config menus modify the values of the current profile. + + + +Save or discard changes to exit edit mode. + + + +## Contributions +Please feel free to clone, hack, and contribute anything you may find useful, +especially in relation to similar to Dell's Thermal Management Feature that +may be available in other hardware platforms. diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/code/set_prefs.sh b/gr.ictpro.jsalatas.plasma.pstate/contents/code/set_prefs.sh deleted file mode 100755 index e017ac7..0000000 --- a/gr.ictpro.jsalatas.plasma.pstate/contents/code/set_prefs.sh +++ /dev/null @@ -1,322 +0,0 @@ -#!/bin/bash - -INTEL_PSTATE=/sys/devices/system/cpu/intel_pstate -CPU_MIN_PERF=$INTEL_PSTATE/min_perf_pct -CPU_MAX_PERF=$INTEL_PSTATE/max_perf_pct -CPU_TURBO=$INTEL_PSTATE/no_turbo - -GPU=/sys/class/drm/card0 -GPU_MIN_FREQ=$GPU/gt_min_freq_mhz -GPU_MAX_FREQ=$GPU/gt_max_freq_mhz -GPU_MIN_LIMIT=$GPU/gt_RP1_freq_mhz -GPU_MAX_LIMIT=$GPU/gt_RP0_freq_mhz -GPU_BOOST_FREQ=$GPU/gt_boost_freq_mhz -GPU_CUR_FREQ=$GPU/gt_cur_freq_mhz - -LG_LAPTOP_DRIVER=/sys/devices/platform/lg-laptop -LG_FAN_MODE=$LG_LAPTOP_DRIVER/fan_mode -LG_BATTERY_CHARGE_LIMIT=$LG_LAPTOP_DRIVER/battery_care_limit -LG_USB_CHARGE=$LG_LAPTOP_DRIVER/usb_charge - -check_lg_drivers() { - if [ -d $LG_LAPTOP_DRIVER ]; then - return 0 - else - return 1 - fi -} - -check_dell_thermal () { - smbios-thermal-ctl -g > /dev/null 2>&1 - OUT=$? - if [ $OUT -eq 0 ]; then - return 0 - else - return 1 - fi -} - -check_nvidia () { - nvidia-settings -q GpuPowerMizerMode > /dev/null 2>&1 - OUT=$? - if [ $OUT -eq 0 ]; then - return 0 - else - return 1 - fi -} - -set_cpu_min_perf () { - minperf=$1 - if [ -n "$minperf" ] && [ "$minperf" != "0" ]; then - printf '%s\n' "$minperf" > $CPU_MIN_PERF; 2> /dev/null - fi -} - -set_cpu_max_perf () { - maxperf=$1 - if [ -n "$maxperf" ] && [ "$maxperf" != "0" ]; then - printf '%s\n' "$maxperf" > $CPU_MAX_PERF; 2> /dev/null - fi -} - -set_cpu_turbo () { - turbo=$1 - if [ -n "$turbo" ]; then - if [ "$turbo" == "true" ]; then - printf '0\n' > $CPU_TURBO; 2> /dev/null - else - printf '1\n' > $CPU_TURBO; 2> /dev/null - fi - fi -} - -set_gpu_min_freq () { - gpuminfreq=$1 - if [ -n "$gpuminfreq" ] && [ "$gpuminfreq" != "0" ]; then - printf '%s\n' "$gpuminfreq" > $GPU_MIN_FREQ; 2> /dev/null - fi -} - -set_gpu_max_freq () { - gpumaxfreq=$1 - if [ -n "$gpumaxfreq" ] && [ "$gpumaxfreq" != "0" ]; then - printf '%s\n' "$gpumaxfreq" > $GPU_MAX_FREQ; 2> /dev/null - fi -} - -set_gpu_boost_freq () { - gpuboostfreq=$1 - if [ -n "$gpuboostfreq" ] && [ "$gpuboostfreq" != "0" ]; then - printf '%s\n' "$gpuboostfreq" > $GPU_BOOST_FREQ; 2> /dev/null - fi -} - -set_cpu_governor () { - gov=$1 - if [ -n "$gov" ]; then - for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do - printf '%s\n' "$gov" > $cpu; 2> /dev/null - done - fi -} - -set_energy_perf () { - energyperf=$1 - if [ -n "$energyperf" ]; then - if [ -f /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference ]; then - for cpu in /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference; do - printf '%s\n' "$energyperf" > $cpu; 2> /dev/null - done - else - pnum=$(echo $energyperf | sed -r 's/^performance$/0/; - s/^balance_performance$/4/; - s/^(default|normal)$/6/; - s/^balance_power?$/8/; - s/^power(save)?$/15/') - - x86_energy_perf_policy $pnum > /dev/null 2>&1 - fi - fi -} - -set_thermal_mode () { - smbios-thermal-ctl --set-thermal-mode=$1 2> /dev/null -} - -set_lg_battery_charge_limit(){ - enabled=$1 - if [ -n "$enabled" ]; then - if [ "$enabled" == "true" ]; then - printf '80\n' > $LG_BATTERY_CHARGE_LIMIT; 2> /dev/null - else - printf '100\n' > $LG_BATTERY_CHARGE_LIMIT; 2> /dev/null - fi - fi -} - -set_lg_fan_mode() { - enabled=$1 - if [ -n "$enabled" ]; then - if [ "$enabled" == "true" ]; then - printf '0\n' > $LG_FAN_MODE; 2> /dev/null - else - printf '1\n' > $LG_FAN_MODE; 2> /dev/null - fi - fi -} - -set_powermizer () { - nvidia-settings -a "[gpu:0]/GpuPowerMizerMode=$1" 2> /dev/null -} - -set_lg_usb_charge() { - enabled=$1 - if [ -n "$enabled" ]; then - if [ "$enabled" == "true" ]; then - printf '1\n' > $LG_USB_CHARGE; 2> /dev/null - else - printf '0\n' > $LG_USB_CHARGE; 2> /dev/null - fi - fi -} - -read_all () { -cpu_min_perf=`cat $CPU_MIN_PERF` -cpu_max_perf=`cat $CPU_MAX_PERF` -cpu_turbo=`cat $CPU_TURBO` -if [ "$cpu_turbo" == "1" ]; then - cpu_turbo="false" -else - cpu_turbo="true" -fi -gpu_min_freq=`cat $GPU_MIN_FREQ` -gpu_max_freq=`cat $GPU_MAX_FREQ` -gpu_min_limit=`cat $GPU_MIN_LIMIT` -gpu_max_limit=`cat $GPU_MAX_LIMIT` -gpu_boost_freq=`cat $GPU_BOOST_FREQ` -gpu_cur_freq=`cat $GPU_CUR_FREQ` -cpu_governor=`cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor` -energy_perf=`cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference` -if [ -z "$energy_perf" ]; then - energy_perf=`x86_energy_perf_policy -r 2>/dev/null | grep -v 'HWP_' | \ - sed -r 's/://; - s/(0x0000000000000000|EPB 0)/performance/; - s/(0x0000000000000004|EPB 4)/balance_performance/; - s/(0x0000000000000006|EPB 6)/default/; - s/(0x0000000000000008|EPB 8)/balance_power/; - s/(0x000000000000000f|EPB 15)/power/' | \ - awk '{ printf "%s\n", $2; }' | head -n 1` -fi -if check_dell_thermal; then - thermal_mode=`smbios-thermal-ctl -g | grep -C 1 "Current Thermal Modes:" | tail -n 1 | awk '{$1=$1;print}' | sed "s/\t//g" | sed "s/ /-/g" | tr "[A-Z]" "[a-z]" ` -fi - -if check_lg_drivers; then - lg_battery_charge_limit=`cat $LG_BATTERY_CHARGE_LIMIT` - if [ "$lg_battery_charge_limit" == "80" ]; then - lg_battery_charge_limit="true" - else - lg_battery_charge_limit="false" - fi - lg_usb_charge=`cat $LG_USB_CHARGE` - if [ "$lg_usb_charge" == "1" ]; then - lg_usb_charge="true" - else - lg_usb_charge="false" - fi - lg_fan_mode=`cat $LG_FAN_MODE` - if [ "$lg_fan_mode" == "1" ]; then - lg_fan_mode="false" - else - lg_fan_mode="true" - fi -fi - -if check_nvidia; then - powermizer=`nvidia-settings -q GpuPowerMizerMode | grep "Attribute 'GPUPowerMizerMode'" | awk -F "): " '{print $2}' | awk -F "." '{print $1}' ` -fi - -json="{" -json="${json}\"cpu_min_perf\":\"${cpu_min_perf}\"" -json="${json},\"cpu_max_perf\":\"${cpu_max_perf}\"" -json="${json},\"cpu_turbo\":\"${cpu_turbo}\"" -json="${json},\"gpu_min_freq\":\"${gpu_min_freq}\"" -json="${json},\"gpu_max_freq\":\"${gpu_max_freq}\"" -json="${json},\"gpu_min_limit\":\"${gpu_min_limit}\"" -json="${json},\"gpu_max_limit\":\"${gpu_max_limit}\"" -json="${json},\"gpu_boost_freq\":\"${gpu_boost_freq}\"" -json="${json},\"gpu_cur_freq\":\"${gpu_cur_freq}\"" -json="${json},\"cpu_governor\":\"${cpu_governor}\"" -json="${json},\"energy_perf\":\"${energy_perf}\"" -if check_dell_thermal; then - json="${json},\"thermal_mode\":\"${thermal_mode}\"" -fi -if check_lg_drivers; then - json="${json},\"lg_battery_charge_limit\":\"${lg_battery_charge_limit}\"" - json="${json},\"lg_usb_charge\":\"${lg_usb_charge}\"" - json="${json},\"lg_fan_mode\":\"${lg_fan_mode}\"" -fi -if check_nvidia; then - json="${json},\"powermizer\":\"${powermizer}\"" -fi -json="${json}}" -echo $json -} - -case $1 in - "-cpu-min-perf") - set_cpu_min_perf $2 - ;; - - "-cpu-max-perf") - set_cpu_max_perf $2 - ;; - - "-cpu-turbo") - set_cpu_turbo $2 - ;; - - "-gpu-min-freq") - set_gpu_min_freq $2 - ;; - - "-gpu-max-freq") - set_gpu_max_freq $2 - ;; - - "-gpu-boost-freq") - set_gpu_boost_freq $2 - ;; - - "-cpu-governor") - set_cpu_governor $2 - ;; - - "-energy-perf") - set_energy_perf $2 - ;; - - "-thermal-mode") - set_thermal_mode $2 - ;; - - "-lg-battery-charge-limit") - set_lg_battery_charge_limit $2 - ;; - - "-lg-fan-mode") - set_lg_fan_mode $2 - ;; - - "-lg-usb-charge") - set_lg_usb_charge $2 - ;; - - "-powermizer") - set_powermizer $2 - ;; - - "-read-all") - read_all - ;; - - *) - echo "Usage:" - echo "1: set_prefs.sh [ -cpu-min-perf |" - echo " -cpu-max-perf |" - echo " -cpu-turbo |" - echo " -gpu-min-freq |" - echo " -gpu-max-freq |" - echo " -gpu-boost-freq |" - echo " -cpu-governor |" - echo " -energy-perf |" - echo " -thermal-mode |" - echo " -lg-battery-charge-limit |" - echo " -lg-fan-mode |" - echo " -lg-usb-charge |" - echo " -powermizer ] value" - echo "2: set_prefs.sh -read-all" - exit 3 - ;; -esac diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/code/utils.js b/gr.ictpro.jsalatas.plasma.pstate/contents/code/utils.js deleted file mode 100644 index db2d82d..0000000 --- a/gr.ictpro.jsalatas.plasma.pstate/contents/code/utils.js +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2018 John Salatas - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU Library General Public License as - * published by the Free Software Foundation; either version 2 or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details - * - * You should have received a copy of the GNU Library General Public - * License along with this program; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -function to_int(item) { - var val = Math.round(parseFloat(item['value']), 0); - return 0 == val || val ? val + item['unit'] : ''; -} - -function array_to_int(items) { - var res = ''; - var keys = Object.keys(items['value']) - for (var i=0; i< keys.length; i++) { - var item = items['value'][keys[i]] - var val = Math.round(parseFloat(item), 0); - if(0 == val || val) { - if(res) { - res += ' | ' - } - res += val + items['unit']; - } - }; - return res; -} - -function to_time(item) { - var val = Math.round(parseFloat(item['value']), 0); - var hours = Math.floor(val / 3600); - var minutes = Math.floor((val - (hours * 3600)) / 60); - - if (minutes < 10) {minutes = "0"+minutes;} - return hours ? hours+':'+minutes : ''; -} - -function to_bool(item) { - return parseInt(item['value'], 10) == 1; -} - -function to_string(item) { - return item['value']; -} - -var sensors = { - // Informational - 'cpu_cur_load': {'value': undefined, 'unit':'%', 'print': to_int}, - 'cpu_cur_freq': {'value': undefined, 'unit':' MHz', 'print': to_int}, - 'gpu_cur_freq': {'value': undefined, 'unit':' MHz', 'print': to_int}, - 'gpu_min_limit': {'value': undefined, 'unit':'', 'print': to_int}, - 'gpu_max_limit': {'value': undefined, 'unit':'', 'print': to_int}, - 'battery_percentage': {'value': undefined, 'unit': '%', 'print': to_int}, - 'battery_remaining_time': {'value': undefined, 'print': to_time}, - 'package_temp': {'value': undefined, 'unit': ' \u2103', 'print': to_int}, - 'fan_speeds': {'value': {}, 'unit': ' RPM', 'print': array_to_int}, - // Tunable - 'cpu_min_perf': {'value': undefined, 'unit':'%', 'print': to_int}, - 'cpu_max_perf': {'value': undefined, 'unit':'%', 'print': to_int}, - 'cpu_turbo': {'value': undefined, 'unit':'', 'print': to_bool}, - 'gpu_min_freq': {'value': undefined, 'unit':' MHz', 'print': to_int}, - 'gpu_max_freq': {'value': undefined, 'unit':' MHz', 'print': to_int}, - 'gpu_boost_freq': {'value': undefined, 'unit':' MHz', 'print': to_int}, - 'cpu_governor': {'value': undefined, 'unit':'', 'print': to_string}, - 'energy_perf': {'value': undefined, 'unit':'', 'print': to_string}, - 'thermal_mode': {'value': undefined, 'unit':'', 'print': to_string}, - 'lg_battery_charge_limit': {'value': undefined, 'unit':'', 'print': to_bool}, - 'lg_usb_charge': {'value': undefined, 'unit':'', 'print': to_bool}, - 'lg_fan_mode': {'value': undefined, 'unit':'', 'print': to_bool}, - 'powermizer': {'value': undefined, 'unit':'', 'print': to_string}, - -} - -var vendors = { - 'dell': {'provides': ['thermal_mode']}, - 'lg-laptop': {'provides': ['lg_battery_charge_limit', 'lg_usb_charge', 'lg_fan_mode']}, - 'nvidia': {'provides': ['powermizer']} -} - -var model = [ - {'type': 'header', 'text': 'Processor Settings', 'icon': 'd', - 'sensors': ['cpu_cur_load', 'cpu_cur_freq', 'gpu_cur_freq'], - 'items': [ - {'type': 'group', 'text': 'CPU Frequencies', 'items' :[ - {'type': 'slider', 'text': 'Min perf', 'min': 0, 'max': 100, 'sensor': 'cpu_min_perf'}, - {'type': 'slider', 'text': 'Max perf', 'min': 0, 'max': 100, 'sensor': 'cpu_max_perf'}, - {'type': 'switch', 'text': 'Turbo', 'sensor': 'cpu_turbo'} - ]}, - {'type': 'group', 'text': 'GPU Frequencies', 'visible': 'showIntelGPU', 'items' :[ - {'type': 'slider', 'text': 'Min freq', 'min': 'gpu_min_limit', 'max': 'gpu_max_limit', 'sensor': 'gpu_min_freq'}, - {'type': 'slider', 'text': 'Max freq', 'min': 'gpu_min_limit', 'max': 'gpu_max_limit', 'sensor': 'gpu_max_freq'}, - {'type': 'slider', 'text': 'Boost freq', 'min': 'gpu_min_limit', 'max': 'gpu_max_limit', 'sensor': 'gpu_boost_freq'}, - ]}, - {'type': 'radio', 'text': 'CPU Governor', 'sensor': 'cpu_governor', 'items' :[ - {'symbol': 'a', 'text': 'Performance', 'sensor_value': 'performance'}, - {'symbol': 'f', 'text': 'Powersave', 'sensor_value': 'powersave'} - ]} - ] - }, - {'type': 'header', 'text': 'Energy Performance', 'icon': 'h', - 'sensors': ['battery_percentage', 'battery_remaining_time'], - 'items': [ - {'type': 'radio', 'text': '', 'sensor': 'energy_perf', 'items' :[ - {'symbol': 'i', 'text': 'Default', 'sensor_value': 'default'}, - {'symbol': 'a', 'text': 'Performance', 'sensor_value': 'performance'}, - {'symbol': 'k', 'text': 'Balance Performance', 'sensor_value': 'balance_performance'}, - {'symbol': 'l', 'text': 'Balance Power', 'sensor_value': 'balance_power'}, - {'symbol': 'f', 'text': 'Power', 'sensor_value': 'power'} - ]} - ] - }, - {'type': 'header', 'text': 'Thermal Management', 'icon': 'b', - 'vendors': ['dell'], - 'sensors': ['package_temp', 'fan_speeds'], - 'items': [ - {'type': 'radio', 'text': '', 'sensor': 'thermal_mode', 'items' :[ - {'symbol': 'e', 'text': 'Performance', 'sensor_value': 'performance'}, - {'symbol': 'j', 'text': 'Balanced', 'sensor_value': 'balanced'}, - {'symbol': 'g', 'text': 'Cool Bottom', 'sensor_value': 'cool-bottom'}, - {'symbol': 'c', 'text': 'Quiet', 'sensor_value': 'quiet'} - ]} - ] - }, - {'type': 'header', 'text': 'Power Supply Management', 'icon': 'm', - 'vendors': ['lg-laptop'], - 'items': [ - {'type': 'switch', 'text': 'Battery Limit', 'sensor': 'lg_battery_charge_limit'}, - {'type': 'switch', 'text': 'USB Charge', 'sensor': 'lg_usb_charge'} - ] - }, - {'type': 'header', 'text': 'Fan Control', 'icon': 'n', - 'vendors': ['lg-laptop'], - 'items': [ - {'type': 'switch', 'text': 'Silent Mode', 'sensor': 'lg_fan_mode'} - ] - }, - {'type': 'header', 'text': 'Nvidia Settings', 'icon': 'o', - 'vendors': ['nvidia'], - 'items': [ - {'type': 'combobox', 'text': 'PowerMizer', 'sensor': 'powermizer', 'items' :[ - {'text': 'Adaptive', 'sensor_value': '0'}, - {'text': 'Prefer Max Performance', 'sensor_value': '1'}, - {'text': 'Auto', 'sensor_value': '2'} - ]} - ] - } -] - -function get_model() { - return model; -} - -function get_sensors() { - return sensors; -} - -function get_vendors() { - return vendors; -} - diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/ComboBox.qml b/gr.ictpro.jsalatas.plasma.pstate/contents/ui/ComboBox.qml deleted file mode 100644 index b37e1a2..0000000 --- a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/ComboBox.qml +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2018 John Salatas - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU Library General Public License as - * published by the Free Software Foundation; either version 2 or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details - * - * You should have received a copy of the GNU Library General Public - * License along with this program; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import QtQuick 2.3 -import QtQuick.Layouts 1.1 -import org.kde.plasma.components 2.0 as PlasmaComponents -import org.kde.plasma.components 3.0 as PlasmaComponents3 - -RowLayout { - Layout.fillWidth: true - - property var sensor: [] - - property bool acceptingChanges: false - property alias text: combobox_title.text - property var props - spacing: 10 - - onPropsChanged: { - acceptingChanges = false - - combobox.model = props['items'] - text = props['text'] - sensor.push(props['sensor']) - - acceptingChanges = true - } - - Component.onCompleted: { - acceptingChanges = true - sensorsValuesChanged() - } - - Connections { - target: main - onSensorsValuesChanged: { - acceptingChanges = false - if(sensor.length != 0) { - var value = sensors_model[sensor[0]]['value']; - for(var i = 0; i < combobox.model.length; i++) { - if(combobox.model[i].sensor_value == value) { - combobox.currentIndex = i - } - } - } - acceptingChanges = true - } - } - - PlasmaComponents.Label { - Layout.alignment: Qt.AlignVCenter - id: combobox_title - font.pointSize: theme.smallestFont.pointSize - color: theme.textColor - horizontalAlignment: Text.AlignRight - Layout.minimumWidth: units.gridUnit * 4 - } - - PlasmaComponents3.ComboBox { - id: combobox - textRole: "text" - onActivated: { - if(acceptingChanges) { - updateSensor(sensor[0], combobox.model[currentIndex].sensor_value) - } - - } - Layout.minimumWidth: parent.width - } -} diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/CompactRepresentation.qml b/gr.ictpro.jsalatas.plasma.pstate/contents/ui/CompactRepresentation.qml deleted file mode 100644 index 0a7871f..0000000 --- a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/CompactRepresentation.qml +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2018 John Salatas - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU Library General Public License as - * published by the Free Software Foundation; either version 2 or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details - * - * You should have received a copy of the GNU Library General Public - * License along with this program; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import QtQuick 2.2 -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.components 2.0 as PlasmaComponents - -Item { - PlasmaCore.IconItem { - id: customIcon - anchors.fill: parent - visible: !plasmoid.configuration.useDefaultIcon - source: plasmoid.configuration.customIcon - } - - - PlasmaComponents.Label { - anchors.fill: parent - visible: plasmoid.configuration.useDefaultIcon - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - - font.pixelSize: Math.min(parent.height, parent.width) * (inTray ? 1: 0.7) - font.pointSize: -1 - font.family: symbolsFont.name - - text: 'd' - } - - MouseArea { - id: mousearea - anchors.fill: parent - onClicked: { - plasmoid.expanded = !plasmoid.expanded - } - } -} diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/FullRepresentation.qml b/gr.ictpro.jsalatas.plasma.pstate/contents/ui/FullRepresentation.qml deleted file mode 100644 index 1dfb139..0000000 --- a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/FullRepresentation.qml +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2018 John Salatas - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU Library General Public License as - * published by the Free Software Foundation; either version 2 or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details - * - * You should have received a copy of the GNU Library General Public - * License along with this program; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import QtQuick 2.3 -import QtQuick.Layouts 1.1 -import org.kde.plasma.components 2.0 as PlasmaComponents - -import '../code/utils.js' as Utils - -Column { - id: fullRoot - spacing: 0.1 - Layout.minimumWidth: units.gridUnit * 15 - Layout.minimumHeight: units.gridUnit * 20 - - property var model: Utils.get_model() - property var vendors: Utils.get_vendors() - - Component { - id: header - Header { - } - } - - Component.onCompleted: { - if(isReady) { - initialize() - sensorsValuesChanged() - } - } - - Connections { - target: main - onDataSourceReady: { - initialize() - sensorsValuesChanged() - } - } - - Connections { - target: plasmoid.configuration - onShowIntelGPUChanged: { - if(isReady) { - initialize() - } - } - } - - Connections { - target: plasmoid.configuration - onUseSudoForReadingChanged: { - if(isReady) { - initialize() - } - } - } - - function is_present(item_vendors) { - if(item_vendors && item_vendors.length != 0) { - for(var j=0; j< item_vendors.length; j++) { - var vendor = vendors[item_vendors[j]] - for(var k=0; k>>>>>>>>>>>> childrenRect: " + childrenRect.width + " - " + childrenRect.height) - print(">>>>>>>>>>>>> w-h: " + w + " - " + h) - Layout.minimumWidth = w - Layout.minimumHeight = h - Layout.maximumWidth = w - Layout.maximumHeight = h - } - - function removeChildren() { - for(var i = fullRoot.children.length; i > 0 ; i--) { - fullRoot.children[i-1].destroy() - } - } -} diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/Header.qml b/gr.ictpro.jsalatas.plasma.pstate/contents/ui/Header.qml deleted file mode 100644 index 9bfd084..0000000 --- a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/Header.qml +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2018 John Salatas - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU Library General Public License as - * published by the Free Software Foundation; either version 2 or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details - * - * You should have received a copy of the GNU Library General Public - * License along with this program; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import QtQuick 2.6 -import QtQuick.Layouts 1.1 -import org.kde.plasma.components 2.0 as PlasmaComponents - -Row { - id: header - - topPadding: 5 - bottomPadding: 10 - - property alias symbol: icon.text - property alias text: title.text - property var sensors: [] - property var items: [] - - property var props - - Component { - id: group - Group { - Layout.topMargin: 5 - Layout.bottomMargin: 10 - } - } - - Component { - id: radio - Radio { - Layout.topMargin: 5 - Layout.bottomMargin: 10 - } - } - - Component { - id: switchbutton - Switch { - Layout.topMargin: 5 - Layout.bottomMargin: 5 - } - } - - Component { - id: combobox - ComboBox { - Layout.topMargin: 5 - Layout.bottomMargin: 5 - } - } - - Connections { - target: main - onSensorsValuesChanged: { - sensors_label.text = get_sensors_text(sensors); - } - } - - onItemsChanged: { - // parent: controls - for(var i = 0; i < items.length; i++) { - switch (items[i]['type']) { - case 'radio': { - radio.createObject(controls, {'props': items[i]}) - break - } - case 'group': { - group.createObject(controls, {'props': items[i]}) - break - } - case 'switch': { - switchbutton.createObject(controls, {'props': items[i]}) - break - } - case 'combobox': { - combobox.createObject(controls, {'props': items[i]}) - break - } - default: console.log("header: unkonwn type: " + items[i]['type']) - } - } - } - - onPropsChanged: { - symbol = props['icon'] - text = props['text'] - sensors = props['sensors'] - items = props['items'] - } - - GridLayout { - id: grid - - columns: 2 - columnSpacing: 10 - rowSpacing: 0 - - PlasmaComponents.Label { - id: icon - - width: units.gridUnit * 2.2 - Layout.minimumWidth : width - - horizontalAlignment: Text.AlignHCenter - - font.pointSize: theme.smallestFont.pointSize * 2.5 - font.family: symbolsFont.name - color: theme.textColor - } - - PlasmaComponents.Label { - id: title - - font.pointSize: theme.smallestFont.pointSize * 2 - color: theme.textColor - } - - PlasmaComponents.Label { - id: spacer0 - visible: sensors_label.text != 'N/A' - } - - PlasmaComponents.Label { - id: sensors_label - - Layout.bottomMargin: 5 - - font.pointSize: theme.smallestFont.pointSize * 1.25 - color: theme.textColor - opacity: 0.8 - - visible: sensors_label.text != 'N/A' - } - - PlasmaComponents.Label { - id: spacer1 - } - - ColumnLayout { - id: controls - } - } -} diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/Radio.qml b/gr.ictpro.jsalatas.plasma.pstate/contents/ui/Radio.qml deleted file mode 100644 index a42aa6b..0000000 --- a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/Radio.qml +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2018 John Salatas - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU Library General Public License as - * published by the Free Software Foundation; either version 2 or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details - * - * You should have received a copy of the GNU Library General Public - * License along with this program; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import QtQuick 2.3 -import QtQuick.Layouts 1.1 -import org.kde.plasma.components 2.0 as PlasmaComponents - -ColumnLayout { - id: radio - - property alias text: radio_title.text - property var sensor: [] - property var items: [] - - property var props - - objectName: "Radio" - - Component { - id: pushButton - PushButton {} - } - - onPropsChanged: { - text = props['text'] - sensor.push(props['sensor']) - items = props['items'] - } - - onItemsChanged: { - for(var i = 0; i < items.length; i++) { - var props = items[i] - props['sensor'] = sensor - pushButton.createObject(buttons, props); - } - } - - PlasmaComponents.Label { - id: radio_title - font.pointSize: theme.smallestFont.pointSize * 1.25 - color: theme.textColor - visible: text != '' - } - RowLayout { - id: buttons - spacing: -0.5 - Layout.topMargin: radio_title.visible ? 0 : 8 - Layout.rightMargin: 15 - - height: units.gridUnit * 4 - } -} diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/Slider.qml b/gr.ictpro.jsalatas.plasma.pstate/contents/ui/Slider.qml deleted file mode 100644 index 3b7bcd2..0000000 --- a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/Slider.qml +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2018 John Salatas - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU Library General Public License as - * published by the Free Software Foundation; either version 2 or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details - * - * You should have received a copy of the GNU Library General Public - * License along with this program; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import QtQuick 2.3 -import QtQuick.Layouts 1.1 -import org.kde.plasma.components 2.0 as PlasmaComponents - -RowLayout { - property alias min: slider.minimumValue - property alias max: slider.maximumValue - property alias value: slider.value - property bool acceptingChanges: false - property alias pressed: slider.pressed - property bool updating: false - - - property var sensor: [] - property var min_sensor: [] - property var max_sensor: [] - - - property alias text: slider_title.text - property var props - spacing: 10 - - onPropsChanged: { - acceptingChanges = false - text = props['text'] - sensor.push(props['sensor']) - if(isNaN(props['min'])) { - min = 0 - min_sensor.push(props['min']) - } else { - min = props['min'] - min_sensor = [] - } - if(isNaN(props['max'])) { - max = 100 - max_sensor.push(props['max']) - } else { - max = props['max'] - max_sensor = [] - } - acceptingChanges = true - } - - Component.onCompleted: { - acceptingChanges = true - sensorsValuesChanged() - } - - Connections { - target: main - onSensorsValuesChanged: { - if(!pressed) { - acceptingChanges = false - if(sensor.length != 0) { - value = parseInt(sensors_model[sensor[0]]['value'], 10); - slider_value.text = get_sensors_text(sensor); - updating = false - } - if(min_sensor.length != 0) { - min = parseInt(sensors_model[min_sensor[0]]['value'], 10); - } - if(max_sensor.length != 0) { - max = parseInt(sensors_model[max_sensor[0]]['value'], 10); - } - acceptingChanges = true - } - } - } - - PlasmaComponents.Label { - Layout.alignment: Qt.AlignTop - id: slider_title - font.pointSize: theme.smallestFont.pointSize - color: theme.textColor - horizontalAlignment: Text.AlignRight - Layout.minimumWidth: units.gridUnit * 4 - } - - PlasmaComponents.Slider { - Layout.fillWidth: true - Layout.alignment: Qt.AlignBottom - id: slider - stepSize: 1 - onPressedChanged: { - //need to resend here - if(acceptingChanges) { - updateSensor(sensor[0], value) - } - } - onValueChanged: { - if(pressed) { - updating = true - slider_value.text = get_value_text(sensor[0], value) - } else { - if(acceptingChanges) { - updateSensor(sensor[0], value) - } - } - } - } - - PlasmaComponents.Label { - Layout.alignment: Qt.AlignTop - id: slider_value - font.pointSize: theme.smallestFont.pointSize - color: pressed || updating? '#ff0000' : theme.textColor - horizontalAlignment: Text.AlignRight - Layout.minimumWidth: units.gridUnit * 3 - } -} diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/Switch.qml b/gr.ictpro.jsalatas.plasma.pstate/contents/ui/Switch.qml deleted file mode 100644 index b1b57eb..0000000 --- a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/Switch.qml +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2018 John Salatas - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU Library General Public License as - * published by the Free Software Foundation; either version 2 or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details - * - * You should have received a copy of the GNU Library General Public - * License along with this program; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import QtQuick 2.3 -import QtQuick.Layouts 1.1 -import org.kde.plasma.components 2.0 as PlasmaComponents - -RowLayout { - property alias text: checkbox_title.text - property alias checked: checkBox.checked - property var sensor: [] - property bool acceptingChanges: false - - property var props - spacing: 10 - - onPropsChanged: { - acceptingChanges = false - - text = props['text'] - sensor.push(props['sensor']) - acceptingChanges = true - } - - Component.onCompleted: { - acceptingChanges = true - sensorsValuesChanged() - } - - Connections { - target: main - onSensorsValuesChanged: { - acceptingChanges = false - - if(sensor.length != 0) { - checked = sensors_model[sensor[0]]['value'] == 'true'; - } - - acceptingChanges = true - } - } - - PlasmaComponents.Label { - Layout.alignment: Qt.AlignVCenter - id: checkbox_title - font.pointSize: theme.smallestFont.pointSize - color: theme.textColor - horizontalAlignment: Text.AlignRight - Layout.minimumWidth: units.gridUnit * 4 - } - - PlasmaComponents.CheckBox { - id: checkBox - onCheckedChanged: { - if(acceptingChanges) { - updateSensor(sensor[0], checked) - } - } - } -} diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/config/ConfigGeneral.qml b/gr.ictpro.jsalatas.plasma.pstate/contents/ui/config/ConfigGeneral.qml deleted file mode 100644 index 849adf4..0000000 --- a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/config/ConfigGeneral.qml +++ /dev/null @@ -1,47 +0,0 @@ -import QtQuick 2.2 -import QtQuick.Controls 1.3 -import QtQuick.Layouts 1.1 - -Item { - property alias cfg_useDefaultIcon: useDefaultIconCheckbox.checked - property alias cfg_showIntelGPU: showIntelGPUCheckbox.checked - property string cfg_customIcon: plasmoid.configuration.customIcon - property alias cfg_useSudoForReading: useSudoForReadingCheckbox.checked - - GridLayout { - Layout.fillWidth: true - columns: 2 - - CheckBox { - id: useDefaultIconCheckbox - text: i18n('Use Default Icon') - Layout.columnSpan: 2 - } - - Label { - text: i18n("Custom Icon:") - Layout.alignment: Qt.AlignRight - } - - IconPicker { - currentIcon: cfg_customIcon - onIconChanged: cfg_customIcon = iconName - onIconCleared: { - useDefaultIconCheckbox.checked = true - } - enabled: !useDefaultIconCheckbox.checked - } - - CheckBox { - id: showIntelGPUCheckbox - text: i18n('Show Intel GPU') - Layout.columnSpan: 2 - } - - CheckBox { - id: useSudoForReadingCheckbox - text: i18n('Use sudo for reading values') - Layout.columnSpan: 2 - } - } -} diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/main.qml b/gr.ictpro.jsalatas.plasma.pstate/contents/ui/main.qml deleted file mode 100644 index 9573490..0000000 --- a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/main.qml +++ /dev/null @@ -1,303 +0,0 @@ -/* - * Copyright 2018 John Salatas - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU Library General Public License as - * published by the Free Software Foundation; either version 2 or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details - * - * You should have received a copy of the GNU Library General Public - * License along with this program; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import QtQuick 2.3 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 1.0 as QtControls - -import org.kde.plasma.plasmoid 2.0 -import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.extras 2.0 as PlasmaExtras - -import '../code/utils.js' as Utils - -Item { - id: main - - signal sensorsValuesChanged - signal dataSourceReady - signal updateSensor(string name, string value) - - FontLoader { - id: symbolsFont; - source: '../font/plasma-pstate.ttf' - } - - property var monitor_sources: [/cpu\/system\/AverageClock/g, /cpu\/system\/TotalLoad/g, /lmsensors\/.*Package_id_0/g, /lmsensors\/.*fan/g] - property var sensors_model: Utils.get_sensors() - property alias isReady: monitorDS.isReady - property bool inTray: (plasmoid.parent === null || plasmoid.parent.objectName === 'taskItemContainer') - property var readCommand: (plasmoid.configuration.useSudoForReading ? 'sudo ' : '') + '/usr/share/plasma/plasmoids/gr.ictpro.jsalatas.plasma.pstate/contents/code/set_prefs.sh -read-all' - - function sensor_short_name(long_name) { - var parts = long_name.split('/'); - return parts[parts.length - 1]; - } - - Plasmoid.compactRepresentation: CompactRepresentation { } - Plasmoid.fullRepresentation: FullRepresentation { } - - Plasmoid.preferredRepresentation: Plasmoid.compactRepresentation - Plasmoid.switchWidth: units.gridUnit * 15 - Plasmoid.switchHeight: units.gridUnit * 20 - - - Plasmoid.toolTipMainText: '' //i18n('Intel pstate and Thermal Management') - Plasmoid.toolTipSubText: '' - Plasmoid.toolTipTextFormat: Text.RichText - Plasmoid.icon: '' - - Component.onCompleted: { - if (!inTray) { - // not in tray - } - } - - onUpdateSensor: { - print("updating sensor " + name +": " + value) - if(value != sensors_model[name]['value']) { - updater.update(name, value) - } else { - print(" same value") - } - } - - function get_value_text(sensor, value) { - // lol! Is this the bwsat way to do it? - var obj = {'value': value, 'unit': sensors_model[sensor]['unit']} - return sensors_model[sensor]['print'](obj) - } - - function get_sensors_text(sensors) { - var res = ''; - if(sensors != undefined) { - for(var i = 0 ; i < sensors.length; i++) { - var value = sensors_model[sensors[i]]['print'](sensors_model[sensors[i]]); - if(value) { - if(res) { - res += ' | '; - } - res += value; - } - } - } - - return res || 'N/A'; - } - - function monitor_source(src) { - for(var i=0; i < monitor_sources.length; i++) { - if(src.match(monitor_sources[i])) { - return true; - } - } - - return false; - } - - onSensorsValuesChanged: { - updateTooltip(); - } - - PlasmaCore.DataSource { - id: systemmonitorDS - engine: 'systemmonitor' - property var seenSources: [] - - onSourceAdded: { - if(monitor_source(source)) { - if(systemmonitorDS.connectedSources.indexOf(source) == -1) { - systemmonitorDS.connectedSources.push(source); - } - } - } - - onNewData: { - var show = false - if(systemmonitorDS.seenSources.indexOf(sourceName) == -1 && data.value != undefined) { - systemmonitorDS.seenSources.push(sourceName) - } - - var source_short_name = sensor_short_name(sourceName); - - if(source_short_name.startsWith('fan')) { - if (sensors_model['fan_speeds'] != undefined && sensors_model['fan_speeds']['value'] != undefined) { - sensors_model['fan_speeds']['value'][source_short_name] = data.value; - } - } else { - switch (source_short_name) { - case 'AverageClock': { - sensors_model['cpu_cur_freq']['value'] = data.value - break; - } - case 'Package_id_0': { - sensors_model['package_temp']['value'] = data.value - break; - } - case 'TotalLoad': { - sensors_model['cpu_cur_load']['value'] = data.value - break; - } - } - } - - sensorsValuesChanged() - } - - Component.onCompleted: { - systemmonitorDS.connectedSources = []; - systemmonitorDS.connectedSources.length = 0; - - for(var i=0; i< systemmonitorDS.sources.length; i++) { - if(monitor_source(systemmonitorDS.sources[i])) { - if(systemmonitorDS.connectedSources.indexOf(systemmonitorDS.sources[i]) == -1) { - systemmonitorDS.connectedSources.push(systemmonitorDS.sources[i]); - } - } - } - } - interval: 2000 - } - - PlasmaCore.DataSource { - id: powermanagementDS - engine: "powermanagement" - connectedSources: ['Battery'] - onDataChanged: { - if(powermanagementDS.data["Battery"]) { - sensors_model['battery_remaining_time']['value'] = Number(powermanagementDS.data["Battery"]["Remaining msec"]) / 1000; - sensors_model['battery_percentage']['value'] = powermanagementDS.data["Battery"]["Percent"]; - sensorsValuesChanged() - } - } - interval: 2000 - } - - Connections { - target: plasmoid.configuration - onUseSudoForReadingChanged: { - monitorDS.commandSource = readCommand - monitorDS.connectedSources = []; - monitorDS.connectedSources.length = 0; - monitorDS.connectedSources.push(monitorDS.commandSource); - } - } - - PlasmaCore.DataSource { - id: monitorDS - engine: 'executable' - - property bool isReady: false - property string commandSource: readCommand - - onNewData: { - if (data['exit code'] > 0) { - print('monitorDS error: ' + data.stderr) - } else { - var obj = JSON.parse(data.stdout); - var keys = Object.keys(obj); - for(var i=0; i< keys.length; i++) { - sensors_model[keys[i]]['value'] = obj[keys[i]]; - } - if(!isReady) { - dataSourceReady(); - isReady = true; - } - sensorsValuesChanged(); - } - } - Component.onCompleted: { - monitorDS.connectedSources = []; - monitorDS.connectedSources.length = 0; - monitorDS.connectedSources.push(monitorDS.commandSource); - - } - interval: 2000 - } - - PlasmaCore.DataSource { - id: updater - engine: 'executable' - - property string commandSource: 'sudo /usr/share/plasma/plasmoids/gr.ictpro.jsalatas.plasma.pstate/contents/code/set_prefs.sh -' - - onNewData: { - updater.connectedSources = [] - updater.connectedSources.length = 0 - if (data['exit code'] > 0) { - print(" error: " + data.stderr) - } else { - print(" done") - } - } - Component.onCompleted: { - updater.connectedSources = []; - updater.connectedSources.length = 0; - } - function update(parameter, value) { - var command = updater.commandSource + parameter.replace(/_/g, '-') + ' ' + value - print("exec: " + command) - updater.connectedSources.push(command); - - } - } - - function updateTooltip() { - var toolTipSubText =''; - var txt = ''; - - toolTipSubText += '' - - toolTipSubText += '' - toolTipSubText += '' - toolTipSubText += '' - toolTipSubText += '' - - txt = get_sensors_text(['battery_percentage', 'battery_remaining_time']); - if(txt != 'N/A') { - toolTipSubText += '' - toolTipSubText += '' - toolTipSubText += '' - toolTipSubText += '' - } - - txt = get_sensors_text(['package_temp', 'fan_speeds']); - if (txt != 'N/A') { - toolTipSubText += '' - toolTipSubText += '' - toolTipSubText += '' - toolTipSubText += '' - } - toolTipSubText += '
' - toolTipSubText += 'd' - toolTipSubText += '' - toolTipSubText += '  '+get_sensors_text(['cpu_cur_load', 'cpu_cur_freq', 'gpu_cur_freq'])+'' - toolTipSubText += '
' - toolTipSubText += 'h' - toolTipSubText += '' - toolTipSubText += '  '+ txt +'' - toolTipSubText += '
' - toolTipSubText += 'b' - toolTipSubText += '' - toolTipSubText += '  '+ txt +'' - toolTipSubText += '
' - - Plasmoid.toolTipSubText = toolTipSubText - } -} diff --git a/install.sh b/install.sh deleted file mode 100755 index 7a06a82..0000000 --- a/install.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -if [[ $EUID -ne 0 ]]; then - echo "This script must be run as root." 2>&1 - exit 1 -fi - -SUDOERS_FILE="99-plasma-pstate" - -cp -R ${SUDOERS_FILE} /etc/sudoers.d/ -kpackagetool5 -g -t Plasma/Applet -i gr.ictpro.jsalatas.plasma.pstate - -chmod 755 /usr/share/plasma/plasmoids/gr.ictpro.jsalatas.plasma.pstate/contents/code/set_prefs.sh - -chown root:root /etc/sudoers.d/${SUDOERS_FILE} -chmod 400 /etc/sudoers.d/${SUDOERS_FILE} - -# Test for wheel group instead of sudo -# this is the case of arch based distros -wheelgroup=`grep wheel /etc/group | wc -l` -sudogroup=`grep sudo /etc/group | wc -l` -if [ "$wheelgroup" -eq "1" ] && [ "$sudogroup" -eq "0" ]; then - # seems to be safe enough: there is a wheel group and not a sudo group - sed -i 's/sudo/wheel/' /etc/sudoers.d/${SUDOERS_FILE} -fi - -echo -e "\nSetup complete." -exit 0 diff --git a/screenshots/Screenshot_1.png b/screenshots/Screenshot_1.png new file mode 100644 index 0000000..baaf7b9 Binary files /dev/null and b/screenshots/Screenshot_1.png differ diff --git a/screenshots/Screenshot_2.png b/screenshots/Screenshot_2.png new file mode 100644 index 0000000..04c4f2d Binary files /dev/null and b/screenshots/Screenshot_2.png differ diff --git a/screenshots/Screenshot_4.png b/screenshots/Screenshot_4.png new file mode 100644 index 0000000..b610a5e Binary files /dev/null and b/screenshots/Screenshot_4.png differ diff --git a/screenshots/Screenshot_5a.jpg b/screenshots/Screenshot_5a.jpg new file mode 100644 index 0000000..04654a7 Binary files /dev/null and b/screenshots/Screenshot_5a.jpg differ diff --git a/screenshots/Screenshot_5b.jpg b/screenshots/Screenshot_5b.jpg new file mode 100644 index 0000000..a9369fe Binary files /dev/null and b/screenshots/Screenshot_5b.jpg differ diff --git a/screenshots/Screenshot_5c.jpg b/screenshots/Screenshot_5c.jpg new file mode 100644 index 0000000..bf102b2 Binary files /dev/null and b/screenshots/Screenshot_5c.jpg differ diff --git a/screenshots/Screenshot_5d.jpg b/screenshots/Screenshot_5d.jpg new file mode 100644 index 0000000..a3de109 Binary files /dev/null and b/screenshots/Screenshot_5d.jpg differ diff --git a/screenshots/Screenshot_5e.jpg b/screenshots/Screenshot_5e.jpg new file mode 100644 index 0000000..85e6364 Binary files /dev/null and b/screenshots/Screenshot_5e.jpg differ diff --git a/screenshots/Screenshot_5f.jpg b/screenshots/Screenshot_5f.jpg new file mode 100644 index 0000000..750b234 Binary files /dev/null and b/screenshots/Screenshot_5f.jpg differ diff --git a/screenshots/Screenshot_5g.jpg b/screenshots/Screenshot_5g.jpg new file mode 100644 index 0000000..10dda90 Binary files /dev/null and b/screenshots/Screenshot_5g.jpg differ diff --git a/screenshots/screenshot_1.png b/screenshots/screenshot_1.png deleted file mode 100644 index 5b6694f..0000000 Binary files a/screenshots/screenshot_1.png and /dev/null differ diff --git a/screenshots/screenshot_2.png b/screenshots/screenshot_2.png deleted file mode 100644 index 4025af4..0000000 Binary files a/screenshots/screenshot_2.png and /dev/null differ diff --git a/screenshots/screenshot_3.png b/screenshots/screenshot_3.png deleted file mode 100644 index af16f68..0000000 Binary files a/screenshots/screenshot_3.png and /dev/null differ diff --git a/screenshots/screenshot_4.png b/screenshots/screenshot_4.png deleted file mode 100644 index 7f6425c..0000000 Binary files a/screenshots/screenshot_4.png and /dev/null differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..9fbecaf --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,28 @@ +# TODO: adapt "org.kde.plasma" here & elsewhere if needed (see README) +add_definitions(-DTRANSLATION_DOMAIN=\"plasma_applet_gr.ictpro.jsalatas.plasma.pstate\") + +add_library(plasma_applet_pstate MODULE + pstate.cpp + command_result.cpp + set_prefs.cpp + ) + +kcoreaddons_desktop_to_json(plasma_applet_pstate package/metadata.desktop SERVICE_TYPES plasma-applet.desktop) + +target_link_libraries(plasma_applet_pstate + Qt5::Gui + KF5::Plasma + KF5::I18n) + + +install(TARGETS plasma_applet_pstate DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/applets) + +plasma_install_package(package gr.ictpro.jsalatas.plasma.pstate) + +# Make the script executable +install(FILES + package/contents/code/set_prefs.sh + DESTINATION + ${CMAKE_INSTALL_PREFIX}/share/plasma/plasmoids/gr.ictpro.jsalatas.plasma.pstate/contents/code/ + PERMISSIONS + OWNER_EXECUTE OWNER_WRITE OWNER_READ) diff --git a/src/command_result.cpp b/src/command_result.cpp new file mode 100644 index 0000000..ba545e0 --- /dev/null +++ b/src/command_result.cpp @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "command_result.h" + +CommandResult::CommandResult(int exitCode, const QStringList &args, + const QString &stdout, const QString &stderr, + QObject *parent) + : QObject(parent), m_exitCode(exitCode), m_args(args), m_stdout(stdout), + m_stderr(stderr) +{ +} + +int CommandResult::exitCode() const +{ + return m_exitCode; +} + +QStringList CommandResult::args() const +{ + return m_args; +} + +QString CommandResult::stdout() const +{ + return m_stdout; +} + +QString CommandResult::stderr() const +{ + return m_stderr; +} diff --git a/src/command_result.h b/src/command_result.h new file mode 100644 index 0000000..33ddf34 --- /dev/null +++ b/src/command_result.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef COMMAND_RESULT_H +#define COMMAND_RESULT_H + +#include + + +class CommandResult : public QObject { + Q_OBJECT + Q_PROPERTY(int exitCode READ exitCode) + Q_PROPERTY(QStringList args READ args) + Q_PROPERTY(QString stdout READ stdout) + Q_PROPERTY(QString stderr READ stderr) + +private: + int m_exitCode; + QStringList m_args; + QString m_stdout; + QString m_stderr; + +public: + CommandResult(int exitCode, const QStringList &args, + const QString &stdout, const QString &stderr, + QObject *parent = 0); + + int exitCode() const; + QStringList args() const; + QString stdout() const; + QString stderr() const; + +}; + +#endif diff --git a/src/package/contents/code/cpu.sh b/src/package/contents/code/cpu.sh new file mode 100644 index 0000000..ddd3358 --- /dev/null +++ b/src/package/contents/code/cpu.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +CPUFREQ_EPP="${CPUFREQ}/energy_performance_preference" + + +CPUFREQ_SCALING_MIN_FREQ=${CPUFREQ}/scaling_min_freq +CPUFREQ_SCALING_MAX_FREQ=${CPUFREQ}/scaling_max_freq + +check_cpu_governor () { + [ -f /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor ] +} + +read_cpu_governor () { + cpu_governor=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor) +} + +set_cpu_governor () { + gov=$1 + if [ -n "$gov" ]; then + for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do + printf '%s\n' "$gov" > "$cpu" 2> /dev/null + done + fi + read_cpu_governor + json="{" + json="${json}\"cpu_governor\":\"${cpu_governor}\"" + json="${json}}" + echo "$json" +} + +check_energy_perf() { + [ -f "${CPUFREQ_EPP}" ] +} + +read_energy_perf () { + energy_perf=$(cat "${CPUFREQ_EPP}" 2>/dev/null) + if [ -z "$energy_perf" ]; then + energy_perf=$(x86_energy_perf_policy -r 2>/dev/null | grep -v 'HWP_' | \ + sed -r 's/://; + s/(0x0000000000000000|EPB 0)/performance/; + s/(0x0000000000000004|EPB 4)/balance_performance/; + s/(0x0000000000000006|EPB 6)/default/; + s/(0x0000000000000008|EPB 8)/balance_power/; + s/(0x000000000000000f|EPB 15)/power/' | \ + awk '{ printf "%s\n", $2; }' | head -n 1) + fi +} + +set_energy_perf () { + energyperf=$1 + if [ -n "$energyperf" ]; then + if [ -f "${CPUFREQ_EPP}" ]; then + for cpu in ${CPUFREQ_EPP}; do + printf '%s\n' "$energyperf" > "$cpu" 2> /dev/null + done + else + pnum=$(echo "$energyperf" | sed -r 's/^performance$/0/; + s/^balance_performance$/4/; + s/^(default|normal)$/6/; + s/^balance_power?$/8/; + s/^power(save)?$/15/') + + x86_energy_perf_policy "$pnum" > /dev/null 2>&1 + fi + fi + read_energy_perf + json="{" + json="${json}\"energy_perf\":\"${energy_perf}\"" + json="${json}}" + echo "$json" +} + +check_cpufreq_scaling_min_freq() { + [ -n "${CPUFREQ_SCALING_AVAIL_FREQ}" ] && [ -f "$CPUFREQ_SCALING_AVAIL_FREQ" ] +} + +read_cpufreq_scaling_min_freq () { + cpufreq_scaling_min_freq=$(cat "${CPUFREQ_SCALING_MIN_FREQ}") +} + +set_cpufreq_scaling_min_freq() { + printf "%s" "${1}" > "${CPUFREQ_SCALING_MIN_FREQ}" 2> /dev/null + read_cpufreq_scaling_min_freq + json="{" + json="${json}\"cpufreq_scaling_min_freq\":\"${cpufreq_scaling_min_freq}\"" + json="${json}}" + echo "$json" +} + +check_cpufreq_scaling_max_freq() { + [ -n "${CPUFREQ_SCALING_AVAIL_FREQ}" ] && [ -f "$CPUFREQ_SCALING_AVAIL_FREQ" ] +} + +read_cpufreq_scaling_max_freq () { + cpufreq_scaling_max_freq=$(cat "${CPUFREQ_SCALING_MAX_FREQ}") +} + +set_cpufreq_scaling_max_freq() { + printf "%s" "${1}" > "${CPUFREQ_SCALING_MAX_FREQ}" 2> /dev/null + read_cpufreq_scaling_max_freq + json="{" + json="${json}\"cpufreq_scaling_max_freq\":\"${cpufreq_scaling_max_freq}\"" + json="${json}}" + echo "$json" +} diff --git a/src/package/contents/code/datasource.js b/src/package/contents/code/datasource.js new file mode 100644 index 0000000..7526884 --- /dev/null +++ b/src/package/contents/code/datasource.js @@ -0,0 +1,82 @@ + +.import "utils.js" as Utils + +function array_unique(a) { + for(var i=0; i/dev/null | \ + grep "name:dell_smm" | sed 's/\/name.*//') + +check_thermal_mode () { + smbios-thermal-ctl -g > /dev/null 2>&1 + OUT=$? + if [ $OUT -eq 0 ]; then + return 0 + else + return 1 + fi +} + +read_thermal_mode () { + thermal_mode=$(smbios-thermal-ctl -g | grep -C 1 "Current Thermal Modes:" | \ + tail -n 1 | awk '{$1=$1;print}' | sed "s/\t//g" | \ + sed "s/ /-/g" | tr '[:upper:]' '[:lower:]') +} + +set_thermal_mode () { + smbios-thermal-ctl --set-thermal-mode="$1" > /dev/null 2>&1 + read_thermal_mode + json="{" + json="${json}\"thermal_mode\":\"${thermal_mode}\"" + json="${json}}" + echo "$json" +} + +check_dell_fan_mode() { + [ -n "${DELL_SMM_HWMON}" ] && [ -d "${DELL_SMM_HWMON}" ] && \ + [ -f "${DELL_SMM_HWMON}"/pwm1_enable ] +} + +read_dell_fan_mode() { + if [ -f "${DELL_SMM_HWMON}"/pwm1_enable ]; then + dell_fan_mode="0" + fi + export dell_fan_mode +} + +return_dell_fan_mode() { + read_dell_fan_mode + json="{" + json="${json}\"dell_fan_mode\":\"${dell_fan_mode}\"" + json="${json}}" + echo "$json" +} + +set_dell_fan_mode() { + printf "%s" "${1}" > "${DELL_SMM_HWMON}"/pwm1_enable 2> /dev/null + echo "{\"dell_fan_mode\":\"${1}\"}" +} + + +check_dell_fan_pwm() { + [ -n "${DELL_SMM_HWMON}" ] && [ -d "${DELL_SMM_HWMON}" ] && \ + [ -f "${DELL_SMM_HWMON}"/pwm1_enable ] +} + +read_dell_fan_pwm() { + mapfile -t _pwm < <(find "${DELL_SMM_HWMON}"/pwm? -printf "%f\n") + dell_fan_pwm=${_pwm[*]} + export dell_fan_pwm + + if [ -f "${DELL_SMM_HWMON}"/pwm1 ]; then + dell_fan_pwm1=$(cat "${DELL_SMM_HWMON}"/pwm1) + append_json '"dell_fan_pwm\/pwm1":"'"${dell_fan_pwm1}"'"' + fi + + if [ -f "${DELL_SMM_HWMON}"/pwm2 ]; then + dell_fan_pwm2=$(cat "${DELL_SMM_HWMON}"/pwm2) + append_json '"dell_fan_pwm\/pwm2":"'"${dell_fan_pwm2}"'"' + fi + + if [ -f "${DELL_SMM_HWMON}"/pwm3 ]; then + dell_fan_pwm3=$(cat "${DELL_SMM_HWMON}"/pwm3) + append_json '"dell_fan_pwm\/pwm3":"'"${dell_fan_pwm3}"'"' + fi +} + +set_dell_fan_pwm() { + case "${1}" in + pwm1|pwm2|pwm3) + printf "%s" "${2}" > "${DELL_SMM_HWMON}/${1}" 2> /dev/null + ;; + esac + + echo "{}" +} diff --git a/src/package/contents/code/intel.sh b/src/package/contents/code/intel.sh new file mode 100644 index 0000000..892766c --- /dev/null +++ b/src/package/contents/code/intel.sh @@ -0,0 +1,275 @@ +#!/bin/bash + +INTEL_PSTATE=/sys/devices/system/cpu/intel_pstate +CPU_MIN_PERF=$INTEL_PSTATE/min_perf_pct +CPU_MAX_PERF=$INTEL_PSTATE/max_perf_pct + +if [ -f $INTEL_PSTATE/no_turbo ]; then + CPU_TURBO=$INTEL_PSTATE/no_turbo + CPU_TURBO_ON="0" + CPU_TURBO_OFF="1" +fi + +GPU=$(grep -H 0x8086 /sys/class/drm/card?/device/vendor 2>/dev/null | \ + head -n1 | sed 's/\/device\/vendor:.*//') +GPU_MIN_FREQ=$GPU/gt_min_freq_mhz +GPU_MAX_FREQ=$GPU/gt_max_freq_mhz +GPU_MIN_LIMIT=$GPU/gt_RP1_freq_mhz +GPU_MAX_LIMIT=$GPU/gt_RP0_freq_mhz +GPU_BOOST_FREQ=$GPU/gt_boost_freq_mhz +GPU_CUR_FREQ=$GPU/gt_cur_freq_mhz + + + +INTEL_TCC=$(grep -H "TCC Offset" /sys/class/thermal/*/type 2>/dev/null | \ + head -n1 | sed 's/\/type.*//') + +INTEL_RAPL=$(grep -H "package-0" /sys/class/powercap/intel-rapl/*/name 2>/dev/null | \ + head -n1 | sed 's/\/name:package-0//') +INTEL_RAPL_LONG=$(grep -H "long_term" "${INTEL_RAPL}"/constraint_*_name 2>/dev/null | \ + head -n1 | sed 's/\/*_name:long_term//') +INTEL_RAPL_LONG=${INTEL_RAPL_LONG}_power_limit_uw +INTEL_RAPL_SHORT=$(grep -H "short_term" "${INTEL_RAPL}"/constraint_*_name 2>/dev/null | \ + head -n1 | sed 's/\/*_name:short_term//') +INTEL_RAPL_SHORT=${INTEL_RAPL_SHORT}_power_limit_uw + + +check_cpu_min_perf () { + [ -n "$CPU_MIN_PERF" ] && [ -f $CPU_MIN_PERF ] +} + +read_cpu_min_perf () { + cpu_min_perf=$(cat $CPU_MIN_PERF) +} + +set_cpu_min_perf () { + minperf=$1 + if [ -n "$minperf" ] && [ "$minperf" != "0" ]; then + printf '%s\n' "$minperf" > $CPU_MIN_PERF 2> /dev/null + fi + read_cpu_min_perf + json="{" + json="${json}\"cpu_min_perf\":\"${cpu_min_perf}\"" + json="${json}}" + echo "$json" +} + +check_cpu_max_perf () { + [ -n "$CPU_MAX_PERF" ] && [ -f $CPU_MAX_PERF ] +} + +read_cpu_max_perf () { + cpu_max_perf=$(cat $CPU_MAX_PERF) +} + +append_cpu_max_perf() { + check_cpu_max_perf || return 1 + read_cpu_max_perf + append_json "\"cpu_max_perf\":\"${cpu_max_perf}\"" +} + +set_cpu_max_perf () { + maxperf=$1 + if [ -n "$maxperf" ] && [ "$maxperf" != "0" ]; then + printf '%s\n' "$maxperf" > $CPU_MAX_PERF 2> /dev/null + fi + read_cpu_max_perf + json="{" + json="${json}\"cpu_max_perf\":\"${cpu_max_perf}\"" + json="${json}}" + echo "$json" +} + +check_cpu_turbo () { + [ -n "$CPU_TURBO" ] && [ -f $CPU_TURBO ] +} + +read_cpu_turbo () { + cpu_turbo=$(cat $CPU_TURBO) + if [ "$cpu_turbo" = "$CPU_TURBO_OFF" ]; then + cpu_turbo="false" + else + cpu_turbo="true" + fi +} + +append_cpu_turbo() { + check_cpu_turbo || return 1 + read_cpu_turbo + append_json "\"cpu_turbo\":\"${cpu_turbo}\"" +} + +set_cpu_turbo () { + turbo=$1 + if [ -n "$turbo" ]; then + if [ "$turbo" = "true" ]; then + printf "%s" "$CPU_TURBO_ON\n" > $CPU_TURBO 2> /dev/null + else + printf "%s" "$CPU_TURBO_OFF\n" > $CPU_TURBO 2> /dev/null + fi + fi + read_cpu_turbo + json="{" + json="${json}\"cpu_turbo\":\"${cpu_turbo}\"" + json="${json}}" + echo "$json" +} + +check_gpu_min_freq () { + [ -n "$GPU_MIN_FREQ" ] && [ -f "$GPU_MIN_FREQ" ] +} + +read_gpu_min_freq () { + gpu_min_freq=$(cat "$GPU_MIN_FREQ") +} + +append_gpu_min_freq() { + check_gpu_min_freq || return 1 + read_gpu_min_freq + append_json "\"gpu_min_freq\":\"${gpu_min_freq}\"" +} + +set_gpu_min_freq () { + gpuminfreq=$1 + if [ -n "$gpuminfreq" ] && [ "$gpuminfreq" != "0" ]; then + printf '%s\n' "$gpuminfreq" > "$GPU_MIN_FREQ" 2> /dev/null + fi + read_gpu_min_freq + json="{" + json="${json}\"gpu_min_freq\":\"${gpu_min_freq}\"" + json="${json}}" + echo "$json" +} + +check_gpu_max_freq () { + [ -n "$GPU_MAX_FREQ" ] && [ -f "$GPU_MAX_FREQ" ] +} + +read_gpu_max_freq () { + gpu_max_freq=$(cat "$GPU_MAX_FREQ") +} + +set_gpu_max_freq () { + gpumaxfreq=$1 + if [ -n "$gpumaxfreq" ] && [ "$gpumaxfreq" != "0" ]; then + printf '%s\n' "$gpumaxfreq" > "$GPU_MAX_FREQ" 2> /dev/null + fi + read_gpu_max_freq + json="{" + json="${json}\"gpu_max_freq\":\"${gpu_max_freq}\"" + json="${json}}" + echo "$json" +} + +check_gpu_min_limit () { + [ -n "$GPU_MIN_LIMIT" ] && [ -f "$GPU_MIN_LIMIT" ] +} + +read_gpu_min_limit () { + gpu_min_limit=$(cat "${GPU_MIN_LIMIT}") + export gpu_min_limit +} + +check_gpu_max_limit () { + [ -n "$GPU_MAX_LIMIT" ] && [ -f "$GPU_MAX_LIMIT" ] +} + +read_gpu_max_limit () { + gpu_max_limit=$(cat "${GPU_MAX_LIMIT}") + export gpu_max_limit +} + +check_gpu_boost_freq () { + [ -n "$GPU_BOOST_FREQ" ] && [ -f "$GPU_BOOST_FREQ" ] +} + +read_gpu_boost_freq () { + gpu_boost_freq=$(cat "$GPU_BOOST_FREQ") +} + +set_gpu_boost_freq () { + gpuboostfreq=$1 + if [ -n "$gpuboostfreq" ] && [ "$gpuboostfreq" != "0" ]; then + printf '%s\n' "$gpuboostfreq" > "$GPU_BOOST_FREQ" 2> /dev/null + fi + read_gpu_boost_freq + json="{" + json="${json}\"gpu_boost_freq\":\"${gpu_boost_freq}\"" + json="${json}}" + echo "$json" +} + +check_gpu_cur_freq () { + [ -n "$GPU_CUR_FREQ" ] && [ -f "$GPU_CUR_FREQ" ] +} + +read_gpu_cur_freq () { + gpu_cur_freq="$(cat "$GPU_CUR_FREQ")" + export gpu_cur_freq +} + +check_intel_tcc() { + [ -n "${INTEL_TCC}" ] && [ -d "${INTEL_TCC}" ] +} + +check_intel_tcc_cur_state() { + [ -n "${INTEL_TCC}" ] && [ -d "${INTEL_TCC}" ] +} + +read_intel_tcc_cur_state() { + intel_tcc_cur_state=$(cat "${INTEL_TCC}/cur_state") +} + +set_intel_tcc_cur_state() { + printf "%s" "$1" > "${INTEL_TCC}/cur_state" 2> /dev/null + read_intel_tcc_cur_state + json="{" + json="${json}\"intel_tcc_cur_state\":\"${intel_tcc_cur_state}\"" + json="${json}}" + echo "$json" +} + +check_intel_tcc_max_state() { + [ -n "${INTEL_TCC}" ] && [ -d "${INTEL_TCC}" ] +} + +read_intel_tcc_max_state() { + intel_tcc_max_state="$(cat "${INTEL_TCC}"/max_state)" + export intel_tcc_max_state +} + +check_intel_rapl_short() { + [ -n "${INTEL_RAPL}" ] && [ -f "${INTEL_RAPL_SHORT}" ] +} + +read_intel_rapl_short() { + intel_rapl_short=$(( $(cat "${INTEL_RAPL_SHORT}") / 1000000 )) +} + +set_intel_rapl_short() { + printf "%s" $(($1 * 1000000)) > "${INTEL_RAPL_SHORT}" 2> /dev/null + printf "1" > "${INTEL_RAPL}/enabled" 2> /dev/null + read_intel_rapl_short + json="{" + json="${json}\"intel_rapl_short\":\"${intel_rapl_short}\"" + json="${json}}" + echo "$json" +} + +check_intel_rapl_long() { + [ -n "${INTEL_RAPL}" ] && [ -f "${INTEL_RAPL_LONG}" ] +} + +read_intel_rapl_long() { + intel_rapl_long=$(( $(cat "${INTEL_RAPL_LONG}") / 1000000 )) +} + +set_intel_rapl_long() { + printf "%s" "$(($1 * 1000000))" > "${INTEL_RAPL_LONG}" 2> /dev/null + printf "1" > "${INTEL_RAPL}/enabled" 2> /dev/null + read_intel_rapl_long + json="{" + json="${json}\"intel_rapl_long\":\"${intel_rapl_long}\"" + json="${json}}" + echo "$json" +} diff --git a/src/package/contents/code/lg_laptop.sh b/src/package/contents/code/lg_laptop.sh new file mode 100644 index 0000000..4ece9ef --- /dev/null +++ b/src/package/contents/code/lg_laptop.sh @@ -0,0 +1,86 @@ +#!/bin/bash + +LG_LAPTOP_DRIVER=/sys/devices/platform/lg-laptop +LG_FAN_MODE=$LG_LAPTOP_DRIVER/fan_mode +LG_BATTERY_CHARGE_LIMIT=$LG_LAPTOP_DRIVER/battery_care_limit +LG_USB_CHARGE=$LG_LAPTOP_DRIVER/usb_charge + + +check_lg_battery_charge_limit() { + [ -d $LG_LAPTOP_DRIVER ] +} + +read_lg_battery_charge_limit() { + check_lg_battery_charge_limit || return 1 + + lg_battery_charge_limit=$(cat $LG_BATTERY_CHARGE_LIMIT) + if [ "$lg_battery_charge_limit" = "80" ]; then + lg_battery_charge_limit="true" + else + lg_battery_charge_limit="false" + fi +} + +set_lg_battery_charge_limit(){ + enabled=$1 + if [ -n "$enabled" ]; then + if [ "$enabled" = "true" ]; then + printf '80\n' > $LG_BATTERY_CHARGE_LIMIT 2> /dev/null + else + printf '100\n' > $LG_BATTERY_CHARGE_LIMIT 2> /dev/null + fi + fi +} + +check_lg_fan_mode() { + [ -d $LG_LAPTOP_DRIVER ] +} + +read_lg_fan_mode() { + check_lg_fan_mode || return 1 + + lg_usb_charge=$(cat $LG_USB_CHARGE) + if [ "$lg_usb_charge" = "1" ]; then + lg_usb_charge="true" + else + lg_usb_charge="false" + fi +} + +set_lg_fan_mode() { + enabled=$1 + if [ -n "$enabled" ]; then + if [ "$enabled" = "true" ]; then + printf '0\n' > $LG_FAN_MODE 2> /dev/null + else + printf '1\n' > $LG_FAN_MODE 2> /dev/null + fi + fi +} + +check_lg_usb_charge() { + [ -d $LG_LAPTOP_DRIVER ] +} + +read_lg_usb_charge() { + check_lg_usb_charge || return 1 + + lg_usb_charge=$(cat $LG_USB_CHARGE) + if [ "$lg_usb_charge" = "1" ]; then + lg_usb_charge="true" + else + lg_usb_charge="false" + fi +} + +set_lg_usb_charge() { + enabled=$1 + if [ -n "$enabled" ]; then + if [ "$enabled" = "true" ]; then + printf '1\n' > $LG_USB_CHARGE 2> /dev/null + else + printf '0\n' > $LG_USB_CHARGE 2> /dev/null + fi + fi +} + diff --git a/src/package/contents/code/nvidia.sh b/src/package/contents/code/nvidia.sh new file mode 100644 index 0000000..bb0f353 --- /dev/null +++ b/src/package/contents/code/nvidia.sh @@ -0,0 +1,40 @@ +#!/bin/bash + + +NV_PCI_DEV=$(grep -H 0x10de /sys/class/drm/card?/device/vendor 2>/dev/null | \ + head -n1 | sed 's/\/vendor:.*//') +NV_RUNTIME_STATUS=${NV_PCI_DEV}/power/runtime_status + +check_powermizer () { + # nvidia-settings -q GpuPowerMizerMode > /dev/null 2>&1 + # OUT=$? + # if [ $OUT -eq 0 ]; then + # return 0 + # else + # return 1 + # fi + return 1 +} + +read_powermizer() { + powermizer="0" + export powermizer +} + +set_powermizer () { + nvidia-settings -a "[gpu:0]/GpuPowerMizerMode=$1" > /dev/null + echo "{}" +} + +check_nvidia_runtime_status() { + [ -n "${NV_PCI_DEV}" ] && [ -f "${NV_RUNTIME_STATUS}" ] +} + +read_nvidia_runtime_status() { + nvidia_runtime_status=$(cat "${NV_RUNTIME_STATUS}") + export nvidia_runtime_status +} + +set_nvidia_runtime_status() { + echo "{}" +} diff --git a/src/package/contents/code/profile.js b/src/package/contents/code/profile.js new file mode 100644 index 0000000..7dafe78 --- /dev/null +++ b/src/package/contents/code/profile.js @@ -0,0 +1,121 @@ + +.import "utils.js" as Utils + + +function deepCopy(p, c) { + c = c || {}; + for (var i in p) { + if (typeof p[i] === 'object') { + c[i] = (p[i].constructor === Array) ? [] : {}; + deepCopy(p[i], c[i]); + } else { + c[i] = p[i]; + } + } + return c; +} + + +function getValueText(listViewItem, sensorModel, value) { + + var v = (value !== undefined) ? value : sensorModel.value + + var item = listViewItem['item'] + if (item['type'] === 'combobox' || item['type'] === 'radio') { + var subItems = item['items'] + for (var i = 0; subItems && (i < subItems.length); i++) { + if (subItems[i]['sensor_value'] === v) { + return subItems[i]['text'] + } + } + } + + return sensorModel.getValueText(v) +} + + +/* + * Push a sensor item to the array + * + * arr: Array of sensor view model items + * item: An item to push + * header: The items header view model + * group: The items group view model (if it exists) + */ +function pushValidItem(arr, item, header, group) { + if (item["type"] === undefined || item["sensor"] === undefined) { + return + } + + var sensorModel = main.sensorsMgr.getSensor(item.sensor) + + if (Utils.is_enum_sensor(sensorModel)) { + print("pushValidItem " + JSON.stringify(sensorModel)) + for (var i = 0; sensorModel.value && i < sensorModel.value.length; i++) { + var enumItem = Utils.deepCopy(item) + enumItem.sensor = sensorModel.sensor + "/" + sensorModel.value[i] + enumItem.text = enumItem.text + " / " + sensorModel.value[i] + + arr.push({ + "sensor": enumItem.sensor, + "item": enumItem, + "header": header, + "group": group ? group : {}, + "valueText": "-", + "headerText": header.text, + "checked": false + }) + } + + return + } + + arr.push({ + "sensor": item.sensor, + "item": item, + "header": header, + "group": group ? group : {}, + "valueText": "-", + "headerText": header.text, + "checked": false + }) +} + +/* + * Traverse the sensor view model + */ +function stepItems(arr, node, header, group) { + // print("stepItems node = " + JSON.stringify(node)) + var items = [] + + if (Array.isArray(node)) { + items = node + } else if (node.constructor === Object && "items" in node) { + if ("items" in node) { + items = node["items"]; + } + } + + if (node["type"] === "header") { + header = node + } + if (node["type"] === "group") { + group = node + } + + for (var i = 0; items && i < items.length; i++) { + var item = items[i]; + + pushValidItem(arr, item, header, group); + stepItems(arr, item, header, group); + } +} + +/* + * Get a flatted array of all tunable sensor items from the view model + */ +function findSensorItems(model) { + var arr = [] + stepItems(arr, model, undefined, undefined) + return arr +} diff --git a/src/package/contents/code/set_prefs.sh b/src/package/contents/code/set_prefs.sh new file mode 100755 index 0000000..3e4e00d --- /dev/null +++ b/src/package/contents/code/set_prefs.sh @@ -0,0 +1,208 @@ +#!/bin/bash + +export CPUFREQ=/sys/devices/system/cpu/cpu0/cpufreq + +BASEDIR=$(dirname "$0") +# shellcheck disable=SC1091 +source "${BASEDIR}"/cpu.sh +# shellcheck disable=SC1091 +source "${BASEDIR}"/intel.sh +# shellcheck disable=SC1091 +source "${BASEDIR}"/nvidia.sh +# shellcheck disable=SC1091 +source "${BASEDIR}"/lg_laptop.sh +# shellcheck disable=SC1091 +source "${BASEDIR}"/dell_laptop.sh +# shellcheck disable=SC1091 +source "${BASEDIR}"/sysfs_available.sh + +sensors_model=( + # "kernel_version" + "cpu_min_perf" + "cpu_max_perf" + "cpu_turbo" + "gpu_min_freq" + "gpu_max_freq" + "gpu_min_limit" + "gpu_max_limit" + "gpu_boost_freq" + "gpu_cur_freq" + "cpu_governor" + "energy_perf" + "thermal_mode" + "lg_battery_charge_limit" + "lg_fan_mode" + "lg_usb_charge" + "powermizer" + "nvidia_runtime_status" + "intel_tcc_cur_state" + "intel_tcc_max_state" + "intel_rapl_short" + "intel_rapl_long" + "dell_fan_mode" + "dell_fan_pwm" + "cpufreq_scaling_min_freq" + "cpufreq_scaling_max_freq" +) + + +# Append a json key/value to the string variable 'json'. +# e.g. +# append_json '"foo":"bar"' +# 1: A json key/value +append_json() { + json="${json}${json:+,}${1}" +} + +generate_read_sensor_func() { + _cmd="read_sensor() { + case \"\$1\" in + " + for sensor in "${sensors_model[@]}" + do + _subcmd=" + \"${sensor}\") + if check_${sensor}; then + read_${sensor}; + if [ -n \"\${${sensor}}\" ]; then + append_json $(printf \"\\\\\"%s\\\\\":\\\\\"%s\\\\\"\" "${sensor}" \$\{"${sensor}"\}); + fi + fi + ;; + " + _cmd="${_cmd}${_subcmd}" + done + + _cmd="${_cmd} + *) + printf \"Invalid sensor \${1}\n\" 1>&2 + ;; + " + + _cmd="${_cmd} + esac + }" + # echo "${_cmd}" + eval "${_cmd}" +} + +is_valid_sensor() { + _arg=$(printf '%s\n' "${sensors_model[@]}" | grep -P "^${1}\$") + if [ -n "${_arg}" ]; then + echo "${_arg}" + else + echo "" + fi +} + +read_all () { + json="" + + for sensor in "${sensors_model[@]}" + do + read_sensor "${sensor}" + done + + echo "{${json}}" +} + +read_some() { + json="" + + for sensor in "${@}" + do + read_sensor "${sensor}" + done + + echo "{${json}}" +} + +write_sensor() { + _sensor=$(is_valid_sensor "$1") + if [ -n "${_sensor}" ]; then + eval "set_${_sensor}" "${@:2}" + return 0 + fi + + return 1 +} + +list_sensors() { + _out= + for sensor in "${sensors_model[@]}" + do + _out="${_out:+$_out }${sensor}" + done + echo "${_out}" +} + +daemon() { + export DAEMON_MODE=1 + while read -n 1024 -r -a line + do + main "${line[@]}" + done < "/dev/stdin" +} + +print_usage() { + echo "Usage:" + echo "1: set_prefs.sh -write-sensor " + echo "2: set_prefs.sh -read-all" + echo "3: set_prefs.sh -read-available" + echo "4: set_prefs.sh -read-some" + exit 3 +} + +print_json_error() { + if [ ! ${DAEMON_MODE} ]; then + print_usage + else + stdbuf -oL printf "{\"error\":\"invalid arg\"}" 1>&2 + echo "" + fi +} + +main() { + case $1 in + + "-read-all") + read_all + ;; + + "-read-available") + read_available + ;; + + "-read-some") + read_some "${@:2}" + ;; + + "-list-sensors") + list_sensors + ;; + + "-daemon") + daemon "$@" + ;; + + "-exit") + exit 0 + ;; + + "-write-sensor") + shift 1 + if write_sensor "${@}"; then + return 0 + fi + + print_json_error + ;; + + *) + print_json_error + ;; + esac +} + +generate_read_sensor_func +main "$@" diff --git a/src/package/contents/code/sysfs_available.sh b/src/package/contents/code/sysfs_available.sh new file mode 100644 index 0000000..b462a81 --- /dev/null +++ b/src/package/contents/code/sysfs_available.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +CPUFREQ_AVAILABLE_GOVERNORS="${CPUFREQ}/scaling_available_governors" +CPUFREQ_SCALING_AVAIL_FREQ=${CPUFREQ}/scaling_available_frequencies + +check_cpu_available_governors () { + [ -f "${CPUFREQ_AVAILABLE_GOVERNORS}" ] +} + +read_cpu_available_governors () { + cpu_available_governors=$(cat "${CPUFREQ_AVAILABLE_GOVERNORS}") +} + +check_cpu_scaling_available_frequencies() { + [ -n "${CPUFREQ_SCALING_AVAIL_FREQ}" ] && [ -f "$CPUFREQ_SCALING_AVAIL_FREQ" ] +} + +read_cpu_scaling_available_frequencies () { + cpu_scaling_available_frequencies=$(cat "${CPUFREQ_SCALING_AVAIL_FREQ}") +} + +read_available () { + json="" + if check_cpu_available_governors; then + read_cpu_available_governors + append_json "\"cpu_governor\":\"${cpu_available_governors}\"" + fi + if check_cpu_scaling_available_frequencies; then + read_cpu_scaling_available_frequencies + append_json "\"cpu_scaling_available_frequencies\":\"${cpu_scaling_available_frequencies}\"" + fi + echo "{${json}}" +} diff --git a/src/package/contents/code/utils.js b/src/package/contents/code/utils.js new file mode 100644 index 0000000..2c85649 --- /dev/null +++ b/src/package/contents/code/utils.js @@ -0,0 +1,351 @@ +/* + SPDX-FileCopyrightText: 2018 John Salatas + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +function to_int(item) { + var val = Math.round(parseFloat(item['value']), 0); + return 0 == val || val ? val + item['unit'] : ''; +} + +function array_to_int(items) { + var res = ''; + var keys = Object.keys(items['value']) + for (var i=0; i< keys.length; i++) { + var item = items['value'][keys[i]] + var val = Math.round(parseFloat(item), 0); + if(0 == val || val) { + if(res) { + res += ' | ' + } + res += val + items['unit']; + } + }; + return res; +} + +function to_time(item) { + var val = Math.round(parseFloat(item['value']), 0); + var hours = Math.floor(val / 3600); + var minutes = Math.floor((val - (hours * 3600)) / 60); + + if (minutes < 10) {minutes = "0"+minutes;} + return hours ? hours+':'+minutes : ''; +} + +function to_bool(item) { + return (typeof(item['value']) == 'boolean' ? item['value'] : + typeof(item['value']) == 'string' ? item['value'] === 'true' : + typeof(item['value']) == 'number' ? !!parseInt(item['value'], 10) : + false).toString(); +} + +function to_string(item) { + return item['value'] !== undefined ? item['value'].toString() : ""; +} + +function fmt_tcc(item) { + var val = Math.round(parseFloat(item['value']), 0); + if (val === 0) { + return to_int(item); + } + return "-" + to_int(item); +} + +function hz_to_mhz(item) { + var val = Math.round(parseFloat(item['value']), 0) / 1000; + return 0 == val || val ? val + item['unit'] : ''; +} + +var sensors = { + // Informational + 'cpu_cur_load': {'value': undefined, 'unit':'%', 'print': to_int, + 'sensor_type': 'sysmon'}, + 'cpu_cur_freq': {'value': undefined, 'unit':' MHz', 'print': to_int, + 'sensor_type': 'sysmon'}, + 'gpu_cur_freq': {'value': undefined, 'unit':' MHz', 'print': to_int}, + 'gpu_min_limit': {'value': undefined, 'unit':'', 'print': to_int}, + 'gpu_max_limit': {'value': undefined, 'unit':'', 'print': to_int}, + 'battery_percentage': {'value': undefined, 'unit': '%', 'print': to_int, + 'sensor_type': 'sysmon'}, + 'battery_remaining_time': {'value': undefined, 'print': to_time, + 'sensor_type': 'sysmon'}, + 'package_temp': {'value': undefined, 'unit': ' \u2103', 'print': to_int, + 'sensor_type': 'sysmon'}, + 'fan_speeds': {'value': {}, 'unit': ' RPM', 'print': array_to_int, + 'sensor_type': 'sysmon'}, + // Tunable + 'cpu_min_perf': {'value': undefined, 'unit':'%', 'print': to_int}, + 'cpu_max_perf': {'value': undefined, 'unit':'%', 'print': to_int}, + 'cpu_turbo': {'value': undefined, 'unit':'', 'print': to_bool}, + 'gpu_min_freq': {'value': undefined, 'unit':' MHz', 'print': to_int}, + 'gpu_max_freq': {'value': undefined, 'unit':' MHz', 'print': to_int}, + 'gpu_boost_freq': {'value': undefined, 'unit':' MHz', 'print': to_int}, + 'cpu_governor': {'value': undefined, 'unit':'', 'print': to_string}, + 'energy_perf': {'value': undefined, 'unit':'', 'print': to_string}, + 'thermal_mode': {'value': undefined, 'unit':'', 'print': to_string}, + 'lg_battery_charge_limit': {'value': undefined, 'unit':'', 'print': to_bool}, + 'lg_usb_charge': {'value': undefined, 'unit':'', 'print': to_bool}, + 'lg_fan_mode': {'value': undefined, 'unit':'', 'print': to_bool}, + 'powermizer': {'value': undefined, 'unit':'', 'print': to_string, 'rw_mode': 'w'}, + 'nvidia_runtime_status': {'value': undefined, 'unit':'', 'print': to_string, 'rw_mode': 'r'}, + 'intel_tcc_cur_state': {'value': undefined, 'unit':' °C', 'print': fmt_tcc}, + 'intel_tcc_max_state': {'value': undefined, 'unit':' °C', 'print': fmt_tcc}, + 'intel_rapl_short': {'value': undefined, 'unit':' W', 'print': to_int}, + 'intel_rapl_long': {'value': undefined, 'unit':' W', 'print': to_int}, + 'dell_fan_mode': {'value': undefined, 'unit': '', 'print': to_string, 'rw_mode':'w'}, + + 'dell_fan_pwm': {'value': undefined, 'unit': '', 'print': to_string, 'sensor_type':'enum'}, + + 'cpufreq_scaling_min_freq': {'value': undefined, 'unit':' MHz', 'print': hz_to_mhz}, + 'cpufreq_scaling_max_freq': {'value': undefined, 'unit':' MHz', 'print': hz_to_mhz}, +} + +var available_values = { + 'cpu_governor': [], + 'cpu_scaling_available_frequencies': [], +} + +var vendors = { + 'dell': {'provides': ['thermal_mode']}, + 'lg-laptop': {'provides': ['lg_battery_charge_limit', 'lg_usb_charge', 'lg_fan_mode']}, + 'nvidia': {'provides': ['powermizer']} +} + +var model = [ + {'type': 'header', 'id': 'processorSettings', 'text': 'Processor Settings', 'icon': 'd', + 'sensors': ['cpu_cur_load', 'cpu_cur_freq', 'gpu_cur_freq'], + 'items': [ + {'type': 'group', 'text': 'CPU Frequencies', 'items' :[ + {'type': 'slider', 'text': 'Min perf', 'min': 0, 'max': 100, 'sensor': 'cpu_min_perf'}, + {'type': 'slider', 'text': 'Max perf', 'min': 0, 'max': 100, 'sensor': 'cpu_max_perf'}, + {'type': 'switch', 'text': 'Turbo', 'sensor': 'cpu_turbo'} + ]}, + {'type': 'group', 'text': 'GPU Frequencies', 'visible': 'showIntelGPU', 'items' :[ + {'type': 'slider', 'text': 'Min freq', 'min': 'gpu_min_limit', 'max': 'gpu_max_limit', 'sensor': 'gpu_min_freq'}, + {'type': 'slider', 'text': 'Max freq', 'min': 'gpu_min_limit', 'max': 'gpu_max_limit', 'sensor': 'gpu_max_freq'}, + {'type': 'slider', 'text': 'Boost freq', 'min': 'gpu_min_limit', 'max': 'gpu_max_limit', 'sensor': 'gpu_boost_freq'}, + ]}, + {'type': 'combobox', 'text': 'CPU Governor', 'sensor': 'cpu_governor', 'items' :[ + {'symbol': 'a', 'text': 'Performance', 'sensor_value': 'performance'}, + {'symbol': 'f', 'text': 'Powersave', 'sensor_value': 'powersave'}, + {'symbol': 'l', 'text': "Ondemand", 'sensor_value': 'ondemand'}, + {'symbol': 'i', 'text': "Userspace", 'sensor_value': 'userspace'}, + {'symbol': 'k', 'text': "Schedutil", 'sensor_value': 'schedutil'}, + {'symbol': 'f', 'text': "Conservative", 'sensor_value': 'conservative'} + ]}, + {'type': 'group', 'text': 'CPU Frequencies', 'items' :[ + {'type': 'combobox', 'text': 'Min freq', 'sensor': 'cpufreq_scaling_min_freq', + 'items' : undefined, 'available_values': 'cpu_scaling_available_frequencies' + }, + {'type': 'combobox', 'text': 'Max freq', 'sensor': 'cpufreq_scaling_max_freq', + 'items' : undefined, 'available_values': 'cpu_scaling_available_frequencies' + }, + ]} + + ] + }, + {'type': 'header', 'id': 'energyPerf', 'text': 'Energy Performance', 'icon': 'h', + 'sensors': ['battery_percentage', 'battery_remaining_time'], + 'items': [ + {'type': 'radio', 'text': 'Energy Performance Preference', 'sensor': 'energy_perf', 'items' :[ + {'symbol': 'i', 'text': 'Default', 'sensor_value': 'default'}, + {'symbol': 'a', 'text': 'Performance', 'sensor_value': 'performance'}, + {'symbol': 'k', 'text': 'Balance Performance', 'sensor_value': 'balance_performance'}, + {'symbol': 'l', 'text': 'Balance Power', 'sensor_value': 'balance_power'}, + {'symbol': 'f', 'text': 'Power', 'sensor_value': 'power'} + ]}, + {'type': 'group', 'text': "Running Average Power Limit (RAPL)", + 'items' :[ + {'type': 'slider', 'text': "Short Term", 'min': 1, 'max': 200, 'sensor': 'intel_rapl_short' }, + {'type': 'slider', 'text': "Long Term", 'min': 1, 'max': 100, 'sensor': 'intel_rapl_long' }, + ] + } + ] + }, + {'type': 'header', 'id': 'thermalManagement', 'text': 'Thermal Management', 'icon': 'b', + 'vendors': ['dell'], + 'sensors': ['package_temp', 'fan_speeds'], + 'items': [ + {'type': 'combobox', 'text': 'Thermal Mode', 'sensor': 'thermal_mode', 'items' :[ + {'symbol': 'e', 'text': 'Performance', 'sensor_value': 'performance'}, + {'symbol': 'j', 'text': 'Balanced', 'sensor_value': 'balanced'}, + {'symbol': 'g', 'text': 'Cool Bottom', 'sensor_value': 'cool-bottom'}, + {'symbol': 'c', 'text': 'Quiet', 'sensor_value': 'quiet'} + ]}, + {'type': 'group', 'id': 'tcc', 'text': "Thermal Control Circuit", + 'items' :[ + {'type': 'slider', 'text': "Offset", 'min': 0, 'max': 'intel_tcc_max_state', + 'sensor': 'intel_tcc_cur_state' }, + ] + }, + ] + }, + {'type': 'header', 'id': 'powerSupply', 'text': 'Power Supply Management', 'icon': 'm', + 'vendors': ['lg-laptop'], + 'items': [ + {'type': 'switch', 'text': 'Battery Limit', 'sensor': 'lg_battery_charge_limit'}, + {'type': 'switch', 'text': 'USB Charge', 'sensor': 'lg_usb_charge'} + ] + }, + {'type': 'header', 'id': 'fanControl', 'text': 'Fan Control', 'icon': 'n', + 'vendors': ['lg-laptop', 'dell'], + 'sensors': ['package_temp', 'fan_speeds'], + 'items': [ + {'type': 'switch', 'text': 'Silent Mode', 'sensor': 'lg_fan_mode'}, + {'type': 'radio', 'text': 'Dell Fan Mode', 'sensor': 'dell_fan_mode', + 'items' :[ + {'symbol': '◯', 'text': "Auto", 'sensor_value': '2'}, + {'symbol': '●', 'text': "Manual", 'sensor_value': '1'}, + ]}, + + {'type': 'radio', 'text': 'Dell Fan', 'sensor': 'dell_fan_pwm', + 'items' :[ + {'symbol': '◯', 'text': "Off", 'sensor_value': '0'}, + {'symbol': '◐', 'text': "Low", 'sensor_value': '128'}, + {'symbol': '●', 'text': "High", 'sensor_value': '255'}, + ]}, + ] + }, + {'type': 'header', 'id': 'nvidiaSettings', 'text': 'Nvidia Settings', 'icon': 'o', + 'vendors': ['nvidia'], + 'sensors': ['nvidia_runtime_status'], + 'items': [ + {'type': 'combobox', 'text': 'PowerMizer', 'sensor': 'powermizer', 'items' :[ + {'text': 'Adaptive', 'sensor_value': '0'}, + {'text': 'Prefer Max Performance', 'sensor_value': '1'}, + {'text': 'Auto', 'sensor_value': '2'} + ]} + ] + } +] + +function get_model() { + return model; +} + +function get_sensors() { + return sensors; +} + +function get_available_values() { + return available_values; +} + +function get_vendors() { + return vendors; +} + +function is_present(item_vendors) { + if(item_vendors && item_vendors.length !== 0) { + for(var j=0; j< item_vendors.length; j++) { + var vendor = vendors[item_vendors[j]] + for(var k=0; k= 0; i--) { + var sensor = item.sensors[i] + if (sensorNames.includes(sensor) && (main.sensorsMgr.hasKey(sensor) && + !!main.sensorsMgr.getSensor(sensor).value)) + { + return true + } + } + } + + for (var i = item.items.length - 1; i >= 0; i--) { + if (sensor_has_value(item.items[i])) { + return true + } + } + + return false + + // return true + } + + if (item.type === 'group') { + if (!item.items) { + return true + } + for (var i = item.items.length - 1; i >= 0; i--) { + if (sensor_has_value(item.items[i])) { + return true + } + } + return false + } + + if (main.sensorsMgr.hasKey(item.sensor)) { + return main.sensorsMgr.getSensor(item.sensor).value !== undefined + } + + return true +} + +function is_tune_sensor(sensorModel) { + return sensorModel.sensor_type === 'tune'; +} + +function is_info_sensor(sensorModel) { + return sensorModel.sensor_type === 'info'; +} + +function is_sysmon_sensor(sensorModel) { + return sensorModel.sensor_type === 'sysmon'; +} + +function is_enum_sensor(sensorModel) { + if (sensorModel && sensorModel.sensor_type) { + return sensorModel.sensor_type === 'enum'; + } + return false +} + +function get_sensors_text(sensors) { + var res = ''; + if(sensors === undefined) { + return res + } + + for(var i = 0 ; i < sensors.length; i++) { + var value = sensorsMgr.getSensor(sensors[i]).getValueText() + if(value) { + if(res) { + res += ' | '; + } + res += value; + } + } + + return res || 'N/A'; +} + +function deepCopy(p, c) { + c = c || {}; + for (var i in p) { + if (typeof p[i] === 'object') { + c[i] = (p[i].constructor === Array) ? [] : {}; + deepCopy(p[i], c[i]); + } else { + c[i] = p[i]; + } + } + return c; +} diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/config/config.qml b/src/package/contents/config/config.qml similarity index 100% rename from gr.ictpro.jsalatas.plasma.pstate/contents/config/config.qml rename to src/package/contents/config/config.qml diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/config/main.xml b/src/package/contents/config/main.xml similarity index 64% rename from gr.ictpro.jsalatas.plasma.pstate/contents/config/main.xml rename to src/package/contents/config/main.xml index 401291c..5b3f1aa 100644 --- a/gr.ictpro.jsalatas.plasma.pstate/contents/config/main.xml +++ b/src/package/contents/config/main.xml @@ -18,5 +18,17 @@ true + + 2 + + + false + + + 60 + + + false + diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/font/plasma-pstate.ttf b/src/package/contents/font/plasma-pstate.ttf similarity index 100% rename from gr.ictpro.jsalatas.plasma.pstate/contents/font/plasma-pstate.ttf rename to src/package/contents/font/plasma-pstate.ttf diff --git a/src/package/contents/ui/ComboBox.qml b/src/package/contents/ui/ComboBox.qml new file mode 100644 index 0000000..5c8f1b5 --- /dev/null +++ b/src/package/contents/ui/ComboBox.qml @@ -0,0 +1,173 @@ +/* + SPDX-FileCopyrightText: 2018 John Salatas + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.3 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.1 + +RowLayout { + id: comboboxRoot + Layout.fillWidth: true + + property var sensorModel: undefined + + property bool acceptingChanges: false + property alias text: combobox_title.text + property var props + spacing: 0 + + + function onValueChanged() { + acceptingChanges = false + var value = sensorModel.value + for(var i = 0; i < combobox.model.length; i++) { + if(combobox.model[i].sensor_value === value) { + combobox.currentIndex = i + } + } + acceptingChanges = true + } + + onPropsChanged: { + acceptingChanges = false + + sensorModel = main.sensorsMgr.getSensor(props['sensor']) + + var filtered = props['items'] + var values = main.sensorsMgr.availableValues[sensorModel.sensor] + + if (filtered && values) { + // Filter by values in the model view definition + filtered = filtered.filter(item => { + return values.includes(item['sensor_value']) + }) + } + + var values = main.sensorsMgr.availableValues[props['available_values']] + if (!filtered && values) { + // Filter by values read from sysfs + filtered = values.map(val => { + var text = sensorModel.getValueText(val) + return { 'text': text, 'sensor_value': val} + }) + } + + + combobox.model = filtered + text = props['text'] + sensorModel.onValueChanged.connect(onValueChanged) + + acceptingChanges = true + } + + Component.onCompleted: { + onValueChanged() + acceptingChanges = true + } + + Component.onDestruction: { + sensorModel.onValueChanged.disconnect(onValueChanged) + } + + RowLayout { + Label { + id: combobox_title + color: theme.textColor + horizontalAlignment: Text.AlignLeft + // Layout.minimumWidth: units.gridUnit * 4 + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: false + } + RwModeLabel { + sensorModel: comboboxRoot.sensorModel + } + /* Spacer */ + Item { + Layout.fillWidth: true + } + } + + ComboBox { + id: combobox + textRole: "text" + + property bool customHovered: false + + onActivated: { + if(acceptingChanges) { + updateSensor(sensorModel.sensor, + combobox.model[currentIndex].sensor_value) + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + combobox.customHovered = true + } + onExited: { + combobox.customHovered = false + } + onClicked: { + combobox.down ? + combobox.popup.close() : + combobox.popup.open() + } + } + + indicator: Canvas { + id: canvas + x: combobox.width - width - combobox.rightPadding + y: combobox.topPadding + (combobox.availableHeight - height) / 2 + width: units.gridUnit * 0.5 + height: width * 0.75 + contextType: "2d" + + Connections { + target: combobox + function onDownChanged() { canvas.requestPaint(); } + function onCustomHoveredChanged() { canvas.requestPaint(); } + } + + onPaint: { + var ctx = context + if (!context) { + ctx = getContext ("2d"); + } + ctx.reset(); + ctx.moveTo(0, 0); + ctx.lineTo(width, 0); + ctx.lineTo(width / 2, height); + ctx.closePath(); + ctx.fillStyle = combobox.down || combobox.customHovered ? + theme.highlightColor : theme.textColor; + ctx.fill(); + } + } + + contentItem: RowLayout { + Text { + Layout.leftMargin: 0 + Layout.rightMargin: combobox.indicator.width * 2 + Layout.alignment: Qt.AlignRight + + text: combobox.displayText + font: combobox.font + color: combobox.pressed ? theme.textColor : theme.highlightColor + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignRight + } + } + + background: Rectangle { + color: Qt.rgba(0, 0, 0, 0) + border.color: theme.highlightColor + border.width: combobox.down || combobox.customHovered ? 1 : 0 + radius: 2 + } + } +} diff --git a/src/package/contents/ui/CompactRepresentation.qml b/src/package/contents/ui/CompactRepresentation.qml new file mode 100644 index 0000000..40a7689 --- /dev/null +++ b/src/package/contents/ui/CompactRepresentation.qml @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2018 John Salatas + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.2 +import org.kde.plasma.core 2.0 as PlasmaCore +import QtQuick.Controls 2.2 + +Item { + PlasmaCore.IconItem { + id: customIcon + anchors.fill: parent + visible: !plasmoid.configuration.useDefaultIcon + source: plasmoid.configuration.customIcon + } + + + Label { + anchors.fill: parent + visible: plasmoid.configuration.useDefaultIcon + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + font.pixelSize: Math.min(parent.height, parent.width) * (inTray ? 1: 0.7) + font.pointSize: -1 + font.family: symbolsFont.name + + text: 'd' + } + + MouseArea { + id: mousearea + anchors.fill: parent + onClicked: { + plasmoid.expanded = !plasmoid.expanded + } + } +} diff --git a/src/package/contents/ui/DataSourceBackend/AvailableValues.qml b/src/package/contents/ui/DataSourceBackend/AvailableValues.qml new file mode 100644 index 0000000..08e7015 --- /dev/null +++ b/src/package/contents/ui/DataSourceBackend/AvailableValues.qml @@ -0,0 +1,36 @@ +import QtQuick 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore + +import '../../code/datasource.js' as Ds + + +Item { + id: availableValuesDS + property string name: "LocalAvailableValues" + + /* required */ property var set_prefs + + signal handleReadAvailResult(string stdout) + + PlasmaCore.DataSource { + id: datasource + engine: 'executable' + readonly property string commandSource: 'pkexec ' + set_prefs + ' -read-available' + + + onNewData: { + if (connectedSources.length) { + disconnectSource(connectedSources[0]) + } + + if (data['exit code'] > 0) { + print('monitorAvailableDS error: ' + data.stderr) + } else { + handleReadAvailResult(data.stdout) + } + } + Component.onCompleted: { + connectSource(commandSource); + } + } +} diff --git a/src/package/contents/ui/DataSourceBackend/Monitor.qml b/src/package/contents/ui/DataSourceBackend/Monitor.qml new file mode 100644 index 0000000..f096755 --- /dev/null +++ b/src/package/contents/ui/DataSourceBackend/Monitor.qml @@ -0,0 +1,62 @@ +import QtQuick 2.3 +import org.kde.plasma.core 2.0 as PlasmaCore + +import '../../code/utils.js' as Utils +import '../../code/datasource.js' as Ds + + +PlasmaCore.DataSource { + id: monitorDS + engine: 'executable' + + property string name: "LocalMonitor" + + property bool isReady: false + property string commandSource: (plasmoid.configuration.useSudoForReading ? 'pkexec ' : '') + + set_prefs + + (!isReady ? ' -read-all' : + ' -read-some ' + main.sensorsMgr.detectedSensors.join(" ")) + + /* required */ property var set_prefs + + signal handleReadResult(var args, string stdout) + + + onNewData: { + if (data['exit code'] > 0) { + print('monitorDS error: ' + data.stderr) + } else { + var prevIsReady = isReady; + var args = sourceName.split(' ') + args = args.slice(args.indexOf(set_prefs) + 1) + + handleReadResult(args, data.stdout) + + // Switch command from from -read-all to -read-some + if (isReady != prevIsReady) { + disconnectSource(sourceName); + connectSource(commandSource) + } + } + } + + Component.onCompleted: { + connectSource(commandSource); + } + interval: pollingInterval + + function restart() { + stop() + start() + } + + function stop() { + while(connectedSources.length) { + disconnectSource(connectedSources[0]); + } + } + + function start() { + connectSource(commandSource); + } +} diff --git a/src/package/contents/ui/DataSourceBackend/Updater.qml b/src/package/contents/ui/DataSourceBackend/Updater.qml new file mode 100644 index 0000000..c805fde --- /dev/null +++ b/src/package/contents/ui/DataSourceBackend/Updater.qml @@ -0,0 +1,55 @@ +import QtQuick 2.3 +import org.kde.plasma.core 2.0 as PlasmaCore + +import '../../code/utils.js' as Utils +import '../../code/datasource.js' as Ds + + +PlasmaCore.DataSource { + + id: updater + engine: 'executable' + + property string name: "LocalUpdater" + + readonly property string commandSource: 'pkexec ' + set_prefs + + /* required */ property var set_prefs + + signal handleSetValueResult(var arg, string stdout) + + onNewData: { + disconnectSource(sourceName) + + if (data['exit code'] > 0) { + print(" error: " + data.stderr) + notify(sourceName) + } else { + var sensor = sourceName.split(' ')[3] + handleSetValueResult(sensor, data.stdout) + + print(" done") + } + + // monitorDS.start() + } + function update(sensor, args) { + // monitorDS.stop() + + var command = [" -write-sensor", sensor] + command = commandSource + command.concat(args).join(' ') + + print("exec: " + command) + connectSource(command); + + if (sensor === 'powermizer') { + nvidiaPowerMizerDS.update() + } + } + function notify(sourceName) { + var args = sourceName.replace(commandSource, "").split(" ") + var sensorModel = main.sensorsMgr.getSensor(args[1]) + var value = sensorModel.getValueText() + notificationSource.createNotification("Failed to set " + sensor + " to " + value) + } +} diff --git a/src/package/contents/ui/FirstInit.qml b/src/package/contents/ui/FirstInit.qml new file mode 100644 index 0000000..13e294f --- /dev/null +++ b/src/package/contents/ui/FirstInit.qml @@ -0,0 +1,72 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 + + +Item { + id: firstInit + + readonly property bool debug: false + + // Stage One + property bool isScriptReady: false + property bool isMonitorReady: false + + // Stage Two + property bool isViewReady: false + property bool isDataReady: false + + signal beginStageOne + signal beginStageTwo + signal initialized + + function scriptReady() { + isScriptReady = true; + checkStageOne() + } + + function monitorReady() { + isMonitorReady = true; + checkStageOne() + } + + function checkStageOne() { + printdbg("init: stage one: isScriptReady = " + isScriptReady + + ", isMonitorReady = " + isMonitorReady) + + if (isScriptReady === true && isMonitorReady === true) { + printdbg("init: stage one: Done.") + + printdbg("init: stage two: Can begin.") + /* emit */ beginStageTwo() + } + } + + function viewReady() { + isViewReady = true + checkStageTwo() + } + + function dataReady() { + isDataReady = true + checkStageTwo() + } + + function checkStageTwo() { + printdbg("init: stage two: isViewReady = " + isViewReady + + ", isDataReady = " + isDataReady) + if (isViewReady == true && isDataReady === true) { + printdbg("init: stage two: Done."); + printdbg("init: emit initialized()") + /* emit */ initialized() + } + } + + function printdbg(args) { + if (this.debug === true) { + print(args) + } + } +} diff --git a/src/package/contents/ui/FullRepresentation.qml b/src/package/contents/ui/FullRepresentation.qml new file mode 100644 index 0000000..5d7fda1 --- /dev/null +++ b/src/package/contents/ui/FullRepresentation.qml @@ -0,0 +1,64 @@ +/* + SPDX-FileCopyrightText: 2018 John Salatas + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.3 +import QtQuick.Layouts 1.1 + +import '../code/utils.js' as Utils + +ColumnLayout { + id: fullRoot + spacing: 0.1 + clip: true + + Layout.preferredWidth: childrenRect.width + Layout.preferredHeight: childrenRect.height + + property var model: Utils.get_model() + property var vendors: Utils.get_vendors() + + Component { + id: header + Header { + } + } + + Connections { + target: plasmoid.configuration + onShowIntelGPUChanged: { + initialize() + } + } + + Connections { + target: plasmoid.configuration + onUseSudoForReadingChanged: { + initialize() + } + } + + function initialize() { + removeChildren() + + for(var i = 0; i < model.length; i++) { + var item = model[i]; + if(Utils.is_present(item['vendors'])) { + switch (item.type) { + case 'header': { + var obj = header.createObject(fullRoot, {'props': item}) + break + } + default: console.log("unkonwn type: " + item.type) + } + } + } + } + + function removeChildren() { + for(var i = fullRoot.children.length; i > 0 ; i--) { + fullRoot.children[i-1].destroy() + } + } +} diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/Group.qml b/src/package/contents/ui/Group.qml similarity index 52% rename from gr.ictpro.jsalatas.plasma.pstate/contents/ui/Group.qml rename to src/package/contents/ui/Group.qml index 726d133..14dc960 100644 --- a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/Group.qml +++ b/src/package/contents/ui/Group.qml @@ -1,25 +1,14 @@ /* - * Copyright 2018 John Salatas - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU Library General Public License as - * published by the Free Software Foundation; either version 2 or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details - * - * You should have received a copy of the GNU Library General Public - * License along with this program; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ + SPDX-FileCopyrightText: 2018 John Salatas + SPDX-License-Identifier: GPL-2.0-or-later +*/ import QtQuick 2.3 +import QtQuick.Controls 2.2 import QtQuick.Layouts 1.1 -import org.kde.plasma.components 2.0 as PlasmaComponents + +import "./" as Pstate + ColumnLayout { id: group @@ -33,26 +22,36 @@ ColumnLayout { onPropsChanged: { text = props['text'] items = props['items'] - visible = props['visible'] == undefined || eval('plasmoid.configuration.' + props['visible']) + visible = props['visible'] === undefined || + plasmoid.configuration[props['visible']] === true } Component { id: slider - Slider { + Pstate.Slider { Layout.topMargin: 5 Layout.bottomMargin: 5 - Layout.minimumWidth: units.gridUnit * 18 + Layout.minimumWidth: units.gridUnit * 1 } } Component { id: switchbutton - Switch { + Pstate.Switch { Layout.topMargin: 5 Layout.bottomMargin: 5 } } + Component { + id: combobox + Pstate.ComboBox { + Layout.leftMargin: units.gridUnit + Layout.topMargin: 0 + Layout.bottomMargin: 0 + } + } + onItemsChanged: { for(var i = 0; i < items.length; i++) { switch (items[i]['type']) { @@ -64,12 +63,16 @@ ColumnLayout { switchbutton.createObject(group, {'props': items[i]}) break } + case 'combobox': { + combobox.createObject(group, {'props': items[i]}) + break + } default: console.log("header: unkonwn type: " + items[i]['type']) } } } - PlasmaComponents.Label { + Label { id: group_title font.pointSize: theme.smallestFont.pointSize * 1.25 color: theme.textColor diff --git a/src/package/contents/ui/Header.qml b/src/package/contents/ui/Header.qml new file mode 100644 index 0000000..b2d72ce --- /dev/null +++ b/src/package/contents/ui/Header.qml @@ -0,0 +1,227 @@ +/* + SPDX-FileCopyrightText: 2018 John Salatas + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.1 + +import "./" as Pstate +import '../code/utils.js' as Utils + +RowLayout { + id: header + + Layout.topMargin: 5 + Layout.bottomMargin: 5 + + property alias symbol: icon.text + property alias text: title.text + property var items: [] + + property var props + property bool showIcon: true + + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + + Component { + id: group + Pstate.Group { + Layout.topMargin: 5 + Layout.bottomMargin: 5 + } + } + + Component { + id: radio + Pstate.Radio { + Layout.topMargin: 5 + Layout.bottomMargin: 5 + } + } + + Component { + id: switchbutton + Pstate.Switch { + Layout.topMargin: 5 + Layout.bottomMargin: 5 + } + } + + Component { + id: combobox + Pstate.ComboBox { + Layout.topMargin: 5 + Layout.bottomMargin: 5 + } + } + + function createItem(sensorItem) { + switch (sensorItem['type']) { + case 'radio': { + radio.createObject(controls, {'props': sensorItem}) + break + } + case 'group': { + group.createObject(controls, {'props': sensorItem}) + break + } + case 'switch': { + switchbutton.createObject(controls, {'props': sensorItem}) + break + } + case 'combobox': { + combobox.createObject(controls, {'props': sensorItem}) + break + } + default: console.log("header: unkonwn type: " + sensorItem['type']) + } + } + + function createEnumerableSensor(sensorModel, sensorItem) { + var enumRootName = sensorModel.sensor + var enumSensors = sensorModel.value + + for (var j = 0; enumSensors && j < enumSensors.length; j++) { + var item = Utils.deepCopy(sensorItem) + item['sensor'] = enumRootName + "/" + enumSensors[j] + item['text'] = item['text'] + " / " + enumSensors[j] + item['value'] = "" + createItem(item) + } + } + + onItemsChanged: { + // parent: controls + for(var i = 0; i < items.length; i++) { + if(!Utils.sensor_has_value(items[i])) { + continue + } + + var sensorItem = items[i] + + var sensorName = sensorItem['sensor'] + var sensorModel = main.sensorsMgr.getSensor(sensorName) + + if (Utils.is_enum_sensor(sensorModel)) { + createEnumerableSensor(sensorModel, sensorItem) + } else { + createItem(sensorItem) + } + } + } + + onPropsChanged: { + symbol = props['icon'] + text = props['text'] + items = props['items'] + + var keys = props['sensors'] + var prevLabel = undefined + for (var i = 0; keys && i < keys.length ; i++) { + var sensorModel = main.sensorsMgr.getSensor(keys[i]) + var p = { "sensorModel": sensorModel, "prevLabel": prevLabel } + var label = sensorLabelComp.createObject(sensorLabels, p) + prevLabel = label + + sensorModel.emitValueChanged() + } + } + + Component { + id: sensorLabelComp + + Label { + property string valueText: "" + property var sensorModel: undefined + property var prevLabel: undefined + + Layout.bottomMargin: 5 + Layout.rightMargin: 0 + + text: "" + font.pointSize: theme.smallestFont.pointSize + color: Qt.rgba(theme.textColor.r, + theme.textColor.g, + theme.textColor.b, 0.6) + + visible: valueText !== "N/A" + + onSensorModelChanged: { + sensorModel.onValueChanged.connect(onValueChanged) + } + + function onValueChanged() { + valueText = sensorModel.getValueText() + if (!valueText || valueText === "") { + valueText = "N/A" + } + + var spacer = "" + if (prevLabel && prevLabel.visible) { + spacer = "| " + } + + text = spacer + valueText + } + + Component.onDestruction: { + sensorModel.onValueChanged.disconnect(onValueChanged) + } + } + } + + GridLayout { + id: grid + + columns: header.showIcon ? 2 : 1 + columnSpacing: 10 + rowSpacing: 0 + + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + + Label { + id: icon + + width: units.gridUnit * 2.2 + Layout.minimumWidth : width + + horizontalAlignment: Text.AlignHCenter + + font.pointSize: theme.defaultFont.pointSize * 1.5 + font.family: symbolsFont.name + color: theme.textColor + + visible: header.showIcon + } + + Label { + id: title + + font.pointSize: theme.defaultFont.pointSize * 1.25 + color: theme.textColor + } + + Label { + id: spacer0 + visible: header.showIcon + } + + + RowLayout { + id: sensorLabels + Layout.fillWidth: false + Layout.fillHeight: false + } + + Label { + id: spacer1 + visible: header.showIcon + } + + ColumnLayout { + id: controls + } + } +} diff --git a/src/package/contents/ui/Model/Profile.qml b/src/package/contents/ui/Model/Profile.qml new file mode 100644 index 0000000..877c326 --- /dev/null +++ b/src/package/contents/ui/Model/Profile.qml @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 + +import '../../code/profile.js' as ProfileUtils + + +QtObject { + objectName: "Profile" + id: profileObject + + property var name: undefined + + /* + * Data format: + * { sensor1: value, sensor2: value, ... } + */ + property var sensors: ({}) + + function copy(profile) { + profileObject.name = profile.name + profileObject.sensors = ProfileUtils.deepCopy(profile.sensors) + return profileObject + } + + function loadProfile(name, data) { + profileObject.name = name + profileObject.sensors = JSON.parse(data) + } + + function getSensorNames() { + return Object.keys(sensors) + } + + function getSensorValue(name) { + return sensors[name] + } +} diff --git a/src/package/contents/ui/Model/ProfileManager.qml b/src/package/contents/ui/Model/ProfileManager.qml new file mode 100644 index 0000000..525b98a --- /dev/null +++ b/src/package/contents/ui/Model/ProfileManager.qml @@ -0,0 +1,240 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 + + +QtObject { + objectName: "ProfileManager" + id: profileMgr + + property var profileComponent: undefined + + property var profilesMap: ({}) + + property var sensorsModelBackup: ({}) + property var shadowProfilesMap: ({}) + property var deletedProfiles: [] + + property bool editMode: false + + + function loadProfiles() { + var ni = plasmoid.nativeInterface; + var profileList = ni.getProfileList() + + profileComponent = Qt.createComponent("./Profile.qml"); + if (profileComponent.status !== Component.Ready) { + print("Profile.qml component not ready.") + return + } + + for (var i = 0; i < profileList.length; i++) { + var name = profileList[i] + var data = ni.getProfile(name) + + var profile = profileComponent.createObject() + profile.loadProfile(name, data) + + profilesMap[name] = profile + } + + sensorsModelBackup = {} + shadowProfilesMap = {} + deletedProfiles = [] + + // print("Model.ProfileManager loadProfiles " + JSON.stringify(profilesMap)) + } + + function getProfileNames() { + return Object.keys(profilesMap) + } + + function _getProfile(name) { + var keys = Object.keys(profilesMap) + for (var i = 0; i < keys.length; i++) { + if (profilesMap[keys[i]].name === name) { + return profilesMap[keys[i]] + } + } + return undefined + } + + function getProfile(name) { + if (!editMode) { + return _getProfile(name) + } else { + return getShadow(name) + } + } + + function getShadow(name) { + if (!name || name === "") { + return undefined + } + + if (!(name in shadowProfilesMap)) { + var p = _getProfile(name) + var s = profileComponent.createObject() + s = s.copy(p) + + shadowProfilesMap[name] = s + } + + return shadowProfilesMap[name] + } + + function generateName(profileNames) { + var max = 0 + profileNames.forEach(n => { + var re = /^New Profile( [0-9]+)$/g + var match = re.exec(n) + if (!match || match.length == 1) { + max = 2 + } else if (match && match[1]) { + max = Math.max(parseInt(match[1].trim())+1, max) + } + }) + + var newName = "New Profile" + (max>0 ? " "+max : "") + return newName + } + + function createProfile(profileNames) { + if (!editMode) { + return undefined + } + + var profile = profileComponent.createObject() + profile.name = generateName(profileNames) + profile.sensors = {} + + shadowProfilesMap[profile.name] = profile + + return profile + } + + function deleteProfile(name) { + if (!editMode) { + return undefined + } + + if (name in shadowProfilesMap) { + deletedProfiles.push(name) + } + } + + function validateName(newName, profileNames) { + if (!newName || newName === "" || newName.length > 256) { + print("validateName: Name length error") + return false + } + + if (profileNames.includes(newName)) { + print("validateName: Another profile name exists.") + return false + } + + return true + } + + function renameProfile(name, newName) { + if (name in shadowProfilesMap) { + // Insert a key with the new name but leave the + // profile.name property as the original. + // The new name will be applied when the changes are committed. + shadowProfilesMap[newName] = shadowProfilesMap[name] + + deletedProfiles.push(name) + delete shadowProfilesMap[name] + } + } + + function enterEditMode() { + sensorsModelBackup = main.sensorsMgr.backupSensorValues() + editMode = true + } + + function exitEditMode() { + main.sensorsMgr.restoreSensorValues(sensorsModelBackup) + sensorsModelBackup = {} + shadowProfilesMap = {} + deletedProfiles = [] + editMode = false + } + + function applySensorModelValues(profileName, sensorNames) { + var profile = getProfile(profileName); + if (!profile) { + return + } + + profile.sensors = {} + + var keys = main.sensorsMgr.getKeys() + for (var i = 0; i < keys.length; i++) { + if (!sensorNames.includes(keys[i])) { + continue + } + + var sensorModel = main.sensorsMgr.getSensor(keys[i]) + profile.sensors[keys[i]] = sensorModel.value + } + } + + function commitProfileChanges(profileNames) { + var keys = Object.keys(shadowProfilesMap) + for (var i = 0; i < keys.length; i++) { + var shadowProfile = shadowProfilesMap[keys[i]] + var profile = profilesMap[shadowProfile.name] + + if (profile === undefined) { + // added new profile + profilesMap[shadowProfile.name] = shadowProfile + profilesMap[shadowProfile.name].name = keys[i] + } else if (shadowProfile.name !== keys[i]) { + // renamed profile + profile.copy(shadowProfile) + profile.name = keys[i] + profilesMap[profile.name] = profile + } else { + // update existing profile + profilesMap[shadowProfile.name] = profile.copy(shadowProfile) + } + } + + for (var i = 0; i < deletedProfiles.length; i++) { + var key = deletedProfiles[i] + + var keys = Object.keys(shadowProfilesMap) + for (var j = 0; j < keys.length; j++) { + if (shadowProfilesMap[keys[j]].name === key) { + delete shadowProfilesMap[keys[j]] + break + } + } + + var keys = Object.keys(profilesMap) + for (var j = 0; j < keys.length; j++) { + if (profilesMap[keys[j]].name === key) { + delete profilesMap[keys[j]] + break + } + } + } + + + keys = Object.keys(profilesMap) + var ni = plasmoid.nativeInterface; + for (var i = 0; i < keys.length; i++) { + var profileModel = profilesMap[keys[i]] + ni.saveProfile(profileModel.name, + JSON.stringify(profileModel.sensors)) + } + + ni.saveProfileList(profileNames) + + print("commitProfileChanges " + JSON.stringify(profilesMap)) + } +} diff --git a/src/package/contents/ui/Model/Sensor.qml b/src/package/contents/ui/Model/Sensor.qml new file mode 100644 index 0000000..128e2d3 --- /dev/null +++ b/src/package/contents/ui/Model/Sensor.qml @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 + +QtObject { + id: sensorObj + objectName: "Sensor" + + signal valueChangedCustom(QtObject sensor) + + property string sensor: "" + property var value: undefined + property string unit: "" + property var print_func: undefined + property var rw_mode: undefined + property string sensor_type: "" + + function copy(sensor) { + sensorObj.sensor = sensor.sensor + sensorObj.value = sensor.value + sensorObj.unit = sensor.unit + sensorObj.print_func = sensor.print_func + sensorObj.rw_mode = sensor.rw_mode + sensorObj.sensor_type = sensor.sensor_type + return sensorObj + } + + function getValueText(value) { + if (print_func === undefined) { + return value ? value.toString() : sensorObj.value.toString() + } + + var obj = {'value': (value !== undefined ? value : sensorObj.value), 'unit': unit} + return print_func(obj) + } + + onValueChanged: { + /* emit */ valueChangedCustom(sensorObj) + + } + + function emitValueChanged() { + // emit onValueChanged + value = value + } +} diff --git a/src/package/contents/ui/Model/SensorsManager.qml b/src/package/contents/ui/Model/SensorsManager.qml new file mode 100644 index 0000000..b9f8b09 --- /dev/null +++ b/src/package/contents/ui/Model/SensorsManager.qml @@ -0,0 +1,128 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 + +import '../../code/utils.js' as Utils + + +QtObject { + objectName: "SensorsManager" + id: sensorsMgr + + property var sensorsMap: ({}) + + property var availableValues: ({}) + + property var detectedSensors: [] + + property var sensorComponent + + function loadSensors() { + var sensors_model = Utils.get_sensors() + + sensorComponent = Qt.createComponent("./Sensor.qml"); + if (sensorComponent.status !== Component.Ready) { + print("Sensor.qml component not ready.") + return + } + + var keys = Object.keys(sensors_model) + for (var i = 0; i < keys.length; i++) { + var obj = sensors_model[keys[i]] + + var sensor = sensorComponent.createObject(sensorsMgr, { + "sensor": keys[i], + "value": sensors_model[keys[i]]["value"], + "unit": sensors_model[keys[i]]["unit"], + "print_func": sensors_model[keys[i]]["print"], + "rw_mode": sensors_model[keys[i]]["rw_mode"], + "sensor_type": sensors_model[keys[i]]["sensor_type"], + }) + + sensorsMap[keys[i]] = sensor + } + } + + function getKeys() { + return Object.keys(sensorsMap) + } + + function hasKey(name) { + return name in sensorsMap + } + + function getSensor(name) { + if (name in sensorsMap) { + return sensorsMap[name] + } + return undefined + } + + function setSensorValue(name, value) { + var sensor = getSensor(name) + if (sensor) { + sensor.value = value + return true + } + return false + } + + /* + * returns { sensor1: value, sensor2: value, ... } + */ + function backupSensorValues() { + var backup = {} + var keys = Object.keys(sensorsMap) + for (var i = 0; i < keys.length; i++) { + backup[keys[i]] = sensorsMap[keys[i]].value + } + return backup + } + + function restoreSensorValues(backup) { + var keys = Object.keys(backup) + for (var i = 0; i < keys.length; i++) { + sensorsMap[keys[i]].value = backup[keys[i]] + } + } + + function initEnumerableSensor(sensorModel) { + var enumSensors = sensorModel.value + + for (var j = 0; enumSensors && j < enumSensors.length; j++) { + var enumSensorName = sensorModel.sensor + "/" + enumSensors[j] + var enumSensor = sensorComponent.createObject(sensorsMgr) + enumSensor = enumSensor.copy(sensorModel) + enumSensor.sensor = enumSensorName + enumSensor.sensor_type = "" + + sensorsMap[enumSensorName] = enumSensor + } + } + + function initSensorsDetected() { + detectedSensors = [] + var keys = getKeys() + for (var i = 0; i < keys.length; i++) { + var sensorModel = getSensor(keys[i]) + + if (detectedSensors.includes(sensorModel.name)) { + continue + } + if (sensorModel.value === undefined) { + continue + } + if (Utils.is_sysmon_sensor(sensorModel)) + { + continue + } + if (Utils.is_enum_sensor(sensorModel)) + { + initEnumerableSensor(sensorModel) + } + detectedSensors.push(keys[i]) + } + } +} diff --git a/src/package/contents/ui/NativeBackend/Init.qml b/src/package/contents/ui/NativeBackend/Init.qml new file mode 100644 index 0000000..087fba9 --- /dev/null +++ b/src/package/contents/ui/NativeBackend/Init.qml @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.0 + +// +// Send/receive a test command from the set_prefs.sh script process. +// +Item { + id: initId + + readonly property var testCmd: ["-list-sensors"] + + /* require */ property bool hasNativeBackend: false + + signal scriptReady + + signal isReadyChanged(bool isReady) /* place holder */ + signal commandFinished() /* place holder */ + + function init(scriptReadySignal) { + scriptReady.connect(scriptReadySignal) + plasmoid.nativeInterface.startScript(); + } + + Connections { + target: hasNativeBackend ? plasmoid.nativeInterface : initId + onCommandFinished: { + if (data.exitCode === 0 && data.args[0] === testCmd[0]) { + /* emit */ scriptReady() + } + } + onIsReadyChanged/*(bool isReady)*/: { + if (hasNativeBackend && isReady) { + plasmoid.nativeInterface.setPrefs(testCmd) + } + } + } +} diff --git a/src/package/contents/ui/NativeBackend/Monitor.qml b/src/package/contents/ui/NativeBackend/Monitor.qml new file mode 100644 index 0000000..d8e8fae --- /dev/null +++ b/src/package/contents/ui/NativeBackend/Monitor.qml @@ -0,0 +1,89 @@ +import QtQuick 2.3 +import org.kde.plasma.core 2.0 as PlasmaCore + +import '../../code/utils.js' as Utils +import '../../code/datasource.js' as Ds + + +Item { + id: nativeMonitor + property string name: "NativeMonitor" + property var args: ['-read-all'] + + signal handleReadResult(var args, string stdout) + signal handleReadAvailResult(string stdout) + signal handleSetValueResult(var arg, string stdout) + + // + // proxy the inner timer object + // + function start() { timer.start() } + function stop() { timer.stop() } + function restart() { timer.restart() } + + property alias interval: timer.interval + property alias running: timer.running + property alias repeat: timer.repeat + property alias triggeredOnStart: timer.triggeredOnStart + + + function init() { + plasmoid.nativeInterface.setPrefs(['-read-available']) + start() + } + + Timer { + id: timer + onTriggered: { + plasmoid.nativeInterface.setPrefs(args) + } + } + + function dataSourceReady() { + var detectedSensors = main.sensorsMgr.detectedSensors + var readable = Ds.filterReadableSensors(detectedSensors) + args = ['-read-some'].concat(readable) + } + + Connections { + target: plasmoid.nativeInterface + + function debugPrint(data) { + var obj = JSON.parse(data.stdout) + var keys = Object.keys(obj) + for (var i=0; i< keys.length; i++) { + print(keys[i], " = ", obj[keys[i]]) + } + } + + onCommandFinished: { + var exitCode = data.exitCode + var args = data.args + + if (exitCode !== 0) { + print('error: ' + data.stderr) + notificationSource.createNotification('error: ' + data.stderr) + return + } + + if (args.length === 0) { + print('error: Command result with no args.') + return + } + + if (args[0] === '-read-all' || args[0] === '-read-some') { + handleReadResult(args, data.stdout) + return + } + + if (args[0] === '-read-available') { + handleReadAvailResult(data.stdout) + return + } + + if (args[0] === '-write-sensor') { + handleSetValueResult(args[1], data.stdout) + } + } + } +} diff --git a/src/package/contents/ui/NativeBackend/Updater.qml b/src/package/contents/ui/NativeBackend/Updater.qml new file mode 100644 index 0000000..30eb3b3 --- /dev/null +++ b/src/package/contents/ui/NativeBackend/Updater.qml @@ -0,0 +1,18 @@ +import QtQuick 2.3 + +Item { + id: updater + property string name: "NativeUpdater" + + function update(sensor, args) { + var _args = ["-write-sensor", sensor] + _args = _args.concat(args) + + print("exec: " + _args) + plasmoid.nativeInterface.setPrefs(_args) + + if (sensor === 'powermizer') { + nvidiaPowerMizerDS.update() + } + } +} diff --git a/src/package/contents/ui/NvidiaPowerMizerDS.qml b/src/package/contents/ui/NvidiaPowerMizerDS.qml new file mode 100644 index 0000000..7cc6d86 --- /dev/null +++ b/src/package/contents/ui/NvidiaPowerMizerDS.qml @@ -0,0 +1,105 @@ +import QtQuick 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore + +Item { + id: nvidiaPowerMizer + + signal dataSourceReady() + + PlasmaCore.DataSource { + id: datasource + engine: 'executable' + interval: 0 + + property var previous_value: undefined + + property var hasNvGpu: undefined + readonly property int maxIter: 5 + property int iterations: 0 + + property string commandCheck: "grep -H 0x10de /sys/class/drm/card?/device/vendor 2>/dev/null" + + property string commandSource: "nvidia-settings -q GpuPowerMizerMode | " + + "grep \"Attribute 'GPUPowerMizerMode'\" | " + + "awk -F \"): \" '{print $2}' | awk -F \".\" '{print $1}'" + + onNewData: { + if (sourceName === commandCheck) { + check_hardware(sourceName, data) + } else { + check_powermizer(sourceName, data) + } + } + + function check_hardware(sourceName, data) { + disconnectSource(commandCheck) + + hasNvGpu = data.stdout.length !== 0 + if (hasNvGpu) { + print("NvidiaPowerMizerDS: Checking powermizer status.") + connectSource(commandSource); + } else { + print("NvidiaPowerMizerDS: No Nvidia GPU found.") + } + } + + function check_powermizer(sourceName, data) { + iterations = iterations + 1 + + if (data['exit code'] !== 0 || data.stderr.length > 0) { + print('NvidiaPowerMizerDS error: ' + data.stderr) + return + } + + var val = parseInt(data.stdout) + + if (previous_value !== val || iterations >= maxIter) { + disconnectSource(sourceName) + + if (iterations >= maxIter) { + var t = (interval * maxIter) / 1000 + print("NvidiaPowerMizerDS: powermizer value did not change after " + t + + " seconds") + } + + var sensorModel = main.sensorsMgr.getSensor("powermizer") + sensorModel.value = val + + if (previous_value === undefined) { + /* emit */ dataSourceReady() + } + + previous_value = val + } + } + + function has_nvgpu() { + return hasNvGpu + } + + function start() { + connectSource(commandCheck); + } + + function update() { + print("NvidiaPowerMizerDS: Updating status.") + iterations = 0 + interval = 1000 + connectSource(commandSource) + } + } + + Component.onCompleted: { + print("NvidiaPowerMizerDS: Checking for Nvidia GPU.") + datasource.start() + } + + function update() { + if (!datasource.has_nvgpu()) { + print("NvidiaPowerMizerDS: No Nvidia GPU detected.") + return; + } + + datasource.update() + } +} diff --git a/src/package/contents/ui/Profile.qml b/src/package/contents/ui/Profile.qml new file mode 100644 index 0000000..d574b7b --- /dev/null +++ b/src/package/contents/ui/Profile.qml @@ -0,0 +1,777 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.4 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.1 + +import '../code/utils.js' as Utils +import '../code/profile.js' as ProfileUtils + +import './Model' as Model + + +ColumnLayout { + id: profileView + + property alias profileNames: profileComboBox.model + property alias currentIndex: profileComboBox.currentIndex + property alias editMode: profileComboBox.editable + + property bool editMode: false + + property int previousIndex: -1 + + property var originalNames: undefined + property int originalIndex: -1 + + property bool hasMessage: false + + // A flattened list of all tunable sensors (view model items) + property var listModelItems: [] + + signal enterEditMode + signal exitEditMode + + ListModel { + id: sensorListModel + dynamicRoles: true + } + + + function getCheckedSensors() { + var checkedSensors = [] + for (var i = 0; i < sensorListModel.count; i++) { + var listItem = sensorListModel.get(i) + if (listItem.checked) { + checkedSensors.push(listItem.sensor) + } + } + + return checkedSensors + } + + + /* + * Populate the list view + * sensors: Array of sensor names to populate the list. + * undefined to populate all items. + */ + function populateList(sensors) { + sensorListModel.clear() + listModelItems.forEach(item => { + if (sensors && !sensors.includes(item.sensor)) { + return + } + var sensorModel = main.sensorsMgr.getSensor(item.sensor) + if (sensorModel.value === undefined) { + return + } + sensorListModel.append(item) + }) + } + + function applyProfile(profile) { + var keys = profile.getSensorNames() + + for (var i = 0; i < sensorListModel.count; i++) { + var listItem = sensorListModel.get(i) + var isChecked = keys.includes(listItem.sensor) + + sensorListModel.setProperty(i, "checked", isChecked) + + if (isChecked) { + var value = profile.getSensorValue(listItem.sensor) + var sensorModel = main.sensorsMgr.getSensor(listItem.sensor) + + if (editMode) { + sensorModel.value = value + } else { + var valueText = ProfileUtils.getValueText(listItem, + sensorModel, + value) + sensorListModel.setProperty(i, "valueText", valueText) + } + } + } + } + + /* + * Show a sensor profile + * + * profile: Profile object + * editMode: true for edit mode + */ + function showProfile(profile, editMode) { + if (!profile) { + print("showProfile: error: !profile") + console.trace() + return + } + + var keys = editMode ? undefined : profile.getSensorNames() + populateList(keys) + applyProfile(profile) + } + + function onExitEditMode() { + clearMessage(); + } + + function listItemClicked(sensor) { + var index; + var listItem; + + // find list item with sensor name + for (index = sensorListModel.count - 1; index > -1; index--) { + listItem = sensorListModel.get(index) + if (listItem.sensor === sensor) { + break + } + } + if (index === -1) { + return + } + + sensorListModel.setProperty(index, "checked", !listItem.checked) + + var sensorModel = main.sensorsMgr.getSensor(sensor) + sensorModel.emitValueChanged() + } + + function newButtonClicked() { + // Save current shadow profile + if (currentIndex > -1) { + mgr.applySensorModelValues(profileComboBox.model[currentIndex], + getCheckedSensors()) + } + + // Create new instance of a profile object + var profile = mgr.createProfile(profileNames) + var arr = profileComboBox.model + arr.push(profile.name) + profileComboBox.model = arr + + // Show the new profile + currentIndex = arr.length - 1 + profile = mgr.getProfile(profileComboBox.model[currentIndex]) + showProfile(profile, editMode) + + // Ensure the new shadow profile will be saved + previousIndex = currentIndex + } + + function editButtonClicked() { + if (editMode) { + return + } + + originalNames = profileNames.slice() + originalIndex = currentIndex + + /* emit */ enterEditMode() + editMode = true + + if (currentIndex > -1) { + var profile = mgr.getProfile(profileComboBox.model[currentIndex]) + showProfile(profile, editMode) + } + } + + + function cancelButtonClicked() { + if (!editMode) { + return + } + + /* emit */ exitEditMode() + editMode = false + + profileNames = originalNames.slice() + currentIndex = originalIndex + previousIndex = currentIndex + + if (currentIndex > -1) { + var profile = mgr.getProfile(profileComboBox.model[currentIndex]) + showProfile(profile, editMode) + } + } + + + function saveButtonClicked() { + print("Save " + profileComboBox.editText) + if (currentIndex > -1) { + mgr.applySensorModelValues(profileComboBox.model[currentIndex], + getCheckedSensors()) + } + + mgr.commitProfileChanges(profileNames) + + /* emit */ exitEditMode() + editMode = false + + currentIndex = originalIndex + previousIndex = currentIndex + + var profile = mgr.getProfile(profileComboBox.model[currentIndex]) + showProfile(profile, false) + } + + function deleteButtonClicked() { + if (!editMode) { + return + } + if (currentIndex === -1) { + return + } + + mgr.deleteProfile(profileComboBox.model[currentIndex]) + + var idx = currentIndex + var model = profileNames + model.splice(idx, 1) + profileNames = model + currentIndex = idx < model.length ? idx : idx - 1 + + if (currentIndex > -1) { + var name = profileNames[currentIndex] + var profile = mgr.getProfile(name) + showProfile(profile, editMode) + } else { + sensorListModel.clear() + } + + previousIndex = currentIndex + } + + + function applyButtonClicked() { + if (currentIndex === -1) { + return + } + + var profile = mgr.getProfile(profileNames[currentIndex]) + + var keys = Object.keys(profile.sensors) + for (var i = 0; i < keys.length; i++) { + var sensor = keys[i] + var value = profile.sensors[keys[i]] + /* emit */ updateSensor(sensor, value) + } + } + + function comboboxItemClicked(index) { + // Save current values to previous profile + if (editMode && previousIndex > -1) { + mgr.applySensorModelValues(profileComboBox.model[previousIndex], + getCheckedSensors()) + } + + if (index > -1) { + var name = profileNames[index] + var profile = mgr.getProfile(name) + showProfile(profile, editMode) + } + + if (!editMode) { + applyButtonClicked() + } + + previousIndex = index + } + + function upButtonClicked() { + if (currentIndex <= 0) { + return + } + var model = profileNames + var idx = currentIndex + var item = model.splice(idx, 1) + model.splice(idx - 1, 0, item[0]) + profileNames = model + currentIndex = idx - 1 + previousIndex = currentIndex + + showProfile(mgr.getProfile(profileNames[currentIndex]), editMode) + } + + function downButtonClicked() { + if (currentIndex >= profileNames.length - 1) { + return + } + var model = profileNames + var idx = currentIndex + var item = model.splice(idx, 1) + model.splice(idx + 1, 0, item[0]) + profileNames = model + currentIndex = idx + 1 + previousIndex = currentIndex + + showProfile(mgr.getProfile(profileNames[currentIndex]), editMode) + } + + + function showMessage(message) { + hasMessage = true + messageBoxLabel.text = message + } + + function clearMessage() { + hasMessage = false + messageBoxLabel.text = "" + } + + function profileComboBoxAccepted() { + if (!editMode) { + return + } + + var newName = profileComboBox.editText + var oldName = profileNames[currentIndex] + + if (newName === oldName || currentIndex === -1) { + return + } + + if (!mgr.validateName(newName, profileNames)) { + showMessage("Invalid profile name.") + } else { + clearMessage() + + mgr.renameProfile(oldName, newName) + + // refresh the combobox + var arr = profileNames + var idx = currentIndex + arr[currentIndex] = newName + profileNames = arr + currentIndex = idx + } + } + + function initialize() { + profileView.exitEditMode.connect(onExitEditMode) + + profileView.enterEditMode.connect(mgr.enterEditMode) + profileView.exitEditMode.connect(mgr.exitEditMode) + + mgr.loadProfiles() + + listModelItems = ProfileUtils.findSensorItems(Utils.get_model()) + sensorListModel.clear() + + profileComboBox.accepted.connect(profileView.profileComboBoxAccepted) + profileView.profileNames = mgr.getProfileNames() + currentIndex = -1 + previousIndex = currentIndex + + var name = profileNames[0] + var profile = mgr.getProfile(name) + showProfile(profile, editMode) + } + + + Model.ProfileManager { + id: mgr + } + + Component { + id: sensorItemDelegate + + MouseArea { + width: profileView.width + height: childrenRect.height + enabled: profileView.editMode + + onClicked: listItemClicked(sensor) + + // save the sensor here so it's accessible in onDestruction + property var sensorModel + + function onValueChanged(sensorModel) { + var item = undefined + + if (!editMode) { + return + } + + if (sensorListModel) { + for (var i = 0; i < sensorListModel.count; i++) { + var listItem = sensorListModel.get(i) + if (listItem.sensor === sensorModel.sensor) { + item = listItem + break; + } + } + } + + if (!item || !item.sensor) { + print("error: sensorItemDelegate onValueChanged error no " + + "item attached") + console.trace() + return + } + + + if (!item.checked) { + valueText = "-" + return + } + + listItem.valueText = ProfileUtils.getValueText(item, + sensorModel) + } + + Component.onCompleted: { + sensorModel = main.sensorsMgr.getSensor(item.sensor) + sensorModel.onValueChangedCustom.connect(onValueChanged) + } + + Component.onDestruction: { + sensorModel.onValueChangedCustom.disconnect(onValueChanged) + sensorModel = undefined + } + + ColumnLayout { + spacing: 0 + + Rectangle { + /* spacer */ + width: listView.width + height: 1 + color: Qt.rgba(0,0,0,0) + } + + Rectangle { + width: profileView.width + height: childrenRect.height + color: !editMode ? Qt.rgba(0,0,0,0.0) : + (checked ? theme.highlightColor : + Qt.rgba(0,0,0,0.0)) + + RowLayout { + width: profileView.width + + ColumnLayout { + /* sensor group */ + Label { + text: group.text ? group.text : item.text + color: !editMode ? theme.disabledTextColor : + checked ? theme.highlightedTextColor : + theme.textColor + + + font.pointSize: group.text ? + theme.smallestFont.pointSize : + theme.defaultFont.pointSize + Layout.fillWidth: true + Layout.fillHeight: true + } + /* sensor name */ + Label { + text: group.text ? item.text : "" + color: !editMode ? theme.disabledTextColor : + checked ? theme.highlightedTextColor : + theme.textColor + Layout.fillWidth: true + Layout.fillHeight: true + } + } + + ColumnLayout { + + Label { + text: valueText + color: !editMode ? theme.disabledTextColor : + checked ? theme.highlightedTextColor : + theme.textColor + Layout.alignment: Qt.AlignRight + horizontalAlignment: Text.AlignRight + + Layout.fillWidth: true + } + } + } + } + + Rectangle { + /* spacer */ + width: listView.width + height: 1 + color: Qt.rgba(theme.textColor.r, + theme.textColor.g, + theme.textColor.b, 0.6) + } + } + } + } + + // Sensor header + Component { + id: sectionHeading + Rectangle { + width: profileView.width + height: childrenRect.height + color: theme.headerBackgroundColor ? theme.headerBackgroundColor : + theme.backgroundColor + + RowLayout { + width: profileView.width + + Label { + text: section + color: theme.headerTextColor ? theme.headerTextColor : + theme.textColor + font.bold: true + font.pixelSize: units.defaultFont * 2.0 + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + Layout.fillHeight: true + } + } + } + } + + ColumnLayout { + spacing: 0 + ColumnLayout { + RowLayout { + ComboBox { + id: profileComboBox + // model: profileNames /* aliased to root object */ + // editable: /* aliased to root object */ + Layout.fillWidth: true + + onActivated: comboboxItemClicked(index) + onAccepted: profileComboBoxAccepted() + onEditTextChanged: profileComboBoxAccepted() + + contentItem: TextField { + id: textField + + padding: 0 + Layout.leftMargin: 0 + Layout.rightMargin: profileComboBox.indicator.width * 2 + + focus: profileComboBox.editable + visible: profileComboBox.editable + enabled: profileComboBox.editable + + text: profileComboBox.editable ? + profileComboBox.editText : + profileComboBox.displayText + } + } + + ColumnLayout { + spacing: 0 + + ToolButton { + text: /* Up */ "▲" + visible: editMode + enabled: profileNames !== undefined && currentIndex > 0 + contentItem: Text { + text: parent.text + font.pointSize: theme.smallestFont.pointSize + color: enabled ? theme.textColor : theme.disabledTextColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + Layout.preferredWidth: units.largeSpacing + Layout.preferredHeight: units.largeSpacing + + Layout.alignment: Qt.AlignVCenter | Text.AlignHCenter + ToolTip.text: "Move profile up" + ToolTip.delay: 250 + ToolTip.visible: hovered + onClicked: upButtonClicked() + } + ToolButton { + text: /* Down */ "▼" + visible: editMode + enabled: profileNames !== undefined && profileNames.length && + -1 < currentIndex && currentIndex < profileNames.length - 1 + contentItem: Text { + text: parent.text + font.pointSize: theme.smallestFont.pointSize + color: enabled ? theme.textColor : theme.disabledTextColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + Layout.preferredWidth: units.largeSpacing + Layout.preferredHeight: units.largeSpacing + Layout.alignment: Qt.AlignVCenter | Text.AlignHCenter + ToolTip.text: "Move profile down" + ToolTip.delay: 250 + ToolTip.visible: hovered + onClicked: downButtonClicked() + } + } + + ToolButton { + id: editButton + text: /* Edit */ "🖉" + visible: !editMode + contentItem: Text { + text: parent.text + color: theme.textColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + TextMetrics { + id: textMetrics + font: editButton.contentItem.font + text: editButton.text + } + Component.onCompleted: { + // Binding the property causes a segfault on exit. + // Workaround by assigning the value here. + Layout.maximumWidth = (2 * textMetrics.width) + + (2 * units.smallSpacing) + } + Layout.alignment: Qt.AlignVCenter | Text.AlignHCenter + ToolTip.text: "Edit profiles" + ToolTip.delay: 250 + ToolTip.visible: hovered + onClicked: editButtonClicked() + } + + ToolButton { + id: newButton + text: /* New */ "🗎" + visible: editMode + contentItem: Text { + text: parent.text + color: theme.textColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + TextMetrics { + id: newTextMetrics + font: newButton.contentItem.font + text: newButton.text + } + Component.onCompleted: { + Layout.maximumWidth = (2 * newTextMetrics.width) + + (2 * units.smallSpacing) + } + Layout.alignment: Qt.AlignVCenter | Text.AlignHCenter + ToolTip.text: "New profile" + ToolTip.delay: 250 + ToolTip.visible: hovered + onClicked: newButtonClicked() + } + ToolButton { + id: delButton + text: /* Delete */ "🗑" + visible: editMode + contentItem: Text { + text: parent.text + color: theme.textColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + TextMetrics { + id: delTextMetrics + font: delButton.contentItem.font + text: delButton.text + } + Component.onCompleted: { + Layout.maximumWidth = (2 * delTextMetrics.width) + + (2 * units.smallSpacing) + } + Layout.alignment: Qt.AlignVCenter | Text.AlignHCenter + ToolTip.text: "Delete profile" + ToolTip.delay: 250 + ToolTip.visible: hovered + onClicked: deleteButtonClicked() + } + ToolButton { + id: cancelButton + text: /* Cancel */ "🗙" + visible: editMode + contentItem: Text { + text: parent.text + color: theme.textColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + TextMetrics { + id: cancelTextMetrics + font: cancelButton.contentItem.font + text: cancelButton.text + } + Component.onCompleted: { + Layout.maximumWidth = (2 * cancelTextMetrics.width) + + (2 * units.smallSpacing) + } + Layout.alignment: Qt.AlignVCenter | Text.AlignHCenter + ToolTip.text: "Discard changes" + ToolTip.delay: 250 + ToolTip.visible: hovered + onClicked: cancelButtonClicked() + } + ToolButton { + id: saveButton + text: /* Save */ "🗸" + enabled: editMode + visible: editMode + contentItem: Text { + text: parent.text + color: theme.textColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + TextMetrics { + id: saveTextMetrics + font: saveButton.contentItem.font + text: saveButton.text + } + Component.onCompleted: { + Layout.maximumWidth = (2 * saveTextMetrics.width) + + (2 * units.smallSpacing) + } + Layout.alignment: Qt.AlignVCenter | Text.AlignHCenter + ToolTip.text: "Save changes" + ToolTip.delay: 250 + ToolTip.visible: hovered + + onClicked: saveButtonClicked() + + } + } + } + + Rectangle { + id: messageBox + Layout.preferredWidth: profileView.width + Layout.preferredHeight: childrenRect.height + visible: hasMessage + color: theme.negativeTextColor ? theme.negativeTextColor : "red" + + Label { + id: messageBoxLabel + text: "" + color: "white" + Layout.fillWidth: true + } + } + + ListView { + id: listView + clip: true + Layout.fillWidth: true + Layout.fillHeight: true + + delegate: sensorItemDelegate + model: sensorListModel + + section.property: "headerText" + section.criteria: ViewSection.FullString + section.delegate: sectionHeading + } + } +} diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/PushButton.qml b/src/package/contents/ui/PushButton.qml similarity index 51% rename from gr.ictpro.jsalatas.plasma.pstate/contents/ui/PushButton.qml rename to src/package/contents/ui/PushButton.qml index aeda2ba..1b3070d 100644 --- a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/PushButton.qml +++ b/src/package/contents/ui/PushButton.qml @@ -1,28 +1,15 @@ /* - * Copyright 2018 John Salatas - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU Library General Public License as - * published by the Free Software Foundation; either version 2 or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details - * - * You should have received a copy of the GNU Library General Public - * License along with this program; if not, write to the - * Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ + SPDX-FileCopyrightText: 2018 John Salatas + SPDX-License-Identifier: GPL-2.0-or-later +*/ import QtQuick 2.3 -import org.kde.plasma.components 2.0 as PlasmaComponents +import QtQuick.Controls 2.2 + Item { - width: units.gridUnit * 4.2 - height: units.gridUnit * 4 + width: units.gridUnit * 3.9 + height: units.gridUnit * 3.8 objectName: "PushButton" @@ -31,25 +18,36 @@ Item { property string sensor_value property bool active: false property bool updating: false - property var sensor: [] + property var sensorModel: undefined property bool acceptingChanges: false - property color borderColor: updating ? '#ff0000' : (active ? Qt.rgba(theme.highlightColor.r, theme.highlightColor.g, theme.highlightColor.b, 0.6) : - Qt.rgba(theme.textColor.r, theme.textColor.g, theme.textColor.b, 0.4)) + property color borderColor: updating ? '#ff0000' : + (active ? Qt.rgba(theme.highlightColor.r, + theme.highlightColor.g, + theme.highlightColor.b, 0.6) : + Qt.rgba(theme.textColor.r, + theme.textColor.g, + theme.textColor.b, 0.4)) property color buttonColor: active ? theme.highlightColor : theme.textColor + function onValueChanged() { + acceptingChanges = false + updating = false + active = sensorModel.value === sensor_value + acceptingChanges = true + } + + onSensorModelChanged: { + sensorModel.onValueChanged.connect(onValueChanged) + acceptingChanges = true + } + Component.onCompleted: { - sensorsValuesChanged() + onValueChanged() } - Connections { - target: main - onSensorsValuesChanged: { - acceptingChanges = false - updating = false - active = sensors_model[sensor[0]]['value'] == sensor_value - acceptingChanges = true - } + Component.onDestruction: { + sensorModel.onValueChanged.disconnect(onValueChanged) } Rectangle { @@ -65,12 +63,12 @@ Item { onClicked: { if(acceptingChanges) { updating = true - updateSensor(sensor[0], sensor_value) + updateSensor(sensorModel.sensor, sensor_value) } } } - PlasmaComponents.Label { + Label { id: icon anchors { margins: units.smallSpacing *1.5 @@ -89,7 +87,7 @@ Item { text: symbol } - PlasmaComponents.Label { + Label { id: label anchors { margins: units.smallSpacing * 1.5 diff --git a/src/package/contents/ui/Radio.qml b/src/package/contents/ui/Radio.qml new file mode 100644 index 0000000..4f4b88a --- /dev/null +++ b/src/package/contents/ui/Radio.qml @@ -0,0 +1,68 @@ +/* + SPDX-FileCopyrightText: 2018 John Salatas + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.3 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.1 + +ColumnLayout { + id: radio + + property alias text: radio_title.text + property var sensorModel: undefined + property var items: [] + + + property var props + + objectName: "Radio" + + Component { + id: pushButton + PushButton {} + } + + onPropsChanged: { + text = props['text'] + sensorModel = main.sensorsMgr.getSensor(props['sensor']) + items = props['items'] + } + + onItemsChanged: { + for(var i = 0; i < items.length; i++) { + var props = items[i] + props['sensorModel'] = sensorModel + pushButton.createObject(buttons, props); + } + } + + RowLayout { + Label { + id: radio_title + font.pointSize: theme.smallestFont.pointSize * 1.25 + color: theme.textColor + visible: text != '' + focus: false + } + RwModeLabel { + sensorModel: radio.sensorModel + } + /* Spacer */ + Item { + Layout.fillWidth: true + } + } + RowLayout { + id: buttons + spacing: -0.5 + Layout.topMargin: radio_title.visible ? 0 : 8 + // Layout.rightMargin: 15 + + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + + height: units.gridUnit * 4 + } +} diff --git a/src/package/contents/ui/RwModeLabel.qml b/src/package/contents/ui/RwModeLabel.qml new file mode 100644 index 0000000..058536d --- /dev/null +++ b/src/package/contents/ui/RwModeLabel.qml @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.0 +import QtQuick.Controls 2.0 + +Label { + id: rwModeLabel + text: "⚠" + color: "orange" + visible: sensorModel.rw_mode === "w" + + property var sensorModel: undefined + property bool isHovered: false + + ToolTip { + text: "Write-only sensor" + delay: 500 + visible: rwModeLabel.isHovered && rwModeLabel.visible + parent: rwModeLabel + } + + MouseArea{ + anchors.fill: parent + hoverEnabled: true + onEntered: rwModeLabel.isHovered = true + onExited: rwModeLabel.isHovered = false + } +} diff --git a/src/package/contents/ui/SetPrefsManager.qml b/src/package/contents/ui/SetPrefsManager.qml new file mode 100644 index 0000000..55e55c8 --- /dev/null +++ b/src/package/contents/ui/SetPrefsManager.qml @@ -0,0 +1,100 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick 2.3 + +import '../code/utils.js' as Utils +import '../code/datasource.js' as Ds + + +Item { + id: manager + + signal setPrefsReady + signal update(string parameter, var args) + + + // The last one to become true emits the signal. + property bool hasReadSensors: false + property bool hasReadAvailable: false + + function doFirstInit() { + if (hasReadSensors && hasReadAvailable) { + setPrefsReady(); + } + } + + // Parse the result of "set_prefs.sh -read-all" or "set_prefs.sh -read-some .." + function handleReadResult(args, stdout) { + var obj = JSON.parse(stdout); + + var x = args.splice(1) + Ds.parseSensorData(obj, x) + + if(!hasReadSensors) { + main.sensorsMgr.initSensorsDetected() + print("detectedSensors: ", main.sensorsMgr.detectedSensors) + + hasReadSensors = true; + doFirstInit(); + } + } + + // Parse the result of "set_prefs.sh -read-available" + function handleReadAvailResult(stdout) { + var obj = JSON.parse(stdout); + var keys = Object.keys(obj); + var available_values = {} + for (var i=0; i < keys.length; i++) { + var d = obj[keys[i]] + var values = d.split(' ').filter(item => item.length > 0) + available_values[keys[i]] = values + } + + main.sensorsMgr.availableValues = available_values + + print("available_values: ", JSON.stringify(available_values)) + + hasReadAvailable = true; + doFirstInit(); + } + + function handleSetValueResult(sensor, stdout) { + if (main.sensorsMgr.detectedSensors.includes(sensor)) { + var obj = JSON.parse(stdout); + + Ds.parseSensorData(obj, undefined, true) + } + } + + + function updateSensor(name, value) { + print("updating sensor " + name +": " + value) + + var sensorModel = main.sensorsMgr.getSensor(name) + var rw_mode = sensorModel.rw_mode + var old_val = sensorModel.value + + var args = [value] + + var enumSensors = sensorModel.sensor.split('/') + if (enumSensors.length > 1) { + args = enumSensors.slice(1).concat(args) + name = enumSensors[0] + } + + if (rw_mode === 'w') { + /* emit */ update(name, args) + main.sensorsMgr.setSensorValue(name, value) + return + } + + + if(value !== old_val) { + /* emit */ update(name, args) + } else { + print(" same value") + } + } +} diff --git a/src/package/contents/ui/Slider.qml b/src/package/contents/ui/Slider.qml new file mode 100644 index 0000000..0a273e7 --- /dev/null +++ b/src/package/contents/ui/Slider.qml @@ -0,0 +1,118 @@ +/* + SPDX-FileCopyrightText: 2018 John Salatas + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.3 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.1 + +RowLayout { + property alias min: slider.from + property alias max: slider.to + property alias value: slider.value + property bool acceptingChanges: false + property alias pressed: slider.pressed + property bool updating: false + + + property alias text: slider_title.text + property var props + spacing: 10 + + + property var sensorModel: undefined + property var sensorModelMin: undefined + property var sensorModelMax: undefined + + + function onValueChanged() { + if (pressed) { + return + } + acceptingChanges = false + + value = parseInt(sensorModel.value, 10); + slider_value.text = sensorModel.getValueText() + + if (sensorModelMin) { + min = parseInt(sensorModelMin.value, 10); + } + if (sensorModelMax) { + max = parseInt(sensorModelMax.value, 10); + } + + updating = false + acceptingChanges = true + } + + onPropsChanged: { + text = props['text'] + + sensorModel = main.sensorsMgr.getSensor(props['sensor']) + sensorModel.onValueChanged.connect(onValueChanged) + + if(isNaN(props['min'])) { + min = 0 + sensorModelMin = main.sensorsMgr.getSensor(props['min']) + } else { + min = props['min'] + sensorModelMin = undefined + } + if(isNaN(props['max'])) { + max = 100 + sensorModelMax = main.sensorsMgr.getSensor(props['max']) + } else { + max = props['max'] + sensorModelMax = undefined + } + + onValueChanged() + } + + Component.onCompleted: { + onValueChanged() + acceptingChanges = true + } + + Component.onDestruction: { + sensorModel.onValueChanged.disconnect(onValueChanged) + } + + Label { + Layout.alignment: Qt.AlignTop + id: slider_title + font.pointSize: theme.smallestFont.pointSize + color: theme.textColor + horizontalAlignment: Text.AlignRight + Layout.minimumWidth: units.gridUnit * 4 + } + + Slider { + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom + id: slider + stepSize: 0.0 + onPressedChanged: { + //need to resend here + if(acceptingChanges) { + updateSensor(sensorModel.sensor, Math.round(value)) + } + } + onValueChanged: { + if(pressed) { + updating = true + slider_value.text = sensorModel.getValueText(value) + } + } + } + + Label { + Layout.alignment: Qt.AlignTop + id: slider_value + font.pointSize: theme.smallestFont.pointSize + color: pressed || updating? '#ff0000' : theme.textColor + horizontalAlignment: Text.AlignRight + Layout.minimumWidth: units.gridUnit * 3 + } +} diff --git a/src/package/contents/ui/Switch.qml b/src/package/contents/ui/Switch.qml new file mode 100644 index 0000000..8cc08a6 --- /dev/null +++ b/src/package/contents/ui/Switch.qml @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2018 John Salatas + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.3 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.2 + +RowLayout { + property alias text: checkbox_title.text + property alias checked: checkBox.checked + property bool acceptingChanges: false + + property var props + spacing: 10 + + property var sensorModel: undefined + + + function onValueChanged() { + acceptingChanges = false + checked = (sensorModel.value === 'true') + acceptingChanges = true + } + + onPropsChanged: { + text = props['text'] + + sensorModel = main.sensorsMgr.getSensor(props['sensor']) + sensorModel.onValueChanged.connect(onValueChanged) + onValueChanged() + } + + Component.onCompleted: { + onValueChanged() + acceptingChanges = true + } + + Component.onDestruction: { + sensorModel.onValueChanged.disconnect(onValueChanged) + } + + Label { + Layout.alignment: Qt.AlignVCenter + id: checkbox_title + font.pointSize: theme.smallestFont.pointSize + color: theme.textColor + horizontalAlignment: Text.AlignRight + Layout.minimumWidth: units.gridUnit * 4 + } + + CheckBox { + id: checkBox + onCheckedChanged: { + if(acceptingChanges) { + updateSensor(sensorModel.sensor, checked) + } + } + } +} diff --git a/src/package/contents/ui/TabbedRepresentation.qml b/src/package/contents/ui/TabbedRepresentation.qml new file mode 100644 index 0000000..3bb9aeb --- /dev/null +++ b/src/package/contents/ui/TabbedRepresentation.qml @@ -0,0 +1,343 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.3 +import QtQuick.Controls 2.2 + +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.2 + +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.extras 2.0 as PlasmaExtras + +import '../code/utils.js' as Utils + +GridLayout { + id: tabbedRepresentation + + Layout.minimumWidth: units.gridUnit * 24 + Layout.minimumHeight: units.gridUnit * 24 + Layout.preferredWidth: Layout.minimumWidth + Layout.preferredHeight: Layout.minimumHeight + + property var model: Utils.get_model() + property var vendors: Utils.get_vendors() + + property var currentItemId: undefined + + property var profileView: undefined + property bool editMode: false + + + Component { + id: header + Header { + } + } + + Component { + id: toolButton + ToolButton { + id: button + text: undefined + ToolTip.visible: hovered + property var symbolText + property var itemId + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + + Layout.maximumWidth: theme.defaultFont.pointSize * 3.0 + Layout.maximumHeight: theme.defaultFont.pointSize * 3.0 + contentItem: Text { + id: buttonText + text: button.symbolText + font.family: symbolsFont.name + font.pointSize: theme.smallestFont.pointSize * 2.5 + color: editMode ? theme.backgroundColor : theme.textColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent + } + background: Rectangle { + color: Qt.rgba(0, 0, 0, 0) + radius: 2 + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + buttonText.color = editMode ? theme.highlightColor : + theme.highlightColor + } + onExited: { + buttonText.color = editMode ? theme.backgroundColor : + theme.textColor + } + onClicked: { + show_item(itemId) + } + } + + Connections { + target: tabbedRepresentation + onEditModeChanged: { + buttonText.color = editMode ? theme.backgroundColor : + theme.textColor + } + } + + onActiveFocusChanged: { + if (activeFocus) { + buttonText.color = theme.highlightColor + } else { + buttonText.color = theme.textColor + } + } + } + } + + Component { + id: scrollComponent + ScrollView { + id: scrollRoot + property alias props: scrolledHeader.props + property alias showIcon: scrolledHeader.showIcon + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.interactive: false + + Header { + id: scrolledHeader + width: scrollRoot.width + } + } + } + + Component { + id: profileComponent + Profile { + id: profileView + + } + } + + property bool isInitialized: false + + Connections { + target: plasmoid.configuration + onShowIntelGPUChanged: { + initialize() + } + } + + function initialize() { + remove_children() + initialize_toolbar() + + if (isInitialized) { + return + } + + if (main.hasNativeBackend) { + profileView.initialize() + } + + isInitialized = true + } + + function initialize_toolbar() { + for(var i = 0; i < model.length; i++) { + var item = model[i]; + // if(!Utils.is_present(item['vendors'])) { + if(!Utils.sensor_has_value(item)) { + continue + } + switch (item.type) { + case 'header': { + var props = { symbolText: item['icon'], itemId: item['id'] } + var obj = toolButton.createObject(toolbar, props) + obj.ToolTip.text = item.text + obj.ToolTip.delay = 1000 + break + } + default: console.log("unkonwn type: " + item.type) + } + } + } + + function remove_children() { + for(var i = toolbar.children.length; i > 0 ; i--) { + toolbar.children[i-1].destroy() + } + } + + function set_indicator_position(itemId) { + var item = undefined + for(var i = toolbar.children.length; i > 0 ; i--) { + var button = toolbar.children[i-1]; + if (button.itemId !== itemId) { + continue + } + + item = button + break + } + + if (!item && itemId === "profilePage") { + item = profileButton + } + + if (item) { + var res = item.mapToItem(toolbar, 0, 0) + toolbarIndicator.y = res.y + toolbarIndicator.height = item.height + } + } + + function onEnterEditMode() { + editMode = true + } + + function onExitEditMode() { + editMode = false + } + + function show_item(itemId) { + if (itemId === "profilePage") { + if (currentItemId !== itemId) { + stackView.clear(StackView.PopTransition) + stackView.push(profileView, StackView.PushTransition) + currentItemId = itemId + } + set_indicator_position(currentItemId) + + return + } + + var item = undefined + model.forEach(m => { + if (!item && m['id'] === itemId) { + item = m + } + }) + if (!item) { + print("error: Couldn't find item with id=" + itemId) + return + } + + + var props = {'props': item, showIcon: false}; + if (currentItemId && currentItemId === itemId) { + // clicking on the same item reloads it + stackView.pop(StackView.Immediate) + stackView.push(scrollComponent, props, StackView.Immediate) + } + + stackView.clear(StackView.PopTransition) + stackView.push(scrollComponent, props, StackView.PushTransition) + tabbedRepresentation.currentItemId = itemId + set_indicator_position(currentItemId) + } + + Component.onCompleted: { + profileView = profileComponent.createObject() + + profileView.enterEditMode.connect(onEnterEditMode) + profileView.exitEditMode.connect(onExitEditMode) + + profileView.enterEditMode.connect(main.enterEditMode) + profileView.exitEditMode.connect(main.exitEditMode) + } + + + RowLayout { + spacing: 0 + + // Tab bar + Rectangle { + Layout.preferredWidth: childrenRect.width + Layout.preferredHeight: tabbedRepresentation.height + + color: editMode ? theme.textColor : Qt.rgba(0,0,0,0) + + ColumnLayout { + height: tabbedRepresentation.height + + ColumnLayout { + id: toolbar + Layout.fillWidth: true + Layout.fillHeight: true + } + + Item { + /* spacer */ + Layout.fillHeight: true + } + + ColumnLayout { + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + + Loader { + id: profileButton + sourceComponent: main.hasNativeBackend ? toolButton : undefined + onLoaded: { + item.symbolText = /* Black Star */ "\u2605" + item.itemId = "profilePage" + item.ToolTip.text = "Profiles" + item.ToolTip.delay = 1000 + } + + Layout.maximumWidth: theme.defaultFont.pointSize * 3.0 + Layout.maximumHeight: theme.defaultFont.pointSize * 3.0 + } + } + } + } + + Rectangle { + id: toolbarIndicator + x: toolbar.width + height: toolbar.height + // This makes the plasmoid crash on removal. + // width: toolbar.width * 0.075 + width: units.smallSpacing.toFixed(3) * 0.5 + color: theme.highlightColor + + Behavior on y { PropertyAnimation {} } + } + + // Vertical separator + GridLayout { + Rectangle { + id: separator + width: 1 + color: Qt.rgba(theme.textColor.r, + theme.textColor.g, + theme.textColor.b, 0.25) + Layout.fillHeight: true + } + } + + RowLayout { + spacing: 0 + clip: true + + // spacer + Rectangle { + width: units.smallSpacing + color: Qt.rgba(1, 1, 1, 0) + Layout.fillHeight: true + } + + StackView { + id: stackView + + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Layout.fillWidth: true + Layout.fillHeight: true + } + } + } + +} diff --git a/src/package/contents/ui/config/ConfigGeneral.qml b/src/package/contents/ui/config/ConfigGeneral.qml new file mode 100644 index 0000000..5f3fa73 --- /dev/null +++ b/src/package/contents/ui/config/ConfigGeneral.qml @@ -0,0 +1,69 @@ +import QtQuick 2.2 +import QtQuick.Controls 2.0 +import QtQuick.Layouts 1.1 + +import org.kde.kirigami 2.3 as Kirigami + +ColumnLayout { + property alias cfg_useDefaultIcon: useDefaultIconCheckbox.checked + property alias cfg_showIntelGPU: showIntelGPUCheckbox.checked + property string cfg_customIcon: plasmoid.configuration.customIcon + property alias cfg_useSudoForReading: useSudoForReadingCheckbox.checked + property alias cfg_pollingInterval: pollingInterval.value + property alias cfg_monitorWhenHidden: monitorWhenHidden.checked + property alias cfg_slowPollingInterval: slowPollingInterval.value + property bool cfg_hasNativeBackend: false + + Kirigami.FormLayout { + Layout.fillHeight: true + + CheckBox { + id: useDefaultIconCheckbox + Kirigami.FormData.label: i18n('Use Default Icon') + } + + IconPicker { + currentIcon: cfg_customIcon + onIconChanged: cfg_customIcon = iconName + onIconCleared: { + useDefaultIconCheckbox.checked = true + } + enabled: !useDefaultIconCheckbox.checked + } + + CheckBox { + id: showIntelGPUCheckbox + Kirigami.FormData.label: i18n('Show Intel GPU') + } + + CheckBox { + id: useSudoForReadingCheckbox + Kirigami.FormData.label: i18n('Use sudo for reading values') + visible: !cfg_hasNativeBackend + } + + SpinBox { + id: pollingInterval + + Kirigami.FormData.label: i18n("Polling Interval (seconds):") + + from: 2 + to: 3600 + } + + CheckBox { + id: monitorWhenHidden + Kirigami.FormData.label: i18n('Show sensors in tool tip') + } + + SpinBox { + id: slowPollingInterval + + Kirigami.FormData.label: i18n("Tool Tip polling interval (seconds):") + + from: 2 + to: 3600 + enabled: monitorWhenHidden.checked + } + } +} diff --git a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/config/IconPicker.qml b/src/package/contents/ui/config/IconPicker.qml similarity index 53% rename from gr.ictpro.jsalatas.plasma.pstate/contents/ui/config/IconPicker.qml rename to src/package/contents/ui/config/IconPicker.qml index 80fece5..7728d60 100644 --- a/gr.ictpro.jsalatas.plasma.pstate/contents/ui/config/IconPicker.qml +++ b/src/package/contents/ui/config/IconPicker.qml @@ -1,24 +1,40 @@ -import QtQuick 2.2 -import QtQuick.Controls 1.3 +import QtQuick 2.5 +import QtQuick.Controls 2.5 import QtQuick.Layouts 1.1 + import org.kde.kquickcontrolsaddons 2.0 as KQuickAddons import org.kde.plasma.core 2.0 as PlasmaCore -import org.kde.plasma.components 2.0 as PlasmaComponents + +import org.kde.kirigami 2.3 as Kirigami + // basically taken from kickoff Button { id: iconButton + Kirigami.FormData.label: i18n("Icon:") + property string currentIcon property string defaultIcon signal iconChanged(string iconName) signal iconCleared - Layout.minimumWidth: previewFrame.width + units.smallSpacing * 2 + Layout.minimumWidth: previewFrame.width Layout.maximumWidth: Layout.minimumWidth - Layout.minimumHeight: previewFrame.height + units.smallSpacing * 2 - Layout.maximumHeight: Layout.minimumWidth + Layout.minimumHeight: previewFrame.height + Layout.maximumHeight: Layout.minimumHeight + + background: Rectangle { + color: Qt.rgba(0,0,0,0) + width: previewFrame.width + height: previewFrame.height + + MouseArea { + anchors.fill: parent + onPressed: iconMenu.opened ? iconMenu.close() : iconMenu.open() + } + } KQuickAddons.IconDialog { id: iconDialog @@ -28,21 +44,11 @@ Button { } } - // just to provide some visual feedback, cannot have checked without checkable enabled - checkable: true - onClicked: { - checked = Qt.binding(function() { // never actually allow it being checked - return iconMenu.status === PlasmaComponents.DialogStatus.Open - }) - - iconMenu.open(0, height) - } - PlasmaCore.FrameSvgItem { id: previewFrame - anchors.centerIn: parent - imagePath: plasmoid.location === PlasmaCore.Types.Vertical || plasmoid.location === PlasmaCore.Types.Horizontal - ? "widgets/panel-background" : "widgets/background" + imagePath: plasmoid.location === PlasmaCore.Types.Vertical || + plasmoid.location === PlasmaCore.Types.Horizontal ? + "widgets/panel-background" : "widgets/background" width: units.iconSizes.large + fixedMargins.left + fixedMargins.right height: units.iconSizes.large + fixedMargins.top + fixedMargins.bottom @@ -58,23 +64,24 @@ Button { function setDefaultIcon() { iconPreview.source = defaultIcon iconChanged(defaultIcon) - iconCleared + iconCleared() } - // QQC Menu can only be opened at cursor position, not a random one - PlasmaComponents.ContextMenu { + Menu { id: iconMenu - visualParent: iconButton - PlasmaComponents.MenuItem { + // Appear below the button + y: +parent.height + + MenuItem { text: i18nc("@item:inmenu Open icon chooser dialog", "Choose...") - icon: "document-open-folder" - onClicked: iconDialog.open() + icon.name: "document-open-folder" + onPressed: iconDialog.open() } - PlasmaComponents.MenuItem { + MenuItem { text: i18nc("@item:inmenu Reset icon to default", "Clear Icon") - icon: "edit-clear" - onClicked: setDefaultIcon() + icon.name: "edit-clear" + onPressed: setDefaultIcon() } } } diff --git a/src/package/contents/ui/main.qml b/src/package/contents/ui/main.qml new file mode 100644 index 0000000..eec259e --- /dev/null +++ b/src/package/contents/ui/main.qml @@ -0,0 +1,607 @@ +/* + SPDX-FileCopyrightText: 2018 John Salatas + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.3 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore + +import '../code/utils.js' as Utils + +import './DataSourceBackend' as DataSourceBackend +import './NativeBackend' as NativeBackend + +import './Model' as Model + +Item { + id: main + + signal updateSensor(string name, string value) + + FontLoader { + id: symbolsFont; + source: '../font/plasma-pstate.ttf' + } + + property var tabbedRep: undefined + + property var monitor_sources: [ + /cpu\/system\/AverageClock/g, + /cpu\/system\/TotalLoad/g, + /lmsensors\/.*Package_id_0/g, + /lmsensors\/.*fan/g + ] + + property var updater: undefined + property var monitorDS: undefined + + property bool isInitialized: false + property bool inTray: false + + property bool editMode: false + + property bool hasNativeBackend: plasmoid.nativeInterface.isReady !== undefined + + readonly property string set_prefs: '/usr/share/plasma/plasmoids/' + + 'gr.ictpro.jsalatas.plasma.pstate/contents/code/' + + 'set_prefs.sh' + + property int pollingInterval: plasmoid.configuration.pollingInterval ? + (plasmoid.configuration.pollingInterval * 1000) : 2000 + property int slowPollingInterval: (plasmoid.configuration.slowPollingInterval * 1000) + + function sensor_short_name(long_name) { + var parts = long_name.split('/'); + return parts[parts.length - 1]; + } + + Plasmoid.compactRepresentation: CompactRepresentation { } + // Plasmoid.fullRepresentation: FullRepresentation { } + + Plasmoid.fullRepresentation: TabbedRepresentation { + id: tabbedRep + Component.onCompleted: { + main.tabbedRep = tabbedRep + + initializeNvidia() + firstInit.viewReady() + } + } + + + Plasmoid.preferredRepresentation: Plasmoid.compactRepresentation + Plasmoid.switchWidth: units.gridUnit * 15 + Plasmoid.switchHeight: units.gridUnit * 20 + + property string toolTipMainText + property string toolTipSubText + property int toolTipTextFormat + property string icon + + property var sensorsMgr + + + Component { + id: sensorsMgrComponent + Model.SensorsManager { + } + } + + FirstInit { + id: firstInit + Component.onCompleted: { + sensorsMgr = sensorsMgrComponent.createObject() + firstInit.beginStageOne.connect(onBeginStageOne) + firstInit.beginStageTwo.connect(onBeginStageTwo) + firstInit.initialized.connect(main.initialized) + /* emit */ firstInit.beginStageOne() + } + + function onBeginStageOne() { + sensorsMgr.loadSensors() + if (main.hasNativeBackend) { + nativeBackendInit.init(firstInit.scriptReady) + } + } + + function onBeginStageTwo() { + if (main.hasNativeBackend) { + firstInit.initialized.connect(monitorDS.dataSourceReady) + main.monitorDS.init() + } + } + } + + NativeBackend.Init { + id: nativeBackendInit + hasNativeBackend: main.hasNativeBackend + } + + Component.onCompleted: { + inTray = plasmoid.parent != null && + ((plasmoid.parent.pluginName === + 'org.kde.plasma.private.systemtray') || + (plasmoid.parent.objectName === 'taskItemContainer')) + } + + function initialized() { + plasmoid.configuration.hasNativeBackend = hasNativeBackend + + main.updateSensor.connect(prefsManager.updateSensor) + prefsManager.update.connect(updater.update) + + toolTipMainText = plasmoid.toolTipMainText + toolTipSubText = plasmoid.toolTipSubText + toolTipTextFormat = plasmoid.toolTipTextFormat + icon = plasmoid.icon + + if (plasmoid.configuration.monitorWhenHidden) { + useCustomToolTip() + } + + if (plasmoid.expanded) { + setMonitorInterval(pollingInterval) + } else { + setMonitorInterval(slowPollingInterval) + } + + isInitialized = true + + + if (shouldMonitor()) { + startMonitors() + } else { + stopMonitors() + } + + main.tabbedRep.initialize() + if (main.tabbedRep.show_item) { + main.tabbedRep.show_item("processorSettings") + } + } + + function enterEditMode() { + stopMonitors() + editMode = true + main.updateSensor.disconnect(prefsManager.updateSensor) + main.updateSensor.connect(main.phonyUpdateSensor) + } + + function exitEditMode() { + main.updateSensor.disconnect(main.phonyUpdateSensor) + main.updateSensor.connect(prefsManager.updateSensor) + editMode = false + startMonitors() + } + + function phonyUpdateSensor(name, value) { + sensorsMgr.setSensorValue(name, value) + } + + + function shouldMonitor() { + if (editMode) { + return false + } + return !inTray || plasmoid.expanded || + plasmoid.configuration.monitorWhenHidden + } + + function startMonitors() { + if (!plasmoid.expanded) { + connectTooltipSensors() + } + powermanagementDS.start() + systemmonitorDS.start() + monitorDS.start() + } + + function stopMonitors() { + powermanagementDS.stop() + systemmonitorDS.stop() + monitorDS.stop() + + if (!plasmoid.expanded) { + disconnectTooltipSensors() + } + } + + function setMonitorInterval(interval) { + powermanagementDS.interval = interval + systemmonitorDS.interval = interval + monitorDS.interval = interval + } + + function useOriginalToolTip() { + Plasmoid.toolTipMainText = toolTipMainText + Plasmoid.toolTipSubText = toolTipSubText + Plasmoid.toolTipTextFormat = toolTipTextFormat + // Plasmoid.icon = icon + } + + function useCustomToolTip() { + Plasmoid.toolTipMainText = '' + Plasmoid.toolTipSubText = '' + Plasmoid.toolTipTextFormat = Text.RichText + // Plasmoid.icon = '' + updateTooltip() + } + + function monitor_source(src) { + for(var i=0; i < monitor_sources.length; i++) { + if(src.match(monitor_sources[i])) { + return true; + } + } + + return false; + } + + PlasmaCore.DataSource { + id: systemmonitorDS + engine: 'systemmonitor' + + property var sources: [] + + onSourceAdded: { + if(monitor_source(source)) { + if(connectedSources.indexOf(source) === -1) { + connectSource(source); + sources.push(source) + } + } + } + + onNewData: { + var source_short_name = sensor_short_name(sourceName); + + if(source_short_name.startsWith('fan')) { + var sensorModel = sensorsMgr.getSensor('fan_speeds') + if (sensorModel.value !== undefined) { + var arr = sensorModel.value + arr[source_short_name] = data.value + sensorModel.value = arr + } + } else { + switch (source_short_name) { + case 'AverageClock': { + sensorsMgr.setSensorValue('cpu_cur_freq', data.value) + break; + } + case 'Package_id_0': { + sensorsMgr.setSensorValue('package_temp', data.value) + break; + } + case 'TotalLoad': { + sensorsMgr.setSensorValue('cpu_cur_load', data.value) + break; + } + } + } + } + interval: pollingInterval + + function start() { + sources.forEach(source => { + if (connectedSources.indexOf(source) === -1) { + connectSource(source) + } + }) + } + + function stop() { + sources.forEach(source => { + if (connectedSources.indexOf(source) !== -1) { + disconnectSource(source) + } + }) + } + } + + PlasmaCore.DataSource { + id: powermanagementDS + engine: "powermanagement" + onDataChanged: { + if(powermanagementDS.data["Battery"]) { + var sensorModel = undefined + + var bat_time = Number(powermanagementDS.data["Battery"]["Remaining msec"]) / 1000; + sensorsMgr.setSensorValue('battery_remaining_time', bat_time) + + var bat_charge = powermanagementDS.data["Battery"]["Percent"]; + sensorsMgr.setSensorValue('battery_percentage', bat_charge) + } + } + interval: pollingInterval + + property var sources: ['Battery'] + + function start() { + sources.forEach(source => { + if (connectedSources.indexOf(source) === -1) { + connectSource(source) + } + }) + } + + function stop() { + sources.forEach(source => { + if (connectedSources.indexOf(source) !== -1) { + disconnectSource(source) + } + }) + } + } + + Connections { + target: plasmoid + function onExpandedChanged() { + if (!main.isInitialized) { + return + } + + stopMonitors() + + if (plasmoid.expanded) { + setMonitorInterval(pollingInterval) + } else { + setMonitorInterval(slowPollingInterval) + } + + if (shouldMonitor()) { + startMonitors() + } + } + } + + Connections { + target: plasmoid.configuration + onUseSudoForReadingChanged: { + if (shouldMonitor()) { + stopMonitors + startMonitors() + } + } + + onPollingIntervalChanged: { + pollingInterval = plasmoid.configuration.pollingInterval * 1000 + + stopMonitors() + setMonitorInterval(pollingInterval) + + if (shouldMonitor()) { + startMonitors() + } + } + + onSlowPollingIntervalChanged: { + slowPollingInterval = plasmoid.configuration.slowPollingInterval * 1000 + + stopMonitors() + setMonitorInterval(slowPollingInterval) + + if (shouldMonitor()) { + startMonitors() + } + } + + onMonitorWhenHiddenChanged: { + if (!plasmoid.configuration.monitorWhenHidden) { + stopMonitors() + useOriginalToolTip() + } else { + startMonitors() + useCustomToolTip() + } + } + } + + SetPrefsManager { + id: prefsManager + + Component.onCompleted: { + prefsManager.setPrefsReady.connect(firstInit.dataReady) + } + } + + Item { + Loader { + id: monitorLoader + onLoaded: { + print("monitorLoader: loaded " + monitorLoader.item.name) + main.monitorDS = monitorLoader.item + monitorDS.handleReadResult + .connect(prefsManager.handleReadResult) + if (hasNativeBackend) { + monitorDS.handleReadAvailResult + .connect(prefsManager.handleReadAvailResult) + monitorDS.handleSetValueResult + .connect(prefsManager.handleSetValueResult) + } + firstInit.monitorReady() + } + } + Component.onCompleted: { + if (hasNativeBackend) { + var native_src = "./NativeBackend/Monitor.qml" + var native_props = { + interval: main.pollingInterval, + running: false, + repeat: main.pollingInterval > 0, + triggeredOnStart: true, + } + monitorLoader.setSource(native_src, native_props); + } else { + var local_src = "./DataSourceBackend/Monitor.qml" + var local_props = { + set_prefs: main.set_prefs, + pollingInterval: main.pollingInterval + } + monitorLoader.setSource(local_src, local_props); + } + } + } + + Item { + Loader { + id: updaterLoader + onLoaded: { + print("updaterLoader: loaded " + updaterLoader.item.name) + main.updater = updaterLoader.item + if (!main.hasNativeBackend) { + main.updater.handleSetValueResult + .connect(prefsManager.handleSetValueResult) + + firstInit.scriptReady() + } + } + } + Component.onCompleted: { + if (hasNativeBackend) { + var native_src = "./NativeBackend/Updater.qml" + var native_props = { + } + updaterLoader.setSource(native_src, native_props); + } else { + var local_src = "./DataSourceBackend/Updater.qml" + var local_props = { + set_prefs: main.set_prefs, + } + updaterLoader.setSource(local_src, local_props); + } + } + } + + Item { + Loader { + id: availableValuesLoader + onLoaded: { + print("availableValuesLoader: loaded " + availableValuesLoader.item.name) + availableValuesLoader.item.handleReadAvailResult + .connect(prefsManager.handleReadAvailResult) + } + } + Component.onCompleted: { + if (hasNativeBackend) { + } else { + var local_src = "./DataSourceBackend/AvailableValues.qml" + var local_props = { set_prefs: main.set_prefs, } + availableValuesLoader.setSource(local_src, local_props); + } + } + } + + function initializeNvidia() { + if (nvidiaPowerMizerDS && nvidiaPowerMizerDS.initialized) { + return + } + if (!tabbedRep || !tabbedRep.initialize) { + return + } + nvidiaPowerMizerDS.dataSourceReady.connect(tabbedRep.initialize) + nvidiaPowerMizerDS.initialized = true + } + + NvidiaPowerMizerDS { + id: nvidiaPowerMizerDS + + property bool initialized: false + + Component.onCompleted: { + initializeNvidia() + } + } + + + PlasmaCore.DataSource { + id: notificationSource + engine: "executable" + + onNewData: { + disconnectSource(sourceName) + } + function createNotification(msg) { + var cmd = ["notify-send"] + cmd.push("-u", "normal") + cmd.push("-t", "5000") + cmd.push("-a", "\"P-state and CPUFreq Manager\"") + cmd.push("-i", "cpu") + cmd.push("-c", "hardware") + cmd.push("\""+msg+"\"") + print(cmd.join(" ")) + connectSource(cmd.join(" ")) + } + } + + + // One sensor from each monitor is enough + property var tooltipSensors: ['cpu_cur_load', 'gpu_cur_freq', 'battery_percentage' ] + + function connectTooltipSensors() { + for (var i in tooltipSensors) { + var sensor = tooltipSensors[i] + var sensorModel = sensorsMgr.getSensor(sensor) + sensorModel.onValueChanged.connect(updateTooltip) + } + } + + function disconnectTooltipSensors() { + for (var i in tooltipSensors) { + var sensor = tooltipSensors[i] + var sensorModel = sensorsMgr.getSensor(sensor) + sensorModel.onValueChanged.disconnect(updateTooltip) + } + } + + function updateTooltip() { + if (!plasmoid.configuration.monitorWhenHidden) { + return + } + + var loadText = Utils.get_sensors_text(['cpu_cur_load', 'cpu_cur_freq', 'gpu_cur_freq']) + + var toolTipSubText =''; + var txt = ''; + + toolTipSubText += '' + + toolTipSubText += '' + toolTipSubText += '' + toolTipSubText += '' + toolTipSubText += '' + + txt = Utils.get_sensors_text(['battery_percentage', 'battery_remaining_time']); + if(txt != 'N/A') { + toolTipSubText += '' + toolTipSubText += '' + toolTipSubText += '' + toolTipSubText += '' + } + + txt = Utils.get_sensors_text(['package_temp', 'fan_speeds']); + if (txt != 'N/A') { + toolTipSubText += '' + toolTipSubText += '' + toolTipSubText += '' + toolTipSubText += '' + } + toolTipSubText += '
' + toolTipSubText += 'd' + toolTipSubText += '' + toolTipSubText += '  ' + loadText +'' + toolTipSubText += '
' + toolTipSubText += 'h' + toolTipSubText += '' + toolTipSubText += '  '+ txt +'' + toolTipSubText += '
' + toolTipSubText += 'b' + toolTipSubText += '' + toolTipSubText += '  '+ txt +'' + toolTipSubText += '
' + + Plasmoid.toolTipSubText = toolTipSubText + } +} diff --git a/gr.ictpro.jsalatas.plasma.pstate/metadata.desktop b/src/package/metadata.desktop similarity index 66% rename from gr.ictpro.jsalatas.plasma.pstate/metadata.desktop rename to src/package/metadata.desktop index c62923b..793ea19 100644 --- a/gr.ictpro.jsalatas.plasma.pstate/metadata.desktop +++ b/src/package/metadata.desktop @@ -1,6 +1,6 @@ [Desktop Entry] Comment=Intel P-state and CPUFreq Manager -Name=Intel P-state and CPUFreq Manager +Name=P-state and CPUFreq Icon=cpu Type=Service X-KDE-ParentApp= @@ -9,9 +9,13 @@ X-KDE-PluginInfo-Category=System Information X-KDE-PluginInfo-Email=jsalatas@ictpro.gr X-KDE-PluginInfo-License=GPL2+ X-KDE-PluginInfo-Name=gr.ictpro.jsalatas.plasma.pstate -X-KDE-PluginInfo-Version=1.0.6 +X-KDE-PluginInfo-Version=2.0.0 X-KDE-PluginInfo-Website=https://jsalatas.ictpro.gr X-KDE-ServiceTypes=Plasma/Applet X-Plasma-API=declarativeappletscript +X-KDE-Library=plasma_applet_pstate X-Plasma-MainScript=ui/main.qml -X-Plasma-StandAloneApp=true +# X-Plasma-StandAloneApp=true +X-KDE-PluginInfo-EnabledByDefault=true +X-Plasma-NotificationArea=true +X-Plasma-NotificationAreaCategory=Hardware diff --git a/src/plasma_pstate.policy b/src/plasma_pstate.policy new file mode 100644 index 0000000..2828a34 --- /dev/null +++ b/src/plasma_pstate.policy @@ -0,0 +1,19 @@ + + + + + + Authentication is required to run the Intel P-state and CPUFreq Manager + cpu + + no + no + yes + + /usr/share/plasma/plasmoids/gr.ictpro.jsalatas.plasma.pstate/contents/code/set_prefs.sh + false + + + diff --git a/src/pstate.cpp b/src/pstate.cpp new file mode 100644 index 0000000..186f958 --- /dev/null +++ b/src/pstate.cpp @@ -0,0 +1,113 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "pstate.h" + +#include + + +#define ORGANIZATION "gr.ictpro.jsalatas.plasma.pstate" +#define APPLICATION "pstate" + + +Pstate::Pstate(QObject *parent, const QVariantList &args) + : Plasma::Applet(parent, args) +{ + m_exec = new SetPrefs{this}; + + QObject::connect(m_exec, &SetPrefs::procStarted, + this, &Pstate::setPrefsStarted); + QObject::connect(m_exec, &SetPrefs::procFinished, + this, &Pstate::setPrefsFinished); + QObject::connect(m_exec, &SetPrefs::commandFinished, + this, &Pstate::setPrefsCommandFinished); +} + +Pstate::~Pstate() +{ + if (m_exec) + delete m_exec; +} + +bool Pstate::isReady() +{ + return m_isReady; +} + +void Pstate::setPrefsStarted() +{ + m_isReady = true; + emit isReadyChanged(m_isReady); +} + +void Pstate::setPrefsFinished() +{ + m_isReady = false; + emit isReadyChanged(m_isReady); +} + +void Pstate::startScript() +{ + m_exec->startScript(); +} + +void Pstate::setPrefs(const QStringList &args) +{ + if (m_exec) { + m_exec->runCommand(args); + } +} + +void Pstate::setPrefsCommandFinished(CommandResult* data) +{ + emit commandFinished(data); +} + + +QStringList Pstate::getProfileList() +{ + QSettings settings(ORGANIZATION, APPLICATION); + QVariant val = settings.value("profileList"); + QStringList data = val.value(); + return data; +} + +void Pstate::saveProfileList(QStringList data) +{ + QSettings settings(ORGANIZATION, APPLICATION); + settings.setValue("profileList", QVariant::fromValue(data)); +} + +void Pstate::saveProfile(QString name, QString data) +{ + QSettings settings(ORGANIZATION, APPLICATION); + settings.beginGroup("profiles"); + settings.setValue(name, data); + settings.endGroup(); + settings.sync(); +} + +void Pstate::deleteProfile(QString name) +{ + QSettings settings(ORGANIZATION, APPLICATION); + settings.beginGroup("profiles"); + settings.remove(name); + settings.endGroup(); + settings.sync(); +} + +QString Pstate::getProfile(QString name) +{ + QSettings settings(ORGANIZATION, APPLICATION); + settings.beginGroup("profiles"); + QVariant val = settings.value(name); + QString data = val.value(); + settings.endGroup(); + return data; +} + +K_PLUGIN_CLASS_WITH_JSON(Pstate, "metadata.json") + +#include "pstate.moc" diff --git a/src/pstate.h b/src/pstate.h new file mode 100644 index 0000000..dba52e6 --- /dev/null +++ b/src/pstate.h @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef PSTATE_H +#define PSTATE_H + + +#include +#include +#include + +#include "command_result.h" +#include "set_prefs.h" + + +class Pstate : public Plasma::Applet +{ + Q_OBJECT + Q_PROPERTY(bool isReady READ isReady NOTIFY isReadyChanged) + + SetPrefs* m_exec; + + +public: + Pstate( QObject *parent, const QVariantList &args ); + ~Pstate(); + + bool isReady(); + +signals: + // Emit command results to QML + void commandFinished(CommandResult* data); + + void isReadyChanged(bool isReady); + +public slots: + void startScript(); + void setPrefs(const QStringList &args); + + QStringList getProfileList(); + void saveProfileList(QStringList); + + void saveProfile(QString name, QString data); + void deleteProfile(QString name); + QString getProfile(QString name); + +private: + bool m_isReady; + +private slots: + void setPrefsStarted(); + void setPrefsFinished(); + void setPrefsCommandFinished(CommandResult* data); +}; + +#endif diff --git a/src/set_prefs.cpp b/src/set_prefs.cpp new file mode 100644 index 0000000..359ed7a --- /dev/null +++ b/src/set_prefs.cpp @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "set_prefs.h" +#include "pstate.h" +#include "command_result.h" + + +#define SET_PREFS_SCRIPT "/usr/share/plasma/plasmoids/" \ + "gr.ictpro.jsalatas.plasma.pstate/contents/code/" \ + "set_prefs.sh" + +SetPrefs::SetPrefs(QObject *parent) : + QObject(parent), m_codec(QTextCodec::codecForName("UTF-8")) +{ + connect(&m_proc, SIGNAL(readyRead()), this, SLOT(read())); + connect(&m_proc, SIGNAL(started()), this, SLOT(started())); + connect(&m_proc, SIGNAL(finished(int)), this, SLOT(finished())); + +} + +SetPrefs::~SetPrefs() +{ + m_proc.write("-exit\n"); + m_proc.waitForFinished(1000); + m_proc.terminate(); + + disconnect(&m_proc, SIGNAL(readyRead()), this, SLOT(read())); + disconnect(&m_proc, SIGNAL(started()), this, SLOT(started())); + disconnect(&m_proc, SIGNAL(finished(int)), this, SLOT(finished())); +} + +void SetPrefs::read() +{ + while (m_proc.canReadLine()) { + QByteArray line = m_proc.readLine(); + QByteArray stderr_buf = m_proc.readAllStandardError(); + + if (m_args.size() == 0) { + fprintf(stderr, "Read a line while having an empty command queue." + "Expected at least one command in the queue"); + continue; + } + + QStringList& args = m_args.front(); + QString stdout = m_codec->toUnicode(line).trimmed(); + QString stderr = m_codec->toUnicode(stderr_buf).trimmed(); + int exitCode = stderr.size() == 0 ? 0 : 1; + + // qDebug() << "args:" << args; + // qDebug() << "stdout:" << stdout; + // qDebug() << "stderr:" << stdout; + + CommandResult data(exitCode, args, stdout, stderr); + emit commandFinished(&data); + + m_args.pop_front(); + } +} + +void SetPrefs::started() +{ + emit procStarted(); +} + +void SetPrefs::finished() +{ + emit procFinished(); +} + +void SetPrefs::startScript() +{ + if (m_proc.state() != QProcess::NotRunning) { + fprintf(stderr, "%s m_proc already running.\n", __PRETTY_FUNCTION__); + return; + } + + m_proc.start("pkexec", {SET_PREFS_SCRIPT, "-daemon"}); + m_proc.waitForStarted(); +} + +void SetPrefs::runCommand(QStringList args) +{ + m_args.push_back(args); + QString str = args.join(" ").append('\n'); + m_proc.write(str.toUtf8()); +} + +#include "set_prefs.moc" diff --git a/src/set_prefs.h b/src/set_prefs.h new file mode 100644 index 0000000..1fc6285 --- /dev/null +++ b/src/set_prefs.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef SETPREFS_H +#define SETPREFS_H + +#include +#include +#include +#include + +#include "sharedqueue.h" +#include "command_result.h" + +class Pstate; + +class SetPrefs : public QObject { + Q_OBJECT + + QTextCodec *m_codec; + SharedQueue m_args; + QProcess m_proc{this}; + +public: + SetPrefs(QObject *parent = nullptr); + ~SetPrefs(); + + void startScript(); + void runCommand(QStringList args); + +signals: + void procStarted(); + void procFinished(); + + // Emit command results to main Plasma Applet + void commandFinished(CommandResult* data); + +public slots: + void read(); + void started(); + void finished(); +}; + + +#endif diff --git a/src/sharedqueue.cpp b/src/sharedqueue.cpp new file mode 100644 index 0000000..d102b3d --- /dev/null +++ b/src/sharedqueue.cpp @@ -0,0 +1,5 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "sharedqueue.h" diff --git a/src/sharedqueue.h b/src/sharedqueue.h new file mode 100644 index 0000000..1aa0e86 --- /dev/null +++ b/src/sharedqueue.h @@ -0,0 +1,98 @@ +/* + SPDX-FileCopyrightText: 2021 Vincent Grabner + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef SHAREDQUEUE_H +#define SHAREDQUEUE_H + +#include +#include +#include + + +template +class SharedQueue +{ +private: + std::mutex m_mutex; + std::condition_variable m_cond; + + std::deque m_queue; + +public: + SharedQueue(); + ~SharedQueue(); + + int size(); + + T& front(); + void pop_front(); + + void push_back(const T& item); + void push_back(T&& item); +}; + +template +SharedQueue::SharedQueue() +{ + +} + +template +SharedQueue::~SharedQueue() +{ + +} + +template +T& SharedQueue::front() +{ + std::unique_lock mlock(this->m_mutex); + while (this->m_queue.empty()) + { + this->m_cond.wait(mlock); + } + return this->m_queue.front(); +} + +template +void SharedQueue::pop_front() +{ + std::unique_lock mlock(this->m_mutex); + while (this->m_queue.empty()) + { + this->m_cond.wait(mlock); + } + this->m_queue.pop_front(); +} + +template +void SharedQueue::push_back(const T& item) +{ + std::unique_lock mlock(this->m_mutex); + this->m_queue.push_back(item); + mlock.unlock(); + this->m_cond.notify_one(); + +} + +template +void SharedQueue::push_back(T&& item) +{ + std::unique_lock mlock(this->m_mutex); + this->m_queue.push_back(std::move(item)); + mlock.unlock(); + this->m_cond.notify_one(); + +} + +template +int SharedQueue::size() +{ + std::unique_lock mlock(this->m_mutex); + int size = this->m_queue.size(); + mlock.unlock(); + return size; +} + +#endif /* SHAREDQUEUE_H */ diff --git a/uninstall.sh b/uninstall.sh deleted file mode 100755 index 2ca56b4..0000000 --- a/uninstall.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -if [[ $EUID -ne 0 ]]; then - echo "This script must be run as root." 2>&1 - exit 1 -fi - -SUDOERS_FILE="/etc/sudoers.d/99-plasma-pstate" - -rm -f ${SUDOERS_FILE} -kpackagetool5 -g -t Plasma/Applet -r gr.ictpro.jsalatas.plasma.pstate - -echo "Uninstall complete." -exit 0