From b98999cde914744a6ae397e8f7a03d4368e314fe Mon Sep 17 00:00:00 2001 From: Josevi Date: Sun, 20 Sep 2026 23:15:48 +0200 Subject: [PATCH] =?UTF-8?q?plasmoids:=20a=C3=B1ade=20commandoutput=20y=20c?= =?UTF-8?q?odexbar=20al=20tracking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ambos ya estaban puestos en un panel real pero no viajaban a la otra máquina -- detectado en la misma auditoría que quitó claudeusageswitcher y tt.launchpadPlasma (sin usar). Sin esto, una reinstalación desde arch-setup dejaría el panel sin estos dos widgets. Co-Authored-By: Claude Sonnet 5 --- .../contents/config/config.qml | 23 + .../contents/config/main.xml | 90 +++ ...ma_applet_com.github.zren.commandoutput.mo | Bin 0 -> 1721 bytes ...ma_applet_com.github.zren.commandoutput.mo | Bin 0 -> 1904 bytes ...ma_applet_com.github.zren.commandoutput.mo | Bin 0 -> 1960 bytes ...ma_applet_com.github.zren.commandoutput.mo | Bin 0 -> 1744 bytes .../contents/ui/config/ConfigActions.qml | 83 +++ .../contents/ui/config/ConfigAppearance.qml | 94 +++ .../contents/ui/config/ConfigCommand.qml | 41 ++ .../contents/ui/libconfig/CheckBox.qml | 12 + .../contents/ui/libconfig/ColorField.qml | 159 +++++ .../contents/ui/libconfig/ComboBox.qml | 102 ++++ .../contents/ui/libconfig/FontFamily.qml | 24 + .../contents/ui/libconfig/FormKCM.qml | 33 ++ .../contents/ui/libconfig/Heading.qml | 83 +++ .../contents/ui/libconfig/SpinBox.qml | 225 +++++++ .../contents/ui/libconfig/TextAlign.qml | 57 ++ .../contents/ui/libconfig/TextField.qml | 48 ++ .../contents/ui/libconfig/TextFormat.qml | 63 ++ .../contents/ui/libconfig/VertAlign.qml | 49 ++ .../contents/ui/main.qml | 393 +++++++++++++ .../metadata.json | 36 ++ .../translate/ReadMe.md | 53 ++ .../translate/bz.po | 166 ++++++ .../translate/fr.po | 148 +++++ .../translate/nl.po | 149 +++++ .../translate/pt_br.po | 166 ++++++ .../translate/template.pot | 139 +++++ .../org.rpa.codexbar/contents/code/parser.js | 548 ++++++++++++++++++ .../contents/config/config.qml | 15 + .../org.rpa.codexbar/contents/config/main.xml | 41 ++ .../contents/icons/codexbar.svg | 15 + .../contents/ui/CompactRepresentation.qml | 65 +++ .../contents/ui/ConfigGeneral.qml | 70 +++ .../contents/ui/ConfigProviders.qml | 70 +++ .../contents/ui/FullRepresentation.qml | 372 ++++++++++++ .../contents/ui/HistoryChart.qml | 49 ++ .../contents/ui/RadialGauge.qml | 101 ++++ .../org.rpa.codexbar/contents/ui/main.qml | 224 +++++++ .../plasmoids/org.rpa.codexbar/metadata.json | 23 + 40 files changed, 4029 insertions(+) create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/config/config.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/config/main.xml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/bz/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/fr/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/nl/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/pt_br/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/config/ConfigActions.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/config/ConfigAppearance.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/config/ConfigCommand.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/CheckBox.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/ColorField.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/ComboBox.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/FontFamily.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/FormKCM.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/Heading.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/SpinBox.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextAlign.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextField.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextFormat.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/VertAlign.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/main.qml create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/metadata.json create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/ReadMe.md create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/bz.po create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/fr.po create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/nl.po create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/pt_br.po create mode 100644 dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/template.pot create mode 100644 dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/code/parser.js create mode 100644 dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/config/config.qml create mode 100644 dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/config/main.xml create mode 100644 dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/icons/codexbar.svg create mode 100644 dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/CompactRepresentation.qml create mode 100644 dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/ConfigGeneral.qml create mode 100644 dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/ConfigProviders.qml create mode 100644 dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/FullRepresentation.qml create mode 100644 dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/HistoryChart.qml create mode 100644 dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/RadialGauge.qml create mode 100644 dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/main.qml create mode 100644 dot_local/share/plasma/plasmoids/org.rpa.codexbar/metadata.json diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/config/config.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/config/config.qml new file mode 100644 index 0000000..ad9aa63 --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/config/config.qml @@ -0,0 +1,23 @@ +import QtQuick + +import org.kde.plasma.configuration + +ConfigModel { + ConfigCategory { + name: i18n("Command") + icon: "configure" + source: "config/ConfigCommand.qml" + } + + ConfigCategory { + name: i18n("Appearance") + icon: "preferences-desktop-color" + source: "config/ConfigAppearance.qml" + } + + ConfigCategory { + name: i18n("Actions") + icon: "preferences-desktop-mouse" + source: "config/ConfigActions.qml" + } +} diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/config/main.xml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/config/main.xml new file mode 100644 index 0000000..783239f --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/config/main.xml @@ -0,0 +1,90 @@ + + + + + + + 1000 + + + + echo "Test $(date +%S)" + + + + + + + + + false + + + 13 + + + + + + false + + + false + + + false + + + + + + + 1 + + + + + + 128 + + + + + + + + + false + + + true + + + + + + + + + + + + + false + + + 100 + + + false + + + 100 + + + false + + + + + diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/bz/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/locale/bz/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo new file mode 100644 index 0000000000000000000000000000000000000000..df0d692926b307b5e15b114d77fb104d9fe39b4f GIT binary patch literal 1721 zcmYk5&ub(_6vs=Au8z8^>#x-nc?Si-bbdi_X?1ZnGr=`GY?w@p?nP_5D?R0OS2y(| zlNt2j!GnT`h~WMQ0re7aLA(fa@!-{~=)t20!IOu@@0;mtET;N1?{(Fy?|b$7xAO~M z35<)F&tm?A`2yxIcj1BY-rYi+0VUV~HFz)R!TZ2T{r)5HEbLFfbKuwD!{8D45O@n* z0H?M6Gsy9Nt^L1)55oQvydV4qzggSgfG@-T9()A+3v7e8L5|ykD9)o>vkyK6+k*IsPw`+~Ux0jHH$l$p z7RXn|H9@FZ&cIB|;~;D0^SE}blLw!}gYSiF#piI2JUHgbKxpwKCQ3Hz6ZW&#r}+=Y z`8j6y$J%&2fysG`X5w{eoM={+l2)2TiH%$*L$OijrFB42NRytBt_NQQFIvhCeOb|- zPBZ0Wv8fMLN^dBg4enTY=LdLI8b5o`rYv+mo_lCVPgE>A+9je>2B&Cmpj0jpKi39t zx~j@$qDbaB88u?3qmlLlauxhebc3N<9ah7Tl+49arK~PfonY^npq{KUsj{TKqjHwO z(rkx1P%dePyGz zNpOEAi|JBd`-W(`ay)BNi3+%}6kE9-YOz&X?eOFd4Pr;?R8p#`b~u3!)Y2a z5Moblrwb%&!pNY%53va045Xpts76mYkLBAkwQ^)c+uy`yI`cf-@VzHdH?_0H@}>l_(EX3 zjOPVB=kF8Zb?~t>_`&!Fd_4}wSa`=7u`#CO26;2-t;-{9kj#r-%F zm;@)lsX8W*&y7IdKUcrM0X~X&6?_AnW)YWW9fZ z?Bg!@EO-VYo&u*pNEFjGXTj^J!DW!w?_l#I;3pua#AhJuxmoi|kad3rPJv&8yzdC) z{6E+H4diJQI(*xV8zG1JN zmvix8k31kpyoiT$u}-e}OL+Jjuus;=`FWhj^9&yDMIP)2pG#ce0E7LD`6Os-yqGTv zC7m>h5(~Lbw#9Qw5AwytIsD5Zkjo}mr>v5LjA-BFI}WU=UIpNNhvy`rs5sazm!t_>D;Rgud? zk<4>4YL_QH?dp(`FEH;#w=^f`a3Coed!Rx&Z3jBR-hF~8a>@*lRtHbE^3|-iD`?p# zv5`27EN#1M;^Sbmz~9X7QeP(9sk0?Y(^Gr!+Oswfx)7VtJWxYf=7EN=JkcHpVoPeC zaV)3E6_;I%yeRg>y0aguBt$EN=w0PJUc|?m$Fdr5ZC!{uKGlQhoig=N&&HI6Q22Oy zI)(kR-^97o@59nGD&$8pk_D%UYWMX?>uXV0?PyLCwPjE-&9-JQMXfiYnM>4qE57{Z z#nx=Au_}FtdguqA%V1qh8`5WzI&0msnu`J7gLuiQ3=J{3W($n#}#dT4l>gl{%(UUqK|EtrXTTAk#+p#&H(AMZLkM_pb z3EV8?p+oasGXeiRFX}2Z2^yO9dGZk9d1bCsZumkvM4nXL^r(Hc^j@!5LKgQ94-%-S z(x~HxQdArsRM+66QjJi3f^j(<>Lgh)!% z`2SvI(9Le@1P)60QjG6RrE$d!ZNEgl#5#Mz@z^5fQ5Z9D@GDVxQ#pV2@L=qRFV5{- H0760}B18h&C5Qk4F(N=0@zw13Y+CO5ZS|w7zUprOdT#Plf%XFW zv*_pV65;~*-~>Ks-+=dl--0dhJMbRx2k>6-$7cR#@GR_G;KSfw&G^6IL$Jl&@Clp+ zC&8)4mLU60K;A#s%wGY|z+MOM2d{w=R3Q8L2<(6#gS`LyX8fndz6Ej~zk{6bKOpP4 z4L$` zh$VtRj%&aNz!50H4?))bdBZP3$gXqe{J7QU20o3>8X#GmLqFZHt-d#01I~kEaO0TV zIEUxZQBLs;I%{|yozH;ttZN8zJCDw_;Je6e3LVEKo@4{%%4Q^% zjLrsP$r$Px$5@7RW~PArsyhw8 zGw(st3@Q|cDp4heNgDNyr>)Ion+`PxNtRWlRL-_%FD31ZN#~O6wA0zwUuk#R?bf;q zG1#FULO@UbgbZ1=+bU-@2AuAxnO& zd&^OdgS$9=X?nI*KNnoGeRwHQMq7w?j2;Zgf>fdSVzbP>%KGunk4ncwM_ydUw5qyB z<)h}&tl5GBR1C=EVjo2$*>|qMp)^+46Xk5KQ;g&zN^t5Y?i>w5#4U7~P45JhR!ODo zNMFB6xVf9S&AJd9l@23~r^E$yl~2W@F~m>DQ^5y{WrsLAoN?}OOYxkdhSXy{veCgV z_n}7;R=P&)@zbIKFO!&w&cP8K?~=9iX|Xph2H967nE^?$IW$TYGSqLaPIt&PI5n_{ xVv>hSJlN8|Rns@_aI zihEH!c-Vu7^|C@l+>;1<@Z^98K@b+igLw1q#e?9%|Lf^&ET;Q6uRh-UzgMsSI6HS$ zU_946|AH^#`TY!D808&8+yh<$Ti_9R7x)o)H~4Y0{u%fX?9ai6!LPsv!JFU%;E&)O zc&o901KHo7&HP{B{jmQA?*-4?DFlHnkk6k7m%vq!&+Rw!Rbzh$a@6GYh(Rh>p#z^FUEZlb(<+R-vqd z=qi5@Y)QL1%~XiRx}K<%-c&jp-?p$8##pi@)Gu0>h0c#=GqkNgP_gK1pNPJ#yrSK) zQn`TtTpO$mRGG^}k<4>4>X0Wr9qKS9Ut->iL1m~>huttFB{Q*9DyMC#6P!IFs3)gP zsvPOy$<_(X*C*6v<&H>fC(a^D*B+Yq#MmkEcWe)7B$I>8*$OonstJBDS2}S4oIA(&!!KJQm^;tur|-WNTfB`aaWXv{q$4 z8rqn~A(TE|SjdoeHJXRth4+xk0xIJR8Of5vL`~L>r>%?8KwZ`xBn|$s(rPc1&e>e{Im}Z}fYoDK5<~wmPVgG6;24LP|gdS-m!|P%@UztMJ;+a6MW* z-N%yll#6;M!4;Y;rqz)SEzzDHPrt*pw@c}!Uz9iUxb$s!Wh1J)vXAfT;>O`%ixQ?FI6RbwC$)04X%{^myh`I;tmmF&4dv zN-B7&3f@>BK!Z!9_AcQw(%!2KO==#fFw5LQl8aIx=4n7R&IRe$S z7)d=rr2ow%x= 0 ? model[currentIndex] : null + + textRole: "text" // Doesn't autodeduce from model if we manually populate it + + // Note that ComboBox.valueRole and ComboBox.currentValue was introduced in Qt 5.14. + // Ubuntu 20.04 only has Qt 5.12. We cannot define a currentValue property or it will + // break when users upgrade to Qt 5.14. + property string _valueRole: "value" + readonly property var _currentValue: _valueRole && currentIndex >= 0 ? model[currentIndex][_valueRole] : null + readonly property alias value: configComboBox._currentValue + + model: [] + + signal populate() + property bool populated: true + + Component.onCompleted: { + populate() + selectValue(configValue) + } + + onCurrentIndexChanged: { + if (typeof model !== 'number' && 0 <= currentIndex && currentIndex < count) { + var item = model[currentIndex] + if (typeof item !== "undefined") { + var val = item[_valueRole] + if (configKey && (typeof val !== "undefined") && populated) { + plasmoid.configuration[configKey] = val + } + } + } + } + + function size() { + if (typeof model === "number") { + return model + } else if (typeof model.count === "number") { + return model.count + } else if (typeof model.length === "number") { + return model.length + } else { + return 0 + } + } + + function findValue(val) { + for (var i = 0; i < size(); i++) { + if (model[i][_valueRole] == val) { + return i + } + } + return -1 + } + + function selectValue(val) { + var index = findValue(val) + if (index >= 0) { + currentIndex = index + } + } +} diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/FontFamily.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/FontFamily.qml new file mode 100644 index 0000000..3f9f8bb --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/FontFamily.qml @@ -0,0 +1,24 @@ +// Version 4 + +import QtQuick + +import "." as LibConfig + +LibConfig.ComboBox { + id: configFontFamily + + populated: false + + // Based on: org.kde.plasma.digitalclock + onPopulate: { + var arr = [] // Use temp array to avoid constant binding stuff + arr.push({ text: i18nc("Use default font", "Default"), value: "" }) + + var fonts = Qt.fontFamilies() + for (var i = 0; i < fonts.length; i++) { + arr.push({ text: fonts[i], value: fonts[i] }) + } + model = arr + populated = true + } +} diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/FormKCM.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/FormKCM.qml new file mode 100644 index 0000000..e2ba60b --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/FormKCM.qml @@ -0,0 +1,33 @@ +// Version 3 + +import QtQuick +import QtQuick.Window +import org.kde.kirigami as Kirigami +import org.kde.kcmutils as KCM + +KCM.SimpleKCM { + id: simpleKCM + default property alias _formChildren: formLayout.data + + Kirigami.FormLayout { + id: formLayout + } + + // https://invent.kde.org/plasma/plasma-desktop/-/blame/master/desktoppackage/contents/configuration/AppletConfiguration.qml + // AppletConfiguration.implicitWidth: Kirigami.Units.gridUnit * 40 = 720 + // AppletConfiguration.Layout.minimumWidth: Kirigami.Units.gridUnit * 30 = 540 + // In practice, Window.width = 744px is a typical FormLayout.wideMode switchWidth + // A rough guess is 128+24+180+10+360+24+20 = 746px + // TabSidebar=128x, Padding=24px, Labels=180px, Spacing=10px, Controls=360px, Padding=24px, Scrollbar=20px + // However the default is only 720px. So we'll set it to a 800px minimum to avoid wideMode=false + property int wideModeMinWidth: 800 * Screen.devicePixelRatio + Window.onWindowChanged: { + if (Window.window) { + Window.window.visibleChanged.connect(function(){ + if (Window.window && Window.window.visible && Window.window.width < wideModeMinWidth) { + Window.window.width = wideModeMinWidth + } + }) + } + } +} diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/Heading.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/Heading.qml new file mode 100644 index 0000000..bff0043 --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/Heading.qml @@ -0,0 +1,83 @@ +// Version 6 + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +/* +** Example: +** +import './libconfig' as LibConfig +LibConfig.Heading { + text: i18n("SpinBox (Double)") +} +*/ + +// While the following Kirigami is very simple: +// Kirigami.Separator { +// Kirigami.FormData.label: "Heading" +// Kirigami.FormData.isSection: true +// } +// +// I want to be able to adjust the label size and make it bold. +// Kirigami's buddy Heading is level=3, which does not stand out +// very well. I also want to center the heading. +// Since we can't access the Heading in the buddy component, we +// need to make sure the Heading has no text, and draw our own. +ColumnLayout { + id: heading + spacing: 0 + + property string text: "" + property alias separator: separator + property alias label: label + property bool useThickTopMargin: true + + property Item __formLayout: { + if (parent && typeof parent.wideMode === 'boolean') { + return parent + } else if (typeof formLayout !== 'undefined' && typeof formLayout.wideMode === 'boolean') { + return formLayout + } else if (typeof page !== 'undefined' && typeof page.wideMode === 'boolean') { + return page + } else { + return null + } + } + + Layout.fillWidth: true + // Kirigami.FormData.isSection: true + Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.FormLabel + + Kirigami.Separator { + id: separator + visible: false + Layout.fillWidth: true + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.bottomMargin: Kirigami.Units.largeSpacing + } + + Kirigami.Heading { + id: label + Layout.topMargin: useThickTopMargin ? Kirigami.Units.largeSpacing * 3 : Kirigami.Units.largeSpacing + Layout.bottomMargin: Kirigami.Units.smallSpacing + Layout.fillWidth: true + text: heading.text + level: 1 + font.weight: Font.Bold + // horizontalAlignment: (!__formLayout || __formLayout.wideMode) ? Text.AlignHCenter : Text.AlignLeft + verticalAlignment: (!__formLayout || __formLayout.wideMode) ? Text.AlignVCenter : Text.AlignBottom + } +} + +//--- Test Default Kirigami Heading +// Kirigami.Separator { +// property string text: "" +// Kirigami.FormData.label: text +// Kirigami.FormData.isSection: true +// property alias separator: separator +// Item { +// id: separator +// } +// } diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/SpinBox.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/SpinBox.qml new file mode 100644 index 0000000..eb8908e --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/SpinBox.qml @@ -0,0 +1,225 @@ +// Version 7 + +import QtQuick +import QtQuick.Controls as QQC2 + +/* +** Example: +** +import './libconfig' as LibConfig +// Integer +LibConfig.SpinBox { + configKey: "leftPadding" + suffix: "px" + from: 0 + to: 1000 + stepSize: 5 +} +// Double +LibConfig.SpinBox { + configKey: "distance" + decimals: 3 + suffix: "m" + minimumValue: 0.0 + maximumValue: 1000.0 + stepSize: Math.round(0.5 * factor) +} +*/ +// QQC1.SpinBox: https://github.com/qt/qtquickcontrols/blob/dev/src/controls/SpinBox.qml +// QQC2.SpinBox: https://github.com/qt/qtquickcontrols2/blob/5.15/src/imports/controls/SpinBox.qml +// KDE Config Theme: https://invent.kde.org/frameworks/qqc2-desktop-style/-/blob/master/org.kde.desktop/SpinBox.qml +// Qt6 QQC2.SpinBox: https://github.com/qt/qtquickcontrols2/blob/dev/src/imports/controls/basic/SpinBox.qml +QQC2.SpinBox { + id: spinBox + + property string configKey: '' + readonly property var configValue: configKey ? plasmoid.configuration[configKey] : 0 + + readonly property real factor: Math.pow(10, decimals) + readonly property real valueReal: value / factor + value: Math.round(configValue * factor) + onValueRealChanged: serializeTimer.start() + + readonly property int spinBox_MININT: Math.ceil(-2147483648 / factor) + readonly property int spinBox_MAXINT: Math.floor(2147483647 / factor) + from: Math.round(minimumValue * factor) + to: Math.round(maximumValue * factor) + + // Reimplement QQC1 properties + // https://github.com/qt/qtquickcontrols/blob/dev/src/controls/SpinBox.qml + property int decimals: 0 + property alias prefix: prefixLabel.text + property alias suffix: suffixLabel.text + property real minimumValue: 0 + property real maximumValue: spinBox_MAXINT + + // Avoid selecting prefix/suffix by drawing them overlayed on top. + // As a bonus, we can draw with in a different color (textColor at 60% opacity). + QQC2.Label { + id: prefixLabel + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.leftMargin: spinBox.leftPadding + anchors.topMargin: spinBox.topPadding + anchors.bottomMargin: spinBox.bottomPadding + font: spinBox.font + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + color: spinBox.palette.text + opacity: 0.6 + } + QQC2.Label { + id: suffixLabel + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.rightMargin: spinBox.rightPadding + anchors.topMargin: spinBox.topPadding + anchors.bottomMargin: spinBox.bottomPadding + font: spinBox.font + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + color: spinBox.palette.text + opacity: 0.6 + } + + Timer { // throttle + id: serializeTimer + interval: 300 + onTriggered: { + if (configKey) { + if (decimals == 0) { + plasmoid.configuration[configKey] = spinBox.value + } else { + plasmoid.configuration[configKey] = spinBox.valueReal + } + } + } + } + + // Note: Qt5 used RegExpValidator { regExp: /[\-\.\d]+/ } + // validator: RegularExpressionValidator { + // regularExpression: /[\-\.\d]+/ + // } + validator: DoubleValidator { + bottom: Math.min(spinBox.from, spinBox.to) + top: Math.max(spinBox.from, spinBox.to) + decimals: spinBox.decimals + notation: DoubleValidator.StandardNotation + } + + textFromValue: function(value, locale) { + return Number(value / factor).toFixed(decimals) + } + + valueFromText: function(text, locale) { + var text2 = text + .replace(/[^\-\.\d]/g, '') // Remove non digit characters + .replace(/\.+/g, '.') // Allow user to type '.' instead of RightArrow to enter to decimals + var val = Number(text2) + if (isNaN(val)) { + val = -0 + } + // console.log('valueFromText', text, val) + return Math.round(val * factor) + } + + // Select value on foucs + onActiveFocusChanged: { + if (activeFocus) { + selectValue() + } + } + function selectValue() { + // Check if SpinBox.contentItem == TextInput + // https://invent.kde.org/frameworks/qqc2-desktop-style/-/blob/master/org.kde.desktop/SpinBox.qml + // https://doc.qt.io/qt-5/qml-qtquick-textinput.html#select-method + if (contentItem && contentItem instanceof TextInput) { + contentItem.selectAll() + } + } + + function fixMinus(str) { + var minusIndex = str.indexOf('-') + if (minusIndex >= 0) { + var a = str.substr(0, minusIndex) + var b = str.substr(minusIndex+1) + console.log('a', a, 'b', b) + + return '-' + a + b + } else { + return str + } + } + function fixDecimals(str) { + var periodIndex = str.indexOf('.') + var a = str.substr(0, periodIndex+1) + var b = str.substr(periodIndex+1) + return a + b.replace(/\.+/g, '') // Remove extra periods + } + + function fixText(str) { + return fixMinus(fixDecimals(str)) + } + + function onTextEdited() { + var oldText = spinBox.contentItem.text + // console.log('onTextEdited', 'oldText1', oldText) + oldText = fixText(oldText) + // console.log('onTextEdited', 'oldText2', oldText) + var oldPeriodIndex = oldText.indexOf('.') + if (oldPeriodIndex == -1) { + oldPeriodIndex = oldText.length + } + var oldCursorPosition = spinBox.contentItem.cursorPosition + var oldCursorDelta = oldPeriodIndex - oldCursorPosition + + spinBox.value = spinBox.valueFromText(oldText, spinBox.locale) + spinBox.valueModified() + + var newText = spinBox.contentItem.text + // console.log('onTextEdited', 'newText1', newText) + newText = fixText(newText) + // console.log('onTextEdited', 'newText2', newText) + var newPeriodIndex = newText.indexOf('.') + if (newPeriodIndex == -1) { + newPeriodIndex = newText.length + } + if (newText != spinBox.contentItem.text) { + spinBox.contentItem.text = Qt.binding(function(){ + return spinBox.textFromValue(spinBox.value, spinBox.locale) + }) + } + spinBox.contentItem.cursorPosition = newPeriodIndex - oldCursorDelta + } + + function bindContentItem() { + if (contentItem && contentItem instanceof TextInput) { + // We bind the left/right padding in the TextInput so that + // clicking the prefix/suffix will focus the TextInput. If we set + // the SpinBox left/right padding, then they do not focus the TextInput. + contentItem.leftPadding = Qt.binding(function(){ return prefixLabel.implicitWidth }) + contentItem.rightPadding = Qt.binding(function(){ return suffixLabel.implicitWidth }) + + // Bind value update on keypress, while retaining cursor position + spinBox.contentItem.textEdited.connect(spinBox.onTextEdited) + } + } + + onContentItemChanged: { + bindContentItem() + } + + Component.onCompleted: { + for (var i = 0; i < data.length; i++) { + if (data[i] instanceof Connections) { + // Remove the Connections where it changes the text/cursor when typing. + // onTextEdited { value = valueFromText() } + data[i].destroy() + break + } + } + bindContentItem() + } +} diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextAlign.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextAlign.qml new file mode 100644 index 0000000..daaff61 --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextAlign.qml @@ -0,0 +1,57 @@ +// Version 4 + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +RowLayout { + id: configTextAlign + + property string configKey: '' + readonly property string configValue: configKey ? plasmoid.configuration[configKey] : "" + + function setValue(val) { + if (configKey) { + plasmoid.configuration[configKey] = val + } + updateChecked() + } + + function updateChecked() { + // button.checked bindings are unbound when clicked + justifyLeftButton.checked = Qt.binding(function(){ return configValue == Text.AlignLeft }) + justifyCenterButton.checked = Qt.binding(function(){ return configValue == Text.AlignHCenter }) + justifyRightButton.checked = Qt.binding(function(){ return configValue == Text.AlignRight }) + justifyFillButton.checked = Qt.binding(function(){ return configValue == Text.AlignJustify }) + } + + Component.onCompleted: updateChecked() + + QQC2.Button { + id: justifyLeftButton + icon.name: "format-justify-left-symbolic" + checkable: true + onClicked: setValue(Text.AlignLeft) + } + + QQC2.Button { + id: justifyCenterButton + icon.name: "format-justify-center-symbolic" + checkable: true + onClicked: setValue(Text.AlignHCenter) + } + + QQC2.Button { + id: justifyRightButton + icon.name: "format-justify-right-symbolic" + checkable: true + onClicked: setValue(Text.AlignRight) + } + + QQC2.Button { + id: justifyFillButton + icon.name: "format-justify-fill-symbolic" + checkable: true + onClicked: setValue(Text.AlignJustify) + } +} diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextField.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextField.qml new file mode 100644 index 0000000..69fd6a0 --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextField.qml @@ -0,0 +1,48 @@ +// Version 6 + +import QtQuick +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami + +QQC2.TextField { + id: textField + + property string configKey: '' + readonly property string configValue: configKey ? plasmoid.configuration[configKey] : "" + onConfigValueChanged: { + if (!focus && text != configValue) { + text = configValue + } + } + + text: configValue + onTextChanged: serializeTimer.start() + + property string defaultValue: "" + + rightPadding: clearButton.width + Kirigami.Units.smallSpacing + + property alias clearButton: clearButton + QQC2.ToolButton { + id: clearButton + visible: textField.text != textField.defaultValue + icon.name: "edit-clear" + onClicked: textField.text = defaultValue + + anchors.top: parent.top + anchors.right: parent.right + anchors.bottom: parent.bottom + + width: implicitBackgroundHeight + } + + Timer { // throttle + id: serializeTimer + interval: 300 + onTriggered: { + if (configKey) { + plasmoid.configuration[configKey] = textField.text + } + } + } +} diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextFormat.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextFormat.qml new file mode 100644 index 0000000..b3f2bdb --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/TextFormat.qml @@ -0,0 +1,63 @@ +// Version 6 + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +import "." as LibConfig + +Flow { + id: configTextFormat + + spacing: Kirigami.Units.smallSpacing * 2 + Layout.fillWidth: true + + property alias boldConfigKey: configBold.configKey + property alias italicConfigKey: configItalic.configKey + property alias underlineConfigKey: configUnderline.configKey + property alias alignConfigKey: configTextAlign.configKey + property alias vertAlignConfigKey: configVertAlign.configKey + + RowLayout { + QQC2.Button { + id: configBold + property string configKey: '' + visible: configKey + icon.name: 'format-text-bold-symbolic' + checkable: true + checked: configKey ? plasmoid.configuration[configKey] : false + onClicked: plasmoid.configuration[configKey] = checked + } + + QQC2.Button { + id: configItalic + property string configKey: '' + visible: configKey + icon.name: 'format-text-italic-symbolic' + checkable: true + checked: configKey ? plasmoid.configuration[configKey] : false + onClicked: plasmoid.configuration[configKey] = checked + } + + QQC2.Button { + id: configUnderline + property string configKey: '' + visible: configKey + icon.name: 'format-text-underline-symbolic' + checkable: true + checked: configKey ? plasmoid.configuration[configKey] : false + onClicked: plasmoid.configuration[configKey] = checked + } + } + + LibConfig.TextAlign { + id: configTextAlign + visible: configKey + } + + LibConfig.VertAlign { + id: configVertAlign + visible: configKey + } +} diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/VertAlign.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/VertAlign.qml new file mode 100644 index 0000000..5c4d585 --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/libconfig/VertAlign.qml @@ -0,0 +1,49 @@ +// Version 5 + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +RowLayout { + id: configVertAlign + + property string configKey: '' + readonly property string configValue: configKey ? plasmoid.configuration[configKey] : "" + + function setValue(val) { + if (configKey) { + plasmoid.configuration[configKey] = val + } + updateChecked() + } + + function updateChecked() { + // button.checked bindings are unbound when clicked + vertTopButton.checked = Qt.binding(function(){ return configValue == Text.AlignTop }) + vertCenterButton.checked = Qt.binding(function(){ return configValue == Text.AlignVCenter }) + vertBottomButton.checked = Qt.binding(function(){ return configValue == Text.AlignBottom }) + } + + Component.onCompleted: updateChecked() + + QQC2.Button { + id: vertTopButton + icon.name: "align-vertical-top" + checkable: true + onClicked: setValue(Text.AlignTop) + } + + QQC2.Button { + id: vertCenterButton + icon.name: "align-vertical-center" + checkable: true + onClicked: setValue(Text.AlignVCenter) + } + + QQC2.Button { + id: vertBottomButton + icon.name: "align-vertical-bottom" + checkable: true + onClicked: setValue(Text.AlignBottom) + } +} diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/main.qml b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/main.qml new file mode 100644 index 0000000..fce613d --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/contents/ui/main.qml @@ -0,0 +1,393 @@ +import QtQuick +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami +import org.kde.plasma.plasmoid +import org.kde.plasma.core as PlasmaCore +import org.kde.plasma.components as PlasmaComponent +import org.kde.plasma.plasma5support as Plasma5Support + +PlasmoidItem { + id: widget + + // https://github.com/KDE/plasma-workspace/blob/master/dataengines/executable/executable.h + // https://github.com/KDE/plasma-workspace/blob/master/dataengines/executable/executable.cpp + // https://github.com/KDE/plasma-framework/blob/master/src/declarativeimports/core/datasource.h + // https://github.com/KDE/plasma-framework/blob/master/src/declarativeimports/core/datasource.cpp + // https://github.com/KDE/plasma-framework/blob/master/src/plasma/scripting/dataenginescript.cpp + Plasma5Support.DataSource { + id: executable + engine: "executable" + connectedSources: [] + onNewData: (sourceName, data) => { + var exitCode = data["exit code"] + var exitStatus = data["exit status"] + var stdout = data["stdout"] + var stderr = data["stderr"] + exited(sourceName, exitCode, exitStatus, stdout, stderr) + disconnectSource(sourceName) // cmd finished + } + function exec(cmd) { + if (cmd) { + connectSource(cmd) + } + } + signal exited(string cmd, int exitCode, int exitStatus, string stdout, string stderr) + } + + function performClick() { + executable.exec(plasmoid.configuration.clickCommand) + } + + function performMouseWheelUp() { + executable.exec(plasmoid.configuration.mousewheelUpCommand) + } + + function performMouseWheelDown() { + executable.exec(plasmoid.configuration.mousewheelDownCommand) + } + + Item { + id: config + readonly property bool active: !!command + readonly property bool waitForCompletion: plasmoid.configuration.waitForCompletion + readonly property int interval: Math.max(0, plasmoid.configuration.interval) + readonly property string command: plasmoid.configuration.command || '' + readonly property string tooltipCommand: plasmoid.configuration.tooltipCommand || '' + readonly property bool clickEnabled: !!plasmoid.configuration.clickCommand + readonly property bool mousewheelEnabled: (plasmoid.configuration.mousewheelUpCommand || plasmoid.configuration.mousewheelDownCommand) + readonly property color textColor: plasmoid.configuration.textColor || Kirigami.Theme.textColor + readonly property color outlineColor: plasmoid.configuration.outlineColor || Kirigami.Theme.backgroundColor + readonly property bool showOutline: plasmoid.configuration.showOutline + + onCommandChanged: widget.runCommand() + onTooltipCommandChanged: widget.runCommand() + onIntervalChanged: { + // interval=0 stops the timer even with Timer.repeat=true, so we may + // need to restart the timer. Might as well restart the interval too. + timer.restart() + } + onWaitForCompletionChanged: { + if (!waitForCompletion) { + // The timer needs to be restarted in case the timer was already + // triggered and the command is running. If we don't restart the + // timer, it'll stop forever. + timer.restart() + } + } + } + + // https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences + property var ansiColors: ({ + 30: '#000000', // Black + 31: '#aa0000', // Red + 32: '#00aa00', // Green + 33: '#aa6500', // Yellow + 34: '#0000aa', // Blue + 35: '#aa00aa', // Magenta + 36: '#00aaaa', // Cyan + 37: '#aaaaaa', // White + 90: '#656565', // Bright Black + 91: '#ff6565', // Bright Red + 92: '#65ff65', // Bright Green + 93: '#ffff65', // Bright Yellow + 94: '#6565ff', // Bright Blue + 95: '#ff65ff', // Bright Magenta + 96: '#65ffff', // Bright Cyan + 97: '#ffffff', // Bright White + }) + function resetState(state) { + var out = state.closeTags.join(' ') + state.bold = false + state.closeTags = [] + return out + } + function parseAnsiCode(n, i, tokens, state) { + if (n == 0) { // Reset + return resetState(state) + } else if (n == 1) { + state.closeTags.push('') + state.bold = true + return '' + } else if (30 <= n && n <= 37 || 90 <= n && n <= 97) { + if (state.bold && 30 <= n && n <= 37) { + // Bold also intensifies the colors to "Bright". + // 30 => 90 + n += 60 + } + var hexColor = ansiColors[n] + state.closeTags.push('') + return '' + } else { + return '' + } + } + // https://stackoverflow.com/questions/4745317/converting-integers-to-hex-string-in-javascript + function formatHexInt(n) { + var num = Number(n) + if (isNaN(num)) { + return "00" + } + num = Math.max(0, Math.min(num, 255)) + var str = num.toString(16) + return str.length == 1 ? '0' + str : str + } + function rgbToHex(r, g, b) { + return '#' + formatHexInt(r) + formatHexInt(g) + formatHexInt(b) + } + function parseColorMode(i, tokens) { + var colorMode = parseInt(tokens[++i], 10) + if (colorMode == 2) { // RGB + var r = parseInt(tokens[++i], 10) + var g = parseInt(tokens[++i], 10) + var b = parseInt(tokens[++i], 10) + return rgbToHex(r, g, b) + } else if (colorMode == 5) { // Preset of 256 colors + // Logic taken from Konsole + // https://invent.kde.org/utilities/konsole/-/blob/master/src/autotests/CharacterColorTest.cpp#L159 + var n = parseInt(tokens[++i], 10) + if (0 <= n && n <= 7) { // Normal + var u = n + 30 + return ansiColors[u] + } else if (8 <= n && n <= 15) { // Bright + var u = n - 8 + 90 + return ansiColors[u] + } else if (16 <= n && n <= 231) { // 212 + var u = n - 16 + var r = Math.floor(((u / 36) % 6) != 0 ? (40 * ((u / 36) % 6) + 55) : 0) + var g = Math.floor(((u / 6) % 6) != 0 ? (40 * ((u / 6) % 6) + 55) : 0) + var b = Math.floor(((u / 1) % 6) != 0 ? (40 * ((u / 1) % 6) + 55) : 0) + return rgbToHex(r, g, b) + } else if (232 <= n && n <= 255) { + var gray = Math.floor((n - 232) * 10 + 8) + return rgbToHex(gray, gray, gray) + } + } + return null + } + function parseAnsiEscape(codes, state) { + var tokens = codes.split(';') + var out = '' + for (var i = 0; i < tokens.length; i++) { + tokens[i] = parseInt(tokens[i], 10) + } + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i] + if (token == 38) { // Set FG + var hexColor = parseColorMode(i, tokens) + if (hexColor) { + state.closeTags.push('') + out += '' + } + } else if (token == 48) { // Set BG + var hexColor = parseColorMode(i, tokens) + // Ignore + } else { + out += parseAnsiCode(token, i, tokens, state) + } + } + return out + } + + property string outputText: '' + property string tooltipText: '' + + function formatOutputText(stdout) { + var formattedText = stdout + + // Newlines + if (plasmoid.configuration.replaceAllNewlines) { + formattedText = formattedText.replace(/\n/g, ' ').trim() + } else if (formattedText.length >= 1 && formattedText[formattedText.length-1] == '\n') { + formattedText = formattedText.substr(0, formattedText.length-1) + } + + // Terminal Colors (Issue #7) + var state = { + html: false, + bold: false, + closeTags: [], + } + formattedText = formattedText.replace(/\033\[(\d+(;\d+)*)?m/g, function(match, p1, p2){ + state.html = true + if (typeof p1 === 'string') { + return parseAnsiEscape(p1, state) + } else { // \033[m is Reset + return parseAnsiEscape('0', state) + } + }) + formattedText += resetState(state) + + // Format Newlines when in HTML mode + if (state.html) { + formattedText = formattedText.replace(/\n/g, '
') + } + return formattedText + } + + Connections { + target: executable + function onExited(cmd, exitCode, exitStatus, stdout, stderr) { + if ((cmd == config.command) || (cmd == config.tooltipCommand)) { + var formattedText = formatOutputText(stdout) + + // console.log('[commandoutput]', 'stdout', JSON.stringify(stdout)) + // console.log('[commandoutput]', 'format', JSON.stringify(formattedText)) + + if (cmd == config.command) { + widget.outputText = formattedText + } else if (cmd == config.tooltipCommand) { + widget.tooltipText = formattedText + } + + if (config.waitForCompletion) { + timer.restart() + } + } + } + } + + function runCommand() { + // console.log('[commandoutput]', Date.now(), 'runCommand', config.command) + executable.exec(config.command) + executable.exec(config.tooltipCommand) + } + + Timer { + id: timer + interval: config.interval + running: true + repeat: !config.waitForCompletion + onTriggered: widget.runCommand() + // onIntervalChanged: console.log('interval', interval) + // onRunningChanged: console.log('running', running) + // onRepeatChanged: console.log('repeat', repeat) + + Component.onCompleted: { + // Run right away in case the interval is very long. + triggered() + } + } + + Plasmoid.onActivated: widget.performClick() + + Plasmoid.backgroundHints: plasmoid.configuration.showBackground ? PlasmaCore.Types.DefaultBackground : PlasmaCore.Types.NoBackground + + fullRepresentation: Item { + id: panelItem + + readonly property bool isHorizontal: plasmoid.formFactor == PlasmaCore.Types.Horizontal + readonly property bool isVertical: plasmoid.formFactor == PlasmaCore.Types.Vertical + readonly property bool isInPanel: isHorizontal || isVertical + readonly property bool isOnDesktop: !isInPanel + + readonly property int itemWidth: { + if (isOnDesktop) { + return Math.ceil(output.contentWidth) + } else if (isHorizontal && plasmoid.configuration.useFixedWidth) { + return plasmoid.configuration.fixedWidth * Kirigami.Units.devicePixelRatio + } else { // isHorizontal || isVertical + return Math.ceil(output.implicitWidth) + } + } + Layout.minimumWidth: isHorizontal ? itemWidth : -1 + Layout.fillWidth: isVertical + Layout.preferredWidth: itemWidth // Panel widget default + // width: itemWidth // Desktop widget default + // onItemWidthChanged: console.log('itemWidth', itemWidth, 'implicitWidth', output.implicitWidth, 'contentWidth', output.contentWidth) + + readonly property int itemHeight: { + if (isOnDesktop) { + return Math.ceil(output.contentHeight) + } else if (isVertical && plasmoid.configuration.useFixedHeight) { + return plasmoid.configuration.fixedHeight * Kirigami.Units.devicePixelRatio + } else { // isHorizontal || isVertical + return Math.ceil(output.implicitHeight) + } + } + Layout.minimumHeight: isVertical ? itemHeight : -1 + Layout.fillHeight: isHorizontal + Layout.preferredHeight: itemHeight // Panel widget default + // height: itemHeight // Desktop widget default + // onItemHeightChanged: console.log('itemHeight', itemHeight, 'implicitHeight', output.implicitHeight, 'contentHeight', output.contentHeight) + + + // Note MouseArea is below the Text so + // that we don't eat the link clicks. + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: config.clickEnabled + + cursorShape: output.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + + onClicked: { + widget.performClick() + } + + property int wheelDelta: 0 + onWheel: (wheel) => { + var delta = wheel.angleDelta.y || wheel.angleDelta.x + wheelDelta += delta + // Magic number 120 for common "one click" + // See: http://qt-project.org/doc/qt-5/qml-qtquick-wheelevent.html#angleDelta-prop + while (wheelDelta >= 120) { + wheelDelta -= 120 + widget.performMouseWheelUp() + } + while (wheelDelta <= -120) { + wheelDelta += 120 + widget.performMouseWheelDown() + } + wheel.accepted = true + } + } + + Text { + id: output + width: parent.width + height: parent.height + + PlasmaCore.ToolTipArea { + anchors.fill: parent + mainText: widget.tooltipText + enabled: widget.tooltipText + } + + text: widget.outputText + + color: config.textColor + style: config.showOutline ? Text.Outline : Text.Normal + styleColor: config.outlineColor + + linkColor: Kirigami.Theme.linkColor + onLinkActivated: Qt.openUrlExternally(link) + + font.pointSize: plasmoid.configuration.fontSize + font.family: plasmoid.configuration.fontFamily || Kirigami.Theme.defaultFont.family + font.weight: plasmoid.configuration.bold ? Font.Bold : Font.Normal + font.italic: plasmoid.configuration.italic + font.underline: plasmoid.configuration.underline + fontSizeMode: Text.FixedSize + horizontalAlignment: plasmoid.configuration.textAlign + verticalAlignment: plasmoid.configuration.vertAlign + + property bool isFixedWidth: { + if (plasmoid.formFactor == PlasmaCore.Types.Planar) { // Desktop Widget + return true + } else if (plasmoid.formFactor == PlasmaCore.Types.Horizontal) { + return plasmoid.configuration.useFixedWidth + } else if (plasmoid.formFactor == PlasmaCore.Types.Vertical) { + return true + } else { + return false + } + } + elide: Text.ElideRight + wrapMode: isFixedWidth ? Text.Wrap : Text.NoWrap + } + + } + + preferredRepresentation: fullRepresentation +} diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/metadata.json b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/metadata.json new file mode 100644 index 0000000..f8e09a1 --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/metadata.json @@ -0,0 +1,36 @@ +{ + "KPackageStructure": "Plasma/Applet", + "KPlugin": { + "Authors": [ + { + "Email": "zrenfire@gmail.com", + "Name": "Chris Holland" + }, + { + "Email": "aricaldeira@gmail.com", + "Name": "Ari Caldeira" + }, + { + "Email": "oded@geek.co.il", + "Name": "Oded Arbel" + } + ], + "Category": "System Information", + "Description": "Run a command periodically and render its output.", + "Description[bz]": "Ezekuta un komandu a kada X milisegundus, i mòstra a saida/rezuwtadu", + "Description[fr]": "Exécute une commande périodiquement et affiche son retour.", + "Description[nl]": "Voer een opdracht periodiek uit en toon de uitvoer ervan.", + "Description[pt_br]": "Executa um comando a cada X milissegundos, e mostra a saída/resultado", + "Icon": "utilities-terminal", + "Id": "com.github.zren.commandoutput", + "License": "GPL-2.0+", + "Name": "Command Output", + "Name[bz]": "Saida di Komandus", + "Name[fr]": "Command Output", + "Name[nl]": "Opdrachtuitvoer", + "Name[pt_br]": "Saída de Comandos", + "Version": "14", + "Website": "https://github.com/Zren/plasma-applet-commandoutput" + }, + "X-Plasma-API-Minimum-Version": "6.0" +} diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/ReadMe.md b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/ReadMe.md new file mode 100644 index 0000000..f24b36e --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/ReadMe.md @@ -0,0 +1,53 @@ +# Translate + +## Status + +| Locale | Lines | % Done| +|----------|---------|-------| +| Template | 30 | | +| bz | 26/30 | 86% | +| fr | 30/30 | 100% | +| nl | 30/30 | 100% | +| pt_br | 27/30 | 90% | + + +## New Translations + +* Fill out [`template.pot`](template.pot) with your translations then open a [new issue](https://github.com/Zren/plasma-applet-commandoutput/issues/new), name the file `spanish.txt`, attach the txt file to the issue (drag and drop). + +Or if you know how to make a pull request + +* Copy the `template.pot` file and name it your locale's code (Eg: `en`/`de`/`fr`) with the extension `.po`. Then fill out all the `msgstr ""`. +* Your region's locale code can be found at: https://stackoverflow.com/questions/3191664/list-of-all-locales-and-their-short-codes/28357857#28357857 + +## Scripts + +Zren's `kpac` script can easily run the `gettext` commands for you, parsing the `metadata.json` and filling out any placeholders for you. `kpac` can be [downloaded here](https://github.com/Zren/plasma-applet-lib/blob/master/kpac) and should be placed at `~/Code/plasmoid-widgetname/kpac` to edit translations at `~/Code/plasmoid-widgetname/package/translate/`. + + +* `python3 ./kpac i18n` will parse the `i18n()` calls in the `*.qml` files and write it to the `template.pot` file. Then it will merge any changes into the `*.po` language files. Then it converts the `*.po` files to it's binary `*.mo` version and move it to `contents/locale/...` which will bundle the translations in the `*.plasmoid` without needing the user to manually install them. +* `python3 ./kpac localetest` will convert the `.po` to the `*.mo` files then run `plasmoidviewer` (part of `plasma-sdk`). + +## How it works + +Since KDE Frameworks v5.37, translations can be bundled with the zipped `*.plasmoid` file downloaded from the store. + +* `xgettext` extracts the messages from the source code into a `template.pot`. +* Translators copy the `template.pot` to `fr.po` to translate the French language. +* When the source code is updated, we use `msgmerge` to update the `fr.po` based on the updated `template.pot`. +* When testing or releasing the widget, we convert the `.po` files to their binary `.mo` form with `msgfmt`. + +The binary `.mo` translation files are placed in `package/contents/locale/` so you may want to add `*.mo` to your `.gitignore`. + +``` +package/contents/locale/fr/LC_MESSAGES/plasma_applet_com.github.zren.commandoutput.mo +``` + +## Links + +* https://develop.kde.org/docs/plasma/widget/translations-i18n/ +* https://l10n.kde.org/stats/gui/trunk-kf5/team/fr/plasma-desktop/ +* https://techbase.kde.org/Development/Tutorials/Localization/i18n_Build_Systems +* https://api.kde.org/frameworks/ki18n/html/prg_guide.html + +> Version 8 of [Zren's i18n scripts](https://github.com/Zren/plasma-applet-lib). diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/bz.po b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/bz.po new file mode 100644 index 0000000..4c1ca45 --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/bz.po @@ -0,0 +1,166 @@ +# Translation of commandoutput in fr +# Copyright (C) 2020 +# This file is distributed under the same license as the commandoutput package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: commandoutput\n" +"Report-Msgid-Bugs-To: https://github.com/Zren/plasma-applet-commandoutput\n" +"POT-Creation-Date: 2024-03-12 16:30-0400\n" +"PO-Revision-Date: 2020-10-05 21:55+020\n" +"Last-Translator: Arí KAWDERA \n" +"Language-Team: \n" +"Language: bz_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../metadata.json +msgid "Command Output" +msgstr "Saida di Komandus" + +#: ../metadata.json +msgid "Run a command periodically and render its output." +msgstr "Ezekuta un komandu a kada X milisegundus, i mòstra a saida/rezuwtadu" + +#: ../contents/config/config.qml ../contents/ui/config/ConfigCommand.qml +msgid "Command" +msgstr "Komandu" + +#: ../contents/config/config.qml +msgid "Appearance" +msgstr "Aparensya" + +#: ../contents/config/config.qml +msgid "Actions" +msgstr "Asoyns" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Tooltip" +msgstr "" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Hover Command:" +msgstr "" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Click" +msgstr "Kliki" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Run Command:" +msgstr "Rodar u komandu:" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Mouse Wheel" +msgstr "Rodiỹa du mawzi" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Scroll Up:" +msgstr "Rolar pra sima:" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Scroll Down:" +msgstr "Rolar pra baxu:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Font" +msgstr "Fonti" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Font Family:" +msgstr "Familya:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Font Size:" +msgstr "Tamaỹu:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "px" +msgstr "px" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Colors" +msgstr "Koris" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Text:" +msgstr "Testu:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Outline:" +msgstr "Kontornu:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Misc" +msgstr "Divèrsus" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Desktop Widget:" +msgstr "Widigèti pru dèskitòpi:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Show background" +msgstr "nuwn dexar transparenti" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Fixed Width:" +msgstr "Largura fiksa:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Fixed Height:" +msgstr "Awtura fiksa:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Replace all newlines with spaces" +msgstr "Junta as liỹas numa liỹa sò, separandu kun espasus" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "Run every " +msgstr "Rodar a kada " + +#: ../contents/ui/config/ConfigCommand.qml +msgid "ms" +msgstr "" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "Wait for completion" +msgstr "Esperar a ezekusawn terminar" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "Enabled" +msgstr "" + +#: ../contents/ui/libconfig/FontFamily.qml +msgctxt "Use default font" +msgid "Default" +msgstr "Padrawn" + +#~ msgid "Version: %1" +#~ msgstr "Versawn: %1" + +#~ msgid "Mouse Click" +#~ msgstr "Kliki du mawzi" + +#~ msgid "Command:" +#~ msgstr "Komandu:" + +#~ msgid "pt" +#~ msgstr "pt" + +#~ msgid "Desktop Widget: Show background" +#~ msgstr "Widigèti pru dèskitòpi: nuwn dexar transparenti" + +#~ msgid "Tooltip Command" +#~ msgstr "Komandu pra pasajen du mawzi" + +#~ msgid "Run" +#~ msgstr "Ezekusawn" + +#~ msgid "Tooltip command" +#~ msgstr "Komandu pra pasajen du mawzi" + +#~ msgid "General" +#~ msgstr "Jeraw" diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/fr.po b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/fr.po new file mode 100644 index 0000000..6748c7a --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/fr.po @@ -0,0 +1,148 @@ +# Translation of commandoutput in fr +# Copyright (C) 2020 +# This file is distributed under the same license as the commandoutput package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: commandoutput\n" +"Report-Msgid-Bugs-To: https://github.com/Zren/plasma-applet-commandoutput\n" +"POT-Creation-Date: 2024-03-12 16:30-0400\n" +"PO-Revision-Date: 2024-03-14 09:56+020\n" +"Last-Translator: Sasha MOREL \n" +"Language-Team: \n" +"Language: fr_FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../metadata.json +msgid "Command Output" +msgstr "Command Output" + +#: ../metadata.json +msgid "Run a command periodically and render its output." +msgstr "Exécute une commande périodiquement et affiche son retour." + +#: ../contents/config/config.qml ../contents/ui/config/ConfigCommand.qml +msgid "Command" +msgstr "Commande" + +#: ../contents/config/config.qml +msgid "Appearance" +msgstr "Apparence" + +#: ../contents/config/config.qml +msgid "Actions" +msgstr "Actions" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Tooltip" +msgstr "Infobulle" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Hover Command:" +msgstr "Commande au survol :" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Click" +msgstr "Clic" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Run Command:" +msgstr "Lancer la commande :" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Mouse Wheel" +msgstr "Molette de souris" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Scroll Up:" +msgstr "Défilement vers le haut :" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Scroll Down:" +msgstr "Défilement vers le bas :" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Font" +msgstr "Police" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Font Family:" +msgstr "Famille :" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Font Size:" +msgstr "Taille :" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "px" +msgstr "px" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Colors" +msgstr "Couleurs" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Text:" +msgstr "Texte :" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Outline:" +msgstr "Contours :" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Misc" +msgstr "Divers" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Desktop Widget:" +msgstr "Pour le widget de bureau :" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Show background" +msgstr "afficher un fond" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Fixed Width:" +msgstr "Largeur fixe :" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Fixed Height:" +msgstr "Hauteur fixe :" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Replace all newlines with spaces" +msgstr "Remplacer les sauts de lignes par des espaces" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "Run every " +msgstr "Exécuter toutes les " + +#: ../contents/ui/config/ConfigCommand.qml +msgid "ms" +msgstr " ms" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "Wait for completion" +msgstr "Attendre la fin de l'éxécution" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "Enabled" +msgstr "Activé" + +#: ../contents/ui/libconfig/FontFamily.qml +msgctxt "Use default font" +msgid "Default" +msgstr "Par défaut" + +#~ msgid "Version: %1" +#~ msgstr "Version : %1" + +#~ msgid "Desktop Widget: Show background" +#~ msgstr "Pour le widget de bureau : afficher un fond" + +#~ msgid "General" +#~ msgstr "Général" diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/nl.po b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/nl.po new file mode 100644 index 0000000..c44df0c --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/nl.po @@ -0,0 +1,149 @@ +# Translation of commandoutput in nl_NL +# Copyright (C) 2019 +# This file is distributed under the same license as the commandoutput package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: commandoutput\n" +"Report-Msgid-Bugs-To: https://github.com/Zren/plasma-applet-commandoutput\n" +"POT-Creation-Date: 2024-03-12 16:30-0400\n" +"PO-Revision-Date: 2024-03-14 10:27+0100\n" +"Last-Translator: Heimen Stoffels \n" +"Language-Team: Dutch \n" +"Language: nl_NL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.4.2\n" + +#: ../metadata.json +msgid "Command Output" +msgstr "Opdrachtuitvoer" + +#: ../metadata.json +msgid "Run a command periodically and render its output." +msgstr "Voer een opdracht periodiek uit en toon de uitvoer ervan." + +#: ../contents/config/config.qml ../contents/ui/config/ConfigCommand.qml +msgid "Command" +msgstr "Opdracht" + +#: ../contents/config/config.qml +msgid "Appearance" +msgstr "Vormgeving" + +#: ../contents/config/config.qml +msgid "Actions" +msgstr "Acties" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Tooltip" +msgstr "Hulpballon" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Hover Command:" +msgstr "Opdracht na aanwijzen:" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Click" +msgstr "Klikken" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Run Command:" +msgstr "Opdracht uitvoeren:" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Mouse Wheel" +msgstr "Muiswiel" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Scroll Up:" +msgstr "Omhoog scrollen:" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Scroll Down:" +msgstr "Omlaag scrollen:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Font" +msgstr "Lettertype" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Font Family:" +msgstr "Lettertype:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Font Size:" +msgstr "Tekstgrootte:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "px" +msgstr "px" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Colors" +msgstr "Kleuren" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Text:" +msgstr "Tekst:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Outline:" +msgstr "Contour:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Misc" +msgstr "Overig" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Desktop Widget:" +msgstr "Bureaubladwidget:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Show background" +msgstr "Achtergrond tonen" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Fixed Width:" +msgstr "Vaste breedte:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Fixed Height:" +msgstr "Vaste hoogte:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Replace all newlines with spaces" +msgstr "Alle witregels vervangen door spaties" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "Run every " +msgstr "Uitvoeren, elke " + +#: ../contents/ui/config/ConfigCommand.qml +msgid "ms" +msgstr "ms" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "Wait for completion" +msgstr "Wachten tot opdracht is afgerond" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "Enabled" +msgstr "Ingeschakeld" + +#: ../contents/ui/libconfig/FontFamily.qml +msgctxt "Use default font" +msgid "Default" +msgstr "Standaard" + +#~ msgid "Version: %1" +#~ msgstr "Versie: %1" + +#~ msgid "Desktop Widget: Show background" +#~ msgstr "Bureaubladwidget: achtergrond tonen" + +#~ msgid "General" +#~ msgstr "Algemeen" diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/pt_br.po b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/pt_br.po new file mode 100644 index 0000000..dfc6e6b --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/pt_br.po @@ -0,0 +1,166 @@ +# Translation of commandoutput in fr +# Copyright (C) 2020 +# This file is distributed under the same license as the commandoutput package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: commandoutput\n" +"Report-Msgid-Bugs-To: https://github.com/Zren/plasma-applet-commandoutput\n" +"POT-Creation-Date: 2024-03-12 16:30-0400\n" +"PO-Revision-Date: 2020-10-05 21:55+020\n" +"Last-Translator: Ari CALDEIRA \n" +"Language-Team: \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../metadata.json +msgid "Command Output" +msgstr "Saída de Comandos" + +#: ../metadata.json +msgid "Run a command periodically and render its output." +msgstr "Executa um comando a cada X milissegundos, e mostra a saída/resultado" + +#: ../contents/config/config.qml ../contents/ui/config/ConfigCommand.qml +msgid "Command" +msgstr "Comando" + +#: ../contents/config/config.qml +msgid "Appearance" +msgstr "Aparência" + +#: ../contents/config/config.qml +msgid "Actions" +msgstr "Ações" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Tooltip" +msgstr "" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Hover Command:" +msgstr "" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Click" +msgstr "Clique" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Run Command:" +msgstr "Rodar o comando:" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Mouse Wheel" +msgstr "Rodinha do mouse" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Scroll Up:" +msgstr "Rolar para cima:" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Scroll Down:" +msgstr "Rolar para baixo:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Font" +msgstr "Fonte" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Font Family:" +msgstr "Família:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Font Size:" +msgstr "Tamanho:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "px" +msgstr "px" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Colors" +msgstr "Cores" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Text:" +msgstr "Texto:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Outline:" +msgstr "Contorno:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Misc" +msgstr "Diversos" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Desktop Widget:" +msgstr "Widget para Desktop:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Show background" +msgstr "não deixa transparente" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Fixed Width:" +msgstr "Largura fixa:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Fixed Height:" +msgstr "Altura fixa:" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Replace all newlines with spaces" +msgstr "Junta as linhas numa linha só, separando com espaços" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "Run every " +msgstr "Executar a cada " + +#: ../contents/ui/config/ConfigCommand.qml +msgid "ms" +msgstr "ms" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "Wait for completion" +msgstr "Esperar a execução terminar" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "Enabled" +msgstr "" + +#: ../contents/ui/libconfig/FontFamily.qml +msgctxt "Use default font" +msgid "Default" +msgstr "Padrão" + +#~ msgid "Version: %1" +#~ msgstr "Versão: %1" + +#~ msgid "Mouse Click" +#~ msgstr "Clique do mouse" + +#~ msgid "Command:" +#~ msgstr "Comando:" + +#~ msgid "pt" +#~ msgstr "pt" + +#~ msgid "Desktop Widget: Show background" +#~ msgstr "Widget para Desktop: não deixa transparente" + +#~ msgid "Tooltip Command" +#~ msgstr "Comando para a passagem do mouse" + +#~ msgid "Run" +#~ msgstr "Execução" + +#~ msgid "Tooltip command" +#~ msgstr "Comando para a passagem do mouse" + +#~ msgid "General" +#~ msgstr "Geral" diff --git a/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/template.pot b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/template.pot new file mode 100644 index 0000000..a20c88b --- /dev/null +++ b/dot_local/share/plasma/plasmoids/com.github.zren.commandoutput/translate/template.pot @@ -0,0 +1,139 @@ +# Translation of commandoutput in LANGUAGE +# Copyright (C) 2024 +# This file is distributed under the same license as the commandoutput package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: commandoutput\n" +"Report-Msgid-Bugs-To: https://github.com/Zren/plasma-applet-commandoutput\n" +"POT-Creation-Date: 2024-03-12 16:30-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../metadata.json +msgid "Command Output" +msgstr "" + +#: ../metadata.json +msgid "Run a command periodically and render its output." +msgstr "" + +#: ../contents/config/config.qml ../contents/ui/config/ConfigCommand.qml +msgid "Command" +msgstr "" + +#: ../contents/config/config.qml +msgid "Appearance" +msgstr "" + +#: ../contents/config/config.qml +msgid "Actions" +msgstr "" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Tooltip" +msgstr "" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Hover Command:" +msgstr "" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Click" +msgstr "" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Run Command:" +msgstr "" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Mouse Wheel" +msgstr "" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Scroll Up:" +msgstr "" + +#: ../contents/ui/config/ConfigActions.qml +msgid "Scroll Down:" +msgstr "" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Font" +msgstr "" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Font Family:" +msgstr "" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Font Size:" +msgstr "" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "px" +msgstr "" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Colors" +msgstr "" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Text:" +msgstr "" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Outline:" +msgstr "" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Misc" +msgstr "" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Desktop Widget:" +msgstr "" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Show background" +msgstr "" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Fixed Width:" +msgstr "" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Fixed Height:" +msgstr "" + +#: ../contents/ui/config/ConfigAppearance.qml +msgid "Replace all newlines with spaces" +msgstr "" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "Run every " +msgstr "" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "ms" +msgstr "" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "Wait for completion" +msgstr "" + +#: ../contents/ui/config/ConfigCommand.qml +msgid "Enabled" +msgstr "" + +#: ../contents/ui/libconfig/FontFamily.qml +msgctxt "Use default font" +msgid "Default" +msgstr "" diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/code/parser.js b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/code/parser.js new file mode 100644 index 0000000..0caeead --- /dev/null +++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/code/parser.js @@ -0,0 +1,548 @@ +// Parsing and formatting helpers for the CodexBar plasmoid. +// This file is imported both from QML (import "../code/parser.js" as Parser) +// and from Node.js unit tests (tests/parser.test.mjs), so it must stay +// engine-neutral: no QML pragmas, no Node-only APIs. + +// Catalog captured from `codexbar config providers` (CodexBar CLI v0.42.1). +var PROVIDER_CATALOG = [ + { id: "codex", name: "Codex" }, + { id: "openai", name: "OpenAI" }, + { id: "azureopenai", name: "Azure OpenAI" }, + { id: "claude", name: "Claude" }, + { id: "cursor", name: "Cursor" }, + { id: "opencode", name: "OpenCode" }, + { id: "opencodego", name: "OpenCode Go" }, + { id: "alibaba", name: "Alibaba" }, + { id: "alibabatokenplan", name: "Alibaba Token Plan" }, + { id: "factory", name: "Droid" }, + { id: "gemini", name: "Gemini" }, + { id: "antigravity", name: "Antigravity" }, + { id: "copilot", name: "Copilot" }, + { id: "devin", name: "Devin" }, + { id: "zai", name: "z.ai" }, + { id: "minimax", name: "MiniMax" }, + { id: "manus", name: "Manus" }, + { id: "kimi", name: "Kimi" }, + { id: "kilo", name: "Kilo" }, + { id: "kiro", name: "Kiro" }, + { id: "vertexai", name: "Vertex AI" }, + { id: "augment", name: "Augment" }, + { id: "jetbrains", name: "JetBrains AI" }, + { id: "kimik2", name: "Kimi K2 (unofficial)" }, + { id: "moonshot", name: "Moonshot / Kimi API" }, + { id: "amp", name: "Amp" }, + { id: "t3chat", name: "T3 Chat" }, + { id: "ollama", name: "Ollama" }, + { id: "synthetic", name: "Synthetic" }, + { id: "warp", name: "Warp" }, + { id: "openrouter", name: "OpenRouter" }, + { id: "elevenlabs", name: "ElevenLabs" }, + { id: "windsurf", name: "Windsurf" }, + { id: "zed", name: "Zed" }, + { id: "perplexity", name: "Perplexity" }, + { id: "mimo", name: "Xiaomi MiMo" }, + { id: "doubao", name: "Doubao" }, + { id: "sakana", name: "Sakana AI" }, + { id: "abacus", name: "Abacus AI" }, + { id: "mistral", name: "Mistral" }, + { id: "deepseek", name: "DeepSeek" }, + { id: "codebuff", name: "Codebuff" }, + { id: "crof", name: "Crof" }, + { id: "venice", name: "Venice" }, + { id: "commandcode", name: "Command Code" }, + { id: "qoder", name: "Qoder" }, + { id: "stepfun", name: "StepFun" }, + { id: "bedrock", name: "AWS Bedrock" }, + { id: "grok", name: "Grok" }, + { id: "groq", name: "Groq" }, + { id: "llmproxy", name: "LLM Proxy" }, + { id: "litellm", name: "LiteLLM" }, + { id: "deepgram", name: "Deepgram" }, + { id: "poe", name: "Poe" }, + { id: "chutes", name: "Chutes" }, + { id: "crossmodel", name: "CrossModel" }, + { id: "clawrouter", name: "ClawRouter" }, + { id: "wayfinder", name: "Wayfinder" }, +]; + +var SHORT_NAMES = { + codex: "Cx", + openai: "OA", + azureopenai: "Az", + claude: "Cl", + cursor: "Cu", + gemini: "Gm", + copilot: "Cp", + antigravity: "Ag", + openrouter: "OR", + minimax: "MM", + deepseek: "DS", + jetbrains: "JB", + vertexai: "Vx", + bedrock: "BR", + litellm: "LL", + llmproxy: "LP", + groq: "Gq", + grok: "Gk", +}; + +function providerDisplayName(id) { + for (var i = 0; i < PROVIDER_CATALOG.length; i++) { + if (PROVIDER_CATALOG[i].id === id) { + return PROVIDER_CATALOG[i].name; + } + } + return id; +} + +function shortName(id) { + if (SHORT_NAMES[id]) { + return SHORT_NAMES[id]; + } + if (!id || id.length === 0) { + return "?"; + } + var base = id.slice(0, 2); + return base.charAt(0).toUpperCase() + base.slice(1).toLowerCase(); +} + +function windowLabel(windowMinutes) { + if (windowMinutes === null || windowMinutes === undefined) { + return "Usage"; + } + if (windowMinutes === 10080) { + return "Weekly"; + } + if (windowMinutes === 43200) { + return "Monthly"; + } + if (windowMinutes === 1440) { + return "Daily"; + } + if (windowMinutes >= 180 && windowMinutes <= 360) { + return "Session"; + } + if (windowMinutes % 1440 === 0) { + return (windowMinutes / 1440) + "d"; + } + if (windowMinutes % 60 === 0) { + return (windowMinutes / 60) + "h"; + } + return windowMinutes + "m"; +} + +function usageStage(usedPercent) { + if (usedPercent >= 90) { + return "crit"; + } + if (usedPercent >= 70) { + return "warn"; + } + return "ok"; +} + +function formatCountdown(resetsAtIso, nowMs) { + if (!resetsAtIso) { + return ""; + } + var target = Date.parse(resetsAtIso); + if (isNaN(target)) { + return ""; + } + var diffMinutes = Math.floor((target - nowMs) / 60000); + if (diffMinutes <= 0) { + return "now"; + } + var days = Math.floor(diffMinutes / 1440); + var hours = Math.floor((diffMinutes % 1440) / 60); + var minutes = diffMinutes % 60; + if (days > 0) { + return days + "d " + hours + "h"; + } + if (hours > 0) { + return hours + "h " + minutes + "m"; + } + return minutes + "m"; +} + +function makeWindow(key, raw, pace, labelOverride, usageKnown) { + var paceSummary = ""; + if (pace && pace[key] && pace[key].summary) { + paceSummary = pace[key].summary; + } + var minutes = raw.windowMinutes === undefined ? null : raw.windowMinutes; + return { + key: key, + label: labelOverride || windowLabel(minutes), + windowMinutes: minutes, + usedPercent: Math.round(raw.usedPercent || 0), + resetsAt: raw.resetsAt || null, + resetDescription: raw.resetDescription || "", + paceSummary: paceSummary, + usageKnown: usageKnown === undefined ? true : usageKnown, + }; +} + +function payloadToModel(payload) { + var usage = payload.usage || null; + var windows = []; + if (usage) { + var keys = ["primary", "secondary", "tertiary"]; + for (var i = 0; i < keys.length; i++) { + var raw = usage[keys[i]]; + if (!raw || raw.isSyntheticPlaceholder) { + continue; + } + windows.push(makeWindow(keys[i], raw, payload.pace || null)); + } + // Named model-specific lanes (e.g. Codex Spark, Code review). + var extras = usage.extraRateWindows || []; + for (var j = 0; j < extras.length; j++) { + var extra = extras[j]; + if (!extra || !extra.window || extra.window.isSyntheticPlaceholder) { + continue; + } + windows.push(makeWindow( + "extra:" + (extra.id || j), + extra.window, + null, + extra.title || extra.id || "Extra", + extra.usageKnown === undefined ? true : extra.usageKnown)); + } + } + // OpenAI web dashboard exposes code review as a bare remaining percent. + var dashboard = payload.openaiDashboard || null; + if (dashboard && typeof dashboard.codeReviewRemainingPercent === "number") { + var hasReviewLane = windows.some(function(w) { + return w.label.toLowerCase().indexOf("code review") !== -1; + }); + if (!hasReviewLane) { + windows.push(makeWindow( + "extra:code-review", + { usedPercent: 100 - dashboard.codeReviewRemainingPercent, windowMinutes: null, resetsAt: null }, + null, + "Code review", + true)); + } + } + var account = null; + if (usage && usage.identity && usage.identity.accountEmail) { + account = usage.identity.accountEmail; + } else if (usage && usage.accountEmail) { + account = usage.accountEmail; + } else if (payload.account) { + account = payload.account; + } + var plan = null; + if (usage && usage.identity && usage.identity.loginMethod) { + plan = usage.identity.loginMethod; + } else if (usage && usage.loginMethod) { + plan = usage.loginMethod; + } + var credits = null; + if (payload.credits && typeof payload.credits.remaining === "number") { + credits = payload.credits.remaining; + } + var status = null; + if (payload.status && payload.status.indicator && payload.status.indicator !== "none") { + status = { + indicator: payload.status.indicator, + description: payload.status.description || "", + }; + } + return { + id: payload.provider || "?", + name: providerDisplayName(payload.provider || "?"), + source: payload.source || "", + version: payload.version || "", + account: account, + plan: plan, + credits: credits, + status: status, + error: payload.error ? String(payload.error.message || "error") : null, + windows: windows, + updatedAt: usage && usage.updatedAt ? usage.updatedAt : null, + }; +} + +function parseUsageJson(text) { + var parsed; + try { + parsed = JSON.parse(text); + } catch (e) { + return { ok: false, error: "Invalid JSON from codexbar: " + e.message, models: [] }; + } + if (!Array.isArray(parsed)) { + return { ok: false, error: "Unexpected codexbar payload (expected array)", models: [] }; + } + var models = []; + for (var i = 0; i < parsed.length; i++) { + models.push(payloadToModel(parsed[i])); + } + return { ok: true, error: "", models: models }; +} + +function worstUsedPercent(model) { + var worst = -1; + for (var i = 0; i < model.windows.length; i++) { + if (model.windows[i].usedPercent > worst) { + worst = model.windows[i].usedPercent; + } + } + return worst; +} + +// Ring assignment for the radial gauge: outer ring shows the short +// (session/daily) window, inner ring the long (weekly/monthly) window. +function gaugeRings(model) { + var outerIdx = -1; + var innerIdx = -1; + var firstKnownIdx = -1; + for (var i = 0; i < model.windows.length; i++) { + if (model.windows[i].usageKnown === false) { + continue; + } + if (firstKnownIdx === -1) { + firstKnownIdx = i; + } + var minutes = model.windows[i].windowMinutes; + if (minutes !== null && minutes !== undefined && minutes <= 1440 && outerIdx === -1) { + outerIdx = i; + } else if (minutes !== null && minutes !== undefined && minutes >= 10080 && innerIdx === -1) { + innerIdx = i; + } + } + if (outerIdx === -1 && innerIdx === -1 && firstKnownIdx !== -1) { + outerIdx = firstKnownIdx; + } + return { outerIdx: outerIdx, innerIdx: innerIdx }; +} + +function gaugeCenterPercent(model) { + var rings = gaugeRings(model); + var idx = rings.outerIdx !== -1 ? rings.outerIdx : rings.innerIdx; + if (idx === -1) { + return -1; + } + return Math.max(0, Math.min(100, model.windows[idx].usedPercent)); +} + +function sweepAngle(percent) { + var clamped = Math.max(0, Math.min(100, percent)); + return clamped * 3.6; +} + +// Local token-cost scan payloads from `codexbar cost --format json`. +function parseCostJson(text) { + var parsed; + try { + parsed = JSON.parse(text); + } catch (e) { + return { ok: false, error: "Invalid cost JSON: " + e.message, byId: {} }; + } + if (!Array.isArray(parsed)) { + return { ok: false, error: "Unexpected cost payload (expected array)", byId: {} }; + } + var byId = {}; + for (var i = 0; i < parsed.length; i++) { + var p = parsed[i]; + if (!p || !p.provider) { + continue; + } + var daily = []; + var rawDaily = p.daily || []; + for (var j = 0; j < rawDaily.length; j++) { + daily.push({ + date: rawDaily[j].date || "", + totalCost: Number(rawDaily[j].totalCost) || 0, + totalTokens: Number(rawDaily[j].totalTokens) || 0, + }); + } + byId[p.provider] = { + todayCostUSD: typeof p.sessionCostUSD === "number" ? p.sessionCostUSD : null, + todayTokens: typeof p.sessionTokens === "number" ? p.sessionTokens : null, + month30CostUSD: typeof p.last30DaysCostUSD === "number" ? p.last30DaysCostUSD : null, + month30Tokens: typeof p.last30DaysTokens === "number" ? p.last30DaysTokens : null, + daily: daily, + }; + } + return { ok: true, error: "", byId: byId }; +} + +function humanTokens(value) { + if (value === null || value === undefined || !isFinite(value)) { + return ""; + } + var n = Number(value); + if (n < 1000) { + return String(Math.round(n)); + } + if (n < 1000000) { + return (Math.round(n / 100) / 10) + "K"; + } + if (n < 1000000000) { + return (Math.round(n / 100000) / 10) + "M"; + } + return (Math.round(n / 100000000) / 10) + "B"; +} + +function formatMoney(value) { + if (value === null || value === undefined || !isFinite(value)) { + return ""; + } + return "$" + Number(value).toFixed(2); +} + +// Zero-filled per-day cost series for the history chart, ending at todayIso. +function chartSeries(daily, days, todayIso) { + var byDate = {}; + for (var i = 0; i < (daily || []).length; i++) { + byDate[daily[i].date] = daily[i]; + } + var series = []; + var today = new Date(todayIso + "T00:00:00Z"); + for (var d = days - 1; d >= 0; d--) { + var day = new Date(today.getTime() - d * 86400000); + var iso = day.toISOString().slice(0, 10); + var entry = byDate[iso]; + series.push({ + date: iso, + value: entry ? entry.totalCost : 0, + tokens: entry ? entry.totalTokens : 0, + }); + } + return series; +} + +function shellQuote(value) { + return "'" + String(value).replace(/'/g, "'\"'\"'") + "'"; +} + +function commandPrefix(proxyUrl) { + // ~/.local/bin is not always on plasmashell's PATH. + var prefix = 'env PATH="$HOME/.local/bin:$PATH"'; + if (proxyUrl && proxyUrl.trim().length > 0) { + var quoted = shellQuote(proxyUrl.trim()); + prefix += " http_proxy=" + quoted + " https_proxy=" + quoted + + " HTTP_PROXY=" + quoted + " HTTPS_PROXY=" + quoted + + " ALL_PROXY=" + quoted; + } + return prefix; +} + +// One usage command per selected provider; a single unscoped command when the +// selection is empty (the CLI then reports the providers enabled in its own +// config file). +function buildCommands(codexbarPath, selected, proxyUrl) { + var base = commandPrefix(proxyUrl) + " " + codexbarPath + " usage --format json --no-color"; + if (!selected || selected.length === 0) { + return [base]; + } + return selected.map(function(id) { + return base + " --provider " + id; + }); +} + +function buildCostCommand(codexbarPath, proxyUrl) { + return commandPrefix(proxyUrl) + " " + codexbarPath + " cost --format json --no-color"; +} + +// Merge freshly parsed provider models into the cached map. Successful +// fetches replace the cache entry; errors keep the last good data and only +// mark it stale, so panel gauges never blank out during background refreshes. +function applyUpdate(prevById, models, nowMs) { + var byId = {}; + var key; + for (key in prevById) { + byId[key] = prevById[key]; + } + for (var i = 0; i < models.length; i++) { + var model = models[i]; + var cached = byId[model.id]; + if (model.error && cached && cached.windows && cached.windows.length > 0) { + var stale = {}; + for (key in cached) { + stale[key] = cached[key]; + } + stale.staleError = model.error; + byId[model.id] = stale; + continue; + } + model.fetchedAtMs = nowMs; + model.staleError = null; + byId[model.id] = model; + } + return { byId: byId }; +} + +function formatAgo(thenMs, nowMs) { + if (thenMs === null || thenMs === undefined || thenMs < 0) { + return ""; + } + var minutes = Math.floor((nowMs - thenMs) / 60000); + if (minutes < 1) { + return "just now"; + } + if (minutes < 60) { + return minutes + "m ago"; + } + var hours = Math.floor(minutes / 60); + if (hours < 24) { + return hours + "h " + (minutes % 60) + "m ago"; + } + var days = Math.floor(hours / 24); + return days + "d " + (hours % 24) + "h ago"; +} + +function selectionList(csv) { + if (!csv) { + return []; + } + var out = []; + var parts = csv.split(","); + for (var i = 0; i < parts.length; i++) { + var id = parts[i].trim(); + if (id.length > 0) { + out.push(id); + } + } + return out; +} + +function toggleSelection(csv, id, checked) { + var list = selectionList(csv); + var index = list.indexOf(id); + if (checked && index === -1) { + list.push(id); + } else if (!checked && index !== -1) { + list.splice(index, 1); + } + return list.join(","); +} + +// Node.js test hook; ignored by the QML JS engine. +if (typeof module !== "undefined" && module.exports) { + module.exports = { + PROVIDER_CATALOG: PROVIDER_CATALOG, + providerDisplayName: providerDisplayName, + shortName: shortName, + windowLabel: windowLabel, + usageStage: usageStage, + formatCountdown: formatCountdown, + parseUsageJson: parseUsageJson, + worstUsedPercent: worstUsedPercent, + selectionList: selectionList, + toggleSelection: toggleSelection, + gaugeRings: gaugeRings, + gaugeCenterPercent: gaugeCenterPercent, + sweepAngle: sweepAngle, + applyUpdate: applyUpdate, + formatAgo: formatAgo, + parseCostJson: parseCostJson, + humanTokens: humanTokens, + formatMoney: formatMoney, + chartSeries: chartSeries, + buildCommands: buildCommands, + buildCostCommand: buildCostCommand, + }; +} diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/config/config.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/config/config.qml new file mode 100644 index 0000000..11e1dc9 --- /dev/null +++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/config/config.qml @@ -0,0 +1,15 @@ +import QtQuick +import org.kde.plasma.configuration + +ConfigModel { + ConfigCategory { + name: i18n("General") + icon: "configure" + source: "ConfigGeneral.qml" + } + ConfigCategory { + name: i18n("Providers") + icon: "applications-development" + source: "ConfigProviders.qml" + } +} diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/config/main.xml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/config/main.xml new file mode 100644 index 0000000..7a4032c --- /dev/null +++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/config/main.xml @@ -0,0 +1,41 @@ + + + + + + + codexbar + + + + 5 + + + + true + + + + true + + + + true + + + + + + + + codex,claude + + + + + + + diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/icons/codexbar.svg b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/icons/codexbar.svg new file mode 100644 index 0000000..e4ebf74 --- /dev/null +++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/icons/codexbar.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/CompactRepresentation.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/CompactRepresentation.qml new file mode 100644 index 0000000..4c8b19f --- /dev/null +++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/CompactRepresentation.qml @@ -0,0 +1,65 @@ +import QtQuick +import QtQuick.Layouts +import org.kde.plasma.plasmoid +import org.kde.plasma.core as PlasmaCore +import org.kde.kirigami as Kirigami +import "../code/parser.js" as Parser + +// Panel view: one two-ring radial gauge per provider +// (outer ring = session left, inner ring = weekly left). +Item { + id: compact + + readonly property bool vertical: Plasmoid.formFactor === PlasmaCore.Types.Vertical + readonly property int gaugeSize: compact.vertical + ? Math.min(compact.width, Kirigami.Units.gridUnit * 2.4) + : Math.min(compact.height, Kirigami.Units.gridUnit * 2.4) + readonly property int count: Math.max(1, root.models.length) + + Layout.minimumWidth: compact.vertical ? compact.gaugeSize : compact.count * (compact.gaugeSize + gaugeGrid.spacing) + Layout.minimumHeight: compact.vertical ? compact.count * (compact.gaugeSize + gaugeGrid.spacing) : compact.gaugeSize + + Grid { + id: gaugeGrid + anchors.centerIn: parent + columns: compact.vertical ? 1 : compact.count + spacing: Kirigami.Units.smallSpacing + + Kirigami.Icon { + visible: root.models.length === 0 + source: Qt.resolvedUrl("../icons/codexbar.svg") + width: compact.gaugeSize + height: compact.gaugeSize + opacity: root.loading ? 0.5 : 1 + } + + Repeater { + model: root.models + + delegate: RadialGauge { + id: meter + required property var modelData + readonly property var rings: Parser.gaugeRings(modelData) + readonly property int centerPercent: Parser.gaugeCenterPercent(modelData) + + width: compact.gaugeSize + height: compact.gaugeSize + outerColor: root.sessionColor + innerColor: root.weeklyColor + outerPercent: rings.outerIdx >= 0 + ? modelData.windows[rings.outerIdx].usedPercent : -1 + innerPercent: rings.innerIdx >= 0 + ? modelData.windows[rings.innerIdx].usedPercent : -1 + centerText: modelData.error + ? "!" + : (meter.centerPercent >= 0 ? String(meter.centerPercent) : "") + centerTextScale: 0.34 + } + } + } + + MouseArea { + anchors.fill: parent + onClicked: root.expanded = !root.expanded + } +} diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/ConfigGeneral.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/ConfigGeneral.qml new file mode 100644 index 0000000..802d2cb --- /dev/null +++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/ConfigGeneral.qml @@ -0,0 +1,70 @@ +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +Kirigami.FormLayout { + id: page + + property alias cfg_codexbarPath: pathField.text + property alias cfg_refreshMinutes: refreshSpin.value + property alias cfg_showPace: paceCheck.checked + property alias cfg_showCost: costCheck.checked + property alias cfg_showHistory: historyCheck.checked + property alias cfg_proxyUrl: proxyField.text + + QQC2.TextField { + id: pathField + Kirigami.FormData.label: i18n("codexbar binary:") + placeholderText: "codexbar" + Layout.fillWidth: true + } + + QQC2.Label { + text: i18n("Plain name is resolved via PATH (including ~/.local/bin); an absolute path also works.") + font.pointSize: Kirigami.Theme.smallFont.pointSize + opacity: 0.6 + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + QQC2.SpinBox { + id: refreshSpin + Kirigami.FormData.label: i18n("Refresh every:") + from: 1 + to: 120 + textFromValue: function(value) { return i18np("%1 minute", "%1 minutes", value) } + valueFromText: function(text) { return parseInt(text) || 5 } + } + + QQC2.CheckBox { + id: paceCheck + Kirigami.FormData.label: i18n("Details:") + text: i18n("Show pace summaries") + } + + QQC2.CheckBox { + id: costCheck + text: i18n("Show local token costs (Codex/Claude)") + } + + QQC2.CheckBox { + id: historyCheck + text: i18n("Show daily spend history chart") + } + + QQC2.TextField { + id: proxyField + Kirigami.FormData.label: i18n("Proxy:") + placeholderText: "socks5://127.0.0.1:1080" + Layout.fillWidth: true + } + + QQC2.Label { + text: i18n("Exported as http_proxy/https_proxy/ALL_PROXY for every CLI call. Useful when provider APIs are blocked in your region. Leave empty for a direct connection.") + font.pointSize: Kirigami.Theme.smallFont.pointSize + opacity: 0.6 + Layout.fillWidth: true + wrapMode: Text.WordWrap + } +} diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/ConfigProviders.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/ConfigProviders.qml new file mode 100644 index 0000000..73bdce0 --- /dev/null +++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/ConfigProviders.qml @@ -0,0 +1,70 @@ +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami +import "../code/parser.js" as Parser + +ColumnLayout { + id: page + + property string cfg_selectedProviders: "" + + readonly property var selectedList: Parser.selectionList(page.cfg_selectedProviders) + + function isSelected(id) { + return page.selectedList.indexOf(id) !== -1 + } + + function toggle(id, checked) { + page.cfg_selectedProviders = Parser.toggleSelection(page.cfg_selectedProviders, id, checked) + } + + QQC2.Label { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: i18n("Pick the coding agents to display. With nothing selected, the widget shows the providers enabled in the CodexBar CLI config (~/.config/codexbar/config.json).") + } + + Kirigami.SearchField { + id: searchField + Layout.fillWidth: true + placeholderText: i18n("Filter providers…") + } + + QQC2.ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + ListView { + id: providerListView + clip: true + model: { + var query = searchField.text.toLowerCase().trim() + if (query.length === 0) { + return Parser.PROVIDER_CATALOG + } + return Parser.PROVIDER_CATALOG.filter(function(p) { + return p.id.indexOf(query) !== -1 + || p.name.toLowerCase().indexOf(query) !== -1 + }) + } + + delegate: QQC2.CheckDelegate { + required property var modelData + width: providerListView.width + text: modelData.name + " (" + modelData.id + ")" + checked: page.isSelected(modelData.id) + onToggled: page.toggle(modelData.id, checked) + } + } + } + + QQC2.Label { + Layout.fillWidth: true + text: page.selectedList.length > 0 + ? i18np("%1 provider selected", "%1 providers selected", page.selectedList.length) + : i18n("Following the CLI config") + opacity: 0.6 + font.pointSize: Kirigami.Theme.smallFont.pointSize + } +} diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/FullRepresentation.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/FullRepresentation.qml new file mode 100644 index 0000000..f23d2c6 --- /dev/null +++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/FullRepresentation.qml @@ -0,0 +1,372 @@ +import QtQuick +import QtQuick.Layouts +import org.kde.plasma.plasmoid +import org.kde.plasma.components as PlasmaComponents3 +import org.kde.plasma.extras as PlasmaExtras +import org.kde.kirigami as Kirigami +import "../code/parser.js" as Parser + +PlasmaExtras.Representation { + id: full + + Layout.preferredWidth: Kirigami.Units.gridUnit * 22 + Layout.preferredHeight: Kirigami.Units.gridUnit * 26 + Layout.minimumWidth: Kirigami.Units.gridUnit * 16 + Layout.minimumHeight: Kirigami.Units.gridUnit * 10 + + collapseMarginsHint: true + + function stageColor(usedPercent) { + var stage = Parser.usageStage(usedPercent) + if (stage === "crit") { + return Kirigami.Theme.negativeTextColor + } + if (stage === "warn") { + return Kirigami.Theme.neutralTextColor + } + return Kirigami.Theme.positiveTextColor + } + + function resetText(w) { + var countdown = Parser.formatCountdown(w.resetsAt, root.nowMs) + if (countdown === "now") { + return i18n("resets soon") + } + if (countdown.length > 0) { + return i18n("resets in %1", countdown) + } + return w.resetDescription + } + + function costLine(cost, kind) { + var money = Parser.formatMoney(kind === "today" ? cost.todayCostUSD : cost.month30CostUSD) + var tokens = Parser.humanTokens(kind === "today" ? cost.todayTokens : cost.month30Tokens) + if (money.length === 0 && tokens.length === 0) { + return "" + } + return money + (tokens.length > 0 ? " · " + tokens + " tok" : "") + } + + header: PlasmaExtras.PlasmoidHeading { + RowLayout { + anchors.fill: parent + spacing: Kirigami.Units.smallSpacing + + Kirigami.Heading { + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.smallSpacing + level: 2 + text: i18n("CodexBar") + } + + PlasmaComponents3.Label { + visible: root.lastUpdatedMs > 0 + text: i18n("updated %1", Parser.formatAgo(root.lastUpdatedMs, root.nowMs)) + opacity: 0.6 + font.pointSize: Kirigami.Theme.smallFont.pointSize + } + + PlasmaComponents3.ToolButton { + id: refreshButton + icon.name: "view-refresh" + enabled: !root.loading + onClicked: root.refresh() + PlasmaComponents3.ToolTip { text: i18n("Refresh") } + + rotation: 0 + RotationAnimation on rotation { + running: root.loading + from: 0 + to: 360 + duration: 1100 + loops: Animation.Infinite + } + onEnabledChanged: if (enabled) { rotation = 0 } + } + } + } + + contentItem: ListView { + id: providerList + clip: true + spacing: Kirigami.Units.smallSpacing + model: root.models + topMargin: Kirigami.Units.smallSpacing + bottomMargin: Kirigami.Units.smallSpacing + leftMargin: Kirigami.Units.smallSpacing + rightMargin: Kirigami.Units.smallSpacing + + PlasmaExtras.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - Kirigami.Units.gridUnit * 2 + visible: root.models.length === 0 + iconName: root.globalError.length > 0 ? "data-warning" : "office-chart-bar" + text: { + if (root.globalError.length > 0) { + return root.globalError + } + if (root.loading) { + return i18n("Fetching usage…") + } + return i18n("No providers configured. Pick providers in the widget settings or enable them with “codexbar config enable --provider ”.") + } + } + + delegate: Item { + id: card + required property var modelData + readonly property var rings: Parser.gaugeRings(modelData) + readonly property int centerPercent: Parser.gaugeCenterPercent(modelData) + readonly property var cost: root.showCost ? (root.costById[modelData.id] || null) : null + + width: providerList.width - providerList.leftMargin - providerList.rightMargin + height: cardContent.implicitHeight + Kirigami.Units.largeSpacing * 2 + + function ringColor(index) { + if (index === card.rings.outerIdx) { + return root.sessionColor + } + if (index === card.rings.innerIdx) { + return root.weeklyColor + } + return Qt.alpha(Kirigami.Theme.textColor, 0.4) + } + + Rectangle { + anchors.fill: parent + radius: Kirigami.Units.cornerRadius !== undefined ? Kirigami.Units.cornerRadius : 5 + color: Qt.alpha(Kirigami.Theme.textColor, 0.06) + } + + RowLayout { + id: cardContent + anchors.fill: parent + anchors.margins: Kirigami.Units.largeSpacing + spacing: Kirigami.Units.largeSpacing + + RadialGauge { + Layout.preferredWidth: Kirigami.Units.gridUnit * 4.5 + Layout.preferredHeight: Kirigami.Units.gridUnit * 4.5 + Layout.alignment: Qt.AlignTop + outerColor: root.sessionColor + innerColor: root.weeklyColor + outerPercent: card.rings.outerIdx >= 0 + ? card.modelData.windows[card.rings.outerIdx].usedPercent : -1 + innerPercent: card.rings.innerIdx >= 0 + ? card.modelData.windows[card.rings.innerIdx].usedPercent : -1 + centerText: card.modelData.error + ? "!" + : (card.centerPercent >= 0 ? card.centerPercent + "%" : "") + centerTextScale: 0.2 + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Kirigami.Units.smallSpacing + + RowLayout { + Layout.fillWidth: true + spacing: Kirigami.Units.smallSpacing + + Kirigami.Heading { + level: 4 + text: card.modelData.name + } + + Rectangle { + visible: card.modelData.status !== null + width: Kirigami.Units.smallSpacing * 2 + height: width + radius: width / 2 + color: Kirigami.Theme.negativeTextColor + + PlasmaComponents3.ToolTip { + text: card.modelData.status + ? (card.modelData.status.indicator + ": " + card.modelData.status.description) + : "" + } + } + + Item { Layout.fillWidth: true } + + PlasmaComponents3.Label { + visible: !!card.modelData.plan + text: card.modelData.plan || "" + opacity: 0.6 + font.pointSize: Kirigami.Theme.smallFont.pointSize + } + } + + PlasmaComponents3.Label { + visible: !!card.modelData.account + Layout.fillWidth: true + text: card.modelData.account || "" + elide: Text.ElideMiddle + opacity: 0.6 + font.pointSize: Kirigami.Theme.smallFont.pointSize + } + + PlasmaComponents3.Label { + visible: !!card.modelData.error + Layout.fillWidth: true + text: card.modelData.error || "" + wrapMode: Text.WordWrap + color: Kirigami.Theme.negativeTextColor + font.pointSize: Kirigami.Theme.smallFont.pointSize + } + + PlasmaComponents3.Label { + visible: !!card.modelData.staleError + Layout.fillWidth: true + text: i18n("Refresh failed, showing data from %1: %2", + Parser.formatAgo(card.modelData.fetchedAtMs !== undefined ? card.modelData.fetchedAtMs : -1, root.nowMs), + card.modelData.staleError || "") + wrapMode: Text.WordWrap + color: Kirigami.Theme.neutralTextColor + font.pointSize: Kirigami.Theme.smallFont.pointSize + } + + Repeater { + model: card.modelData.windows + + delegate: ColumnLayout { + id: windowRow + required property var modelData + required property int index + Layout.fillWidth: true + spacing: 0 + + RowLayout { + Layout.fillWidth: true + spacing: Kirigami.Units.smallSpacing + + Rectangle { + width: Kirigami.Units.smallSpacing * 2 + height: width + radius: width / 2 + color: card.ringColor(windowRow.index) + } + + PlasmaComponents3.Label { + text: windowRow.modelData.label + font.pointSize: Kirigami.Theme.smallFont.pointSize + } + + Item { Layout.fillWidth: true } + + PlasmaComponents3.Label { + text: windowRow.modelData.usageKnown === false + ? i18n("usage n/a") + : i18n("%1% used", windowRow.modelData.usedPercent) + font.pointSize: Kirigami.Theme.smallFont.pointSize + color: windowRow.modelData.usageKnown === false + ? Kirigami.Theme.disabledTextColor + : full.stageColor(windowRow.modelData.usedPercent) + } + + PlasmaComponents3.Label { + text: "· " + full.resetText(windowRow.modelData) + visible: full.resetText(windowRow.modelData).length > 0 + opacity: 0.6 + font.pointSize: Kirigami.Theme.smallFont.pointSize + } + } + + PlasmaComponents3.Label { + visible: root.showPace && windowRow.modelData.paceSummary.length > 0 + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.smallSpacing * 3 + text: windowRow.modelData.paceSummary + elide: Text.ElideRight + opacity: 0.5 + font.pointSize: Kirigami.Theme.smallFont.pointSize + } + } + } + + PlasmaComponents3.Label { + visible: card.modelData.credits !== null && card.modelData.credits > 0 + text: i18n("Credits: %1", card.modelData.credits !== null + ? Number(card.modelData.credits).toLocaleString(Qt.locale(), "f", 1) + : "") + font.pointSize: Kirigami.Theme.smallFont.pointSize + opacity: 0.7 + } + + // Local token cost scan (Codex/Claude): spend and tokens. + RowLayout { + Layout.fillWidth: true + Layout.topMargin: Kirigami.Units.smallSpacing + visible: card.cost !== null + spacing: Kirigami.Units.largeSpacing + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + PlasmaComponents3.Label { + text: i18n("Today") + opacity: 0.6 + font.pointSize: Kirigami.Theme.smallFont.pointSize + } + + PlasmaComponents3.Label { + text: card.cost ? full.costLine(card.cost, "today") : "" + font.bold: true + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + PlasmaComponents3.Label { + text: i18n("Last 30 days") + opacity: 0.6 + font.pointSize: Kirigami.Theme.smallFont.pointSize + horizontalAlignment: Text.AlignRight + Layout.fillWidth: true + } + + PlasmaComponents3.Label { + text: card.cost ? full.costLine(card.cost, "month30") : "" + font.bold: true + horizontalAlignment: Text.AlignRight + Layout.fillWidth: true + } + } + } + + HistoryChart { + Layout.fillWidth: true + Layout.preferredHeight: Kirigami.Units.gridUnit * 2.2 + visible: root.showHistory && card.cost !== null && card.cost.daily.length > 0 + accentColor: root.sessionColor + series: card.cost !== null + ? Parser.chartSeries(card.cost.daily, 14, new Date(root.nowMs).toISOString().slice(0, 10)) + : [] + } + + PlasmaComponents3.Label { + Layout.fillWidth: true + visible: text.length > 0 + text: { + var parts = [] + if (card.modelData.source) { + parts.push(i18n("Source: %1", card.modelData.source)) + } + if (card.modelData.version) { + parts.push(i18n("Version: %1", card.modelData.version)) + } + return parts.join(" · ") + } + opacity: 0.45 + font.pointSize: Kirigami.Theme.smallFont.pointSize + horizontalAlignment: Text.AlignRight + elide: Text.ElideLeft + } + } + } + } + } +} diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/HistoryChart.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/HistoryChart.qml new file mode 100644 index 0000000..315be71 --- /dev/null +++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/HistoryChart.qml @@ -0,0 +1,49 @@ +import QtQuick +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +// Rounded per-day spend bars for the last N days (zero days render as dots). +Item { + id: chart + + property var series: [] + property color accentColor: Kirigami.Theme.highlightColor + + readonly property real maxValue: { + var max = 0 + for (var i = 0; i < chart.series.length; i++) { + if (chart.series[i].value > max) { + max = chart.series[i].value + } + } + return max + } + + RowLayout { + anchors.fill: parent + spacing: 2 + + Repeater { + model: chart.series + + delegate: Item { + required property var modelData + + Layout.fillWidth: true + Layout.fillHeight: true + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: chart.maxValue > 0 + ? Math.max(3, parent.height * modelData.value / chart.maxValue) + : 3 + radius: Math.min(width / 2, 4) + color: chart.accentColor + opacity: modelData.value > 0 ? 0.75 : 0.2 + } + } + } + } +} diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/RadialGauge.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/RadialGauge.qml new file mode 100644 index 0000000..8073d43 --- /dev/null +++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/RadialGauge.qml @@ -0,0 +1,101 @@ +import QtQuick +import QtQuick.Shapes +import org.kde.kirigami as Kirigami +import "../code/parser.js" as Parser + +// Two-ring radial gauge: outer ring = session window, inner ring = weekly +// window. A percent of -1 leaves the ring as a bare track. +Item { + id: gauge + + property real outerPercent: -1 + property real innerPercent: -1 + property color outerColor: Kirigami.Theme.highlightColor + property color innerColor: "#f67400" + property string centerText: "" + property real centerTextScale: 0.32 + + readonly property real ringWidth: Math.max(2, Math.min(width, height) / 15) + readonly property real ringGap: ringWidth * 1.2 + readonly property real outerRadius: (Math.min(width, height) - ringWidth) / 2 + readonly property real innerRadius: outerRadius - ringWidth - ringGap + readonly property color trackColor: Qt.alpha(Kirigami.Theme.textColor, 0.18) + + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + // Outer track. + ShapePath { + strokeColor: gauge.trackColor + fillColor: "transparent" + strokeWidth: gauge.ringWidth + capStyle: ShapePath.FlatCap + PathAngleArc { + centerX: gauge.width / 2 + centerY: gauge.height / 2 + radiusX: gauge.outerRadius + radiusY: gauge.outerRadius + startAngle: 0 + sweepAngle: 360 + } + } + + // Inner track. + ShapePath { + strokeColor: gauge.trackColor + fillColor: "transparent" + strokeWidth: gauge.ringWidth + capStyle: ShapePath.FlatCap + PathAngleArc { + centerX: gauge.width / 2 + centerY: gauge.height / 2 + radiusX: gauge.innerRadius + radiusY: gauge.innerRadius + startAngle: 0 + sweepAngle: 360 + } + } + + // Outer progress (session). + ShapePath { + strokeColor: gauge.outerColor + fillColor: "transparent" + strokeWidth: gauge.ringWidth + capStyle: ShapePath.RoundCap + PathAngleArc { + centerX: gauge.width / 2 + centerY: gauge.height / 2 + radiusX: gauge.outerRadius + radiusY: gauge.outerRadius + startAngle: -90 + sweepAngle: gauge.outerPercent >= 0 ? Parser.sweepAngle(gauge.outerPercent) : 0 + } + } + + // Inner progress (weekly). + ShapePath { + strokeColor: gauge.innerColor + fillColor: "transparent" + strokeWidth: gauge.ringWidth + capStyle: ShapePath.RoundCap + PathAngleArc { + centerX: gauge.width / 2 + centerY: gauge.height / 2 + radiusX: gauge.innerRadius + radiusY: gauge.innerRadius + startAngle: -90 + sweepAngle: gauge.innerPercent >= 0 ? Parser.sweepAngle(gauge.innerPercent) : 0 + } + } + } + + Text { + anchors.centerIn: parent + visible: gauge.centerText.length > 0 + text: gauge.centerText + color: Kirigami.Theme.textColor + font.pixelSize: Math.max(7, Math.min(gauge.width, gauge.height) * gauge.centerTextScale) + font.bold: true + } +} diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/main.qml b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/main.qml new file mode 100644 index 0000000..0ae4ce8 --- /dev/null +++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/contents/ui/main.qml @@ -0,0 +1,224 @@ +import QtQuick +import org.kde.plasma.plasmoid +import org.kde.plasma.core as PlasmaCore +import org.kde.plasma.plasma5support as Plasma5Support +import org.kde.kirigami as Kirigami +import "../code/parser.js" as Parser + +PlasmoidItem { + id: root + + // Cached provider view models keyed by provider id. Entries are only + // replaced when fresh data arrives, so the panel never blanks out while + // a background refresh is in flight. + property var modelsById: ({}) + // Display order of provider ids (selection order, or arrival order when + // following the CLI config). + property var modelOrder: [] + // Ordered list consumed by the representations. + property var models: [] + property string globalError: "" + property bool loading: false + property int pendingCount: 0 + property double lastUpdatedMs: -1 + // Ticks every 30s so countdown and "ago" labels stay fresh. + property double nowMs: Date.now() + + // Ring palette, shared by compact and full representations. The weekly + // orange is darkened on light themes to keep contrast against the track. + readonly property bool darkTheme: Kirigami.Theme.textColor.hslLightness > 0.5 + readonly property color sessionColor: root.darkTheme ? "#3daee9" : "#2980b9" + readonly property color weeklyColor: root.darkTheme ? "#f67400" : "#c85400" + + // Local token-cost stats keyed by provider id (Codex/Claude scans). + property var costById: ({}) + + readonly property string codexbarPath: Plasmoid.configuration.codexbarPath || "codexbar" + readonly property bool showPace: Plasmoid.configuration.showPace + readonly property bool showCost: Plasmoid.configuration.showCost + readonly property bool showHistory: Plasmoid.configuration.showHistory + readonly property string proxyUrl: Plasmoid.configuration.proxyUrl || "" + readonly property var selectedProviders: Parser.selectionList(Plasmoid.configuration.selectedProviders || "") + + readonly property var commands: Parser.buildCommands(root.codexbarPath, root.selectedProviders, root.proxyUrl) + readonly property string costCommand: Parser.buildCostCommand(root.codexbarPath, root.proxyUrl) + + switchWidth: Kirigami.Units.gridUnit * 15 + switchHeight: Kirigami.Units.gridUnit * 12 + + Plasmoid.icon: "office-chart-bar" + toolTipMainText: i18n("CodexBar") + toolTipSubText: { + var lines = [] + for (var i = 0; i < root.models.length; i++) { + var m = root.models[i] + if (m.error) { + lines.push(m.name + ": " + m.error) + continue + } + var parts = [] + for (var j = 0; j < m.windows.length; j++) { + var w = m.windows[j] + if (w.usageKnown === false) { + continue + } + parts.push(w.label + " " + w.usedPercent + "% used") + } + lines.push(m.name + ": " + (parts.length ? parts.join(", ") : i18n("no data"))) + } + if (root.globalError.length > 0 && root.models.length === 0) { + lines.push(root.globalError) + } + var ago = Parser.formatAgo(root.lastUpdatedMs, root.nowMs) + if (root.loading) { + lines.push(ago.length > 0 + ? i18n("Updating… (last update %1)", ago) + : i18n("Updating…")) + } else if (ago.length > 0) { + lines.push(i18n("Updated %1", ago)) + } + return lines.join("\n") + } + + onCommandsChanged: root.refresh() + + function refresh() { + if (root.loading) { + return + } + root.loading = true + root.pendingCount = root.commands.length + (root.showCost ? 1 : 0) + // Old models stay on screen; the executable engine reruns each + // command and results merge in as they arrive. + executable.connectedSources = [] + for (var i = 0; i < root.commands.length; i++) { + executable.connectSource(root.commands[i]) + } + if (root.showCost) { + executable.connectSource(root.costCommand) + } + } + + function handleCostResult(stdout) { + var parsed = Parser.parseCostJson(stdout) + if (parsed.ok) { + root.costById = parsed.byId + root.persistCache() + } + // A failed cost scan keeps the previous numbers; usage is unaffected. + } + + function handleResult(stdout, stderr, exitCode) { + var parsed = Parser.parseUsageJson(stdout) + if (!parsed.ok) { + var message = stderr && stderr.trim().length > 0 + ? stderr.trim().split("\n").pop() + : parsed.error + if (exitCode === 127) { + message = i18n("codexbar binary not found — set its path in the widget settings") + } + // No parseable payload: keep whatever is cached, surface the error. + root.globalError = root.models.length === 0 ? message : "" + return + } + root.globalError = "" + var now = Date.now() + var state = Parser.applyUpdate(root.modelsById, parsed.models, now) + root.modelsById = state.byId + var hadFreshData = parsed.models.some(function(m) { return !m.error }) + if (hadFreshData) { + root.lastUpdatedMs = now + } + for (var i = 0; i < parsed.models.length; i++) { + var id = parsed.models[i].id + if (root.modelOrder.indexOf(id) === -1) { + root.modelOrder = root.modelOrder.concat([id]) + } + } + root.rebuildModels() + root.persistCache() + } + + function rebuildModels() { + var order = root.selectedProviders.length > 0 + ? root.selectedProviders + : root.modelOrder + var list = [] + for (var i = 0; i < order.length; i++) { + var m = root.modelsById[order[i]] + if (m) { + list.push(m) + } + } + root.models = list + } + + function persistCache() { + Plasmoid.configuration.cacheJson = JSON.stringify({ + byId: root.modelsById, + order: root.modelOrder, + costById: root.costById, + updatedAtMs: root.lastUpdatedMs, + }) + } + + function loadCache() { + var raw = Plasmoid.configuration.cacheJson || "" + if (raw.length === 0) { + return + } + try { + var cache = JSON.parse(raw) + root.modelsById = cache.byId || {} + root.modelOrder = cache.order || [] + root.costById = cache.costById || {} + root.lastUpdatedMs = cache.updatedAtMs !== undefined ? cache.updatedAtMs : -1 + root.rebuildModels() + } catch (e) { + // Corrupt cache is not fatal; the next refresh rewrites it. + } + } + + Plasma5Support.DataSource { + id: executable + engine: "executable" + connectedSources: [] + onNewData: function(source, data) { + executable.disconnectSource(source) + root.pendingCount = Math.max(0, root.pendingCount - 1) + if (root.pendingCount === 0) { + root.loading = false + } + if (source === root.costCommand) { + root.handleCostResult(data["stdout"] || "") + } else { + root.handleResult( + data["stdout"] || "", + data["stderr"] || "", + data["exit code"]) + } + } + } + + Timer { + interval: Math.max(1, Plasmoid.configuration.refreshMinutes) * 60 * 1000 + running: true + repeat: true + onTriggered: root.refresh() + } + + Timer { + interval: 30 * 1000 + running: true + repeat: true + onTriggered: root.nowMs = Date.now() + } + + Component.onCompleted: { + root.loadCache() + root.refresh() + } + + compactRepresentation: CompactRepresentation {} + fullRepresentation: FullRepresentation {} +} diff --git a/dot_local/share/plasma/plasmoids/org.rpa.codexbar/metadata.json b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/metadata.json new file mode 100644 index 0000000..5b9306d --- /dev/null +++ b/dot_local/share/plasma/plasmoids/org.rpa.codexbar/metadata.json @@ -0,0 +1,23 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "freelancerevil@gmail.com", + "Name": "Pavel Rykov" + } + ], + "Category": "System Information", + "Description": "AI coding-provider limits from the CodexBar CLI", + "Description[ru]": "Лимиты AI-агентов из CodexBar CLI", + "Icon": "office-chart-bar", + "Id": "org.rpa.codexbar", + "License": "MIT", + "Name": "CodexBar", + "Version": "0.5.0", + "Website": "https://github.com/EvilFreelancer/CodexBar-KDE" + }, + "KPackageStructure": "Plasma/Applet", + "X-Plasma-API-Minimum-Version": "6.0", + "X-Plasma-NotificationArea": true, + "X-Plasma-NotificationAreaCategory": "SystemServices" +}