├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── cosmic-applet-minimon.png ├── i18n.toml ├── i18n ├── da │ └── cosmic_applet_minimon.ftl ├── en │ └── cosmic_applet_minimon.ftl ├── nl │ └── cosmic_applet_minimon.ftl ├── pt-BR │ └── cosmic_applet_minimon.ftl └── sv │ └── cosmic_applet_minimon.ftl ├── justfile ├── mini ├── res ├── icons │ └── apps │ │ ├── io.github.cosmic-utils.cosmic-applet-minimon-gpu.svg │ │ ├── io.github.cosmic-utils.cosmic-applet-minimon-harddisk.svg │ │ ├── io.github.cosmic-utils.cosmic-applet-minimon-network.svg │ │ ├── io.github.cosmic-utils.cosmic-applet-minimon-ram.svg │ │ ├── io.github.cosmic-utils.cosmic-applet-minimon-temperature.svg │ │ └── io.github.cosmic-utils.cosmic-applet-minimon.svg ├── io.github.cosmic-utils.cosmic-applet-minimon.desktop └── io.github.cosmic-utils.cosmic-applet-minimon.metainfo.xml └── src ├── app.rs ├── charts ├── heat.rs ├── line.rs ├── mod.rs └── ring.rs ├── colorpicker.rs ├── config.rs ├── i18n.rs ├── main.rs ├── sensors ├── cpu.rs ├── cputemp.rs ├── disks.rs ├── gpu │ ├── amd.rs │ ├── intel.rs │ ├── mod.rs │ └── nvidia.rs ├── gpus.rs ├── memory.rs ├── mod.rs └── network.rs ├── sleepinhibitor.rs └── svg_graph.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | # Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cosmic-applet-minimon" 3 | version = "0.5.2" 4 | edition = "2024" 5 | license = "GPL-3.0" 6 | 7 | [features] 8 | default = [] 9 | caffeine = [] 10 | 11 | [profile.release] 12 | lto = "thin" 13 | opt-level = 3 14 | debug = false 15 | 16 | [dependencies] 17 | systemd-journal-logger = "2.2" 18 | i18n-embed-fl = "0.8" 19 | rust-embed = "8.3.0" 20 | sysinfo = "0.34" 21 | serde = "1.0.197" 22 | nvml-wrapper = "0.10.0" 23 | log = "0.4.27" 24 | anyhow = { version = "1.0.97", features = ["backtrace"] } 25 | zbus = { version = "5" } 26 | zvariant = "5" 27 | fern = "0.7" 28 | chrono = "0.4" 29 | sha2 = "0.10" 30 | hex = "0.4" 31 | 32 | [dependencies.libcosmic] 33 | git = "https://github.com/pop-os/libcosmic.git" 34 | default-features = false 35 | features = ["applet", "applet-token"] 36 | 37 | [dependencies.i18n-embed] 38 | version = "0.14" 39 | features = ["fluent-system", "desktop-requester"] 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minimon COSMIC Applet 2 | 3 | A configurable applet for displaying the following: 4 | * CPU load 5 | * CPU temperature 6 | * Memory usage 7 | * Network utilization 8 | * Disk activity 9 | * GPU and VRAM usage on Nvidia and AMD GPUs. 10 | 11 | Can sit in the panel or Dock. Configurable refresh rate and many display options. 12 | 13 | ![Image](cosmic-applet-minimon.png) 14 | 15 | 16 | ![Image](https://github.com/user-attachments/assets/5d697c74-f7dc-4213-8516-465c32e5567b) 17 | 18 | 19 | ![Image](https://github.com/user-attachments/assets/b6fa25a0-2945-4a40-bdf4-38ef946b8d26) 20 | 21 | 22 | 23 | ![Image](https://github.com/user-attachments/assets/2787cf05-2121-4c25-b1a2-d0b511c30215) 24 | 25 | ![Image](https://github.com/user-attachments/assets/fa6f4b2c-ab95-4815-b7ab-fdd7557797f7) 26 | 27 | ## Installing 28 | If on a .deb based distibution download [latest version](https://github.com/Hyperchaotic/minimon-applet/releases) and install with the following commands: 29 | 30 | ```sh 31 | sudo dpkg -i cosmic-applet-minimon_0.3.10_amd64.deb 32 | ``` 33 | 34 | ## Building 35 | 36 | To build the applet, you will need [just](https://github.com/casey/just) and probably xkbcommon, if you're on Pop!\_OS, you can install it with the following command: 37 | 38 | ```sh 39 | sudo apt install just libxkbcommon-dev 40 | ``` 41 | 42 | Run the following commands to build and install the applet: 43 | 44 | ```sh 45 | just build-release 46 | just install 47 | ``` 48 | 49 | Alternatively generate a deb or rpm file for installation: 50 | 51 | ```sh 52 | just build-release 53 | just deb 54 | just rpm 55 | ``` 56 | and install with: 57 | 58 | ```sh 59 | sudo dpkg -i 60 | sudo dnf install 61 | ``` 62 | 63 | For checking logs: 64 | 65 | ``` 66 | journalctl SYSLOG_IDENTIFIER=cosmic-applet-minimon 67 | ``` 68 | -------------------------------------------------------------------------------- /cosmic-applet-minimon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmic-utils/minimon-applet/8d22a4fba6470c44f0cf719c99a8177d8275a55c/cosmic-applet-minimon.png -------------------------------------------------------------------------------- /i18n.toml: -------------------------------------------------------------------------------- 1 | fallback_language = "en" 2 | 3 | [fluent] 4 | assets_dir = "i18n" -------------------------------------------------------------------------------- /i18n/da/cosmic_applet_minimon.ftl: -------------------------------------------------------------------------------- 1 | cpu-title = CPU-belastning (gennemsnit) 2 | enable-cpu-chart = Vis CPU-diagram 3 | enable-cpu-label = Vis CPU-etiket 4 | net-title = Netværksbelastning 5 | net-title-combined = Netværksbelastning i bit pr. sekund 6 | net-title-dl = Netværksdownload i bit pr. sekund 7 | net-title-ul = Netværksupload i bit pr. sekund 8 | enable-net-chart = Vis netværksdiagram 9 | enable-net-label = Vis netværksetiket 10 | memory-title = Hukommelsesforbrug 11 | enable-memory-chart = Vis hukommelsesdiagram 12 | enable-memory-label = Vis hukommelsesetiket 13 | use-adaptive = Brug adaptiv skala 14 | net-bandwidth = Netværkshastighed 15 | refresh-rate = Opdateringsfrekvens (sekunder) 16 | change-colors = Skift farver 17 | change-label-size = Etiketstørrelse 18 | colorpicker-colors = farver 19 | colorpicker-defaults = Standarder 20 | colorpicker-accent = Fremhævningsfarve 21 | colorpicker-cancel = Annuller 22 | colorpicker-save = Gem 23 | graph-type-ring = Ring 24 | graph-type-line = Linje 25 | sensor-cpu = CPU 26 | sensor-network = Netværk 27 | sensor-memory = Hukommelse 28 | sensor-disks = Disk 29 | graph-ring-r1 = Ring1. 30 | graph-ring-r2 = Ring2. 31 | graph-ring-back = Baggrund. 32 | graph-ring-text = Tekst. 33 | graph-line-graph = Diagram. 34 | graph-line-back = Baggrund. 35 | graph-line-frame = Ramme. 36 | graph-network-download = Download. 37 | graph-network-upload = Upload. 38 | graph-network-back = Baggrund. 39 | graph-network-frame = Ramme. 40 | graph-disks-write = Skriv. 41 | graph-disks-read = Læs. 42 | graph-disks-back = Baggrund. 43 | graph-disks-frame = Ramme. 44 | settings-subpage-back = Tilbage 45 | settings-subpage-general = Generelle indstillinger 46 | enable-net-combined = Kombinér download og upload 47 | settings-monospace_font = Monospace-skrifttype til etiketter 48 | disks-title = Diskbelastning 49 | disks-title-combined = Diskbelastning i byte pr. sekund 50 | disks-title-write = Disk skrivning i byte pr. sekund 51 | disks-title-read = Disk læsning i byte pr. sekund 52 | enable-disks-chart = Vis diskdiagram 53 | enable-disks-label = Vis disketiket 54 | enable-disks-combined = Kombinér skriv og læs for disk 55 | choose-sysmon = Systemovervågning 56 | memory-as-percentage = Som procent 57 | settings-cpu = CPU 58 | settings-memory = Hukommelse 59 | settings-network = Netværk 60 | settings-disks = Disk 61 | -------------------------------------------------------------------------------- /i18n/en/cosmic_applet_minimon.ftl: -------------------------------------------------------------------------------- 1 | cpu-title = CPU Load Average 2 | cpu-temperature-title = CPU Temperature 3 | temperature-unit = Temperature unit 4 | enable-chart = Show chart 5 | enable-label = Show label 6 | net-title = Network load 7 | net-title-combined = Network load in bits per second 8 | net-title-dl = Network download in bits per second 9 | net-title-ul = Network upload in bits per second 10 | memory-title = Memory Usage 11 | use-adaptive = Use adaptive scale 12 | net-bandwidth = Network speed 13 | refresh-rate = Refresh rate (seconds) 14 | change-colors = Change colors 15 | change-label-size = Label size 16 | colorpicker-colors = colors 17 | colorpicker-defaults = Defaults 18 | colorpicker-accent = Accent 19 | colorpicker-cancel = Cancel 20 | colorpicker-save = Save 21 | graph-type-ring = Ring 22 | graph-type-line = Line 23 | sensor-cpu = CPU 24 | sensor-cpu-temperature = CPU Temperature 25 | sensor-network = Network 26 | sensor-memory = Memory 27 | sensor-disks = Disk 28 | graph-ring-r1 = Ring1. 29 | graph-ring-r2 = Ring2. 30 | graph-ring-back = Background. 31 | graph-ring-text = Text. 32 | graph-line-graph = Graph. 33 | graph-line-back = Background. 34 | graph-line-frame = Frame. 35 | graph-network-download = Download. 36 | graph-network-upload = Upload. 37 | graph-network-back = Background. 38 | graph-network-frame = Frame. 39 | graph-disks-write = Write. 40 | graph-disks-read = Read. 41 | graph-disks-back = Background. 42 | graph-disks-frame = Frame. 43 | settings-subpage-back = Back 44 | settings-subpage-general = General settings 45 | enable-net-combined = Combine download and upload 46 | settings-monospace_font = Monospace font for labels 47 | disks-title = Disk load 48 | disks-title-combined = Disk load in bytes per second 49 | disks-title-write = Disk write in bytes per second 50 | disks-title-read = Disk read in bytes per second 51 | enable-disks-combined = Combine disk Write and Read 52 | choose-sysmon = System Monitor 53 | memory-as-percentage = As percentage 54 | settings-cpu = CPU 55 | settings-cpu-temperature = CPU Temperature 56 | settings-memory = Memory 57 | settings-network = Network 58 | settings-disks = Disk 59 | settings-gpu = GPU 60 | gpu-title = Graphics 61 | gpu-title-usage = GPU load 62 | gpu-title-temperature = GPU Temperature 63 | gpu-title-vram = VRAM load 64 | sensor-gpu = GPU 65 | sensor-vram = VRAM 66 | settings-disable-on-battery = Disable monitoring while on battery to allow the GPU to sleep if possible 67 | enable-symbols = Show symbols 68 | settings-gpu-stack-labels = If GPU and VRAM labels enabled stack them vertically 69 | settings-tight-spacing = Compact spacing 70 | cpu-temp-amd = For AMD processors shows 'Tdie' (true die temperature) if found, otherwise show 'Tctl' (a temperature with an offset set by AMD). 71 | cpu-temp-intel = For Intel processors shows single highest temperature found across all sensors/cores. 72 | sensor-gpu-temp = GPU temperature 73 | cpu-no-decimals = Round to nearest integer 74 | inhibit-sleep = Inhibit screen/system sleep: 75 | minutes-left = min left 76 | -------------------------------------------------------------------------------- /i18n/nl/cosmic_applet_minimon.ftl: -------------------------------------------------------------------------------- 1 | cpu-title = Gemiddeld CPU-gebruik 2 | cpu-temperature-title = CPU-temperatuur 3 | cpu-temperature-unit = Eenheid temperatuur 4 | enable-cpu-chart = CPU-grafiek tonen 5 | enable-cpu-label = CPU-label tonen 6 | enable-cpu-temperature-chart = grafiek CPU-temperatuur tonen 7 | enable-cpu-temperature-label = label CPU-temperatuur tonen 8 | net-title = Netwerkgebruik 9 | net-title-combined = Netwerkgebruik in bit/s 10 | net-title-dl = Netwerkdownloads in bit/s 11 | net-title-ul = Networkuploads in bit/s 12 | enable-net-chart = Netwerkgrafiek tonen 13 | enable-net-label = Netwerklabel tonen 14 | memory-title = Geheugengebruik 15 | enable-memory-chart = Geheugengrafiek tonen 16 | enable-memory-label = Geheugenlabel tonen 17 | use-adaptive = Schaal past zich aan de inhoud aan 18 | net-bandwidth = Netwerksnelheid 19 | refresh-rate = Refreshrate (in seconde) 20 | change-colors = Kleuren van de assen wijzigen 21 | change-label-size = Labelgrootte 22 | colorpicker-colors = Kleuren 23 | colorpicker-defaults = Standaard 24 | colorpicker-accent = Accentkleur 25 | colorpicker-cancel = Annuleren 26 | colorpicker-save = Opslaan 27 | graph-type-ring = Ring 28 | graph-type-line = Lijn 29 | sensor-cpu = CPU 30 | sensor-cpu-temperature = CPU-temperatuur 31 | sensor-network = Netwerk 32 | sensor-memory = Geheugen 33 | sensor-disks = Schijf 34 | graph-ring-r1 = Ring1. 35 | graph-ring-r2 = Ring2. 36 | graph-ring-back = Achtergrond. 37 | graph-ring-text = Tekst. 38 | graph-line-graph = Grafiek. 39 | graph-line-back = Achtergrond. 40 | graph-line-frame = Frame. 41 | graph-network-download = Downloads. 42 | graph-network-upload = Uploads. 43 | graph-network-back = Achtergrond. 44 | graph-network-frame = Frame. 45 | graph-disks-write = Schrijven. 46 | graph-disks-read = Lezen. 47 | graph-disks-back = Achtergrond. 48 | graph-disks-frame = Frame. 49 | settings-subpage-back = Terug 50 | settings-subpage-general = Algemene instellingen 51 | enable-net-combined = Downloads en uploads gecombineerd weergeven 52 | settings-monospace_font = Monospace lettertype voor labels gebruiken 53 | disks-title = Schijfgebruik 54 | disks-title-combined = Schijfgebruik in bit/s 55 | disks-title-write = Schrijven in bit/s 56 | disks-title-read = Lezen in bit/s 57 | enable-disks-chart = Grafiek schijfgebruik tonen 58 | enable-disks-label = Label schijfgebruik tonen 59 | enable-disks-combined = Lezen en schrijven gecombineerd weergeven 60 | choose-sysmon = Systeemmonitor 61 | memory-as-percentage = Als percentage 62 | settings-cpu = CPU 63 | settings-cpu-temperature = CPU-temperatuur 64 | settings-memory = Geheugen 65 | settings-network = Netwerk 66 | settings-disks = Schijf 67 | settings-gpu = GPU 68 | enable-gpu-chart = GPU-grafiek tonen 69 | enable-gpu-label = GPU-label tonen 70 | enable-vram-chart = VRAM-grafiek tonen 71 | enable-vram-label = VRAM-label tonen 72 | gpu-title = Grafische kaart 73 | gpu-title-usage = GPU-gebruik 74 | gpu-title-vram = VRAM-gebruik 75 | sensor-gpu = GPU 76 | sensor-vram = VRAM 77 | settings-disable-on-battery = Systeemmonitor pauzeren als u uw laptop van het stroomnet ontkoppelt zodat de GPU in slaapmodus kan gaan, indien mogelijk 78 | enable-symbols = Symbolen tonen 79 | settings-gpu-stack-labels = GPU- en VRAM-labels verticaal weergeven, indien van toepassing 80 | settings-tight-spacing = Compacte weergave 81 | cpu-temp-amd = Indien beschikbaar geeft dit de 'Tdie' (echte die-temperatuur) weer voor AMD-processoren, en anders de 'Tctl' (de temperatuur met een offset ingesteld door AMD). 82 | cpu-temp-intel = Toont voor Intel-processoren de hoogste temperatuur gemeten bij alle sensoren/cores. 83 | -------------------------------------------------------------------------------- /i18n/pt-BR/cosmic_applet_minimon.ftl: -------------------------------------------------------------------------------- 1 | cpu-title = Média de carga da CPU 2 | cpu-temperature-title = Temperatura da CPU 3 | cpu-temperature-unit = Unidade de temperatura 4 | enable-cpu-chart = Mostrar gráfico da CPU 5 | enable-cpu-label = Mostrar legenda da CPU 6 | enable-cpu-temperature-chart = Mostrar gráfico de temperatura da CPU 7 | enable-cpu-temperature-label = Mostrar legenda de temperatura da CPU 8 | net-title = Carga de rede 9 | net-title-combined = Carga de rede em bits por segundo 10 | net-title-dl = Downloads em bits por segundo 11 | net-title-ul = Uploads in bits por segundo 12 | enable-net-chart = Mostrar gráfico da rede 13 | enable-net-label = Mostrar legenda da rede 14 | memory-title = Uso de Memória 15 | enable-memory-chart = Mostrar gráfico de memória 16 | enable-memory-label = Mostrar legenda de memória 17 | use-adaptive = Usar escala adaptável 18 | net-bandwidth = Velocidade da rede 19 | refresh-rate = Taxa de atualização (em segundos) 20 | change-colors = Alterar cores 21 | change-label-size = Tamanho da legenda 22 | colorpicker-colors = cores 23 | colorpicker-defaults = Padrões 24 | colorpicker-accent = Realce 25 | colorpicker-cancel = Cancelar 26 | colorpicker-save = Salvar 27 | graph-type-ring = Anel 28 | graph-type-line = Linha 29 | sensor-cpu = CPU 30 | sensor-cpu-temperature = Temperatura da CPU 31 | sensor-network = Rede 32 | sensor-memory = Memória 33 | sensor-disks = Disco 34 | graph-ring-r1 = Anel1. 35 | graph-ring-r2 = Anel2. 36 | graph-ring-back = Fundo. 37 | graph-ring-text = Texto. 38 | graph-line-graph = Gráfico. 39 | graph-line-back = Fundo. 40 | graph-line-frame = Quadro. 41 | graph-network-download = Download. 42 | graph-network-upload = Upload. 43 | graph-network-back = Fundo. 44 | graph-network-frame = Quadro. 45 | graph-disks-write = Escrita. 46 | graph-disks-read = Leitura. 47 | graph-disks-back = Fundo. 48 | graph-disks-frame = Quadro. 49 | settings-subpage-back = Voltar 50 | settings-subpage-general = Configurações gerais 51 | enable-net-combined = Combinar download e upload 52 | settings-monospace_font = Fonte monoespaçada para legendas 53 | disks-title = Carga de disco 54 | disks-title-combined = Carga de disco em bytes por segundo 55 | disks-title-write = Escrita em disco em bytes por segundo 56 | disks-title-read = Leitura de disco em bytes por segundo 57 | enable-disks-chart = Mostrar gráfico de disco 58 | enable-disks-label = Mostrar legenda de disco 59 | enable-disks-combined = Combinar leitura e escrita de disco 60 | choose-sysmon = Monitor do Sistema 61 | memory-as-percentage = Mostrar como porcentagem 62 | settings-cpu = CPU 63 | settings-cpu-temperature = Temperatura da CPU 64 | settings-memory = Memória 65 | settings-network = Rede 66 | settings-disks = Disco 67 | settings-gpu = GPU 68 | enable-gpu-chart = Mostrar gráfico da GPU 69 | enable-gpu-label = Mostrar legenda da GPU 70 | enable-vram-chart = Mostrar gráfico da VRAM 71 | enable-vram-label = Mostrar legenda da VRAM 72 | gpu-title = Gráficos 73 | gpu-title-usage = Carga de GPU 74 | gpu-title-vram = Carga de VRAM 75 | sensor-gpu = GPU 76 | sensor-vram = VRAM 77 | settings-disable-on-battery = Desabilitar o monitoramento enquanto estiver usando a bateria para permitir que a GPU entre em repouso, se possível 78 | enable-symbols = Mostrar símbolos 79 | settings-gpu-stack-labels = Se as legendas de GPU e VRAM estiverem habilitadas, empilhe-as verticalmente 80 | settings-tight-spacing = Espaçamento compacto 81 | cpu-temp-amd = Para processadores AMD, mostrar 'Tdie' (true die temperature) se encontrado, caso contrário, mostrar 'Tctl' (uma temperatura com um deslocamento definido pela AMD). 82 | cpu-temp-intel = Para processadores Intel, mostrar a temperatura mais alta encontrada em todos os sensores/núcleos. 83 | -------------------------------------------------------------------------------- /i18n/sv/cosmic_applet_minimon.ftl: -------------------------------------------------------------------------------- 1 | cpu-title = Genomsnittlig CPU-belastning 2 | cpu-temperature-title = CPU temperatur 3 | temperature-unit = Temperatur enhet 4 | enable-chart = Visa diagram 5 | enable-label = Visa etikett 6 | net-title = Nätverksbelastning 7 | net-title-combined = Nätverksbelastning i bitar per sekund 8 | net-title-dl = Nätverksnedladdning i bitar per sekund 9 | net-title-ul = Nätverksuppladning i bitar per sekund 10 | memory-title = Minnesanvändning 11 | use-adaptive = Använd adaptiv skala 12 | net-bandwidth = Nätverkshastighet 13 | refresh-rate = Uppdateringshastighet (sekunder) 14 | change-colors = Ändra färger 15 | change-label-size = Etikettstorlek 16 | enable-net-chart = Visa nätverksdiagram 17 | enable-net-label = Visa nätverksetikett 18 | enable-memory-chart = Visa minnesdiagram 19 | enable-memory-label = Visa minnesetikett 20 | change-colors = Ändra färger 21 | colorpicker-colors = färger 22 | colorpicker-defaults = Standardvärden 23 | colorpicker-accent = Accent 24 | colorpicker-cancel = Avbryt 25 | colorpicker-save = Spara 26 | graph-type-ring = Ring 27 | graph-type-line = Linje 28 | sensor-cpu = CPU 29 | sensor-network = Nätverk 30 | sensor-memory = Minne 31 | sensor-disks = Disk 32 | graph-ring-r1 = Ring1. 33 | graph-ring-r2 = Ring2. 34 | graph-ring-back = Bakgrund. 35 | graph-ring-text = Text. 36 | graph-line-graph = Graf. 37 | graph-line-back = Bakgrund. 38 | graph-line-frame = Ram. 39 | graph-network-download = Nedladdning. 40 | graph-network-upload = Uppladning. 41 | graph-network-back = Bakgrund. 42 | graph-network-frame = Ram. 43 | graph-disks-write = Skriv. 44 | graph-disks-read = Läs. 45 | graph-disks-back = Bakgrund. 46 | graph-disks-frame = Ram. 47 | settings-subpage-back = Tillbaka 48 | settings-subpage-general = Allmänna inställningar 49 | enable-net-combined = Kombinera nedladdning och uppladdning 50 | settings-monospace_font = Monospace-teckensnitt för etiketter 51 | disks-title = Hårddiskbelastning 52 | disks-title-combined = Hårddiskbelastning i byte per sekund 53 | disks-title-write = Diskskrivning i byte per sekund 54 | disks-title-read = Disk läst i byte per sekund 55 | enable-disks-combined = Kombinera disk läs och skriv 56 | choose-sysmon = Systemövervakare 57 | memory-as-percentage = Som procent 58 | settings-cpu = CPU 59 | settings-cpu-temperature = CPU temperatur 60 | settings-memory = Minne 61 | settings-network = Nätverk 62 | settings-disks = Disk 63 | settings-gpu = GPU 64 | gpu-title = Grafik 65 | gpu-title-usage = GPU belastning 66 | gpu-title-temperature = GPU temperatur 67 | gpu-title-vram = VRAM belastning 68 | sensor-gpu = GPU 69 | sensor-vram = VRAM 70 | settings-disable-on-battery = Inaktivera övervakning vid batteridrift för att låta GPU:n gå i viloläge om möjligt 71 | settings-gpu-stack-labels = Om GPU och VRAM-etiketter är aktiverade staplas de vertikalt 72 | settings-tight-spacing = Kompakt avstånd 73 | cpu-temp-amd = För AMD-processorer visar "Tdie" (true die temperature) om den hittas, annars visas "Tctl" (en temperatur med en offset inställd av AMD). 74 | cpu-temp-intel = För Intel-processorer visas den högsta temperaturen som hittats över alla sensorer/kärnor. 75 | sensor-gpu-temp = GPU temperatur 76 | cpu-no-decimals = Avrunda till närmaste heltal 77 | inhibit-sleep = Hindra skärmens/systemets viloläge: 78 | minutes-left = min kvar 79 | enable-symbols = Visa symboler 80 | enable-disks-chart = Visa disk diagram 81 | enable-disks-label = Visa disk etikett 82 | enable-disks-combined = Kombinera disk skrivning och läs 83 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | name := `grep -m 1 -oP '(?<=).*?(?=)' $(ls ./res/*.xml | head -n 1)` 2 | 3 | architecture := if arch() == "x86_64" { "amd64" } else { arch() } 4 | version := `sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.toml | head -1` 5 | debname := name+'_'+version+'_'+architecture 6 | debdir := debname / 'DEBIAN' 7 | debcontrol := debdir / 'control' 8 | 9 | id := `grep -m 1 -oP '(?<=).*?(?=)' $(ls ./res/*.xml | head -n 1)` 10 | summary := `grep -m 1 -oP '(?<=).*?(?=)' $(ls ./res/*.xml | head -n 1)` 11 | dev_name := `grep -m 1 -oP '(?<=).*?(?=)' $(ls ./res/*.xml | head -n 1)` 12 | email := `grep -m 1 -oP '(?<=).*?(?=)' $(ls ./res/*.xml | head -n 1)` 13 | 14 | export APPID := id 15 | 16 | rootdir := '' 17 | prefix := '/usr' 18 | flatpak-prefix := '/app' 19 | 20 | base-dir := absolute_path(clean(rootdir / prefix)) 21 | flatpak-base-dir := absolute_path(clean(rootdir / flatpak-prefix)) 22 | 23 | export INSTALL_DIR := base-dir / 'share' 24 | 25 | bin-src := 'target' / 'release' / name 26 | bin-dst := base-dir / 'bin' / name 27 | flatpak-bin-dst := flatpak-base-dir / 'bin' / name 28 | 29 | desktop := APPID + '.desktop' 30 | desktop-src := 'res' / desktop 31 | desktop-dst := clean(rootdir / prefix) / 'share' / 'applications' / desktop 32 | 33 | metainfo := APPID + '.metainfo.xml' 34 | metainfo-src := 'res' / metainfo 35 | metainfo-dst := clean(rootdir / prefix) / 'share' / 'metainfo' / metainfo 36 | 37 | icons-src := 'res' / 'icons' 38 | icons-dst := clean(rootdir / prefix) / 'share' / 'icons' / 'hicolor' / 'scalable' 39 | 40 | default: build-release 41 | 42 | # Runs `cargo clean` 43 | clean: 44 | cargo clean 45 | 46 | # Removes vendored dependencies 47 | clean-vendor: 48 | rm -rf .cargo vendor vendor.tar 49 | 50 | # `cargo clean` and removes vendored dependencies 51 | clean-dist: clean clean-vendor 52 | 53 | # Compiles with debug profile 54 | build-debug *args: 55 | cargo build {{args}} 56 | 57 | # Compiles with release profile and caffeine feature 58 | build-caffeine *args: (build-debug '--release --features caffeine' args) 59 | 60 | # Compiles with release profile 61 | build-release *args: (build-debug '--release' args) 62 | 63 | # Compiles release profile with vendored dependencies 64 | build-vendored *args: vendor-extract (build-release '--frozen --offline' args) 65 | 66 | # Runs a clippy check 67 | check *args: 68 | cargo clippy --all-features {{args}} -- -W clippy::pedantic 69 | 70 | # Runs a clippy check with JSON message format 71 | check-json: (check '--message-format=json') 72 | 73 | dev *args: 74 | cargo fmt 75 | just run {{args}} 76 | 77 | # Run with debug logs 78 | run *args: 79 | env RUST_LOG=cosmic_tasks=info RUST_BACKTRACE=full cargo run --release {{args}} 80 | 81 | # Installs files 82 | install: 83 | strip {{bin-src}} 84 | install -Dm0755 {{bin-src}} {{bin-dst}} 85 | install -Dm0644 {{desktop-src}} {{desktop-dst}} 86 | install -Dm0644 {{metainfo-src}} {{metainfo-dst}} 87 | for svg in {{icons-src}}/apps/*.svg; do \ 88 | install -D "$svg" "{{icons-dst}}/apps/$(basename $svg)"; \ 89 | done 90 | 91 | # Uninstalls installed files 92 | uninstall: 93 | rm {{bin-dst}} 94 | rm {{desktop-dst}} 95 | rm {{metainfo-dst}} 96 | for svg in {{icons-src}}/apps/*.svg; do \ 97 | rm "{{icons-dst}}/apps/$(basename $svg)"; \ 98 | done 99 | 100 | # Vendor dependencies locally 101 | vendor: 102 | #!/usr/bin/env bash 103 | mkdir -p .cargo 104 | cargo vendor --sync Cargo.toml | head -n -1 > .cargo/config.toml 105 | echo 'directory = "vendor"' >> .cargo/config.toml 106 | echo >> .cargo/config.toml 107 | echo '[env]' >> .cargo/config.toml 108 | if [ -n "${SOURCE_DATE_EPOCH}" ] 109 | then 110 | source_date="$(date -d "@${SOURCE_DATE_EPOCH}" "+%Y-%m-%d")" 111 | echo "VERGEN_GIT_COMMIT_DATE = \"${source_date}\"" >> .cargo/config.toml 112 | fi 113 | if [ -n "${SOURCE_GIT_HASH}" ] 114 | then 115 | echo "VERGEN_GIT_SHA = \"${SOURCE_GIT_HASH}\"" >> .cargo/config.toml 116 | fi 117 | tar pcf vendor.tar .cargo vendor 118 | rm -rf .cargo vendor 119 | 120 | # Extracts vendored dependencies 121 | vendor-extract: 122 | rm -rf vendor 123 | tar pxf vendor.tar 124 | 125 | deb: 126 | strip {{bin-src}} 127 | install -D {{bin-src}} {{debname}}{{bin-dst}} 128 | install -D {{desktop-src}} {{debname}}{{desktop-dst}} 129 | for svg in {{icons-src}}/apps/*.svg; do \ 130 | install -D "$svg" "{{debname}}{{icons-dst}}/apps/$(basename $svg)"; \ 131 | done 132 | mkdir -p {{debdir}} 133 | echo "Package: {{name}}" > {{debcontrol}} 134 | echo "Version: {{version}}" >> {{debcontrol}} 135 | echo "Architecture: {{architecture}}" >> {{debcontrol}} 136 | echo "Maintainer: {{dev_name}} <{{email}}>" >> {{debcontrol}} 137 | echo "Description: {{summary}}" >> {{debcontrol}} 138 | dpkg-deb --build --root-owner-group {{debname}} 139 | rm -Rf {{debname}}/ 140 | 141 | rpmarch := arch() 142 | rpmname := name + '-' + version + '-1.' + rpmarch 143 | rpmdir := rpmname / 'BUILDROOT' 144 | rpminstall := rpmdir / prefix 145 | rpm_bin_dst := rpminstall / 'bin' / name 146 | rpm_desktop_dst := rpminstall / 'share' / 'applications' / desktop 147 | rpm_metainfo_dst := rpminstall / 'share' / 'metainfo' / metainfo 148 | rpm_icons_dst := rpminstall / 'share' / 'icons' / 'hicolor' / 'scalable' / 'apps' 149 | 150 | rpm: 151 | strip {{bin-src}} 152 | install -D {{bin-src}} {{rpm_bin_dst}} 153 | install -D {{desktop-src}} {{rpm_desktop_dst}} 154 | install -D {{metainfo-src}} {{rpm_metainfo_dst}} 155 | for svg in {{icons-src}}/apps/*.svg; do \ 156 | install -D "$svg" "{{rpm_icons_dst}}/$(basename $svg)"; \ 157 | done 158 | 159 | mkdir -p {{rpmname}} 160 | echo "Name: {{name}}" > {{rpmname}}/spec.spec 161 | echo "Version: {{version}}" >> {{rpmname}}/spec.spec 162 | echo "Release: 1%{?dist}" >> {{rpmname}}/spec.spec 163 | echo "Summary: {{summary}}" >> {{rpmname}}/spec.spec 164 | echo "" >> {{rpmname}}/spec.spec 165 | echo "License: GPLv3" >> {{rpmname}}/spec.spec 166 | echo "Group: Applications/Utilities" >> {{rpmname}}/spec.spec 167 | echo "%description" >> {{rpmname}}/spec.spec 168 | echo "{{summary}}" >> {{rpmname}}/spec.spec 169 | echo "" >> {{rpmname}}/spec.spec 170 | echo "%files" >> {{rpmname}}/spec.spec 171 | echo "%defattr(-,root,root,-)" >> {{rpmname}}/spec.spec 172 | echo "{{prefix}}/bin/{{name}}" >> {{rpmname}}/spec.spec 173 | echo "{{prefix}}/share/applications/{{desktop}}" >> {{rpmname}}/spec.spec 174 | echo "{{prefix}}/share/metainfo/{{metainfo}}" >> {{rpmname}}/spec.spec 175 | echo "{{prefix}}/share/icons/hicolor/scalable/apps/*.svg" >> {{rpmname}}/spec.spec 176 | 177 | rpmbuild -bb --buildroot="$(pwd)/{{rpmdir}}" {{rpmname}}/spec.spec \ 178 | --define "_rpmdir $(pwd)" \ 179 | --define "_topdir $(pwd)/{{rpmname}}" \ 180 | --define "_buildrootdir $(pwd)/{{rpmdir}}" 181 | 182 | rm -rf {{rpmname}} {{rpmdir}} 183 | mv x86_64/* . 184 | rmdir x86_64 185 | -------------------------------------------------------------------------------- /mini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmic-utils/minimon-applet/8d22a4fba6470c44f0cf719c99a8177d8275a55c/mini -------------------------------------------------------------------------------- /res/icons/apps/io.github.cosmic-utils.cosmic-applet-minimon-gpu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /res/icons/apps/io.github.cosmic-utils.cosmic-applet-minimon-harddisk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /res/icons/apps/io.github.cosmic-utils.cosmic-applet-minimon-network.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /res/icons/apps/io.github.cosmic-utils.cosmic-applet-minimon-ram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /res/icons/apps/io.github.cosmic-utils.cosmic-applet-minimon-temperature.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /res/icons/apps/io.github.cosmic-utils.cosmic-applet-minimon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /res/io.github.cosmic-utils.cosmic-applet-minimon.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Minimon 3 | Exec=cosmic-applet-minimon %F 4 | Terminal=false 5 | Type=Application 6 | StartupNotify=true 7 | NoDisplay=true 8 | Icon=io.github.cosmic-utils.cosmic-applet-minimon-symbolic 9 | Categories=COSMIC 10 | Keywords=Monitor;System;CPU;GPU;Memory;Network;Disk; 11 | X-CosmicApplet=true 12 | X-CosmicHoverPopup=Auto 13 | -------------------------------------------------------------------------------- /res/io.github.cosmic-utils.cosmic-applet-minimon.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.github.cosmic-utils.cosmic-applet-minimon 4 | CC0-1.0 5 | GPL-3.0-only 6 | COSMIC 7 | hyperchaotic 8 | hyperchaotic@gmail.com 9 | Minimon 10 | A System Monitor applet for COSMIC 11 | 12 |

A lightweight hardware monitoring applet for the COSMIC desktop environment, supporting GPU, network, memory, and disk usage.

13 |
14 | io.github.cosmic-utils.cosmic-applet-minimon.desktop 15 | io.github.cosmic-utils.cosmic-applet-minimon 16 | 17 | 18 | cosmic-applet-minimon 19 | 20 | 21 |
22 | -------------------------------------------------------------------------------- /src/charts/heat.rs: -------------------------------------------------------------------------------- 1 | use cosmic::iced::mouse::Cursor; 2 | use cosmic::iced::{Point, Renderer}; 3 | use cosmic::iced::{Rectangle, Size}; 4 | use cosmic::iced_widget::canvas::Geometry; 5 | use cosmic::theme; 6 | use cosmic::widget::canvas::{self, Stroke, Style, path}; 7 | use std::collections::VecDeque; 8 | 9 | use cosmic::iced::Color; 10 | 11 | use crate::app::Message; 12 | use crate::config::GraphColors; 13 | 14 | use super::GraphColorsIced; 15 | 16 | #[derive(Debug)] 17 | pub struct HeatChart { 18 | pub steps: usize, 19 | pub samples: VecDeque, 20 | pub max_y: Option, 21 | pub colors: GraphColorsIced, 22 | } 23 | 24 | impl HeatChart { 25 | pub fn new( 26 | steps: usize, 27 | samples: &VecDeque, 28 | max: Option, 29 | colors: &GraphColors, 30 | ) -> Self { 31 | HeatChart { 32 | steps, 33 | samples: samples.clone(), 34 | max_y: max, 35 | colors: (*colors).into(), 36 | } 37 | } 38 | } 39 | 40 | impl canvas::Program for HeatChart { 41 | type State = (); 42 | 43 | fn draw( 44 | &self, 45 | _state: &Self::State, 46 | renderer: &Renderer, 47 | theme: &theme::Theme, 48 | bounds: Rectangle, 49 | _cursor: Cursor, 50 | ) -> Vec> { 51 | 52 | let frame_start = 1.5; 53 | 54 | let mut frame = canvas::Frame::new(renderer, bounds.size()); 55 | 56 | let top_left = Point::new( 57 | frame.center().x - frame.size().width / 2. + 1., 58 | frame.center().y - frame.size().height / 2. + 1., 59 | ); 60 | let bottom_right = Point::new( 61 | frame.center().x + frame.size().width / 2. - 1., 62 | frame.center().y + frame.size().height / 2. - 1., 63 | ); 64 | let scale = bottom_right - top_left; 65 | 66 | // Find max value, if not provided will scale to largest value 67 | let max_value: f64 = if let Some(m) = self.max_y { 68 | m 69 | } else { 70 | let max_point = self.samples.iter().cloned().fold(0.0, f64::max); 71 | if max_point > 0.0 { max_point } else { 100.0 } 72 | }; 73 | 74 | let corner_radius = frame.size().width.min(frame.size().height) / 7.0; 75 | 76 | let frame_color = self.colors.color2; 77 | let bg_color = self.colors.color1; 78 | 79 | frame.fill_rectangle(Point { x: frame_start, y: frame_start }, Size { 80 | width: frame.size().width - 2.0, 81 | height: frame.size().height - 2.0, 82 | }, bg_color); 83 | 84 | let step_length = scale.x / self.steps as f32; 85 | let scaling = (scale.y - 1.0) as f64 / max_value; 86 | 87 | let mut builder = path::Builder::new(); 88 | let mut shade_builder = path::Builder::new(); 89 | 90 | let mut previous_point = None; 91 | 92 | for i in 0..self.samples.len() { 93 | let sample = self.samples[i].min(max_value); 94 | let y = 0.5 + scale.y - (sample * scaling) as f32; 95 | let x = (top_left.x + step_length * i as f32).round(); 96 | let p = Point::new(x, y.round()); 97 | 98 | if i == 0 { 99 | builder.move_to(p); 100 | shade_builder.move_to(Point::new(top_left.x, bottom_right.y)); 101 | shade_builder.line_to(p); 102 | } else { 103 | builder.line_to(p); 104 | shade_builder.line_to(p); 105 | } 106 | 107 | previous_point = Some(p); 108 | } 109 | 110 | /* Bezier version, much more expensive, only looks better on big charts 111 | 112 | // Draw graph 113 | let step_length = scale.x / self.steps as f32; 114 | let scaling = (scale.y - 0.5) as f64 / max_value; 115 | 116 | let mut builder = path::Builder::new(); 117 | let mut shade_builder = path::Builder::new(); 118 | 119 | let mut previous_point: Option = None; 120 | 121 | for i in 0..self.samples.len() { 122 | let sample = self.samples[i].min(max_value); 123 | let y = 0.5 + scale.y - (sample * scaling) as f32; 124 | let x = (top_left.x + step_length * i as f32).round(); 125 | let p = Point::new(x, y.round()); 126 | 127 | if i == 0 { 128 | shade_builder.move_to(Point::new(top_left.x, bottom_right.y)); 129 | shade_builder.line_to(p); 130 | } else if let Some(prev) = previous_point { 131 | let control_prev = Point::new(prev.x + step_length * 0.5, prev.y); 132 | let control_curr = Point::new(p.x - step_length * 0.5, p.y); 133 | 134 | builder.move_to(prev); 135 | builder.bezier_curve_to(control_prev, control_curr, p); 136 | shade_builder.bezier_curve_to(control_prev, control_curr, p); 137 | } 138 | 139 | previous_point = Some(p); 140 | } 141 | */ 142 | 143 | if previous_point.is_some() { 144 | shade_builder.line_to(bottom_right); 145 | } 146 | 147 | // Draw the chart, with a gradient 148 | 149 | let linear = cosmic::widget::canvas::gradient::Linear::new( 150 | Point::new(0.0, frame.size().height), 151 | Point::new(0.0, 0.0), 152 | ) 153 | .add_stop(0.0, Color::from_rgba(1.0, 0.65, 0.0, 1.0)) 154 | .add_stop(1.0, Color::from_rgba(1.0, 0.0, 0.0, 1.0)); 155 | 156 | frame.fill( 157 | &shade_builder.build(), 158 | canvas::Fill { 159 | style: canvas::Style::Gradient(canvas::Gradient::Linear(linear)), 160 | ..Default::default() 161 | }, 162 | ); 163 | 164 | let frame_size: Size = Size { 165 | width: frame.size().width - 2.0, 166 | height: frame.size().height - 2.0, 167 | }; 168 | 169 | // Erase corners, with transparent pixels 170 | for i in 0..=corner_radius.trunc() as i32 { 171 | let mut square = path::Builder::new(); 172 | square.rounded_rectangle(Point { x: frame_start, y: frame_start }, frame_size, i.into()); 173 | frame.stroke( 174 | &square.build(), 175 | Stroke { 176 | style: Style::Solid(theme.cosmic().bg_color().into()), 177 | width: 1.0, 178 | ..Default::default() 179 | }, 180 | ); 181 | } 182 | 183 | // Draw background square 184 | let mut square = path::Builder::new(); 185 | square.rounded_rectangle(Point { x: frame_start, y: frame_start }, frame_size, corner_radius.into()); 186 | frame.stroke( 187 | &square.build(), 188 | Stroke { 189 | style: Style::Solid(frame_color), 190 | width: 1.0, 191 | ..Default::default() 192 | }, 193 | ); 194 | 195 | vec![frame.into_geometry()] 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/charts/line.rs: -------------------------------------------------------------------------------- 1 | use cosmic::iced::mouse::Cursor; 2 | use cosmic::iced::{Color, Point, Renderer}; 3 | use cosmic::iced::{Rectangle, Size}; 4 | use cosmic::iced_widget::canvas::Geometry; 5 | use cosmic::theme; 6 | use cosmic::widget::canvas::{self, Path, Stroke, Style, path}; 7 | use std::collections::VecDeque; 8 | 9 | use crate::app::Message; 10 | use crate::config::GraphColors; 11 | 12 | use super::GraphColorsIced; 13 | 14 | // Trait for numeric sample types 15 | pub trait SampleValue: Copy + PartialOrd { 16 | fn to_f64(self) -> f64; 17 | } 18 | 19 | impl SampleValue for u64 { 20 | fn to_f64(self) -> f64 { 21 | self as f64 22 | } 23 | } 24 | 25 | impl SampleValue for f64 { 26 | fn to_f64(self) -> f64 { 27 | self 28 | } 29 | } 30 | 31 | // Generic LineChart widget 32 | // * Draws a graph of the last 'steps' samples. 33 | // * Can take u64 or f64. 34 | // * Can be adaptive or take a fixed max_y. 35 | // * If samples2.len() is 0, only draws samples1 graph. 36 | #[derive(Debug)] 37 | pub struct LineChart { 38 | pub steps: usize, 39 | pub samples1: VecDeque, 40 | pub samples2: VecDeque, 41 | pub max_y: Option, 42 | pub colors: GraphColorsIced, 43 | } 44 | 45 | 46 | impl LineChart { 47 | pub fn new( 48 | steps: usize, 49 | samples1: &VecDeque, 50 | samples2: &VecDeque, 51 | max: Option, 52 | colors: &GraphColors, 53 | ) -> Self { 54 | Self { 55 | steps, 56 | samples1: samples1.clone(), 57 | samples2: samples2.clone(), 58 | max_y: max, 59 | colors: (*colors).into(), 60 | } 61 | } 62 | } 63 | 64 | // The new(..) function clones the samples and creates a new object. 65 | // Alternatively the sensor could have a LineChart member and access 66 | // the samples directly on update. 67 | impl canvas::Program for LineChart { 68 | type State = (); 69 | 70 | fn draw( 71 | &self, 72 | _state: &Self::State, 73 | renderer: &Renderer, 74 | theme: &theme::Theme, 75 | bounds: Rectangle, 76 | _cursor: Cursor, 77 | ) -> Vec> { 78 | fn draw_graph(frame: &mut canvas::Frame, path: Path, shade: Path, mut color: Color) { 79 | frame.stroke( 80 | &path, 81 | canvas::Stroke { 82 | style: canvas::Style::Solid(color), 83 | width: 1.0, 84 | line_join: canvas::LineJoin::Round, 85 | ..Default::default() 86 | }, 87 | ); 88 | 89 | color.a = 0.3; 90 | 91 | frame.fill( 92 | &shade, 93 | canvas::Fill { 94 | style: canvas::Style::Solid(color), 95 | ..Default::default() 96 | }, 97 | ); 98 | } 99 | 100 | let mut frame = canvas::Frame::new(renderer, bounds.size()); 101 | let top_left = Point::new( 102 | frame.center().x - frame.size().width / 2. + 1., 103 | frame.center().y - frame.size().height / 2. + 1., 104 | ); 105 | let bottom_right = Point::new( 106 | frame.center().x + frame.size().width / 2. - 1., 107 | frame.center().y + frame.size().height / 2. - 1., 108 | ); 109 | let scale = bottom_right - top_left; 110 | 111 | let min = scale.y as f64; 112 | let max_value = self.max_y.map(|v| v.to_f64()).unwrap_or_else(|| { 113 | let max1 = self.samples1.iter().map(|s| s.to_f64()).fold(0.0, f64::max); 114 | let max2 = self.samples2.iter().map(|s| s.to_f64()).fold(0.0, f64::max); 115 | max1.max(max2).max(min) 116 | }); 117 | 118 | let dual_graph = !self.samples2.is_empty(); 119 | 120 | let step_length = scale.x / (self.steps-1) as f32; 121 | let scaling = (scale.y - 1.0) as f64 / max_value; 122 | 123 | let mut builder1 = path::Builder::new(); 124 | let mut builder2 = path::Builder::new(); 125 | let mut shade1 = path::Builder::new(); 126 | let mut shade2 = path::Builder::new(); 127 | 128 | let len = self.samples1.len().min(self.steps); 129 | let start_index1 = self.samples1.len().saturating_sub(len); 130 | let start_index2 = self.samples2.len().saturating_sub(len); 131 | 132 | let iter1 = self.samples1.iter().skip(start_index1).take(len); 133 | let iter2 = self.samples2.iter().skip(start_index2).take(len); 134 | 135 | let mut iter2_opt = dual_graph.then_some(iter2); 136 | 137 | for (i, sample1) in iter1.enumerate() { 138 | let x = (top_left.x + step_length * i as f32).round(); 139 | let y1 = 0.5 + scale.y - (sample1.to_f64().min(max_value) * scaling) as f32; 140 | let p1 = Point::new(x, y1.round()); 141 | 142 | if i == 0 { 143 | builder1.move_to(p1); 144 | shade1.move_to(Point::new(top_left.x, bottom_right.y)); 145 | shade1.line_to(p1); 146 | } else { 147 | builder1.line_to(p1); 148 | shade1.line_to(p1); 149 | } 150 | 151 | if let Some(iter2) = iter2_opt.as_mut() { 152 | if let Some(sample2) = iter2.next() { 153 | let y2 = 0.5 + scale.y - (sample2.to_f64().min(max_value) * scaling) as f32; 154 | let p2 = Point::new(x, y2.round()); 155 | 156 | if i == 0 { 157 | builder2.move_to(p2); 158 | shade2.move_to(Point::new(top_left.x, bottom_right.y)); 159 | shade2.line_to(p2); 160 | } else { 161 | builder2.line_to(p2); 162 | shade2.line_to(p2); 163 | } 164 | } 165 | } 166 | } 167 | 168 | shade1.line_to(bottom_right); 169 | if dual_graph { 170 | shade2.line_to(bottom_right); 171 | } 172 | 173 | draw_graph( 174 | &mut frame, 175 | builder1.build(), 176 | shade1.build(), 177 | self.colors.color2, 178 | ); 179 | 180 | if dual_graph { 181 | draw_graph( 182 | &mut frame, 183 | builder2.build(), 184 | shade2.build(), 185 | self.colors.color3, 186 | ); 187 | } 188 | 189 | // Frame needs to be on mid-pixel, to avoid an anti-aliased outwashed double line 190 | let frame_start = 1.5; 191 | let frame_size = Size { 192 | width: frame.size().width - 2.0, 193 | height: frame.size().height - 2.0, 194 | }; 195 | 196 | let corner_radius = frame.size().width.min(frame.size().height) / 7.0; 197 | 198 | for i in 0..=corner_radius.trunc() as i32 { 199 | let mut square = path::Builder::new(); 200 | square.rounded_rectangle(Point { x: frame_start, y: frame_start }, frame_size, i.into()); 201 | frame.stroke( 202 | &square.build(), 203 | Stroke { 204 | style: Style::Solid(theme.cosmic().bg_color().into()), 205 | width: 1.0, 206 | ..Default::default() 207 | }, 208 | ); 209 | } 210 | 211 | let mut square = path::Builder::new(); 212 | square.rounded_rectangle(Point { x: frame_start, y: frame_start }, frame_size, corner_radius.into()); 213 | frame.stroke( 214 | &square.build(), 215 | Stroke { 216 | style: Style::Solid(self.colors.color4), 217 | width: 1.0, 218 | ..Default::default() 219 | }, 220 | ); 221 | 222 | vec![frame.into_geometry()] 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/charts/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::config::GraphColors; 2 | 3 | #[derive(Debug, Clone, Copy)] 4 | pub struct GraphColorsIced { 5 | pub color1: cosmic::iced::Color, 6 | pub color2: cosmic::iced::Color, 7 | pub color3: cosmic::iced::Color, 8 | pub color4: cosmic::iced::Color, 9 | } 10 | 11 | impl From for GraphColorsIced { 12 | fn from(colors: GraphColors) -> Self { 13 | fn to_iced_color(srgba: cosmic::cosmic_theme::palette::Srgba) -> cosmic::iced::Color { 14 | cosmic::iced::Color { 15 | r: srgba.color.red as f32 / 255.0, 16 | g: srgba.color.green as f32 / 255.0, 17 | b: srgba.color.blue as f32 / 255.0, 18 | a: srgba.alpha as f32 / 255.0, 19 | } 20 | } 21 | 22 | GraphColorsIced { 23 | color1: to_iced_color(colors.color1), 24 | color2: to_iced_color(colors.color2), 25 | color3: to_iced_color(colors.color3), 26 | color4: to_iced_color(colors.color4), 27 | } 28 | } 29 | } 30 | 31 | pub mod ring; 32 | pub mod heat; 33 | pub mod line; -------------------------------------------------------------------------------- /src/charts/ring.rs: -------------------------------------------------------------------------------- 1 | use cosmic::Renderer; 2 | use cosmic::iced::Point; 3 | use cosmic::iced::Radians; 4 | use cosmic::iced::Rectangle; 5 | use cosmic::iced::mouse::Cursor; 6 | use cosmic::theme; 7 | use cosmic::widget::canvas; 8 | use cosmic::widget::canvas::Geometry; 9 | 10 | use cosmic::widget::canvas::Path; 11 | use cosmic::widget::canvas::Text; 12 | use cosmic::widget::canvas::path::Arc; 13 | 14 | use std::f32::consts::PI; 15 | 16 | use crate::app::Message; 17 | use crate::config::GraphColors; 18 | 19 | use super::GraphColorsIced; 20 | 21 | #[derive(Debug)] 22 | pub struct RingChart { 23 | // How much if the ring is filled. 0..100 24 | pub percent: f32, 25 | 26 | //Text to display inside, if any 27 | pub text: String, 28 | pub colors: GraphColorsIced, 29 | } 30 | 31 | impl RingChart { 32 | pub fn new(percent: f32, text: &str, colors: &GraphColors) -> Self { 33 | RingChart { 34 | percent: if percent <= 100.0 { percent } else { 100.0 }, 35 | text: text.to_string(), 36 | colors: (*colors).into(), 37 | } 38 | } 39 | } 40 | 41 | impl canvas::Program for RingChart { 42 | type State = (); 43 | 44 | fn draw( 45 | &self, 46 | _state: &Self::State, 47 | renderer: &Renderer, 48 | _theme: &theme::Theme, 49 | bounds: Rectangle, 50 | _cursor: Cursor, 51 | ) -> Vec> { 52 | let mut frame = canvas::Frame::new(renderer, bounds.size()); 53 | 54 | // The starting poing of the Ring graph, bottom/6pm 55 | let starting_point = PI / 2.0; 56 | 57 | // Max height/width of chart/widget. Side length in a square 58 | let limit = bounds.width.min(bounds.height)-2.0; 59 | 60 | // Width and radius of ring 61 | let stroke_width = 0.08 * limit; 62 | let radius = (limit / 2.0) - stroke_width / 2.0; 63 | let center = Point::new(bounds.width / 2.0, bounds.height / 2.0); 64 | 65 | // Draw outer background ring segment as circle 66 | let outer_circle = Path::circle(center, radius+(stroke_width / 2.0)); 67 | frame.fill(&outer_circle, self.colors.color3); 68 | 69 | // Fill background color inside ring 70 | let inner_circle = Path::circle(center, radius - stroke_width / 2.0); 71 | frame.fill(&inner_circle, self.colors.color1); 72 | 73 | // Draw highlighted ring segment showing status/percentage 74 | let ring = Path::new(|p| { 75 | p.arc(Arc { 76 | center, 77 | radius, 78 | start_angle: Radians::from(starting_point), 79 | end_angle: Radians::from(starting_point + (PI * 2.0 * (self.percent / 100.0))), 80 | }); 81 | }); 82 | 83 | frame.stroke( 84 | &ring, 85 | canvas::Stroke { 86 | style: canvas::Style::Solid(self.colors.color4), 87 | width: stroke_width, 88 | ..Default::default() 89 | }, 90 | ); 91 | 92 | // Create text object 93 | let text = Text { 94 | content: self.text.clone(), 95 | position: center, 96 | color: self.colors.color2, 97 | size: cosmic::iced::Pixels(radius * 0.93), 98 | horizontal_alignment: cosmic::iced::alignment::Horizontal::Center, 99 | vertical_alignment: cosmic::iced::alignment::Vertical::Center, 100 | ..Default::default() 101 | }; 102 | 103 | frame.fill_text(text); 104 | 105 | vec![frame.into_geometry()] 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/colorpicker.rs: -------------------------------------------------------------------------------- 1 | use cosmic::iced::Background; 2 | use cosmic::iced::alignment::Horizontal; 3 | use cosmic::iced::{ 4 | Alignment, 5 | widget::{column, row}, 6 | }; 7 | use cosmic::{Element, cosmic_theme::palette::Srgba}; 8 | use std::rc::Rc; 9 | 10 | use cosmic::{ 11 | iced::{ 12 | Color, Length, Radians, 13 | gradient::{ColorStop, Linear}, 14 | }, 15 | theme, 16 | widget::{ 17 | self, 18 | slider::{self, HandleShape}, 19 | }, 20 | }; 21 | use theme::iced::Slider; 22 | 23 | use crate::app::Message; 24 | use crate::config::{ColorVariant, DeviceKind, GraphColors}; 25 | use crate::fl; 26 | use log::info; 27 | 28 | const RED_RECT: &str = ""; 29 | const GREEN_RECT: &str = ""; 30 | const BLUE_RECT: &str = ""; 31 | const ALPHA_RECT: &str = "\ 32 | \ 33 | \ 34 | \ 35 | "; 36 | 37 | use std::sync::{LazyLock, Mutex}; 38 | 39 | pub static COLOR_STOPS_RED_LOW: LazyLock> = LazyLock::new(|| { 40 | Mutex::new([ 41 | ColorStop { 42 | offset: 0.0, 43 | color: Color::from_rgb(0.0, 0.0, 0.0), 44 | }, 45 | ColorStop { 46 | offset: 1.0, 47 | color: Color::from_rgb(0.0, 0.0, 0.0), 48 | }, 49 | ]) 50 | }); 51 | 52 | pub static COLOR_STOPS_RED_HIGH: LazyLock> = LazyLock::new(|| { 53 | Mutex::new([ 54 | ColorStop { 55 | offset: 0.0, 56 | color: Color::from_rgb(0.0, 0.0, 0.0), 57 | }, 58 | ColorStop { 59 | offset: 1.0, 60 | color: Color::from_rgb(1.0, 0.0, 0.0), 61 | }, 62 | ]) 63 | }); 64 | 65 | pub static COLOR_STOPS_GREEN_LOW: LazyLock> = LazyLock::new(|| { 66 | Mutex::new([ 67 | ColorStop { 68 | offset: 0.0, 69 | color: Color::from_rgb(0.0, 0.0, 0.0), 70 | }, 71 | ColorStop { 72 | offset: 1.0, 73 | color: Color::from_rgb(0.0, 0.0, 0.0), 74 | }, 75 | ]) 76 | }); 77 | 78 | pub static COLOR_STOPS_GREEN_HIGH: LazyLock> = LazyLock::new(|| { 79 | Mutex::new([ 80 | ColorStop { 81 | offset: 0.0, 82 | color: Color::from_rgb(0.0, 0.0, 0.0), 83 | }, 84 | ColorStop { 85 | offset: 1.0, 86 | color: Color::from_rgb(0.0, 1.0, 0.0), 87 | }, 88 | ]) 89 | }); 90 | 91 | pub static COLOR_STOPS_BLUE_LOW: LazyLock> = LazyLock::new(|| { 92 | Mutex::new([ 93 | ColorStop { 94 | offset: 0.0, 95 | color: Color::from_rgb(0.0, 0.0, 0.0), 96 | }, 97 | ColorStop { 98 | offset: 1.0, 99 | color: Color::from_rgb(0.0, 0.0, 0.0), 100 | }, 101 | ]) 102 | }); 103 | 104 | pub static COLOR_STOPS_BLUE_HIGH: LazyLock> = LazyLock::new(|| { 105 | Mutex::new([ 106 | ColorStop { 107 | offset: 0.0, 108 | color: Color::from_rgb(0.0, 0.0, 0.0), 109 | }, 110 | ColorStop { 111 | offset: 1.0, 112 | color: Color::from_rgb(0.0, 0.0, 1.0), 113 | }, 114 | ]) 115 | }); 116 | 117 | const ERROR: &str = " 118 | 119 | "; 120 | 121 | pub trait DemoGraph { 122 | fn demo(&self) -> String; 123 | fn colors(&self) -> GraphColors; 124 | fn set_colors(&mut self, colors: GraphColors); 125 | fn color_choices(&self) -> Vec<(&'static str, ColorVariant)>; 126 | fn id(&self) -> Option; 127 | } 128 | 129 | /// Data for managing the `ColorPicker` dialog 130 | pub struct ColorPicker { 131 | demo_chart: Option>, 132 | device: DeviceKind, 133 | // Current field being adjusted background/text/etc. 134 | color_variant: ColorVariant, 135 | ///Current slider values 136 | slider_red_val: u8, 137 | slider_green_val: u8, 138 | slider_blue_val: u8, 139 | slider_alpha_val: u8, 140 | } 141 | 142 | impl Default for ColorPicker { 143 | fn default() -> Self { 144 | ColorPicker { 145 | demo_chart: None, 146 | device: DeviceKind::Cpu, 147 | color_variant: ColorVariant::Color1, 148 | slider_red_val: 0, 149 | slider_green_val: 0, 150 | slider_blue_val: 0, 151 | slider_alpha_val: 0, 152 | } 153 | } 154 | } 155 | 156 | impl ColorPicker { 157 | pub fn device(&self) -> DeviceKind { 158 | self.device 159 | } 160 | 161 | pub fn active(&self) -> bool { 162 | self.demo_chart.is_some() 163 | } 164 | 165 | pub fn activate(&mut self, device: DeviceKind, demo_chart: Box) { 166 | info!("colorpicker::activate({device:?})"); 167 | self.device = device; 168 | self.color_variant = ColorVariant::Color1; 169 | self.demo_chart = Some(demo_chart); 170 | } 171 | 172 | pub fn deactivate(&mut self) { 173 | self.demo_chart = None; 174 | } 175 | 176 | // This function is largely borrowed from the PixelDoted color picker: 177 | // https://github.com/PixelDoted/cosmic-ext-color-picker 178 | fn color_slider<'b, Message>( 179 | value: u8, 180 | on_change: impl Fn(u8) -> Message + 'b, 181 | color_stops_low: &'static Mutex<[ColorStop; 2]>, 182 | color_stops_high: &'static Mutex<[ColorStop; 2]>, 183 | ) -> cosmic::Element<'b, Message> 184 | where 185 | Message: Clone + 'b, 186 | { 187 | widget::slider(0..=255, value, on_change) 188 | .width(Length::Fixed(220.0)) 189 | .step(1) 190 | .class(Slider::Custom { 191 | active: Rc::new(|t| { 192 | let cosmic = t.cosmic(); 193 | let mut a = 194 | slider::Catalog::style(t, &Slider::default(), slider::Status::Active); 195 | 196 | a.rail.backgrounds = ( 197 | Background::Gradient(cosmic::iced::Gradient::Linear( 198 | Linear::new(Radians(90.0)) 199 | .add_stops(color_stops_low.lock().unwrap().iter().copied()), 200 | )), 201 | Background::Gradient(cosmic::iced::Gradient::Linear( 202 | Linear::new(Radians(90.0)) 203 | .add_stops(color_stops_high.lock().unwrap().iter().copied()), 204 | )), 205 | ); 206 | a.rail.width = 8.0; 207 | a.handle.background = Color::TRANSPARENT.into(); 208 | a.handle.shape = HandleShape::Circle { radius: 8.0 }; 209 | a.handle.border_color = cosmic.palette.neutral_10.into(); 210 | a.handle.border_width = 4.0; 211 | a 212 | }), 213 | hovered: Rc::new(|t| { 214 | let cosmic = t.cosmic(); 215 | let mut a = 216 | slider::Catalog::style(t, &Slider::default(), slider::Status::Hovered); 217 | 218 | a.rail.backgrounds = ( 219 | Background::Gradient(cosmic::iced::Gradient::Linear( 220 | Linear::new(Radians(90.0)) 221 | .add_stops(color_stops_low.lock().unwrap().iter().copied()), 222 | )), 223 | Background::Gradient(cosmic::iced::Gradient::Linear( 224 | Linear::new(Radians(90.0)) 225 | .add_stops(color_stops_high.lock().unwrap().iter().copied()), 226 | )), 227 | ); 228 | a.rail.width = 8.0; 229 | a.handle.background = Color::TRANSPARENT.into(); 230 | a.handle.shape = HandleShape::Circle { radius: 8.0 }; 231 | a.handle.border_color = cosmic.palette.neutral_10.into(); 232 | a.handle.border_width = 4.0; 233 | a 234 | }), 235 | dragging: Rc::new(|t| { 236 | let cosmic = t.cosmic(); 237 | let mut a = 238 | slider::Catalog::style(t, &Slider::default(), slider::Status::Dragged); 239 | 240 | a.rail.backgrounds = ( 241 | Background::Gradient(cosmic::iced::Gradient::Linear( 242 | Linear::new(Radians(90.0)) 243 | .add_stops(color_stops_low.lock().unwrap().iter().copied()), 244 | )), 245 | Background::Gradient(cosmic::iced::Gradient::Linear( 246 | Linear::new(Radians(90.0)) 247 | .add_stops(color_stops_high.lock().unwrap().iter().copied()), 248 | )), 249 | ); 250 | a.rail.width = 8.0; 251 | a.handle.background = Color::TRANSPARENT.into(); 252 | a.handle.shape = HandleShape::Circle { radius: 8.0 }; 253 | a.handle.border_color = cosmic.palette.neutral_10.into(); 254 | a.handle.border_width = 4.0; 255 | a 256 | }), 257 | }) 258 | .into() 259 | } 260 | 261 | pub fn sliders(&self) -> Srgba { 262 | Srgba::from_components(( 263 | self.slider_red_val, 264 | self.slider_green_val, 265 | self.slider_blue_val, 266 | self.slider_alpha_val, 267 | )) 268 | } 269 | 270 | pub fn demo(&self) -> String { 271 | if let Some(d) = self.demo_chart.as_ref() { 272 | let demo = d.demo(); 273 | return demo; 274 | } 275 | ERROR.to_string() 276 | } 277 | 278 | pub fn update_color(&mut self, color: Srgba) { 279 | self.slider_red_val = color.red; 280 | self.slider_green_val = color.green; 281 | self.slider_blue_val = color.blue; 282 | self.slider_alpha_val = color.alpha; 283 | 284 | let dmo = self.demo_chart.as_mut().expect("ERROR: No demo svg!"); 285 | let mut col = dmo.colors(); 286 | col.set_color(color, self.color_variant); 287 | dmo.set_colors(col); 288 | 289 | // Set the shading for sliders, this is required to be static lifetime 290 | COLOR_STOPS_RED_LOW.lock().unwrap()[1].color = 291 | Color::from_rgb(f32::from(color.red) / f32::from(u8::MAX), 0.0, 0.0); 292 | COLOR_STOPS_RED_HIGH.lock().unwrap()[0].color = 293 | Color::from_rgb(f32::from(color.red) / f32::from(u8::MAX), 0.0, 0.0); 294 | 295 | COLOR_STOPS_GREEN_LOW.lock().unwrap()[1].color = 296 | Color::from_rgb(0.0, f32::from(color.green) / f32::from(u8::MAX), 0.0); 297 | COLOR_STOPS_GREEN_HIGH.lock().unwrap()[0].color = 298 | Color::from_rgb(0.0, f32::from(color.green) / f32::from(u8::MAX), 0.0); 299 | 300 | COLOR_STOPS_BLUE_LOW.lock().unwrap()[1].color = 301 | Color::from_rgb(0.0, 0.0, f32::from(color.blue) / f32::from(u8::MAX)); 302 | COLOR_STOPS_BLUE_HIGH.lock().unwrap()[0].color = 303 | Color::from_rgb(0.0, 0.0, f32::from(color.blue) / f32::from(u8::MAX)); 304 | } 305 | 306 | pub fn default_colors(&mut self) { 307 | let colors = GraphColors::new(self.device); 308 | let dmo = self.demo_chart.as_mut().expect("ERROR: No demo svg!"); 309 | dmo.set_colors(colors); 310 | self.update_color(colors.get_color(self.color_variant)); 311 | } 312 | 313 | pub fn color_variant(&self) -> ColorVariant { 314 | self.color_variant 315 | } 316 | 317 | pub fn set_color_variant(&mut self, variant: ColorVariant) { 318 | let dmo = self.demo_chart.as_mut().expect("ERROR: No demo svg!"); 319 | self.color_variant = variant; 320 | let color = dmo.colors().get_color(variant); 321 | self.update_color(color); 322 | } 323 | 324 | pub fn colors(&self) -> GraphColors { 325 | let dmo = self.demo_chart.as_ref().expect("ERROR: No demo svg!"); 326 | dmo.colors() 327 | } 328 | 329 | pub fn view_colorpicker(&self) -> Element { 330 | let color = self.sliders(); 331 | let title = format!("{} {}", self.device, fl!("colorpicker-colors")); 332 | 333 | let mut children = Vec::new(); 334 | 335 | let dmo = self.demo_chart.as_ref().expect("ERROR: No demo svg!"); 336 | children.push(widget::horizontal_space().into()); 337 | for (s, c) in dmo.color_choices() { 338 | children.push(Element::from(widget::radio( 339 | s, 340 | c, 341 | if self.color_variant() == c { 342 | Some(c) 343 | } else { 344 | None 345 | }, 346 | Message::ColorPickerSelectVariant, 347 | ))); 348 | children.push(widget::horizontal_space().into()); 349 | } 350 | 351 | let fields = cosmic::widget::row::with_children(children); 352 | 353 | let c = widget::list_column() 354 | .padding(0) 355 | .spacing(0) 356 | .add( 357 | widget::text::title2(title) 358 | .width(Length::Fill) 359 | .align_x(Horizontal::Center), 360 | ) 361 | .add( 362 | widget::svg(widget::svg::Handle::from_memory(self.demo().into_bytes())) 363 | .width(Length::Fill) 364 | .height(100), 365 | ) 366 | .add(column!( 367 | Element::from( 368 | row!( 369 | widget::horizontal_space(), 370 | widget::svg(widget::svg::Handle::from_memory(RED_RECT.as_bytes())) 371 | .height(20), 372 | widget::horizontal_space(), 373 | ColorPicker::color_slider( 374 | color.red, 375 | Message::ColorPickerSliderRedChanged, 376 | &COLOR_STOPS_RED_LOW, 377 | &COLOR_STOPS_RED_HIGH 378 | ), 379 | widget::horizontal_space(), 380 | widget::text_input("", color.red.to_string()) 381 | .width(50) 382 | .on_input(Message::ColorTextInputRedChanged), 383 | widget::horizontal_space(), 384 | ) 385 | .align_y(Alignment::Center) 386 | ), 387 | Element::from( 388 | row!( 389 | widget::horizontal_space(), 390 | widget::svg(widget::svg::Handle::from_memory(GREEN_RECT.as_bytes())) 391 | .height(20), 392 | widget::horizontal_space(), 393 | ColorPicker::color_slider( 394 | color.green, 395 | Message::ColorPickerSliderGreenChanged, 396 | &COLOR_STOPS_GREEN_LOW, 397 | &COLOR_STOPS_GREEN_HIGH 398 | ), 399 | widget::horizontal_space(), 400 | widget::text_input("", color.green.to_string()) 401 | .width(50) 402 | .on_input(Message::ColorTextInputGreenChanged), 403 | widget::horizontal_space(), 404 | ) 405 | .align_y(Alignment::Center) 406 | ), 407 | Element::from( 408 | row!( 409 | widget::horizontal_space(), 410 | widget::svg(widget::svg::Handle::from_memory(BLUE_RECT.as_bytes())) 411 | .height(20), 412 | widget::horizontal_space(), 413 | ColorPicker::color_slider( 414 | color.blue, 415 | Message::ColorPickerSliderBlueChanged, 416 | &COLOR_STOPS_BLUE_LOW, 417 | &COLOR_STOPS_BLUE_HIGH 418 | ), 419 | widget::horizontal_space(), 420 | widget::text_input("", color.blue.to_string()) 421 | .width(50) 422 | .on_input(Message::ColorTextInputBlueChanged), 423 | widget::horizontal_space(), 424 | ) 425 | .align_y(Alignment::Center) 426 | ), 427 | Element::from( 428 | row!( 429 | widget::horizontal_space(), 430 | widget::svg(widget::svg::Handle::from_memory(ALPHA_RECT.as_bytes())) 431 | .height(20), 432 | widget::horizontal_space(), 433 | widget::slider( 434 | 0..=255, 435 | color.alpha, 436 | Message::ColorPickerSliderAlphaChanged 437 | ) 438 | .width(Length::Fixed(220.0)) 439 | .step(1), 440 | widget::horizontal_space(), 441 | widget::text_input("", color.alpha.to_string()) 442 | .width(50) 443 | .on_input(Message::ColorTextInputAlphaChanged), 444 | widget::horizontal_space(), 445 | ) 446 | .align_y(Alignment::Center) 447 | ), 448 | )) 449 | .add(fields) 450 | .spacing(10) 451 | .add( 452 | row!( 453 | widget::button::standard(fl!("colorpicker-defaults")) 454 | .on_press(Message::ColorPickerDefaults), 455 | widget::button::standard(fl!("colorpicker-accent")) 456 | .on_press(Message::ColorPickerAccent), 457 | row!( 458 | widget::horizontal_space(), 459 | widget::button::destructive(fl!("colorpicker-cancel")) 460 | .on_press(Message::ColorPickerClose(false, dmo.id())), 461 | widget::button::suggested(fl!("colorpicker-save")) 462 | .on_press(Message::ColorPickerClose(true, dmo.id())) 463 | ) 464 | .width(Length::Fill) 465 | .spacing(5) 466 | .align_y(Alignment::End) 467 | ) 468 | .padding(5) 469 | .spacing(5) 470 | .width(Length::Fill), 471 | ); 472 | 473 | c.into() 474 | } 475 | } 476 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use cosmic::{ 4 | cosmic_config::{self, CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry}, 5 | cosmic_theme::palette::Srgba, 6 | }; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::{fl, sensors::TempUnit}; 10 | 11 | #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq)] 12 | pub enum ColorVariant { 13 | Color1, 14 | Color2, 15 | Color3, 16 | Color4, 17 | } 18 | 19 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 20 | pub enum GraphKind { 21 | Ring, 22 | Line, 23 | Heat, 24 | } 25 | 26 | impl From for GraphKind { 27 | fn from(index: usize) -> Self { 28 | match index { 29 | 0 => GraphKind::Ring, 30 | 1 => GraphKind::Line, 31 | 2 => GraphKind::Heat, 32 | _ => panic!("Invalid index for SvgKind"), 33 | } 34 | } 35 | } 36 | 37 | impl From for usize { 38 | fn from(kind: GraphKind) -> Self { 39 | match kind { 40 | GraphKind::Ring => 0, 41 | GraphKind::Line => 1, 42 | GraphKind::Heat => 2, 43 | } 44 | } 45 | } 46 | 47 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 48 | pub enum DeviceKind { 49 | Cpu, 50 | CpuTemp, 51 | Memory, 52 | Network(NetworkVariant), 53 | Disks(DisksVariant), 54 | Gpu, 55 | Vram, 56 | GpuTemp, 57 | } 58 | 59 | impl std::fmt::Display for DeviceKind { 60 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 61 | match self { 62 | DeviceKind::Cpu => write!(f, "{}", fl!("sensor-cpu")), 63 | DeviceKind::CpuTemp => write!(f, "{}", fl!("sensor-cpu-temperature")), 64 | DeviceKind::Memory => write!(f, "{}", fl!("sensor-memory")), 65 | DeviceKind::Network(_) => write!(f, "{}", fl!("sensor-network")), 66 | DeviceKind::Disks(_) => write!(f, "{}", fl!("sensor-disks")), 67 | DeviceKind::Gpu => write!(f, "{}", fl!("sensor-gpu")), 68 | DeviceKind::Vram => write!(f, "{}", fl!("sensor-vram")), 69 | DeviceKind::GpuTemp => write!(f, "{}", fl!("sensor-gpu-temp")), 70 | } 71 | } 72 | } 73 | 74 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, CosmicConfigEntry, PartialEq, Eq)] 75 | #[version = 1] 76 | pub struct GraphColors { 77 | pub color1: Srgba, 78 | pub color2: Srgba, 79 | pub color3: Srgba, 80 | pub color4: Srgba, 81 | } 82 | 83 | impl Default for GraphColors { 84 | fn default() -> Self { 85 | Self { 86 | color1: Srgba::from_components((0x2b, 0x2b, 0x2b, 0xff)), 87 | color2: Srgba::from_components((255, 255, 255, 255)), 88 | color3: Srgba::from_components((85, 85, 85, 255)), 89 | color4: Srgba::from_components((255, 6, 0, 255)), 90 | } 91 | } 92 | } 93 | 94 | impl GraphColors { 95 | pub fn new(kind: DeviceKind) -> Self { 96 | match kind { 97 | DeviceKind::Cpu => GraphColors::default(), 98 | DeviceKind::CpuTemp => GraphColors::default(), 99 | 100 | DeviceKind::Memory => GraphColors { 101 | color4: Srgba::from_components((187, 41, 187, 255)), 102 | ..Default::default() 103 | }, 104 | 105 | DeviceKind::Network(_) => GraphColors { 106 | color1: Srgba::from_components((0x2b, 0x2b, 0x2b, 255)), 107 | color2: Srgba::from_components((47, 141, 255, 255)), 108 | color3: Srgba::from_components((0, 255, 0, 255)), 109 | color4: Srgba::from_components((0x2b, 0x2b, 0x2b, 255)), 110 | }, 111 | 112 | DeviceKind::Disks(_) => GraphColors { 113 | color1: Srgba::from_components((0x2b, 0x2b, 0x2b, 255)), 114 | color2: Srgba::from_components((255, 102, 0, 255)), 115 | color3: Srgba::from_components((255, 255, 0, 255)), 116 | color4: Srgba::from_components((0x2b, 0x2b, 0x2b, 255)), 117 | }, 118 | DeviceKind::Gpu => GraphColors { 119 | color4: Srgba::from_components((0, 255, 0, 255)), 120 | ..Default::default() 121 | }, 122 | DeviceKind::Vram => GraphColors { 123 | color4: Srgba::from_components((0, 255, 0, 255)), 124 | ..Default::default() 125 | }, 126 | DeviceKind::GpuTemp => GraphColors { 127 | color4: Srgba::from_components((255, 95, 31, 255)), 128 | ..Default::default() 129 | }, 130 | } 131 | } 132 | 133 | pub fn set_color(&mut self, srgb: Srgba, variant: ColorVariant) { 134 | match variant { 135 | ColorVariant::Color1 => self.color1 = srgb, 136 | ColorVariant::Color2 => self.color2 = srgb, 137 | ColorVariant::Color3 => self.color3 = srgb, 138 | ColorVariant::Color4 => self.color4 = srgb, 139 | } 140 | } 141 | 142 | pub fn get_color(self, variant: ColorVariant) -> Srgba { 143 | match variant { 144 | ColorVariant::Color1 => self.color1, 145 | ColorVariant::Color2 => self.color2, 146 | ColorVariant::Color3 => self.color3, 147 | ColorVariant::Color4 => self.color4, 148 | } 149 | } 150 | } 151 | 152 | #[derive(Debug, Clone, Serialize, Deserialize, CosmicConfigEntry, PartialEq, Eq)] 153 | #[version = 1] 154 | pub struct CpuConfig { 155 | pub chart: bool, 156 | pub label: bool, 157 | pub kind: GraphKind, 158 | pub colors: GraphColors, 159 | pub no_decimals: bool, 160 | } 161 | 162 | impl Default for CpuConfig { 163 | fn default() -> Self { 164 | Self { 165 | chart: true, 166 | label: false, 167 | kind: GraphKind::Ring, 168 | colors: GraphColors::new(DeviceKind::Cpu), 169 | no_decimals: false, 170 | } 171 | } 172 | } 173 | 174 | impl CpuConfig { 175 | pub fn is_visible(&self) -> bool { 176 | self.chart || self.label 177 | } 178 | } 179 | 180 | #[derive(Debug, Clone, Serialize, Deserialize, CosmicConfigEntry, PartialEq, Eq)] 181 | #[version = 1] 182 | pub struct CpuTempConfig { 183 | pub chart: bool, 184 | pub label: bool, 185 | pub kind: GraphKind, 186 | pub colors: GraphColors, 187 | pub unit: TempUnit, 188 | } 189 | 190 | impl Default for CpuTempConfig { 191 | fn default() -> Self { 192 | Self { 193 | chart: true, 194 | label: false, 195 | kind: GraphKind::Heat, 196 | colors: GraphColors::new(DeviceKind::CpuTemp), 197 | unit: TempUnit::Celcius, 198 | } 199 | } 200 | } 201 | 202 | impl CpuTempConfig { 203 | pub fn is_visible(&self) -> bool { 204 | self.chart || self.label 205 | } 206 | } 207 | 208 | #[derive(Debug, Clone, Serialize, Deserialize, CosmicConfigEntry, PartialEq)] 209 | #[version = 1] 210 | pub struct MemoryConfig { 211 | pub chart: bool, 212 | pub label: bool, 213 | pub kind: GraphKind, 214 | pub colors: GraphColors, 215 | pub percentage: bool, 216 | } 217 | 218 | impl Default for MemoryConfig { 219 | fn default() -> Self { 220 | Self { 221 | chart: true, 222 | label: false, 223 | kind: GraphKind::Ring, 224 | colors: GraphColors::new(DeviceKind::Memory), 225 | percentage: false, 226 | } 227 | } 228 | } 229 | 230 | impl MemoryConfig { 231 | pub fn is_visible(&self) -> bool { 232 | self.chart || self.label 233 | } 234 | } 235 | 236 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] 237 | pub enum NetworkVariant { 238 | Download, 239 | Upload, 240 | Combined, 241 | } 242 | 243 | #[derive(Debug, Clone, Serialize, Deserialize, CosmicConfigEntry, PartialEq)] 244 | #[version = 1] 245 | pub struct NetworkConfig { 246 | pub chart: bool, 247 | pub label: bool, 248 | pub adaptive: bool, 249 | pub bandwidth: u64, 250 | pub unit: Option, 251 | pub colors: GraphColors, 252 | pub variant: NetworkVariant, 253 | } 254 | 255 | impl NetworkConfig { 256 | pub fn is_visible(&self) -> bool { 257 | self.chart || self.label 258 | } 259 | } 260 | 261 | impl Default for NetworkConfig { 262 | fn default() -> Self { 263 | Self { 264 | chart: true, 265 | label: false, 266 | adaptive: true, 267 | bandwidth: 62_500_000, // 500Mbit/s 268 | unit: Some(0), 269 | colors: GraphColors::new(DeviceKind::Network(NetworkVariant::Combined)), 270 | variant: NetworkVariant::Combined, 271 | } 272 | } 273 | } 274 | 275 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] 276 | pub enum DisksVariant { 277 | Write, 278 | Read, 279 | Combined, 280 | } 281 | 282 | #[derive(Debug, Clone, Serialize, Deserialize, CosmicConfigEntry, PartialEq)] 283 | #[version = 1] 284 | pub struct DisksConfig { 285 | pub chart: bool, 286 | pub label: bool, 287 | pub colors: GraphColors, 288 | pub variant: DisksVariant, 289 | } 290 | 291 | impl DisksConfig { 292 | pub fn is_visible(&self) -> bool { 293 | self.chart || self.label 294 | } 295 | } 296 | 297 | impl Default for DisksConfig { 298 | fn default() -> Self { 299 | Self { 300 | chart: false, 301 | label: false, 302 | colors: GraphColors::new(DeviceKind::Disks(DisksVariant::Combined)), 303 | variant: DisksVariant::Combined, 304 | } 305 | } 306 | } 307 | 308 | #[derive(Debug, Clone, Serialize, Deserialize, CosmicConfigEntry, PartialEq, Eq)] 309 | #[version = 1] 310 | pub struct GpuUsageConfig { 311 | pub chart: bool, 312 | pub label: bool, 313 | pub kind: GraphKind, 314 | pub colors: GraphColors, 315 | } 316 | 317 | impl Default for GpuUsageConfig { 318 | fn default() -> Self { 319 | Self { 320 | chart: true, 321 | label: false, 322 | kind: GraphKind::Ring, 323 | colors: GraphColors::new(DeviceKind::GpuTemp), 324 | } 325 | } 326 | } 327 | 328 | impl GpuUsageConfig { 329 | pub fn is_visible(&self) -> bool { 330 | self.chart || self.label 331 | } 332 | } 333 | 334 | #[derive(Debug, Clone, Serialize, Deserialize, CosmicConfigEntry, PartialEq, Eq)] 335 | #[version = 1] 336 | pub struct GpuVramConfig { 337 | pub chart: bool, 338 | pub label: bool, 339 | pub kind: GraphKind, 340 | pub colors: GraphColors, 341 | } 342 | 343 | impl Default for GpuVramConfig { 344 | fn default() -> Self { 345 | Self { 346 | chart: true, 347 | label: false, 348 | kind: GraphKind::Ring, 349 | colors: GraphColors::new(DeviceKind::Vram), 350 | } 351 | } 352 | } 353 | 354 | impl GpuVramConfig { 355 | pub fn is_visible(&self) -> bool { 356 | self.chart || self.label 357 | } 358 | } 359 | 360 | #[derive(Debug, Clone, Serialize, Deserialize, CosmicConfigEntry, PartialEq, Eq)] 361 | #[version = 1] 362 | pub struct GpuTempConfig { 363 | pub chart: bool, 364 | pub label: bool, 365 | pub kind: GraphKind, 366 | pub colors: GraphColors, 367 | pub unit: TempUnit, 368 | } 369 | 370 | impl Default for GpuTempConfig { 371 | fn default() -> Self { 372 | Self { 373 | chart: false, 374 | label: false, 375 | kind: GraphKind::Heat, 376 | colors: GraphColors::new(DeviceKind::GpuTemp), 377 | unit: TempUnit::Celcius, 378 | } 379 | } 380 | } 381 | 382 | impl GpuTempConfig { 383 | pub fn is_visible(&self) -> bool { 384 | self.chart || self.label 385 | } 386 | } 387 | 388 | #[derive(Debug, Clone, Serialize, Deserialize, CosmicConfigEntry, PartialEq, Eq)] 389 | #[version = 1] 390 | pub struct GpuConfig { 391 | pub usage: GpuUsageConfig, 392 | pub vram: GpuVramConfig, 393 | pub temp: GpuTempConfig, 394 | pub pause_on_battery: bool, 395 | pub stack_labels: bool, 396 | } 397 | 398 | impl GpuConfig { 399 | pub fn is_visible(&self) -> bool { 400 | self.usage.is_visible() || self.vram.is_visible() || self.temp.is_visible() 401 | } 402 | } 403 | 404 | impl Default for GpuConfig { 405 | fn default() -> Self { 406 | Self { 407 | usage: GpuUsageConfig::default(), 408 | vram: GpuVramConfig::default(), 409 | temp: GpuTempConfig::default(), 410 | pause_on_battery: true, 411 | stack_labels: true, 412 | } 413 | } 414 | } 415 | 416 | #[derive(Debug, Clone, Serialize, Deserialize, CosmicConfigEntry, PartialEq)] 417 | #[version = 1] 418 | pub struct MinimonConfig { 419 | pub refresh_rate: u32, 420 | pub label_size_default: u16, 421 | pub monospace_labels: bool, 422 | 423 | pub cpu: CpuConfig, 424 | pub cputemp: CpuTempConfig, 425 | pub memory: MemoryConfig, 426 | 427 | pub network1: NetworkConfig, 428 | pub network2: NetworkConfig, 429 | 430 | pub disks1: DisksConfig, 431 | pub disks2: DisksConfig, 432 | 433 | pub gpus: HashMap, 434 | 435 | pub sysmon: usize, 436 | 437 | pub symbols: bool, 438 | pub tight_spacing: bool, 439 | } 440 | 441 | impl Default for MinimonConfig { 442 | fn default() -> Self { 443 | Self { 444 | refresh_rate: 1000, 445 | label_size_default: 11, 446 | monospace_labels: false, 447 | cpu: CpuConfig::default(), 448 | cputemp: CpuTempConfig::default(), 449 | memory: MemoryConfig::default(), 450 | network1: NetworkConfig { 451 | variant: NetworkVariant::Combined, 452 | ..Default::default() 453 | }, 454 | network2: NetworkConfig { 455 | variant: NetworkVariant::Upload, 456 | ..Default::default() 457 | }, 458 | disks1: DisksConfig { 459 | variant: DisksVariant::Combined, 460 | ..Default::default() 461 | }, 462 | disks2: DisksConfig { 463 | variant: DisksVariant::Read, 464 | ..Default::default() 465 | }, 466 | gpus: HashMap::new(), 467 | sysmon: 0, 468 | symbols: false, 469 | tight_spacing: false, 470 | } 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /src/i18n.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: {{LICENSE}} 2 | 3 | //! Provides localization support for this crate. 4 | 5 | use std::sync::LazyLock; 6 | 7 | use i18n_embed::{ 8 | fluent::{fluent_language_loader, FluentLanguageLoader}, 9 | unic_langid::LanguageIdentifier, 10 | DefaultLocalizer, LanguageLoader, Localizer, 11 | }; 12 | use log::info; 13 | use rust_embed::RustEmbed; 14 | 15 | /// Applies the requested language(s) to requested translations from the `fl!()` macro. 16 | pub fn init(requested_languages: &[LanguageIdentifier]) { 17 | if let Err(why) = localizer().select(requested_languages) { 18 | info!("error while loading fluent localizations: {why}"); 19 | } 20 | } 21 | 22 | // Get the `Localizer` to be used for localizing this library. 23 | #[must_use] 24 | pub fn localizer() -> Box { 25 | Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) 26 | } 27 | 28 | #[derive(RustEmbed)] 29 | #[folder = "i18n/"] 30 | struct Localizations; 31 | 32 | pub static LANGUAGE_LOADER: LazyLock = LazyLock::new(|| { 33 | let loader: FluentLanguageLoader = fluent_language_loader!(); 34 | 35 | loader 36 | .load_fallback_language(&Localizations) 37 | .expect("Error while loading fallback language"); 38 | 39 | loader 40 | }); 41 | 42 | /// Request a localized string by ID from the i18n/ directory. 43 | #[macro_export] 44 | macro_rules! fl { 45 | ($message_id:literal) => {{ 46 | i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id) 47 | }}; 48 | 49 | ($message_id:literal, $($args:expr),*) => {{ 50 | i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, $($args), *) 51 | }}; 52 | } 53 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | use app::Minimon; 4 | 5 | mod app; 6 | mod charts; 7 | mod colorpicker; 8 | mod config; 9 | mod i18n; 10 | mod sensors; 11 | #[cfg(feature = "caffeine")] 12 | mod sleepinhibitor; 13 | mod svg_graph; 14 | 15 | use chrono::Local; 16 | use log::info; 17 | use std::io; 18 | 19 | fn setup_logger() -> Result<(), Box> { 20 | #[cfg(debug_assertions)] 21 | { 22 | // Debug builds: use fern with stdout 23 | fern::Dispatch::new() 24 | .level(log::LevelFilter::Warn) 25 | .level_for("cosmic_applet_minimon", log::LevelFilter::Debug) 26 | .format(|out, message, record| { 27 | out.finish(format_args!( 28 | "{} [{}] {}", 29 | Local::now().format("%H:%M:%S"), 30 | record.level(), 31 | message 32 | )); 33 | }) 34 | .chain(io::stdout()) 35 | .apply()?; 36 | } 37 | 38 | // In release builds we log to the systemd journal with fern/stdout fallback 39 | // To retrieve logs use "journalctl SYSLOG_IDENTIFIER=cosmic-applet-minimon" 40 | #[cfg(not(debug_assertions))] 41 | { 42 | let dispatch = fern::Dispatch::new() 43 | .level(log::LevelFilter::Warn) 44 | .level_for("cosmic_applet_minimon", log::LevelFilter::Debug); 45 | 46 | // Try to use systemd journal first 47 | match systemd_journal_logger::JournalLog::new() { 48 | Ok(journal_logger) => { 49 | let journal_logger = journal_logger.with_extra_fields(vec![ 50 | ("VERSION", env!("CARGO_PKG_VERSION")), 51 | ("APPLET", "cosmic_applet_minimon"), 52 | ]); 53 | 54 | dispatch 55 | .chain(Box::new(journal_logger) as Box) 56 | .apply()?; 57 | } 58 | Err(_) => { 59 | // Fallback to same fern logging as debug builds 60 | fern::Dispatch::new() 61 | .level(log::LevelFilter::Warn) 62 | .level_for("cosmic_applet_minimon", log::LevelFilter::Debug) 63 | .format(|out, message, record| { 64 | out.finish(format_args!( 65 | "{} [{}] {}", 66 | Local::now().format("%H:%M:%S"), 67 | record.level(), 68 | message 69 | )); 70 | }) 71 | .chain(io::stdout()) 72 | .apply()?; 73 | } 74 | } 75 | } 76 | 77 | Ok(()) 78 | } 79 | 80 | fn main() -> cosmic::iced::Result { 81 | setup_logger().expect("Failed to initialize logger"); 82 | 83 | #[cfg(not(debug_assertions))] 84 | println!("In Release builds use 'journalctl SYSLOG_IDENTIFIER=cosmic-applet-minimon' to see logs"); 85 | 86 | info!("Application started"); 87 | 88 | let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages(); 89 | i18n::init(&requested_languages); 90 | cosmic::applet::run::(()) 91 | } 92 | -------------------------------------------------------------------------------- /src/sensors/cpu.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | colorpicker::DemoGraph, 3 | config::{ColorVariant, CpuConfig, DeviceKind, GraphColors, GraphKind}, 4 | fl, 5 | svg_graph::SvgColors, 6 | }; 7 | use cosmic::Element; 8 | use std::any::Any; 9 | 10 | use cosmic::widget; 11 | use cosmic::widget::{settings, toggler}; 12 | 13 | use cosmic::{ 14 | iced::{ 15 | Alignment, 16 | widget::{column, row}, 17 | }, 18 | iced_widget::Row, 19 | }; 20 | 21 | use crate::app::Message; 22 | 23 | use std::{ 24 | collections::{HashMap, VecDeque}, 25 | fmt::Write, 26 | fs::File, 27 | io::{BufRead, BufReader}, 28 | path::Path, 29 | }; 30 | 31 | use super::Sensor; 32 | 33 | const MAX_SAMPLES: usize = 21; 34 | 35 | const GRAPH_OPTIONS: [&str; 2] = ["Ring", "Line"]; 36 | 37 | #[derive(Debug)] 38 | struct CpuTimes { 39 | user: u64, 40 | nice: u64, 41 | system: u64, 42 | idle: u64, 43 | iowait: u64, 44 | irq: u64, 45 | softirq: u64, 46 | steal: u64, 47 | } 48 | 49 | #[derive(Debug, Clone, Copy)] 50 | struct CpuLoad { 51 | user_pct: f64, 52 | system_pct: f64, 53 | } 54 | 55 | #[derive(Debug)] 56 | pub struct Cpu { 57 | // Total CPU load since last update split into user and system 58 | total_cpu_load: CpuLoad, 59 | // Load per core since last update split into user and system 60 | core_loads: HashMap, 61 | // Load per core since last update split into values read from /proc 62 | prev_core_times: HashMap, 63 | // Total CPU load for the last MAX_SAMPLES updates 64 | samples_sum: VecDeque, 65 | // CPU load for the last MAX_SAMPLES updates, split into user and system 66 | samples_split: VecDeque, 67 | graph_options: Vec<&'static str>, 68 | /// colors cached so we don't need to convert to string every time 69 | svg_colors: SvgColors, 70 | config: CpuConfig, 71 | } 72 | 73 | impl DemoGraph for Cpu { 74 | fn demo(&self) -> String { 75 | match self.config.kind { 76 | GraphKind::Ring => { 77 | // show a number of 40% of max 78 | let val = 40; 79 | let percentage: u64 = 40; 80 | crate::svg_graph::ring( 81 | &format!("{val}"), 82 | &format!("{percentage}"), 83 | &self.svg_colors, 84 | ) 85 | } 86 | GraphKind::Line => { 87 | crate::svg_graph::line(&VecDeque::from(DEMO_SAMPLES), 100.0, &self.svg_colors) 88 | } 89 | GraphKind::Heat => panic!("Wrong graph choice!"), 90 | } 91 | } 92 | 93 | fn colors(&self) -> GraphColors { 94 | self.config.colors 95 | } 96 | 97 | fn set_colors(&mut self, colors: GraphColors) { 98 | self.config.colors = colors; 99 | self.svg_colors.set_colors(&colors); 100 | } 101 | 102 | fn color_choices(&self) -> Vec<(&'static str, ColorVariant)> { 103 | if self.config.kind == GraphKind::Line { 104 | (*super::COLOR_CHOICES_LINE).into() 105 | } else { 106 | (*super::COLOR_CHOICES_RING).into() 107 | } 108 | } 109 | 110 | fn id(&self) -> Option { 111 | None 112 | } 113 | } 114 | 115 | impl Sensor for Cpu { 116 | fn update_config(&mut self, config: &dyn Any, _refresh_rate: u32) { 117 | if let Some(cfg) = config.downcast_ref::() { 118 | self.config = cfg.clone(); 119 | self.svg_colors.set_colors(&cfg.colors); 120 | } 121 | } 122 | 123 | fn graph_kind(&self) -> GraphKind { 124 | self.config.kind 125 | } 126 | 127 | fn set_graph_kind(&mut self, kind: GraphKind) { 128 | assert!(kind == GraphKind::Line || kind == GraphKind::Ring); 129 | self.config.kind = kind; 130 | } 131 | 132 | fn update(&mut self) { 133 | self.update_stats(); 134 | 135 | if self.samples_split.len() >= MAX_SAMPLES { 136 | self.samples_split.pop_front(); 137 | } 138 | self.samples_split.push_back(self.total_cpu_load); 139 | 140 | let new_sum = self.total_cpu_load.user_pct + self.total_cpu_load.system_pct; 141 | if self.samples_sum.len() >= MAX_SAMPLES { 142 | self.samples_sum.pop_front(); 143 | } 144 | self.samples_sum.push_back(new_sum); 145 | } 146 | 147 | fn demo_graph(&self) -> Box { 148 | let mut dmo = Cpu::default(); 149 | dmo.update_config(&self.config, 0); 150 | Box::new(dmo) 151 | } 152 | 153 | fn graph(&self) -> String { 154 | if self.config.kind == GraphKind::Ring { 155 | let latest = self.latest_sample(); 156 | let mut value = String::with_capacity(10); 157 | let mut percentage = String::with_capacity(10); 158 | 159 | if self.config.no_decimals { 160 | write!(value, "{}%", latest.round()).unwrap(); 161 | } else if latest < 10.0 { 162 | write!(value, "{latest:.2}").unwrap() 163 | } else if latest <= 99.9 { 164 | write!(value, "{latest:.1}").unwrap(); 165 | } else { 166 | write!(value, "100").unwrap(); 167 | } 168 | 169 | write!(percentage, "{latest}").unwrap(); 170 | 171 | crate::svg_graph::ring(&value, &percentage, &self.svg_colors) 172 | } else { 173 | crate::svg_graph::line(&self.samples_sum, 100.0, &self.svg_colors) 174 | } 175 | } 176 | 177 | fn settings_ui(&self) -> Element { 178 | let theme = cosmic::theme::active(); 179 | let cosmic = theme.cosmic(); 180 | 181 | let mut cpu_elements = Vec::new(); 182 | 183 | let cpu = self.to_string(); 184 | cpu_elements.push(Element::from( 185 | column!( 186 | widget::svg(widget::svg::Handle::from_memory( 187 | self.graph().as_bytes().to_owned(), 188 | )) 189 | .width(90) 190 | .height(60), 191 | cosmic::widget::text::body(cpu), 192 | ) 193 | .padding(5) 194 | .align_x(Alignment::Center), 195 | )); 196 | 197 | let selected: Option = Some(self.graph_kind().into()); 198 | 199 | let config = &self.config; 200 | let cpu_kind = self.graph_kind(); 201 | cpu_elements.push(Element::from( 202 | column!( 203 | settings::item( 204 | fl!("enable-chart"), 205 | toggler(config.chart).on_toggle(|value| { Message::ToggleCpuChart(value) }), 206 | ), 207 | settings::item( 208 | fl!("enable-label"), 209 | toggler(config.label).on_toggle(|value| { Message::ToggleCpuLabel(value) }), 210 | ), 211 | settings::item( 212 | fl!("cpu-no-decimals"), 213 | row!( 214 | widget::checkbox("", config.no_decimals) 215 | .on_toggle(Message::ToggleCpuNoDecimals) 216 | ), 217 | ), 218 | row!( 219 | widget::dropdown(&self.graph_options, selected, move |m| { 220 | Message::SelectGraphType(DeviceKind::Cpu, m.into()) 221 | },) 222 | .width(70), 223 | widget::horizontal_space(), 224 | widget::button::standard(fl!("change-colors")) 225 | .on_press(Message::ColorPickerOpen(DeviceKind::Cpu, cpu_kind, None)), 226 | ) 227 | ) 228 | .spacing(cosmic.space_xs()), 229 | )); 230 | 231 | Row::with_children(cpu_elements) 232 | .align_y(Alignment::Center) 233 | .spacing(0) 234 | .into() 235 | } 236 | } 237 | 238 | impl Default for Cpu { 239 | fn default() -> Self { 240 | // value and percentage are pre-allocated and reused as they're changed often. 241 | let mut percentage = String::with_capacity(6); 242 | write!(percentage, "0").unwrap(); 243 | 244 | let mut value = String::with_capacity(6); 245 | write!(value, "0").unwrap(); 246 | 247 | let mut cpu = Cpu { 248 | total_cpu_load: CpuLoad { 249 | user_pct: 0., 250 | system_pct: 0., 251 | }, 252 | core_loads: HashMap::new(), 253 | prev_core_times: Cpu::read_cpu_stats(), 254 | samples_sum: VecDeque::from(vec![0.0; MAX_SAMPLES]), 255 | samples_split: VecDeque::from(vec![ 256 | CpuLoad { 257 | user_pct: 0., 258 | system_pct: 0. 259 | }; 260 | MAX_SAMPLES 261 | ]), 262 | graph_options: GRAPH_OPTIONS.to_vec(), 263 | svg_colors: SvgColors::new(&GraphColors::default()), 264 | config: CpuConfig::default(), 265 | }; 266 | cpu.set_colors(GraphColors::default()); 267 | cpu 268 | } 269 | } 270 | 271 | impl Cpu { 272 | pub fn latest_sample(&self) -> f64 { 273 | *self.samples_sum.back().unwrap_or(&0f64) 274 | } 275 | 276 | // Read CPU statistics from /proc/stat 277 | fn read_cpu_stats() -> HashMap { 278 | let mut cpu_stats = HashMap::new(); 279 | 280 | // Open /proc/stat file 281 | let Ok(file) = File::open(Path::new("/proc/stat")) else { 282 | return cpu_stats; 283 | }; 284 | 285 | let reader = BufReader::new(file); 286 | 287 | // Read each line from the file 288 | for line in reader.lines() { 289 | let Ok(line) = line else { continue }; 290 | // Split line into parts 291 | let parts: Vec<&str> = line.split_whitespace().collect(); 292 | 293 | // Check if line starts with 'cpu' followed by a number 294 | if parts.is_empty() || !parts[0].starts_with("cpu") || parts[0] == "cpu" { 295 | continue; 296 | } 297 | 298 | // Extract CPU number 299 | let Ok(cpu_num) = parts[0].trim_start_matches("cpu").parse::() else { 300 | continue; 301 | }; 302 | 303 | // Ensure we have enough parts for all fields 304 | if parts.len() < 9 { 305 | continue; 306 | } 307 | 308 | // Parse all CPU time values 309 | let user = parts[1].parse::().unwrap_or(0); 310 | let nice = parts[2].parse::().unwrap_or(0); 311 | let system = parts[3].parse::().unwrap_or(0); 312 | let idle = parts[4].parse::().unwrap_or(0); 313 | let iowait = parts[5].parse::().unwrap_or(0); 314 | let irq = parts[6].parse::().unwrap_or(0); 315 | let softirq = parts[7].parse::().unwrap_or(0); 316 | let steal = parts[8].parse::().unwrap_or(0); 317 | 318 | // Create CpuTimes struct and insert into HashMap 319 | let cpu_times = CpuTimes { 320 | user, 321 | nice, 322 | system, 323 | idle, 324 | iowait, 325 | irq, 326 | softirq, 327 | steal, 328 | }; 329 | 330 | cpu_stats.insert(cpu_num, cpu_times); 331 | } 332 | 333 | cpu_stats 334 | } 335 | 336 | // Update current CPU load by comparing to previous samples 337 | fn update_stats(&mut self) { 338 | // Read current CPU stats 339 | let current_cpu_times = Cpu::read_cpu_stats(); 340 | 341 | // Temporary storage for new per-core loads 342 | let mut new_cpu_loads = HashMap::with_capacity(current_cpu_times.len()); 343 | 344 | // Running totals for average computation 345 | let mut total_user_pct = 0.0; 346 | let mut total_system_pct = 0.0; 347 | let mut counted_cores = 0; 348 | 349 | for (&cpu_num, current) in ¤t_cpu_times { 350 | if let Some(prev) = self.prev_core_times.get(&cpu_num) { 351 | // Compute time deltas 352 | let user = current.user.saturating_sub(prev.user); 353 | let nice = current.nice.saturating_sub(prev.nice); 354 | let system = current.system.saturating_sub(prev.system); 355 | let idle = current.idle.saturating_sub(prev.idle); 356 | let iowait = current.iowait.saturating_sub(prev.iowait); 357 | let irq = current.irq.saturating_sub(prev.irq); 358 | let softirq = current.softirq.saturating_sub(prev.softirq); 359 | let steal = current.steal.saturating_sub(prev.steal); 360 | 361 | let total = user + nice + system + idle + iowait + irq + softirq + steal; 362 | if total == 0 { 363 | continue; 364 | } 365 | 366 | let total_f64 = total as f64; 367 | let user_pct = (user + nice) as f64 / total_f64 * 100.0; 368 | let system_pct = system as f64 / total_f64 * 100.0; 369 | 370 | new_cpu_loads.insert( 371 | cpu_num, 372 | CpuLoad { 373 | user_pct, 374 | system_pct, 375 | }, 376 | ); 377 | 378 | total_user_pct += user_pct; 379 | total_system_pct += system_pct; 380 | counted_cores += 1; 381 | } 382 | } 383 | 384 | self.core_loads = new_cpu_loads; 385 | 386 | if counted_cores > 0 { 387 | let core_count_f64 = f64::from(counted_cores); 388 | self.total_cpu_load = CpuLoad { 389 | user_pct: total_user_pct / core_count_f64, 390 | system_pct: total_system_pct / core_count_f64, 391 | }; 392 | } 393 | 394 | self.prev_core_times = current_cpu_times; 395 | } 396 | } 397 | 398 | use std::fmt; 399 | 400 | impl fmt::Display for Cpu { 401 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 402 | let current_val = self.latest_sample(); 403 | 404 | if self.config.no_decimals { 405 | write!(f, "{}%", current_val.round()) 406 | } else if current_val < 10.0 { 407 | write!(f, "{current_val:.2}%") 408 | } else if current_val < 100.0 { 409 | write!(f, "{current_val:.1}%") 410 | } else { 411 | write!(f, "{current_val}%") 412 | } 413 | } 414 | } 415 | 416 | const DEMO_SAMPLES: [f64; 21] = [ 417 | 0.0, 418 | 12.689857482910156, 419 | 12.642768859863281, 420 | 12.615306854248047, 421 | 12.658184051513672, 422 | 12.65273666381836, 423 | 12.626102447509766, 424 | 12.624862670898438, 425 | 12.613967895507813, 426 | 12.619949340820313, 427 | 19.061111450195313, 428 | 21.691085815429688, 429 | 21.810935974121094, 430 | 21.28915786743164, 431 | 22.041973114013672, 432 | 21.764171600341797, 433 | 21.89263916015625, 434 | 15.258216857910156, 435 | 14.770732879638672, 436 | 14.496528625488281, 437 | 13.892818450927734, 438 | ]; 439 | -------------------------------------------------------------------------------- /src/sensors/cputemp.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | colorpicker::DemoGraph, 3 | config::{ColorVariant, CpuTempConfig, DeviceKind, GraphColors, GraphKind}, 4 | fl, 5 | svg_graph::SvgColors, 6 | }; 7 | use cosmic::Element; 8 | 9 | use cosmic::widget; 10 | use cosmic::widget::{settings, toggler}; 11 | 12 | use cosmic::{ 13 | iced::{ 14 | Alignment, 15 | widget::{column, row}, 16 | }, 17 | iced_widget::Row, 18 | }; 19 | use log::info; 20 | 21 | use crate::app::Message; 22 | use std::any::Any; 23 | 24 | use std::{ 25 | collections::VecDeque, 26 | fmt::Write, 27 | fs, 28 | path::{Path, PathBuf}, 29 | }; 30 | 31 | use std::fs::read_dir; 32 | use std::io; 33 | 34 | use super::{CpuVariant, Sensor, TempUnit}; 35 | 36 | const MAX_SAMPLES: usize = 21; 37 | 38 | const GRAPH_OPTIONS: [&str; 3] = ["Ring", "Line", "Heat"]; 39 | const UNIT_OPTIONS: [&str; 4] = ["Celcius", "Farenheit", "Kelvin", "Rankine"]; 40 | 41 | #[derive(Debug)] 42 | pub struct HwmonTemp { 43 | pub temp_paths: Vec, 44 | pub crit_temp: f64, 45 | pub cpu: super::CpuVariant, 46 | } 47 | 48 | impl HwmonTemp { 49 | /// Initialize and return the most relevant CPU temperature sensors 50 | pub fn find_cpu_sensor() -> io::Result> { 51 | info!("Find CPU temperature sensor"); 52 | let hwmon_base = Path::new("/sys/class/hwmon"); 53 | 54 | for entry in read_dir(hwmon_base)? { 55 | let hwmon = entry?.path(); 56 | let name_path = hwmon.join("name"); 57 | 58 | let Ok(name) = fs::read_to_string(&name_path) else { 59 | continue; 60 | }; 61 | let name = name.trim().to_lowercase(); 62 | info!(" path: {name_path:?}. name: {name}"); 63 | 64 | if name.contains("coretemp") || name.contains("k10temp") || name.contains("cpu") { 65 | let mut tdie: Option<(PathBuf, String)> = None; 66 | let mut tctl: Option<(PathBuf, String)> = None; 67 | let mut core_fallbacks = vec![]; 68 | 69 | for i in 0..100 { 70 | let label_path = hwmon.join(format!("temp{i}_label")); 71 | let input_path = hwmon.join(format!("temp{i}_input")); 72 | 73 | if !input_path.exists() { 74 | continue; 75 | } 76 | if let Ok(label) = fs::read_to_string(&label_path) { 77 | let label = label.trim(); 78 | 79 | if label.eq_ignore_ascii_case("Tdie") { 80 | info!(" found sensor {label_path:?} {label}"); 81 | tdie = Some((input_path.clone(), label.to_string())); 82 | } else if label.eq_ignore_ascii_case("Tctl") { 83 | info!(" found sensor {label_path:?} {label}"); 84 | tctl = Some((input_path.clone(), label.to_string())); 85 | } else if label.starts_with("Core") || label.contains("Package") { 86 | info!(" found sensor {label_path:?} {label}"); 87 | core_fallbacks.push((input_path.clone(), label.to_string())); 88 | } 89 | } 90 | } 91 | 92 | // Prioritize Tdie > Tctl 93 | if let Some((path, _label)) = tdie.or(tctl) { 94 | let crit_path = hwmon.join("temp1_crit"); 95 | let crit_temp = fs::read_to_string(&crit_path) 96 | .ok() 97 | .and_then(|v| v.trim().parse::().ok()) 98 | .map_or(100.0, |v| v / 1000.0); 99 | 100 | return Ok(Some(HwmonTemp { 101 | temp_paths: vec![path.clone()], 102 | crit_temp, 103 | cpu: CpuVariant::Amd, 104 | })); 105 | } else if !core_fallbacks.is_empty() { 106 | return Ok(Some(HwmonTemp { 107 | temp_paths: core_fallbacks.iter().map(|(p, _)| p.clone()).collect(), 108 | crit_temp: 100.0, 109 | cpu: CpuVariant::Intel, 110 | })); 111 | } 112 | } 113 | } 114 | 115 | Ok(None) 116 | } 117 | 118 | /// Read current max temperature from all tracked sensor paths 119 | pub fn read_temp(&self) -> io::Result { 120 | let mut max_temp = f32::MIN; 121 | 122 | for path in &self.temp_paths { 123 | let raw = fs::read_to_string(path)?; 124 | let millideg: i32 = raw.trim().parse().map_err(|e| { 125 | io::Error::new(io::ErrorKind::InvalidData, format!("Parse error: {e}")) 126 | })?; 127 | let temp_c = millideg as f32 / 1000.0; 128 | max_temp = max_temp.max(temp_c); 129 | } 130 | 131 | Ok(max_temp) 132 | } 133 | } 134 | 135 | #[derive(Debug)] 136 | pub struct CpuTemp { 137 | hwmon_temp: Option, 138 | pub samples: VecDeque, 139 | graph_options: Vec<&'static str>, 140 | unit_options: Vec<&'static str>, 141 | /// colors cached so we don't need to convert to string every time 142 | svg_colors: SvgColors, 143 | config: CpuTempConfig, 144 | } 145 | 146 | impl DemoGraph for CpuTemp { 147 | fn demo(&self) -> String { 148 | match self.config.kind { 149 | GraphKind::Ring => { 150 | // show a number of 40% of max 151 | let val = 40; 152 | let percentage: u64 = 40; 153 | crate::svg_graph::ring( 154 | &format!("{val}"), 155 | &format!("{percentage}"), 156 | &self.svg_colors, 157 | ) 158 | } 159 | GraphKind::Line => { 160 | crate::svg_graph::line(&VecDeque::from(DEMO_SAMPLES), 100.0, &self.svg_colors) 161 | } 162 | GraphKind::Heat => { 163 | crate::svg_graph::heat(&VecDeque::from(DEMO_SAMPLES), 100, &self.svg_colors) 164 | } 165 | } 166 | } 167 | 168 | fn colors(&self) -> GraphColors { 169 | self.config.colors 170 | } 171 | 172 | fn set_colors(&mut self, colors: GraphColors) { 173 | self.config.colors = colors; 174 | self.svg_colors.set_colors(&colors); 175 | } 176 | 177 | fn color_choices(&self) -> Vec<(&'static str, ColorVariant)> { 178 | match self.config.kind { 179 | GraphKind::Line => (*super::COLOR_CHOICES_LINE).into(), 180 | GraphKind::Ring => (*super::COLOR_CHOICES_RING).into(), 181 | GraphKind::Heat => (*super::COLOR_CHOICES_HEAT).into(), 182 | } 183 | } 184 | 185 | fn id(&self) -> Option { 186 | None 187 | } 188 | } 189 | 190 | impl Sensor for CpuTemp { 191 | fn update_config(&mut self, config: &dyn Any, _refresh_rate: u32) { 192 | if let Some(cfg) = config.downcast_ref::() { 193 | self.config = cfg.clone(); 194 | self.svg_colors.set_colors(&cfg.colors); 195 | } 196 | } 197 | 198 | fn graph_kind(&self) -> GraphKind { 199 | self.config.kind 200 | } 201 | 202 | fn set_graph_kind(&mut self, kind: GraphKind) { 203 | assert!(kind == GraphKind::Line || kind == GraphKind::Ring || kind == GraphKind::Heat); 204 | self.config.kind = kind; 205 | } 206 | 207 | fn update(&mut self) { 208 | if let Some(hw) = &self.hwmon_temp { 209 | match hw.read_temp() { 210 | Ok(temp) => { 211 | if self.samples.len() >= MAX_SAMPLES { 212 | self.samples.pop_front(); 213 | } 214 | self.samples.push_back(f64::from(temp)); 215 | } 216 | Err(e) => info!("Error reading temp data {e:?}"), 217 | } 218 | } 219 | } 220 | 221 | fn demo_graph(&self) -> Box { 222 | let mut dmo = CpuTemp::default(); 223 | dmo.update_config(&self.config, 0); 224 | Box::new(dmo) 225 | } 226 | 227 | fn graph(&self) -> String { 228 | let mut max: f64 = 100.0; 229 | if let Some(hwmon) = &self.hwmon_temp { 230 | max = hwmon.crit_temp; 231 | } 232 | match self.config.kind { 233 | GraphKind::Ring => { 234 | let latest = self.latest_sample(); 235 | let mut value = self.to_string(); 236 | 237 | // remove the C/F/K unit if there's not enough space 238 | if value.len() > 3 { 239 | let _ = value.pop(); 240 | } 241 | let mut percentage = String::with_capacity(10); 242 | 243 | write!(percentage, "{latest}").unwrap(); 244 | 245 | crate::svg_graph::ring(&value, &percentage, &self.svg_colors) 246 | } 247 | GraphKind::Line => crate::svg_graph::line(&self.samples, max, &self.svg_colors), 248 | GraphKind::Heat => crate::svg_graph::heat(&self.samples, max as u64, &self.svg_colors), 249 | } 250 | } 251 | 252 | fn settings_ui(&self) -> Element { 253 | let theme = cosmic::theme::active(); 254 | let cosmic = theme.cosmic(); 255 | 256 | let mut temp_elements = Vec::new(); 257 | 258 | let temp = self.to_string(); 259 | 260 | temp_elements.push(Element::from( 261 | column!( 262 | widget::svg(widget::svg::Handle::from_memory( 263 | self.graph().as_bytes().to_owned(), 264 | )) 265 | .width(90) 266 | .height(60), 267 | cosmic::widget::text::body(temp), 268 | ) 269 | .padding(5) 270 | .align_x(Alignment::Center), 271 | )); 272 | 273 | let selected_graph: Option = Some(self.graph_kind().into()); 274 | let selected_unit: Option = Some(self.config.unit.into()); 275 | 276 | let config = &self.config; 277 | let temp_kind = self.graph_kind(); 278 | temp_elements.push(Element::from( 279 | column!( 280 | settings::item( 281 | fl!("enable-chart"), 282 | toggler(config.chart).on_toggle(|value| { Message::ToggleCpuTempChart(value) }), 283 | ), 284 | settings::item( 285 | fl!("enable-label"), 286 | toggler(config.label).on_toggle(|value| { Message::ToggleCpuTempLabel(value) }), 287 | ), 288 | settings::item( 289 | fl!("temperature-unit"), 290 | widget::dropdown(&self.unit_options, selected_unit, |m| { 291 | Message::SelectCpuTempUnit(m.into()) 292 | },) 293 | ), 294 | row!( 295 | widget::dropdown(&self.graph_options, selected_graph, |m| { 296 | Message::SelectGraphType(DeviceKind::CpuTemp, m.into()) 297 | },) 298 | .width(70), 299 | widget::horizontal_space(), 300 | widget::button::standard(fl!("change-colors")).on_press( 301 | Message::ColorPickerOpen(DeviceKind::CpuTemp, temp_kind, None) 302 | ), 303 | ) 304 | ) 305 | .spacing(cosmic.space_xs()), 306 | )); 307 | 308 | let mut expl = String::with_capacity(128); 309 | if let Some(hw) = &self.hwmon_temp { 310 | if hw.cpu == super::CpuVariant::Amd { 311 | _ = write!(expl, "{}", fl!("cpu-temp-amd")); 312 | } else { 313 | _ = write!(expl, "{}", fl!("cpu-temp-intel")); 314 | } 315 | } 316 | 317 | column!( 318 | Element::from(widget::text::body(expl)), 319 | Element::from( 320 | Row::with_children(temp_elements) 321 | .align_y(Alignment::Center) 322 | .spacing(0) 323 | ) 324 | ) 325 | .spacing(10) 326 | .into() 327 | } 328 | } 329 | 330 | impl Default for CpuTemp { 331 | fn default() -> Self { 332 | let mut hwmon = None; 333 | 334 | match HwmonTemp::find_cpu_sensor() { 335 | Ok(hwmon_option) => { 336 | hwmon = hwmon_option; 337 | if hwmon.is_none() { 338 | info!("CpuTemp:detect: No CPU Temp IF found."); 339 | } 340 | } 341 | Err(e) => info!("CpuTemp:detect: No CPU Temp IF found. {e:?}"), 342 | } 343 | 344 | let mut cpu = CpuTemp { 345 | hwmon_temp: hwmon, 346 | samples: VecDeque::from(vec![0.0; MAX_SAMPLES]), 347 | graph_options: GRAPH_OPTIONS.to_vec(), 348 | svg_colors: SvgColors::new(&GraphColors::default()), 349 | unit_options: UNIT_OPTIONS.to_vec(), 350 | config: CpuTempConfig::default(), 351 | }; 352 | cpu.set_colors(GraphColors::default()); 353 | cpu 354 | } 355 | } 356 | 357 | impl CpuTemp { 358 | // true if a CPU temperature hwmon path was found 359 | pub fn is_found(&self) -> bool { 360 | self.hwmon_temp.is_some() 361 | } 362 | 363 | pub fn latest_sample(&self) -> f64 { 364 | *self.samples.back().unwrap_or(&0f64) 365 | } 366 | } 367 | 368 | use std::fmt; 369 | 370 | impl fmt::Display for CpuTemp { 371 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 372 | let current_val = self.latest_sample(); 373 | match self.config.unit { 374 | TempUnit::Celcius => write!(f, "{}C", current_val.trunc()), 375 | TempUnit::Farenheit => write!(f, "{}F", (current_val * 9.0 / 5.0 + 32.0).trunc()), 376 | TempUnit::Kelvin => write!(f, "{}K", (current_val + 273.15).trunc()), 377 | TempUnit::Rankine => write!(f, "{}R", (current_val * 9.0 / 5.0 + 491.67).trunc()), 378 | } 379 | } 380 | } 381 | 382 | const DEMO_SAMPLES: [f64; 21] = [ 383 | 41.0, 42.0, 43.5, 45.0, 48.0, 51.0, 55.0, 57.0, 59.5, 62.0, 64.0, 67.0, 70.0, 74.0, 78.0, 83.0, 384 | 87.0, 90.0, 95.0, 98.0, 100.0, 385 | ]; 386 | -------------------------------------------------------------------------------- /src/sensors/disks.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use sysinfo::{DiskRefreshKind, Disks as DisksInfo}; 4 | 5 | use crate::{ 6 | colorpicker::DemoGraph, 7 | config::{ColorVariant, DeviceKind, DisksConfig, GraphColors, GraphKind}, 8 | fl, 9 | svg_graph::SvgColors, 10 | }; 11 | 12 | use cosmic::{Element, widget::Column}; 13 | 14 | use cosmic::widget; 15 | use cosmic::widget::settings; 16 | 17 | use cosmic::{ 18 | iced::{ 19 | Alignment, 20 | widget::{column, row}, 21 | }, 22 | iced_widget::Row, 23 | }; 24 | 25 | use crate::app::Message; 26 | use crate::config::DisksVariant; 27 | use std::any::Any; 28 | 29 | use super::Sensor; 30 | 31 | const MAX_SAMPLES: usize = 30; 32 | const GRAPH_SAMPLES: usize = 21; 33 | const UNITS_SHORT: [&str; 5] = ["B", "K", "M", "G", "T"]; 34 | const UNITS_LONG: [&str; 5] = ["B/s", "KB/s", "MB/s", "GB/s", "TB/s"]; 35 | use std::sync::LazyLock; 36 | 37 | pub static COLOR_CHOICES_COMBINED: LazyLock<[(&'static str, ColorVariant); 4]> = 38 | LazyLock::new(|| { 39 | [ 40 | (fl!("graph-disks-write").leak(), ColorVariant::Color2), 41 | (fl!("graph-disks-read").leak(), ColorVariant::Color3), 42 | (fl!("graph-disks-back").leak(), ColorVariant::Color1), 43 | (fl!("graph-disks-frame").leak(), ColorVariant::Color4), 44 | ] 45 | }); 46 | 47 | pub static COLOR_CHOICES_WRITE: LazyLock<[(&'static str, ColorVariant); 3]> = LazyLock::new(|| { 48 | [ 49 | (fl!("graph-disks-write").leak(), ColorVariant::Color2), 50 | (fl!("graph-disks-back").leak(), ColorVariant::Color1), 51 | (fl!("graph-disks-frame").leak(), ColorVariant::Color4), 52 | ] 53 | }); 54 | 55 | pub static COLOR_CHOICES_READ: LazyLock<[(&'static str, ColorVariant); 3]> = LazyLock::new(|| { 56 | [ 57 | (fl!("graph-disks-read").leak(), ColorVariant::Color3), 58 | (fl!("graph-disks-back").leak(), ColorVariant::Color1), 59 | (fl!("graph-disks-frame").leak(), ColorVariant::Color4), 60 | ] 61 | }); 62 | 63 | #[derive(Debug, PartialEq, Eq)] 64 | pub enum UnitVariant { 65 | Short, 66 | Long, 67 | } 68 | 69 | #[derive(Debug)] 70 | pub struct Disks { 71 | disks: DisksInfo, 72 | write: VecDeque, 73 | read: VecDeque, 74 | max_y: Option, 75 | svg_colors: SvgColors, 76 | config: DisksConfig, 77 | refresh_rate: u32, 78 | } 79 | 80 | impl DemoGraph for Disks { 81 | fn demo(&self) -> String { 82 | let write = VecDeque::from(DL_DEMO); 83 | let read = VecDeque::from(UL_DEMO); 84 | 85 | match self.config.variant { 86 | DisksVariant::Combined => { 87 | crate::svg_graph::double_line(&write, &read, GRAPH_SAMPLES, &self.svg_colors, None) 88 | } 89 | DisksVariant::Write => { 90 | crate::svg_graph::line_adaptive(&write, GRAPH_SAMPLES, &self.svg_colors, None) 91 | } 92 | DisksVariant::Read => { 93 | let mut cols = self.svg_colors.clone(); 94 | cols.color2 = cols.color3.clone(); 95 | crate::svg_graph::line_adaptive(&read, GRAPH_SAMPLES, &cols, None) 96 | } 97 | } 98 | } 99 | 100 | fn colors(&self) -> GraphColors { 101 | self.config.colors 102 | } 103 | 104 | fn set_colors(&mut self, colors: GraphColors) { 105 | self.config.colors = colors; 106 | self.svg_colors.set_colors(&colors); 107 | } 108 | 109 | fn color_choices(&self) -> Vec<(&'static str, ColorVariant)> { 110 | match self.config.variant { 111 | DisksVariant::Combined => (*COLOR_CHOICES_COMBINED).into(), 112 | DisksVariant::Write => (*COLOR_CHOICES_WRITE).into(), 113 | DisksVariant::Read => (*COLOR_CHOICES_READ).into(), 114 | } 115 | } 116 | 117 | fn id(&self) -> Option { 118 | None 119 | } 120 | } 121 | 122 | impl Sensor for Disks { 123 | fn update_config(&mut self, config: &dyn Any, refresh_rate: u32) { 124 | if let Some(cfg) = config.downcast_ref::() { 125 | self.config = cfg.clone(); 126 | self.svg_colors.set_colors(&cfg.colors); 127 | self.refresh_rate = refresh_rate; 128 | } 129 | } 130 | 131 | fn graph_kind(&self) -> GraphKind { 132 | GraphKind::Line 133 | } 134 | 135 | fn set_graph_kind(&mut self, kind: GraphKind) { 136 | assert!(kind == GraphKind::Line); 137 | } 138 | 139 | /// Retrieve the amount of data transmitted since last update. 140 | fn update(&mut self) { 141 | let r = DiskRefreshKind::nothing().with_io_usage(); 142 | self.disks.refresh_specifics(true, r); 143 | let mut wr = 0; 144 | let mut rd = 0; 145 | 146 | for disk in self.disks.list() { 147 | let usage = disk.usage(); 148 | wr += usage.written_bytes; 149 | rd += usage.read_bytes; 150 | } 151 | 152 | if self.write.len() >= MAX_SAMPLES { 153 | self.write.pop_front(); 154 | } 155 | self.write.push_back(wr); 156 | 157 | if self.read.len() >= MAX_SAMPLES { 158 | self.read.pop_front(); 159 | } 160 | self.read.push_back(rd); 161 | } 162 | 163 | fn demo_graph(&self) -> Box { 164 | let mut dmo = Disks::default(); 165 | dmo.update_config(&self.config, 0); 166 | Box::new(dmo) 167 | } 168 | 169 | fn graph(&self) -> String { 170 | match self.config.variant { 171 | DisksVariant::Combined => crate::svg_graph::double_line( 172 | &self.write, 173 | &self.read, 174 | GRAPH_SAMPLES, 175 | &self.svg_colors, 176 | self.max_y, 177 | ), 178 | DisksVariant::Write => crate::svg_graph::line_adaptive( 179 | &self.write, 180 | GRAPH_SAMPLES, 181 | &self.svg_colors, 182 | self.max_y, 183 | ), 184 | DisksVariant::Read => { 185 | let mut cols = self.svg_colors.clone(); 186 | cols.color2 = cols.color3.clone(); 187 | crate::svg_graph::line_adaptive(&self.read, GRAPH_SAMPLES, &cols, self.max_y) 188 | } 189 | } 190 | } 191 | 192 | fn settings_ui(&self) -> Element { 193 | let theme = cosmic::theme::active(); 194 | let cosmic = theme.cosmic(); 195 | let mut disk_elements = Vec::new(); 196 | 197 | let sample_rate_ms = self.refresh_rate; 198 | 199 | let wrrate = format!("W {}", &self.write_label(sample_rate_ms, UnitVariant::Long)); 200 | 201 | let rdrate = format!("R {}", &self.read_label(sample_rate_ms, UnitVariant::Long)); 202 | 203 | let config = &self.config; 204 | let k = self.config.variant; 205 | 206 | let mut rate = column!(Element::from( 207 | widget::svg(widget::svg::Handle::from_memory( 208 | self.graph().as_bytes().to_owned(), 209 | )) 210 | .width(90) 211 | .height(60) 212 | )); 213 | 214 | rate = rate.push(Element::from(cosmic::widget::text::body(""))); 215 | 216 | match self.config.variant { 217 | DisksVariant::Combined => { 218 | rate = rate.push(cosmic::widget::text::body(wrrate)); 219 | rate = rate.push(cosmic::widget::text::body(rdrate)); 220 | } 221 | DisksVariant::Write => { 222 | rate = rate.push(cosmic::widget::text::body(wrrate)); 223 | } 224 | DisksVariant::Read => { 225 | rate = rate.push(cosmic::widget::text::body(rdrate)); 226 | } 227 | } 228 | disk_elements.push(Element::from(rate)); 229 | 230 | let mut disk_bandwidth_items = Vec::new(); 231 | 232 | disk_bandwidth_items.push( 233 | settings::item( 234 | fl!("enable-chart"), 235 | widget::toggler(config.chart).on_toggle(move |t| Message::ToggleDisksChart(k, t)), 236 | ) 237 | .into(), 238 | ); 239 | disk_bandwidth_items.push( 240 | settings::item( 241 | fl!("enable-label"), 242 | widget::toggler(config.label).on_toggle(move |t| Message::ToggleDisksLabel(k, t)), 243 | ) 244 | .into(), 245 | ); 246 | 247 | disk_bandwidth_items.push( 248 | row!( 249 | widget::horizontal_space(), 250 | widget::button::standard(fl!("change-colors")).on_press(Message::ColorPickerOpen( 251 | DeviceKind::Disks(self.config.variant), 252 | GraphKind::Line, 253 | None 254 | )), 255 | widget::horizontal_space() 256 | ) 257 | .into(), 258 | ); 259 | 260 | let disk_right_column = Column::with_children(disk_bandwidth_items); 261 | 262 | disk_elements.push(Element::from(disk_right_column.spacing(cosmic.space_xs()))); 263 | 264 | let title_content = match self.config.variant { 265 | DisksVariant::Combined => fl!("disks-title-combined"), 266 | DisksVariant::Write => fl!("disks-title-write"), 267 | DisksVariant::Read => fl!("disks-title-read"), 268 | }; 269 | let title = widget::text::heading(title_content); 270 | 271 | column![ 272 | title, 273 | Row::with_children(disk_elements).align_y(Alignment::Center) 274 | ] 275 | .spacing(cosmic::theme::spacing().space_xs) 276 | .into() 277 | } 278 | } 279 | 280 | impl Default for Disks { 281 | fn default() -> Self { 282 | let disks = DisksInfo::new_with_refreshed_list(); 283 | Disks { 284 | disks, 285 | write: VecDeque::from(vec![0; MAX_SAMPLES]), 286 | read: VecDeque::from(vec![0; MAX_SAMPLES]), 287 | max_y: None, 288 | svg_colors: SvgColors::new(&GraphColors::default()), 289 | config: DisksConfig::default(), 290 | refresh_rate: 1000, 291 | } 292 | } 293 | } 294 | 295 | impl Disks { 296 | fn makestr(val: u64, format: UnitVariant) -> String { 297 | let mut formatted = String::with_capacity(20); 298 | 299 | let mut value = val as f64; 300 | let mut unit_index = 0; 301 | let units = if format == UnitVariant::Short { 302 | UNITS_SHORT 303 | } else { 304 | UNITS_LONG 305 | }; 306 | 307 | // Find the appropriate unit 308 | while value >= 999.0 && unit_index < units.len() - 1 { 309 | value /= 1024.0; 310 | unit_index += 1; 311 | } 312 | 313 | let s = if value < 10.0 { 314 | &format!("{value:.2}") 315 | } else if value < 99.0 { 316 | &format!("{value:.1}") 317 | } else { 318 | &format!("{value:.0}") 319 | }; 320 | 321 | if format == UnitVariant::Long { 322 | if s.len() == 3 { 323 | formatted.push(' '); 324 | } 325 | if unit_index == 0 { 326 | formatted.push(' '); 327 | } 328 | } 329 | formatted.push_str(s); 330 | 331 | if format == UnitVariant::Long { 332 | formatted.push(' '); 333 | } 334 | 335 | formatted.push_str(units[unit_index]); 336 | 337 | if format == UnitVariant::Long { 338 | let padding = 9usize.saturating_sub(formatted.len()); 339 | if padding > 0 { 340 | formatted = " ".repeat(padding) + &formatted; 341 | } 342 | } 343 | 344 | formatted 345 | } 346 | 347 | // If the sample rate doesn't match exactly one second (more or less), 348 | // we grab enough samples to cover it and average the value of samples cover a longer duration. 349 | fn last_second_rate(samples: &VecDeque, sample_interval_ms: u32) -> u64 { 350 | let mut total_duration = 0u32; 351 | let mut total_bitrate = 0u64; 352 | 353 | // Iterate from newest to oldest 354 | for &bitrate in samples.iter().rev() { 355 | if total_duration >= 1000 { 356 | break; 357 | } 358 | 359 | total_bitrate += bitrate; 360 | total_duration += sample_interval_ms; 361 | } 362 | 363 | // Scale to exactly 1000ms 364 | let scale = 1000.0 / f64::from(total_duration); 365 | 366 | (total_bitrate as f64 * scale).floor() as u64 367 | } 368 | 369 | // Get bytes per second 370 | pub fn write_label(&self, sample_interval_ms: u32, format: UnitVariant) -> String { 371 | let val = Disks::last_second_rate(&self.write, sample_interval_ms); 372 | Disks::makestr(val, format) 373 | } 374 | 375 | // Get bytes per second 376 | pub fn read_label(&self, sample_interval_ms: u32, format: UnitVariant) -> String { 377 | let val = Disks::last_second_rate(&self.read, sample_interval_ms); 378 | Disks::makestr(val, format) 379 | } 380 | } 381 | 382 | const DL_DEMO: [u64; 21] = [ 383 | 208, 2071, 0, 1056588, 912575, 912875, 912975, 912600, 1397, 1173024, 1228, 6910, 2493, 384 | 1102101, 380, 2287, 1109656, 1541, 3798, 1132822, 68479, 385 | ]; 386 | const UL_DEMO: [u64; 21] = [ 387 | 0, 1687, 0, 9417, 9161, 838, 6739, 1561, 212372, 312372, 412372, 512372, 512372, 512372, 388 | 412372, 312372, 112372, 864, 0, 8587, 760, 389 | ]; 390 | -------------------------------------------------------------------------------- /src/sensors/gpu/amd.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result, anyhow}; 2 | use hex; 3 | use log::{debug, info}; 4 | use sha2::{Digest, Sha256}; 5 | use std::collections::HashMap; 6 | use std::fs; 7 | use std::io; 8 | use std::path::{Path, PathBuf}; 9 | use std::process::Command; 10 | use std::sync::LazyLock; 11 | 12 | use crate::sensors::gpus::Gpu; 13 | //use log::{debug, warn}; 14 | 15 | pub struct AmdGpu { 16 | name: String, 17 | id: String, 18 | usage_path: String, 19 | vram_used_path: String, 20 | power_status_path: String, 21 | temp_input_path: Option, 22 | vram_total: u64, 23 | paused: bool, 24 | } 25 | 26 | impl AmdGpu { 27 | pub fn new(name: &str, card: &str, id: &str, vram_total: u64) -> Self { 28 | let base = format!("/sys/class/drm/{card}/device"); 29 | let temp_input_path = AmdGpu::find_temp_input_path(card); 30 | Self { 31 | name: name.to_string(), 32 | id: id.to_string(), 33 | usage_path: format!("{base}/gpu_busy_percent"), 34 | vram_used_path: format!("{base}/mem_info_vram_used"), 35 | power_status_path: format!("{base}/power/runtime_status"), 36 | temp_input_path, 37 | vram_total, 38 | paused: false, 39 | } 40 | } 41 | 42 | fn powered_on(&self) -> bool { 43 | Self::read_file_to_string(&self.power_status_path).map_or(true, |s| s != "suspended") 44 | } 45 | 46 | fn parse_u32_file(path: &str) -> Option { 47 | Self::read_file_to_string(path).ok()?.parse().ok() 48 | } 49 | 50 | fn parse_u64_file(path: &str) -> Option { 51 | Self::read_file_to_string(path).ok()?.parse().ok() 52 | } 53 | 54 | fn read_file_to_string>(path: P) -> io::Result { 55 | fs::read_to_string(path).map(|s| s.trim().to_string()) 56 | } 57 | 58 | fn get_amd_cards() -> Vec { 59 | debug!("AmdGpu::get_amd_cards()."); 60 | let mut cards = Vec::new(); 61 | if let Ok(entries) = fs::read_dir("/sys/class/drm/") { 62 | for entry in entries.flatten() { 63 | let path = entry.path(); 64 | debug!(" entry {path:?}"); 65 | if path.join("device/vendor").exists() { 66 | if let Ok(vendor_id) = Self::read_file_to_string(path.join("device/vendor")) { 67 | if vendor_id == "0x1002" { 68 | debug!(" AMD vendor ID"); 69 | if let Some(card) = path.file_name().and_then(|n| n.to_str()) { 70 | if card.contains("card") { 71 | debug!(" phyical Card."); 72 | cards.push(card.to_string()); 73 | } else { 74 | debug!(" virtual card"); 75 | } 76 | } 77 | } else { 78 | debug!(" Not AMD"); 79 | } 80 | } 81 | } 82 | } 83 | } 84 | cards 85 | } 86 | 87 | fn find_temp_input_path(card: &str) -> Option { 88 | log::info!("AMD find_temp_input_path({card})"); 89 | let hwmon_base = format!("/sys/class/drm/{card}/device/hwmon"); 90 | let entries = fs::read_dir(hwmon_base).ok()?; 91 | 92 | for entry in entries.flatten() { 93 | let path = entry.path().join("temp1_input"); 94 | if path.exists() { 95 | log::info!(" Found temperature file {path:?}"); 96 | return Some(path.to_string_lossy().to_string()); 97 | } 98 | } 99 | 100 | log::info!(" Couldn't find temp1_input."); 101 | None 102 | } 103 | 104 | fn get_vram_total(card: &str) -> Option { 105 | let path = format!("/sys/class/drm/{card}/device/mem_info_vram_total"); 106 | Self::parse_u64_file(&path) 107 | } 108 | 109 | fn get_pci_slot(card: &str) -> Option { 110 | let path = format!("/sys/class/drm/{card}/device/uevent"); 111 | Self::read_file_to_string(path) 112 | .ok()? 113 | .lines() 114 | .find_map(|line| { 115 | line.strip_prefix("PCI_SLOT_NAME=") 116 | .map(|s| s.to_lowercase().to_string()) 117 | }) 118 | } 119 | 120 | fn get_lspci_gpu_names() -> Vec<(String, String)> { 121 | fn clean_gpu_name(model: &str) -> String { 122 | let (_, truncated) = model.split_once("]:").unwrap_or((model, model)); 123 | let truncated = truncated.split("[1002:").next().unwrap_or(model); 124 | truncated 125 | .replace("Corporation", "") 126 | .replace("[AMD/ATI]", "") 127 | .replace("compatible controller", "") 128 | .replace("controller", "") 129 | .replace("VGA", "") 130 | .replace("3D", "") 131 | .replace("Display", "") 132 | .replace(':', "") 133 | .replace(" ", " ") 134 | .replace('[', "(") 135 | .replace(']', ")") 136 | .trim() 137 | .to_string() 138 | } 139 | 140 | let mut map = Vec::new(); 141 | let output = Command::new("lspci").arg("-nn").output(); 142 | let Ok(output) = output else { 143 | return map; 144 | }; 145 | let Ok(stdout) = String::from_utf8(output.stdout) else { 146 | return map; 147 | }; 148 | 149 | for line in stdout.lines() { 150 | if line.contains("VGA") || line.contains("Display") || line.contains("3D") { 151 | if let Some((slot, rest)) = line.split_once(' ') { 152 | let model = rest.trim(); 153 | let name = clean_gpu_name(model); 154 | map.push((slot.to_lowercase().to_string(), name)); 155 | } 156 | } 157 | } 158 | map 159 | } 160 | 161 | fn get_gpu_name(card: &str, lspci_map: &Vec<(String, String)>) -> String { 162 | info!("Resolving GPU name for card: {card}"); 163 | 164 | // Use static lookup table first, with nice names 165 | let device_id_path = format!("/sys/class/drm/{card}/device/device"); 166 | if let Ok(dev_id) = AmdGpu::read_file_to_string(&device_id_path) { 167 | info!("Read device ID from sysfs: {dev_id}"); 168 | if let Some(name) = AMD_GPU_DEVICE_IDS.get(dev_id.to_uppercase().as_str()) { 169 | debug!("Found name in static map: {name}"); 170 | return (*name).to_string(); 171 | } 172 | info!("No entry in static map for device ID: {dev_id}"); 173 | } else { 174 | debug!("Failed to read device ID from path: {device_id_path}"); 175 | } 176 | 177 | // Fallback: Get PCI slot and look for it in the lspci list 178 | if let Some(slot) = &AmdGpu::get_pci_slot(card) { 179 | info!("Resolved PCI slot for card {card}: {slot:?}"); 180 | for (p, n) in lspci_map { 181 | if slot.contains(p) { 182 | info!("Found name in lspci_map: {n}"); 183 | return n.clone(); 184 | } 185 | } 186 | debug!("No entry in lspci_map for slot: {slot}"); 187 | } 188 | 189 | debug!("Falling back to unknown GPU name"); 190 | "Unknown AMD GPU".to_string() 191 | } 192 | 193 | fn generate_gpu_id(card: &str) -> Option { 194 | let device_path = PathBuf::from(format!("/sys/class/drm/{card}/device")); 195 | let pci_address = device_path.canonicalize().ok()?; 196 | let subsystem_vendor = 197 | Self::read_file_to_string(device_path.join("subsystem_vendor")).ok()?; 198 | let subsystem_device = 199 | Self::read_file_to_string(device_path.join("subsystem_device")).ok()?; 200 | 201 | let mut hasher = Sha256::new(); 202 | hasher.update(pci_address.to_string_lossy().as_bytes()); 203 | hasher.update(subsystem_vendor.as_bytes()); 204 | hasher.update(subsystem_device.as_bytes()); 205 | 206 | Some(hex::encode(hasher.finalize())) 207 | } 208 | 209 | pub fn get_gpus() -> Vec { 210 | debug!("AmdGpu::get_gpus()."); 211 | 212 | let mut gpus = Vec::new(); 213 | 214 | let lspci_map = AmdGpu::get_lspci_gpu_names(); 215 | debug!("Available lspci_map entries:"); 216 | for (k, v) in &lspci_map { 217 | debug!(" {k} -> {v}"); 218 | } 219 | 220 | let cards = AmdGpu::get_amd_cards(); 221 | 222 | for card in cards { 223 | debug!(" Found card {card}"); 224 | if let Some(vram_total) = AmdGpu::get_vram_total(&card) { 225 | debug!(" total vram {vram_total}"); 226 | if let Some(id) = AmdGpu::generate_gpu_id(&card) { 227 | debug!(" id {id}"); 228 | let name = AmdGpu::get_gpu_name(&card, &lspci_map); 229 | debug!(" name {name}"); 230 | gpus.push(Gpu::new(Box::new(AmdGpu::new( 231 | &name, &card, &id, vram_total, 232 | )))); 233 | } 234 | } 235 | } 236 | gpus 237 | } 238 | } 239 | 240 | impl super::GpuIf for AmdGpu { 241 | fn restart(&mut self) { 242 | debug!("AmdGpu::restart({}).", self.name); 243 | self.paused = false; 244 | } 245 | 246 | fn stop(&mut self) { 247 | debug!("AmdGpu::stop({}).", self.name); 248 | self.paused = true; 249 | } 250 | 251 | fn is_active(&self) -> bool { 252 | !self.paused 253 | } 254 | 255 | fn name(&self) -> String { 256 | self.name.clone() 257 | } 258 | 259 | fn id(&self) -> String { 260 | self.id.clone() 261 | } 262 | 263 | fn vram_total(&self) -> u64 { 264 | debug!("AmdGpu::vram_total({}) - {}.", self.name, self.vram_total); 265 | self.vram_total 266 | } 267 | 268 | fn usage(&self) -> Result { 269 | if !self.is_active() { 270 | return Err(anyhow!("AMD device paused")); 271 | } 272 | if !self.powered_on() { 273 | return Ok(0); 274 | } 275 | Ok(Self::parse_u32_file(&self.usage_path).unwrap_or(0)) 276 | } 277 | 278 | fn temperature(&self) -> Result { 279 | if !self.powered_on() { 280 | return Ok(0); 281 | } 282 | 283 | let path = self 284 | .temp_input_path 285 | .as_ref() 286 | .context("Temperature path not found")?; 287 | 288 | let contents = fs::read_to_string(path) 289 | .with_context(|| format!("Failed to read temperature from {path}"))?; 290 | 291 | let temp_millidegrees: u32 = contents 292 | .trim() 293 | .parse() 294 | .context("Failed to parse temperature value")?; 295 | 296 | Ok(temp_millidegrees) 297 | } 298 | 299 | fn vram_used(&self) -> Result { 300 | if !self.is_active() { 301 | return Err(anyhow!("AMD device paused")); 302 | } 303 | if !self.powered_on() { 304 | return Ok(0); 305 | } 306 | Ok(Self::parse_u64_file(&self.vram_used_path).unwrap_or(0)) 307 | } 308 | } 309 | 310 | impl std::fmt::Debug for AmdGpu { 311 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 312 | write!( 313 | f, 314 | "AmdGpu {{ name: {}, id: {}, paused: {} }}", 315 | self.name, self.id, self.paused 316 | ) 317 | } 318 | } 319 | 320 | // A hashmap containing AMD graphics card subsystem device IDs and their names 321 | // Keys are the values found in /sys/class/drm/card?/device/subsystem_device 322 | pub static AMD_GPU_DEVICE_IDS: LazyLock> = 323 | LazyLock::new(|| { 324 | let mut m = HashMap::new(); 325 | 326 | // Radeon RX 7000 Series 327 | m.insert("0x744C", "AMD Radeon RX 7700S"); 328 | m.insert("0x73FF", "AMD Radeon RX 7900 XTX"); 329 | m.insert("0x73DF", "AMD Radeon RX 7900 XT"); 330 | m.insert("0x7470", "AMD Radeon RX 7800 XT"); 331 | m.insert("0x7460", "AMD Radeon RX 7700 XT"); 332 | m.insert("0x7420", "AMD Radeon RX 7600"); 333 | m.insert("0x7422", "AMD Radeon RX 7600 XT"); 334 | 335 | // Radeon RX 6000 Series 336 | m.insert("0x73BF", "AMD Radeon RX 6950 XT"); 337 | m.insert("0x73A5", "AMD Radeon RX 6900 XT"); 338 | m.insert("0x73A3", "AMD Radeon RX 6800 XT"); 339 | m.insert("0x73AB", "AMD Radeon RX 6800"); 340 | m.insert("0x73DF", "AMD Radeon RX 6750 XT"); 341 | m.insert("0x73D5", "AMD Radeon RX 6700 XT"); 342 | m.insert("0x73FF", "AMD Radeon RX 6700"); 343 | m.insert("0x73EF", "AMD Radeon RX 6650 XT"); 344 | m.insert("0x73E8", "AMD Radeon RX 6600 XT"); 345 | m.insert("0x73E3", "AMD Radeon RX 6600"); 346 | m.insert("0x7422", "AMD Radeon RX 6500 XT"); 347 | m.insert("0x7424", "AMD Radeon RX 6400"); 348 | 349 | // Radeon RX 5000 Series 350 | m.insert("0x731F", "AMD Radeon RX 5700 XT"); 351 | m.insert("0x7340", "AMD Radeon RX 5700"); 352 | m.insert("0x7341", "AMD Radeon RX 5600 XT"); 353 | m.insert("0x7347", "AMD Radeon RX 5500 XT"); 354 | 355 | // Radeon RX Vega Series 356 | m.insert("0x687F", "AMD Radeon VII"); 357 | m.insert("0x6863", "AMD Radeon RX Vega 64"); 358 | m.insert("0x6867", "AMD Radeon RX Vega 56"); 359 | 360 | // Radeon RX 500 Series 361 | m.insert("0x67DF", "AMD Radeon RX 590"); 362 | m.insert("0x67FF", "AMD Radeon RX 580"); 363 | m.insert("0x67EF", "AMD Radeon RX 570"); 364 | m.insert("0x67E0", "AMD Radeon RX 560"); 365 | m.insert("0x699F", "AMD Radeon RX 550"); 366 | 367 | // APUs - Integrated Graphics 368 | m.insert("0x15BF", "AMD Radeon 780M iGPU"); 369 | m.insert("0x1681", "AMD Radeon 780M iGPU"); 370 | m.insert("0x15E7", "AMD Radeon 760M iGPU"); 371 | m.insert("0x15D8", "AMD Radeon 680M iGPU"); 372 | m.insert("0x1638", "AMD Radeon 660M iGPU"); 373 | m.insert("0x164C", "AMD Radeon 610M iGPU"); 374 | m.insert("0x15DD", "AMD Radeon Vega 8 iGPU"); 375 | m.insert("0x15D8", "AMD Radeon Vega 7 iGPU"); 376 | 377 | // Radeon Pro Series 378 | m.insert("0x73A2", "AMD Radeon Pro W6800"); 379 | m.insert("0x73A3", "AMD Radeon Pro W6600"); 380 | m.insert("0x6867", "AMD Radeon Pro VII"); 381 | m.insert("0x66AF", "AMD Radeon Pro WX 9100"); 382 | m.insert("0x67C4", "AMD Radeon Pro WX 7100"); 383 | 384 | m 385 | }); 386 | -------------------------------------------------------------------------------- /src/sensors/gpu/intel.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::sensors::gpus::Gpu; 4 | //use log::{debug, warn}; 5 | 6 | pub struct IntelGpu { 7 | pub name: String, 8 | pub id: String, 9 | } 10 | 11 | #[allow(dead_code)] 12 | impl IntelGpu { 13 | pub fn new(name: String, id: String) -> Self { 14 | IntelGpu { name, id } 15 | } 16 | } 17 | 18 | impl super::GpuIf for IntelGpu { 19 | fn restart(&mut self) { 20 | todo!(); 21 | } 22 | 23 | fn stop(&mut self) { 24 | todo!(); 25 | } 26 | 27 | fn is_active(&self) -> bool { 28 | todo!(); 29 | } 30 | 31 | fn name(&self) -> String { 32 | self.name.clone() 33 | } 34 | 35 | fn id(&self) -> String { 36 | self.id.clone() 37 | } 38 | 39 | fn usage(&self) -> Result { 40 | todo!(); 41 | } 42 | 43 | fn temperature(&self) -> Result { 44 | todo!(); 45 | } 46 | 47 | fn vram_total(&self) -> u64 { 48 | todo!(); 49 | } 50 | 51 | fn vram_used(&self) -> Result { 52 | todo!(); 53 | } 54 | } 55 | 56 | impl IntelGpu { 57 | pub fn get_gpus() -> Vec { 58 | Vec::new() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/sensors/gpu/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | pub mod amd; 4 | pub mod intel; 5 | pub mod nvidia; 6 | 7 | pub trait GpuIf { 8 | fn name(&self) -> String; 9 | fn id(&self) -> String; 10 | fn usage(&self) -> Result; 11 | 12 | // Temp in millidegrees Celcius 13 | fn temperature(&self) -> Result; 14 | fn vram_total(&self) -> u64; 15 | fn vram_used(&self) -> Result; 16 | 17 | // Stop polling, to allow it to sleep 18 | fn stop(&mut self); 19 | // Resume active polling 20 | fn restart(&mut self); 21 | // Stopped or active for polling? 22 | fn is_active(&self) -> bool; 23 | } 24 | -------------------------------------------------------------------------------- /src/sensors/gpu/nvidia.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result, anyhow}; 2 | use log::{debug, info, warn}; 3 | use nvml_wrapper::{Device, Nvml, error::NvmlError}; 4 | 5 | use std::sync::LazyLock; 6 | 7 | use crate::sensors::gpus::Gpu; 8 | 9 | pub static NVML: LazyLock> = LazyLock::new(|| { 10 | let nvml = Nvml::init(); 11 | 12 | if let Err(error) = nvml.as_ref() { 13 | warn!("Connection to NVML failed, reason: {error}"); 14 | } else { 15 | debug!("Successfully connected to NVML"); 16 | } 17 | nvml 18 | }); 19 | 20 | pub struct NvidiaGpu<'a> { 21 | // Index returned by NVML 22 | pub index: u32, 23 | pub name: String, 24 | pub uuid: String, 25 | vram_total: u64, 26 | device: Option>, 27 | } 28 | 29 | impl NvidiaGpu<'_> { 30 | pub fn new(index: u32, name: String, uuid: String) -> Self { 31 | let mut device = None; 32 | let mut vram = 0; 33 | 34 | if let Ok(nvml) = NVML.as_ref() { 35 | if let Ok(dev) = nvml.device_by_index(index) { 36 | if let Ok(mem) = dev.memory_info() { 37 | vram = mem.total; 38 | device = Some(dev); 39 | } 40 | } 41 | } 42 | 43 | NvidiaGpu { 44 | index, 45 | name, 46 | uuid, 47 | vram_total: vram, 48 | device, 49 | } 50 | } 51 | } 52 | 53 | impl super::GpuIf for NvidiaGpu<'_> { 54 | fn restart(&mut self) { 55 | if self.device.is_none() { 56 | if let Ok(nvml) = NVML.as_ref() { 57 | if let Ok(dev) = nvml.device_by_index(self.index) { 58 | self.device = Some(dev); 59 | } 60 | } 61 | } 62 | } 63 | 64 | fn stop(&mut self) { 65 | if self.device.is_some() { 66 | // Drop device 67 | self.device = None; 68 | } 69 | } 70 | 71 | fn is_active(&self) -> bool { 72 | self.device.is_some() 73 | } 74 | 75 | fn name(&self) -> String { 76 | self.name.clone() 77 | } 78 | 79 | fn id(&self) -> String { 80 | self.uuid.clone() 81 | } 82 | 83 | fn usage(&self) -> Result { 84 | self.with_device(|device_ref| { 85 | let rates = device_ref.utilization_rates()?; 86 | Ok(rates.gpu) 87 | }) 88 | } 89 | 90 | fn temperature(&self) -> Result { 91 | self.with_device(|device_ref| { 92 | let temp = device_ref 93 | .temperature(nvml_wrapper::enum_wrappers::device::TemperatureSensor::Gpu)? 94 | * 1000; 95 | Ok(temp) 96 | }) 97 | } 98 | 99 | fn vram_total(&self) -> u64 { 100 | self.vram_total 101 | } 102 | 103 | fn vram_used(&self) -> Result { 104 | self.with_device(|device_ref| { 105 | let mem = device_ref.memory_info()?; 106 | Ok(mem.used) 107 | }) 108 | } 109 | } 110 | 111 | impl NvidiaGpu<'_> { 112 | pub fn get_gpus() -> Vec { 113 | let mut v: Vec = Vec::new(); 114 | 115 | // Nvidia GPUs 116 | if let Ok(count) = NvidiaGpu::gpus() { 117 | let nvidia_gpus = (0..count) 118 | .filter_map(|i| { 119 | // Try to get both name and UUID, skip this GPU if either fails 120 | let name = NvidiaGpu::name(i).ok()?; 121 | let uuid = NvidiaGpu::uuid(i).ok()?; 122 | 123 | Some(Gpu::new(Box::new(NvidiaGpu::new(i, name, uuid)))) 124 | }) 125 | .collect::>(); 126 | 127 | v.extend(nvidia_gpus); 128 | } else { 129 | info!("No Nvidia GPUs found"); 130 | } 131 | v 132 | } 133 | 134 | pub fn uuid(idx: u32) -> Result { 135 | NVML.as_ref() 136 | .context("unable to establish NVML connection") 137 | .and_then(|nvml| { 138 | let dev = nvml.device_by_index(idx)?; 139 | dev.uuid().context("Unable to retrieve uuid") 140 | }) 141 | } 142 | 143 | pub fn name(idx: u32) -> Result { 144 | NVML.as_ref() 145 | .context("unable to establish NVML connection") 146 | .and_then(|nvml| { 147 | let dev = nvml.device_by_index(idx)?; 148 | dev.name().context("Unable to retrieve name") 149 | }) 150 | } 151 | 152 | fn gpus() -> Result { 153 | NVML.as_ref() 154 | .context("unable to establish NVML connection") 155 | .and_then(|nvml| nvml.device_count().context("failed to get GPU count")) 156 | } 157 | 158 | fn with_device(&self, f: F) -> Result 159 | where 160 | F: FnOnce(&Device) -> Result, 161 | { 162 | match self.device.as_ref() { 163 | Some(device_ref) => f(device_ref), 164 | None => Err(anyhow!("nvml device not loaded")), 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/sensors/memory.rs: -------------------------------------------------------------------------------- 1 | use cosmic::Element; 2 | use sysinfo::{MemoryRefreshKind, System}; 3 | 4 | use crate::{ 5 | colorpicker::DemoGraph, 6 | config::{ColorVariant, DeviceKind, GraphColors, GraphKind, MemoryConfig}, 7 | fl, 8 | svg_graph::SvgColors, 9 | }; 10 | 11 | use cosmic::widget; 12 | use cosmic::widget::{settings, toggler}; 13 | use std::any::Any; 14 | 15 | use cosmic::{ 16 | iced::{ 17 | Alignment, 18 | widget::{column, row}, 19 | }, 20 | iced_widget::Row, 21 | }; 22 | 23 | use crate::app::Message; 24 | 25 | use std::{collections::VecDeque, fmt::Write}; 26 | 27 | use super::Sensor; 28 | 29 | const GRAPH_OPTIONS: [&str; 2] = ["Ring", "Line"]; 30 | 31 | const MAX_SAMPLES: usize = 21; 32 | 33 | #[derive(Debug)] 34 | pub struct Memory { 35 | samples: VecDeque, 36 | max_val: f64, 37 | system: System, 38 | graph_options: Vec<&'static str>, 39 | /// colors cached so we don't need to convert to string every time 40 | svg_colors: SvgColors, 41 | config: MemoryConfig, 42 | } 43 | 44 | impl DemoGraph for Memory { 45 | fn demo(&self) -> String { 46 | match self.config.kind { 47 | GraphKind::Ring => { 48 | // show a number of 40% of max 49 | let val = self.max_val * 0.4; 50 | let percentage: u64 = ((val / self.max_val) * 100.0) as u64; 51 | crate::svg_graph::ring( 52 | &format!("{val}"), 53 | &format!("{percentage}"), 54 | &self.svg_colors, 55 | ) 56 | } 57 | GraphKind::Line => crate::svg_graph::line( 58 | &VecDeque::from(DEMO_SAMPLES), 59 | self.max_val, 60 | &self.svg_colors, 61 | ), 62 | GraphKind::Heat => panic!("Wrong graph choice!"), 63 | } 64 | } 65 | 66 | fn colors(&self) -> GraphColors { 67 | self.config.colors 68 | } 69 | 70 | fn set_colors(&mut self, colors: GraphColors) { 71 | self.config.colors = colors; 72 | self.svg_colors.set_colors(&colors); 73 | } 74 | 75 | fn color_choices(&self) -> Vec<(&'static str, ColorVariant)> { 76 | if self.config.kind == GraphKind::Line { 77 | (*super::COLOR_CHOICES_LINE).into() 78 | } else { 79 | (*super::COLOR_CHOICES_RING).into() 80 | } 81 | } 82 | 83 | fn id(&self) -> Option { 84 | None 85 | } 86 | } 87 | 88 | impl Sensor for Memory { 89 | fn update_config(&mut self, config: &dyn Any, _refresh_rate: u32) { 90 | if let Some(cfg) = config.downcast_ref::() { 91 | self.config = cfg.clone(); 92 | self.svg_colors.set_colors(&cfg.colors); 93 | } 94 | } 95 | 96 | fn graph_kind(&self) -> GraphKind { 97 | self.config.kind 98 | } 99 | 100 | fn set_graph_kind(&mut self, kind: GraphKind) { 101 | assert!(kind == GraphKind::Line || kind == GraphKind::Ring); 102 | self.config.kind = kind; 103 | } 104 | 105 | fn update(&mut self) { 106 | let r = MemoryRefreshKind::nothing().with_ram(); 107 | 108 | self.system.refresh_memory_specifics(r); 109 | let new_val: f64 = self.system.used_memory() as f64 / 1_073_741_824.0; 110 | 111 | if self.samples.len() >= MAX_SAMPLES { 112 | self.samples.pop_front(); 113 | } 114 | self.samples.push_back(new_val); 115 | } 116 | 117 | fn demo_graph(&self) -> Box { 118 | let mut dmo = Memory::default(); 119 | dmo.update_config(&self.config, 0); 120 | Box::new(dmo) 121 | } 122 | 123 | fn graph(&self) -> String { 124 | if self.config.kind == GraphKind::Ring { 125 | let mut latest = self.latest_sample(); 126 | let mut value = String::with_capacity(10); 127 | let mut percentage = String::with_capacity(10); 128 | 129 | let mut pct: u64 = ((latest / self.max_val) * 100.0) as u64; 130 | if pct > 100 { 131 | pct = 100; 132 | } 133 | 134 | write!(percentage, "{pct}").unwrap(); 135 | 136 | // If set, convert to percentage 137 | if self.config.percentage { 138 | latest = (latest * 100.0) / self.max_val; 139 | } 140 | 141 | if latest < 10.0 { 142 | write!(value, "{latest:.2}").unwrap(); 143 | } else if latest <= 99.9 { 144 | write!(value, "{latest:.1}").unwrap(); 145 | } else { 146 | write!(value, "100").unwrap(); 147 | } 148 | 149 | crate::svg_graph::ring(&value, &percentage, &self.svg_colors) 150 | } else { 151 | crate::svg_graph::line(&self.samples, self.max_val, &self.svg_colors) 152 | } 153 | } 154 | 155 | fn settings_ui(&self) -> Element { 156 | let theme = cosmic::theme::active(); 157 | let cosmic = theme.cosmic(); 158 | 159 | let mut mem_elements = Vec::new(); 160 | let mem = self.to_string(false); 161 | mem_elements.push(Element::from( 162 | column!( 163 | widget::svg(widget::svg::Handle::from_memory( 164 | self.graph().as_bytes().to_owned(), 165 | )) 166 | .width(90) 167 | .height(60), 168 | cosmic::widget::text::body(mem), 169 | ) 170 | .padding(5) 171 | .align_x(Alignment::Center), 172 | )); 173 | 174 | let config = &self.config; 175 | let selected: Option = Some(self.graph_kind().into()); 176 | let mem_kind = self.graph_kind(); 177 | mem_elements.push(Element::from( 178 | column!( 179 | settings::item( 180 | fl!("enable-chart"), 181 | toggler(config.chart).on_toggle(|value| { Message::ToggleMemoryChart(value) }), 182 | ), 183 | settings::item( 184 | fl!("enable-label"), 185 | toggler(config.label).on_toggle(|value| { Message::ToggleMemoryLabel(value) }), 186 | ), 187 | settings::item( 188 | fl!("memory-as-percentage"), 189 | toggler(config.percentage).on_toggle(Message::ToggleMemoryPercentage), 190 | ), 191 | row!( 192 | widget::dropdown(&self.graph_options, selected, move |m| { 193 | Message::SelectGraphType(DeviceKind::Memory, m.into()) 194 | },) 195 | .width(70), 196 | widget::horizontal_space(), 197 | widget::button::standard(fl!("change-colors")) 198 | .on_press(Message::ColorPickerOpen(DeviceKind::Memory, mem_kind, None)), 199 | ) 200 | ) 201 | .spacing(cosmic.space_xs()), 202 | )); 203 | 204 | Row::with_children(mem_elements) 205 | .align_y(Alignment::Center) 206 | .spacing(0) 207 | .into() 208 | } 209 | } 210 | 211 | impl Default for Memory { 212 | fn default() -> Self { 213 | let mut system = System::new(); 214 | system.refresh_memory(); 215 | 216 | let max_val: f64 = system.total_memory() as f64 / 1_073_741_824.0; 217 | log::info!( 218 | "System memory: {} / {:.2} GB", 219 | system.total_memory(), 220 | max_val 221 | ); 222 | 223 | // value and percentage are pre-allocated and reused as they're changed often. 224 | let mut percentage = String::with_capacity(6); 225 | write!(percentage, "0").unwrap(); 226 | 227 | let mut value = String::with_capacity(6); 228 | write!(value, "0").unwrap(); 229 | 230 | let mut memory = Memory { 231 | samples: VecDeque::from(vec![0.0; MAX_SAMPLES]), 232 | max_val, 233 | system, 234 | config: MemoryConfig::default(), 235 | graph_options: GRAPH_OPTIONS.to_vec(), 236 | svg_colors: SvgColors::new(&GraphColors::default()), 237 | }; 238 | memory.set_colors(GraphColors::default()); 239 | memory 240 | } 241 | } 242 | 243 | impl Memory { 244 | pub fn set_percentage(&mut self, percentage: bool) { 245 | self.config.percentage = percentage; 246 | } 247 | 248 | pub fn latest_sample(&self) -> f64 { 249 | *self.samples.back().unwrap_or(&0f64) 250 | } 251 | 252 | pub fn total(&self) -> f64 { 253 | self.max_val 254 | } 255 | 256 | pub fn to_string(&self, vertical_panel: bool) -> String { 257 | let mut current_val = self.latest_sample(); 258 | let unit: &str; 259 | 260 | if self.config.percentage { 261 | current_val = (current_val * 100.0) / self.max_val; 262 | unit = "%"; 263 | } else if !vertical_panel { 264 | unit = " GB"; 265 | } else { 266 | unit = "GB"; 267 | } 268 | 269 | if current_val < 10.0 { 270 | format!("{current_val:.2}{unit}") 271 | } else if current_val < 100.0 { 272 | format!("{current_val:.1}{unit}") 273 | } else { 274 | format!("{current_val}{unit}") 275 | } 276 | } 277 | } 278 | 279 | const DEMO_SAMPLES: [f64; 21] = [ 280 | 0.0, 281 | 12.689857482910156, 282 | 12.642768859863281, 283 | 12.615306854248047, 284 | 12.658184051513672, 285 | 12.65273666381836, 286 | 12.626102447509766, 287 | 12.624862670898438, 288 | 12.613967895507813, 289 | 12.619949340820313, 290 | 19.061111450195313, 291 | 21.691085815429688, 292 | 21.810935974121094, 293 | 21.28915786743164, 294 | 22.041973114013672, 295 | 21.764171600341797, 296 | 21.89263916015625, 297 | 15.258216857910156, 298 | 14.770732879638672, 299 | 14.496528625488281, 300 | 13.892818450927734, 301 | ]; 302 | -------------------------------------------------------------------------------- /src/sensors/mod.rs: -------------------------------------------------------------------------------- 1 | use cosmic::Element; 2 | use serde::{Deserialize, Serialize}; 3 | use std::sync::LazyLock; 4 | 5 | use crate::{ 6 | config::{ColorVariant, GpuConfig}, 7 | fl, 8 | }; 9 | 10 | pub static COLOR_CHOICES_RING: LazyLock<[(&'static str, ColorVariant); 4]> = LazyLock::new(|| { 11 | [ 12 | (fl!("graph-ring-r1").leak(), ColorVariant::Color4), 13 | (fl!("graph-ring-r2").leak(), ColorVariant::Color3), 14 | (fl!("graph-ring-back").leak(), ColorVariant::Color1), 15 | (fl!("graph-ring-text").leak(), ColorVariant::Color2), 16 | ] 17 | }); 18 | 19 | pub static COLOR_CHOICES_LINE: LazyLock<[(&'static str, ColorVariant); 3]> = LazyLock::new(|| { 20 | [ 21 | (fl!("graph-line-graph").leak(), ColorVariant::Color4), 22 | (fl!("graph-line-back").leak(), ColorVariant::Color1), 23 | (fl!("graph-line-frame").leak(), ColorVariant::Color2), 24 | ] 25 | }); 26 | 27 | pub static COLOR_CHOICES_HEAT: LazyLock<[(&'static str, ColorVariant); 2]> = LazyLock::new(|| { 28 | [ 29 | (fl!("graph-line-back").leak(), ColorVariant::Color1), 30 | (fl!("graph-line-frame").leak(), ColorVariant::Color2), 31 | ] 32 | }); 33 | 34 | use crate::{colorpicker::DemoGraph, config::GraphKind}; 35 | 36 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] 37 | pub enum TempUnit { 38 | Celcius, 39 | Farenheit, 40 | Kelvin, 41 | Rankine, 42 | } 43 | 44 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 45 | pub enum CpuVariant { 46 | Amd, 47 | Intel, 48 | } 49 | 50 | use std::any::Any; 51 | pub trait Sensor: Default { 52 | fn update_config(&mut self, config: &dyn Any, refresh_rate: u32); 53 | fn graph_kind(&self) -> GraphKind; 54 | fn set_graph_kind(&mut self, kind: GraphKind); 55 | fn update(&mut self); 56 | fn demo_graph(&self) -> Box; 57 | fn graph(&self) -> String; 58 | fn settings_ui(&self) -> Element; 59 | } 60 | 61 | pub mod cpu; 62 | pub mod cputemp; 63 | pub mod disks; 64 | pub mod gpu; 65 | pub mod gpus; 66 | pub mod memory; 67 | pub mod network; 68 | 69 | impl From for TempUnit { 70 | fn from(index: usize) -> Self { 71 | match index { 72 | 0 => TempUnit::Celcius, 73 | 1 => TempUnit::Farenheit, 74 | 2 => TempUnit::Kelvin, 75 | 3 => TempUnit::Rankine, 76 | _ => panic!("Invalid index for TempUnit"), 77 | } 78 | } 79 | } 80 | 81 | impl From for usize { 82 | fn from(kind: TempUnit) -> Self { 83 | match kind { 84 | TempUnit::Celcius => 0, 85 | TempUnit::Farenheit => 1, 86 | TempUnit::Kelvin => 2, 87 | TempUnit::Rankine => 3, 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/sensors/network.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use cosmic::{Element, iced_widget::Column}; 4 | use sysinfo::Networks; 5 | 6 | use crate::{ 7 | colorpicker::DemoGraph, 8 | config::{ColorVariant, DeviceKind, GraphColors, GraphKind, NetworkConfig, NetworkVariant}, 9 | fl, 10 | svg_graph::SvgColors, 11 | }; 12 | 13 | use cosmic::widget; 14 | use cosmic::widget::settings; 15 | 16 | use crate::app::Message; 17 | use cosmic::{ 18 | iced::{ 19 | Alignment, 20 | widget::{column, row}, 21 | }, 22 | iced_widget::Row, 23 | }; 24 | use std::any::Any; 25 | 26 | use super::Sensor; 27 | 28 | const MAX_SAMPLES: usize = 30; 29 | const GRAPH_SAMPLES: usize = 21; 30 | const UNITS_SHORT: [&str; 5] = ["b", "K", "M", "G", "T"]; 31 | const UNITS_LONG: [&str; 5] = ["bps", "Kbps", "Mbps", "Gbps", "Tbps"]; 32 | use std::sync::LazyLock; 33 | 34 | pub static COLOR_CHOICES_COMBINED: LazyLock<[(&'static str, ColorVariant); 4]> = 35 | LazyLock::new(|| { 36 | [ 37 | (fl!("graph-network-download").leak(), ColorVariant::Color2), 38 | (fl!("graph-network-upload").leak(), ColorVariant::Color3), 39 | (fl!("graph-network-back").leak(), ColorVariant::Color1), 40 | (fl!("graph-network-frame").leak(), ColorVariant::Color4), 41 | ] 42 | }); 43 | 44 | pub static COLOR_CHOICES_DL: LazyLock<[(&'static str, ColorVariant); 3]> = LazyLock::new(|| { 45 | [ 46 | (fl!("graph-network-download").leak(), ColorVariant::Color2), 47 | (fl!("graph-network-back").leak(), ColorVariant::Color1), 48 | (fl!("graph-network-frame").leak(), ColorVariant::Color4), 49 | ] 50 | }); 51 | 52 | pub static COLOR_CHOICES_UL: LazyLock<[(&'static str, ColorVariant); 3]> = LazyLock::new(|| { 53 | [ 54 | (fl!("graph-network-upload").leak(), ColorVariant::Color3), 55 | (fl!("graph-network-back").leak(), ColorVariant::Color1), 56 | (fl!("graph-network-frame").leak(), ColorVariant::Color4), 57 | ] 58 | }); 59 | 60 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 61 | pub enum UnitVariant { 62 | Short, 63 | Long, 64 | } 65 | 66 | #[derive(Debug)] 67 | pub struct Network { 68 | networks: Networks, 69 | download: VecDeque, 70 | upload: VecDeque, 71 | max_y: Option, 72 | svg_colors: SvgColors, 73 | dropdown_options: Vec<&'static str>, 74 | config: NetworkConfig, 75 | refresh_rate: u32, 76 | } 77 | 78 | impl DemoGraph for Network { 79 | fn demo(&self) -> String { 80 | let download = VecDeque::from(DL_DEMO); 81 | let upload = VecDeque::from(UL_DEMO); 82 | 83 | match self.config.variant { 84 | NetworkVariant::Combined => crate::svg_graph::double_line( 85 | &download, 86 | &upload, 87 | GRAPH_SAMPLES, 88 | &self.svg_colors, 89 | None, 90 | ), 91 | NetworkVariant::Download => { 92 | crate::svg_graph::line_adaptive(&download, GRAPH_SAMPLES, &self.svg_colors, None) 93 | } 94 | NetworkVariant::Upload => { 95 | let mut cols = self.svg_colors.clone(); 96 | cols.color2 = cols.color3.clone(); 97 | crate::svg_graph::line_adaptive(&upload, GRAPH_SAMPLES, &cols, None) 98 | } 99 | } 100 | } 101 | 102 | fn colors(&self) -> GraphColors { 103 | self.config.colors 104 | } 105 | 106 | fn set_colors(&mut self, colors: GraphColors) { 107 | self.config.colors = colors; 108 | self.svg_colors.set_colors(&colors); 109 | } 110 | 111 | fn color_choices(&self) -> Vec<(&'static str, ColorVariant)> { 112 | match self.config.variant { 113 | NetworkVariant::Combined => (*COLOR_CHOICES_COMBINED).into(), 114 | NetworkVariant::Download => (*COLOR_CHOICES_DL).into(), 115 | NetworkVariant::Upload => (*COLOR_CHOICES_UL).into(), 116 | } 117 | } 118 | 119 | fn id(&self) -> Option { 120 | None 121 | } 122 | } 123 | 124 | impl Sensor for Network { 125 | fn update_config(&mut self, config: &dyn Any, refresh_rate: u32) { 126 | if let Some(cfg) = config.downcast_ref::() { 127 | self.config = cfg.clone(); 128 | self.svg_colors.set_colors(&cfg.colors); 129 | self.refresh_rate = refresh_rate; 130 | 131 | if cfg.adaptive { 132 | self.max_y = None; 133 | } else { 134 | let unit = cfg.unit.unwrap_or(1).min(4); // ensure safe index 135 | let multiplier = [1, 1_000, 1_000_000, 1_000_000_000, 1_000_000_000_000]; 136 | let sec_per_tic = refresh_rate as f64 / 1000.0; 137 | let new_y = (cfg.bandwidth * multiplier[unit]) as f64 * sec_per_tic; 138 | self.max_y = Some(new_y.round() as u64); 139 | } 140 | } 141 | } 142 | 143 | fn graph_kind(&self) -> GraphKind { 144 | GraphKind::Line 145 | } 146 | 147 | fn set_graph_kind(&mut self, kind: GraphKind) { 148 | assert!(kind == GraphKind::Line); 149 | } 150 | 151 | /// Retrieve the amount of data transmitted since last update. 152 | fn update(&mut self) { 153 | self.networks.refresh(true); 154 | let mut dl = 0; 155 | let mut ul = 0; 156 | 157 | for (_, network) in &self.networks { 158 | dl += network.received() * 8; 159 | ul += network.transmitted() * 8; 160 | } 161 | 162 | if self.download.len() >= MAX_SAMPLES { 163 | self.download.pop_front(); 164 | } 165 | self.download.push_back(dl); 166 | 167 | if self.upload.len() >= MAX_SAMPLES { 168 | self.upload.pop_front(); 169 | } 170 | self.upload.push_back(ul); 171 | } 172 | 173 | fn demo_graph(&self) -> Box { 174 | let mut dmo = Network::default(); 175 | dmo.update_config(&self.config, self.refresh_rate); 176 | Box::new(dmo) 177 | } 178 | 179 | fn graph(&self) -> String { 180 | match self.config.variant { 181 | NetworkVariant::Combined => crate::svg_graph::double_line( 182 | &self.download, 183 | &self.upload, 184 | GRAPH_SAMPLES, 185 | &self.svg_colors, 186 | self.max_y, 187 | ), 188 | NetworkVariant::Download => crate::svg_graph::line_adaptive( 189 | &self.download, 190 | GRAPH_SAMPLES, 191 | &self.svg_colors, 192 | self.max_y, 193 | ), 194 | NetworkVariant::Upload => { 195 | let mut cols = self.svg_colors.clone(); 196 | cols.color2 = cols.color3.clone(); 197 | crate::svg_graph::line_adaptive(&self.upload, GRAPH_SAMPLES, &cols, self.max_y) 198 | } 199 | } 200 | } 201 | 202 | fn settings_ui(&self) -> Element { 203 | let theme = cosmic::theme::active(); 204 | let cosmic = theme.cosmic(); 205 | let mut net_elements = Vec::new(); 206 | 207 | let sample_rate_ms = self.refresh_rate; 208 | 209 | let dlrate = format!( 210 | "↓ {}", 211 | &self.download_label(sample_rate_ms, UnitVariant::Long) 212 | ); 213 | 214 | let ulrate = format!( 215 | "↑ {}", 216 | &self.upload_label(sample_rate_ms, UnitVariant::Long) 217 | ); 218 | 219 | let config = &self.config; 220 | let k = self.config.variant; 221 | 222 | let mut rate = column!(Element::from( 223 | widget::svg(widget::svg::Handle::from_memory( 224 | self.graph().as_bytes().to_owned(), 225 | )) 226 | .width(90) 227 | .height(60) 228 | )); 229 | 230 | rate = rate.push(Element::from(cosmic::widget::text::body(""))); 231 | 232 | match self.config.variant { 233 | NetworkVariant::Combined => { 234 | rate = rate.push(cosmic::widget::text::body(dlrate)); 235 | rate = rate.push(cosmic::widget::text::body(ulrate)); 236 | } 237 | NetworkVariant::Download => { 238 | rate = rate.push(cosmic::widget::text::body(dlrate)); 239 | } 240 | NetworkVariant::Upload => { 241 | rate = rate.push(cosmic::widget::text::body(ulrate)); 242 | } 243 | } 244 | net_elements.push(Element::from(rate)); 245 | 246 | let mut net_bandwidth_items = Vec::new(); 247 | 248 | net_bandwidth_items.push( 249 | settings::item( 250 | fl!("enable-chart"), 251 | widget::toggler(config.chart).on_toggle(move |t| Message::ToggleNetChart(k, t)), 252 | ) 253 | .into(), 254 | ); 255 | net_bandwidth_items.push( 256 | settings::item( 257 | fl!("enable-label"), 258 | widget::toggler(config.label).on_toggle(move |t| Message::ToggleNetLabel(k, t)), 259 | ) 260 | .into(), 261 | ); 262 | net_bandwidth_items.push( 263 | settings::item( 264 | fl!("use-adaptive"), 265 | row!( 266 | widget::checkbox("", config.adaptive) 267 | .on_toggle(move |t| Message::ToggleAdaptiveNet(k, t)) 268 | ), 269 | ) 270 | .into(), 271 | ); 272 | 273 | if !config.adaptive { 274 | net_bandwidth_items.push( 275 | settings::item( 276 | fl!("net-bandwidth"), 277 | row!( 278 | widget::text_input("", config.bandwidth.to_string()) 279 | .width(100) 280 | .on_input(move |b| Message::TextInputBandwidthChanged(k, b)), 281 | widget::dropdown(&self.dropdown_options, config.unit, move |u| { 282 | Message::NetworkSelectUnit(k, u) 283 | },) 284 | .width(50) 285 | ), 286 | ) 287 | .into(), 288 | ); 289 | } 290 | 291 | net_bandwidth_items.push( 292 | row!( 293 | widget::horizontal_space(), 294 | widget::button::standard(fl!("change-colors")).on_press(Message::ColorPickerOpen( 295 | DeviceKind::Network(self.config.variant), 296 | GraphKind::Line, 297 | None 298 | )), 299 | widget::horizontal_space() 300 | ) 301 | .into(), 302 | ); 303 | 304 | let net_right_column = Column::with_children(net_bandwidth_items); 305 | 306 | net_elements.push(Element::from(net_right_column.spacing(cosmic.space_xs()))); 307 | 308 | let title_content = match self.config.variant { 309 | NetworkVariant::Combined => fl!("net-title-combined"), 310 | NetworkVariant::Download => fl!("net-title-dl"), 311 | NetworkVariant::Upload => fl!("net-title-ul"), 312 | }; 313 | let title = widget::text::heading(title_content); 314 | 315 | column![ 316 | title, 317 | Row::with_children(net_elements).align_y(Alignment::Center) 318 | ] 319 | .spacing(cosmic::theme::spacing().space_xs) 320 | .into() 321 | } 322 | } 323 | 324 | impl Default for Network { 325 | fn default() -> Self { 326 | let networks = Networks::new_with_refreshed_list(); 327 | Network { 328 | networks, 329 | download: VecDeque::from(vec![0; MAX_SAMPLES]), 330 | upload: VecDeque::from(vec![0; MAX_SAMPLES]), 331 | max_y: None, 332 | dropdown_options: ["b", "Kb", "Mb", "Gb", "Tb"].into(), 333 | svg_colors: SvgColors::new(&GraphColors::default()), 334 | config: NetworkConfig::default(), 335 | refresh_rate: 1000, 336 | } 337 | } 338 | } 339 | 340 | impl Network { 341 | fn makestr(val: u64, format: UnitVariant) -> String { 342 | let mut value = val as f64; 343 | let mut unit_index = 0; 344 | 345 | let units = match format { 346 | UnitVariant::Short => UNITS_SHORT, 347 | UnitVariant::Long => UNITS_LONG, 348 | }; 349 | 350 | // Scale the value to the appropriate unit 351 | while value >= 999.0 && unit_index < units.len() - 1 { 352 | value /= 1024.0; 353 | unit_index += 1; 354 | } 355 | 356 | // Format the number with varying precision 357 | let value_str = if value < 10.0 { 358 | format!("{value:.2}") 359 | } else if value < 99.0 { 360 | format!("{value:.1}") 361 | } else { 362 | format!("{value:.0}") 363 | }; 364 | 365 | let unit_str = units[unit_index]; 366 | let mut result = String::with_capacity(20); 367 | 368 | if format == UnitVariant::Long { 369 | if value_str.len() == 3 { 370 | result.push(' '); 371 | } 372 | if unit_index == 0 { 373 | result.push(' '); 374 | } 375 | } 376 | 377 | result.push_str(&value_str); 378 | 379 | if format == UnitVariant::Long { 380 | result.push(' '); 381 | } 382 | 383 | result.push_str(unit_str); 384 | 385 | if format == UnitVariant::Long { 386 | let padding = 9usize.saturating_sub(result.len()); 387 | if padding > 0 { 388 | result = " ".repeat(padding) + &result; 389 | } 390 | } 391 | 392 | result 393 | } 394 | 395 | // If the sample rate doesn't match exactly one second (more or less), 396 | // we grab enough samples to cover it and average the value of samples cover a longer duration. 397 | fn last_second_bitrate(samples: &VecDeque, sample_interval_ms: u32) -> u64 { 398 | let mut total_duration = 0u32; 399 | let mut total_bitrate = 0u64; 400 | 401 | // Iterate from newest to oldest 402 | for &bitrate in samples.iter().rev() { 403 | if total_duration >= 1000 { 404 | break; 405 | } 406 | 407 | total_bitrate += bitrate; 408 | total_duration += sample_interval_ms; 409 | } 410 | 411 | // Scale to exactly 1000ms 412 | let scale = 1000.0 / f64::from(total_duration); 413 | 414 | (total_bitrate as f64 * scale).floor() as u64 415 | } 416 | 417 | // Get bits per second 418 | pub fn download_label(&self, sample_interval_ms: u32, format: UnitVariant) -> String { 419 | let rate = Network::last_second_bitrate(&self.download, sample_interval_ms); 420 | Network::makestr(rate, format) 421 | } 422 | 423 | // Get bits per second 424 | pub fn upload_label(&self, sample_interval_ms: u32, format: UnitVariant) -> String { 425 | let rate = Network::last_second_bitrate(&self.upload, sample_interval_ms); 426 | Network::makestr(rate, format) 427 | } 428 | } 429 | 430 | const DL_DEMO: [u64; 21] = [ 431 | 208, 2071, 0, 1056588, 912575, 912875, 912975, 912600, 1397, 1173024, 1228, 6910, 2493, 432 | 1102101, 380, 2287, 1109656, 1541, 3798, 1132822, 68479, 433 | ]; 434 | const UL_DEMO: [u64; 21] = [ 435 | 0, 1687, 0, 9417, 9161, 838, 6739, 1561, 212372, 312372, 412372, 512372, 512372, 512372, 436 | 412372, 312372, 112372, 864, 0, 8587, 760, 437 | ]; 438 | -------------------------------------------------------------------------------- /src/sleepinhibitor.rs: -------------------------------------------------------------------------------- 1 | use zbus::{blocking::Connection, blocking::Proxy}; 2 | use zvariant::OwnedFd; 3 | 4 | pub const INHIBITOR_OPTIONS: [&str; 4] = [" 15 min ", " 30 min ", " 60 min ", " Infinite "]; 5 | 6 | pub struct SleepAndScreenInhibitor { 7 | connection: Connection, 8 | sleep_fd: Option, 9 | screensaver_cookie: Option, 10 | } 11 | 12 | impl SleepAndScreenInhibitor { 13 | pub fn new() -> zbus::Result { 14 | let connection = Connection::session()?; // For screensaver 15 | Ok(Self { 16 | connection, 17 | sleep_fd: None, 18 | screensaver_cookie: None, 19 | }) 20 | } 21 | 22 | /// Inhibit system sleep and screen dimming 23 | pub fn inhibit(&mut self, app_name: &str, reason: &str) -> zbus::Result<()> { 24 | // Inhibit system sleep via system bus 25 | let sys_conn = Connection::system()?; 26 | let login_proxy = Proxy::new( 27 | &sys_conn, 28 | "org.freedesktop.login1", 29 | "/org/freedesktop/login1", 30 | "org.freedesktop.login1.Manager", 31 | )?; 32 | 33 | let sleep_fd = login_proxy 34 | .call_method("Inhibit", &("sleep", app_name, reason, "block"))? 35 | .body::()?; 36 | 37 | self.sleep_fd = Some(sleep_fd); 38 | 39 | // Inhibit screen dimming via session bus 40 | let screensaver_proxy = Proxy::new( 41 | &self.connection, 42 | "org.freedesktop.ScreenSaver", 43 | "/org/freedesktop/ScreenSaver", 44 | "org.freedesktop.ScreenSaver", 45 | )?; 46 | 47 | let cookie = screensaver_proxy 48 | .call_method("Inhibit", &(app_name, reason))? 49 | .body::()?; 50 | 51 | self.screensaver_cookie = Some(cookie); 52 | 53 | Ok(()) 54 | } 55 | 56 | /// Uninhibit both sleep and screen 57 | pub fn uninhibit(&mut self) { 58 | self.sleep_fd = None; // Drop the fd 59 | 60 | if let Some(cookie) = self.screensaver_cookie.take() { 61 | if let Ok(proxy) = Proxy::new( 62 | &self.connection, 63 | "org.freedesktop.ScreenSaver", 64 | "/org/freedesktop/ScreenSaver", 65 | "org.freedesktop.ScreenSaver", 66 | ) { 67 | let _ = proxy.call_method("UnInhibit", &(cookie)); 68 | } 69 | } 70 | } 71 | 72 | pub fn is_inhibiting(&self) -> bool { 73 | self.sleep_fd.is_some() || self.screensaver_cookie.is_some() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/svg_graph.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use cosmic::cosmic_theme::palette::Srgba; 4 | 5 | use crate::config::GraphColors; 6 | 7 | use std::fmt::Write; 8 | 9 | #[derive(Debug, Clone, PartialEq, Eq)] 10 | pub struct SvgColors { 11 | pub color1: String, 12 | pub color2: String, 13 | pub color3: String, 14 | pub color4: String, 15 | } 16 | 17 | impl From for SvgColors { 18 | fn from(graph_colors: GraphColors) -> Self { 19 | fn to_hex(color: Srgba) -> String { 20 | format!( 21 | "#{:02X}{:02X}{:02X}{:02X}", 22 | color.red, color.green, color.blue, color.alpha 23 | ) 24 | } 25 | 26 | SvgColors { 27 | color1: to_hex(graph_colors.color1), 28 | color2: to_hex(graph_colors.color2), 29 | color3: to_hex(graph_colors.color3), 30 | color4: to_hex(graph_colors.color4), 31 | } 32 | } 33 | } 34 | 35 | impl SvgColors { 36 | pub fn new(colors: &GraphColors) -> SvgColors { 37 | (*colors).into() 38 | } 39 | 40 | pub fn set_colors(&mut self, colors: &GraphColors) { 41 | *self = (*colors).into(); 42 | } 43 | } 44 | 45 | pub fn ring(value: &str, percentage: &str, color: &SvgColors) -> String { 46 | let mut svg = String::with_capacity(RINGSVG_LEN); 47 | svg.push_str(RINGSVG_1); 48 | svg.push_str(&color.color1); 49 | svg.push_str(RINGSVG_1_1); 50 | svg.push_str(&color.color3); 51 | svg.push_str(RINGSVG_2); 52 | svg.push_str(&color.color4); 53 | svg.push_str(RINGSVG_3); 54 | svg.push_str(percentage); 55 | svg.push_str(RINGSVG_4); 56 | svg.push_str(&color.color2); 57 | svg.push_str(RINGSVG_5); 58 | svg.push_str(value); 59 | svg.push_str(RINGSVG_6); 60 | svg 61 | } 62 | 63 | pub fn line(samples: &VecDeque, max_y: f64, colors: &SvgColors) -> String { 64 | // Generate list of coordinates for line 65 | 66 | let scaling: f32 = 40.0 / max_y as f32; 67 | let est_len = samples.len() * 10; // Rough estimate: each pair + separator 68 | 69 | let indexed_string = samples.iter().enumerate().fold( 70 | String::with_capacity(est_len), 71 | |mut acc, (index, &value)| { 72 | let x = ((index * 2) + 1) as u32; 73 | let y = (41.0 - (scaling * value as f32)).round() as u32; 74 | if index > 0 { 75 | acc.push(' '); 76 | } 77 | write!(&mut acc, "{x},{y}").unwrap(); 78 | acc 79 | }, 80 | ); 81 | 82 | let mut svg = String::with_capacity(LINE_LEN); 83 | svg.push_str(LINESVG_1); 84 | svg.push_str(&colors.color1); 85 | svg.push_str(LINESVG_2); 86 | svg.push_str(&colors.color2); 87 | svg.push_str(LINESVG_3); 88 | svg.push_str(LINESVG_4); 89 | svg.push_str(&colors.color4); 90 | svg.push_str(LINESVG_5); 91 | svg.push_str(&indexed_string); 92 | svg.push_str(LINESVG_6); 93 | svg.push_str(&colors.color4); 94 | svg.push_str(LINESVG_7); 95 | svg.push_str(&indexed_string); 96 | svg.push_str(LINESVG_8); 97 | svg.push_str(LINESVG_9); 98 | 99 | svg 100 | } 101 | 102 | pub fn double_line( 103 | samples: &VecDeque, 104 | samples2: &VecDeque, 105 | graph_samples: usize, 106 | colors: &SvgColors, 107 | max_y: Option, 108 | ) -> String { 109 | assert!(samples.len() == samples2.len()); 110 | 111 | let len = samples.len(); 112 | 113 | let start = len.saturating_sub(graph_samples); 114 | 115 | let max = max_y.unwrap_or_else(|| { 116 | let calculated_max = samples 117 | .iter() 118 | .chain(samples2.iter()) 119 | .copied() 120 | .max() 121 | .unwrap_or(40); 122 | std::cmp::max(40, calculated_max) // Ensure min value is 40 123 | }); 124 | 125 | // Generate list of coordinates for line 126 | let est_len = (samples.len() - start) * 10; 127 | let scaling: f64 = 40.0 / max as f64; 128 | let (indexed_string, indexed_string2) = samples 129 | .iter() 130 | .skip(start) 131 | .zip(samples2.iter().skip(start)) 132 | .enumerate() 133 | .fold( 134 | ( 135 | String::with_capacity(est_len), 136 | String::with_capacity(est_len), 137 | ), 138 | |(mut acc1, mut acc2), (index, (&value1, &value2))| { 139 | let x = ((index * 2) + 1) as u32; 140 | let y1 = (41.0 - (scaling * value1 as f64)).round() as u32; 141 | let y2 = (41.0 - (scaling * value2 as f64)).round() as u32; 142 | write!(&mut acc1, "{x},{y1} ").unwrap(); 143 | write!(&mut acc2, "{x},{y2} ").unwrap(); 144 | (acc1, acc2) 145 | }, 146 | ); 147 | 148 | let mut svg = String::with_capacity(DBLLINESVG_LEN); 149 | svg.push_str(DBLLINESVG_1); 150 | svg.push_str(&colors.color1); 151 | svg.push_str(DBLLINESVG_2); 152 | svg.push_str(&colors.color4); 153 | svg.push_str(DBLLINESVG_3); 154 | 155 | //First graph and polygon 156 | svg.push_str(DBLLINESVG_4); 157 | svg.push_str(&colors.color2); 158 | svg.push_str(DBLLINESVG_5); 159 | svg.push_str(&indexed_string); 160 | svg.push_str(DBLLINESVG_6); 161 | svg.push_str(&colors.color2); 162 | svg.push_str(DBLLINESVG_7); 163 | svg.push_str(&indexed_string); 164 | svg.push_str(DBLLINESVG_8); 165 | 166 | //Second graph and polygon 167 | svg.push_str(DBLLINESVG_4); 168 | svg.push_str(&colors.color3); 169 | svg.push_str(DBLLINESVG_5); 170 | svg.push_str(&indexed_string2); 171 | svg.push_str(DBLLINESVG_6); 172 | svg.push_str(&colors.color3); 173 | svg.push_str(DBLLINESVG_7); 174 | svg.push_str(&indexed_string2); 175 | svg.push_str(DBLLINESVG_8); 176 | 177 | svg.push_str(DBLLINESVG_9); 178 | 179 | svg 180 | } 181 | 182 | pub fn line_adaptive( 183 | samples: &VecDeque, 184 | graph_samples: usize, 185 | colors: &SvgColors, 186 | max_y: Option, 187 | ) -> String { 188 | let len = samples.len(); 189 | let start = len.saturating_sub(graph_samples); 190 | 191 | let max = max_y.unwrap_or_else(|| { 192 | let calculated_max = samples.iter().copied().max().unwrap_or(40); 193 | std::cmp::max(40, calculated_max) // Ensure min value is 40 194 | }); 195 | 196 | // Generate list of coordinates for line 197 | let est_len = (samples.len() - start) * 10; 198 | let scaling: f64 = 40.0 / max as f64; 199 | let indexed_string = samples.iter().skip(start).enumerate().fold( 200 | String::with_capacity(est_len), 201 | |mut acc, (index, &value)| { 202 | let x = ((index * 2) + 1) as u32; 203 | let y = (41.0 - (scaling * value as f64)).round() as u32; 204 | write!(&mut acc, "{x},{y} ").unwrap(); 205 | acc 206 | }, 207 | ); 208 | 209 | let mut svg = String::with_capacity(DBLLINESVG_LEN); 210 | svg.push_str(DBLLINESVG_1); 211 | svg.push_str(&colors.color1); 212 | svg.push_str(DBLLINESVG_2); 213 | svg.push_str(&colors.color4); 214 | svg.push_str(DBLLINESVG_3); 215 | 216 | //First graph and polygon 217 | svg.push_str(DBLLINESVG_4); 218 | svg.push_str(&colors.color2); 219 | svg.push_str(DBLLINESVG_5); 220 | svg.push_str(&indexed_string); 221 | svg.push_str(DBLLINESVG_6); 222 | svg.push_str(&colors.color2); 223 | svg.push_str(DBLLINESVG_7); 224 | svg.push_str(&indexed_string); 225 | svg.push_str(DBLLINESVG_8); 226 | 227 | svg.push_str(DBLLINESVG_9); 228 | 229 | svg 230 | } 231 | 232 | pub fn heat(samples: &VecDeque, max_y: u64, colors: &SvgColors) -> String { 233 | // Generate list of coordinates for line 234 | 235 | let scaling: f32 = 40.0 / max_y as f32; 236 | let est_len = samples.len() * 10; // Rough estimate: each pair + separator 237 | 238 | let indexed_string = samples.iter().enumerate().fold( 239 | String::with_capacity(est_len), 240 | |mut acc, (index, &value)| { 241 | let x = ((index * 2) + 1) as u32; 242 | let y = (41.0 - (scaling * value as f32)).round() as u32; 243 | if index > 0 { 244 | acc.push(' '); 245 | } 246 | write!(&mut acc, "{x},{y}").unwrap(); 247 | acc 248 | }, 249 | ); 250 | 251 | let mut svg = String::with_capacity(LINE_LEN); 252 | svg.push_str(HEATSVG_1); 253 | svg.push_str(&colors.color1); 254 | svg.push_str(HEATSVG_2); 255 | svg.push_str(&colors.color2); 256 | svg.push_str(HEATSVG_3); 257 | svg.push_str(&indexed_string); 258 | svg.push_str(HEATSVG_8); 259 | svg.push_str(&colors.color2); 260 | svg.push_str(HEATSVG_9); 261 | 262 | svg 263 | } 264 | /* 265 | const RECT1: &str = ""#; 267 | 268 | const GRADIENT: &str = r#" 269 | 270 | 271 | "#; 272 | 273 | const GRADIENT2: &str = r#" 274 | 275 | 276 | "#; 277 | */ 278 | const HEATSVG_1: &str = r#" 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | "#; 295 | 296 | const LINESVG_1: &str = r#" 297 | 298 | 299 | 300 | 301 | 302 | 303 | "#; 307 | const LINESVG_4: &str = r#""#; 312 | const LINESVG_9: &str = r#""#; 313 | 314 | const LINE_LEN: usize = 640; // Just for preallocation 315 | // Ring SVG 316 | const RINGSVG_1: &str = r#" 317 | 318 | 329 | 342 | 352 | "#; 353 | 354 | const RINGSVG_6: &str = r#""#; 355 | const RINGSVG_LEN: usize = 680; // For preallocation 356 | 357 | // Double Line SVG 358 | const DBLLINESVG_1: &str = r#" 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 370 | "#; 371 | 372 | // Line 373 | const DBLLINESVG_4: &str = r#" 378 | "#; 381 | 382 | const DBLLINESVG_9: &str = r#""#; 383 | 384 | const DBLLINESVG_LEN: usize = 1000; // For preallocation 385 | 386 | /* 387 | pub fn dbl_circle( 388 | samples: &VecDeque, 389 | samples2: &VecDeque, 390 | graph_samples: usize, 391 | colors: &SvgColors, 392 | ) -> String { 393 | let mut dl: u64 = 0; 394 | let mut ul: u64 = 0; 395 | 396 | 397 | if self.max_val > 0 && !self.download.is_empty() { 398 | let scaling_dl: f32 = 94.0 / self.max_val as f32; 399 | let scaling_ul: f32 = 69.0 / self.max_val as f32; 400 | 401 | dl = *self.download.get(self.download.len() - 1).unwrap_or(&0u64); 402 | ul = *self.upload.get(self.upload.len() - 1).unwrap_or(&0u64); 403 | dl = (dl as f32 * scaling_dl) as u64; 404 | ul = (ul as f32 * scaling_ul) as u64; 405 | } 406 | 407 | let background = "none"; 408 | let strokebg = "white"; 409 | let outerstrokefg = "blue"; 410 | let outerpercentage = dl.to_string(); 411 | let innerstrokefg = "red"; 412 | let innerpercentage = ul.to_string(); 413 | let mut svg = String::with_capacity(SVG_LEN); 414 | svg.push_str(DBLCIRCLESTART); 415 | svg.push_str(&background); 416 | svg.push_str(DBLCIRCLEPART2); 417 | svg.push_str(&strokebg); 418 | svg.push_str(DBLCIRCLEPART3); 419 | svg.push_str(&outerstrokefg); 420 | svg.push_str(DBLCIRCLEPART4); 421 | svg.push_str(&outerpercentage); 422 | svg.push_str(DBLCIRCLEPART5); 423 | svg.push_str(&strokebg); 424 | svg.push_str(DBLCIRCLEPART6); 425 | svg.push_str(&innerstrokefg); 426 | svg.push_str(DBLCIRCLEPART7); 427 | svg.push_str(&innerpercentage); 428 | svg.push_str(DBLCIRCLEPART8); 429 | 430 | svg 431 | } 432 | 433 | const SVG_LEN: usize = DBLCIRCLESTART.len() 434 | + DBLCIRCLEPART2.len() 435 | + DBLCIRCLEPART3.len() 436 | + DBLCIRCLEPART4.len() 437 | + DBLCIRCLEPART5.len() 438 | + DBLCIRCLEPART6.len() 439 | + DBLCIRCLEPART7.len() 440 | + DBLCIRCLEPART8.len() 441 | + 40; 442 | 443 | const DBLCIRCLESTART: &str = " 444 | 451 | 458 | 463 | "; 470 | */ 471 | /* 472 | 473 | 476 | 477 | 480 | 481 | 484 | 485 | 488 | 489 | */ 490 | --------------------------------------------------------------------------------