From 4322787066b893d515cb081c9794e2cc949b2b55 Mon Sep 17 00:00:00 2001 From: SuiziM Date: Sat, 30 Aug 2025 20:40:01 +0200 Subject: [PATCH 01/27] feat: Improved spacing between elements in panel --- extension.js | 67 ++++++++++++++++++++++---------------------------- stylesheet.css | 23 +++++++++-------- 2 files changed, 42 insertions(+), 48 deletions(-) diff --git a/extension.js b/extension.js index 6f14df7c..d6278e75 100644 --- a/extension.js +++ b/extension.js @@ -51,7 +51,7 @@ var VitalsMenuButton = GObject.registerClass({ this._warnings = []; this._sensorMenuItems = {}; this._hotLabels = {}; - this._hotIcons = {}; + this._hotItems = {}; this._groups = {}; this._widths = {}; this._numGpus = 1; @@ -67,7 +67,8 @@ var VitalsMenuButton = GObject.registerClass({ x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, reactive: true, - x_expand: true + x_expand: true, + style_class: 'vitals-panel-menu' }); this._drawMenu(); @@ -219,8 +220,7 @@ var VitalsMenuButton = GObject.registerClass({ // removes sensors that are no longer available if (!this._sensorMenuItems[sensor]) { hotSensors.splice(i, 1); - this._removeHotLabel(sensor); - this._removeHotIcon(sensor); + this._removeHotItem(sensor); } } @@ -255,9 +255,16 @@ var VitalsMenuButton = GObject.registerClass({ } _createHotItem(key, value) { - let icon = this._defaultIcon(key); - this._hotIcons[key] = icon; - this._menuLayout.add_child(icon) + let item = new St.BoxLayout({ + style_class: 'vitals-panel-item', + }); + this._hotItems[key] = item; + this._menuLayout.add_child(item); + + if (!this._settings.get_boolean('hide-icons') || key == '_default_icon_') { + let icon = this._defaultIcon(key); + item.add_child(icon); + } // don't add a label when no sensors are in the panel if (key == '_default_icon_') return; @@ -268,18 +275,15 @@ var VitalsMenuButton = GObject.registerClass({ y_expand: true, y_align: Clutter.ActorAlign.CENTER }); - // attempt to prevent ellipsizes label.get_clutter_text().ellipsize = 0; - // keep track of label for removal later this._hotLabels[key] = label; - // prevent "called on the widget" "which is not in the stage" errors by adding before width below - this._menuLayout.add_child(label); + item.add_child(label); // support for fixed widths #55, save label (text) width - this._widths[key] = label.width; + this._widths[key] = label.get_clutter_text().width; } _showHideSensorsChanged(self, sensor) { @@ -329,35 +333,23 @@ var VitalsMenuButton = GObject.registerClass({ this._redrawMenu(); } - _removeHotLabel(key) { - if (key in this._hotLabels) { - let label = this._hotLabels[key]; - delete this._hotLabels[key]; - // make sure set_label is not called on non existent actor - label.destroy(); + _removeHotItems(){ + for (let key in this._hotItems) { + this._removeHotItem(key); } } - _removeHotLabels() { - for (let key in this._hotLabels) - this._removeHotLabel(key); - } - - _removeHotIcon(key) { - if (key in this._hotIcons) { - this._hotIcons[key].destroy(); - delete this._hotIcons[key]; + _removeHotItem(key) { + if (key in this._hotItems) { + this._hotItems[key].destroy(); + delete this._hotItems[key]; + delete this._hotLabels[key]; + delete this._widths[key]; } } - _removeHotIcons() { - for (let key in this._hotIcons) - this._removeHotIcon(key); - } - _redrawMenu() { - this._removeHotIcons(); - this._removeHotLabels(); + this._removeHotItems(); for (let key in this._sensorMenuItems) { if (key.includes('-group')) continue; @@ -452,8 +444,7 @@ var VitalsMenuButton = GObject.registerClass({ } else { // remove selected sensor from panel hotSensors.splice(hotSensors.indexOf(self.key), 1); - this._removeHotLabel(self.key); - this._removeHotIcon(self.key); + this._removeHotItem(self.key); } if (hotSensors.length <= 0) { @@ -465,7 +456,7 @@ var VitalsMenuButton = GObject.registerClass({ if (defIconPos >= 0) { // remove generic icon from panel when sensors are selected hotSensors.splice(defIconPos, 1); - this._removeHotIcon('_default_icon_'); + this._removeHotItem('_default_icon_'); } } @@ -508,7 +499,7 @@ var VitalsMenuButton = GObject.registerClass({ // don't use the default system icon if the type is a gpu; use the universal gpu icon instead if (type == 'default' || (!(type in this._sensorIcons) && !type.startsWith('gpu'))) { icon.gicon = Gio.icon_new_for_string(this._sensorIconPath('system')); - } else if (!this._settings.get_boolean('hide-icons')) { // support for hide icons #80 + } else { // support for hide icons #80 let iconObj = (split.length == 2)?'icon-' + split[1]:'icon'; icon.gicon = Gio.icon_new_for_string(this._sensorIconPath(type, iconObj)); } diff --git a/stylesheet.css b/stylesheet.css index adec4251..4fcd9858 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -1,15 +1,18 @@ .vitals-icon { icon-size: 16px; } .vitals-menu-button-container {} -.vitals-panel-icon-temperature { margin: 0 1px 0 8px; padding: 0; } -.vitals-panel-icon-voltage { margin: 0 0 0 8px; padding: 0; } -.vitals-panel-icon-fan { margin: 0 4px 0 8px; padding: 0; } -.vitals-panel-icon-memory { margin: 0 2px 0 8px; padding: 0; } -.vitals-panel-icon-processor { margin: 0 3px 0 8px; padding: 0; } -.vitals-panel-icon-system { margin: 0 3px 0 8px; padding: 0; } -.vitals-panel-icon-network { margin: 0 3px 0 8px; padding: 0; } -.vitals-panel-icon-storage { margin: 0 2px 0 8px; padding: 0; } -.vitals-panel-icon-battery { margin: 0 4px 0 8px; padding: 0; } -.vitals-panel-label { margin: 0 3px 0 0; padding: 0; } +.vitals-panel-item{spacing: 0;} +.vitals-panel-menu{spacing: 11px; padding: 3px; } +.vitals-panel-icon-default {} +.vitals-panel-icon-temperature { margin: 0 1px 0 0; padding: 0; } +.vitals-panel-icon-voltage { margin: 0 0 0 0; padding: 0; } +.vitals-panel-icon-fan { margin: 0 4px 0 0; padding: 0; } +.vitals-panel-icon-memory { margin: 0 2px 0 0; padding: 0; } +.vitals-panel-icon-processor { margin: 0 3px 0 0; padding: 0; } +.vitals-panel-icon-system { margin: 0 3px 0 0; padding: 0; } +.vitals-panel-icon-network { margin: 0 3px 0 0; padding: 0; } +.vitals-panel-icon-storage { margin: 0 2px 0 0; padding: 0; } +.vitals-panel-icon-battery { margin: 0 4px 0 0; padding: 0; } +.vitals-panel-label {} .vitals-button-action { -st-icon-style: symbolic; border-radius: 32px; margin: 0px; min-height: 22px; min-width: 22px; padding: 10px; font-size: 100%; border: 1px solid transparent; } .vitals-button-action:hover, .vitals-button-action:focus { border-color: #777; } .vitals-button-action > StIcon { icon-size: 16px; } From 56cf3f0fa9825c18a86fc84b2d6c779b1163a915 Mon Sep 17 00:00:00 2001 From: SuiziM Date: Sat, 30 Aug 2025 20:03:58 +0200 Subject: [PATCH 02/27] fix: Remove trailing whitespace in values for "System\>Last XXmin". --- values.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/values.js b/values.js index a7cabb03..eae5aa08 100644 --- a/values.js +++ b/values.js @@ -200,7 +200,7 @@ export const Values = GObject.registerClass({ ending = 'Wh'; break; case 'load': - format = (use_higher_precision)?'%.2f %s':'%.1f %s'; + format = (use_higher_precision)?'%.2f %s':'%.1f'; break; case 'pcie': let split = value.split('x'); From d58a3b917c84738bb33310515b47dfc7d24ac879 Mon Sep 17 00:00:00 2001 From: zay-yar-lwin Date: Fri, 26 Sep 2025 13:54:54 +0630 Subject: [PATCH 03/27] add support for Burmese (my_MM) --- locale/my_MM/LC_MESSAGES/vitals.mo | Bin 0 -> 17140 bytes locale/my_MM/LC_MESSAGES/vitals.po | 731 +++++++++++++++++++++++++++++ 2 files changed, 731 insertions(+) create mode 100644 locale/my_MM/LC_MESSAGES/vitals.mo create mode 100644 locale/my_MM/LC_MESSAGES/vitals.po diff --git a/locale/my_MM/LC_MESSAGES/vitals.mo b/locale/my_MM/LC_MESSAGES/vitals.mo new file mode 100644 index 0000000000000000000000000000000000000000..e194797aaf82c8fae117fe847e20183432142531 GIT binary patch literal 17140 zcmd6t50qS0ea9bx00smFHxNuV1Y-WM*-e5#1>VlQ-FakY-Y{<_ z$;Lm-@>iu6LV-ZQAgEA*kVq{d2BP(7EA6SLN{@$Ad&-%D)gqoAtZfxfZ9l*JyYJ20 z-P!D}cutv&*Wt5=b|&qf>5lVn;IYT>#b@tvjw9W#S$qMU z!T3cGQs*DQcY*&1o(R5X$G--TVm$45U7rEU_$2T>;F)&5)#3#fms(t9aRYcH^w)w% zfCJ#s;3iPcuUOm$3SA964ZIy}0UrV9fG>iw@0Xy^{TA#7ry-2UwE~p+wRXG#6nlIe zJPF)p@lK0hwzwbs0PEie-w*x*Yy*#Zuj8Brc7dmZSAl1MRZ!@*gNwjDU_1CT@NDq3 z6O`^^Q1)F7&ILDvvi?O7(Vf2n-wplZwQ0#R$%DxXg7ep0jDJb&wTD;lf*Dd}8>|*_7Hi;bTEM8-Alf|vz$*kWB z&H^6-#ZP|;au!Z%~*Ly(YnZnNw6 zg1FTAiXHE>>wgW3zCW__uY)4@TcF4@?Nl9~0!p5p1&ZG1*ztT&^z8y$!ER8_xd9Y; zZU#l4J3;a9Llz$ch0k6){tHm}e-o7R{{|GkKLo|zKL!!WsoVA6fc$d~!)YG^Pq(7Sd8?rKZ5t@|9R|e@ zcY&hs)1dIV2Nbz?gAyP6K*@)1fhLbYIp;{GJb)e|E2Nb*f02IHx4qgJzTA*@WZ}9<(-?RAN z;Dyl5K2PHz1BxFHg2Lx_U^lpEq5Aogp!n^Zp!oT}z;)m-_3wQ=N6K9Hjmw>`I1V0FV1{8Up0L9)v1djn<0gncM4ju-6k|y?xKWEVq ze_u{>X~Kh$a2}$4nl?xiJBx25k4~X|nI^F(pBc0-(dN;%(BvcMt*4b}eKh$jqJ4~( zp{#Gl8mVV=mi+K(_tEa4 zEvMZ=6Z>CDlTSo@fOb7?m?odi3g$h!jsESlKcYzpSR;9z$0lZ?Rba9o50g);_E#$i9z}F(*BHgH!VkdH|;pu z@wE5Q-b=fO_Ho)Y+O0H+2g2C7nU?;1k%8p)3AA%*vuPV?PtoM_Sp^f9Tj~F)9se;% zc{2Gm6Fi(Yish?cfaXB-`H(ZlLY5V^45tZ@1fBEtmab#q`&BTZ2Ni zVEXI5@}SRje^})^pAR!$#m~|!<-C5s5^VF`$S+1=IdYb6_JX|EpZDqcWpB`TmU)$m zUk?1p_J(3~=j4KH)-TRYCOVQ{XVP1c^v+9q3!P;_(JR9#sK7QH3=2Vt&QOI@y1h)! z*Y7Nl&s6hB>vo5Q{-Ee*-Hl$pYRHOiHS*oTV9@JlqZ^j#_Xnmwz@Xq4LXPP6CZLNd zq0zJ3D|wlq!VXO6mo2!JoDZvF)vbi4A7sL!TlNRNayIWrksA(}=~5U4m4Kz7sAI3_ z=bi4b?7L^9#!v=gXStsV3SJ(}dqXak-{Sj4YkId7=JP>u&{-ZtC9`boKIAT2y1u>H zWJj~f&M}kSYnF}OUhpzeA=BEgMTV| z#cogD_sGA^0fFyc=()MFKXB2^T%}Tqy5`Rx3@W*5e;X3d&yWO}FiULD-xyT9d^A6Z zBACrwa%oVxtlEF!d~ZpcyEb&Uc;&KJtPIU_{jDYO0dXaME|l_tpKTj^NQ1LEC>s{3 z6m6wkY37pUrZ24BV89&;tAuN{m?Z{+Na_X_XmeJW$a7YdeP6!Nu$UQgF7>e*S$bKJ z^-Tu21Ld#~S4fQ_561*W5+TScos@-0f}s+YNQSb`Zu83_-ZKSjWigYl!m&jXy2ag+ zBlQUwQ{J$RI9Cgt7Th5xy`f{O8#<@7L5jqbHjFGFQ`u_DS*#j^jzmueoyowIt(a`& zX$j4tlD$DRI`F<(!aKnr0`Rl8Nl6c4~O9&~gb+b!dr_S{cxnVIsjn}`ARO?L!cKt>==`AQ6&9Qo{mZET^C6D)8=D96uZ|Rk#%(l!OL0vMBhaL*5&wGyR&gs!x&s`rBI3d0Qot`+j zon9)R)9Vu_TpPAm;r7ilIY2GVfstR{Omt{r@1rIcvr+O2sg}6xIjPS+1TD(5MmP{tcyyg!_g_(rN>5je%e*skkM`R&vpp ziS*?w#5ocRckz(O@I>L|#$?vn=ohnL+1aSiCg+M^Mc}RuiksYZRW5U0|7NC-*Aj0z zEz?(&1AbnZw9>BTm90sQ+7sCju(jsfd%K-)?_D9;dOdH))|J`Tjec1q>2hcET;AI2 z%hM?=wo*`hrdE4VrFA_Iporf)VcE@(`{0PWKa!Yo(*Oq-=!$tGsudA~`)#rQV zOipTWYkQZyaNDkrni6z$#aHck6rEiXF9Xqd>kBYFzXewk+T)R&*CjPWvtm4&g$Tlr z+=3U)56gpXgT=7mw?#QWpKnX{Ms1tTW4UcmN!zv-@=dimKbE-~%#dIE_1Z)ATCHCD zM!oi6y>_r(+pV*Y)N2paYX|DJr|Pxm>b2YIwLSIPv-EZO-g@m;vk4+L-9_K2*VqKB z=j*kvnSHkyisx8>oGJKhSB6RpYtB@@!-hLNVSp^@vzgg#$dvv5dJSSWvK{)*8v6Up zv3lS>a~@K_a}}fc0#SP9Wb;Ui-daeK^E?hg+XX`KOL`$MVc%(nVM6t zA=nGbXKoXM`tWem*_)cq-rRKdmZr0}GJCf<8VUQJ#MFTbv30R9MmRVEDBp%gtP&KUu z1WKZW>c=4{#5}xhxYTQ;y$TGIn6>H%_N@5&zI62hEwF(yJ{XM$Y?U}m&Q zVF(4rBjg)6hbzv2O^dw!AI*b7|7?LEF zS$wTdA2x+XC8Z3I0jNGoS^_}b(C~q4GDTU)Rjm=sJ|s|u_EDLE~hQ56Fqlc{W*+?tX!n43yLc2fvAbhLbwqX`QnRfK}P+nuPKD{m}=1+=bE<}i!Y zMBS`aoHIw4Ks_SaG-`--lcjVY#W+eYaty6X3MHpCEy_sv%vc*BBJxnece2$nTH+z< zK3X>}II6U)Cm{wQvK%l}@MQt*wEQ!~>F~T0A_mID9+0lbqhvAf85ChB7SKBxb3SWy zL`u#`2NQ}br8N$Rkydh;TOS^-53>$AA~7XK8nu3i#U?x9)Z|CMF&0a>0mEW*`bdf2 zQ3LbrG{Ytihg5BqIF=c-VLw*igQ(hL7JiLid*d#c( zA-7>zqJhxjB#mo^`-q7o$&d&Urf@Q8%^uhgq+*fe!k7Y&=Ak+zF_3|Rqn1Pr6=?zy z9CLz_j&pxa#S%&RB5%pTbig7PL*$y8Z71nf@nIuljDPXx-uRMD96fXy)EG+IemP~3LCS}8J7P!j@0Ih}yO*CR_ z7~mE41O$!p#s<7Z++jP25DXd89+ThA>Mt&k#y%t22oa&fKuE_wxgnBFLYJL}zm?H! zwNWsK$LRgK-ZGCq91usy&5+9X6#tJvGiGR>s7YVzWh1=U10$7}j+y`s4#X*e%JgMk zNii-GV+DTQrS-2Fi$JHFZ49YMD5NK_2RhyS6l5;i>_F#PGInlg2>ooYeGDdpQi@;j8i!QnjWg z;V(fP#@c@KEys|K;yfgQJEv&?W5{P?T1Xm(f^=-qr2RZrlu5JJ+jazl)F09l{bY`N zb0l5kPwNf3#9CwP{m9LRP`u!#-gWZFBf?&Tr)DgL2QjlvTb4(2A>VN)#=@`~pRy0a z5o03_Td^z4Hdsw&V@HhzvO)box?u2|(BpEC{C23XKL) z&5&ViwE{8?n^LDtlsJh_jk2_tpr4$+DN#s0Jq-CqB!W?3r?HjQ-qk9n=b{lZ^YrCE z#S-K6MMpOk>5}Jg*XpO69!@JN)e%)yXMS=i2|A>;g(;^ao7_}{$|0_SL+#{{12HG0 zRpVn@CXrlDn1DUYBW$7MuthYstNW5%YjR^E9m=MQ&+`mngVA+X-3}f6xFecxNEhMl z=G-JI65b7)8K)#Kh2Q@qcB+ONUYw-vM}I7+mBa=j8a4D}fRIX#q0)|pXfMk z8e=$dG|`~vX-p7B>EUqfH;Fj&W~rGD5*#$A!3Za7@lrEl1EPFl)=(>V<%r&RiEmgV zn*1RG#=^;XNl%+>d?q`soG{a#sxji%IVM#J9!s6YxdGitHub. No warranty, expressed or implied. Donate if you found this " +"useful." +msgstr "" +"လိုချင်တဲ့စွမ်းရည်များ သို့ ပြစ်ချက်များ ပြောပြလိုလျှင်GitHubသို့လာခဲ့ပါ။ လှူဒါန်းလိုပါက " + +#: prefs.ui:421 +msgid "Sensors" +msgstr "အာရုံခံစက်များ" + +#: prefs.ui:449 schemas/org.gnome.shell.extensions.vitals.gschema.xml:36 +msgid "Monitor temperature" +msgstr "အပူချိန်စောင့်ကြည့်မည်" + +#: prefs.ui:500 schemas/org.gnome.shell.extensions.vitals.gschema.xml:46 +msgid "Monitor voltage" +msgstr "ဗို့အားစောင့်ကြည့်မည်" + +#: prefs.ui:528 schemas/org.gnome.shell.extensions.vitals.gschema.xml:51 +msgid "Monitor fan" +msgstr "ပန်ကာစောင့်ကြည့်မည်" + +#: prefs.ui:556 schemas/org.gnome.shell.extensions.vitals.gschema.xml:56 +msgid "Monitor memory" +msgstr "မှတ်ဉာဏ်စောင့်ကြည့်မည်" + +#: prefs.ui:607 schemas/org.gnome.shell.extensions.vitals.gschema.xml:61 +msgid "Monitor processor" +msgstr "စက်ဦးနှောက်စောင့်ကြည့်မည်" + +#: prefs.ui:658 schemas/org.gnome.shell.extensions.vitals.gschema.xml:66 +msgid "Monitor system" +msgstr "စနစ်စောင့်ကြည့်မည်" + +#: prefs.ui:709 schemas/org.gnome.shell.extensions.vitals.gschema.xml:76 +msgid "Monitor network" +msgstr "ကွန်ရက်စောင့်ကြည့်" + +#: prefs.ui:760 schemas/org.gnome.shell.extensions.vitals.gschema.xml:71 +msgid "Monitor storage" +msgstr "သိုလှောင်နေရာစောင့်ကြည့်မည်" + +#: prefs.ui:811 schemas/org.gnome.shell.extensions.vitals.gschema.xml:96 +msgid "Monitor battery" +msgstr "ဘက်ထရီစောင့်ကြည့်မည်" + +#: prefs.ui:890 +msgid "Path" +msgstr "လမ်းကြောင်း" + +#: prefs.ui:922 prefs.ui:1031 +msgid "Measurement" +msgstr "တိုင်းတာမှု" + +#: prefs.ui:930 prefs.ui:1039 +msgid "Binary" +msgstr "ဒွိကိန်း" + +#: prefs.ui:931 prefs.ui:1040 +msgid "Decimal" +msgstr "ဒသမကိန်း" + +#: prefs.ui:976 +msgid "Unit" +msgstr "ယူနစ်" + +#: prefs.ui:985 +msgid "°C" +msgstr "°စင်" + +#: prefs.ui:986 +msgid "°F" +msgstr "°ဖာ" + +#: prefs.ui:1086 +msgid "Monitor BAT0" +msgstr "BAT0စောင့်ကြည့်မည်" + +#: prefs.ui:1116 +msgid "Monitor BAT1" +msgstr "BAT1စောင့်ကြည့်မည်" + +#: prefs.ui:1146 +msgid "Monitor BAT2" +msgstr "BAT2စောင့်ကြည့်မည်" + +#: prefs.ui:1176 +msgid "Monitor CMB0" +msgstr "CMB0စောင့်ကြည့်မည်" + +#: prefs.ui:1206 +msgid "Monitor macsmc-battery" +msgstr "ဘက်ထရီmacsmc စောင့်ကြည့်မည်" + +#: prefs.ui:1247 +msgid "Calculate Combined Values" +msgstr "ပေါင်းပြဒေတာများတွက်ချက်မည်" + +#: prefs.ui:1277 schemas/org.gnome.shell.extensions.vitals.gschema.xml:141 +msgid "Include BAT0" +msgstr "BAT0အပါအဝင်" + +#: prefs.ui:1307 schemas/org.gnome.shell.extensions.vitals.gschema.xml:146 +msgid "Include BAT1" +msgstr "BAT1အပါအဝင်" + +#: prefs.ui:1337 schemas/org.gnome.shell.extensions.vitals.gschema.xml:151 +msgid "Include BAT2" +msgstr "BAT2အပါအဝင်" + +#: prefs.ui:1367 schemas/org.gnome.shell.extensions.vitals.gschema.xml:156 +msgid "Include CMB0" +msgstr "CMB0အပါအဝင်" + +#: prefs.ui:1397 schemas/org.gnome.shell.extensions.vitals.gschema.xml:161 +msgid "Include macsmc-battery" +msgstr "ဘက်ထရီmacsmc အပါအဝင်" + +#: prefs.ui:1446 +msgid "Monitor command" +msgstr "စနစ်စောင့်ကြည့်အမိန့်" + +#: prefs.ui:1498 +msgid "Include static info" +msgstr "အငြိမ်အချက်အလက်ပါဖော်ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:6 +msgid "Sensors to show in panel" +msgstr "ပန်နယ်တွင်ပြမည့်အာရုံခံစက်များ" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:7 +msgid "List of sensors to be shown in the panel" +msgstr "ပန်နယ်တွင်ပြမည့်အာရုံခံစက်များစာရင်း" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:12 +msgid "Delay between sensor polling" +msgstr "အာရုံခံစက်တန်ဖိုးဖတ်မည့်နှုန်း" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:17 +msgid "Position in Panel ('left', 'center', 'right')" +msgstr "ပန်နယ်ရှိနေရာ('ဘယ်','အလယ်','ညာ')" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:22 +msgid "Show one extra digit after decimal" +msgstr "ဒသမနောက် နောက်ထပ်ကိန်းတစ်လုံးပိုပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:27 +msgid "Display sensors in alphabetical order" +msgstr "အာရုံခံစက်များကိုအက္ခရာစဉ်ဖြင့်ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:32 +msgid "Hide data from sensors that are invalid" +msgstr "အာရုံခံစက်များမှ မမှန်ကန်သောဒေတာများဖွက်မည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:37 +msgid "Display temperature of various components" +msgstr "စက်ပစ္စည်းများ၏အပူချိန် ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:41 +msgid "Temperature unit" +msgstr "အပူချိန်ယူနစ်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:42 +msgid "" +"The unit ('centigrade' or 'fahrenheit') the extension should display the " +"temperature in" +msgstr "အပူချိန်ကိုဖော်ပြမည့်ယူနစ် (စင်တိီဂရိတ် သို့ ဖာရင်ဟိုက်)" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:47 +msgid "Display voltage of various components" +msgstr "စက်ပစ္စည်းများ၏ဗို့အား ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:52 +msgid "Display fan rotation per minute" +msgstr "ပန်ကာလည်နှုန်းပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:57 +msgid "Display memory information" +msgstr "မှတ်ဉာဏ်အချက်အလက်ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:62 +msgid "Display processor information" +msgstr "စက်ဦးနှောက်အချက်အလက်ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:67 +msgid "Display system information" +msgstr "စနစ်အချက်အလက်ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:72 +msgid "Display storage information" +msgstr "သိုလှောင်နေရာအချက်အလက်ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:77 +msgid "Display network information" +msgstr "ကွန်ရက်အချက်အလက်ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:82 +msgid "Display public IP address of internet connection" +msgstr "အများပြည်သူIPလိပ်စာဖော်ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:86 +msgid "Network speed format" +msgstr "ကွန်ရက်မြန်နှုန်းပုံစံ" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:87 +msgid "Should speed display in bits or bytes?" +msgstr "ဘစ် သို့ ဘိုက်ဖြင့် ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:91 +msgid "Storage path" +msgstr "သိုလှောင်နေရာလမ်းကြောင်း" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:92 +msgid "Storage path for monitoring" +msgstr "စောင့်ကြည့်မည့်သိုလှောင်နေရာလမ်းကြောင်း" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:97 +msgid "Monitor battery health" +msgstr "ဘက်ထရီကျန်းမာရေးစောင့်ကြည့်မည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:101 +msgid "Memory measurement" +msgstr "မှတ်ဉာဏ်တိုင်းတာမှု" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:102 +msgid "Can use gigabyte or gibibyte for memory" +msgstr "ဂစ်ဂါဘိုက် သို့ ဂစ်ဘီဘိုက်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:106 +msgid "Storage measurement" +msgstr "သိုလှောင်နေရာတိုင်းတာမှု" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:107 +msgid "Can use gigabyte or gibibyte for storage" +msgstr "ဂစ်ဂါဘိုက် သို့ ဂစ်ဘီဘိုက်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:111 +msgid "Display battery BAT0" +msgstr "BAT0ပြ" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:112 +msgid "Should battery 'BAT0' be displayed?" +msgstr "BAT0ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:116 +msgid "Display battery BAT1" +msgstr "BAT1ပြ" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:117 +msgid "Should battery 'BAT1' be displayed?" +msgstr "BAT1ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:121 +msgid "Display battery BAT2" +msgstr "BAT2ပြ" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:122 +msgid "Should battery 'BAT2' be displayed?" +msgstr "BAT2ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:126 +msgid "Display battery CMB0" +msgstr "CMB0ပြ" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:127 +msgid "Should battery 'CMB0' be displayed?" +msgstr "CMB0ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:131 +msgid "Display battery macsmc-battery" +msgstr "ဘက်ထရီmacsmc ပြ" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:132 +msgid "Should battery 'macsmc-battery' be displayed?" +msgstr "ဘက်ထရီmacsmc ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:136 +msgid "Display combined battery data" +msgstr "ဘက်ထရီဒေတာပေါင်းပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:137 +msgid "Display combined values for selected batteries" +msgstr "ရွေးချယ်ထားသောဘက်ထရီများ၏ဒေတာများပေါင်းပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:142 +msgid "Include 'BAT0' when calculating combined Battery" +msgstr "ဘက်ထရီဒေတာပေါင်းပြဖို့တွက်ချက်ရာတွင် 'BAT0'ပါဝင်စေမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:147 +msgid "Include 'BAT1' when calculating combined Battery" +msgstr "ဘက်ထရီဒေတာပေါင်းပြဖို့တွက်ချက်ရာတွင် 'BAT1'ပါဝင်စေမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:152 +msgid "Include 'BAT2' when calculating combined Battery" +msgstr "ဘက်ထရီဒေတာပေါင်းပြဖို့တွက်ချက်ရာတွင် 'BAT2'ပါဝင်စေမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:157 +msgid "Include 'CMB0' when calculating combined Battery" +msgstr "ဘက်ထရီဒေတာပေါင်းပြဖို့တွက်ချက်ရာတွင် 'CMBO'ပါဝင်စေမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:162 +msgid "Include 'macsmc-battery' when calculating combined Battery" +msgstr "ဘက်ထရီဒေတာပေါင်းပြဖို့တွက်ချက်ရာတွင် 'macsmc-battery'ပါဝင်စေမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:166 +msgid "Use fixed widths in top bar" +msgstr "ထိပ်ဆုံးတန်းတွင်ပုံသေအနံအသုံးပြုမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:167 +msgid "Keep sensors in top bar from jumping around" +msgstr "ထိပ်ဆုံးတန်းရှိ အာရုံခံစက်ပြခြင်းကို အသေထားမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:172 +msgid "Keep top bar clean by only showing sensor values" +msgstr "ရှင်းရှင်းလင်းလင်းဖြစ်အောင် တန်ဖိုးပဲပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:176 +msgid "Make the menu centered" +msgstr "မီနူးကိုအမြဲတမ်းအလယ်ထားမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:177 +msgid "Center the menu to the icon regardless of the position in the panel" +msgstr "ပန်နယ်တွင်ဘယ်နေရာရောက်ရောက် မီနူးကို အိုင်ကွန်ဖြင့် အလယ်ထားမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:181 +msgid "System Monitor command" +msgstr "စနစ်စောင့်ကြည့်အမိန့်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:182 +msgid "The command run when system monitor button is clicked" +msgstr "စနစ်စောင့်ကြည့်ခလုတ်ကိုနှိပ်လိုက်သောအခါလုပ်ဆောင်မည့်အမိန့်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:186 +msgid "Include processor static information" +msgstr "စက်ဦးနှောက်၏အငြိမ်အချက်အလက်ပြမည်" + +#: schemas/org.gnome.shell.extensions.vitals.gschema.xml:187 +msgid "Display processor static information that doesn't change" +msgstr "စက်ဦးနှောက်၏မပြောင်းလဲသောအချက်အလက်ပြမည်" + +#: sensors.js:65 +msgid "Public IP" +msgstr "အများပြည်သူIP" + +#: sensors.js:133 sensors.js:181 +msgid "Usage" +msgstr "အသုံးပြု" + +#: sensors.js:134 +msgid "memory" +msgstr "မှတ်ဉာဏ်" + +#: sensors.js:135 +msgid "Physical" +msgstr "အစစ်" + +#: sensors.js:136 +msgid "Available" +msgstr "ရရှိနိုင်" + +#: sensors.js:137 +msgid "Allocated" +msgstr "ပေးထား" + +#: sensors.js:138 +msgid "Cached" +msgstr "ယာယီကတ်ရှ္" + +#: sensors.js:139 sensors.js:355 +msgid "Free" +msgstr "လွတ်" + +#: sensors.js:140 +msgid "Swap" +msgstr "Swap" + +#: sensors.js:248 +msgid "Average" +msgstr "ပျမ်းမျှ" + +#: sensors.js:180 +msgid "processor" +msgstr "စက်ဦးနှောက်" + +#: sensors.js:183 +#, javascript-format +msgid "Core %d" +msgstr "ဦးနှောက်ငယ် %d" + +#: sensors.js:213 sensors.js:222 +msgid "Frequency" +msgstr "ကြိမ်နှုန်း" + +#: sensors.js:231 +msgid "Open Files" +msgstr "ဖွင့်ထားသောဖိုင်" + +#: sensors.js:238 +msgid "Load 1m" +msgstr "ဝန် ၁မိနစ်" + +#: sensors.js:239 +msgid "system" +msgstr "စနစ်" + +#: sensors.js:240 +msgid "Load 5m" +msgstr "ဝန် ၅မိနစ်" + +#: sensors.js:241 +msgid "Load 15m" +msgstr "ဝန် ၁၅မိနစ်" + +#: sensors.js:242 +msgid "Threads Active" +msgstr "သက်ဝင်အလုပ်တန်း" + +#: sensors.js:243 +msgid "Threads Total" +msgstr "စုစုပေါင်းအလုပ်တန်း" + +#: sensors.js:248 +msgid "Uptime" +msgstr "စက်နိုးကြာချိန်" + +#: sensors.js:252 +msgid "Process Time" +msgstr "လုပ်ဆောင်ကြာချိန်" + +#: sensors.js:302 +msgid "WiFi Link Quality" +msgstr "ဝိုင်ဖိုင်ချိတ်ဆက်မှုအရည်အသွေး" + +#: sensors.js:303 +msgid "WiFi Signal Level" +msgstr "ဝိုင်ဖိုင်အချက်ပြပမာဏ" + +#: sensors.js:318 +msgid "ARC Target" +msgstr "ARCပစ်မှတ်" + +#: sensors.js:319 +msgid "ARC Maximum" +msgstr "ARCအများဆုံး" + +#: sensors.js:320 +msgid "ARC Current" +msgstr "ARCလက်ရှိ" + +#: sensors.js:330 +msgid "Read total" +msgstr "စုစုပေါင်းဖတ်မှု" + +#: sensors.js:331 +msgid "Write total" +msgstr "စုစုပေါင်းရေးမှု" + +#: sensors.js:332 +msgid "Read rate" +msgstr "ဖတ်နှုန်း" + +#: sensors.js:333 +msgid "Write rate" +msgstr "ရေးနှုန်း" + +#: sensors.js:352 +msgid "Total" +msgstr "စုစုပေါင်း" + +#: sensors.js:353 +msgid "Used" +msgstr "သုံး" + +#: sensors.js:354 +msgid "Reserved" +msgstr "အရန်" + +#: sensors.js:356 +msgid "storage" +msgstr "သိုလှောင်နေရာ" + +#: sensors.js:393 +msgid "Label" +msgstr "အညွှန်း" + +#: sensors.js:404 sensors.js:529 +msgid "State" +msgstr "အခြေအနေ" + +#: sensors.js:409 sensors.js:519 +msgid "Cycles" +msgstr "အားသွင်းအကြိမ်ရေ" + +#: sensors.js:414 +msgid "Voltage" +msgstr "ဗို့အား" + +#: sensors.js:418 +msgid "Level" +msgstr "လျှပ်သိုပမာဏ" + +#: sensors.js:422 sensors.js:553 +msgid "Percentage" +msgstr "ရာခိုင်နှုန်း" + +#: sensors.js:432 sensors.js:527 +msgid "Rate" +msgstr "နှုန်း" + +#: sensors.js:442 sensors.js:535 +msgid "Energy (full)" +msgstr "စွမ်းအင်(အပြည့်)" + +#: sensors.js:451 sensors.js:541 +msgid "Energy (design)" +msgstr "စွမ်းအင်(ဒီဇိုင်း)" + +#: sensors.js:455 +msgid "Capacity" +msgstr "လျှပ်သိုအား" + +#: sensors.js:464 sensors.js:547 +msgid "Energy (now)" +msgstr "စွမ်းအင်(ယခု)" + +#: sensors.js:501 sensors.js:504 sensors.js:588 sensors.js:591 +msgid "Time left" +msgstr "ကျန်ရှိချိန်" + +#: sensors.js:677 +msgid "Vendor" +msgstr "ထုတ်လုပ်သူ" + +#: sensors.js:678 +msgid "Bogomips" +msgstr "Bogomips" + +#: sensors.js:679 +msgid "Sockets" +msgstr "ဆော့ကတ်" + +#: sensors.js:680 +msgid "Cache" +msgstr "ကက်ရှ်" + +#: sensors.js:685 +msgid "Kernel" +msgstr "ကာနယ်" + +#: prefs.js:98 +msgid "Temperature" +msgstr "အပူချိန်" + +#: prefs.js:98 +msgid "Network" +msgstr "ကွန်ရက်" + +#: prefs.js:98 +msgid "Storage" +msgstr "သိုလှောင်နေရာ" + +#: prefs.js:98 +msgid "Memory" +msgstr "မှတ်ဉာဏ်" + +#: prefs.js:98 +msgid "Battery" +msgstr "ဘက်ထရီ" + +#: extension.js:112 +msgid "Battery 1" +msgstr "ဘက်ထရီ၁" + +#: extension.js:112 +msgid "Battery 2" +msgstr "ဘက်ထရီ၂" + +#: extension.js:112 +msgid "Battery 3" +msgstr "ဘက်ထရီ၃" + +#: extension.js:112 +msgid "Battery 4" +msgstr "ဘက်ထရီ၄" + +#: extension.js:112 +msgid "Battery 5" +msgstr "ဘက်ထရီ၅" + +#: extension.js:115 +msgid "Battery (hidden)" +msgstr "ဘက်ထရီ(ဖွက်)" + +#: prefs.js:98 +msgid "System" +msgstr "စနစ်" + +#: prefs.js:98 +msgid "Processor" +msgstr "ပရိုဆက်ဆာ" + +#: sensors.js:759 +msgid "Fan" +msgstr "ပန်ကာ" From ddb0377e20516bcd972b71d2a040d13b3b48f411 Mon Sep 17 00:00:00 2001 From: Chris Monahan <3803591+corecoding@users.noreply.github.com> Date: Fri, 3 Oct 2025 08:31:31 -0400 Subject: [PATCH 04/27] Update metadata.json --- metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metadata.json b/metadata.json index d97ea9bf..2fba3a90 100644 --- a/metadata.json +++ b/metadata.json @@ -8,7 +8,7 @@ ], "url": "https://github.com/corecoding/Vitals", "uuid": "Vitals@CoreCoding.com", - "version": 72, + "version": 73, "donations": { "paypal": "corecoding" } From 89de55d1f3b6d9ffd362b3dcc7c687c85f58ea2a Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Fri, 3 Oct 2025 08:42:02 -0400 Subject: [PATCH 05/27] bring back %s and add trim --- values.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/values.js b/values.js index eae5aa08..e9192e56 100644 --- a/values.js +++ b/values.js @@ -200,7 +200,7 @@ export const Values = GObject.registerClass({ ending = 'Wh'; break; case 'load': - format = (use_higher_precision)?'%.2f %s':'%.1f'; + format = (use_higher_precision)?'%.2f %s':'%.1f %s'; break; case 'pcie': let split = value.split('x'); @@ -212,7 +212,7 @@ export const Values = GObject.registerClass({ break; } - return format.format(value, ending); + return format.format(value, ending).trim(); } returnIfDifferent(dwell, label, value, type, format, key) { From 5816a0a6e9f54b41e2a88da0369e586375125c31 Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Sun, 22 Feb 2026 15:37:28 -0500 Subject: [PATCH 06/27] history start --- extension.js | 111 +++++++++++++++++- historyGraph.js | 104 ++++++++++++++++ prefs.js | 3 +- prefs.ui | 29 +++++ schemas/gschemas.compiled | Bin 1972 -> 2084 bytes ....gnome.shell.extensions.vitals.gschema.xml | 10 ++ stylesheet.css | 18 +++ values.js | 35 ++++++ 8 files changed, 303 insertions(+), 7 deletions(-) create mode 100644 historyGraph.js diff --git a/extension.js b/extension.js index d6278e75..9c05b74b 100644 --- a/extension.js +++ b/extension.js @@ -17,6 +17,7 @@ import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js'; import * as Values from './values.js'; import * as Config from 'resource:///org/gnome/shell/misc/config.js'; import * as MenuItem from './menuItem.js'; +import * as HistoryGraph from './historyGraph.js'; let vitalsMenu; @@ -58,6 +59,10 @@ var VitalsMenuButton = GObject.registerClass({ this._newGpuDetected = false; this._newGpuDetectedCount = 0; this._last_query = new Date().getTime(); + this._historyPopout = null; + this._historyPopoutLeaveId = 0; + this._historyHideTimeoutId = null; + this._historyPopoutSensorKey = null; this._sensors = new Sensors.Sensors(this._settings, this._sensorIcons); this._values = new Values.Values(this._settings, this._sensorIcons); @@ -81,7 +86,7 @@ var VitalsMenuButton = GObject.registerClass({ this._addSettingChangedSignal('menu-centered', this._positionInPanelChanged.bind(this)); this._addSettingChangedSignal('icon-style', this._iconStyleChanged.bind(this)); - let settings = [ 'use-higher-precision', 'alphabetize', 'hide-zeros', 'fixed-widths', 'hide-icons', 'unit', 'memory-measurement', 'include-public-ip', 'network-speed-format', 'storage-measurement', 'include-static-info', 'include-static-gpu-info' ]; + let settings = [ 'use-higher-precision', 'alphabetize', 'hide-zeros', 'fixed-widths', 'hide-icons', 'unit', 'memory-measurement', 'include-public-ip', 'network-speed-format', 'storage-measurement', 'include-static-info', 'include-static-gpu-info', 'show-sensor-history-graph' ]; for (let setting of Object.values(settings)) this._addSettingChangedSignal(setting, this._redrawMenu.bind(this)); @@ -90,6 +95,7 @@ var VitalsMenuButton = GObject.registerClass({ this._addSettingChangedSignal('show-' + sensor, this._showHideSensorsChanged.bind(this)); this._initializeMenu(); + this._createHistoryPopout(); // start off with fresh sensors this._querySensors(); @@ -176,8 +182,84 @@ var VitalsMenuButton = GObject.registerClass({ // refresh sensors now this._querySensors(); + } else { + this._hideHistoryPopout(); + } + }); + } + + _createHistoryPopout() { + const popoutWidth = 240; + const popoutHeight = 110; + this._historyPopout = new St.BoxLayout({ + vertical: true, + style_class: 'vitals-history-popout', + width: popoutWidth, + height: popoutHeight, + reactive: true, + visible: false + }); + this._historyGraph = new HistoryGraph.HistoryGraph(); + this._historyTimeLabel = new St.Label({ + text: _('1 Hour'), + style_class: 'vitals-history-popout-label', + x_align: Clutter.ActorAlign.END + }); + this._historyPopout.add_child(this._historyTimeLabel); + this._historyPopout.add_child(this._historyGraph); + this._historyPopout.connect('enter-event', () => { + if (this._historyHideTimeoutId) { + GLib.Source.remove(this._historyHideTimeoutId); + this._historyHideTimeoutId = null; } }); + this._historyPopout.connect('leave-event', () => { + this._scheduleHistoryPopoutHide(); + }); + } + + _showHistoryPopout(key, label, itemActor) { + if (!this._settings.get_boolean('show-sensor-history-graph')) return; + const samples = this._values.getTimeSeries(key); + if (samples.length === 0) return; + this._historyPopoutSensorKey = key; + this._historyGraph.setData(samples, label, ''); + const parent = this.menu.actor.get_parent(); + if (!parent) return; + if (this._historyPopout.get_parent() !== parent) { + if (this._historyPopout.get_parent()) + this._historyPopout.get_parent().remove_child(this._historyPopout); + parent.add_child(this._historyPopout); + } + const menuX = this.menu.actor.get_x(); + const menuY = this.menu.actor.get_y(); + this._historyPopout.set_position(menuX - this._historyPopout.get_width() - 8, menuY); + this._historyPopout.show(); + if (this._historyHideTimeoutId) { + GLib.Source.remove(this._historyHideTimeoutId); + this._historyHideTimeoutId = null; + } + } + + _scheduleHistoryPopoutHide() { + if (this._historyHideTimeoutId) return; + this._historyHideTimeoutId = GLib.timeout_add(250, GLib.PRIORITY_DEFAULT, () => { + this._hideHistoryPopout(); + this._historyHideTimeoutId = null; + return GLib.SOURCE_REMOVE; + }); + } + + _hideHistoryPopout() { + if (this._historyHideTimeoutId) { + GLib.Source.remove(this._historyHideTimeoutId); + this._historyHideTimeoutId = null; + } + if (this._historyPopout && this._historyPopout.get_parent()) { + this._historyPopout.hide(); + this._historyPopout.get_parent().remove_child(this._historyPopout); + } + this._historyPopoutSensorKey = null; } _initializeMenuGroup(groupName, optionName, menuSuffix = '', position = -1) { @@ -245,11 +327,9 @@ var VitalsMenuButton = GObject.registerClass({ GLib.PRIORITY_DEFAULT, update_time, (self) => { - // only update menu if we have hot sensors - if (Object.values(this._hotLabels).length > 0) - this._querySensors(); - // keep the timer running - return GLib.SOURCE_CONTINUE; + // always query sensors (for panel display when hot, and for history graph data) + this._querySensors(); + return GLib.SOURCE_CONTINUE; } ); } @@ -349,6 +429,7 @@ var VitalsMenuButton = GObject.registerClass({ } _redrawMenu() { + this._hideHistoryPopout(); this._removeHotItems(); for (let key in this._sensorMenuItems) { @@ -477,6 +558,23 @@ var VitalsMenuButton = GObject.registerClass({ } this._groups[type].menu.addMenuItem(item, i); + + if (this._settings.get_boolean('show-sensor-history-graph')) { + const key = item.key; + const label = item.label; + item.actor.connect('enter-event', () => { + const samples = this._values.getTimeSeries(key); + if (samples.length > 0) + this._showHistoryPopout(key, label, item.actor); + if (this._historyHideTimeoutId) { + GLib.Source.remove(this._historyHideTimeoutId); + this._historyHideTimeoutId = null; + } + }); + item.actor.connect('leave-event', () => { + this._scheduleHistoryPopoutHide(); + }); + } } _defaultLabel() { @@ -629,6 +727,7 @@ var VitalsMenuButton = GObject.registerClass({ } destroy() { + this._hideHistoryPopout(); this._destroyTimer(); this._sensors.destroy(); diff --git a/historyGraph.js b/historyGraph.js new file mode 100644 index 00000000..e4412546 --- /dev/null +++ b/historyGraph.js @@ -0,0 +1,104 @@ +/* + Copyright (c) 2018, Chris Monahan + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the GNOME nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import Clutter from 'gi://Clutter'; +import GObject from 'gi://GObject'; +import St from 'gi://St'; + +const GRAPH_WIDTH = 220; +const GRAPH_HEIGHT = 90; +const PADDING = 4; +const BAR_GAP = 1; + +export const HistoryGraph = GObject.registerClass({ + GTypeName: 'HistoryGraph', +}, class HistoryGraph extends St.Widget { + + _init(params = {}) { + super._init({ + width: GRAPH_WIDTH, + height: GRAPH_HEIGHT, + style_class: 'vitals-history-graph', + ...params + }); + this._samples = []; + this._label = ''; + this._unit = ''; + this._canvas = new Clutter.Canvas(); + this._canvas.set_size(GRAPH_WIDTH, GRAPH_HEIGHT); + this._canvas.connect('draw', this._onDraw.bind(this)); + this.set_content(this._canvas); + } + + setData(samples, label, unit) { + this._samples = Array.isArray(samples) ? samples : []; + this._label = label || ''; + this._unit = unit || ''; + this._canvas.invalidate(); + } + + _onDraw(canvas, cr, width, height) { + cr.setOperator(1); // CAIRO_OPERATOR_CLEAR + cr.paint(); + cr.setOperator(0); // CAIRO_OPERATOR_OVER + + const data = this._samples; + if (data.length === 0) return true; + + const x0 = PADDING; + const y0 = PADDING; + const graphW = width - 2 * PADDING; + const graphH = height - 2 * PADDING; + if (graphW <= 0 || graphH <= 0) return true; + + const vals = data.map(d => d.v); + let vMin = Math.min(...vals); + let vMax = Math.max(...vals); + if (vMax <= vMin) { + vMin = vMin - 1; + vMax = vMax + 1; + } + const vRange = vMax - vMin; + const tMin = data[0].t; + const tMax = data[data.length - 1].t; + const tRange = Math.max(0.001, tMax - tMin); + + const barWidth = Math.max(1, (graphW - (data.length - 1) * BAR_GAP) / data.length - BAR_GAP); + + cr.setSourceRgba(0.2, 0.5, 0.9, 0.85); + for (let i = 0; i < data.length; i++) { + const v = data[i].v; + const norm = (v - vMin) / vRange; + const barH = Math.max(1, norm * graphH); + const x = x0 + (data[i].t - tMin) / tRange * (graphW - barWidth); + const y = y0 + graphH - barH; + cr.rectangle(x, y, barWidth, barH); + } + cr.fill(); + + return true; + } +}); diff --git a/prefs.js b/prefs.js index 07916bdf..ce7885cb 100644 --- a/prefs.js +++ b/prefs.js @@ -49,7 +49,8 @@ const Settings = new GObject.Class({ 'alphabetize', 'hide-zeros', 'include-public-ip', 'show-battery', 'fixed-widths', 'hide-icons', 'menu-centered', 'include-static-info', - 'show-gpu', 'include-static-gpu-info' ]; + 'show-gpu', 'include-static-gpu-info', + 'show-sensor-history-graph' ]; for (let key in sensors) { let sensor = sensors[key]; diff --git a/prefs.ui b/prefs.ui index 950c1b0f..aa63bde1 100644 --- a/prefs.ui +++ b/prefs.ui @@ -415,6 +415,35 @@ + + + 100 + 0 + + + 0 + 6 + 6 + + + 1 + 0 + start + 5 + 5 + Show sensor history graph on hover + + + + + end + 5 + + + + + + diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index 936fdb3a506ffb8d2405e995628fbde747e729c0..e60cb0f1a650ca893c3a45db007e3a7cdee12b84 100644 GIT binary patch literal 2084 zcmZ8iL1-LR7=G2p)}(2+X>AiVn%FA4%Iq4d)uM=?#T+~oY^C%veY^XzGj?X)IBzD| zja3BOgCbb)Qcy_`ih`)|AcjiCi`a`0p)Fpdh?gKXr3Yzp(D;3CW}C)^kMEmr{`~K~ z|NZmlg_mXG%P1AaZwP!O*X7#^Zv*&p_4j>bz8e!0;sp5VjY9O?CB!Ozqnlxf7X^6_ zpp#a$73ok_ji-X3s@kcFjISe8o$^x|7={Z`ejsPT++6SW|DgxZn*gT2vp^qs3$V37 z2|NgF2X+A1(oO7y-c^8S8wTzM?g8!t?k_M3UaGxvnCtLdePT*XfF*DxJMbNS>R$LU zI0N3*Zy%*k&Gy?2E^HRDf%&j963?h<|K&G65HKLK9bx3Zr;^)~qP;Pb$> z2jgD))H~q+3jPgv_`;`O)2HUTi{RZTRQE2Zc^7-Y9|H%npQag4y#?`e;BSEUZ~S?X zKJ`}k7r;LQKfL}%AMcd9hwFm>1+tSL*!oa2Pw5WS85o&;Y=wEK>6gI{@KocWGJR^E z>sjywaBX({GJR^s9|0c&zI<-AMxUDJioqH1x~!KZ*^_vr{0D5v*6E& zuS)0WQ?vf_;Pb$ZcfQ|ApL#p|1@I-{;_OEY^r^X@Meq`EeP*w{FY12yY{VX5?Bo-V zFrK;ue+WDRES~%JL;BRr^C88Sjj$=~x9*sZNmb|+FVOL(;`vSG%&0^c zU%V*SE5_5)PN+hibT;tWZ|KM|X(v!v1;4VNhfK;Se;+l13~A!59{3H%j|Juh{w_1h z@%)yj5+_bn!{>a-GcZZL;_1{e7&|&KZoZtG8QD^>T0T838zN_!|Q_pE#E|WAKTJQGhe9 zALs=*i#U_E0i02sZ}$S6Uz|^zQ)oS5%QdBg6kE6Fwnm)jhB7!CB8qxcW0`vPQPePi zYxYuVb^zDb25>DOz%?YE^xLZGO#98$E3RiPe^HM-U0&~48$U&j9Y1nn8L1#Y5AUlk z(^MrL#{~N3{}TE#h&@?XsXvq7?OOi)2J&ZnN4QFZcIr6IERkG>OP5$g@Tn9j>~nXEp^^qGVq^Jh_7>ywcEv}s{x$OF s@BQ^hZ^c=0eAdPGM{y&Jx4*8B(C%}$A<7EvzB+z~1|PRe;k)nKZ)azIzu)Zq z?zxxzEc6pA%IkjcbfK$vm)>3A8?_(D$$WNHoD%22eSJdgI3UCwTq`?ai08#B_;WyK z?OHq0v8tIsMNv(4tV&F%6H}WFtsfZ<7ovK^kr14l>k;<=m|Aek9{Qn)ff9q@J-}XI zAFv-dSmF?vd5n~J0Q}%K9EK(yF7e&@HwKu4%SX(L3Kky+j$U~B$$K$|x&r?c_yypn zM6J-L-VZ+jr$B40(WOs40RK(!Md0el(mDFneef@V7l6t9+8llA-SC&eE5K$=pX$(ti5XtbYf59$0-V_=`UEA@~d6 zMc|M5W3CR=gYZ|uUjVNi-xvltJq&*hH`jp=ZvQexpL!4cAHY8Y-!CoFa+>jQvA==* z!iO$jYWfv?v!x%5&!Y5MrNw_r> z1ojB_6rTSIa38>P&pyH)z@D)SV2@xAVm$0E>>)gU>_x*s5Vn*Io9JJ-aToG+K^m*G zGD!F?PUp|~N$)<;RvNTFof$7yvCg_)Zt#vLJkvF(qZetv<&DKKyj~Hrju-nK&l5vO zj)|&oiq(qO%XJu!G`1TRwPbzEXk@H>gN=z*+1i;KpT`=7)|qU^Ejq$&wJf5Lk<_;D Zqul5J`KW^Pu%7$q=2fN{9PZ1p{{VT+hkyV8 diff --git a/schemas/org.gnome.shell.extensions.vitals.gschema.xml b/schemas/org.gnome.shell.extensions.vitals.gschema.xml index 7a0decd7..72f8f409 100644 --- a/schemas/org.gnome.shell.extensions.vitals.gschema.xml +++ b/schemas/org.gnome.shell.extensions.vitals.gschema.xml @@ -151,5 +151,15 @@ Icon styles Set the style for the displayed sensor icons ('original', 'updated') + + true + Show sensor history graph on hover + When hovering a sensor row in the menu, show a pop-out graph of its history to the left + + + 3600 + Sensor history duration (seconds) + How many seconds of history to keep and display in the hover graph (e.g. 3600 for 1 hour) + diff --git a/stylesheet.css b/stylesheet.css index 4fcd9858..d2f8d2d4 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -17,3 +17,21 @@ .vitals-button-action:hover, .vitals-button-action:focus { border-color: #777; } .vitals-button-action > StIcon { icon-size: 16px; } .vitals-button-box { padding: 0px; spacing: 22px; } + +.vitals-history-popout { + padding: 6px; + spacing: 4px; + border-radius: 8px; + border: 1px solid rgba(128, 128, 128, 0.4); + background-color: rgba(30, 30, 30, 0.95); +} + +.vitals-history-popout-label { + font-size: 10px; + color: rgba(200, 200, 200, 0.9); +} + +.vitals-history-graph { + background-color: rgba(0, 0, 0, 0.3); + border-radius: 4px; +} diff --git a/values.js b/values.js index e9192e56..2ce9f012 100644 --- a/values.js +++ b/values.js @@ -47,9 +47,40 @@ export const Values = GObject.registerClass({ this._history = {}; //this._history2 = {}; + this._timeSeries = {}; + this._graphableFormats = ['temp', 'in', 'fan', 'percent', 'hertz', 'memory', 'speed', 'storage', 'watt', 'watt-gpu', 'milliamp', 'milliamp-hour', 'load']; this.resetHistory(); } + _getHistoryDurationSeconds() { + if (this._settings && this._settings.get_int) + return Math.max(60, this._settings.get_int('sensor-history-duration')); + return 3600; + } + + _pushTimePoint(key, value, format) { + if (!this._graphableFormats.includes(format)) return; + const num = typeof value === 'number' ? value : parseFloat(value); + if (num !== num) return; // NaN check + const now = Date.now() / 1000; + if (!(key in this._timeSeries)) this._timeSeries[key] = []; + const buf = this._timeSeries[key]; + buf.push({ t: now, v: num }); + const maxAge = this._getHistoryDurationSeconds(); + while (buf.length > 0 && buf[0].t < now - maxAge) buf.shift(); + const maxPoints = 3600; + while (buf.length > maxPoints) buf.shift(); + } + + getTimeSeries(key) { + if (!(key in this._timeSeries)) return []; + return this._timeSeries[key].slice(); + } + + isGraphableFormat(format) { + return this._graphableFormats.includes(format); + } + _legible(value, sensorClass) { let unit = 1000; if (value === null) return 'N/A'; @@ -242,6 +273,8 @@ export const Values = GObject.registerClass({ // save previous values to update screen on changes only let previousValue = this._history[type][key]; this._history[type][key] = [legible, value]; + if (type !== 'network-rx' && type !== 'network-tx') + this._pushTimePoint(key, value, format); // process average, min and max values if (type == 'temperature' || type == 'voltage' || type == 'fan') { @@ -284,6 +317,7 @@ export const Values = GObject.registerClass({ // calculate speed for this interface let speed = (value - previousValue[1]) / dwell; output.push([label, this._legible(speed, 'speed'), type, key]); + this._pushTimePoint(key, speed, 'speed'); // store speed for Device report if (!(direction in this._networkSpeeds)) this._networkSpeeds[direction] = {}; @@ -350,5 +384,6 @@ export const Values = GObject.registerClass({ this._history['gpu#' + i] = {}; this._history['gpu#' + i + '-group'] = {}; } + this._timeSeries = {}; } }); From e9871e7da24776d92f5b72a3f0252189e68d629f Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Sun, 22 Feb 2026 16:41:25 -0500 Subject: [PATCH 07/27] fixes graphs --- extension.js | 27 ++++++++++++++++++++---- historyGraph.js | 56 ++++++++++++++++++++++++++++--------------------- stylesheet.css | 10 +++++++++ 3 files changed, 65 insertions(+), 28 deletions(-) diff --git a/extension.js b/extension.js index 9c05b74b..61977701 100644 --- a/extension.js +++ b/extension.js @@ -223,7 +223,11 @@ var VitalsMenuButton = GObject.registerClass({ const samples = this._values.getTimeSeries(key); if (samples.length === 0) return; this._historyPopoutSensorKey = key; - this._historyGraph.setData(samples, label, ''); + try { + this._historyGraph.setData(samples, label, ''); + } catch (e) { + // still show popout even if graph fails to build + } const parent = this.menu.actor.get_parent(); if (!parent) return; if (this._historyPopout.get_parent() !== parent) { @@ -233,7 +237,20 @@ var VitalsMenuButton = GObject.registerClass({ } const menuX = this.menu.actor.get_x(); const menuY = this.menu.actor.get_y(); - this._historyPopout.set_position(menuX - this._historyPopout.get_width() - 8, menuY); + let popoutY = menuY; + if (itemActor) { + let relY = 0; + let node = itemActor; + while (node && node !== this.menu.actor) { + relY += node.get_y(); + node = node.get_parent(); + } + const rowH = itemActor.get_height(); + const popoutH = this._historyPopout.get_height(); + popoutY = menuY + relY + Math.round((rowH - popoutH) / 2); + popoutY = Math.max(0, popoutY); + } + this._historyPopout.set_position(menuX - this._historyPopout.get_width() - 8, popoutY); this._historyPopout.show(); if (this._historyHideTimeoutId) { GLib.Source.remove(this._historyHideTimeoutId); @@ -564,12 +581,14 @@ var VitalsMenuButton = GObject.registerClass({ const label = item.label; item.actor.connect('enter-event', () => { const samples = this._values.getTimeSeries(key); - if (samples.length > 0) - this._showHistoryPopout(key, label, item.actor); if (this._historyHideTimeoutId) { GLib.Source.remove(this._historyHideTimeoutId); this._historyHideTimeoutId = null; } + if (samples.length > 0) + this._showHistoryPopout(key, label, item.actor); + else + this._hideHistoryPopout(); }); item.actor.connect('leave-event', () => { this._scheduleHistoryPopoutHide(); diff --git a/historyGraph.js b/historyGraph.js index e4412546..1a60749c 100644 --- a/historyGraph.js +++ b/historyGraph.js @@ -47,32 +47,39 @@ export const HistoryGraph = GObject.registerClass({ this._samples = []; this._label = ''; this._unit = ''; - this._canvas = new Clutter.Canvas(); - this._canvas.set_size(GRAPH_WIDTH, GRAPH_HEIGHT); - this._canvas.connect('draw', this._onDraw.bind(this)); - this.set_content(this._canvas); + this._barContainer = new St.BoxLayout({ + vertical: false, + style_class: 'vitals-history-graph-bars', + x_expand: true, + y_expand: true, + y_align: Clutter.ActorAlign.END + }); + this.add_child(this._barContainer); } setData(samples, label, unit) { this._samples = Array.isArray(samples) ? samples : []; this._label = label || ''; this._unit = unit || ''; - this._canvas.invalidate(); + this._rebuildBars(); } - _onDraw(canvas, cr, width, height) { - cr.setOperator(1); // CAIRO_OPERATOR_CLEAR - cr.paint(); - cr.setOperator(0); // CAIRO_OPERATOR_OVER - + _rebuildBars() { + try { + const children = this._barContainer.get_children(); + if (children && children.length > 0) { + for (let i = children.length - 1; i >= 0; i--) + children[i].destroy(); + } + } catch (e) { + // ignore + } const data = this._samples; - if (data.length === 0) return true; + if (data.length === 0) return; - const x0 = PADDING; - const y0 = PADDING; - const graphW = width - 2 * PADDING; - const graphH = height - 2 * PADDING; - if (graphW <= 0 || graphH <= 0) return true; + const graphW = GRAPH_WIDTH - 2 * PADDING; + const graphH = GRAPH_HEIGHT - 2 * PADDING; + if (graphW <= 0 || graphH <= 0) return; const vals = data.map(d => d.v); let vMin = Math.min(...vals); @@ -88,17 +95,18 @@ export const HistoryGraph = GObject.registerClass({ const barWidth = Math.max(1, (graphW - (data.length - 1) * BAR_GAP) / data.length - BAR_GAP); - cr.setSourceRgba(0.2, 0.5, 0.9, 0.85); for (let i = 0; i < data.length; i++) { const v = data[i].v; const norm = (v - vMin) / vRange; - const barH = Math.max(1, norm * graphH); - const x = x0 + (data[i].t - tMin) / tRange * (graphW - barWidth); - const y = y0 + graphH - barH; - cr.rectangle(x, y, barWidth, barH); + const barH = Math.max(1, Math.round(norm * graphH)); + const bar = new St.Bin({ + width: Math.round(barWidth), + height: barH, + style_class: 'vitals-history-graph-bar', + y_align: Clutter.ActorAlign.END, + x_align: Clutter.ActorAlign.CENTER + }); + this._barContainer.add_child(bar); } - cr.fill(); - - return true; } }); diff --git a/stylesheet.css b/stylesheet.css index d2f8d2d4..2458f2dc 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -34,4 +34,14 @@ .vitals-history-graph { background-color: rgba(0, 0, 0, 0.3); border-radius: 4px; + padding: 4px; +} + +.vitals-history-graph-bars { + spacing: 1px; +} + +.vitals-history-graph-bar { + border-radius: 1px; + background-color: rgba(51, 128, 230, 0.87); } From dc582f7f8dd87383ef3be5422f2aaaf13e9fd727 Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Sun, 22 Feb 2026 17:55:07 -0500 Subject: [PATCH 08/27] changes label placement --- extension.js | 77 ++++++++++++++++++++++++++++++++++++++++++++++--- historyGraph.js | 23 +++++++++++++++ stylesheet.css | 38 ++++++++++++++++++++++-- values.js | 15 ++++++++-- 4 files changed, 144 insertions(+), 9 deletions(-) diff --git a/extension.js b/extension.js index 61977701..3f0f3b69 100644 --- a/extension.js +++ b/extension.js @@ -189,8 +189,8 @@ var VitalsMenuButton = GObject.registerClass({ } _createHistoryPopout() { - const popoutWidth = 240; - const popoutHeight = 110; + const popoutWidth = 280; + const popoutHeight = 145; this._historyPopout = new St.BoxLayout({ vertical: true, style_class: 'vitals-history-popout', @@ -199,6 +199,7 @@ var VitalsMenuButton = GObject.registerClass({ reactive: true, visible: false }); + this._historyPopout.clip_to_allocation = true; this._historyGraph = new HistoryGraph.HistoryGraph(); this._historyTimeLabel = new St.Label({ text: _('1 Hour'), @@ -206,7 +207,64 @@ var VitalsMenuButton = GObject.registerClass({ x_align: Clutter.ActorAlign.END }); this._historyPopout.add_child(this._historyTimeLabel); - this._historyPopout.add_child(this._historyGraph); + this._historyGraphRow = new St.BoxLayout({ + vertical: false, + x_expand: true, + style_class: 'vitals-history-graph-row' + }); + this._historyYAxis = new St.BoxLayout({ + vertical: true, + width: 56, + style_class: 'vitals-history-y-axis' + }); + this._historyYMax = new St.Label({ + text: '', + style_class: 'vitals-history-popout-axis', + x_align: Clutter.ActorAlign.START + }); + this._historyYMin = new St.Label({ + text: '', + style_class: 'vitals-history-popout-axis', + x_align: Clutter.ActorAlign.START + }); + this._historyYSpacer = new St.BoxLayout({ vertical: true, y_expand: true }); + this._historyYAxis.add_child(this._historyYMax); + this._historyYAxis.add_child(this._historyYSpacer); + this._historyYAxis.add_child(this._historyYMin); + this._historyGraphRow.add_child(this._historyYAxis); + this._historyGraphRow.add_child(this._historyGraph); + this._historyPopout.add_child(this._historyGraphRow); + this._historyXWrap = new St.BoxLayout({ + vertical: false, + x_expand: true, + style_class: 'vitals-history-x-wrap' + }); + this._historyXSpacer = new St.BoxLayout({ + vertical: true, + width: 62, + style_class: 'vitals-history-x-spacer' + }); + this._historyXRow = new St.BoxLayout({ + vertical: false, + x_expand: true, + style_class: 'vitals-history-x-row' + }); + this._historyXLeft = new St.Label({ + text: _('1h ago'), + style_class: 'vitals-history-popout-axis', + x_align: Clutter.ActorAlign.START, + x_expand: true + }); + this._historyXRight = new St.Label({ + text: _('now'), + style_class: 'vitals-history-popout-axis', + x_align: Clutter.ActorAlign.END + }); + this._historyXRow.add_child(this._historyXLeft); + this._historyXRow.add_child(this._historyXRight); + this._historyXWrap.add_child(this._historyXSpacer); + this._historyXWrap.add_child(this._historyXRow); + this._historyPopout.add_child(this._historyXWrap); this._historyPopout.connect('enter-event', () => { if (this._historyHideTimeoutId) { GLib.Source.remove(this._historyHideTimeoutId); @@ -225,8 +283,19 @@ var VitalsMenuButton = GObject.registerClass({ this._historyPopoutSensorKey = key; try { this._historyGraph.setData(samples, label, ''); + const rawRange = this._historyGraph.getRawRange(); + if (rawRange) { + this._historyYMax.text = this._values.formatValue(key, rawRange.max); + this._historyYMin.text = this._values.formatValue(key, rawRange.min); + this._historyYAxis.show(); + } else { + this._historyYMax.text = ''; + this._historyYMin.text = ''; + this._historyYAxis.hide(); + } } catch (e) { - // still show popout even if graph fails to build + this._historyYMax.text = ''; + this._historyYMin.text = ''; } const parent = this.menu.actor.get_parent(); if (!parent) return; diff --git a/historyGraph.js b/historyGraph.js index 1a60749c..6247d1ad 100644 --- a/historyGraph.js +++ b/historyGraph.js @@ -47,6 +47,8 @@ export const HistoryGraph = GObject.registerClass({ this._samples = []; this._label = ''; this._unit = ''; + this._vMin = 0; + this._vMax = 0; this._barContainer = new St.BoxLayout({ vertical: false, style_class: 'vitals-history-graph-bars', @@ -88,6 +90,8 @@ export const HistoryGraph = GObject.registerClass({ vMin = vMin - 1; vMax = vMax + 1; } + this._vMin = vMin; + this._vMax = vMax; const vRange = vMax - vMin; const tMin = data[0].t; const tMax = data[data.length - 1].t; @@ -109,4 +113,23 @@ export const HistoryGraph = GObject.registerClass({ this._barContainer.add_child(bar); } } + + getRangeLabel() { + if (this._samples.length === 0) return ''; + const a = this._vMin; + const b = this._vMax; + const fmt = (v) => Number.isInteger(v) ? String(v) : v.toFixed(1); + return fmt(a) + ' – ' + fmt(b); + } + + getRange() { + if (this._samples.length === 0) return null; + const fmt = (v) => Number.isInteger(v) ? String(v) : v.toFixed(1); + return { min: fmt(this._vMin), max: fmt(this._vMax) }; + } + + getRawRange() { + if (this._samples.length === 0) return null; + return { min: this._vMin, max: this._vMax }; + } }); diff --git a/stylesheet.css b/stylesheet.css index 2458f2dc..66e3b262 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -20,7 +20,6 @@ .vitals-history-popout { padding: 6px; - spacing: 4px; border-radius: 8px; border: 1px solid rgba(128, 128, 128, 0.4); background-color: rgba(30, 30, 30, 0.95); @@ -31,10 +30,45 @@ color: rgba(200, 200, 200, 0.9); } +.vitals-history-popout-axis { + font-size: 9px; + color: rgba(180, 180, 180, 0.85); +} + +.vitals-history-graph-row { + margin-top: 2px; + margin-bottom: 2px; +} + +.vitals-history-y-axis { + margin-right: 6px; + padding-top: 2px; + padding-bottom: 2px; +} + +.vitals-history-x-wrap { + margin-top: 2px; +} + +.vitals-history-x-spacer { + width: 62px; + min-width: 62px; +} + +.vitals-history-x-row { + min-width: 0; +} + +.vitals-history-x-row .vitals-history-popout-axis { + margin-left: 0; + margin-right: 4px; +} + .vitals-history-graph { - background-color: rgba(0, 0, 0, 0.3); + background-color: rgba(0, 0, 0, 0.25); border-radius: 4px; padding: 4px; + margin: 2px 0; } .vitals-history-graph-bars { diff --git a/values.js b/values.js index 2ce9f012..d52ef8f8 100644 --- a/values.js +++ b/values.js @@ -48,6 +48,7 @@ export const Values = GObject.registerClass({ this._history = {}; //this._history2 = {}; this._timeSeries = {}; + this._timeSeriesFormat = {}; this._graphableFormats = ['temp', 'in', 'fan', 'percent', 'hertz', 'memory', 'speed', 'storage', 'watt', 'watt-gpu', 'milliamp', 'milliamp-hour', 'load']; this.resetHistory(); } @@ -62,6 +63,7 @@ export const Values = GObject.registerClass({ if (!this._graphableFormats.includes(format)) return; const num = typeof value === 'number' ? value : parseFloat(value); if (num !== num) return; // NaN check + this._timeSeriesFormat[key] = format; const now = Date.now() / 1000; if (!(key in this._timeSeries)) this._timeSeries[key] = []; const buf = this._timeSeries[key]; @@ -81,6 +83,11 @@ export const Values = GObject.registerClass({ return this._graphableFormats.includes(format); } + formatValue(key, rawValue) { + const format = key in this._timeSeriesFormat ? this._timeSeriesFormat[key] : 'percent'; + return this._legible(rawValue, format); + } + _legible(value, sensorClass) { let unit = 1000; if (value === null) return 'N/A'; @@ -329,11 +336,12 @@ export const Values = GObject.registerClass({ // calculate total upload and download device speed for (let direction in this._networkSpeeds) { - let sum = 0; + let sumNum = 0; for (let iface in this._networkSpeeds[direction]) - sum += parseFloat(this._networkSpeeds[direction][iface]); + sumNum += parseFloat(this._networkSpeeds[direction][iface]); - sum = this._legible(sum, 'speed'); + this._pushTimePoint('__network-' + direction + '_max__', sumNum, 'speed'); + let sum = this._legible(sumNum, 'speed'); output.push(['Device ' + direction, sum, 'network-' + direction, '__network-' + direction + '_max__']); // append download speed to group itself if (direction == 'rx') output.push([type, sum, type + '-group', '']); @@ -385,5 +393,6 @@ export const Values = GObject.registerClass({ this._history['gpu#' + i + '-group'] = {}; } this._timeSeries = {}; + this._timeSeriesFormat = {}; } }); From bc30c98dfe64a7af3b283e5e7f4d5b5d9a388be8 Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Sun, 22 Feb 2026 18:03:21 -0500 Subject: [PATCH 09/27] shows label of item hovered --- extension.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/extension.js b/extension.js index 3f0f3b69..cad8a9fd 100644 --- a/extension.js +++ b/extension.js @@ -201,12 +201,12 @@ var VitalsMenuButton = GObject.registerClass({ }); this._historyPopout.clip_to_allocation = true; this._historyGraph = new HistoryGraph.HistoryGraph(); - this._historyTimeLabel = new St.Label({ - text: _('1 Hour'), + this._historyTitleLabel = new St.Label({ + text: '', style_class: 'vitals-history-popout-label', x_align: Clutter.ActorAlign.END }); - this._historyPopout.add_child(this._historyTimeLabel); + this._historyPopout.add_child(this._historyTitleLabel); this._historyGraphRow = new St.BoxLayout({ vertical: false, x_expand: true, @@ -220,12 +220,12 @@ var VitalsMenuButton = GObject.registerClass({ this._historyYMax = new St.Label({ text: '', style_class: 'vitals-history-popout-axis', - x_align: Clutter.ActorAlign.START + x_align: Clutter.ActorAlign.END }); this._historyYMin = new St.Label({ text: '', style_class: 'vitals-history-popout-axis', - x_align: Clutter.ActorAlign.START + x_align: Clutter.ActorAlign.END }); this._historyYSpacer = new St.BoxLayout({ vertical: true, y_expand: true }); this._historyYAxis.add_child(this._historyYMax); @@ -282,6 +282,8 @@ var VitalsMenuButton = GObject.registerClass({ if (samples.length === 0) return; this._historyPopoutSensorKey = key; try { + this._historyTitleLabel.text = label + ' ' + _('history'); + this._historyTitleLabel.show(); this._historyGraph.setData(samples, label, ''); const rawRange = this._historyGraph.getRawRange(); if (rawRange) { From 5a60c8a2938c325942ef393b971d24896f688dc4 Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Sun, 22 Feb 2026 18:06:29 -0500 Subject: [PATCH 10/27] boot and session rx/tx --- stylesheet.css | 1 + values.js | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/stylesheet.css b/stylesheet.css index 66e3b262..0fdc7faa 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -69,6 +69,7 @@ border-radius: 4px; padding: 4px; margin: 2px 0; + margin-right: 8px; } .vitals-history-graph-bars { diff --git a/values.js b/values.js index d52ef8f8..d5aadd38 100644 --- a/values.js +++ b/values.js @@ -312,6 +312,8 @@ export const Values = GObject.registerClass({ // appends total upload and download for all interfaces for #216 let vals = Object.values(this._history[type]).map(x => parseFloat(x[1])); let sum = vals.reduce((partialSum, a) => partialSum + a, 0); + const memUnit = this._settings.get_int('memory-measurement') ? 1000 : 1024; + this._pushTimePoint('__' + type + '_boot__', sum / memUnit, 'memory'); output.push(['Boot ' + direction, this._legible(sum, format), type, '__' + type + '_boot__']); // keeps track of session start point @@ -319,6 +321,8 @@ export const Values = GObject.registerClass({ this._networkSpeedOffset[key] = sum; // outputs session upload and download for all interfaces for #234 + const sessionVal = sum - this._networkSpeedOffset[key]; + this._pushTimePoint('__' + type + '_ses__', sessionVal / memUnit, 'memory'); output.push(['Session ' + direction, this._legible(sum - this._networkSpeedOffset[key], format), type, '__' + type + '_ses__']); // calculate speed for this interface From 470212c604575b63386e841d21a5935534fba8ef Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Sun, 22 Feb 2026 19:19:28 -0500 Subject: [PATCH 11/27] padding changes --- extension.js | 6 ++++++ historyGraph.js | 6 ++++-- stylesheet.css | 7 +++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/extension.js b/extension.js index cad8a9fd..9e17e1d9 100644 --- a/extension.js +++ b/extension.js @@ -233,6 +233,12 @@ var VitalsMenuButton = GObject.registerClass({ this._historyYAxis.add_child(this._historyYMin); this._historyGraphRow.add_child(this._historyYAxis); this._historyGraphRow.add_child(this._historyGraph); + this._historyGraphRightSpacer = new St.BoxLayout({ + vertical: true, + width: 18, + style_class: 'vitals-history-graph-right-spacer' + }); + this._historyGraphRow.add_child(this._historyGraphRightSpacer); this._historyPopout.add_child(this._historyGraphRow); this._historyXWrap = new St.BoxLayout({ vertical: false, diff --git a/historyGraph.js b/historyGraph.js index 6247d1ad..6ce064f6 100644 --- a/historyGraph.js +++ b/historyGraph.js @@ -28,7 +28,7 @@ import Clutter from 'gi://Clutter'; import GObject from 'gi://GObject'; import St from 'gi://St'; -const GRAPH_WIDTH = 220; +const GRAPH_WIDTH = 204; const GRAPH_HEIGHT = 90; const PADDING = 4; const BAR_GAP = 1; @@ -49,6 +49,7 @@ export const HistoryGraph = GObject.registerClass({ this._unit = ''; this._vMin = 0; this._vMax = 0; + this.clip_to_allocation = true; this._barContainer = new St.BoxLayout({ vertical: false, style_class: 'vitals-history-graph-bars', @@ -56,6 +57,7 @@ export const HistoryGraph = GObject.registerClass({ y_expand: true, y_align: Clutter.ActorAlign.END }); + this._barContainer.clip_to_allocation = true; this.add_child(this._barContainer); } @@ -97,7 +99,7 @@ export const HistoryGraph = GObject.registerClass({ const tMax = data[data.length - 1].t; const tRange = Math.max(0.001, tMax - tMin); - const barWidth = Math.max(1, (graphW - (data.length - 1) * BAR_GAP) / data.length - BAR_GAP); + const barWidth = Math.max(1, Math.floor(graphW / data.length)); for (let i = 0; i < data.length; i++) { const v = data[i].v; diff --git a/stylesheet.css b/stylesheet.css index 0fdc7faa..9906b1c4 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -19,7 +19,7 @@ .vitals-button-box { padding: 0px; spacing: 22px; } .vitals-history-popout { - padding: 6px; + padding: 6px 14px 6px 6px; border-radius: 8px; border: 1px solid rgba(128, 128, 128, 0.4); background-color: rgba(30, 30, 30, 0.95); @@ -68,12 +68,11 @@ background-color: rgba(0, 0, 0, 0.25); border-radius: 4px; padding: 4px; - margin: 2px 0; - margin-right: 8px; + margin: 2px 0 2px 0; } .vitals-history-graph-bars { - spacing: 1px; + spacing: 0px; } .vitals-history-graph-bar { From 07cd954b4144a711a2d1dee8338690d3196fd7b9 Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Sun, 22 Feb 2026 21:01:19 -0500 Subject: [PATCH 12/27] fill in empty space --- extension.js | 14 +++++++++-- historyGraph.js | 67 ++++++++++++++++++++++++++++--------------------- values.js | 23 +++++++++++++++-- 3 files changed, 72 insertions(+), 32 deletions(-) diff --git a/extension.js b/extension.js index 9e17e1d9..03515d27 100644 --- a/extension.js +++ b/extension.js @@ -81,7 +81,7 @@ var VitalsMenuButton = GObject.registerClass({ this._settingChangedSignals = []; this._refreshTimeoutId = null; - this._addSettingChangedSignal('update-time', this._updateTimeChanged.bind(this)); + this._addSettingChangedSignal('update-time', this._updateTimeSettingChanged.bind(this)); this._addSettingChangedSignal('position-in-panel', this._positionInPanelChanged.bind(this)); this._addSettingChangedSignal('menu-centered', this._positionInPanelChanged.bind(this)); this._addSettingChangedSignal('icon-style', this._iconStyleChanged.bind(this)); @@ -256,7 +256,7 @@ var VitalsMenuButton = GObject.registerClass({ style_class: 'vitals-history-x-row' }); this._historyXLeft = new St.Label({ - text: _('1h ago'), + text: '', style_class: 'vitals-history-popout-axis', x_align: Clutter.ActorAlign.START, x_expand: true @@ -291,6 +291,10 @@ var VitalsMenuButton = GObject.registerClass({ this._historyTitleLabel.text = label + ' ' + _('history'); this._historyTitleLabel.show(); this._historyGraph.setData(samples, label, ''); + const tSpan = this._historyGraph.getTimeSpan(); + const maxDuration = Math.max(60, this._settings.get_int('sensor-history-duration')); + const clampedSpan = Math.min(tSpan, maxDuration); + this._historyXLeft.text = this._values.formatDuration(clampedSpan) + ' ' + _('ago'); const rawRange = this._historyGraph.getRawRange(); if (rawRange) { this._historyYMax.text = this._values.formatValue(key, rawRange.max); @@ -558,6 +562,12 @@ var VitalsMenuButton = GObject.registerClass({ } } + _updateTimeSettingChanged() { + this._destroyTimer(); + this._values.clearTimeSeries(); + this._initializeTimer(); + } + _updateTimeChanged() { this._destroyTimer(); this._initializeTimer(); diff --git a/historyGraph.js b/historyGraph.js index 6ce064f6..d508c312 100644 --- a/historyGraph.js +++ b/historyGraph.js @@ -31,7 +31,7 @@ import St from 'gi://St'; const GRAPH_WIDTH = 204; const GRAPH_HEIGHT = 90; const PADDING = 4; -const BAR_GAP = 1; +const MIN_BAR_WIDTH = 2; export const HistoryGraph = GObject.registerClass({ GTypeName: 'HistoryGraph', @@ -49,13 +49,11 @@ export const HistoryGraph = GObject.registerClass({ this._unit = ''; this._vMin = 0; this._vMax = 0; + this._tSpan = 0; this.clip_to_allocation = true; - this._barContainer = new St.BoxLayout({ - vertical: false, - style_class: 'vitals-history-graph-bars', + this._barContainer = new St.Widget({ x_expand: true, - y_expand: true, - y_align: Clutter.ActorAlign.END + y_expand: true }); this._barContainer.clip_to_allocation = true; this.add_child(this._barContainer); @@ -95,43 +93,56 @@ export const HistoryGraph = GObject.registerClass({ this._vMin = vMin; this._vMax = vMax; const vRange = vMax - vMin; - const tMin = data[0].t; - const tMax = data[data.length - 1].t; - const tRange = Math.max(0.001, tMax - tMin); - const barWidth = Math.max(1, Math.floor(graphW / data.length)); + const now = Date.now() / 1000; + const tOldest = data[0].t; + this._tSpan = now - tOldest; + const tRange = Math.max(0.001, this._tSpan); + + const gaps = []; + for (let i = 1; i < data.length; i++) + gaps.push(data[i].t - data[i - 1].t); + gaps.sort((a, b) => a - b); + const medianGap = gaps.length > 0 + ? gaps[Math.floor(gaps.length / 2)] + : tRange; + const maxBarTime = medianGap * 2.5; + + const xPixels = data.map(d => + Math.round((d.t - tOldest) / tRange * graphW)); for (let i = 0; i < data.length; i++) { const v = data[i].v; const norm = (v - vMin) / vRange; const barH = Math.max(1, Math.round(norm * graphH)); + const x = xPixels[i]; + const nextT = (i < data.length - 1) ? data[i + 1].t : now; + const gap = nextT - data[i].t; + let rightEdge; + if (gap <= maxBarTime) { + rightEdge = (i < data.length - 1) ? xPixels[i + 1] : graphW; + } else { + rightEdge = Math.min(graphW, + Math.round((data[i].t - tOldest + maxBarTime) / tRange * graphW)); + } + const barW = Math.max(MIN_BAR_WIDTH, rightEdge - x); + const y = graphH - barH; const bar = new St.Bin({ - width: Math.round(barWidth), + width: Math.min(barW, graphW - x), height: barH, - style_class: 'vitals-history-graph-bar', - y_align: Clutter.ActorAlign.END, - x_align: Clutter.ActorAlign.CENTER + style_class: 'vitals-history-graph-bar' }); + bar.set_position(x, y); this._barContainer.add_child(bar); } } - getRangeLabel() { - if (this._samples.length === 0) return ''; - const a = this._vMin; - const b = this._vMax; - const fmt = (v) => Number.isInteger(v) ? String(v) : v.toFixed(1); - return fmt(a) + ' – ' + fmt(b); - } - - getRange() { - if (this._samples.length === 0) return null; - const fmt = (v) => Number.isInteger(v) ? String(v) : v.toFixed(1); - return { min: fmt(this._vMin), max: fmt(this._vMax) }; - } - getRawRange() { if (this._samples.length === 0) return null; return { min: this._vMin, max: this._vMax }; } + + getTimeSpan() { + return this._tSpan; + } }); diff --git a/values.js b/values.js index d5aadd38..a1e314e1 100644 --- a/values.js +++ b/values.js @@ -67,6 +67,11 @@ export const Values = GObject.registerClass({ const now = Date.now() / 1000; if (!(key in this._timeSeries)) this._timeSeries[key] = []; const buf = this._timeSeries[key]; + const minInterval = Math.max(1, this._settings.get_int('update-time')) - 0.5; + if (buf.length > 0 && (now - buf[buf.length - 1].t) < minInterval) { + buf[buf.length - 1].v = num; + return; + } buf.push({ t: now, v: num }); const maxAge = this._getHistoryDurationSeconds(); while (buf.length > 0 && buf[0].t < now - maxAge) buf.shift(); @@ -74,6 +79,11 @@ export const Values = GObject.registerClass({ while (buf.length > maxPoints) buf.shift(); } + clearTimeSeries() { + this._timeSeries = {}; + this._timeSeriesFormat = {}; + } + getTimeSeries(key) { if (!(key in this._timeSeries)) return []; return this._timeSeries[key].slice(); @@ -88,6 +98,17 @@ export const Values = GObject.registerClass({ return this._legible(rawValue, format); } + formatDuration(seconds) { + seconds = Math.round(Math.abs(seconds)); + if (seconds < 60) return seconds + 's'; + const m = Math.floor(seconds / 60); + if (m < 60) return m + 'm'; + const h = Math.floor(m / 60); + const rm = m % 60; + if (rm === 0) return h + 'h'; + return h + 'h ' + rm + 'm'; + } + _legible(value, sensorClass) { let unit = 1000; if (value === null) return 'N/A'; @@ -396,7 +417,5 @@ export const Values = GObject.registerClass({ this._history['gpu#' + i] = {}; this._history['gpu#' + i + '-group'] = {}; } - this._timeSeries = {}; - this._timeSeriesFormat = {}; } }); From 8e3bd02af4224a4a2fd1ed7039be471b99b02a67 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 23 Feb 2026 21:48:09 +0700 Subject: [PATCH 13/27] add sign before battery rate status --- sensors.js | 7 +++++-- values.js | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/sensors.js b/sensors.js index f3101617..6f8f353b 100644 --- a/sensors.js +++ b/sensors.js @@ -427,8 +427,11 @@ export const Sensors = GObject.registerClass({ } if ('POWER_NOW' in output) { - this._returnValue(callback, 'Rate', output['POWER_NOW'], 'battery', 'watt'); - this._returnValue(callback, 'battery', output['POWER_NOW'], 'battery-group', 'watt'); + const powerValue = ( + parseFloat(output['POWER_NOW']) * (output['STATUS'] === 'Discharging' ? -1 : 1) + ); + this._returnValue(callback, 'Power Rate', powerValue, 'battery', 'watt'); + this._returnValue(callback, 'battery', powerValue, 'battery-group', 'watt'); } if ('CHARGE_FULL' in output && 'VOLTAGE_MIN_DESIGN' in output && (!('ENERGY_FULL' in output))) { diff --git a/values.js b/values.js index a7cabb03..da83d3f1 100644 --- a/values.js +++ b/values.js @@ -186,7 +186,9 @@ export const Values = GObject.registerClass({ ending = 'mAh'; break; case 'watt': - format = (use_higher_precision)?'%.2f %s':'%.1f %s'; + format = ( + ((value > 0) ? '+' : '') + ((use_higher_precision)?'%.2f %s':'%.1f %s') + ); value = value / 1000000; ending = 'W'; break; From 0d2746e0d471c20ea0be89c431131129233201a2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 23 Feb 2026 22:15:02 +0700 Subject: [PATCH 14/27] add sign before battery rate status --- sensors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sensors.js b/sensors.js index 6f8f353b..cfb22f3d 100644 --- a/sensors.js +++ b/sensors.js @@ -429,7 +429,7 @@ export const Sensors = GObject.registerClass({ if ('POWER_NOW' in output) { const powerValue = ( parseFloat(output['POWER_NOW']) * (output['STATUS'] === 'Discharging' ? -1 : 1) - ); + ); this._returnValue(callback, 'Power Rate', powerValue, 'battery', 'watt'); this._returnValue(callback, 'battery', powerValue, 'battery-group', 'watt'); } From 2a2dcb9e101e4f450ddee7ba534d2b1db78f3db4 Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Mon, 23 Feb 2026 19:21:34 -0500 Subject: [PATCH 15/27] save history --- extension.js | 5 +++- values.js | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/extension.js b/extension.js index 03515d27..019ffbcb 100644 --- a/extension.js +++ b/extension.js @@ -66,6 +66,8 @@ var VitalsMenuButton = GObject.registerClass({ this._sensors = new Sensors.Sensors(this._settings, this._sensorIcons); this._values = new Values.Values(this._settings, this._sensorIcons); + this._historyCachePath = GLib.get_user_cache_dir() + '/vitals/history.json'; + this._values.loadTimeSeries(this._historyCachePath); this._menuLayout = new St.BoxLayout({ vertical: false, clip_to_allocation: true, @@ -564,7 +566,7 @@ var VitalsMenuButton = GObject.registerClass({ _updateTimeSettingChanged() { this._destroyTimer(); - this._values.clearTimeSeries(); + this._values.clearTimeSeries(this._historyCachePath); this._initializeTimer(); } @@ -835,6 +837,7 @@ var VitalsMenuButton = GObject.registerClass({ destroy() { this._hideHistoryPopout(); this._destroyTimer(); + this._values.saveTimeSeries(this._historyCachePath); this._sensors.destroy(); for (let signal of Object.values(this._settingChangedSignals)) diff --git a/values.js b/values.js index a1e314e1..dc54638e 100644 --- a/values.js +++ b/values.js @@ -24,6 +24,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import GLib from 'gi://GLib'; +import Gio from 'gi://Gio'; import GObject from 'gi://GObject'; const cbFun = (d, c) => { @@ -79,9 +81,69 @@ export const Values = GObject.registerClass({ while (buf.length > maxPoints) buf.shift(); } - clearTimeSeries() { + clearTimeSeries(cachePath) { this._timeSeries = {}; this._timeSeriesFormat = {}; + if (cachePath) { + try { + const file = Gio.File.new_for_path(cachePath); + if (file.query_exists(null)) + file.delete(null); + } catch (e) { + // ignore + } + } + } + + saveTimeSeries(path) { + try { + const obj = { + version: 1, + timeSeries: this._timeSeries, + timeSeriesFormat: this._timeSeriesFormat + }; + const json = JSON.stringify(obj); + const dir = GLib.path_get_dirname(path); + GLib.mkdir_with_parents(dir, 0o755); + GLib.file_set_contents(path, json); + } catch (e) { + // ignore write failures + } + } + + loadTimeSeries(path) { + try { + const file = Gio.File.new_for_path(path); + if (!file.query_exists(null)) return; + const [ok, contents] = GLib.file_get_contents(path); + if (!ok) return; + const decoder = new TextDecoder('utf-8'); + const json = decoder.decode(contents); + const obj = JSON.parse(json); + if (!obj || obj.version !== 1) return; + if (obj.timeSeries && typeof obj.timeSeries === 'object') + this._timeSeries = obj.timeSeries; + if (obj.timeSeriesFormat && typeof obj.timeSeriesFormat === 'object') + this._timeSeriesFormat = obj.timeSeriesFormat; + const now = Date.now() / 1000; + const maxAge = this._getHistoryDurationSeconds(); + const cutoff = now - maxAge; + for (const key in this._timeSeries) { + const buf = this._timeSeries[key]; + if (!Array.isArray(buf)) { + delete this._timeSeries[key]; + continue; + } + while (buf.length > 0 && buf[0].t < cutoff) + buf.shift(); + if (buf.length === 0) { + delete this._timeSeries[key]; + delete this._timeSeriesFormat[key]; + } + } + } catch (e) { + // ignore corrupt or missing file + } } getTimeSeries(key) { From 317eb27a7c9da212a087f144f1bf107edafbe51c Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Mon, 23 Feb 2026 19:38:33 -0500 Subject: [PATCH 16/27] bars are same width --- historyGraph.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/historyGraph.js b/historyGraph.js index d508c312..a26f9c78 100644 --- a/historyGraph.js +++ b/historyGraph.js @@ -96,8 +96,6 @@ export const HistoryGraph = GObject.registerClass({ const now = Date.now() / 1000; const tOldest = data[0].t; - this._tSpan = now - tOldest; - const tRange = Math.max(0.001, this._tSpan); const gaps = []; for (let i = 1; i < data.length; i++) @@ -105,8 +103,14 @@ export const HistoryGraph = GObject.registerClass({ gaps.sort((a, b) => a - b); const medianGap = gaps.length > 0 ? gaps[Math.floor(gaps.length / 2)] - : tRange; - const maxBarTime = medianGap * 2.5; + : 0; + const maxBarTime = medianGap > 0 ? medianGap * 2.5 : Infinity; + + const tEnd = medianGap > 0 + ? data[data.length - 1].t + medianGap + : now; + this._tSpan = tEnd - tOldest; + const tRange = Math.max(0.001, this._tSpan); const xPixels = data.map(d => Math.round((d.t - tOldest) / tRange * graphW)); @@ -116,7 +120,7 @@ export const HistoryGraph = GObject.registerClass({ const norm = (v - vMin) / vRange; const barH = Math.max(1, Math.round(norm * graphH)); const x = xPixels[i]; - const nextT = (i < data.length - 1) ? data[i + 1].t : now; + const nextT = (i < data.length - 1) ? data[i + 1].t : tEnd; const gap = nextT - data[i].t; let rightEdge; if (gap <= maxBarTime) { From 73e055ba52c579fa97dee5d15e84029bcebc6b4b Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Mon, 23 Feb 2026 20:11:28 -0500 Subject: [PATCH 17/27] less spaces and overlap --- historyGraph.js | 67 +++++++++++++++++++++++++++++++++++++------------ stylesheet.css | 3 +-- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/historyGraph.js b/historyGraph.js index a26f9c78..e7fff67b 100644 --- a/historyGraph.js +++ b/historyGraph.js @@ -80,7 +80,7 @@ export const HistoryGraph = GObject.registerClass({ if (data.length === 0) return; const graphW = GRAPH_WIDTH - 2 * PADDING; - const graphH = GRAPH_HEIGHT - 2 * PADDING; + const graphH = GRAPH_HEIGHT - PADDING; if (graphW <= 0 || graphH <= 0) return; const vals = data.map(d => d.v); @@ -97,37 +97,72 @@ export const HistoryGraph = GObject.registerClass({ const now = Date.now() / 1000; const tOldest = data[0].t; - const gaps = []; + const rawGaps = []; for (let i = 1; i < data.length; i++) - gaps.push(data[i].t - data[i - 1].t); - gaps.sort((a, b) => a - b); - const medianGap = gaps.length > 0 - ? gaps[Math.floor(gaps.length / 2)] + rawGaps.push(data[i].t - data[i - 1].t); + rawGaps.sort((a, b) => a - b); + const rawMedianGap = rawGaps.length > 0 + ? rawGaps[Math.floor(rawGaps.length / 2)] : 0; - const maxBarTime = medianGap > 0 ? medianGap * 2.5 : Infinity; - const tEnd = medianGap > 0 - ? data[data.length - 1].t + medianGap + const tEnd = rawMedianGap > 0 + ? data[data.length - 1].t + rawMedianGap : now; this._tSpan = tEnd - tOldest; const tRange = Math.max(0.001, this._tSpan); - const xPixels = data.map(d => + const maxBars = Math.floor(graphW / MIN_BAR_WIDTH); + let displayData; + if (data.length <= maxBars) { + displayData = data; + } else { + const bucketDuration = tRange / maxBars; + displayData = []; + let di = 0; + for (let b = 0; b < maxBars; b++) { + const bEnd = tOldest + (b + 1) * bucketDuration; + let sum = 0, count = 0; + while (di < data.length && data[di].t < bEnd) { + sum += data[di].v; + count++; + di++; + } + if (count > 0) { + displayData.push({ + t: tOldest + b * bucketDuration, + v: sum / count + }); + } + } + } + + if (displayData.length === 0) return; + + const gaps = []; + for (let i = 1; i < displayData.length; i++) + gaps.push(displayData[i].t - displayData[i - 1].t); + gaps.sort((a, b) => a - b); + const medianGap = gaps.length > 0 + ? gaps[Math.floor(gaps.length / 2)] + : 0; + const maxBarTime = medianGap > 0 ? medianGap * 2.5 : Infinity; + + const xPixels = displayData.map(d => Math.round((d.t - tOldest) / tRange * graphW)); - for (let i = 0; i < data.length; i++) { - const v = data[i].v; + for (let i = 0; i < displayData.length; i++) { + const v = displayData[i].v; const norm = (v - vMin) / vRange; const barH = Math.max(1, Math.round(norm * graphH)); const x = xPixels[i]; - const nextT = (i < data.length - 1) ? data[i + 1].t : tEnd; - const gap = nextT - data[i].t; + const nextT = (i < displayData.length - 1) ? displayData[i + 1].t : tEnd; + const gap = nextT - displayData[i].t; let rightEdge; if (gap <= maxBarTime) { - rightEdge = (i < data.length - 1) ? xPixels[i + 1] : graphW; + rightEdge = (i < displayData.length - 1) ? xPixels[i + 1] : graphW; } else { rightEdge = Math.min(graphW, - Math.round((data[i].t - tOldest + maxBarTime) / tRange * graphW)); + Math.round((displayData[i].t - tOldest + maxBarTime) / tRange * graphW)); } const barW = Math.max(MIN_BAR_WIDTH, rightEdge - x); const y = graphH - barH; diff --git a/stylesheet.css b/stylesheet.css index 9906b1c4..e4e3d1b3 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -67,7 +67,7 @@ .vitals-history-graph { background-color: rgba(0, 0, 0, 0.25); border-radius: 4px; - padding: 4px; + padding: 4px 4px 0 4px; margin: 2px 0 2px 0; } @@ -76,6 +76,5 @@ } .vitals-history-graph-bar { - border-radius: 1px; background-color: rgba(51, 128, 230, 0.87); } From 264e486dc1e4471aab08db308a7ec35c32a3852b Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Mon, 23 Feb 2026 20:32:20 -0500 Subject: [PATCH 18/27] use max instead of avg, rename file --- extension.js | 37 ++++++++++++++++++++++++++++++++++- historyGraph.js => history.js | 6 +++--- 2 files changed, 39 insertions(+), 4 deletions(-) rename historyGraph.js => history.js (97%) diff --git a/extension.js b/extension.js index 019ffbcb..d284acde 100644 --- a/extension.js +++ b/extension.js @@ -17,7 +17,7 @@ import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js'; import * as Values from './values.js'; import * as Config from 'resource:///org/gnome/shell/misc/config.js'; import * as MenuItem from './menuItem.js'; -import * as HistoryGraph from './historyGraph.js'; +import * as HistoryGraph from './history.js'; let vitalsMenu; @@ -63,6 +63,7 @@ var VitalsMenuButton = GObject.registerClass({ this._historyPopoutLeaveId = 0; this._historyHideTimeoutId = null; this._historyPopoutSensorKey = null; + this._historyPopoutLabel = null; this._sensors = new Sensors.Sensors(this._settings, this._sensorIcons); this._values = new Values.Values(this._settings, this._sensorIcons); @@ -289,6 +290,7 @@ var VitalsMenuButton = GObject.registerClass({ const samples = this._values.getTimeSeries(key); if (samples.length === 0) return; this._historyPopoutSensorKey = key; + this._historyPopoutLabel = label; try { this._historyTitleLabel.text = label + ' ' + _('history'); this._historyTitleLabel.show(); @@ -360,6 +362,37 @@ var VitalsMenuButton = GObject.registerClass({ this._historyPopout.get_parent().remove_child(this._historyPopout); } this._historyPopoutSensorKey = null; + this._historyPopoutLabel = null; + } + + _refreshHistoryPopout() { + const key = this._historyPopoutSensorKey; + const label = this._historyPopoutLabel; + if (!key || !label) return; + if (!this._historyPopout || !this._historyPopout.visible) return; + + const samples = this._values.getTimeSeries(key); + if (samples.length === 0) return; + + try { + this._historyGraph.setData(samples, label, ''); + const tSpan = this._historyGraph.getTimeSpan(); + const maxDuration = Math.max(60, this._settings.get_int('sensor-history-duration')); + const clampedSpan = Math.min(tSpan, maxDuration); + this._historyXLeft.text = this._values.formatDuration(clampedSpan) + ' ' + _('ago'); + const rawRange = this._historyGraph.getRawRange(); + if (rawRange) { + this._historyYMax.text = this._values.formatValue(key, rawRange.max); + this._historyYMin.text = this._values.formatValue(key, rawRange.min); + this._historyYAxis.show(); + } else { + this._historyYMax.text = ''; + this._historyYMin.text = ''; + this._historyYAxis.hide(); + } + } catch (e) { + // ignore + } } _initializeMenuGroup(groupName, optionName, menuSuffix = '', position = -1) { @@ -824,6 +857,8 @@ var VitalsMenuButton = GObject.registerClass({ this._notify('Vitals', this._warnings.join("\n"), 'folder-symbolic'); this._warnings = []; } + + this._refreshHistoryPopout(); } _notify(msg, details, icon) { diff --git a/historyGraph.js b/history.js similarity index 97% rename from historyGraph.js rename to history.js index e7fff67b..7965829e 100644 --- a/historyGraph.js +++ b/history.js @@ -121,16 +121,16 @@ export const HistoryGraph = GObject.registerClass({ let di = 0; for (let b = 0; b < maxBars; b++) { const bEnd = tOldest + (b + 1) * bucketDuration; - let sum = 0, count = 0; + let peak = -Infinity, count = 0; while (di < data.length && data[di].t < bEnd) { - sum += data[di].v; + if (data[di].v > peak) peak = data[di].v; count++; di++; } if (count > 0) { displayData.push({ t: tOldest + b * bucketDuration, - v: sum / count + v: peak }); } } From 1e46faa6aaa60753bf04616ec29d34e1823db6cc Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Tue, 24 Feb 2026 06:49:22 -0500 Subject: [PATCH 19/27] smooths out history rebuild --- extension.js | 16 +++++--- history.js | 114 ++++++++++++++------------------------------------- 2 files changed, 40 insertions(+), 90 deletions(-) diff --git a/extension.js b/extension.js index d284acde..2fd234ad 100644 --- a/extension.js +++ b/extension.js @@ -294,10 +294,12 @@ var VitalsMenuButton = GObject.registerClass({ try { this._historyTitleLabel.text = label + ' ' + _('history'); this._historyTitleLabel.show(); - this._historyGraph.setData(samples, label, ''); + const historyDuration = Math.max(60, this._settings.get_int('sensor-history-duration')); + const interval = Math.max(1, this._settings.get_int('update-time')); + const base = Math.max(1, Math.ceil(historyDuration / interval / 800)); + this._historyGraph.setData(samples, label, '', base); const tSpan = this._historyGraph.getTimeSpan(); - const maxDuration = Math.max(60, this._settings.get_int('sensor-history-duration')); - const clampedSpan = Math.min(tSpan, maxDuration); + const clampedSpan = Math.min(tSpan, historyDuration); this._historyXLeft.text = this._values.formatDuration(clampedSpan) + ' ' + _('ago'); const rawRange = this._historyGraph.getRawRange(); if (rawRange) { @@ -375,10 +377,12 @@ var VitalsMenuButton = GObject.registerClass({ if (samples.length === 0) return; try { - this._historyGraph.setData(samples, label, ''); + const historyDuration = Math.max(60, this._settings.get_int('sensor-history-duration')); + const interval = Math.max(1, this._settings.get_int('update-time')); + const base = Math.max(1, Math.ceil(historyDuration / interval / 800)); + this._historyGraph.setData(samples, label, '', base); const tSpan = this._historyGraph.getTimeSpan(); - const maxDuration = Math.max(60, this._settings.get_int('sensor-history-duration')); - const clampedSpan = Math.min(tSpan, maxDuration); + const clampedSpan = Math.min(tSpan, historyDuration); this._historyXLeft.text = this._values.formatDuration(clampedSpan) + ' ' + _('ago'); const rawRange = this._historyGraph.getRawRange(); if (rawRange) { diff --git a/history.js b/history.js index 7965829e..b242bc8d 100644 --- a/history.js +++ b/history.js @@ -24,14 +24,13 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import Clutter from 'gi://Clutter'; import GObject from 'gi://GObject'; import St from 'gi://St'; -const GRAPH_WIDTH = 204; +const GRAPH_WIDTH = 208; const GRAPH_HEIGHT = 90; const PADDING = 4; -const MIN_BAR_WIDTH = 2; +const MIN_BAR_WIDTH = 1; export const HistoryGraph = GObject.registerClass({ GTypeName: 'HistoryGraph', @@ -49,7 +48,7 @@ export const HistoryGraph = GObject.registerClass({ this._unit = ''; this._vMin = 0; this._vMax = 0; - this._tSpan = 0; + this._base = 1; this.clip_to_allocation = true; this._barContainer = new St.Widget({ x_expand: true, @@ -59,10 +58,11 @@ export const HistoryGraph = GObject.registerClass({ this.add_child(this._barContainer); } - setData(samples, label, unit) { + setData(samples, label, unit, base) { this._samples = Array.isArray(samples) ? samples : []; this._label = label || ''; this._unit = unit || ''; + this._base = Math.max(1, base); this._rebuildBars(); } @@ -83,95 +83,40 @@ export const HistoryGraph = GObject.registerClass({ const graphH = GRAPH_HEIGHT - PADDING; if (graphW <= 0 || graphH <= 0) return; - const vals = data.map(d => d.v); - let vMin = Math.min(...vals); - let vMax = Math.max(...vals); - if (vMax <= vMin) { - vMin = vMin - 1; - vMax = vMax + 1; + const base = this._base; + const maxBars = Math.floor(graphW / MIN_BAR_WIDTH); + const totalBars = Math.ceil(data.length / base); + const numBars = Math.min(totalBars, maxBars); + const dataOffset = (totalBars - numBars) * base; + const barWidth = graphW / numBars; + + let vMin = Infinity, vMax = -Infinity; + for (let i = dataOffset; i < data.length; i++) { + if (data[i].v < vMin) vMin = data[i].v; + if (data[i].v > vMax) vMax = data[i].v; } + if (vMax <= vMin) { vMin -= 1; vMax += 1; } this._vMin = vMin; this._vMax = vMax; const vRange = vMax - vMin; - const now = Date.now() / 1000; - const tOldest = data[0].t; - - const rawGaps = []; - for (let i = 1; i < data.length; i++) - rawGaps.push(data[i].t - data[i - 1].t); - rawGaps.sort((a, b) => a - b); - const rawMedianGap = rawGaps.length > 0 - ? rawGaps[Math.floor(rawGaps.length / 2)] - : 0; - - const tEnd = rawMedianGap > 0 - ? data[data.length - 1].t + rawMedianGap - : now; - this._tSpan = tEnd - tOldest; - const tRange = Math.max(0.001, this._tSpan); - - const maxBars = Math.floor(graphW / MIN_BAR_WIDTH); - let displayData; - if (data.length <= maxBars) { - displayData = data; - } else { - const bucketDuration = tRange / maxBars; - displayData = []; - let di = 0; - for (let b = 0; b < maxBars; b++) { - const bEnd = tOldest + (b + 1) * bucketDuration; - let peak = -Infinity, count = 0; - while (di < data.length && data[di].t < bEnd) { - if (data[di].v > peak) peak = data[di].v; - count++; - di++; - } - if (count > 0) { - displayData.push({ - t: tOldest + b * bucketDuration, - v: peak - }); - } + for (let b = 0; b < numBars; b++) { + const iStart = dataOffset + b * base; + const iEnd = Math.min(iStart + base, data.length); + let peak = -Infinity; + for (let i = iStart; i < iEnd; i++) { + if (data[i].v > peak) peak = data[i].v; } - } - - if (displayData.length === 0) return; - - const gaps = []; - for (let i = 1; i < displayData.length; i++) - gaps.push(displayData[i].t - displayData[i - 1].t); - gaps.sort((a, b) => a - b); - const medianGap = gaps.length > 0 - ? gaps[Math.floor(gaps.length / 2)] - : 0; - const maxBarTime = medianGap > 0 ? medianGap * 2.5 : Infinity; - - const xPixels = displayData.map(d => - Math.round((d.t - tOldest) / tRange * graphW)); - - for (let i = 0; i < displayData.length; i++) { - const v = displayData[i].v; - const norm = (v - vMin) / vRange; + const norm = (peak - vMin) / vRange; const barH = Math.max(1, Math.round(norm * graphH)); - const x = xPixels[i]; - const nextT = (i < displayData.length - 1) ? displayData[i + 1].t : tEnd; - const gap = nextT - displayData[i].t; - let rightEdge; - if (gap <= maxBarTime) { - rightEdge = (i < displayData.length - 1) ? xPixels[i + 1] : graphW; - } else { - rightEdge = Math.min(graphW, - Math.round((displayData[i].t - tOldest + maxBarTime) / tRange * graphW)); - } - const barW = Math.max(MIN_BAR_WIDTH, rightEdge - x); - const y = graphH - barH; + const x = Math.round(b * barWidth); + const w = Math.round((b + 1) * barWidth) - x; const bar = new St.Bin({ - width: Math.min(barW, graphW - x), + width: w, height: barH, style_class: 'vitals-history-graph-bar' }); - bar.set_position(x, y); + bar.set_position(x, graphH - barH); this._barContainer.add_child(bar); } } @@ -182,6 +127,7 @@ export const HistoryGraph = GObject.registerClass({ } getTimeSpan() { - return this._tSpan; + if (this._samples.length < 2) return 0; + return this._samples[this._samples.length - 1].t - this._samples[0].t; } }); From 1d43a683f7b744bd3cdcfe3b11b1b148708a2229 Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Tue, 24 Feb 2026 06:54:04 -0500 Subject: [PATCH 20/27] go back to avg --- history.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/history.js b/history.js index b242bc8d..436142c8 100644 --- a/history.js +++ b/history.js @@ -103,11 +103,12 @@ export const HistoryGraph = GObject.registerClass({ for (let b = 0; b < numBars; b++) { const iStart = dataOffset + b * base; const iEnd = Math.min(iStart + base, data.length); - let peak = -Infinity; + let sum = 0; for (let i = iStart; i < iEnd; i++) { - if (data[i].v > peak) peak = data[i].v; + sum += data[i].v; } - const norm = (peak - vMin) / vRange; + const avg = sum / (iEnd - iStart); + const norm = (avg - vMin) / vRange; const barH = Math.max(1, Math.round(norm * graphH)); const x = Math.round(b * barWidth); const w = Math.round((b + 1) * barWidth) - x; From 14bc0cb7dd5e87b46aa78010e2b354911ed493d0 Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Tue, 24 Feb 2026 07:16:48 -0500 Subject: [PATCH 21/27] fixes some bugs --- extension.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extension.js b/extension.js index 2fd234ad..e02333be 100644 --- a/extension.js +++ b/extension.js @@ -347,7 +347,7 @@ var VitalsMenuButton = GObject.registerClass({ _scheduleHistoryPopoutHide() { if (this._historyHideTimeoutId) return; - this._historyHideTimeoutId = GLib.timeout_add(250, GLib.PRIORITY_DEFAULT, () => { + this._historyHideTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, () => { this._hideHistoryPopout(); this._historyHideTimeoutId = null; return GLib.SOURCE_REMOVE; @@ -875,6 +875,10 @@ var VitalsMenuButton = GObject.registerClass({ destroy() { this._hideHistoryPopout(); + if (this._historyPopout) { + this._historyPopout.destroy(); + this._historyPopout = null; + } this._destroyTimer(); this._values.saveTimeSeries(this._historyCachePath); this._sensors.destroy(); From c3a4bcc0557165911ef765b929a2065ac661528d Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Tue, 24 Feb 2026 07:47:31 -0500 Subject: [PATCH 22/27] code cleanup --- history.js | 7 +++++-- values.js | 4 ---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/history.js b/history.js index 436142c8..b0a969e6 100644 --- a/history.js +++ b/history.js @@ -49,6 +49,7 @@ export const HistoryGraph = GObject.registerClass({ this._vMin = 0; this._vMax = 0; this._base = 1; + this._dataOffset = 0; this.clip_to_allocation = true; this._barContainer = new St.Widget({ x_expand: true, @@ -88,6 +89,7 @@ export const HistoryGraph = GObject.registerClass({ const totalBars = Math.ceil(data.length / base); const numBars = Math.min(totalBars, maxBars); const dataOffset = (totalBars - numBars) * base; + this._dataOffset = dataOffset; const barWidth = graphW / numBars; let vMin = Infinity, vMax = -Infinity; @@ -128,7 +130,8 @@ export const HistoryGraph = GObject.registerClass({ } getTimeSpan() { - if (this._samples.length < 2) return 0; - return this._samples[this._samples.length - 1].t - this._samples[0].t; + const start = this._dataOffset; + if (this._samples.length - start < 2) return 0; + return this._samples[this._samples.length - 1].t - this._samples[start].t; } }); diff --git a/values.js b/values.js index dc54638e..3e617117 100644 --- a/values.js +++ b/values.js @@ -151,10 +151,6 @@ export const Values = GObject.registerClass({ return this._timeSeries[key].slice(); } - isGraphableFormat(format) { - return this._graphableFormats.includes(format); - } - formatValue(key, rawValue) { const format = key in this._timeSeriesFormat ? this._timeSeriesFormat[key] : 'percent'; return this._legible(rawValue, format); From 6f2a2878560ab643dd435f43b3fba275b8bb723a Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Tue, 24 Feb 2026 08:17:53 -0500 Subject: [PATCH 23/27] reuse code --- extension.js | 56 +++++++++++++++++++++------------------------------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/extension.js b/extension.js index e02333be..a42a3be8 100644 --- a/extension.js +++ b/extension.js @@ -285,6 +285,26 @@ var VitalsMenuButton = GObject.registerClass({ }); } + _updateHistoryGraph(key, label, samples) { + const historyDuration = Math.max(60, this._settings.get_int('sensor-history-duration')); + const interval = Math.max(1, this._settings.get_int('update-time')); + const base = Math.max(1, Math.ceil(historyDuration / interval / 800)); + this._historyGraph.setData(samples, label, '', base); + const tSpan = this._historyGraph.getTimeSpan(); + const clampedSpan = Math.min(tSpan, historyDuration); + this._historyXLeft.text = this._values.formatDuration(clampedSpan) + ' ' + _('ago'); + const rawRange = this._historyGraph.getRawRange(); + if (rawRange) { + this._historyYMax.text = this._values.formatValue(key, rawRange.max); + this._historyYMin.text = this._values.formatValue(key, rawRange.min); + this._historyYAxis.show(); + } else { + this._historyYMax.text = ''; + this._historyYMin.text = ''; + this._historyYAxis.hide(); + } + } + _showHistoryPopout(key, label, itemActor) { if (!this._settings.get_boolean('show-sensor-history-graph')) return; const samples = this._values.getTimeSeries(key); @@ -294,23 +314,7 @@ var VitalsMenuButton = GObject.registerClass({ try { this._historyTitleLabel.text = label + ' ' + _('history'); this._historyTitleLabel.show(); - const historyDuration = Math.max(60, this._settings.get_int('sensor-history-duration')); - const interval = Math.max(1, this._settings.get_int('update-time')); - const base = Math.max(1, Math.ceil(historyDuration / interval / 800)); - this._historyGraph.setData(samples, label, '', base); - const tSpan = this._historyGraph.getTimeSpan(); - const clampedSpan = Math.min(tSpan, historyDuration); - this._historyXLeft.text = this._values.formatDuration(clampedSpan) + ' ' + _('ago'); - const rawRange = this._historyGraph.getRawRange(); - if (rawRange) { - this._historyYMax.text = this._values.formatValue(key, rawRange.max); - this._historyYMin.text = this._values.formatValue(key, rawRange.min); - this._historyYAxis.show(); - } else { - this._historyYMax.text = ''; - this._historyYMin.text = ''; - this._historyYAxis.hide(); - } + this._updateHistoryGraph(key, label, samples); } catch (e) { this._historyYMax.text = ''; this._historyYMin.text = ''; @@ -377,23 +381,7 @@ var VitalsMenuButton = GObject.registerClass({ if (samples.length === 0) return; try { - const historyDuration = Math.max(60, this._settings.get_int('sensor-history-duration')); - const interval = Math.max(1, this._settings.get_int('update-time')); - const base = Math.max(1, Math.ceil(historyDuration / interval / 800)); - this._historyGraph.setData(samples, label, '', base); - const tSpan = this._historyGraph.getTimeSpan(); - const clampedSpan = Math.min(tSpan, historyDuration); - this._historyXLeft.text = this._values.formatDuration(clampedSpan) + ' ' + _('ago'); - const rawRange = this._historyGraph.getRawRange(); - if (rawRange) { - this._historyYMax.text = this._values.formatValue(key, rawRange.max); - this._historyYMin.text = this._values.formatValue(key, rawRange.min); - this._historyYAxis.show(); - } else { - this._historyYMax.text = ''; - this._historyYMin.text = ''; - this._historyYAxis.hide(); - } + this._updateHistoryGraph(key, label, samples); } catch (e) { // ignore } From e99e44a77975c66d8ba654b60f59dd088a20f3b0 Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Tue, 24 Feb 2026 08:23:30 -0500 Subject: [PATCH 24/27] align panel, reuse code --- extension.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extension.js b/extension.js index a42a3be8..c8910164 100644 --- a/extension.js +++ b/extension.js @@ -341,7 +341,12 @@ var VitalsMenuButton = GObject.registerClass({ popoutY = menuY + relY + Math.round((rowH - popoutH) / 2); popoutY = Math.max(0, popoutY); } - this._historyPopout.set_position(menuX - this._historyPopout.get_width() - 8, popoutY); + const popoutW = this._historyPopout.get_width(); + const menuW = this.menu.actor.get_width(); + let popoutX = menuX - popoutW - 8; + if (popoutX < 0) + popoutX = menuX + menuW + 8; + this._historyPopout.set_position(popoutX, popoutY); this._historyPopout.show(); if (this._historyHideTimeoutId) { GLib.Source.remove(this._historyHideTimeoutId); From 1ecf06825de89ab7760ee72efff3116a6deff775 Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Tue, 24 Feb 2026 09:49:54 -0500 Subject: [PATCH 25/27] shift null values, show null values --- history.js | 11 +++++++++-- values.js | 18 ++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/history.js b/history.js index b0a969e6..8a712784 100644 --- a/history.js +++ b/history.js @@ -94,9 +94,11 @@ export const HistoryGraph = GObject.registerClass({ let vMin = Infinity, vMax = -Infinity; for (let i = dataOffset; i < data.length; i++) { + if (data[i].v === null) continue; if (data[i].v < vMin) vMin = data[i].v; if (data[i].v > vMax) vMax = data[i].v; } + if (vMin === Infinity) return; if (vMax <= vMin) { vMin -= 1; vMax += 1; } this._vMin = vMin; this._vMax = vMax; @@ -106,10 +108,15 @@ export const HistoryGraph = GObject.registerClass({ const iStart = dataOffset + b * base; const iEnd = Math.min(iStart + base, data.length); let sum = 0; + let count = 0; for (let i = iStart; i < iEnd; i++) { - sum += data[i].v; + if (data[i].v !== null) { + sum += data[i].v; + count++; + } } - const avg = sum / (iEnd - iStart); + if (count === 0) continue; + const avg = sum / count; const norm = (avg - vMin) / vRange; const barH = Math.max(1, Math.round(norm * graphH)); const x = Math.round(b * barWidth); diff --git a/values.js b/values.js index 3e617117..3fda6b3d 100644 --- a/values.js +++ b/values.js @@ -69,11 +69,23 @@ export const Values = GObject.registerClass({ const now = Date.now() / 1000; if (!(key in this._timeSeries)) this._timeSeries[key] = []; const buf = this._timeSeries[key]; - const minInterval = Math.max(1, this._settings.get_int('update-time')) - 0.5; - if (buf.length > 0 && (now - buf[buf.length - 1].t) < minInterval) { + const interval = Math.max(1, this._settings.get_int('update-time')); + const minInterval = interval - 0.5; + if (buf.length > 0 && buf[buf.length - 1].v !== null && (now - buf[buf.length - 1].t) < minInterval) { buf[buf.length - 1].v = num; return; } + if (buf.length > 0) { + const lastT = buf[buf.length - 1].t; + const gap = now - lastT; + if (gap > interval * 3) { + let fillT = lastT + interval; + while (fillT < now - interval * 0.5) { + buf.push({ t: fillT, v: null }); + fillT += interval; + } + } + } buf.push({ t: now, v: num }); const maxAge = this._getHistoryDurationSeconds(); while (buf.length > 0 && buf[0].t < now - maxAge) buf.shift(); @@ -136,6 +148,8 @@ export const Values = GObject.registerClass({ } while (buf.length > 0 && buf[0].t < cutoff) buf.shift(); + while (buf.length > 0 && buf[0].v === null) + buf.shift(); if (buf.length === 0) { delete this._timeSeries[key]; delete this._timeSeriesFormat[key]; From cc2c10622e8c159e814d13228295915e7e9f38f1 Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Tue, 24 Feb 2026 21:12:15 -0500 Subject: [PATCH 26/27] better history and suspend support --- extension.js | 16 +++++++++------- history.js | 17 +++++++++++++++-- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/extension.js b/extension.js index c8910164..470b28d9 100644 --- a/extension.js +++ b/extension.js @@ -60,7 +60,6 @@ var VitalsMenuButton = GObject.registerClass({ this._newGpuDetectedCount = 0; this._last_query = new Date().getTime(); this._historyPopout = null; - this._historyPopoutLeaveId = 0; this._historyHideTimeoutId = null; this._historyPopoutSensorKey = null; this._historyPopoutLabel = null; @@ -287,12 +286,15 @@ var VitalsMenuButton = GObject.registerClass({ _updateHistoryGraph(key, label, samples) { const historyDuration = Math.max(60, this._settings.get_int('sensor-history-duration')); - const interval = Math.max(1, this._settings.get_int('update-time')); - const base = Math.max(1, Math.ceil(historyDuration / interval / 800)); - this._historyGraph.setData(samples, label, '', base); - const tSpan = this._historyGraph.getTimeSpan(); - const clampedSpan = Math.min(tSpan, historyDuration); - this._historyXLeft.text = this._values.formatDuration(clampedSpan) + ' ' + _('ago'); + const nowSec = Date.now() / 1000; + const cutoff = nowSec - historyDuration; + const windowed = samples.filter(s => s.t >= cutoff); + while (windowed.length > 0 && windowed[0].v === null) windowed.shift(); + const base = Math.max(1, Math.ceil(windowed.length / 200)); + this._historyGraph.setData(windowed, label, '', base); + const actualSpan = this._historyGraph.getTimeSpan(); + const displayDuration = actualSpan > 0 ? Math.min(historyDuration, Math.round(actualSpan)) : historyDuration; + this._historyXLeft.text = this._values.formatDuration(displayDuration) + ' ' + _('ago'); const rawRange = this._historyGraph.getRawRange(); if (rawRange) { this._historyYMax.text = this._values.formatValue(key, rawRange.max); diff --git a/history.js b/history.js index 8a712784..2c065b58 100644 --- a/history.js +++ b/history.js @@ -98,8 +98,21 @@ export const HistoryGraph = GObject.registerClass({ if (data[i].v < vMin) vMin = data[i].v; if (data[i].v > vMax) vMax = data[i].v; } - if (vMin === Infinity) return; - if (vMax <= vMin) { vMin -= 1; vMax += 1; } + if (vMin === Infinity) { + vMin = 0; + vMax = 1; + } else if (vMax <= vMin) { + const v = vMin; + if (v >= 0 && v <= 1) { + const margin = 0.05; + vMin = Math.max(0, v - margin); + vMax = Math.min(1, v + margin); + if (vMax <= vMin) vMax = vMin + margin; + } else { + vMin -= 1; + vMax += 1; + } + } this._vMin = vMin; this._vMax = vMax; const vRange = vMax - vMin; From 7e60fb55d92ab3d4aa2cdc57c5607e0774b8173d Mon Sep 17 00:00:00 2001 From: Chris Monahan Date: Tue, 24 Feb 2026 21:59:09 -0500 Subject: [PATCH 27/27] refresh rate monitor --- sensors.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/sensors.js b/sensors.js index f3101617..a7c2e056 100644 --- a/sensors.js +++ b/sensors.js @@ -24,6 +24,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import * as SubProcessModule from './helpers/subprocess.js'; import * as FileModule from './helpers/file.js'; @@ -60,6 +61,12 @@ export const Sensors = GObject.registerClass({ this._nvidia_labels = []; this._bad_split_count = 0; + this._frameMonitorSignalId = 0; + this._frameMonitorLastTime = 0; + this._frameMonitorFrameCount = 0; + this._frameMonitorAccTime = 0; + this._frameMonitorCurrentHz = 0; + if (hasGTop) { this.storage = new GTop.glibtop_fsusage(); this._storageDevice = ''; @@ -500,10 +507,56 @@ export const Sensors = GObject.registerClass({ }).catch(err => { }); } + _initFrameMonitor() { + if (this._frameMonitorSignalId) return; + this._frameMonitorLastTime = 0; + this._frameMonitorFrameCount = 0; + this._frameMonitorAccTime = 0; + this._frameMonitorCurrentHz = 0; + this._frameMonitorSignalId = global.stage.connect('after-paint', () => { + this._onAfterPaint(); + }); + } + + _destroyFrameMonitor() { + if (this._frameMonitorSignalId) { + global.stage.disconnect(this._frameMonitorSignalId); + this._frameMonitorSignalId = 0; + } + this._frameMonitorLastTime = 0; + this._frameMonitorCurrentHz = 0; + } + + _onAfterPaint() { + const now = GLib.get_monotonic_time(); + + if (this._frameMonitorLastTime === 0) { + this._frameMonitorLastTime = now; + return; + } + + const delta = now - this._frameMonitorLastTime; + this._frameMonitorLastTime = now; + + this._frameMonitorFrameCount++; + this._frameMonitorAccTime += delta; + + if (this._frameMonitorAccTime >= 500000) { + this._frameMonitorCurrentHz = this._frameMonitorFrameCount / (this._frameMonitorAccTime / 1000000); + this._frameMonitorFrameCount = 0; + this._frameMonitorAccTime = 0; + } + } + _queryGpu(callback) { + if (this._frameMonitorCurrentHz > 0) + this._returnValue(callback, 'Refresh Rate', this._frameMonitorCurrentHz, 'gpu#1', 'hertz'); + if (!this._nvidia_smi_process) { // no nvidia-smi, so we use sysfs DRM if any cards was discovered if (!this._gpu_drm_indices){ + if (this._frameMonitorCurrentHz > 0) + this._returnValue(callback, 'Refresh Rate', this._frameMonitorCurrentHz, 'gpu#1-group', 'hertz'); this._disableGpuLabels(callback); return; } else { @@ -801,6 +854,7 @@ export const Sensors = GObject.registerClass({ // Launch nvidia-smi subprocess if nvidia querying is enabled this._reconfigureNvidiaSmiProcess(); this._discoverGpuDrm(); + this._initFrameMonitor(); } _discoverGpuDrm() { @@ -992,9 +1046,13 @@ export const Sensors = GObject.registerClass({ this._battery_charge_status = ''; this._nvidia_labels = []; this._bad_split_count = 0; + this._frameMonitorLastTime = 0; + this._frameMonitorFrameCount = 0; + this._frameMonitorAccTime = 0; } destroy() { + this._destroyFrameMonitor(); this._terminateNvidiaSmiProcess(); for (let signal of Object.values(this._settingChangedSignals))