From f15713d18323c46900afe248e378b4f3a3c54f74 Mon Sep 17 00:00:00 2001 From: alexion Date: Sun, 5 Jul 2026 22:21:26 -0400 Subject: [PATCH] dotcli: Enable shortcuts to be discovered by diff. --- .config/dot/.claude/scheduled_tasks.lock | 1 + .../0010-kde-shortcuts-diff-broadscan.md | 102 +++++++++++ .../kde/__pycache__/kde.cpython-314.pyc | Bin 0 -> 23079 bytes .config/dot/commands/kde/kde.py | 164 +++++++++++++----- .config/dot/kde-manifest | 27 +++ 5 files changed, 255 insertions(+), 39 deletions(-) create mode 100644 .config/dot/.claude/scheduled_tasks.lock create mode 100644 .config/dot/.claude/tasks/0010-kde-shortcuts-diff-broadscan.md create mode 100644 .config/dot/commands/kde/__pycache__/kde.cpython-314.pyc diff --git a/.config/dot/.claude/scheduled_tasks.lock b/.config/dot/.claude/scheduled_tasks.lock new file mode 100644 index 0000000..0a93074 --- /dev/null +++ b/.config/dot/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"d1732179-27b7-49fb-82af-6669b783a444","pid":45537,"procStart":"7266197","acquiredAt":1783288905666} \ No newline at end of file diff --git a/.config/dot/.claude/tasks/0010-kde-shortcuts-diff-broadscan.md b/.config/dot/.claude/tasks/0010-kde-shortcuts-diff-broadscan.md new file mode 100644 index 0000000..0d1ca7d --- /dev/null +++ b/.config/dot/.claude/tasks/0010-kde-shortcuts-diff-broadscan.md @@ -0,0 +1,102 @@ +--- +blocked-by: [0005-kde-shortcuts-mechanism, 0009-kde-shortcut-completion] +--- + +## What to build + +`dot kde diff`'s broad-scan (the pass that reports *undeclared* drift, not +just already-declared entries) currently only walks schema-backed +identifiers via `iter_schema_identifiers`. Shortcuts are treated the same +as freeform in `cmd_diff` -- checked only when already present in the +manifest -- per the code comment at the top of that loop. That comment is +overstated for shortcuts: unlike freeform, which genuinely has no +enumeration source, shortcuts *are* enumerable via `kglobalaccel`'s +`allMainComponents`/`allActionsForComponent`, and `iter_shortcut_identifiers` +(added in 0009 for tab-completion) already walks exactly that. + +Add a second broad-scan pass in `cmd_diff`, after the existing schema-backed +one, over `sorted(set(iter_shortcut_identifiers()))`: for each identifier, +compare `shortcutKeys` against `defaultShortcutKeys` (the same live/default +read already used for declared shortcuts), and tag `declared`/`undeclared` +exactly like the schema loop. Remove the shortcuts branch from the +manifest-only loop below it (now redundant), leaving that loop for freeform +only, since freeform is the only mechanism that still can't be enumerated. + +Tolerate two failure modes without aborting the whole command: +- The enumeration call itself (`allMainComponents`) failing (no live + session, no `busctl`) -- print one diagnostic to stderr and skip the + shortcuts block entirely, same as any other reported problem in `diff`. +- An individual action failing to resolve (`_resolve_shortcut_action_id` + raising because its owning app hasn't registered with kglobalaccel this + session) -- print that one identifier's error to stderr and continue, + matching the schema loop's existing per-identifier tolerance. + +Update `DIFF_USAGE` to reflect that shortcuts now participate in broad-scan +alongside schema-backed settings, leaving only freeform as declared-only. + +## Acceptance criteria + +- [x] `dot kde diff` reports undeclared shortcut drift (a shortcut changed + from its packaged default but never `dot kde save`d) without requiring + it to be in the manifest first +- [x] Already-declared shortcut drift is still reported, tagged `declared`, + with no duplicate line from the old manifest-only loop +- [x] A shortcut belonging to an app that hasn't registered with kglobalaccel + this session produces one stderr diagnostic for that identifier and + does not stop the rest of the scan (schema block, other shortcuts, + freeform block) from completing +- [x] If the `allMainComponents` enumeration itself fails (no `busctl`, no + live session), `diff` prints one diagnostic, skips the shortcuts block, + and still completes the schema and freeform passes, exiting 0 +- [x] Freeform remains declared-only (unchanged) -- only its loop comment and + the removed shortcuts branch change +- [x] `DIFF_USAGE` text updated to describe shortcuts as broad-scanned +- [x] Verified manually against the real session (consistent with the + shortcuts mechanism's existing test carve-out, 0005/0009) -- no new + automated tests +- [x] Full existing test suite still passes unchanged + +## Implementation Notes + +- `cmd_diff` (`commands/kde/kde.py`) gained a second broad-scan pass between + the existing schema-backed loop and the manifest-only loop: it walks + `sorted(set(iter_shortcut_identifiers()))` (the same enumeration + `cmd_complete` already uses), compares `shortcutKeys` against + `defaultShortcutKeys` per identifier, and tags `declared`/`undeclared` + exactly like the schema loop. +- The manifest-only loop below it lost its `shortcuts` branch entirely + (`resolve_mechanism` returning `"shortcuts"` now just falls through + `if mechanism != "freeform": continue`), since the new broad-scan pass + already reports every declared shortcut mismatch -- keeping the old branch + would have double-printed them. +- Two failure modes, handled at different granularity: `iter_shortcut_identifiers()` + itself is wrapped in `try/except (RuntimeError, OSError)` -- a failure there + (no live session, missing `busctl`) prints one diagnostic and skips the + whole shortcuts block, letting the schema and freeform passes still run. + Inside the per-identifier loop, `read_shortcut_value` raising `RuntimeError` + (an app that hasn't registered with kglobalaccel this session yet) prints + one diagnostic for that identifier and continues, matching the schema + loop's existing per-identifier tolerance. +- Real-world validation on this machine: manually ran the same enumeration in + a throwaway script before implementing, confirming 29 of 278 registered + shortcuts differed from default (the Meta+1-9 desktop-switch remap, + Meta+Shift+1-9 window-to-desktop binds, and Meta+A/Meta+Shift+A activity + switching) -- all 29 were `dot kde save`d into the manifest in the same + session as a prerequisite for testing this cleanly. After implementing, + `dot kde diff` reported all 30 shortcuts (29 plus the pre-existing + `ksmserver.Lock Session`) as `declared` with correct default values, and + ~34 unrelated `RuntimeError`s for apps not launched this session (Konsole, + Spectacle, Dolphin, etc.) printed to stderr without aborting the scan. + Removing one entry (`kwin.Switch to Desktop 1`) from the manifest and + re-running confirmed it flips to `undeclared` with the same live/default + values, then restoring the manifest flipped it back to `declared` -- + confirms both tags work and the manifest was left untouched by `diff` + itself (read-only, as documented). +- Full test suite re-run after the change: 101/101 pass, unchanged from + before this task. No automated tests added for the new pass itself, per + the shortcuts mechanism's existing carve-out (0005's Implementation Notes: + a live `kglobalaccel` D-Bus session isn't practically substitutable without + disproportionate mock infrastructure) -- the existing tests already + exercise `dot kde diff` against the real live session and continued to + pass with the new pass active, incidentally covering that it doesn't break + anything even though it isn't asserting on the new pass's own output. diff --git a/.config/dot/commands/kde/__pycache__/kde.cpython-314.pyc b/.config/dot/commands/kde/__pycache__/kde.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b6f99b53dd2e26324f9a7fc2d4a775ec157b93a2 GIT binary patch literal 23079 zcmd^ndvqJudFKo;c!L1=eu@N1@d1htOQzm0QxqxDlqhOQfwn^nr3i?GMH18iv?N+~ zXs2l?Cs{#hQcGiwaM0U+pXm|-8fBm7c?l69y7JhMtgd)d(Ku5_ng?- zv%A0V4h9bjvYj^F{bR4hi^0sDd+*%)J$~O~u)&sL=1_KIbw2ahL5}+ceNl@(c5wf% zc#fOmj&ma4!G)AhKE$){nvjNlYC~G~DIC;IXv(;Q`jDZFT539s2TdWQMwwM8U$p0Qp#sr?x=_qST_k3qE*7&7fVoAiR(~Ti>0V*#4^;iq7$`CEJs}@R-oP>R-&#K*Q0I_t57$Jo5bpT zZiEjti8c8Ch**nyqv%4tNvuQtsJH=jvsjOMv)F)oi`a;I>(krBrfpAfoVT9iO1P)B z-IBWaiO+KG>8<$Fc83z@)Fbhc`Xcq1eE_Asxvc>B=IWRF2 z^7;ouvUzM^)aMONjF0$SyexDNgoY<;o*NE@#&O8<2wXjB}Q)m2RtQT2NhtlBW?TO&PkZ*_68wfcu8)tCfS)cPOey?vVoS|WV&^hW0(&J4&x8^Aa#>Yp_(UWyg1c!aW z*x0GhQWI1k)vHY085j!%&~M7vjbAwUKrVP{&G=sb$&)mE&)~qAI(l$$*f%=RaAIKa z6c#Y(3x)h+Ls*@G!Qp`uBR&i)1tx}u*#w9XurlN0Xo7A+)S%u88j3v$RzM03PK0p# zkRMAVp~o}+(6BR^Na}HQ9PMZr8U-B%#(d5*!+~Jz5!rq@X|g^knAp=8iPvM?p&|6F z?iD(y9mihMe*4GRMAapAIRlb&VoYt>=xiAY24dp}AJ}ws1e)-j3`nEuLIl5HUf34D zgqbkrBLYJo%8EL%X(yz>fVUxt)dqzIK$zIvz}Uz+C)S)T_nBeem^zL4^SEepMUZ0y zYunHOiU*N8&q)3dh=wRZeLi};-+J+=tKh1^}x3*tUk44Sa*sR9|H`lLI&$|V6G?C}IZhD$EMT|?(ah+TD z;COv)k7_FMv}O1k*Z4V?R^}VKAV9UJeCJ$(Y=*RyLP4TuSvxv#R@RRA#$^3SFeLfM zU3ysn2}1T_v%P-r0Cq~|2W7+PKnR;1l=Yy{lm4@^NdmX|L6K6BG;wt`i4M7;X*e+I zYZ@5wo%ILCni>_s+XU$d89a*JCK2uR(O={EIXTZ093SzAJb^LKsj6TDI?umhn(uwfbIo((==<4!-M75?@Ws>9r?1pa9f;VvKDK0i zXwSZ#;RxryGPsygG1b1}$oX#1Oy2K*CEOozR4hAO3l7&@Wz^9))$y?<2aQ)OnGs7# zL@4?6UM6Qh!UqX(e7(A5w=l0ql|osxM@f{`AEz9eDi!QM-yPp=Zp78-G=<|@Q zl?8CTBpW(g+YflUdSvZ@*XuIK!b$&#Pc~8u52UfIXMF`_VH}zsbQlPL4%s=6Z^Sn$ z6=G^Z;s}M>oKh~1<(wGpDB{Ya^dVK_(qs4wj-#65Rx=#mJVx|TaM^mvI@3G5X~|JN zEv(v$W}2gR=d`e5vA?|QLu<}TQR%C$E3Q8}GwYw5h^*hdRJ3LK@s;d#m-k%SGg}?a zc1`d9*q(hMwBpFSY`$ckXAd>>k`$0!2FZ!8B|BA+znrXNVnr&TW8NbX^Tjrp$rELR z^J7vXd4oL|QUbwqhv}B5P+^a)6|>EYuHDzYQP<(9wQH(v#bEuwP_SapnYP|BbGe0= zyDoLjJ{rxbdGYX6+w|ba7Tc<=AX3~o7n(ac-!kvL_C#cNU!?GfsO`y!@yQj7V|qtK z$d!m+2IJh0FT{gvH~)YQoY+kfYpd?v8Qd|Rw7X9xYa(#l>5AiAGCqbT$xwKUFRC(7Rla6e`h*LMnwSsl))B zA`KX^Y(>Mb&>bgGfmm{?BQ-m&?~mrRfEb=)wDZ0>>gxHa`TF+xmN)C?>VIlpaP>s3 zy^M}tdVbYbv|`&#L~@7Y_YCu>Glu#5TCLrDhtrzOcZ{4hbL!Bi_cFM;UOq^>HIvz* zH>cMj@3UE?#~%p|&p5ggdLfQ0ykJh?7J+Dz%8)`EM2)O~w-wsZY)YmLji_ao99R$X zAWlgMv~dP#L~sPtjPlAD!zAEU3*fo+FOk_v^M4VKIO3sRHZ=4LI zL6=Sf{Nf}6RuXLnV_%6x6I6jYVIwigzyt+plW|2_kc3DU4}>;id+t#5Oi~BC(3p@z za1hlLx02^v&Z}F((>HD&1TR9 zL6Cl{Rp=1kPTYm<7`0^ zA{)r^_2ZgAO{5(-CpFRq9W$R4beXi>86&Vo02dUyS!zeqN&E$YOK7icj;RCCbNkjf$&dD1!8(2pvi||a4_Udqp7N0{e;6s zRS{Y;R$v+k(jGLFQ?(lrE(nObPpn+Qm0?+Xxpt7o=}emFCe2xHN(V#;9Y%H!**KfE zejeT%-rRMS!>6Y45otee4$?|h$N1E=YsfbS^dRk#%b5un-^9jUBLS#@;GV{KlWDp; z2h}ezdZc*AwYItaGbd;FeZTVuZJ#os|59c)-vw2o1GF3s%4VfYMsdP30sk1}4bhj> zN)@B3HXO-1Mu<#0GZd-l4O{Ajq#iW=J^TebQGu%Vw7oO>_UK;*7Oh=x2NqmA=8D4R z>GLxiXO7Jrh-R#RJ22I@VC`BFjNdr&;*s#a%LgtUSfEKqi+4nGb}R`yKfSNzGGJbi zJ^NKR%VxolI=8bL9{Y>ZX34AgJV=$vx@SJ`y>p(y2=JXdIgVn6RKC>9M4ubNCaQa)=?_KA*;TL zgCL2TDejI&tIJunXI?z}^4S^Tn=dTes}}54QF~3qSo5n@OUA|B7j}Qb@wyz@o_$A) z&(DHj4eq7NYVMD7_iF!&->Xf(Sii%%PcK*d`{$i`y=23%5rt#Sx+SK^;S+#E9lNHeiZNfY8LV zCp|Uyd4!Ux3rPDUU;x(lbG3px2DHu)X`OzE1*N#->&QSX<_f0?c)A}jH`XE!QFik2 zIXD%9?1vmwN;e?R+5&?$YwfvMsG-k&mVH^=qmB*X1)G9K5I#Ec={{K?vFQeVd$ zzb=bJYfj1vP7=L8=<)g`Ss#QS(C3u}aw#iJKwB+4TEqjMUh!C)M{I5HIMmj|=!KDy ztOus{jd`In%`wAK*^v{;2EVwEkmD=7lXQ*6eQ|S+-UzkomP_t-WjKGB@4H z`XYUKhf%E?I$I+G)x&;o8pRj0V9(u#*k>O9*J=WUlPrOmPrUdHWVXh<^*Oa8W z?!jDj-DF>{DJgT+CFV-b0$&-8?$&oZ*4%`zDi?Xdvkz-?m>2bm;YlvXGN=)P1)c9I z{L^_i1`}}JW$0qIzQO@ILVo8Ms+SEh*RymGgSt!#53R#-Ld+^&0t3ljG8f5LAd#Mw z1ycHCLoi1Y`Hq6JAr=5g@R5XlY;~Pw*G~ zPpGE2j~qqmSmst*?~%Rm;tLmEm^m4>SIyUdDKkif6R-~B%UQz{5By_!{x2{7@W$KY(1TZ3|)AAIV$%+kbQta+woFLE#CR)oo%myb0V!Z}w3z(NKp_B5-&8u=w!XOVy6KIRc zazV0^2-2?W#r49ZHpGwEf?*L6z!e!z3IQ|zpjrq?hUtooL_V)xLR5@dvd-fTDpC)U^^J2 zw3P>slMmi;Z5@$fmeM zhJx)zHA)COiaA`Elv6ORRvO2x3QdgQC`eBSF{#)~nTQbuMxOO~Mtumcj`@S5(sQ`} zAE_aTl{VO5`^Nbf&qwk%EE+bf*qsrh^R}&gw)XY<*Xrlaym|iW`Rih|;;|bwQQPCd zCf=Bal0cJ?O6dRtY%-0g!$192njZ>*mu}KhYsztsDeNv9DZ%xk zA^mee0vgjlN$g6RXTcQ{#*PE%#01hoX;p^9{a{qVJb9DT0qk_3Sv0G6f#NK(^%S{& zl)&GX&-4uq1YSQgG&HbK!DqUL2Dt7gO$|so*Y0o2s$88Eb~pDX?8fJ$I%LS+=&kbRnTGsO;4N{mjZ)GU68f#Ps?V>hrY)K z1Bg}9vw|H$IFzJ)>KBYE>m?=rDJXPLBrj_RN4>b<3nC#!aWq5TDGXp|{w?yPdzhO_ z`ZjJ@!C&w?Dl+W~XSU7uz25&?|2!#ecn{w1i@2X&b`LDL2O{PZQ>`(YH(J7$nZAg< z>ZY-3T6;Sq|KjnNkI%Hu3)h<$Gaf_8%8;>aC|EEQ%(N^T)-fk^_3W9~&%btlb__9# zp|?h_jm~>+wA>i_$*Ffw-FP}8_C}6Zn$$Ej^h!|m++<-&yJQ}azDCV|7gZO~vXj2VUF6c&>Fe)N^$n_s=q1uijN+jb zm}CdK;iPX;^(|Cu$lIa#!;`_k?e zqdlB|x%g7?iqX1a%(#;|BO-ca^Gt z0=$^k+3n^ZUhZf^St#j z@C_@Jxw%0p1hS=L=oGtc3nt{NSbj*~(~c^>S|0LT=M??#4*&#=m$%@qd4S$9aa?;D`ifi(8S1 z!)w#T;Wgb!xKW*Wf7~hK7EpNGj*WTk$>EeSzPhWmI97+BY`u>+tCdM5S3T- zh>zN6L1eNy6li7l`{2Zs+HhVGIs%b8K! zq)BT-!!yCa*lwm`Wr0#i(kp>&7$mL&ps1kQu}?}#D1w-<$E?!{do9E^ge}Q=Sr-bN z@{J|Oc_b#|s~*g0Nh9V9iN@*4nUpWtbVC|$suDZoGK15EM4li(kMy-sb0+S@StE=l`xI-dc%&J^^ z`0wEx8A#dTCuUxldv?`QwphMtvAO+yJ)(b>Zhoru?+uO>Ba$$7FVOl#OLpP#rg8U| zvW{k+p4&243olUnJBQysyu9nk!mcCHT}LB3#Yk~a)Ycm@_O2rAw&Ud;;ggp~FO4FD zW7d7sQXdiOAJlSjuTC|abAoP&$B)DF&>RDfO0E|L&Y*_#K#DDclq(f%4@i)aN^X^` z83?*~whn0Ifb3PydnU&Gz>Km1vYGBlrYvO0#?E?R`Fi5_NPmS1mD6fc0210evof-N z*LClWmdJs=Xyy|yK0ejAV#!hy7SJYMNLp3m?>uY@?Es6rj_i#0Wsy8%&?Dg%cf1M zdG&Ko&Y%9%{-wMf)9tHSg^{AhdHww9e`mVBN#Cgp&}jG4)7qzS6Fi&I%p=cJ9^N{Ex7Qv!;j*f-k-QdZa(^6|}E$5(KG-N>qqxVA54?U>dhS36w(?d_3*+9gZvoO{0I zPoKQru+(s1CA;wD!*_LD_Rdd?oF#w7yl!T6VO`_gSY-F%guz0DP7OxrF1=u z9_e~I{e#?2Qj4hOJ~@`A808=1V{Z;XFPzRyrR3CaNZf&*BicpwCa5-+i3u|iJWmFq z)QJj=uO9>zdXWmHeA1=3S5*P&^zx5KxhlPn%Pq7TWEmh6zG_3%QvaHJrVT4`Fnsi4 z=XB?5?gb{kKK;N_&&>F*7dVCV$;TnqNrK(`;>oHh!5X~&kYM2iNNxn=KEiL-mvaKg zgoBW z07-E=g`=~X11?TKiWBZ4D z^>iKav>fX_a_C6Q{*JB#il#Fp?Q0v|_%~GjYpSkOMZh#jD_pI_V3pm?QT$J9wl{)i zowU89IDlwK+Fnc1%;u=2d}{w~TV~kx&9O*E^UOI^Z&c2C|BVX=Q|%wwv!}HyMq7Ae zco3Oe1GAZttje3l%9r(U9654s3FdDce(~^PmTOU{yDj8J@@wbnBEp{eXP1OM3Q-WR zs)ZJd35!3#!9$2blMuUo<#Dx&UZiHXHWmyjXYiXu4=c!PA<(g=;4G=_HAU?argq#U z?OUHX3$8ndTd-}r@WBE!5^+e9r|{c!U8khSVI~}N4a%mtvq~aAc#QSp%G5|NIpUoi zpA^N_KH88L9AHCir8ARJTl3U`)xx^Dt`HR-P$IgPu=B=+;^e1N#!dKU z);u4SUu>Md$5Nl1EP})NOjIN(oii6S zdQnQka(+|;cQmul$MuQT6@^|sVs*d43L&>sH?H5Sr}Qq)+&HWM7mn*hypp$8Sc4@NIF%Nc5JH(nsaOS>njK{+bfs9sQ5i0sG*HHQ%~ zI}Zsc=@zaJ6Q>YBg-6L?2@Bz)-!sf?S#*@I8Z*B2%%ZXIL%ZX)!93mkwR7Q(zxzDS zMr@D#F!jZswV&-8FS^)m}35bkx3K>fjy0sLTA=VqbOE&gIMv{AvDt zZPdAQ`ru6Y_xe7tuKPu4b=bUG(=>06*0hCeNSK7TBm)lPKV_6 zgeE7;RX$=8J{`wNA5uH}ns!|-vn1n!Y-#E4KJ;Xa-&S#EEvvEAAJQ5LesdY6pVNuI zqbkl{(#P~Qp`93i5$uQ|vl#F;5reVWO25Fxmx#mu5C`cv?9`&M2u$?%1`Fe?jen_q z$NaYWrm^**oQ2@iO1bOx($`9t$~P`p*G)gZYRwHlGgq)=-2|9cvFWarvu03GYAyTN zXZe|6#8&x6<(qX^>uwr1C$Ucf=J)RUs(lvjN0!W1o%Tm1rLCOyJx+(?6viRaV}Qd8 z7W!?gzyCLklTd;Q71a*|Ln#UCPF2Gvf4ek9iGpLV}!6YcS2Cqv4bKl@u*^D8kUmrUYw%p}%#t9Vp> zV0?5I-9SZ%mc^#BCKblk%(ExQ9@j+bCuw zjrAlO3!+geVsO?ypkeCI!61=(>zMLD_t_;~#l>c|X^);}698Ok+a(*-ABU06i64rQ zdFQ0ox!XBuan{C+ON5Si*JPcFCzRV&M@!Hd#P5FKmu3c@MZO*VEDoAy$L>sa3&FSW zXIL@jkC!It=pd>|lR62fD__AU=KUS*?VkfrPSCwYR83Ijq>9wFl1?Gf5h)3lU#CQ% z-^EwiaHK~`cljAzoTVz^ z1(|&wB*Cv@Avi-+Pb9NAXc?t{K!X{=Ln|;E)8V0*#i8P%FSImnXBWOwxmdb+emGjX zH(K=AQg+Ls!07?YfZ_t=@lQD4NwZZMdTmOu4JsaKuEjo2U0vmXG6Gdl4_cRZ_iJvgyJz*R9i9 zvPQDQy71BPWO!t@a@ITRp4&NpX5N4Obi~%OWZZYRn9JG6->DXKWve9>fAr{L&12D; zebLI+XvzMFvGBf`%c=S@6GgHS&4%(B@f^ryJaz3Ax1ov`kPOf|n;ErK&bHjNR7dLeEL!$p74B%vm{4Bz z>|``=*Ytta@=fzwqvd$Fvvj&+#a=M;_@ccUiRp<2M5=mN`qm`Q-GLLa2u7aC7BcH?Yi zB&+(3o;RPm`qWM9w!gFHkO^qY{q-kKE~D_3nuw)>48}6T(~ce2H(sxd*!KLm=O<6S z`_xTi_ex3WlCh8(jb*?7BCkC@Y$@s3&HZ(5 zi{|lNnxE8_bvA3>w`8IIsm{`|L-$ihX~$;WPdDr6cz5RG+qHjVb3Wdz{hMYT9q+Pq zZq)tEP}*6q`&qq?j+-+NHR=9VS9+*U_qTOA9H#{I$rNGO{Exw6$sX54=qx!%nQ%5G zq7(^(Qq{BSClCgp8$y>!CTY5)3212q*3!L&6}yrq=^C0j9%6%D-x5cX^fas_%{7k9 zABrRqh55ff+ZYOoTLS%Y%+ zqL35{V&(ID7KP*)srQSzdW(8LVXc&JhU9N}4GmTxL+nuNa9+px4nKPmcNx(e;-V?3 z6&y%Pd7za!sa5VnTUl5uD;qIiZI!UWlanSztMVgFaP(q-mGWhF&y_J*7z1}ZoCxEC zGvGJ%I)?yq#+Wnd&wmO2;JUhG$NnR|p3ePk9`?Ij`#VHgIC$i6+oU1(yKS--9Zg#C zV{Hwp>s1!Ww<_zH0|+@tfIYHNWxk|=C&t(hy2VF#Ss5IZ5*{jw1<0BJUe)xCJ?obO zV+`lXS^L}CTaF#-_4KqJY&+cI!K|cPXv&;T3}G=AMLeZy{uv2}2%1omp`G-o&Gkd=Tjp!Q+v{Oy>6v;3vW=zW>B@?`mf5 zcTZe?=F&5l$1aUUs?&Sw%;t=5Y4HdJ9jf@+x5*i za~`{~=Vs2)sl&Gnws7^Lp2hgjV>JS&R}vB)kpoAM<#m`*=q6y{@=` zr8IRF-coqiwNv^bt?vJ%>g!a|4#~M^M@Jg*iw{0uV;g?d0G|I|9LqX(O131Olibwa zG8h;c!OwzG{t|mZUJ~fEfjsyl{u2^yw(=(L|DvO7RQ+43en8cKrpimzM^yb6s{R{Q zBn%S2DyIBk5QX*?tlv&YJyexYb(|`uXPDJ6L0@4*$4rMX9sL1Sl=;lkO_Q>S-lcOt zrs{pFm{CDAq68|I=;#(zzo3d>J|jX#PqgBcoA2@92O!QObC$BIS+WP)5G8 zk2+^1r!vB2uHRyqF&%Rfn;dk9L3w+K! zo4}Xcv)OR2!p=MHt+(+z?o}9ZF3-$Y-z&=C&G(!JzTjSthCjgHGuzOn!o)XF582d1 zJAcn+JRa#k82#Ls|)#zJKH$H qIz73lEnE={-#GZv!3(|N%FDHvYG*cmuRfYlwj?;Am4za<#Qz7sTny#_ literal 0 HcmV?d00001 diff --git a/.config/dot/commands/kde/kde.py b/.config/dot/commands/kde/kde.py index 98174c5..4b0effc 100644 --- a/.config/dot/commands/kde/kde.py +++ b/.config/dot/commands/kde/kde.py @@ -38,23 +38,85 @@ APPLY_USAGE = """usage: dot kde apply DIFF_USAGE = """usage: dot kde diff Scans every schema-backed setting reachable through the kcfg mapping - table and reports each one whose live value differs from its - schema-declared default, tagged declared (present in the manifest) - or undeclared. Also reports already-declared freeform and shortcut - settings whose live value differs from their default (neither has a - schema/mapping table to broad-scan, so both are only checked when - already declared). Read-only -- never writes the manifest or the - live system. + table, and every shortcut registered with kglobalaccel, reporting each + one whose live value differs from its default, tagged declared + (present in the manifest) or undeclared. Also reports already-declared + freeform settings whose live value differs from their default (no + schema to broad-scan, so it's only checked when already declared). + Read-only -- never writes the manifest or the live system. help show this message""" Setting = namedtuple("Setting", ["file", "group", "key"]) -def parse_identifier(identifier): - parts = identifier.split(".", 2) - if len(parts) != 3: +def _split_on_known_prefix(rest, candidates): + matches = [c for c in candidates if rest == c or rest.startswith(c + ".")] + if not matches: + return None + + prefix = max(matches, key=len) + remainder = rest[len(prefix):].lstrip(".") + if not remainder: + return None + return prefix, remainder + + +def _known_schema_groups(file, kcfg_map): + groups = set() + for path in kcfg_map.get(file, []): + root = _parse_kcfg(path) + if root is None: + continue + for group_elem in root.iter(f"{KCFG_NS}group"): + name = group_elem.get("name") + if name: + groups.add(name) + return groups + + +def _split_schema_group_key(file, rest, kcfg_map): + match = _split_on_known_prefix(rest, _known_schema_groups(file, kcfg_map)) + if match is not None: + return match + + # No schema group matches -- freeform. Its group is never known to contain + # a dot (there's no schema to have told us otherwise), so the boundary is + # just the first remaining dot. + group, _, key = rest.partition(".") + if not key: + raise ValueError(f"invalid identifier {file}.{rest!r} (expected file.group.key)") + return group, key + + +def _split_shortcut_group_key(rest): + (components,) = _kglobalaccel_call("allMainComponents", None) + match = _split_on_known_prefix(rest, [component[0] for component in components]) + if match is None: + raise RuntimeError( + f"no live kglobalaccel component matches {rest!r} " + "(the owning application may need to run once to register its shortcuts with kglobalaccel)" + ) + return match + + +# Only the file segment is unambiguous (rc file names never contain a dot). +# The group/key boundary can't be found by counting dots -- both KConfig group +# names (e.g. "org.kde.kdecoration2") and kglobalaccel componentUnique names +# (e.g. "org.kde.dolphin.desktop") routinely contain their own dots -- so it's +# resolved against known-good data instead: the live kglobalaccel component +# list for shortcuts, the kcfg schema's declared group names for everything +# else (falling back to freeform's first-dot split when no schema matches). +def parse_identifier(identifier, kcfg_map): + file, sep, rest = identifier.partition(".") + if not sep or not rest: raise ValueError(f"invalid identifier {identifier!r} (expected file.group.key)") - return Setting(*parts) + + if file == "kglobalshortcutsrc": + group, key = _split_shortcut_group_key(rest) + else: + group, key = _split_schema_group_key(file, rest, kcfg_map) + + return Setting(file, group, key) def load_manifest(path): @@ -137,7 +199,7 @@ def iter_schema_identifiers(kcfg_map): for entry in group_elem.findall(f"{KCFG_NS}entry"): key = entry.get("key") or entry.get("name") if key: - yield f"{rcfile}.{group}.{key}" + yield Setting(rcfile, group, key) def resolve_mechanism(setting, kcfg_map): @@ -233,7 +295,7 @@ def iter_shortcut_identifiers(): (components,) = _kglobalaccel_call("allMainComponents", None) for component in components: for action in _actions_for_component(component[0]): - yield f"kglobalshortcutsrc.{action[0]}.{action[1]}" + yield Setting("kglobalshortcutsrc", action[0], action[1]) def _resolve_shortcut_action_id(component_unique, action_unique): @@ -279,7 +341,7 @@ def write_shortcut_value(component_unique, action_unique, value): def save_one(identifier, kcfg_map): - setting = parse_identifier(identifier) + setting = parse_identifier(identifier, kcfg_map) mechanism, default = resolve_mechanism(setting, kcfg_map) if mechanism == "shortcuts": return read_shortcut_value(setting.group, setting.key) @@ -287,7 +349,7 @@ def save_one(identifier, kcfg_map): def apply_one(identifier, value, kcfg_map): - setting = parse_identifier(identifier) + setting = parse_identifier(identifier, kcfg_map) mechanism, _default = resolve_mechanism(setting, kcfg_map) if mechanism == "shortcuts": write_shortcut_value(setting.group, setting.key, value) @@ -355,8 +417,8 @@ def cmd_diff(args, manifest_path, schema_dir): kcfg_map = build_kcfg_map(schema_dir) manifest = load_manifest(manifest_path) - for identifier in sorted(set(iter_schema_identifiers(kcfg_map))): - setting = parse_identifier(identifier) + for setting in sorted(set(iter_schema_identifiers(kcfg_map))): + identifier = f"{setting.file}.{setting.group}.{setting.key}" default = find_schema_default(kcfg_map.get(setting.file, []), setting) try: live = read_live_value(setting, default) @@ -370,24 +432,48 @@ def cmd_diff(args, manifest_path, schema_dir): tag = "declared" if identifier in manifest else "undeclared" print(f"{tag} {identifier} = {live} (default: {default})") - # Freeform and shortcuts settings have no schema/mapping table to enumerate, - # so unlike the schema-backed loop above, they can only be checked by walking - # identifiers already in the manifest -- neither ever surfaces an undeclared - # setting via broad scan. - for identifier in manifest: + # Shortcuts are enumerable via kglobalaccel's allMainComponents/ + # allActionsForComponent (the same source iter_shortcut_identifiers already + # walks for tab-completion), so unlike freeform they can participate in + # broad undeclared-drift discovery too. + try: + shortcut_settings = sorted(set(iter_shortcut_identifiers())) + except (RuntimeError, OSError) as e: + print(f"dot kde diff: shortcuts scan unavailable: {e}", file=sys.stderr) + shortcut_settings = [] + + for setting in shortcut_settings: + identifier = f"{setting.file}.{setting.group}.{setting.key}" try: - setting = parse_identifier(identifier) + live = read_shortcut_value(setting.group, setting.key) + default = read_shortcut_value(setting.group, setting.key, method="defaultShortcutKeys") + except RuntimeError as e: + print(f"dot kde diff: {e}", file=sys.stderr) + continue + + if live == default: + continue + + tag = "declared" if identifier in manifest else "undeclared" + print(f"{tag} {identifier} = {live} (default: {default})") + + # Freeform settings have no schema to enumerate from, so unlike the + # schema-backed and shortcuts scans above, they can only be checked by + # walking identifiers already in the manifest -- they never surface an + # undeclared setting via broad scan. Shortcuts entries are skipped here + # (rather than re-parsed) since the broad-scan pass above already reports + # every declared shortcut mismatch; parsing one here would also mean an + # extra live kglobalaccel round-trip per entry for no benefit. + for identifier in manifest: + if identifier.split(".", 1)[0] == "kglobalshortcutsrc": + continue + try: + setting = parse_identifier(identifier, kcfg_map) mechanism, default = resolve_mechanism(setting, kcfg_map) - if mechanism == "shortcuts": - live = read_shortcut_value(setting.group, setting.key) - default = read_shortcut_value(setting.group, setting.key, method="defaultShortcutKeys") - if live == default: - continue - elif mechanism == "freeform": - live = read_live_value(setting, default) - if live == "": - continue - else: + if mechanism != "freeform": + continue + live = read_live_value(setting, default) + if live == "": continue except (ValueError, RuntimeError) as e: print(f"dot kde diff: {e}", file=sys.stderr) @@ -400,19 +486,19 @@ def cmd_diff(args, manifest_path, schema_dir): def cmd_complete(schema_dir): kcfg_map = build_kcfg_map(schema_dir) - for identifier in sorted(set(iter_schema_identifiers(kcfg_map))): - print(identifier) + for setting in sorted(set(iter_schema_identifiers(kcfg_map))): + print(f"{setting.file}.{setting.group}.{setting.key}") try: # Fish's completion runs this on every TAB press, in shells that may have no # live KDE session (or no busctl at all) -- a broken shortcuts source must # never cost the schema-backed candidates already printed above. - shortcut_identifiers = sorted(set(iter_shortcut_identifiers())) + shortcut_settings = sorted(set(iter_shortcut_identifiers())) except (RuntimeError, OSError): - shortcut_identifiers = [] + shortcut_settings = [] - for identifier in shortcut_identifiers: - print(identifier) + for setting in shortcut_settings: + print(f"{setting.file}.{setting.group}.{setting.key}") return 0 diff --git a/.config/dot/kde-manifest b/.config/dot/kde-manifest index 95e0e70..56996ee 100644 --- a/.config/dot/kde-manifest +++ b/.config/dot/kde-manifest @@ -1,2 +1,29 @@ kxkbrc.Layout.Options=caps:escape_shifted_capslock kglobalshortcutsrc.ksmserver.Lock Session=Meta+X Screensaver +kglobalshortcutsrc.kwin.Window to Desktop 1=Meta+! +kglobalshortcutsrc.kwin.Window to Desktop 2=Meta+@ +kglobalshortcutsrc.kwin.Window to Desktop 3=Meta+# +kglobalshortcutsrc.kwin.Window to Desktop 4=Meta+$ +kglobalshortcutsrc.kwin.Window to Desktop 5=Meta+% +kglobalshortcutsrc.kwin.Window to Desktop 6=Meta+^ +kglobalshortcutsrc.kwin.Window to Desktop 7=Meta+& +kglobalshortcutsrc.kwin.Window to Desktop 8=Meta+* +kglobalshortcutsrc.kwin.Window to Desktop 9=Meta+( +kglobalshortcutsrc.kwin.Switch to Desktop 1=Meta+1 +kglobalshortcutsrc.kwin.Switch to Desktop 2=Meta+2 +kglobalshortcutsrc.kwin.Switch to Desktop 3=Meta+3 +kglobalshortcutsrc.kwin.Switch to Desktop 4=Meta+4 +kglobalshortcutsrc.kwin.Switch to Desktop 5=Meta+5 +kglobalshortcutsrc.kwin.Switch to Desktop 6=Meta+6 +kglobalshortcutsrc.kwin.Switch to Desktop 7=Meta+7 +kglobalshortcutsrc.kwin.Switch to Desktop 8=Meta+8 +kglobalshortcutsrc.kwin.Switch to Desktop 9=Meta+9 +kglobalshortcutsrc.plasmashell.activate task manager entry 1= +kglobalshortcutsrc.plasmashell.activate task manager entry 2= +kglobalshortcutsrc.plasmashell.activate task manager entry 3= +kglobalshortcutsrc.plasmashell.activate task manager entry 4= +kglobalshortcutsrc.plasmashell.activate task manager entry 5= +kglobalshortcutsrc.plasmashell.activate task manager entry 6= +kglobalshortcutsrc.plasmashell.activate task manager entry 7= +kglobalshortcutsrc.plasmashell.activate task manager entry 8= +kglobalshortcutsrc.plasmashell.activate task manager entry 9=