diff --git a/VERSION b/VERSION index 879b416e..c043eea7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1 +2.2.1 diff --git a/assets/gtk/gtk.css b/assets/gtk/gtk.css index 037a6792..0b453d22 100644 --- a/assets/gtk/gtk.css +++ b/assets/gtk/gtk.css @@ -3,13 +3,22 @@ @define-color cy_bg rgba(7,9,13,0.88); /* window (glassy) */ @define-color cy_bg2 rgba(16,11,14,0.93); /* raised surfaces */ @define-color cy_base rgba(10,7,10,0.80); /* fields / views (NOT white) */ +@define-color cy_hdr rgba(11,8,11,0.93); /* headerbars / toolbars */ +@define-color cy_side rgba(9,6,9,0.90); /* sidebars / places */ +@define-color cy_btn rgba(16,11,14,0.90); /* buttons */ +@define-color cy_head rgba(14,9,12,0.95); /* treeview column heads */ +@define-color cy_trough rgba(16,11,14,0.80); /* scale / progress groove */ +@define-color cy_tip rgba(5,7,9,0.96); /* tooltips */ @define-color cy_fg #e7f1f6; @define-color cy_dim #9a777e; @define-color cy_red #ff2d3d; /* primary HUD accent */ @define-color cy_red2 #ff5a64; @define-color cy_cyan #2de2ff; /* secondary accent */ -@define-color cy_sel rgba(255,45,61,0.34); /* red selection fill */ -@define-color cy_border rgba(255,45,61,0.42); /* red HUD borders */ +@define-color cy_warn #ffb454; +@define-color cy_ink #0a0d12; /* text on a solid accent */ +@define-color cy_sel rgba(255,45,61,0.34); /* accent selection fill */ +@define-color cy_border rgba(255,45,61,0.42); /* accent HUD borders */ +/* __CYBER_PALETTE__ */ /* ── GTK3 (Adwaita) named tokens ───────────────────────────────────────── */ @define-color theme_bg_color @cy_bg; @@ -30,19 +39,19 @@ @define-color window_fg_color @cy_fg; @define-color view_bg_color @cy_base; @define-color view_fg_color @cy_fg; -@define-color headerbar_bg_color rgba(11,8,11,0.93); +@define-color headerbar_bg_color @cy_hdr; @define-color headerbar_fg_color @cy_fg; @define-color popover_bg_color @cy_bg2; @define-color popover_fg_color @cy_fg; @define-color card_bg_color @cy_bg2; @define-color dialog_bg_color @cy_bg; -@define-color sidebar_bg_color rgba(9,6,9,0.90); +@define-color sidebar_bg_color @cy_side; @define-color accent_color @cy_red2; @define-color accent_bg_color @cy_red; @define-color accent_fg_color #ffffff; @define-color destructive_color @cy_red; @define-color success_color @cy_cyan; -@define-color warning_color #ffb454; +@define-color warning_color @cy_warn; @define-color error_color @cy_red; /* ── base / windows ────────────────────────────────────────────────────── */ @@ -53,7 +62,7 @@ window, .background, dialog, assistant, messagedialog, .csd { /* headerbars / toolbars / menubars — glass + red HUD edge */ headerbar, .titlebar, .toolbar, toolbar, .primary-toolbar, .csd headerbar { - background-color: rgba(11,8,11,0.93); color: @cy_fg; + background-color: @cy_hdr; color: @cy_fg; border-bottom: 2px solid @cy_red; box-shadow: none; background-image: none; } menubar, .menubar { background-color: transparent; border-bottom: 1px solid alpha(@cy_red,0.5); } @@ -77,7 +86,7 @@ entry image, entry placeholder, placeholder { color: @cy_dim; } background-color: @cy_base; color: @cy_fg; } treeview.view:hover, row:hover, list row:hover, .view:hover { background-color: alpha(@cy_red,0.10); } -treeview.view header button { background-color: rgba(14,9,12,0.95); color: @cy_fg; border: 1px solid alpha(@cy_red,0.3); } +treeview.view header button { background-color: @cy_head; color: @cy_fg; border: 1px solid alpha(@cy_red,0.3); } /* ── selection — red HUD with a red left-spine ── */ selection { background-color: @cy_sel; color: #ffffff; } @@ -89,7 +98,7 @@ placessidebar row:selected, .sidebar row:selected, columnview row:selected { /* ── buttons — angular glass, red border ── */ button, .button, spinbutton button { - background-color: rgba(16,11,14,0.9); color: @cy_fg; + background-color: @cy_btn; color: @cy_fg; border: 1px solid @cy_border; border-radius: 2px; box-shadow: none; background-image: none; } button:hover { border-color: @cy_red; color: #ffffff; background-color: alpha(@cy_red,0.12); } @@ -101,7 +110,7 @@ button.flat:hover { background-color: alpha(@cy_red,0.12); border-color: alpha(@ /* ── sidebars / places ── */ .sidebar, placessidebar, stacksidebar, .navigation-sidebar, paned.horizontal > .sidebar { - background-color: rgba(9,6,9,0.90); color: @cy_fg; border-right: 1px solid alpha(@cy_red,0.3); + background-color: @cy_side; color: @cy_fg; border-right: 1px solid alpha(@cy_red,0.3); } /* ── scrollbars — red ── */ @@ -111,13 +120,13 @@ scrollbar slider:hover { background-color: @cy_red; } /* ── checks / radios / switches ── */ check, radio { background-color: @cy_base; border: 1px solid @cy_border; border-radius: 2px; } -check:checked, radio:checked { background-color: @cy_red; border-color: @cy_red2; color: #0a0d12; } +check:checked, radio:checked { background-color: @cy_red; border-color: @cy_red2; color: @cy_ink; } switch { background-color: @cy_bg2; border: 1px solid @cy_border; border-radius: 2px; } switch:checked { background-color: alpha(@cy_red,0.40); border-color: @cy_red; } switch slider { background-color: @cy_fg; border-radius: 2px; } /* ── progress / scales ── */ -trough { background-color: rgba(16,11,14,0.8); border-radius: 2px; } +trough { background-color: @cy_trough; border-radius: 2px; } progressbar progress, levelbar block.filled { background-color: @cy_red; } scale highlight { background-color: @cy_red; } scale slider { background-color: @cy_fg; border: 1px solid @cy_red; } @@ -139,7 +148,7 @@ notebook > header tab:checked { color: #ffffff; box-shadow: inset 0 -2px @cy_red .path-bar button:checked { color: @cy_red2; box-shadow: inset 0 -2px @cy_red; } /* ── misc ── */ -tooltip { background-color: rgba(5,7,9,0.96); color: @cy_red2; border: 1px solid @cy_red; } +tooltip { background-color: @cy_tip; color: @cy_red2; border: 1px solid @cy_red; } link, *:link { color: @cy_cyan; } spinner { color: @cy_red; } label.dim-label, .dim-label { color: @cy_dim; } diff --git a/assets/rio/shaders/blade-baseline/accumulate.slang b/assets/rio/shaders/blade-baseline/accumulate.slang new file mode 100644 index 00000000..7d5312b2 --- /dev/null +++ b/assets/rio/shaders/blade-baseline/accumulate.slang @@ -0,0 +1,91 @@ +#version 450 + +// newpixie CRT +// by Mattias Gustavsson +// adapted for slang by hunterk + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses - you may choose the one you like. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2016 Mattias Gustavsson +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ + +layout(push_constant) uniform Push +{ + vec4 SourceSize; + vec4 OriginalSize; + vec4 OutputSize; + uint FrameCount; + float acc_modulate; +} params; + +#pragma parameter acc_modulate "Accumulate Modulation" 0.65 0.0 1.0 0.01 +#define modulate params.acc_modulate + +#define tex0 PassFeedback1 +#define tex1 Source + +layout(std140, set = 0, binding = 0) uniform UBO +{ + mat4 MVP; +} global; + +#pragma stage vertex +layout(location = 0) in vec4 Position; +layout(location = 1) in vec2 TexCoord; +layout(location = 0) out vec2 vTexCoord; + +void main() +{ + gl_Position = global.MVP * Position; + vTexCoord = TexCoord; +} + +#pragma stage fragment +layout(location = 0) in vec2 vTexCoord; +layout(location = 0) out vec4 FragColor; +layout(set = 0, binding = 2) uniform sampler2D Source; +layout(set = 0, binding = 3) uniform sampler2D PassFeedback1; + +void main() +{ + vec4 a = texture(tex0, vTexCoord.xy) * vec4(modulate); + vec4 b = texture(tex1, vTexCoord.xy); + FragColor = max( a, b * 0.96 ); +} diff --git a/assets/rio/shaders/blade-baseline/blade-baseline.slang b/assets/rio/shaders/blade-baseline/blade-baseline.slang new file mode 100644 index 00000000..88b5575e --- /dev/null +++ b/assets/rio/shaders/blade-baseline/blade-baseline.slang @@ -0,0 +1,142 @@ +#version 450 + +layout(push_constant) uniform Push +{ + vec4 SourceSize; + vec4 OutputSize; + uint FrameCount; + float bloom; + float sheen; +} params; + +#pragma parameter bloom "Deck Bloom" 1.0 0.0 2.0 0.05 +#pragma parameter sheen "Deck Sheen" 1.0 0.0 2.0 0.05 +#define backbuffer accum1 +#define blurbuffer blur2 + +layout(std140, set = 0, binding = 0) uniform UBO +{ + mat4 MVP; +} global; + +#pragma stage vertex +layout(location = 0) in vec4 Position; +layout(location = 1) in vec2 TexCoord; +layout(location = 0) out vec2 vTexCoord; + +void main() +{ + gl_Position = global.MVP * Position; + vTexCoord = vec2(TexCoord.x, 1.0 - TexCoord.y); +} + +#pragma stage fragment +layout(location = 0) in vec2 vTexCoord; +layout(location = 0) out vec4 FragColor; +layout(set = 0, binding = 2) uniform sampler2D Source; +layout(set = 0, binding = 3) uniform sampler2D accum1; +layout(set = 0, binding = 4) uniform sampler2D blur2; + +vec3 fetch( sampler2D samp, vec2 tc ) + { + return texture( samp, vec2( tc.x, 1.0 - tc.y ) ).rgb; + } +float luma( vec3 c ) + { + return dot( c, vec3( 0.299, 0.587, 0.114 ) ); + } +float hash( vec2 p ) + { + return fract( sin( dot( p, vec2( 127.1, 311.7 ) ) ) * 43758.5453 ); + } + +#define BODY_DARK vec3( 0.055, 0.055, 0.060 ) +#define BODY_LITE vec3( 0.180, 0.170, 0.155 ) +#define BEVEL vec3( 0.340, 0.310, 0.270 ) +#define PHOS vec3( 0.004, 0.707, 0.698 ) +#define PHOS_HI vec3( 0.560, 0.960, 0.950 ) +#define LED vec3( 0.792, 0.325, 0.667 ) +#define STEEL vec3( 0.500, 0.480, 0.440 ) + +#define FRAME_X 26.0 +#define FRAME_Y 24.0 +#define CORNER_R 1.0 + +float sdfBox( vec2 p, vec2 b, float r ) + { + vec2 q = abs( p ) - b + r; + return length( max( q, vec2( 0.0 ) ) ) + min( max( q.x, q.y ), 0.0 ) - r; + } + +void main() +{ + vec2 uv = vTexCoord.xy; + vec2 res = params.OutputSize.xy; + vec2 px = uv * res; + vec2 pc = px - res * 0.5; + + float outer = sdfBox( pc, res * 0.5, CORNER_R ); + float screen = sdfBox( pc, res * 0.5 - vec2( FRAME_X, FRAME_Y ), max( CORNER_R - 1.0, 0.0 ) ); + + float bodyA = 1.0 - smoothstep( -0.9, 0.9, outer ); + float scrA = 1.0 - smoothstep( -0.9, 0.9, screen ); + + float bezel = 1.0 - smoothstep( 1.0, 3.0, abs( screen + 2.0 ) ); + float edge = 1.0 - smoothstep( 1.0, 3.0, abs( outer + 2.0 ) ); + + float grain = hash( floor( px * 0.5 ) ) * 0.5 + hash( floor( px * 0.25 ) ) * 0.5; + vec3 body = mix( BODY_DARK, BODY_LITE, 0.35 + grain * 0.30 ); + body += BEVEL * pow( clamp( 1.0 - abs( uv.y - 0.5 ) * 2.0, 0.0, 1.0 ), 3.0 ) * 0.35; + body = mix( body, BEVEL, bezel * 0.85 ); + body = mix( body, BEVEL * 1.4, edge * 0.6 ); + + vec2 screwUV[4]; + screwUV[0] = vec2( FRAME_X * 0.5, FRAME_Y * 0.5 ); + screwUV[1] = vec2( res.x - FRAME_X * 0.5, FRAME_Y * 0.5 ); + screwUV[2] = vec2( FRAME_X * 0.5, res.y - FRAME_Y * 0.5 ); + screwUV[3] = vec2( res.x - FRAME_X * 0.5, res.y - FRAME_Y * 0.5 ); + for ( int i = 0; i < 4; i++ ) + { + float sd = length( pc - ( screwUV[i] - res * 0.5 ) ); + body = mix( body, STEEL * ( 0.7 + 0.3 * grain ), 1.0 - smoothstep( 2.4, 4.2, sd ) ); + body = mix( body, vec3( 0.10 ), 1.0 - smoothstep( 0.0, 1.1, sd ) ); + } + float led = length( pc - ( vec2( res.x - FRAME_X * 0.55, FRAME_Y * 0.5 ) - res * 0.5 ) ); + float ledPulse = 0.75 + 0.25 * sin( float( params.FrameCount ) * 0.08 ); + body = mix( body, LED * ledPulse, 1.0 - smoothstep( 2.5, 6.5, led ) ); + + vec2 suv = ( px - vec2( FRAME_X, FRAME_Y ) ) / ( res - vec2( FRAME_X * 2.0, FRAME_Y * 2.0 ) ); + vec3 src = fetch( backbuffer, uv ); + vec3 blr = fetch( blurbuffer, uv + vec2( -0.0009, 0.0012 ) ); + float slum = luma( src ); + float ink = smoothstep( 0.060, 0.280, slum ); + float mat = smoothstep( 0.040, 0.320, luma( blr ) ); + + vec3 crt = src * vec3( 0.55, 1.05, 1.02 ) + blr * PHOS * 0.55; + crt += PHOS * mat * 0.48 * params.bloom; + crt *= 1.0 + 0.14 * mat; + crt += PHOS_HI * ink * 0.16; + + float scan = 0.80 + 0.20 * sin( px.y * 3.14159265 ); + crt *= mix( 1.0, scan, 0.60 ); + + float vscr = 0.94 + 0.06 * sin( px.x * 1.047 ); + crt *= mix( 1.0, vscr, 0.25 ); + + float flick = 0.96 + 0.04 * hash( vec2( float( params.FrameCount ), 0.0 ) ); + crt *= flick; + + crt += vec3( ( hash( floor( px ) ) - 0.5 ) * 0.012 ); + + vec2 vp = ( suv - 0.5 ) * vec2( 1.05, 1.0 ); + crt *= 1.0 - 0.34 * pow( clamp( length( vp ) * 1.30, 0.0, 1.0 ), 2.4 ); + + crt = mix( crt, PHOS * 0.35, bezel * 0.9 ); + crt += PHOS_HI * pow( clamp( suv.y, 0.0, 1.0 ), 8.0 ) * 0.05 * params.sheen; + crt = clamp( crt, vec3( 0.0 ), vec3( 1.0 ) ); + + vec3 col = mix( crt, body, 1.0 - scrA ); + float alpha = max( bodyA, scrA * 0.35 ); + + FragColor = vec4( col * alpha, alpha ); +} diff --git a/assets/rio/shaders/blade-baseline/blade-baseline.slangp b/assets/rio/shaders/blade-baseline/blade-baseline.slangp new file mode 100644 index 00000000..24fc84cb --- /dev/null +++ b/assets/rio/shaders/blade-baseline/blade-baseline.slangp @@ -0,0 +1,30 @@ +shaders = 4 + +shader0 = accumulate.slang +alias0 = accum1 +scale0 = 1.0 +scale_type0 = source +filter_linear0 = true + +shader1 = blur_horiz.slang +alias1 = blur1 +scale1 = 1.0 +scale_type1 = source +filter_linear1 = true + +shader2 = blur_vert.slang +alias2 = blur2 +scale2 = 1.0 +scale_type2 = source +filter_linear2 = true + +shader3 = blade-baseline.slang +filter_linear3 = true +scale_type3 = viewport + +acc_modulate = 0.14 +blur_x = 5.00 +blur_y = 5.00 + +bloom = 1.00 +sheen = 1.00 diff --git a/assets/rio/shaders/blade-baseline/blur_horiz.slang b/assets/rio/shaders/blade-baseline/blur_horiz.slang new file mode 100644 index 00000000..7361edf0 --- /dev/null +++ b/assets/rio/shaders/blade-baseline/blur_horiz.slang @@ -0,0 +1,96 @@ +#version 450 + +// newpixie CRT +// by Mattias Gustavsson +// adapted for slang by hunterk + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses - you may choose the one you like. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2016 Mattias Gustavsson +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ + +layout(push_constant) uniform Push +{ + vec4 SourceSize; + vec4 OriginalSize; + vec4 OutputSize; + uint FrameCount; + float blur_x; +} params; + +#pragma parameter blur_x "Horizontal Blur" 1.0 0.0 5.0 0.25 + +vec2 blur = vec2(params.blur_x, 0.0) * params.OutputSize.zw; + +layout(std140, set = 0, binding = 0) uniform UBO +{ + mat4 MVP; +} global; + +#pragma stage vertex +layout(location = 0) in vec4 Position; +layout(location = 1) in vec2 TexCoord; +layout(location = 0) out vec2 vTexCoord; + +void main() +{ + gl_Position = global.MVP * Position; + vTexCoord = TexCoord; +} + +#pragma stage fragment +layout(location = 0) in vec2 vTexCoord; +layout(location = 0) out vec4 FragColor; +layout(set = 0, binding = 2) uniform sampler2D Source; + +void main() +{ + vec2 uv = vTexCoord.xy; + vec4 sum = texture( Source, uv ) * 0.2270270270; + sum += texture(Source, vec2( uv.x - 4.0 * blur.x, uv.y - 4.0 * blur.y ) ) * 0.0162162162; + sum += texture(Source, vec2( uv.x - 3.0 * blur.x, uv.y - 3.0 * blur.y ) ) * 0.0540540541; + sum += texture(Source, vec2( uv.x - 2.0 * blur.x, uv.y - 2.0 * blur.y ) ) * 0.1216216216; + sum += texture(Source, vec2( uv.x - 1.0 * blur.x, uv.y - 1.0 * blur.y ) ) * 0.1945945946; + sum += texture(Source, vec2( uv.x + 1.0 * blur.x, uv.y + 1.0 * blur.y ) ) * 0.1945945946; + sum += texture(Source, vec2( uv.x + 2.0 * blur.x, uv.y + 2.0 * blur.y ) ) * 0.1216216216; + sum += texture(Source, vec2( uv.x + 3.0 * blur.x, uv.y + 3.0 * blur.y ) ) * 0.0540540541; + sum += texture(Source, vec2( uv.x + 4.0 * blur.x, uv.y + 4.0 * blur.y ) ) * 0.0162162162; + FragColor = sum; +} diff --git a/assets/rio/shaders/blade-baseline/blur_vert.slang b/assets/rio/shaders/blade-baseline/blur_vert.slang new file mode 100644 index 00000000..91d42c8b --- /dev/null +++ b/assets/rio/shaders/blade-baseline/blur_vert.slang @@ -0,0 +1,96 @@ +#version 450 + +// newpixie CRT +// by Mattias Gustavsson +// adapted for slang by hunterk + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses - you may choose the one you like. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2016 Mattias Gustavsson +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ + +layout(push_constant) uniform Push +{ + vec4 SourceSize; + vec4 OriginalSize; + vec4 OutputSize; + uint FrameCount; + float blur_y; +} params; + +#pragma parameter blur_y "Vertical Blur" 1.0 0.0 5.0 0.25 + +vec2 blur = vec2(0.0, params.blur_y) * params.OutputSize.zw; + +layout(std140, set = 0, binding = 0) uniform UBO +{ + mat4 MVP; +} global; + +#pragma stage vertex +layout(location = 0) in vec4 Position; +layout(location = 1) in vec2 TexCoord; +layout(location = 0) out vec2 vTexCoord; + +void main() +{ + gl_Position = global.MVP * Position; + vTexCoord = TexCoord; +} + +#pragma stage fragment +layout(location = 0) in vec2 vTexCoord; +layout(location = 0) out vec4 FragColor; +layout(set = 0, binding = 2) uniform sampler2D Source; + +void main() +{ + vec2 uv = vTexCoord.xy; + vec4 sum = texture( Source, uv ) * 0.2270270270; + sum += texture(Source, vec2( uv.x - 4.0 * blur.x, uv.y - 4.0 * blur.y ) ) * 0.0162162162; + sum += texture(Source, vec2( uv.x - 3.0 * blur.x, uv.y - 3.0 * blur.y ) ) * 0.0540540541; + sum += texture(Source, vec2( uv.x - 2.0 * blur.x, uv.y - 2.0 * blur.y ) ) * 0.1216216216; + sum += texture(Source, vec2( uv.x - 1.0 * blur.x, uv.y - 1.0 * blur.y ) ) * 0.1945945946; + sum += texture(Source, vec2( uv.x + 1.0 * blur.x, uv.y + 1.0 * blur.y ) ) * 0.1945945946; + sum += texture(Source, vec2( uv.x + 2.0 * blur.x, uv.y + 2.0 * blur.y ) ) * 0.1216216216; + sum += texture(Source, vec2( uv.x + 3.0 * blur.x, uv.y + 3.0 * blur.y ) ) * 0.0540540541; + sum += texture(Source, vec2( uv.x + 4.0 * blur.x, uv.y + 4.0 * blur.y ) ) * 0.0162162162; + FragColor = sum; +} diff --git a/assets/rio/styles/arctic.toml b/assets/rio/styles/arctic.toml index bf40f58a..67f51977 100644 --- a/assets/rio/styles/arctic.toml +++ b/assets/rio/styles/arctic.toml @@ -26,8 +26,8 @@ blur = true decorations = "Enabled" [fonts] -family = "FiraCode Nerd Font Mono" -size = 14 +family = "Geist Mono" +size = 16 use-drawable-chars = true hinting = true features = ["calt", "liga"] diff --git a/assets/rio/styles/dark.toml b/assets/rio/styles/blade.toml similarity index 87% rename from assets/rio/styles/dark.toml rename to assets/rio/styles/blade.toml index 71ea87d3..f39b59fb 100644 --- a/assets/rio/styles/dark.toml +++ b/assets/rio/styles/blade.toml @@ -1,4 +1,4 @@ -theme = "crimson" +theme = "blade" draw-bold-text-with-light-colors = true @@ -14,20 +14,20 @@ hide-mouse-cursor-when-typing = true [colors] -cursor = "#FF142D" -vi-cursor = "#FF6B7C" -selection-background = "#4A0A14" -selection-foreground = "#FFFFFF" +cursor = "#01B4B2" +vi-cursor = "#CA53AA" +selection-background = "#0D3E4B" +selection-foreground = "#01B4B2" [window] -opacity = 0.0 +opacity = 0.35 opacity-cells = false blur = true decorations = "Enabled" [fonts] -family = "FiraCode Nerd Font Mono" -size = 14 +family = "JetBrains Mono" +size = 15 use-drawable-chars = true hinting = true features = ["calt", "liga"] @@ -65,7 +65,7 @@ blinking-interval = 600 [effects] trail-cursor = true custom-mouse-cursor = false -trail-cursor-color = "#FF142D" +trail-cursor-color = "#01B4B2" [navigation] mode = "Tab" @@ -78,23 +78,23 @@ enable-scroll-bar = true [[navigation.color-automation]] program = "nvim" -color = "#FF3B52" +color = "#BB38A0" [[navigation.color-automation]] program = "vim" -color = "#FF3B52" +color = "#BB38A0" [[navigation.color-automation]] program = "ssh" -color = "#8C9299" +color = "#80297B" [[navigation.color-automation]] program = "sudo" -color = "#FF142D" +color = "#CA53AA" [[navigation.color-automation]] program = "btop" -color = "#C6CAD0" +color = "#029B9C" [panel] padding = [6] @@ -112,6 +112,6 @@ audio = true [renderer] backend = "Webgpu" -filters = ["__RIO_SHADERS__/crimson-smooth/crimson-smooth.slangp"] +filters = ["__RIO_SHADERS__/blade-baseline/blade-baseline.slangp"] strategy = "events" disable-unfocused-render = false diff --git a/assets/rio/styles/bloodmoon.toml b/assets/rio/styles/bloodmoon.toml index acadafc9..b6bef3ba 100644 --- a/assets/rio/styles/bloodmoon.toml +++ b/assets/rio/styles/bloodmoon.toml @@ -26,8 +26,8 @@ blur = true decorations = "Enabled" [fonts] -family = "FiraCode Nerd Font Mono" -size = 14 +family = "JetBrains Mono" +size = 15 use-drawable-chars = true hinting = true features = ["calt", "liga"] diff --git a/assets/rio/styles/cybercore.toml b/assets/rio/styles/cybercore.toml index a0506174..29eca9bb 100644 --- a/assets/rio/styles/cybercore.toml +++ b/assets/rio/styles/cybercore.toml @@ -26,8 +26,8 @@ blur = true decorations = "Enabled" [fonts] -family = "FiraCode Nerd Font Mono" -size = 14 +family = "JetBrains Mono" +size = 15 use-drawable-chars = true hinting = true features = ["calt", "liga"] diff --git a/assets/rio/styles/johnny.toml b/assets/rio/styles/johnny.toml index cc357cda..886e0ca6 100644 --- a/assets/rio/styles/johnny.toml +++ b/assets/rio/styles/johnny.toml @@ -26,8 +26,8 @@ blur = true decorations = "Enabled" [fonts] -family = "FiraCode Nerd Font Mono" -size = 14 +family = "JetBrains Mono" +size = 15 use-drawable-chars = true hinting = true features = ["calt", "liga"] diff --git a/assets/rio/styles/synthwave.toml b/assets/rio/styles/synthwave.toml index 7e0b876f..8514640a 100644 --- a/assets/rio/styles/synthwave.toml +++ b/assets/rio/styles/synthwave.toml @@ -26,8 +26,8 @@ blur = true decorations = "Enabled" [fonts] -family = "FiraCode Nerd Font Mono" -size = 14 +family = "JetBrains Mono" +size = 15 use-drawable-chars = true hinting = true features = ["calt", "liga"] diff --git a/assets/rio/themes/blade.toml b/assets/rio/themes/blade.toml new file mode 100644 index 00000000..04ccbeda --- /dev/null +++ b/assets/rio/themes/blade.toml @@ -0,0 +1,54 @@ +[colors] +background = '#030507' +foreground = '#01B4B2' + +black = '#081A22' +red = '#CA53AA' +green = '#029B9C' +yellow = '#056B7A' +blue = '#0D3E4B' +magenta = '#BB38A0' +cyan = '#01B4B2' +white = '#087B8A' + +light-black = '#0C2B39' +light-red = '#D977BF' +light-green = '#01B4B2' +light-yellow = '#087B8A' +light-blue = '#056B7A' +light-magenta = '#D977BF' +light-cyan = '#4E1446' +light-white = '#08869B' +light-foreground = '#01B4B2' + +dim-black = '#030507' +dim-red = '#80297B' +dim-green = '#085867' +dim-yellow = '#081A22' +dim-blue = '#081A22' +dim-magenta = '#67216A' +dim-cyan = '#085867' +dim-white = '#056B7A' +dim-foreground = '#056B7A' + +cursor = '#01B4B2' +vi-cursor = '#CA53AA' + +tabs = '#081A22' +tabs-active = '#01B4B2' +tab-border = '#0C2B39' +bar = '#030507' + +split = '#0C2B39' +split-active = '#01B4B2' + +selection-background = '#0D3E4B' +selection-foreground = '#01B4B2' + +search-match-background = '#085867' +search-match-foreground = '#01B4B2' +search-focused-match-background = '#01B4B2' +search-focused-match-foreground = '#030507' + +hint-background = '#01B4B2' +hint-foreground = '#030507' diff --git a/assets/wallpapers/blade_runner/chinatown.mp4 b/assets/wallpapers/blade_runner/chinatown.mp4 new file mode 100644 index 00000000..67258ce0 Binary files /dev/null and b/assets/wallpapers/blade_runner/chinatown.mp4 differ diff --git a/assets/wallpapers/blade_runner/city.mp4 b/assets/wallpapers/blade_runner/city.mp4 new file mode 100644 index 00000000..33392e2c Binary files /dev/null and b/assets/wallpapers/blade_runner/city.mp4 differ diff --git a/components/modules/appsmenu.ts b/components/modules/appsmenu.ts index 977e5016..f3fc6c6b 100644 --- a/components/modules/appsmenu.ts +++ b/components/modules/appsmenu.ts @@ -4,6 +4,7 @@ import { interval, timeout, execAsync } from "astal" import Gdk from "gi://Gdk?version=3.0" import Gtk from "gi://Gtk?version=3.0" import Gio from "gi://Gio" +import GLib from "gi://GLib" import { SCREEN_WIDTH, SCREEN_HEIGHT, CYBER_DIR } from "../../env.ts" import { NEON, USER, USER_A, f, onColorChange, menuBg, glassMode } from "./colors.ts" import { sndOn, sndFile, animOn } from "./config.ts" @@ -315,10 +316,41 @@ export const closeWheel = () => { active = false; introTarget = 0; animate() const r = wheelCfg.onReset; if (r) r() } +const LAUNCH_HELPER = `${GLib.get_home_dir()}/.local/bin/cyberarch-launch-desktop` +const ensureSessionEnv = () => { + const rt = GLib.getenv("XDG_RUNTIME_DIR") || `/run/user/${GLib.get_user_name() === "root" ? 0 : 1000}` + if (!GLib.getenv("XDG_RUNTIME_DIR")) GLib.setenv("XDG_RUNTIME_DIR", rt, true) + if (!GLib.getenv("DBUS_SESSION_BUS_ADDRESS")) GLib.setenv("DBUS_SESSION_BUS_ADDRESS", `unix:path=${rt}/bus`, true) + if (!GLib.getenv("XDG_SESSION_TYPE")) GLib.setenv("XDG_SESSION_TYPE", "wayland", true) + if (!GLib.getenv("XDG_CURRENT_DESKTOP")) GLib.setenv("XDG_CURRENT_DESKTOP", "Hyprland", true) + if (!GLib.getenv("XDG_SESSION_DESKTOP")) GLib.setenv("XDG_SESSION_DESKTOP", "Hyprland", true) + if (!GLib.getenv("DISPLAY")) GLib.setenv("DISPLAY", ":0", true) +} +const launchAppRobust = (a) => { + ensureSessionEnv() + const name = (() => { try { return a.get_name?.() || "unknown" } catch { return "unknown" } })() + const id = (() => { try { return a.get_id?.() || "" } catch { return "" } })() + const file = (() => { try { return a.get_filename?.() || "" } catch { return "" } })() + print(`[apps] ACTIVATE name=${name} id=${id} file=${file}`) + if (file) { + execAsync([LAUNCH_HELPER, file, id, name]).catch((e) => print("[apps] desktop helper error:", e)) + return + } + try { + const ctx = new Gio.AppLaunchContext() + ctx.setenv("DBUS_SESSION_BUS_ADDRESS", GLib.getenv("DBUS_SESSION_BUS_ADDRESS") || "") + ctx.setenv("XDG_SESSION_TYPE", "wayland") + ctx.setenv("XDG_CURRENT_DESKTOP", "Hyprland") + const ok = a.launch([], ctx) + print(`[apps] Gio launch result=${ok}`) + } catch (e) { print("[apps] Gio launch error:", e) } +} + export const openAppsMenu = () => { if (!menuWin) return if (active) { closeWheel(); return } - openWheel({ title: "APPS", subtitle: "// CYBERDECK.OS — RUNNING", footer: FOOTER_APPS, searchable: true, onActivate: (a) => { try { a.launch([], null) } catch (e) { print("[apps] launch:", e) } closeWheel() }, onSecondary: null, onReset: null, emptyText: "// NO APPS" }, buildAppEntries()) + appInfoCache = null + openWheel({ title: "APPS", subtitle: "// CYBERDECK.OS — RUNNING", footer: FOOTER_APPS, searchable: true, keymode: Keymode.EXCLUSIVE, onActivate: (a) => { launchAppRobust(a); closeWheel() }, onSecondary: null, onReset: null, emptyText: "// NO APPS" }, buildAppEntries()) } export const AppsMenuWindow = () => { @@ -333,7 +365,23 @@ export const AppsMenuWindow = () => { evt.connect("button-press-event", (_w, e) => { if (!active) return true let b = 1; try { b = e.get_button?.()[1] ?? e.button } catch {} - const r = rowAtY(mouseY) + // Use the click event's coordinates directly. The previous code relied on + // a cached motion-notify Y position, which can be stale after wheel/touchpad scroll. + let clickY = mouseY + try { + const c = e.get_coords?.() + if (c && c.length >= 3 && c[0]) clickY = c[2] + else if (typeof e.y === "number") clickY = e.y + } catch { if (typeof e.y === "number") clickY = e.y } + mouseY = clickY + let r = rowAtY(clickY) + if (!r && RENDER.length) { + r = RENDER.reduce((best, cur) => { + const by = (best.y0 + best.y1) / 2, cy = (cur.y0 + cur.y1) / 2 + return Math.abs(cy - clickY) < Math.abs(by - clickY) ? cur : best + }, RENDER[0]) + } + print(`[apps] CLICK y=${clickY} row=${r ? r.entry?.label : "none"}`) if (b === 3) { if (r && wheelCfg.onSecondary) wheelCfg.onSecondary(r.entry.data); else closeWheel(); return true } if (r) { if (typeof r.idxAbs === "number" && r.idxAbs !== Math.round(scroll)) { @@ -372,7 +420,10 @@ export const AppsMenuWindow = () => { if (k === Gdk.KEY_Escape) { if (query) { query = ""; applyFilter() } else closeWheel(); return true } if (k === Gdk.KEY_Up) { scrollTarget -= 1; const n = filtered.length; if (n <= VISIBLE) scrollTarget = Math.max(0, scrollTarget); beep(); animate(); return true } if (k === Gdk.KEY_Down) { scrollTarget += 1; const n = filtered.length; if (n <= VISIBLE) scrollTarget = Math.min(n - 1, scrollTarget); beep(); animate(); return true } - if (k === Gdk.KEY_Return || k === Gdk.KEY_KP_Enter) { if (n) activate(filtered[mod(Math.round(scroll), n)]); return true } + if (k === Gdk.KEY_Return || k === Gdk.KEY_KP_Enter) { + if (n) { const e = filtered[mod(Math.round(scroll), n)]; print(`[apps] ENTER row=${e?.label || "unknown"}`); activate(e) } + return true + } if (k === Gdk.KEY_BackSpace) { if (wheelCfg.searchable && query) { query = query.slice(0, -1); applyFilter() } return true } const uni = Gdk.keyval_to_unicode(k) diff --git a/components/modules/aurbar.ts b/components/modules/aurbar.ts index a94753a0..22bf0aac 100644 --- a/components/modules/aurbar.ts +++ b/components/modules/aurbar.ts @@ -10,7 +10,7 @@ import { CYBER_DIR, USER_DIR } from "../../env.ts" import { TITLE, RAJDHANI, RAJDHANI_MED } from "./fonts.ts" import { makePlane, tiltText, strokePath } from "./proj.ts" import { passthrough } from "./anim.ts" -import { NEON, USER_A, onColorChange, glassAlpha, glassMode, tintSurface, tintSurfaceFlat, imgTint, circleTint, neonBtn } from "./colors.ts" +import { NEON, USER_A, onColorChange, glassAlpha, glassMode, tintSurface, tintSurfaceFlat, imgTint, circleTint, neonBtn, aurTitleTint } from "./colors.ts" import { animOn } from "./config.ts" const Cairo = (imports as any).cairo @@ -263,7 +263,7 @@ const draw = (ctx: any) => { const tcx = BARX + 18 pfill(ctx, [[tcx, ROWY - 7], [tcx + 8, ROWY + 5], [tcx - 8, ROWY + 5]], BLACK, 0.92 * V.textA) const tfs = cTitle.length > 22 ? 13 : 16 - tiltText(ctx, plane, BARX + 34, ROWY + tfs * 0.34, cTitle, TFONT, tfs, neonBtn.value ? NEON.press : (glassMode.value ? WHT : BLACK), 0.95 * V.textA, { align: "l", bold: true, glow: (glassMode.value || neonBtn.value) ? 0.55 : 0 } as any) + tiltText(ctx, plane, BARX + 34, ROWY + tfs * 0.34, cTitle, TFONT, tfs, neonBtn.value ? NEON.press : (aurTitleTint.value || (glassMode.value ? WHT : BLACK)), 0.95 * V.textA, { align: "l", bold: true, glow: (glassMode.value || neonBtn.value || aurTitleTint.value) ? 0.55 : 0 } as any) ctx.restore() if (V.badgeA > 0.01) pimg(ctx, png("updt.png"), BARX + BW - 45, ROWY, 54, 46, V.badgeA, BADGE_ROT, true, (imgTint.value && !glassMode.value) ? BLACK : null) diff --git a/components/modules/cmodal.ts b/components/modules/cmodal.ts index dd64ec8d..05321f3e 100644 --- a/components/modules/cmodal.ts +++ b/components/modules/cmodal.ts @@ -273,6 +273,7 @@ export const createModal = (spec) => { if (contains(r, x, y)) { hit = true if (r.kind === "sld") { drag = r; dragKey = r.key; r.on(sliderParam(r, x, y)); area.queue_draw() } + else if (r.onXY) { drag = r; dragKey = r.key ?? null; r.onXY(x, y); area.queue_draw() } else if (b === 3 && r.onRight) r.onRight() else if (r.on) { if (r.key) { pressKey = r.key; area.queue_draw() }; r.on() } break @@ -286,7 +287,7 @@ export const createModal = (spec) => { if (!point) { if (hoverKey !== null) { hoverKey = null; area.queue_draw() }; return false } const [x, y] = point lastXY = [x, y] - if (drag) { drag.on(sliderParam(drag, x, y)); area.queue_draw(); return false } + if (drag) { if (drag.onXY) drag.onXY(x, y); else drag.on(sliderParam(drag, x, y)); area.queue_draw(); return false } let nk: any = null for (const r of hitRegions) { if (r.hoverable && contains(r, x, y)) { nk = r.key; break } } if (nk !== hoverKey) { hoverKey = nk; area.queue_draw() } @@ -303,7 +304,7 @@ export const createModal = (spec) => { }) const kv = (e) => { let k = 0; try { const r = e.get_keyval?.(); k = r ? r[1] : e.keyval } catch {} return k } const kmask = (e) => { try { const r = e.get_state?.(); return r ? r[1] : (e.state || 0) } catch { return 0 } } - const onKeyPress = (_w, e) => { const k = kv(e); const m = kmask(e); if (spec.onKeyRaw) spec.onKeyRaw(k, m, true); if (k === Gdk.KEY_Escape) ctrl.close(); else spec.onKey?.(k); return true } + const onKeyPress = (_w, e) => { const k = kv(e); const m = kmask(e); if (spec.onKeyRaw) spec.onKeyRaw(k, m, true); if (k === Gdk.KEY_Escape) { let eaten = false; try { eaten = spec.onKey?.(k) === true } catch {} if (!eaten) ctrl.close() } else spec.onKey?.(k); return true } const onKeyRelease = (_w, e) => { if (spec.onKeyRaw) spec.onKeyRaw(kv(e), kmask(e), false); return true } const winOpts: any = { name: `modal_${name}`, namespace: `modal_${name}`, className: "aug modal", layer: Layer.OVERLAY, exclusivity: Exclusivity.IGNORE, keymode: spec.keymode ?? Keymode.EXCLUSIVE, visible: false, child: evt } @@ -1391,7 +1392,8 @@ fi`).then(() => timeout(200, fetchInitApps)) const pulse = 0.94 + 0.06 * Math.sin(Date.now() / 500) const vcx = FW / 2, vcy = FH / 2, reach = Math.hypot(FW, FH) / 2 const vg = new Cairo.RadialGradient(vcx, vcy, reach * 0.16, vcx, vcy, reach * 0.98) - const [ov0, ov1, ov2] = USER.overlay + // the sys monitor veil is dusk orange, kill mode and screenshot use the cyan overlay + const [ov0, ov1, ov2] = USER.sysveil || USER.overlay vg.addColorStopRGBA(0, ov0 * 0.08, ov1 * 0.08, ov2 * 0.08, 0.46) vg.addColorStopRGBA(0.5, ov0 * 0.2, ov1 * 0.2, ov2 * 0.2, 0.68) vg.addColorStopRGBA(1, ov0 * 0.62, ov1 * 0.62, ov2 * 0.62, 0.92 * pulse) @@ -1641,6 +1643,7 @@ const KeysCtrl = () => createModal({ const cregistry: any = {} const register = (c) => { cregistry[c.name] = c; return c.win } +export const registerCModal = (c) => { cregistry[c.name] = c } const chgCbs: any[] = [] export const onModalChange = (cb) => { chgCbs.push(cb) } const fireChange = () => { for (const cb of chgCbs) cb() } diff --git a/components/modules/colors.ts b/components/modules/colors.ts index a6ca238c..5bc0d765 100644 --- a/components/modules/colors.ts +++ b/components/modules/colors.ts @@ -1,6 +1,7 @@ import GLib from "gi://GLib" import GdkPixbufLib from "gi://Gdk?version=3.0" import { execAsync } from "astal" +import { applyWmFromTheme } from "./wmconfig.ts" import { CYBER_DIR, USER_DIR } from "../../env.ts" export type RGB = [number, number, number] @@ -44,6 +45,7 @@ export const NEON: Record = { aurwht: [232, 255, 240], f25: [242, 91, 86], overlay: [255, 42, 58], + sysveil: [255, 42, 58], pure: [255, 255, 255], glassacc: [196, 248, 255], modalbg: [0, 0, 0], @@ -87,7 +89,7 @@ export const NEON: Record = { radioctl: [94, 244, 248], } -export type PaletteName = "NETWATCH" | "DARK" | "KITTY" | "BLOODMOON" | "ARCTIC" | "SYNTHWAVE" | "JOHNNY" | "GHOST" +export type PaletteName = "NETWATCH" | "BLADE" | "KITTY" | "BLOODMOON" | "ARCTIC" | "SYNTHWAVE" | "JOHNNY" | "GHOST" export const PALETTES: Record>> = { NETWATCH: { @@ -119,63 +121,77 @@ export const PALETTES: Record>> = { glassacc: [196, 248, 255], overlay: [255, 42, 58], }, - DARK: { - red: [0, 0, 0], cyan: [0, 0, 0], magenta: [0, 0, 0], - green: [0, 0, 0], amber: [0, 0, 0], blue: [0, 0, 0], - white: [255, 255, 255], dim: [10, 10, 12], grid: [0, 0, 0], - dock: [0, 0, 0], press: [255, 20, 45], badge: [255, 20, 45], - stamina: [255, 20, 45], ram: [0, 0, 0], netinfo: [0, 0, 0], - cpu: [0, 0, 0], - notifred: [0, 0, 0], - notifyel: [0, 0, 0], - notifcyn: [0, 0, 0], - goldf: [8, 8, 10], - goldd: [4, 4, 5], - notifgrey: [255, 255, 255], - glyphcol: [0, 0, 0], - msggrey: [10, 10, 12], - dimred: [0, 0, 0], - appsred: [0, 0, 0], - hudcyan: [255, 20, 45], - darkred: [0, 0, 0], - notifbadge: [255, 20, 45], - aurgreen: [0, 0, 0], - aurbrt: [255, 255, 255], - aurblack: [0, 0, 0], - aurwht: [255, 255, 255], - f25: [0, 0, 0], - pure: [255, 255, 255], - glassacc: [255, 255, 255], - overlay: [0, 0, 0], + // replaced the old dark theme. dusk amber/rust base, pink/purple only on + // notifications and toasts, terminal interfaces in cyan/teal + BLADE: { + red: [183, 81, 7], cyan: [1, 180, 178], magenta: [202, 83, 170], + green: [146, 99, 43], amber: [195, 120, 18], blue: [86, 155, 178], + white: [224, 214, 200], dim: [127, 123, 157], grid: [26, 18, 14], + dock: [183, 81, 7], press: [187, 56, 160], badge: [195, 120, 18], + stamina: [146, 99, 43], ram: [189, 128, 59], netinfo: [189, 128, 59], + cpu: [195, 120, 18], + notifred: [202, 83, 170], + notifyel: [195, 120, 18], + notifcyn: [1, 180, 178], + goldf: [58, 28, 122], + goldd: [34, 16, 70], + notifgrey: [127, 123, 157], + glyphcol: [187, 56, 160], + msggrey: [127, 123, 157], + dimred: [116, 50, 8], + appsred: [195, 120, 18], + hudcyan: [2, 155, 156], + darkred: [80, 41, 11], + notifbadge: [187, 56, 160], + aurgreen: [187, 56, 160], + aurbrt: [217, 119, 191], + aurblack: [42, 8, 36], + aurwht: [224, 214, 200], + f25: [187, 56, 160], + pure: [232, 226, 216], + glassacc: [1, 180, 178], + overlay: [1, 180, 178], + dockvh: [255, 230, 126], + dockhh: [255, 230, 126], + wheelfg: [1, 180, 178], + modalhov: [1, 180, 178], + notiffg: [1, 180, 178], + notiflbl: [1, 180, 178], + notifheads: [217, 119, 191], + notiftitle: [2, 155, 156], + notifphone: [187, 56, 160], + notifmail: [187, 56, 160], + notifbg: [13, 66, 72], + sysveil: [183, 81, 7], }, KITTY: { - red: [255, 65, 185], cyan: [225, 110, 255], magenta: [255, 80, 210], - green: [255, 150, 230], amber: [255, 150, 205], blue: [210, 140, 255], - white: [255, 240, 250], dim: [150, 95, 160], grid: [35, 15, 40], - dock: [255, 65, 185], press: [225, 110, 255], badge: [255, 105, 200], - stamina: [255, 150, 230], ram: [225, 110, 255], netinfo: [255, 80, 210], - cpu: [255, 65, 185], - notifred: [255, 65, 185], - notifyel: [255, 150, 205], - notifcyn: [255, 65, 185], - goldf: [76, 45, 61], - goldd: [45, 27, 36], - notifgrey: [255, 240, 250], - glyphcol: [255, 65, 185], - msggrey: [150, 95, 160], - dimred: [255, 65, 185], - appsred: [255, 65, 185], - hudcyan: [255, 105, 200], - darkred: [255, 65, 185], - notifbadge: [255, 105, 200], - aurgreen: [255, 150, 230], - aurbrt: [255, 240, 250], - aurblack: [35, 15, 40], - aurwht: [255, 240, 250], - f25: [255, 65, 185], - pure: [255, 240, 250], - glassacc: [255, 240, 250], - overlay: [255, 80, 210], + red: [255, 170, 215], cyan: [255, 200, 232], magenta: [255, 180, 222], + green: [255, 205, 236], amber: [255, 206, 230], blue: [255, 195, 228], + white: [255, 248, 252], dim: [222, 180, 205], grid: [50, 30, 44], + dock: [255, 170, 215], press: [255, 190, 226], badge: [255, 182, 222], + stamina: [255, 205, 236], ram: [255, 195, 228], netinfo: [255, 180, 222], + cpu: [255, 170, 215], + notifred: [255, 170, 215], + notifyel: [255, 206, 230], + notifcyn: [255, 170, 215], + goldf: [92, 60, 76], + goldd: [56, 36, 47], + notifgrey: [255, 248, 252], + glyphcol: [255, 170, 215], + msggrey: [222, 180, 205], + dimred: [255, 170, 215], + appsred: [255, 170, 215], + hudcyan: [255, 182, 222], + darkred: [255, 170, 215], + notifbadge: [255, 182, 222], + aurgreen: [255, 205, 236], + aurbrt: [255, 248, 252], + aurblack: [50, 30, 44], + aurwht: [255, 248, 252], + f25: [255, 170, 215], + pure: [255, 248, 252], + glassacc: [255, 248, 252], + overlay: [255, 180, 222], }, BLOODMOON: { red: [255, 32, 32], cyan: [255, 60, 60], magenta: [255, 40, 60], @@ -375,9 +391,10 @@ const DERIVE: Record = { } const resetAlpha = () => { for (const k of Object.keys(USER_A)) if (!OVR[k]) USER_A[k] = 1 } -const applyDeriveTable = () => { +const applyDeriveTable = (pinned: Record>> = PALETTES, name = curPalette) => { + const pin = pinned[name] || {} for (const k of Object.keys(DERIVE)) { - if (OVR[k]) continue + if (OVR[k] || pin[k]) continue const [src, mul] = DERIVE[k] const s = NEON[src] if (!s) continue @@ -389,8 +406,8 @@ const applyDeriveTable = () => { export const imgTint = { value: null as RGB | null, strength: 0 } const IMG_TINT: Record = { ARCTIC: [[255, 255, 255], 1], - DARK: [[0, 0, 0], 0.97], - KITTY: [[255, 80, 210], 0.85], + BLADE: [[81, 37, 11], 0.62], + KITTY: [[255, 180, 222], 0.8], JOHNNY: [[255, 208, 60], 0.85], BLOODMOON: [[255, 32, 32], 0.9], GHOST: [[0, 255, 120], 0.88], @@ -407,7 +424,7 @@ const updateImgTint = (name: string) => { export const tintSurface = (ctx: any, surf: any, w: number, h: number, a = 1, colorOverride: RGB | null = null, strength = -1) => { const tc = colorOverride || imgTint.value if (!tc || !surf) return - const st = strength >= 0 ? strength : imgTint.strength + const st = strength >= 0 ? strength : (colorOverride ? 1 : imgTint.strength) try { ctx.setSourceSurface(surf, 0, 0); ctx.paintWithAlpha(a) ctx.setOperator(27) @@ -423,7 +440,7 @@ export const tintSurfaceFlat = (ctx: any, surf: any, w: number, h: number, a = 1 try { ctx.setSourceSurface(surf, 0, 0) const pat = ctx.getSource() - ctx.setSourceRGBA(tc[0] / 255, tc[1] / 255, tc[2] / 255, imgTint.strength * a) + ctx.setSourceRGBA(tc[0] / 255, tc[1] / 255, tc[2] / 255, (colorOverride ? 1 : imgTint.strength) * a) ctx.mask(pat) } catch {} } @@ -438,25 +455,26 @@ export const tintOpaque = (ctx: any, w: number, h: number, a = 1) => { ctx.restore() } -export const tintPixbuf = (ctx: any, pb: any, x: number, y: number, a = 1) => { - const tc = imgTint.value +export const tintPixbuf = (ctx: any, pb: any, x: number, y: number, a = 1, colorOverride: RGB | null = null, strength = -1) => { + const tc = colorOverride || imgTint.value if (!tc || !pb) return + const st = strength >= 0 ? strength : (colorOverride ? 1 : imgTint.strength) try { GdkPixbufLib.cairo_set_source_pixbuf(ctx, pb, x, y) const pat = ctx.getSource() ctx.paintWithAlpha(a) ctx.setOperator(27) - ctx.setSourceRGBA(tc[0] / 255, tc[1] / 255, tc[2] / 255, imgTint.strength * a) + ctx.setSourceRGBA(tc[0] / 255, tc[1] / 255, tc[2] / 255, st * a) ctx.mask(pat) ctx.setOperator(2) } catch {} } -const GLASS_ALPHA: Record = { GHOST: 0.14, DARK: 0.85 } +const GLASS_ALPHA: Record = { GHOST: 0.14, BLADE: 0.85 } export const glassAlpha = { value: 1 } const updateGlassAlpha = (name: string) => { glassAlpha.value = GLASS_ALPHA[name] ?? 1 } -const GLASS_MODE: Record = { ARCTIC: true, DARK: true } +const GLASS_MODE: Record = { ARCTIC: true, BLADE: true } export const glassMode = { value: false } const updateGlassMode = (name: string) => { glassMode.value = GLASS_MODE[name] ?? false } @@ -465,9 +483,9 @@ const MENU_BG_DEF: MenuBg = { bg: [2, 1, 4], bgA: 0.5, fog: [255, 42, 58], fogA: const MENU_BG: Record = { NETWATCH: { bg: [2, 1, 4], bgA: 0.5, fog: [255, 42, 58], fogA: 0.18 }, ARCTIC: { bg: [2, 1, 4], bgA: 0.42, fog: [255, 255, 255], fogA: 0.18 }, - KITTY: { bg: [10, 2, 8], bgA: 0.5, fog: [255, 65, 185], fogA: 0.2 }, + KITTY: { bg: [16, 6, 12], bgA: 0.5, fog: [255, 185, 224], fogA: 0.2 }, JOHNNY: { bg: [2, 4, 10], bgA: 0.5, fog: [255, 208, 60], fogA: 0.18 }, - DARK: { bg: [0, 0, 0], bgA: 0.55, fog: [140, 15, 30], fogA: 0.22 }, + BLADE: { bg: [3, 2, 2], bgA: 0.55, fog: [128, 33, 122], fogA: 0.26 }, BLOODMOON: { bg: [6, 0, 0], bgA: 0.5, fog: [255, 32, 32], fogA: 0.2 }, GHOST: { bg: [0, 6, 3], bgA: 0.45, fog: [0, 255, 120], fogA: 0.18 }, SYNTHWAVE: { bg: [6, 0, 10], bgA: 0.5, fog: [255, 60, 220], fogA: 0.2 }, @@ -482,8 +500,10 @@ type MapAccent = { clock: RGB; city: RGB; forecast: RGB | null } const MAP_ACCENT_DEF: MapAccent = { clock: [130, 231, 215], city: [176, 255, 157], forecast: null } const MAP_ACCENT: Record = { ARCTIC: { clock: [255, 255, 255], city: [130, 220, 255], forecast: null }, + KITTY: { clock: [255, 182, 222], city: [255, 205, 236], forecast: [255, 180, 222] }, SYNTHWAVE: { clock: [255, 60, 220], city: [100, 255, 170], forecast: [255, 60, 220] }, JOHNNY: { clock: [255, 208, 60], city: [176, 255, 157], forecast: null }, + BLADE: { clock: [189, 128, 59], city: [2, 155, 156], forecast: [1, 180, 178] }, } export const mapAccent = { ...MAP_ACCENT_DEF } const updateMapAccent = (name: string) => { @@ -500,7 +520,8 @@ type RGB01 = [number, number, number] type HudSoft = { acc: RGB01; label: RGB01 } const HUD_SOFT: Record = { ARCTIC: { acc: [1, 1, 1], label: [1, 1, 1] }, - KITTY: { acc: [1, 0.2549, 0.7255], label: [1, 0.4118, 0.7843] }, + KITTY: { acc: [1, 0.6667, 0.8431], label: [1, 0.7451, 0.8824] }, + BLADE: { acc: [0.004, 0.707, 0.698], label: [0.325, 0.878, 0.871] }, } export const hudSoft = { acc: [1, 1, 1] as RGB01, label: [1, 1, 1] as RGB01 } const updateHudSoft = (name: string) => { @@ -515,7 +536,7 @@ const NEON_BTN: Record = { DARK: true } export const neonBtn = { value: false } const updateNeonBtn = (name: string) => { neonBtn.value = NEON_BTN[name] ?? false } -const CIRCLE_TINT: Record = { DARK: [255, 20, 45] } +const CIRCLE_TINT: Record = { BLADE: [187, 56, 160] } export const circleTint = { value: null as RGB | null } const updateCircleTint = (name: string) => { const e = CIRCLE_TINT[name] @@ -558,6 +579,16 @@ const updateNotifBubble = (name: string) => { notifBubble.value = (e || OVR.notifphone) ? NEON.notifphone : null } +export const notifIconTint = { value: null as RGB | null } +const updateNotifIconTint = (name: string) => { + notifIconTint.value = curPalette === "BLADE" ? NEON.notifphone : null +} + +export const aurTitleTint = { value: null as RGB | null } +const updateAurTitleTint = (name: string) => { + aurTitleTint.value = curPalette === "BLADE" ? NEON.aurbrt : null +} + const changeBus: Array<() => void> = [] export const onColorChange = (cb: () => void): (() => void) => { changeBus.push(cb); return () => { const i = changeBus.indexOf(cb); if (i >= 0) changeBus.splice(i, 1) } } const notifyColorChange = () => { for (const cb of [...changeBus]) { try { cb() } catch (e) { print("[color] notify:", e) } } } @@ -581,19 +612,25 @@ export const rgbToHex = ([r, g, b]: RGB) => "#" + [r, g, b].map((v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0")).join("") // each theme has its own .toml terminal file, this maps them -const RIO_STYLES: Record = { GHOST: "ghost", KITTY: "kitty", SYNTHWAVE: "synthwave", ARCTIC: "arctic", BLOODMOON: "bloodmoon", DARK: "dark", JOHNNY: "johnny" } +const RIO_STYLES: Record = { GHOST: "ghost", KITTY: "kitty", SYNTHWAVE: "synthwave", ARCTIC: "arctic", BLOODMOON: "bloodmoon", BLADE: "blade", JOHNNY: "johnny" } const applyRioStyle = (name: string) => { execAsync([`${CYBER_DIR}/scripts/rio-style`, RIO_STYLES[name] ?? "cybercore"]).catch(() => "") } +const applyShellTheme = (name: string) => { + execAsync(["bash", "-c", `CYBER_DIR="${CYBER_DIR}" "${CYBER_DIR}/scripts/shell-theme" "${name}"`]) + .then(() => { applyWmFromTheme() }) + .catch(() => "") +} + let curPalette = "NETWATCH" const applyDerived = (name: string) => { curPalette = name resetAlpha() updateImgTint(name) - applyDeriveTable() + applyDeriveTable(PALETTES, name) updateGlassAlpha(name) updateMenuBg(name) updateMapAccent(name) @@ -605,6 +642,9 @@ const applyDerived = (name: string) => { updateLauncherLabelTint(name) updateRadioBg(name) updateNotifBubble(name) + updateNotifIconTint(name) + updateAurTitleTint(name) + applyShellTheme(name) } export const applyPalette = (name: string) => { @@ -657,7 +697,7 @@ const USER_PATH = `${USER_DIR}/user_colors.lua` export const loadUserColors = (): void => { try { const [ok, bytes] = GLib.file_get_contents(USER_PATH) - if (!ok) return + if (!ok) { applyPalette("NETWATCH"); return } const src = new TextDecoder().decode(bytes) const lines = src.split("\n") let saved = "" diff --git a/components/modules/glass.ts b/components/modules/glass.ts index fd8136ca..37461138 100644 --- a/components/modules/glass.ts +++ b/components/modules/glass.ts @@ -12,13 +12,16 @@ import { TITLE, MONO, ICONF } from "./fonts.ts" export { TITLE, MONO, ICONF } export const CYAN: [number, number, number] = [...USER.dock] as [number, number, number] const syncCyan = () => { - const src = neonBtn.value ? USER.press : USER.dock + const src = neonBtn.value ? USER.press : USER.wheelfg CYAN[0] = src[0]; CYAN[1] = src[1]; CYAN[2] = src[2] } +export const RED: [number, number, number] = [...USER.wheelfg] as [number, number, number] +const syncRed = () => { RED[0] = USER.wheelfg[0]; RED[1] = USER.wheelfg[1]; RED[2] = USER.wheelfg[2] } onColorChange(syncCyan) +onColorChange(syncRed) syncCyan() +syncRed() export const ACC: [number, number, number] = USER.glassacc -export const RED: [number, number, number] = USER.red export const RACC: [number, number, number] = USER.glassacc export const ch = (c: number) => String.fromCharCode(c) diff --git a/components/modules/kbconflicts.ts b/components/modules/kbconflicts.ts new file mode 100644 index 00000000..de4c8fc7 --- /dev/null +++ b/components/modules/kbconflicts.ts @@ -0,0 +1,92 @@ +import { execAsync } from "astal" +import { CYBER_DIR } from "../../env.ts" +import { createModal, drawBtn, registerCModal } from "./cmodal.ts" +import { txt as gtxt, CYAN as GCYAN, ACC as GACC, HEADER as GHEAD, TITLE as GTITLE, MONO as GMONO } from "./glass.ts" + +export type KbConflict = { combo: string, where: string, theme: string, label: string } + +let modal: any = null +let conflicts: KbConflict[] = [] +let scroll = 0 +let resolving = false +let status = "" +let statusUntil = 0 + +const say = (s: string) => { status = s; statusUntil = Date.now() + 5000; modal?.requestDraw() } + +const prettyCombo = (c: string) => c.split("+").filter(Boolean).join(" + ").toUpperCase() + +const ensure = () => { + if (modal) return + modal = createModal({ + name: "kbconflicts", tabTitle: "KEYBIND CONFLICTS", W: 560, H: 460, hud: true, + onOpen: () => { scroll = 0 }, + onScroll: (d: number) => { + scroll = Math.max(0, Math.min(Math.max(0, conflicts.length - 1), scroll + d)) + modal.requestDraw() + }, + draw: (ctx: any, g: any) => { + const x = g.X + 44, w = g.w - 68 + let cy = g.Y + GHEAD + 40 + gtxt(ctx, x, cy, "KEYBIND CONFLICTS DETECTED!", GTITLE, 17, GCYAN, 1, 1, 0.5) + gtxt(ctx, x, cy + 20, `// ${conflicts.length} user bind${conflicts.length === 1 ? "" : "s"} overlap the theme`, GMONO, 9, GACC, 0.75) + cy += 38 + + const btnH = 36 + const listBottom = g.Y + g.h - 48 - btnH - 18 + const rowH = 44 + const vis = Math.max(1, Math.floor((listBottom - cy) / rowH)) + ctx.save() + ctx.rectangle(x - 2, cy - 2, w + 4, listBottom - cy + 4); ctx.clip() + for (let i = 0; i < vis; i++) { + const idx = scroll + i + if (idx >= conflicts.length) break + const c = conflicts[idx] + const ry = cy + i * rowH + ctx.setSourceRGBA(GCYAN[0], GCYAN[1], GCYAN[2], 0.08); ctx.rectangle(x, ry, w, rowH - 8); ctx.fill() + ctx.setSourceRGBA(GCYAN[0], GCYAN[1], GCYAN[2], 0.3); ctx.setLineWidth(0.8); ctx.rectangle(x, ry, w, rowH - 8); ctx.stroke() + gtxt(ctx, x + 12, ry + 16, prettyCombo(c.combo), GTITLE, 11, GCYAN, 0.98, 1) + const act = c.label ? ` FOR ${c.label}` : "" + gtxt(ctx, x + 12, ry + 30, `CONFLICT AT ${c.where} |::| THEME USES ${c.theme}${act}`, GMONO, 8, GACC, 0.7) + } + ctx.restore() + if (conflicts.length > vis) { + gtxt(ctx, x, g.Y + g.h - 60, `${scroll + 1}-${Math.min(scroll + vis, conflicts.length)} of ${conflicts.length} · scroll for more`, GMONO, 8, GACC, 0.5) + } + + const by = g.Y + g.h - 44 - btnH, gap = 12, hbw = (w - gap) / 2 + if (resolving) { + gtxt(ctx, x, by + btnH / 2, "RESOLVING — COMMENTING OUT CONFLICTS …", GMONO, 10, GCYAN, 0.9) + } else { + drawBtn(ctx, g.push, x, by, hbw, btnH, "RESOLVE CONFLICTS", () => { + resolving = true; modal.requestDraw() + execAsync(["sh", "-c", `'${CYBER_DIR}/scripts/kbconflicts' --fix`]) + .then(() => { resolving = false; say("CONFLICTS COMMENTED OUT · RELOADING"); modal.close() }) + .catch(() => { resolving = false; say("RESOLVE FAILED — SEE ~/.config/hypr BACKUPS") }) + }, true, GCYAN) + drawBtn(ctx, g.push, x + hbw + gap, by, hbw, btnH, "CANCEL", () => modal.close(), false, GCYAN) + } + + const live = status && Date.now() < statusUntil + if (live) gtxt(ctx, x, g.Y + g.h - 12, status, GMONO, 8.5, GCYAN, 0.9) + }, + }) + registerCModal(modal) +} + +export const openKbConflictsModal = (request: string) => { + // payload: "kbconflicts ; :: ;…" + const body = request.slice("kbconflicts".length).trim() + const parts = body.split(";").filter((s) => s.trim().length) + const count = parts.length ? parseInt(parts[0].trim(), 10) || 0 : 0 + conflicts = parts.slice(1).map((p) => { + const m = p.trim().match(/^(\S+)\s+(\S+)\s*::\s*(\S+)(?:\s*::\s*(.*))?$/) + if (!m) return { combo: p.trim(), where: "?", theme: "?", label: "" } + return { combo: m[1], where: m[2], theme: m[3], label: (m[4] || "").trim() } + }).filter((c) => c.where !== "?") + if (!conflicts.length) return + scroll = 0 + ensure() + modal.open() + modal.requestDraw() +} diff --git a/components/modules/markets.ts b/components/modules/markets.ts index dd4e80c7..faed355a 100644 --- a/components/modules/markets.ts +++ b/components/modules/markets.ts @@ -6,7 +6,7 @@ import { CYBER_DIR, USER_DIR } from "../../env.ts" import { makePlane, tiltText, fillQuad, strokePath } from "./proj.ts" import { NEON, USER, onColorChange, tintSurface, imgTint, neonBtn } from "./colors.ts" import { createModal } from "./cmodal.ts" -import { txt as gtxt, pango as gpango, RED, RACC, HEADER as GHEAD, TITLE as GTITLE, MONO as GMONO, pip, projQuad, CYAN, ACC } from "./glass.ts" +import { txt as gtxt, pango as gpango, RACC, HEADER as GHEAD, TITLE as GTITLE, MONO as GMONO, pip, projQuad, CYAN, ACC } from "./glass.ts" import { TITLE, MONO, ENIXE, FROSTBITE_WIDE, GUNSHIP_ITAL } from "./fonts.ts" const Cairo = (imports as any).cairo diff --git a/components/modules/notifpopup.ts b/components/modules/notifpopup.ts index fed6fc2f..c294aaf2 100644 --- a/components/modules/notifpopup.ts +++ b/components/modules/notifpopup.ts @@ -8,7 +8,7 @@ import { makePlane, tiltText, strokePath } from "./proj.ts" import { setReadFilter, removeFromHistory } from "./notifmessages.ts" import { dockNotifDecr } from "./dock.ts" import { passthrough } from "./anim.ts" -import { NEON, USER_A, onColorChange, glassAlpha, glassMode, tintSurface, imgTint, neonBtn, isOvr } from "./colors.ts" +import { NEON, USER_A, onColorChange, glassAlpha, glassMode, tintSurface, imgTint, neonBtn, isOvr, notifIconTint } from "./colors.ts" import { sndOn, sndFile, animOn } from "./config.ts" const Cairo = (imports as any).cairo @@ -157,13 +157,13 @@ const draw = (ctx: any) => { const pA = seg(intro, 0, 0.30), aA = beep(pA) glyphBlock(ctx, -15, 2, aA * 0.9) glitchText(ctx, 20, 9, "CONNECTION 201.89.43", ORBITRON, 7, GLYPH_COL, aA, pA, { bold: true, glow: 0.4 }) - drawIcon(ctx, png("notif.png"), -24, 35, 42, aA, true, 1 - pA, isOvr("notifphone") ? NEON.notifphone : null) + drawIcon(ctx, png("notif.png"), -24, 35, 42, aA, true, 1 - pA, isOvr("notifphone") ? NEON.notifphone : notifIconTint.value) const pB = seg(intro, 0.22, 0.48) - drawIcon(ctx, png("file.png"), 3, 23, 76, softg(pB), true, (1 - pB) * 0.5, isOvr("notifbadge") ? NEON.notifbadge : null) + drawIcon(ctx, png("file.png"), 3, 23, 76, softg(pB), true, (1 - pB) * 0.5, isOvr("notifbadge") ? NEON.notifbadge : notifIconTint.value) const pC = seg(intro, 0.52, 0.74), aC = beep(pC) - drawIcon(ctx, png("message.png"), 67, 16, 30, aC, true, 1 - pC, isOvr("notifmail") ? NEON.notifmail : null) + drawIcon(ctx, png("message.png"), 67, 16, 30, aC, true, 1 - pC, isOvr("notifmail") ? NEON.notifmail : notifIconTint.value) glitchText(ctx, CX + 32, 36, msgs.length > 1 ? "NEW MESSAGES" : "NEW MESSAGE", NAVINE, 15, NEON.notifheads, aC, pC, { bold: true, glow: 0.5, shadow: 0.8 }) const pN = seg(intro, 0.58, 0.80) glitchText(ctx, CX + 2, 63, msgs[0].app, NAVINE, 20, NEON.notiftitle, clamp(pN * 1.3), pN, { bold: true, glow: 0.45 }) diff --git a/components/modules/region.ts b/components/modules/region.ts index 99431648..dae5b4a4 100644 --- a/components/modules/region.ts +++ b/components/modules/region.ts @@ -3,7 +3,7 @@ import { Anchor, Layer, Exclusivity, Keymode } from "./widget.ts" import { interval, timeout, execAsync } from "astal" import Gdk from "gi://Gdk?version=3.0" import { SCREEN_WIDTH , SCREEN_HEIGHT } from "../../env.ts" -import { NEON, f, onColorChange, USER, tintSurface, tintPixbuf, imgTint } from "./colors.ts" +import { NEON, f, onColorChange, USER, tintSurface, tintPixbuf, imgTint, notifIconTint } from "./colors.ts" import { MONO } from "./fonts.ts" import { CYBER_DIR } from "../../env.ts" import { setRecording, isRecording } from "./anim.ts" @@ -188,8 +188,8 @@ const drawRecIcon = (ctx, x, y, sz, a, pulse) => { } ctx.setOperator(2) const scaled = recIconPix.scale_simple(dw, dh, GdkPixbuf.InterpType.BILINEAR) - if (imgTint.value) { - tintPixbuf(ctx, scaled, dx, dy, a) + if (imgTint.value || notifIconTint.value) { + tintPixbuf(ctx, scaled, dx, dy, a, notifIconTint.value) } else { Gdk.cairo_set_source_pixbuf(ctx, scaled, dx, dy) ctx.paintWithAlpha(a) @@ -209,7 +209,7 @@ const drawCard = (ctx, cp) => { const trace = (pts) => { ctx.newPath(); pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y)); ctx.closePath() } ctx.save() - trace(bevel()); ctx.setSourceRGBA(USER.overlay[0] * 0.16, USER.overlay[1] * 0.16, USER.overlay[2] * 0.16, 0.55 * load); ctx.fill() + trace(bevel()); ctx.setSourceRGBA(USER.overlay[0] * 0.10, USER.overlay[1] * 0.10, USER.overlay[2] * 0.12, 0.32 * load); ctx.fill() const [ovr2, ovg2, ovb2] = f(NEON.overlay) ctx.setOperator(12); trace(bevel()); ctx.setSourceRGBA(ovr2, ovg2, ovb2, 0.12 * load); ctx.setLineWidth(4); ctx.stroke(); ctx.setOperator(2) trace(bevel()); ctx.setSourceRGBA(ovr2, ovg2, ovb2, 0.85 * load); ctx.setLineWidth(1.4); ctx.stroke() @@ -262,8 +262,8 @@ const drawAlertIcon = (ctx, x, y, sz, a) => { const dx = x + Math.round((sz - dw) / 2), dy = y + Math.round((sz - dh) / 2) ctx.save() const sc = alertPix.scale_simple(dw, dh, GdkPixbuf.InterpType.BILINEAR) - if (imgTint.value) { - tintPixbuf(ctx, sc, dx, dy, a) + if (imgTint.value || notifIconTint.value) { + tintPixbuf(ctx, sc, dx, dy, a, notifIconTint.value) } else { Gdk.cairo_set_source_pixbuf(ctx, sc, dx, dy) ctx.paintWithAlpha(a) diff --git a/components/modules/sidepanel.ts b/components/modules/sidepanel.ts index 26fc17f3..0c2756a3 100644 --- a/components/modules/sidepanel.ts +++ b/components/modules/sidepanel.ts @@ -10,7 +10,7 @@ import { CYBER_DIR, USER_DIR } from "../../env.ts" import { makePlane, tiltText, strokePath, fillQuad, alertChip } from "./proj.ts" import { NEON, USER, onColorChange, tintOpaque, mapAccent } from "./colors.ts" import { createModal } from "./cmodal.ts" -import { txt as gtxt, pango as gpango, RED, RACC, CYAN as GCYAN, ACC as GACC, HEADER as GHEAD, TITLE as GTITLE, MONO as GMONO, pip, projQuad } from "./glass.ts" +import { txt as gtxt, pango as gpango, CYAN as GCYAN, ACC as GACC, HEADER as GHEAD, TITLE as GTITLE, MONO as GMONO, pip, projQuad } from "./glass.ts" import { openTimeModal } from "./timeset.ts" const Cairo = (imports).cairo @@ -375,7 +375,7 @@ const wxPick = (r) => { wxLat = r.lat; wxLon = r.lon; wxName = String(r.name || const ensureWxModal = () => { if (wxModal) return wxModal = createModal({ - name: "weather", tabTitle: "WEATHER UPLINK", W: 384, H: 312, hud: true, col: RED, accent: RACC, + name: "weather", tabTitle: "WEATHER UPLINK", W: 384, H: 312, hud: true, onOpen: () => { wxQuery = ""; wxResults = []; wxHint = "TYPE A CITY ▸"; wxScroll = 0 }, onKey: (k) => { if (k === Gdk.KEY_BackSpace) wxQuery = wxQuery.slice(0, -1) @@ -385,27 +385,27 @@ const ensureWxModal = () => { onScroll: (d) => { wxScroll = Math.max(0, Math.min(Math.max(0, wxResults.length - 1), wxScroll + d)); wxModal.requestDraw() }, draw: (ctx, g) => { const x = g.X + 18, w = g.w - 36 - gtxt(ctx, x + w - 4 - ctx.textExtents(`NOW: ${wxName}`).width, g.Y + GHEAD + 22, `NOW: ${wxName}`, GMONO, 9, RED, 0.6) - gtxt(ctx, x, g.Y + GHEAD + 22, "SEARCH FORECAST CITY", GMONO, 10, RED, 0.85, 1) + gtxt(ctx, x + w - 4 - ctx.textExtents(`NOW: ${wxName}`).width, g.Y + GHEAD + 22, `NOW: ${wxName}`, GMONO, 9, GCYAN, 0.6) + gtxt(ctx, x, g.Y + GHEAD + 22, "SEARCH FORECAST CITY", GMONO, 10, GCYAN, 0.85, 1) const by = g.Y + GHEAD + 32, bh = 30 - aPath(ctx, x, by, w, bh, 6); ctx.setSourceRGBA(RED[0] * 0.12, RED[1] * 0.06, RED[2] * 0.06, 0.5); ctx.fill() - aPath(ctx, x, by, w, bh, 6); ctx.setSourceRGBA(RED[0], RED[1], RED[2], 0.85); ctx.setLineWidth(0.9); ctx.stroke() + aPath(ctx, x, by, w, bh, 6); ctx.setSourceRGBA(GCYAN[0] * 0.12, GCYAN[1] * 0.06, GCYAN[2] * 0.06, 0.5); ctx.fill() + aPath(ctx, x, by, w, bh, 6); ctx.setSourceRGBA(GCYAN[0], GCYAN[1], GCYAN[2], 0.85); ctx.setLineWidth(0.9); ctx.stroke() const cur = (Math.floor(Date.now() / 450) % 2) ? "▌" : " " - gpango(ctx, x + 14, by + bh / 2 + 5, (wxQuery ? wxQuery + cur : "Search a city…"), GTITLE, false, 13, wxQuery ? RACC : RED, wxQuery ? 0.96 : 0.4) - gtxt(ctx, x, by + bh + 18, "// " + wxHint, GMONO, 9, RED, 0.6) + gpango(ctx, x + 14, by + bh / 2 + 5, (wxQuery ? wxQuery + cur : "Search a city…"), GTITLE, false, 13, wxQuery ? GACC : GCYAN, wxQuery ? 0.96 : 0.4) + gtxt(ctx, x, by + bh + 18, "// " + wxHint, GMONO, 9, GCYAN, 0.6) const ly = by + bh + 28, lh = (g.Y + g.h) - ly - 22, rowH = 30, gap = 5, step = rowH + gap, vis = Math.max(1, Math.floor(lh / step)) const maxS = Math.max(0, wxResults.length - vis), sc = Math.min(wxScroll, maxS) ctx.save(); ctx.rectangle(x - 2, ly - 2, w + 4, lh + 4); ctx.clip() for (let i = 0; i <= vis; i++) { const idx = sc + i; if (idx >= wxResults.length) break const r = wxResults[idx], ry = ly + i * step; if (ry + rowH > ly + lh + step) break - aPath(ctx, x, ry, w, rowH, 5); ctx.setSourceRGBA(RED[0] * 0.16, RED[1] * 0.08, RED[2] * 0.08, 0.34); ctx.fill() - aPath(ctx, x, ry, w, rowH, 5); ctx.setSourceRGBA(RED[0], RED[1], RED[2], 0.6); ctx.setLineWidth(0.9); ctx.stroke() - gpango(ctx, x + 14, ry + rowH / 2 + 4, r.full, GTITLE, true, 11, RACC, 0.95) + aPath(ctx, x, ry, w, rowH, 5); ctx.setSourceRGBA(GCYAN[0] * 0.16, GCYAN[1] * 0.08, GCYAN[2] * 0.08, 0.34); ctx.fill() + aPath(ctx, x, ry, w, rowH, 5); ctx.setSourceRGBA(GCYAN[0], GCYAN[1], GCYAN[2], 0.6); ctx.setLineWidth(0.9); ctx.stroke() + gpango(ctx, x + 14, ry + rowH / 2 + 4, r.full, GTITLE, true, 11, GACC, 0.95) g.push({ kind: "row", bx0: x, by0: ry, bx1: x + w, by1: ry + rowH, on: () => wxPick(r) }) } ctx.restore() - gtxt(ctx, x, g.Y + g.h - 10, "type ▸ click a result · ESC cancels", GMONO, 8, RED, 0.42) + gtxt(ctx, x, g.Y + g.h - 10, "type ▸ click a result · ESC cancels", GMONO, 8, GCYAN, 0.42) }, }) } @@ -415,8 +415,8 @@ let fcModal: any = null, fcSel = 0, fcTick = 0, fcOpenAt = 0, fcCityTap = 0 const ease = (t) => 1 - Math.pow(1 - Math.max(0, Math.min(1, t)), 3) const fcReveal = () => ease((Date.now() - fcOpenAt) / 620) -const YEL: [number, number, number] = USER.amber -const ARA: [number, number, number] = RACC +const YEL: [number, number, number] = USER.cyan +const ARA: [number, number, number] = GACC const HOT: [number, number, number] = USER.red const chamfer = (ctx, x, y, w, h, c = 11) => { @@ -500,7 +500,7 @@ const ensureForecastModal = () => { if (fcModal) return fcModal = createModal({ name: "forecast", tabTitle: "ATMOSPHERIC UPLINK · 7-DAY", W: 940, H: 480, - col: RED, accent: YEL, yaw: -29, pitch: -5, roll: -0.4, focal: 2400, dist: 2300, glass: 0.88, anchorLeft: true, keymode: Keymode.ON_DEMAND, + col: GCYAN, accent: GACC, yaw: -29, pitch: -5, roll: -0.4, focal: 2400, dist: 2300, glass: 0.88, anchorLeft: true, keymode: Keymode.ON_DEMAND, onOpen: () => { fcSel = 0; fcTick = 0; fcOpenAt = Date.now(); refreshWeather() }, onFrame: () => { fcTick++; fcModal.requestDraw() }, onKey: (k) => { diff --git a/components/modules/themesettings.ts b/components/modules/themesettings.ts index 350d61b0..5d655263 100644 --- a/components/modules/themesettings.ts +++ b/components/modules/themesettings.ts @@ -5,8 +5,12 @@ import GLib from "gi://GLib" import { Keymode } from "./widget.ts" import { PALETTES, getPaletteName, applyPalette, saveUserColors, setUserColor, getUserColor, rgbToHex, hasAlpha, getUserAlpha, setUserAlpha } from "./colors.ts" import { TITLE, MONO, CYAN, ACC, HEADER, txt, drawGlass, Cairo } from "./glass.ts" -import { createModal, drawBtn, drawToggle, sectionHeader, drawKeyCap, btnPath } from "./cmodal.ts" +import { createModal, drawBtn, drawToggle, drawSlider, sectionHeader, drawKeyCap, btnPath } from "./cmodal.ts" import { cfgBool, cfgStr, setCfg, toggleCfg, resetCfg, adoptSound, clearSound, GAUGE_OPTS, METRIC_LABEL } from "./config.ts" +import { + wmBool, wmNum, wmStr, setWm, toggleWm, resetWm, wmCornersIs, + CORNER_OPTS, CORNER_LABEL, OPACITY_MODES, OPACITY_MODE_LABEL, type WmVal, +} from "./wmconfig.ts" import { USER_DIR } from "../../env.ts" import { openWheel, closeWheel, buildAppEntries, openAppsMenu } from "./appsmenu.ts" import { @@ -61,10 +65,29 @@ export const ThemesCtrl = () => { kbScroll = 0 cfgOpen = null cfgExpand = {} + wmOpen = null + wmExpand = {} + wmColorPick = null + wmAppText = "" }, onKeyRaw: onKbKeyRaw, onScroll: (d) => { cfgOpen = null; kbScroll = Math.max(0, Math.min(kbMaxScroll, kbScroll + d * 32)); ctrl.requestDraw() }, onKey: (k: number) => { + if (wmColorPick) { + if (k === Gdk.KEY_Escape) { closeWmPicker(); return true } + return true + } + if (wmAppText !== "" || (tab === "wm" && wmAppEditing)) { + if (k === Gdk.KEY_Escape) { wmAppText = ""; wmAppEditing = false; ctrl.requestDraw() } + else if (k === Gdk.KEY_Return) { commitWmApps() } + else if (k === Gdk.KEY_BackSpace) { wmAppText = wmAppText.slice(0, -1); ctrl.requestDraw() } + else { + const name = Gdk.keyval_name(k) || "" + if (name.length === 1) { wmAppText += name; ctrl.requestDraw() } + else if (name === "space") { wmAppText += " "; ctrl.requestDraw() } + } + return true + } if (kbCaptureKind === "newuser" && kbAddStep === "command" && k !== Gdk.KEY_Escape) { const name = Gdk.keyval_name(k) || "" if (k === Gdk.KEY_BackSpace) { @@ -125,6 +148,7 @@ export const ThemesCtrl = () => { if (tab === "colors") drawColors(ctx, g, x, panelY + HEADER + 12, w) else if (tab === "keybinds") drawKeybinds(ctx, g, x, panelY + HEADER + 12, w) else if (tab === "anim") drawConfig(ctx, g, x, panelY + HEADER + 12, w) + else if (tab === "wm") drawWm(ctx, g, x, panelY + HEADER + 12, w) else drawWip(ctx, g, x, panelY + HEADER + 12, w) }, }) @@ -367,6 +391,11 @@ let kbDeleteConfirm: { raw_line: number; combo: string; label: string } | null = let kbScroll = 0 let kbMaxScroll = 0 let kbAddStep: "prompt" | "command" | "app" | "capture" | null = null +let wmOpen: string | null = null +let wmExpand: Record = {} +let wmColorPick: { key: string; h: number; s: number; v: number } | null = null +let wmAppEditing = false +let wmAppText = "" let kbCommandText = "" const ROW_H = 42 @@ -1064,6 +1093,333 @@ const drawWip = (ctx, g, x, y, w) => { txt(ctx, cx - ctx.textExtents(t2).width / 2, y + 98, t2, MONO, 11, g.col, 0.6) } +const commitWmApps = () => { + const apps = wmAppText.split(/[,\n]/).map((s) => s.trim()).filter(Boolean) + if (apps.length > 0) setWm("wmOpacityApps", apps.join(",")) + wmAppText = "" + wmAppEditing = false + ctrl.requestDraw() +} + +const rgbToHsv = (r: number, g: number, b: number): [number, number, number] => { + const rr = r / 255, gg = g / 255, bb = b / 255 + const mx = Math.max(rr, gg, bb), mn = Math.min(rr, gg, bb), d = mx - mn + let h = 0 + if (d !== 0) { + if (mx === rr) h = ((gg - bb) / d + (gg < bb ? 6 : 0)) * 60 + else if (mx === gg) h = ((bb - rr) / d + 2) * 60 + else h = ((rr - gg) / d + 4) * 60 + } + return [h, mx === 0 ? 0 : d / mx, mx] +} +const hsvToRgb = (h: number, s: number, v: number): [number, number, number] => { + const c = v * s, hh = ((h % 360) + 360) % 360 / 60, x = c * (1 - Math.abs((hh % 2) - 1)) + let r = 0, g = 0, b = 0 + if (hh < 1) { r = c; g = x } else if (hh < 2) { r = x; g = c } else if (hh < 3) { g = c; b = x } + else if (hh < 4) { g = x; b = c } else if (hh < 5) { r = x; b = c } else { r = c; b = x } + const m = v - c + return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)] +} +const rgbToHex = (r: number, g: number, b: number): string => + [r, g, b].map((c) => Math.max(0, Math.min(255, Math.round(c))).toString(16).padStart(2, "0")).join("") + +const openWmPicker = (key: string) => { + const cur = hexToRgb(wmStr(key)) + if (cur) wmColorPick = { key, ...rgbToHsv(cur[0], cur[1], cur[2]) } + else wmColorPick = { key, h: 0, s: 0.85, v: 0.95 } + wmOpen = null + ctrl.requestDraw() +} +const liveWmPick = () => { + if (!wmColorPick) return + const [r, gg, b] = hsvToRgb(wmColorPick.h, wmColorPick.s, wmColorPick.v) + setWm(wmColorPick.key, rgbToHex(r, gg, b)) + ctrl.requestDraw() +} +const closeWmPicker = () => { wmColorPick = null; ctrl.requestDraw() } + +const drawColorCell = (ctx, g, x, ry, h, rgb: [number, number, number] | null) => { + if (rgb) ctx.setSourceRGBA(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1) + else { ctx.setSourceRGBA(g.col[0], g.col[1], g.col[2], 0.18) } + ctx.rectangle(x, ry, h, h); ctx.fill() + ctx.setSourceRGBA(g.accent[0], g.accent[1], g.accent[2], 0.8); ctx.setLineWidth(1) + ctx.rectangle(x + 0.5, ry + 0.5, h - 1, h - 1); ctx.stroke() +} + +const hexToRgb = (h: string): [number, number, number] | null => { + if (!/^[0-9a-fA-F]{6}$/.test(h)) return null + return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)] +} + +const drawWmSlider = (ctx, g, x, ry, w, key, min, max, fmt) => { + const trackX = x + w - 190, trackW = 150 + const v = wmNum(key) + const norm = Math.max(0, Math.min(1, (v - min) / (max - min))) + drawSlider(ctx, g.push, trackX, ry + 11, trackW, norm, (nv) => { + setWm(key, Math.round((min + nv * (max - min)) * 100) / 100) + }) + txt(ctx, trackX - 40, ry + 15, fmt(v), MONO, 8.5, g.accent, 0.9, 0, 0) +} + +const WM_SECTIONS: { title: string; keys: string[]; rows: { t: "tog" | "sld" | "sel" | "col" | "car" | "apps" | "corner"; k: string; label: string; grp?: string }[] }[] = [ + { + title: "// ::window transparency", + keys: ["wmOpacity", "wmOpacityVal", "wmOpacityMode", "wmOpacityApps"], + rows: [ + { t: "tog", k: "wmOpacity", label: "WINDOW BACKGROUND TRANSPARENCY" }, + { t: "sld", k: "wmOpacityVal", label: "OPACITY LEVEL" }, + { t: "sel", k: "wmOpacityMode", label: "APPLICATION MODE" }, + { t: "apps", k: "wmOpacityApps", label: "SELECTED APPLICATIONS" }, + ], + }, + { + title: "// ::glow & shadow", + keys: ["wmGlow", "wmGlowRange", "wmGlowRp", "wmShadow", "wmShadowColor", "wmShadowAlpha", "wmShadowRange"], + rows: [ + { t: "tog", k: "wmGlow", label: "WINDOW GLOW" }, + { t: "sld", k: "wmGlowRange", label: "GLOW RANGE" }, + { t: "sld", k: "wmGlowRp", label: "GLOW INTENSITY" }, + { t: "tog", k: "wmShadow", label: "WINDOW SHADOW" }, + { t: "col", k: "wmShadowColor", label: "SHADOW COLOUR" }, + { t: "sld", k: "wmShadowAlpha", label: "SHADOW STRENGTH" }, + { t: "sld", k: "wmShadowRange", label: "SHADOW RANGE" }, + ], + }, + { + title: "// ::borders", + keys: ["wmBorders", "wmBorderSize", "wmBorderColor"], + rows: [ + { t: "tog", k: "wmBorders", label: "WINDOW BORDERS" }, + { t: "sld", k: "wmBorderSize", label: "BORDER WIDTH" }, + { t: "col", k: "wmBorderColor", label: "BORDER COLOUR" }, + ], + }, + { + title: "// ::corners", + keys: ["wmCorners", "wmRounding"], + rows: [ + { t: "sel", k: "wmCorners", label: "CORNER STYLE" }, + { t: "sld", k: "wmRounding", label: "CORNER SIZE" }, + ], + }, +] + +const drawWmRow = (ctx, g, x, ry, w, r, hit) => { + const push = hit ? g.push : noPush + const lx = r.grp ? x + 30 : x + 16 + const dis = r.k === "wmOpacityVal" ? !wmBool("wmOpacity") + : r.k === "wmRounding" ? wmCornersIs("sharp") + : false + + if (r.t === "car") { + const open = wmExpand[r.k] === true + drawBtn(ctx, push, x + 16, ry + 4, 230, 18, `${open ? "▾" : "▸"} ${r.label}`, () => { wmExpand[r.k] = !open; wmOpen = null; ctrl.requestDraw() }, open, g.col, "", 9) + return + } + + if (r.t === "tog") { + txt(ctx, lx, ry + 16, r.label, TITLE, 9, g.col, 0.9) + drawToggle(ctx, push, x + w - 44, ry + 4, wmBool(r.k), () => { toggleWm(r.k) }, false, g.col) + return + } + + if (r.t === "sld") { + txt(ctx, lx, ry + 16, r.label, TITLE, 9, dis ? [0.5, 0.54, 0.58] : g.col, dis ? 0.38 : 0.9) + if (dis) return + const fmts: Record string> = { + wmOpacityVal: (v) => `${Math.round(v * 100)}%`, + wmBorderSize: (v) => `${Math.round(v)}px`, + wmGlowRange: (v) => `${Math.round(v)}px`, + wmGlowRp: (v) => `x${Math.round(v)}`, + wmShadowRange: (v) => `${Math.round(v)}px`, + wmShadowAlpha: (v) => `${Math.round(v)}%`, + wmRounding: (v) => `${Math.round(v)}px`, + } + const ranges: Record = { + wmOpacityVal: [0.4, 1], wmBorderSize: [0, 5], wmGlowRange: [0, 30], wmGlowRp: [1, 5], + wmShadowRange: [0, 40], wmShadowAlpha: [10, 100], wmRounding: [0, 40], + } + drawWmSlider(ctx, g, x, ry, w, r.k, ranges[r.k]?.[0] ?? 0, ranges[r.k]?.[1] ?? 1, fmts[r.k] ?? ((v) => `${v}`)) + return + } + + if (r.t === "col") { + txt(ctx, lx, ry + 16, r.label, TITLE, 9, g.col, 0.9) + const cur = wmStr(r.k) + const rgb = hexToRgb(cur) + const picking = wmColorPick?.key === r.k + const label = cur ? `#${cur}` : "THEME" + drawColorCell(ctx, g, x + w - 120, ry + 3, 18, rgb) + push({ kind: "btn", bx0: x + w - 124, by0: ry + 1, bx1: x + w - 50, by1: ry + 22, on: () => { if (!picking) openWmPicker(r.k) } }) + txt(ctx, x + w - 96, ry + 16, label, MONO, 8.5, rgb ? g.accent : g.col, 0.9) + drawBtn(ctx, push, x + w - 44, ry + 3, 40, 18, picking ? "OPEN" : "PICK", () => { + if (!picking) openWmPicker(r.k) + }, picking, g.col, "", 8) + return + } + + if (r.t === "apps") { + const apps = wmStr(r.k).split(",").map((s) => s.trim()).filter(Boolean) + const mode = wmStr("wmOpacityMode") + const editing = wmAppEditing && wmAppText !== "" + const shown = editing ? `${wmAppText}_` : apps.length ? apps.join(", ") : "NONE" + const editable = mode !== "off" + txt(ctx, lx, ry + 16, r.label, TITLE, 9, editable ? g.col : [0.5, 0.54, 0.58], editable ? 0.9 : 0.38) + if (!editable) return + txt(ctx, lx + 190, ry + 16, fitTxt(ctx, shown, MONO, 8, w - 320), MONO, 8, editing ? g.accent : g.col, editing ? 1 : 0.7) + drawBtn(ctx, push, x + w - 44, ry + 3, 40, 18, editing ? "OK" : "EDIT", () => { + if (editing) commitWmApps() + else { wmAppEditing = true; wmAppText = ""; ctrl.requestDraw() } + }, editing, g.col, "", 8) + return + } + + const isCorner = r.k === "wmCorners" + const isMode = r.k === "wmOpacityMode" + const opts = isCorner ? CORNER_OPTS : isMode ? OPACITY_MODES : [] + const labels = isCorner ? CORNER_LABEL : isMode ? OPACITY_MODE_LABEL : {} + const cur = wmStr(r.k), open = wmOpen === r.k + txt(ctx, lx, ry + 16, r.label, TITLE, 9, g.col, 0.9) + drawBtn(ctx, push, x + w - 266, ry + 3, 264, 20, `${labels[cur] ?? cur} ${open ? "▴" : "▾"}`, () => { wmOpen = open ? null : r.k; ctrl.requestDraw() }, open, g.col, "", 9.5) +} + +const drawWm = (ctx, g, x, y, w) => { + const visTop = y + 16, visBottom = g.Y + g.h - 14, visHeight = visBottom - visTop + + const layout: { y: number; kind: "sec" | "row"; title: string; sec?: { title: string; keys: string[]; rows: any[] }; row?: any }[] = [] + let yAcc = 0 + for (const sec of WM_SECTIONS) { + layout.push({ y: yAcc, kind: "sec", title: sec.title, sec }) + yAcc += GSEC_H + for (const r of sec.rows) { + if (r.grp && !wmExpand[r.grp]) continue + layout.push({ y: yAcc, kind: "row", title: r.label, row: r }); yAcc += GROW_H + } + yAcc += 10 + } + + const maxScroll = Math.max(0, yAcc + 12 - visHeight) + kbMaxScroll = maxScroll + if (kbScroll > maxScroll) kbScroll = maxScroll + if (kbScroll < 0) kbScroll = 0 + + let pop: { key: string; bx: number; by: number; bw: number } | null = null + let colAnchor: number | null = null + + ctx.save() + ctx.rectangle(x - 4, visTop, w + 8, visHeight) + ctx.clip() + for (const it of layout) { + const ry = visTop + it.y - kbScroll + if (it.kind === "sec") { + if (ry + GSEC_H < visTop || ry > visBottom) continue + sectionHeader(ctx, g, x, ry + 12, it.sec!.title, w - 66) + const shown = ry >= visTop - 1 && ry + 18 <= visBottom + 1 + drawBtn(ctx, (shown && !wmColorPick && !wmOpen) ? g.push : noPush, x + w - 62, ry + 1, 62, 16, "DEFAULTS", () => { resetWm(it.sec!.keys); wmOpen = null; ctrl.requestDraw() }, false, [1, 0.4, 0.44], "", 8) + continue + } + if (ry + GROW_H < visTop || ry > visBottom) continue + const hit = ry >= visTop - 1 && ry + GROW_H <= visBottom + 1 + const r = it.row! + if (r.t === "sel" && wmOpen === r.k) pop = { key: r.k, bx: x + w - 266, by: ry + 23, bw: 264 } + if (r.t === "col" && wmColorPick?.key === r.k) colAnchor = ry + drawWmRow(ctx, g, x, ry, w, r, hit && !wmOpen && !wmColorPick) + } + ctx.restore() + + if (maxScroll > 0) { + const fillH = visHeight * (kbScroll / maxScroll) + const barH = Math.max(20, visHeight - fillH) + ctx.setSourceRGBA(g.col[0], g.col[1], g.col[2], 0.5); ctx.setLineWidth(2) + ctx.newPath(); ctx.moveTo(x + w + 4, visTop); ctx.lineTo(x + w + 4, visBottom); ctx.stroke() + ctx.setSourceRGBA(g.accent[0], g.accent[1], g.accent[2], 0.85); ctx.setLineWidth(3) + ctx.newPath(); ctx.moveTo(x + w + 4, visTop + fillH); ctx.lineTo(x + w + 4, visTop + fillH + barH); ctx.stroke() + } + + if (pop) { + const isCorner = pop.key === "wmCorners" + const opts = isCorner ? CORNER_OPTS : OPACITY_MODES + const labels = isCorner ? CORNER_LABEL : OPACITY_MODE_LABEL + const ih = 20, listH = opts.length * ih + 6 + let ly = pop.by + 2 + if (ly + listH > visBottom) ly = Math.max(visTop, pop.by - 25 - listH) + ctx.setSourceRGBA(0.02, 0.05, 0.07, 0.97); ctx.rectangle(pop.bx, ly, pop.bw, listH); ctx.fill() + ctx.setSourceRGBA(g.accent[0], g.accent[1], g.accent[2], 0.7); ctx.setLineWidth(1) + ctx.rectangle(pop.bx + 0.5, ly + 0.5, pop.bw - 1, listH - 1); ctx.stroke() + const cur = wmStr(pop.key) + opts.forEach((o, i) => { + drawBtn(ctx, g.push, pop!.bx + 3, ly + 3 + i * ih, pop!.bw - 6, ih - 2, labels[o] ?? o, () => { setWm(pop!.key, o); wmOpen = null; ctrl.requestDraw() }, cur === o, g.col, "", 9) + }) + g.push({ kind: "btn", bx0: g.X, by0: g.Y, bx1: g.X + g.w, by1: g.Y + g.h, on: () => { wmOpen = null; ctrl.requestDraw() } }) + } + + if (wmColorPick) { + const p = wmColorPick + const pW = 232, pH = 228 + const px = x + w - pW - 8 + let py = colAnchor != null ? colAnchor + 24 : visTop + (visHeight - pH) / 2 + if (py + pH > visBottom) py = Math.max(visTop, (colAnchor != null ? colAnchor - pH - 8 : visTop) ) + const sx = px + 14, sy = py + 14, svw = 148, svh = 148 + const hx = px + 14, hy = py + 172, hw = 148, hh = 14 + const [ar, ag, ab] = hsvToRgb(p.h, p.s, p.v) + ctx.setSourceRGBA(0.02, 0.05, 0.07, 0.98); ctx.rectangle(px, py, pW, pH); ctx.fill() + ctx.setSourceRGBA(g.accent[0], g.accent[1], g.accent[2], 0.7); ctx.setLineWidth(1) + ctx.rectangle(px + 0.5, py + 0.5, pW - 1, pH - 1); ctx.stroke() + + const [hr, hg, hb] = hsvToRgb(p.h, 1, 1) + const gws = new Cairo.LinearGradient(sx, 0, sx + svw, 0) + gws.addColorStopRGB(0, 1, 1, 1); gws.addColorStopRGB(1, hr / 255, hg / 255, hb / 255) + ctx.setSource(gws); ctx.rectangle(sx, sy, svw, svh); ctx.fill() + const gbs = new Cairo.LinearGradient(0, sy, 0, sy + svh) + gbs.addColorStopRGBA(0, 0, 0, 0, 0); gbs.addColorStopRGBA(1, 0, 0, 0, 1) + ctx.setSource(gbs); ctx.rectangle(sx, sy, svw, svh); ctx.fill() + ctx.setSourceRGBA(g.col[0], g.col[1], g.col[2], 0.4); ctx.setLineWidth(1) + ctx.rectangle(sx + 0.5, sy + 0.5, svw - 1, svh - 1); ctx.stroke() + + ctx.setSourceRGB(ar / 255, ag / 255, ab / 255) + ctx.rectangle(px + 170, sy, 48, 122); ctx.fill() + ctx.setSourceRGBA(g.accent[0], g.accent[1], g.accent[2], 0.8); ctx.setLineWidth(1) + ctx.rectangle(px + 170.5, sy + 0.5, 47, 121); ctx.stroke() + txt(ctx, px + 170, sy + 138, `#${rgbToHex(ar, ag, ab)}`, MONO, 8, g.accent, 0.95) + + const ghu = new Cairo.LinearGradient(hx, 0, hx + hw, 0) + for (let i = 0; i <= 6; i++) { const [r2, g2, b2] = hsvToRgb(i * 60, 1, 1); ghu.addColorStopRGB(i / 6, r2 / 255, g2 / 255, b2 / 255) } + ctx.setSource(ghu); ctx.rectangle(hx, hy, hw, hh); ctx.fill() + ctx.setSourceRGBA(g.col[0], g.col[1], g.col[2], 0.4); ctx.setLineWidth(1) + ctx.rectangle(hx + 0.5, hy + 0.5, hw - 1, hh - 1); ctx.stroke() + + const ccx = sx + p.s * svw, ccy = sy + (1 - p.v) * svh + ctx.setSourceRGBA(0, 0, 0, 0.8); ctx.setLineWidth(0.8) + ctx.newPath(); ctx.arc(ccx, ccy, 6.2, 0, 2 * Math.PI); ctx.stroke() + ctx.setSourceRGBA(1, 1, 1, 0.95); ctx.setLineWidth(1.4) + ctx.newPath(); ctx.arc(ccx, ccy, 5, 0, 2 * Math.PI); ctx.stroke() + const hcx = hx + (p.h / 360) * hw + ctx.setSourceRGBA(0, 0, 0, 0.8); ctx.setLineWidth(3) + ctx.newPath(); ctx.moveTo(hcx, hy - 3); ctx.lineTo(hcx, hy + hh + 3); ctx.stroke() + ctx.setSourceRGBA(1, 1, 1, 0.95); ctx.setLineWidth(1.2) + ctx.newPath(); ctx.moveTo(hcx, hy - 3); ctx.lineTo(hcx, hy + hh + 3); ctx.stroke() + + drawBtn(ctx, g.push, px + 14, py + pH - 30, 70, 20, "THEME", () => { setWm(p.key, ""); closeWmPicker() }, false, [1, 0.4, 0.44], "", 8) + drawBtn(ctx, g.push, px + 92, py + pH - 30, 70, 20, "DONE", () => closeWmPicker(), true, g.col, "", 8) + + g.push({ + kind: "sld2", key: "wmPickSV", bx0: sx - 3, by0: sy - 3, bx1: sx + svw + 3, by1: sy + svh + 3, + onXY: (mx, my) => { + p.s = Math.max(0, Math.min(1, (mx - sx) / svw)) + p.v = Math.max(0, Math.min(1, 1 - (my - sy) / svh)) + liveWmPick() + }, + }) + g.push({ + kind: "sld", key: "wmPickHue", u0: hx, v0: hy + hh / 2, u1: hx + hw, v1: hy + hh / 2, + bx0: hx - 5, by0: hy - 5, bx1: hx + hw + 5, by1: hy + hh + 5, + on: (t) => { p.h = t * 360; liveWmPick() }, + }) + g.push({ kind: "btn", bx0: px, by0: py, bx1: px + pW, by1: py + pH, on: () => {} }) + g.push({ kind: "btn", bx0: g.X, by0: g.Y, bx1: g.X + g.w, by1: g.Y + g.h, on: () => closeWmPicker() }) + } +} + type CfgKind = "tog" | "snd" | "sel" | "car" type CfgRow = { t: CfgKind; k: string; label: string; dep?: string; grp?: string; master?: boolean } diff --git a/components/modules/timeset.ts b/components/modules/timeset.ts index ca6f5528..b80f519b 100644 --- a/components/modules/timeset.ts +++ b/components/modules/timeset.ts @@ -3,13 +3,13 @@ import GLib from "gi://GLib" import Gio from "gi://Gio" import { execAsync } from "astal" import { createModal } from "./cmodal.ts" -import { txt as gtxt, pango as gpango, RED, RACC, Cairo, HEADER as GHEAD, TITLE as GTITLE, MONO as GMONO } from "./glass.ts" +import { txt as gtxt, pango as gpango, CYAN as GCYAN, ACC as GACC, Cairo, HEADER as GHEAD, TITLE as GTITLE, MONO as GMONO } from "./glass.ts" import { USER } from "./colors.ts" -const YEL: [number, number, number] = USER.amber -const ARA: [number, number, number] = RACC +const YEL: [number, number, number] = USER.cyan +const ARA: [number, number, number] = GACC const AC2: [number, number, number] = USER.glassacc -const HOT: [number, number, number] = USER.red +const HOT: [number, number, number] = USER.cyan const pad2 = (n: number) => String(n).padStart(2, "0") @@ -181,7 +181,7 @@ const ensure = () => { if (modal) return modal = createModal({ name: "time", tabTitle: "SYSTEM TIME", W: 620, H: 560, - col: RED, accent: YEL, yaw: 13, pitch: -3, roll: 0.8, focal: 2400, dist: 2300, noGlass: true, anchorRight: true, + col: GCYAN, accent: GACC, yaw: 13, pitch: -3, roll: 0.8, focal: 2400, dist: 2300, noGlass: true, anchorRight: true, onOpen: () => { query = ""; scroll = 0; status = ""; tick = 0; focusField = "filter"; seedManual(); readState(); loadZones() }, onFrame: () => { tick++; if (tick % 8 === 0) modal.requestDraw() }, onScroll: (d) => { scroll = Math.max(0, Math.min(Math.max(0, results.length - 1), scroll + d * 2)); modal.requestDraw() }, diff --git a/components/modules/toast.ts b/components/modules/toast.ts index 5510901d..44086df6 100644 --- a/components/modules/toast.ts +++ b/components/modules/toast.ts @@ -5,7 +5,7 @@ import Gdk from "gi://Gdk?version=3.0" import GdkPixbuf from "gi://GdkPixbuf" import { SCREEN_WIDTH, SCREEN_HEIGHT, CYBER_DIR } from "../../env.ts" import { makePlane, strokePath, tiltText } from "./proj.ts" -import { NEON, f, onColorChange, tintPixbuf, imgTint } from "./colors.ts" +import { NEON, f, onColorChange, tintPixbuf, imgTint, notifIconTint } from "./colors.ts" import { TITLE } from "./fonts.ts" import { passthrough } from "./anim.ts" @@ -55,8 +55,8 @@ const drawIcon = (ctx, a) => { const pbW = ALERT.get_width(), pbH = ALERT.get_height() ctx.save() ctx.translate(pc[0], pc[1]); ctx.rotate(ang); ctx.scale((2 * hw) / pbW, (2 * hh) / pbH) - if (imgTint.value) { - tintPixbuf(ctx, ALERT, -pbW / 2, -pbH / 2, a) + if (imgTint.value || notifIconTint.value) { + tintPixbuf(ctx, ALERT, -pbW / 2, -pbH / 2, a, notifIconTint.value) } else { ctx.setOperator(12) Gdk.cairo_set_source_pixbuf(ctx, ALERT, -pbW / 2, -pbH / 2); ctx.paintWithAlpha(0.3 * a) diff --git a/components/modules/wmconfig.ts b/components/modules/wmconfig.ts new file mode 100644 index 00000000..38efa0ac --- /dev/null +++ b/components/modules/wmconfig.ts @@ -0,0 +1,189 @@ +import GLib from "gi://GLib" +import { execAsync } from "astal" +import { USER_DIR } from "../../env.ts" + + +export type WmVal = boolean | number | string + +const DEF: Record = { + wmOpacity: true, wmOpacityVal: 0.88, wmOpacityMode: "off", wmOpacityApps: "", + wmGlow: true, wmGlowRange: 14, wmGlowRp: 3, wmShadow: true, + wmShadowColor: "", wmShadowAlpha: 38, wmShadowRange: 16, + wmBorders: true, wmBorderSize: 1, wmBorderColor: "", + wmCorners: "round", wmRounding: 10, +} +const WM: Record = { ...DEF } +const Touched: Record = {} + +const WM_PATH = `${USER_DIR}/wm_config.lua` +const WM_KEYS = Object.keys(DEF) + +export const CORNER_OPTS = ["round", "bevel", "sharp"] +export const CORNER_LABEL: Record = { round: "ROUNDED", bevel: "BEVELED", sharp: "SHARP" } +export const OPACITY_MODES = ["off", "only", "except"] +export const OPACITY_MODE_LABEL: Record = { off: "GLOBAL ONLY", only: "ONLY SELECTED APPS", except: "ALL EXCEPT SELECTED" } + +const changeBus: Array<() => void> = [] +export const onWmChange = (cb: () => void): (() => void) => { + changeBus.push(cb) + return () => { const i = changeBus.indexOf(cb); if (i >= 0) changeBus.splice(i, 1) } +} +const notifyWmChange = () => { for (const cb of [...changeBus]) { try { cb() } catch (e) { print("[wm] notify:", e) } } } + +export const wmBool = (k: string): boolean => WM[k] === true +export const wmNum = (k: string): number => typeof WM[k] === "number" ? WM[k] as number : DEF[k] as number +export const wmStr = (k: string): string => typeof WM[k] === "string" ? WM[k] as string : "" +export const wmIsDefault = (k: string): boolean => WM[k] === DEF[k] + + +const loadWmConfig = (): void => { + try { + if (!GLib.file_test(WM_PATH, GLib.FileTest.EXISTS)) return + const [ok, bytes] = GLib.file_get_contents(WM_PATH) + if (!ok) return + for (const line of new TextDecoder().decode(bytes).split("\n")) { + const t = /t\["(\w+)"\]\s*=\s*true/.exec(line) + if (t && WM_KEYS.includes(t[1])) { Touched[t[1]] = true; continue } + const m = /wm\["(\w+)"\]\s*=\s*(true|false|"([^"]*)"|-?\d+(?:\.\d+)?)/.exec(line) + if (!m || !WM_KEYS.includes(m[1])) continue + if (typeof DEF[m[1]] === "boolean") WM[m[1]] = m[2] === "true" + else if (typeof DEF[m[1]] === "number") WM[m[1]] = parseFloat(m[2]) + else { if (m[3] === undefined) continue; WM[m[1]] = m[3] } + } + } catch (e) { print("[wm] load:", e) } +} +const saveWmConfig = (): void => { + try { + let out = "local wm = {}\nlocal t = {}\n" + for (const k of WM_KEYS) { + const v = WM[k] + out += typeof v === "boolean" ? `wm["${k}"] = ${v}\n` + : typeof v === "number" ? `wm["${k}"] = ${v}\n` + : `wm["${k}"] = "${String(v).replace(/"/g, "")}"\n` + if (Touched[k]) out += `t["${k}"] = true\n` + } + out += "return { wm = wm, t = t }\n" + GLib.file_set_contents(WM_PATH, out) + } catch (e) { print("[wm] save:", e) } +} + + +const hex2rgb = (h: string): [number, number, number] | null => { + if (!/^[0-9a-fA-F]{6}$/.test(h)) return null + return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)] +} +const hx = (v: number) => Math.round(v).toString(16).padStart(2, "0") + +export const wmCornersIs = (v: string): boolean => wmStr("wmCorners") === v + +const readShellColors = (): Record => { + const out: Record = {} + try { + const p = `${USER_DIR}/shell_colors.lua` + if (!GLib.file_test(p, GLib.FileTest.EXISTS)) return out + const [ok, bytes] = GLib.file_get_contents(p) + if (!ok) return out + for (const line of new TextDecoder().decode(bytes).split("\n")) { + const m = /(\w+)\s*=\s*("?)([0-9a-fA-F.]+)\2/.exec(line) + if (m) out[m[1]] = m[3] + } + } catch {} + return out +} + +export const applyWmLive = (): void => { + const sc = hex2rgb(wmStr("wmShadowColor")) + const bc = hex2rgb(wmStr("wmBorderColor")) + const theme = readShellColors() + const ta = hex2rgb(theme.accent || "") + const ta2 = hex2rgb(theme.accent2 || theme.accent || "") + const b1 = bc || ta + const b2 = bc || ta2 || ta + const glowOn = wmBool("wmGlow") && wmBool("wmBorders") + const op = wmBool("wmOpacity") ? wmNum("wmOpacityVal") : 1 + const alpha = Math.round(wmNum("wmShadowAlpha") * 2.55) + let round = Math.round(wmNum("wmRounding")) + if (wmCornersIs("sharp")) round = 0 + if (round < 0) round = 0 + if (round > 40) round = 40 + const power = wmCornersIs("bevel") ? 1 : 2 + let conf = `general={border_size=${wmBool("wmBorders") ? wmNum("wmBorderSize") : 0}` + if (b1 && b2) { + conf += `,col={active_border={colors={"rgba(${hx(b1[0])}${hx(b1[1])}${hx(b1[2])}ff)","rgba(${hx(b2[0])}${hx(b2[1])}${hx(b2[2])}ff)"},angle=45},inactive_border="rgba(${hx(b1[0])}${hx(b1[1])}${hx(b1[2])}44)"}` + } + conf += `},decoration={rounding=${round},rounding_power=${power},active_opacity=${op},inactive_opacity=${Math.max(0.5, op - 0.08).toFixed(2)}` + conf += `,glow={enabled=${glowOn}` + if (sc) conf += `,color="rgba(${hx(sc[0])}${hx(sc[1])}${hx(sc[2])}ff)"` + conf += `,range=${wmNum("wmGlowRange")},render_power=${wmNum("wmGlowRp")}}` + conf += `,shadow={enabled=${wmBool("wmShadow")},range=${wmNum("wmShadowRange")},render_power=${wmNum("wmGlowRp")}` + if (sc) conf += `,color="rgba(${hx(sc[0])}${hx(sc[1])}${hx(sc[2])}${alpha.toString(16).padStart(2, "0")})"` + const ic = sc || bc + if (ic) conf += `,color_inactive="rgba(${hx(ic[0])}${hx(ic[1])}${hx(ic[2])}18)"` + conf += `}}` + execAsync(["hyprctl", "eval", `hl.config({${conf}})`]).catch(() => {}) +} + + +const RULE_TAG = "cyberarch-wm" + +const ruleLua = (cls: string, op: number): string => + `hl.window_rule({ name = "${RULE_TAG}", match = { class = "${cls}" }, opacity = ${op} })` + +export const applyWmRules = (): void => { + const mode = wmStr("wmOpacityMode") + const apps = wmStr("wmOpacityApps").split(/[,\n]/).map((s) => s.trim()).filter(Boolean) + if (mode === "off" || !wmBool("wmOpacity") || apps.length === 0) return + const op = wmNum("wmOpacityVal") + const jobs: string[] = [] + for (const app of apps) { + const c = app.replace(/["\\]/g, "") + if (!c) continue + const cls = mode === "only" ? `(?i)^${c}$` : `(?i)^((?!${c}).)*$` + jobs.push(`h[#h+1] = ${ruleLua(cls, op)}`) + } + if (jobs.length === 0) return + execAsync(["hyprctl", "eval", `local h = {}; ${jobs.join(" ")}; return "ok"`]).catch(() => {}) +} + +export const setWm = (k: string, v: WmVal): void => { + if (!(k in DEF) || WM[k] === v) return + WM[k] = v + Touched[k] = true + saveWmConfig() + if (OPACITY_KEYS.includes(k)) applyWmRules() + if (LIVE_KEYS.includes(k)) applyWmLive() + notifyWmChange() +} +export const toggleWm = (k: string): void => { setWm(k, WM[k] !== true) } +export const resetWm = (keys: string[]): void => { + let hit = false + for (const k of keys) if (k in DEF && (WM[k] !== DEF[k] || Touched[k])) { WM[k] = DEF[k]; Touched[k] = false; hit = true } + if (!hit) return + saveWmConfig() + applyWmRules() + applyWmLive() + notifyWmChange() +} + +const OPACITY_KEYS = ["wmOpacity", "wmOpacityVal", "wmOpacityMode", "wmOpacityApps"] +const LIVE_KEYS = OPACITY_KEYS.concat(["wmGlow", "wmGlowRange", "wmGlowRp", "wmShadow", + "wmShadowColor", "wmShadowAlpha", "wmShadowRange", "wmBorders", "wmBorderSize", + "wmBorderColor", "wmCorners", "wmRounding"]) + +export const applyWmFromTheme = (): void => { + const sc = readShellColors() + if (Object.keys(sc).length === 0) return + if (!Touched["wmGlowRange"] && sc.glow_range) setWmSilent("wmGlowRange", parseInt(sc.glow_range, 10)) + if (!Touched["wmShadowRange"] && sc.glow_range) setWmSilent("wmShadowRange", parseInt(sc.glow_range, 10)) + if (!Touched["wmGlowRp"] && sc.glow_rp) setWmSilent("wmGlowRp", parseInt(sc.glow_rp, 10)) + if (!Touched["wmRounding"] && sc.rounding) setWmSilent("wmRounding", parseInt(sc.rounding, 10)) + if (!Touched["wmCorners"] && sc.rounding_power) setWmSilent("wmCorners", parseFloat(sc.rounding_power) < 1.5 ? "bevel" : "round") + applyWmLive() +} +const setWmSilent = (k: string, v: WmVal): void => { + if (!(k in DEF) || typeof v !== typeof DEF[k]) return + if (typeof v === "number" && isNaN(v)) return + WM[k] = v +} + +loadWmConfig() diff --git a/config/keybinds.lua b/config/keybinds.lua index 1f74a99c..f500160b 100644 --- a/config/keybinds.lua +++ b/config/keybinds.lua @@ -281,3 +281,10 @@ if f then pcall(f) end hl.define_submap("cyberdeck_capture", function() hl.bind("SUPER + CTRL + ALT + SHIFT + Escape", hl.dsp.submap("reset")) end) + +-- fresh hyprland installs ship an auto-generated hyprland.lua whose binds run +-- before the theme loads, so both fire on the same combo. the theme scan above +-- only covers user.lua; this sweeps the other files the compositor loads and, +-- when anything collides, tells the HUD to raise the conflict modal instead of +-- silently double-binding. +hl.exec_cmd("sh -c '" .. cyberpunk .. "/scripts/kbconflicts notify 2>/dev/null' &") diff --git a/core.ts b/core.ts index dfc1e4d7..0556d624 100644 --- a/core.ts +++ b/core.ts @@ -18,6 +18,7 @@ import GLib from "gi://GLib" import Gdk from "gi://Gdk?version=3.0" import { COMPONENTS_DIR, CYBER_DIR, SCREEN_WIDTH, SCREEN_HEIGHT } from "./env.ts" import { loadUserColors } from "./components/modules/colors.ts" +import { applyWmRules, applyWmFromTheme } from "./components/modules/wmconfig.ts" import { Monitors, setWorkspaceBadge } from "./components/modules/monitors.ts" import { SidePanel, openCityModal, openForecastModal } from "./components/modules/sidepanel.ts" import { MarketsPanel, openMarketsModal } from "./components/modules/markets.ts" @@ -35,6 +36,7 @@ import { RegionWindow, triggerRegion, triggerRecordRegion } from "./components/m import { ToastWindow, showToast } from "./components/modules/toast.ts" import { setTextHalo } from "./components/modules/proj.ts" import { CModalWindows, toggleModal } from "./components/modules/cmodal.ts" +import { openKbConflictsModal } from "./components/modules/kbconflicts.ts" import { AurBarWindow, dismissAurBar, dismissThemeBar, showInstalled } from "./components/modules/aurbar.ts" import { LauncherWindow } from "./components/modules/launcher.ts" import { AppsMenuWindow, openAppsMenu } from "./components/modules/appsmenu.ts" @@ -43,6 +45,7 @@ import { NowPlayingWindow } from "./components/modules/nowplaying.ts" const SCSS = `${COMPONENTS_DIR}/style/cyber.scss` const CSS = `${COMPONENTS_DIR}/style/cyber.css` +const AURBAR_DISABLED = GLib.file_test(`${GLib.get_home_dir()}/.config/cyberarch/disable-aurbar`, GLib.FileTest.EXISTS) const compileCss = async () => { try { @@ -294,6 +297,9 @@ App.start({ } else if (request.startsWith("pkg-installed ")) { try { const d = request.slice(14); const i = d.indexOf("|"); showInstalled(i < 0 ? d : d.slice(0, i), i < 0 ? "" : d.slice(i + 1)) } catch (e) { print(e) } reply("ok") +} else if (request.startsWith("kbconflicts")) { + try { openKbConflictsModal(request) } catch (e) { print(e) } + reply("ok") } else reply("unknown request") }, @@ -302,6 +308,8 @@ App.start({ main() { compileCss() loadUserColors() + applyWmRules() + applyWmFromTheme() for (const mon of (App as any).get_monitors()) { surface(mon, "monitors", Anchor.TOP | Anchor.LEFT, Monitors()) @@ -313,7 +321,7 @@ App.start({ } passthrough(OsdWindow()) passthrough(NotifPopupWindow()) - passthrough(AurBarWindow()) + if (!AURBAR_DISABLED) passthrough(AurBarWindow()) NotifHudWindow() NowPlayingWindow() WsAnimWindow() diff --git a/install.sh b/install.sh index abd73b43..4c0772d7 100755 --- a/install.sh +++ b/install.sh @@ -5,7 +5,14 @@ R=$'\033[0m'; B=$'\033[1m'; DIM=$'\033[2m' RED=$'\033[38;2;255;45;61m'; CYAN=$'\033[38;2;119;226;242m' YEL=$'\033[38;2;255;214;31m'; GRN=$'\033[38;2;90;230;130m'; GREY=$'\033[38;2;120;120;130m' line() { printf "${RED}%s${R}\n" "────────────────────────────────────────────────────────────"; } -hdr() { printf "\n${CYAN}${B}▓▒░ %s ░▒▓${R}\n" "$1"; } +hdr() { + local t="▓▒░ $1 ░▒▓" + local pad=$(( 61 - ${#t} )) + (( pad < 1 )) && pad=1 + printf "\n${CYAN}${B} ╔═══════════════════════════════════════════════════════════╗\n" + printf " ║ %s%*s║\n" "$t" "$pad" "" + printf " ╚═══════════════════════════════════════════════════════════╝${R}\n" +} step() { printf "${CYAN}▸${R} %s\n" "$1"; } ok() { printf " ${GRN}✓${R} %s\n" "$1"; } warn() { printf " ${YEL}⚠${R} %s\n" "$1"; } @@ -13,9 +20,9 @@ err() { printf " ${RED}✗${R} %s\n" "$1"; } fatal() { printf "\n${RED}${B}" cat <<'EOF' - ╔═══════════════════════════════════════════════════════════╗ - ║ ▓▒░ I N S T A L L A B O R T E D ░▒▓ ║ - ╚═══════════════════════════════════════════════════════════╝ + ╔════════════════════════════════════════════════════════════════════╗ + ║ ▓▒░ F L A T L I N E D ░▒▓ ║ + ╚════════════════════════════════════════════════════════════════════╝ EOF printf "${R}\n ${RED}${B}✗ %s${R}\n\n" "$1"; shift for l in "$@"; do printf " ${CYAN}→${R} %s\n" "$l"; done @@ -79,10 +86,26 @@ pac_install() { aur_install() { local label="$1"; shift local helper; helper="$(command -v paru || command -v yay || true)" + if [ -z "$helper" ]; then + if [ -t 0 ]; then + hdr "BOOTSTRAPPING YAY" + step "building yay from the AUR (base-devel + git + makepkg)…" + sudo pacman -S --needed --noconfirm base-devel git >/dev/null 2>&1 + local ytmp; ytmp="$(mktemp -d)" + if git clone --depth 1 https://aur.archlinux.org/yay.git "$ytmp/yay" 2>/dev/null; then + if (cd "$ytmp/yay" && makepkg -si --noconfirm --needed >/dev/null 2>&1); then + ok "yay installed"; helper="$(command -v yay || true)" + else err "yay build failed — will fall back to manual instructions." + fi + else err "could not clone the yay AUR repo — network or AUR down?" + fi + rm -rf "$ytmp" + fi + fi if [ -z "$helper" ]; then fatal "$label needs an AUR helper and this deck has none." \ "paru or yay has to build: $*" \ - "Bootstrap it: sudo pacman -S --needed base-devel git && git clone https://aur.archlinux.org/paru.git && cd paru && makepkg -si" \ + "Bootstrap it: sudo pacman -S --needed base-devel git && git clone https://aur.archlinux.org/yay.git && cd yay && makepkg -si" \ "Then re-run: ./install.sh" fi if "$helper" -S --needed "$@"; then ok "$label installed"; return 0; fi @@ -95,14 +118,14 @@ aur_install() { "Most AUR flatlines are a half-synced system — run sudo pacman -Syu first, then reboot." \ "Then re-run: ./install.sh" } -dm_current() { - local l u - l="$(readlink -f /etc/systemd/system/display-manager.service 2>/dev/null || true)" - if [ -n "$l" ] && [ -e "$l" ]; then basename "$l" .service; return 0; fi - for u in sddm gdm lightdm ly greetd lxdm cosmic-greeter plasma-login; do - if systemctl is-enabled --quiet "$u.service" 2>/dev/null; then printf '%s' "$u"; return 0; fi - done - printf '' +dm_current() { +local l u +l="$(readlink -f /etc/systemd/system/display-manager.service 2>/dev/null | true)" +if [ -n "$1" ] && [ -e "$1" ]; then basename "$1" .service; return 0; fi +for u in sddm gdm lightdm ly greetd lxdm cosmic-greeter plasma-login; do +if systemctl is-enabled --quiet "$u.service" 2>/dev/null; then printf '%s' "$u"; return 0; fi +done +printf '' } MESA_PKGS="mesa mesa-utils libdrm lib32-libdrm lib32-mesa" HYP_PKGS="hyprland hyprgraphics hyprland-guiutils hyprlock hyprtoolkit hyprwire xdg-desktop-portal-hyprland lua lua54 gcc gcc-libs hyprlang ffmpeg ffmpeg4.4 chromaprint" @@ -118,6 +141,7 @@ REPO=( openconnect pipewire-jack pipewire-v4l2 pipewire-x11-bell pipewire-zeroconf ) AUR=( + yay aylurs-gtk-shell libastal-gjs-git libastal-notifd-git libastal-wireplumber-git libastal-mpris-git pamtester @@ -219,7 +243,13 @@ if [ ${#miss_repo[@]} -gt 0 ] || [ ${#miss_aur[@]} -gt 0 ]; then helper="$(command -v paru || command -v yay || true)" printf "\n${CYAN}AUR PACKAGES REQUIRED:${R} ${B}%s${R}\n" "${miss_aur[*]}" if [ -z "$helper" ]; then - warn "no AUR helper (paru/yay) found. Install these manually." + printf "[!] No AUR helper found. Bootstrap yay now and install AUR deps? (y/N) " + read -r ans /dev/null 2>&1; then printf 'no'; return 0; fi fi if command -v hyprctl >/dev/null 2>&1; then @@ -500,11 +543,12 @@ lock_proto_state() { fi printf 'unknown' } + if [ "$LOCK_STACK" != 1 ]; then QS_LOGIN_OK=0 warn "theme lock skipped |::| no quickshell, no PAM rewrite, $CUR_DM untouched" else - QT6="glibc lib32-glibc qt6-multimedia-ffmpeg qt6-base qt6-declarative qt6-svg qt6-wayland qt6-5compat" + QT6="glibc lib32-glibc qt6-multimedia qt6-multimedia-ffmpeg qt6-base qt6-declarative qt6-svg qt6-wayland qt6-5compat" pac_install "qt6 runtime" $QT6 if ! qs_ok; then step "installing quickshell" @@ -961,52 +1005,6 @@ if [ -f "$HYDIR/hyprland.conf" ]; then fi hdr "KEYBIND CONFLICTS" -LUA_BIN="$(command -v lua5.4 || command -v lua || true)" -THEME_KEYS="" -if [ -n "$LUA_BIN" ] && [ -f "$THEME/theme.lua" ]; then - THEME_KEYS="$("$LUA_BIN" - 2>/dev/null <<'LUAE' -local combos = {} -local node -node = function() - return setmetatable({}, { - __index = function() return node() end, - __call = function() return {} end, - }) -end -hl = { - dsp = node(), - exec_cmd = function() end, exec = function() end, exec_once = function() end, - on = function() end, env = function() end, config = function() end, - curve = function() end, animation = function() end, - window_rule = function() end, layer_rule = function() end, plugin = node(), - gesture = function() end, workspace_rule = function() end, submap = function() end, - bind = function(mods) - local d = "" - local info = debug.getinfo(2, "Sl") - if info and info.short_src and info.currentline then - local fh = io.open(info.short_src, "r") - if fh then - local i = 0 - for line in fh:lines() do - i = i + 1 - if i == info.currentline then d = line break end - end - fh:close() - end - end - combos[#combos + 1] = tostring(mods) .. "\t" .. d - end, - define_submap = function(_, fn) if type(fn) == "function" then fn() end end, -} -local mod = os.getenv("HOME") .. "/.config/hypr/themes/cyberpunk" -package.path = mod .. "/?.lua;" .. package.path -local ok, err = pcall(require, "theme") -if not ok then print("__ERROR__ " .. tostring(err)) end -for i = 1, #combos do print(combos[i]) end -LUAE -)" -fi -mapfile -t THEME_RAW <<< "$THEME_KEYS" canon() { local combo="$1" t key="" n combo="$(printf '%s' "$combo" | tr '[:lower:]' '[:upper:]')" @@ -1022,113 +1020,38 @@ canon() { printf '%s' "$key" fi } -kb_pretty() { - case "$1" in - vol) printf 'volume' ;; - brt) printf 'brightness' ;; - pwr) printf 'power menu' ;; - bt) printf 'bluetooth' ;; - bat) printf 'battery' ;; - sys) printf 'system monitor' ;; - keys) printf 'keybind help' ;; - aur) printf 'system upgrade' ;; - wifi) printf 'wifi' ;; - mic) printf 'microphone' ;; - toggle-hud) printf 'toggle HUD' ;; - player) printf 'music player' ;; - forecast) printf 'weather forecast' ;; - clock) printf 'system time' ;; - weather) printf 'city picker' ;; - aur-dismiss) printf 'dismiss update bar' ;; - notif-hud) printf 'notification center' ;; - notif-read) printf 'open notification' ;; - notif-dismiss) printf 'dismiss notification' ;; - overkill) printf 'kill mode' ;; - screenrecord) printf 'screen record' ;; - screenshot) printf 'screenshot' ;; - launcher) printf 'app launcher' ;; - terminal) printf 'netrunner terminal' ;; - peek) printf 'peek desktop' ;; - ws) printf 'workspace switch' ;; - TERM) printf 'terminal' ;; - *) printf '%s' "${1//-/ }" ;; - esac -} -kb_desc() { - local l="$1" m - m="$(printf '%s' "$l" | sed -n 's/.*sock("\([^"]*\)").*/\1/p')" - if [ -n "$m" ]; then - case "$m" in - "modal "*) kb_pretty "${m#modal }" ;; - *) kb_pretty "$m" ;; - esac - return 0 - fi - m="$(printf '%s' "$l" | sed -n 's|.*/scripts/\([A-Za-z0-9_-]*\).*|\1|p')" - [ -n "$m" ] && { kb_pretty "$m"; return 0; } - m="$(printf '%s' "$l" | sed -n 's/.*exec_cmd(\([A-Za-z0-9_]*\)).*/\1/p')" - [ -n "$m" ] && { kb_pretty "$m"; return 0; } - m="$(printf '%s' "$l" | sed -n 's/.*hl\.dsp\.\([A-Za-z0-9_.]*\).*/\1/p')" - if [ -n "$m" ] && [ "$m" != "exec_cmd" ]; then printf '%s' "${m//./ }"; return 0; fi - printf 'theme bind' -} -declare -a THEME_COMBOS=() -declare -A THEME_DESC=() -for c in "${THEME_RAW[@]}"; do - [[ "$c" == __ERROR__* ]] && { warn "${c#__ERROR__ }"; continue; } - [ -z "$c" ] && continue - _combo="${c%% *}"; _src="${c#* }" - [ "$_src" = "$c" ] && _src="" - _cc="$(canon "$_combo")" - THEME_COMBOS+=("$_cc") - [ -n "$_src" ] && THEME_DESC["$_cc"]="$(kb_desc "$_src")" -done -THAS() { - local want="$1" t - for t in "${THEME_COMBOS[@]}"; do [ "$t" = "$want" ] && return 0; done - return 1 -} -declare -a CF_FILE=() CF_LINE=() CF_TEXT=() CF_COMBO=() CF_DESC=() CF_MARK=() +declare -a CF_FILE=() CF_LINE=() CF_TEXT=() CF_COMBO=() CF_DESC=() CF_MARK=() CF_TSRC=() cf_add() { CF_FILE+=("$1"); CF_LINE+=("$2"); CF_TEXT+=("$3"); CF_COMBO+=("$4") - CF_DESC+=("${THEME_DESC[$4]:-theme bind}"); CF_MARK+=(1) + CF_DESC+=("${5:-theme bind}"); CF_MARK+=(1) } cf_comment() { local f="$1" n="$2" if [[ "$f" == *.lua ]]; then sed -i "${n}s|^|-- |" "$f"; else sed -i "${n}s|^|#|" "$f"; fi } -lua_loadvars() { - local f="$1" ln name val - [ -f "$f" ] || return 0 - while IFS= read -r ln; do - [[ "$ln" =~ ^[[:space:]]*local[[:space:]]+([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=[[:space:]]*["'\''"]([^"'\''"]*)["'\''"] ]] || continue - val="${BASH_REMATCH[2]}"; val="$(printf '%s' "$val" | sed 's/[[:space:]]*$//')" - LVARS["${BASH_REMATCH[1]}"]="$val" - done < "$f" +# the lua side of the scan lives in scripts/kbconflicts — same engine the theme +# runs on load, so the installer and the runtime modal never disagree on what +# counts as a conflict +kbc_records() { + local kbc="$THEME/scripts/kbconflicts" rec f n combo tsrc text lbl + [ -f "$kbc" ] || { warn "scripts/kbconflicts missing |::| lua-side keybind scan skipped."; return 0; } + while IFS= read -r rec; do + [ -n "$rec" ] || continue + f="${rec%%|*}"; rec="${rec#*|}" + n="${rec%%|*}"; rec="${rec#*|}" + combo="${rec%%|*}"; rec="${rec#*|}" + tsrc="${rec%%|*}"; rec="${rec#*|}" + text="${rec%%|*}"; lbl="${rec#*|}" + cf_add "$f" "$n" "$text" "$combo" "theme: ${lbl:+$lbl @ }$tsrc" + done < <(bash "$kbc" check 2>/dev/null) } -lua_arg() { - local s="$1" name pass - for pass in 1 2 3; do for name in "${!LVARS[@]}"; do s="${s//$name/${LVARS[$name]}}"; done; done - s="${s//\"/}"; s="${s//\'/}"; s="${s//../ }" - s="$(printf '%s' "$s" | tr -s ' ' | sed 's/^ *//;s/ *$//')" - printf '%s' "$s" -} -lua_scan() { - local f="$1" n=0 ln c raw - declare -A LVARS=() - [ -f "$f" ] || return 0 - lua_loadvars "$f" - while IFS= read -r ln; do - n=$((n+1)) - [[ "$ln" =~ ^[[:space:]]*-- ]] && continue - raw="$(printf '%s\n' "$ln" | sed -n 's/.*hl\.bind([[:space:]]*\([^,)]*\).*/\1/p')" - [ -n "$raw" ] || continue - c="$(canon "$(lua_arg "$raw")")" - THAS "$c" || continue - cf_add "$f" "$n" "$(printf '%s' "$ln" | sed 's/^[[:space:]]*//')" "$c" - done < "$f" - return 0 +THAS() { + local want="$1" t + for t in "${THEME_KEYS_CANON[@]}"; do [ "$t" = "$want" ] && return 0; done + return 1 } +declare -a THEME_KEYS_CANON=() +[ -f "$THEME/scripts/kbconflicts" ] && mapfile -t THEME_KEYS_CANON < <(bash "$THEME/scripts/kbconflicts" --theme 2>/dev/null) USERCONF="$HYDIR/user.conf" declare -A HVARS=() kb_loadvars() { @@ -1262,10 +1185,8 @@ kb_picker() { else ok "$applied conflicting bind(s) commented out."; fi return 0 } +kbc_records kb_loadvars "$HYDIR/hyprland.conf"; kb_loadvars "$USERCONF" -lua_scan "$HYLUA" -lua_scan "$HYDIR/user.lua" -for _lf in "$HYDIR"/land/*.lua; do [ -f "$_lf" ] && lua_scan "$_lf"; done kb_scan "$HYDIR/hyprland.conf" kb_scan "$USERCONF" kb_picker @@ -1315,7 +1236,7 @@ GTCFG="$HOME/.config/rio" GTBIN="${CARGO_HOME:-$HOME/.cargo}/bin/rio" GTVER="0.4.5" GTKEY="SUPER + T" -gt_has_gpu() { command -v strings >/dev/null 2>&1 || return 1; [ -x "$1" ] && strings -n 8 "$1" 2>/dev/null | grep -qi librashader; } +gt_has_gpu() { command -v strings >/dev/null 2>&1 || return 1; [ -x "$1" ] && strings -n 8 "$1" 2>/dev/null | grep -qi librashader >/dev/null; } gt_runs() { [ -x "$1" ] && "$1" --version >/dev/null 2>&1; } gt_rust_ok() { command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1 && rustc -vV >/dev/null 2>&1 && cargo -V >/dev/null 2>&1; } gt_rust_err() { rustc -vV 2>&1 | grep -v '^$' | head -1; } @@ -1337,7 +1258,7 @@ if [ ! -d "$GTSRC" ]; then warn "GPU Terminal assets missing. |::| Skipping..." else GT_OK=0 - GT_DEPS="rust llvm-libs cmake pkgconf binutils fontconfig freetype2 libxkbcommon wayland vulkan-icd-loader mesa glibc lib32-glibc" + GT_DEPS="rust llvm-libs cmake pkgconf librashader binutils fontconfig freetype2 libxkbcommon wayland vulkan-icd-loader mesa glibc lib32-glibc" step "installing rust toolchain + build dependencies..." pac_install "rio build dependencies" $GT_DEPS if gt_icd_present; then @@ -1483,6 +1404,8 @@ fi hdr "MESA PACKAGES INSTALLATION" step "installing/refreshing $MESA_PKGS before restart…" sudo pacman -S --needed $MESA_PKGS +step "re-asserting qt6-multimedia (quickshell lock screen needs it)…" +sudo pacman -S --needed qt6-multimedia clear printf "${RED}${B}" @@ -1521,18 +1444,27 @@ else fi if dm_active; then - printf "[!] Restart Hyprland now? (y/N) " - read -r ans /dev/null || hyprctl dispatch exit >/dev/null 2>&1 -else - printf "${GREY} Restart Hyprland yourself when ready (log out / back in, or: ${B}pkill Hyprland${R}${GREY}).${R}\n" -fi + warn "no active greeter on this deck |::| If you delta the compositor right now, will drop you to a TTY space" + printf "[:!:] Restart Hyprland anyway ? (y/N) " + read -r ans restarting Hyprland.${R}\n" + pkill -x Hyprland 2>/dev/null || hyprctl dispatch exit >/dev/null 2>&1 + if [ -n "${DM_OLD:-}" ] && pgrep -x plasmalogin >/dev/null 2>&1; then + sudo systemctl start sddm 2>/dev/null || true + sudo systemctl stop plasmalogin.service 2>/dev/null || true + ok 'greeter swapped plasmalogin -> sddm' + fi + if dm_active; then + sleep 1 + sudo chvt 1 2>/dev/null || true + ok "greeter screen is on tty1 |::| press CTRL+ALT+F1 if the login screen does not come up" + fi + else + printf "${GREY} Restart Hyprland yourself when ready (logout / back in, or: ${B}pkill Hyprland${R}${GREY}).${R}\n" + fi line diff --git a/scripts/apply_theme b/scripts/apply_theme index 72c19656..33133647 100755 --- a/scripts/apply_theme +++ b/scripts/apply_theme @@ -10,7 +10,9 @@ ICON_NAME="iconpack" mkdir -p "$HOME/.local/share/icons" ln -sfn "$GTK/iconpack" "$HOME/.local/share/icons/$ICON_NAME" -# ── GTK 3 and 4: dark base theme + icons, and link in gtk.css glass tweaks +# ── GTK 3 and 4: dark base theme + icons, and the per-theme gtk.css from shell-theme +PALETTE=$(sed -n 's/^local palette = "\([A-Z]*\)".*/\1/p' \ + "${XDG_CONFIG_HOME:-$HOME/.config}/cyberarch/user_colors.lua" 2>/dev/null | tail -n 1) for v in 3.0 4.0; do d="$HOME/.config/gtk-$v"; mkdir -p "$d" cat > "$d/settings.ini" </dev/null 2>&1; then @@ -30,17 +31,8 @@ if command -v gsettings >/dev/null 2>&1; then gsettings set org.gnome.desktop.interface cursor-theme "neurodance" 2>/dev/null fi -KV_NAME="Daemon" -KV_SRC="$GTK/DaemonKvantum" -KVDIR="$HOME/.config/Kvantum/$KV_NAME" rm -rf "$HOME/.config/Kvantum/daemon-2.0" -mkdir -p "$KVDIR" -cp -f "$KV_SRC/$KV_NAME.kvconfig" "$KVDIR/$KV_NAME.kvconfig" -cp -f "$KV_SRC/$KV_NAME.svg" "$KVDIR/$KV_NAME.svg" -cat > "$HOME/.config/Kvantum/kvantum.kvconfig" </dev/null <<'LUAE' +local binds = {} +local node +node = function() return setmetatable({}, { __index = function() return node() end, __call = function() return {} end }) end +local noop = function() end +hl = { + dsp = node(), exec_cmd = noop, exec = noop, exec_once = noop, + on = noop, env = noop, config = noop, curve = noop, animation = noop, + window_rule = noop, layer_rule = noop, plugin = node(), + gesture = noop, workspace_rule = noop, submap = noop, + bind = function(combo) + local info = debug.getinfo(3, "Sl") + if info and info.short_src and info.currentline then + binds[#binds + 1] = tostring(combo) .. "\t" .. info.short_src .. "\t" .. info.currentline + end + end, + define_submap = function(_, fn) if type(fn) == "function" then fn() end end, +} +local mod = os.getenv("HOME") .. "/.config/hypr/themes/cyberpunk" +package.path = mod .. "/?.lua;" .. package.path +if pcall(require, "theme") then + for i = 1, #binds do print(binds[i]) end + local function canonkey(c) + if not c then return nil end + local parts = {} + for p in string.gmatch(tostring(c), "[^%+]+") do + p = p:gsub("^%s+", ""):gsub("%s+$", ""):upper() + if p ~= "" then parts[#parts + 1] = p end + end + table.sort(parts) + return table.concat(parts, "+") + end + if CD and CD.reserved then + for _, r in ipairs(CD.reserved) do + local k = canonkey(r.combo) + if k then print(k .. "\tLABEL\t" .. tostring(r.label or "")) end + end + end +end +LUAE +)" +fi + +# canon: same normalisation the theme's normCombo uses — mods sorted, key last, +# so SHIFT+SUPER+Z and SUPER+SHIFT+Z compare equal. +canon() { + # sets CANON_OUT instead of printing — saves a fork per call + local combo="${1^^}" t key n i swapped + local -a parts=() + read -ra parts <<< "${combo//+/ }" + n="${#parts[@]}"; [ "$n" -eq 0 ] && return 1 + key="${parts[n-1]}" + unset 'parts[n-1]' + local -a clean=() + for t in "${parts[@]:-}"; do t="${t// /}"; [ -n "$t" ] && clean+=("$t"); done + # insertion sort — at most 4 modifier tokens, cheaper than forking sort + for ((i = 1; i < ${#clean[@]}; i++)); do + t="${clean[i]}"; swapped=$((i - 1)) + while ((swapped >= 0)) && [[ "${clean[swapped]}" > "$t" ]]; do + clean[swapped + 1]="${clean[swapped]}"; swapped=$((swapped - 1)) + done + clean[swapped + 1]="$t" + done + local mods="" + for t in "${clean[@]:-}"; do mods+="$t+"; done + CANON_OUT="$mods$key" + return 0 +} + +# theme bind table: canon -> "src:line" +declare -A THEME_BY_KEY=() THEME_SRC=() THEME_LABEL=() +while IFS=$'\t' read -r combo src line; do + [ -n "${combo:-}" ] || continue + if [ "$src" = "LABEL" ]; then THEME_LABEL["$combo"]="$line"; continue; fi + canon "$combo" || continue + c="$CANON_OUT" + THEME_BY_KEY["$c"]=1 + [ -n "${THEME_SRC[$c]:-}" ] || THEME_SRC["$c"]="$([ "${src##*/}" = "[C]" ] && printf 'keybinds.lua' || printf '%s:%s' "${src##*/}" "$line")" +done <<< "$THEME_KEYS" + +# ---- user side: parse hl.bind( lines out of every lua file the compositor loads +# besides the theme itself. local string vars get expanded, and for-loop bodies +# like `hl.bind(mainMod .. " + " .. i, ...)` get expanded for i = 0..9 since the +# theme's workspace binds live exactly there. +declare -a CF_FILE=() CF_LINE=() CF_TEXT=() CF_COMBO=() CF_TSRC=() CF_TLBL=() + +scan_lua() { + local f="$1" n=0 ln raw c i exp + [ -f "$f" ] || return 0 + local -A LVARS=() + while IFS= read -r ln; do + if [[ "$ln" =~ ^[[:space:]]*local[[:space:]]+([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=[[:space:]]*[\"\']([^\"\']*)[\"\'] ]]; then + LVARS["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}" + fi + done < "$f" + while IFS= read -r ln; do + n=$((n+1)) + [[ "$ln" =~ ^[[:space:]]*-- ]] && continue + raw="$(printf '%s\n' "$ln" | sed -n 's/.*hl\.bind([[:space:]]*\([^,)]*\).*/\1/p')" + [ -n "$raw" ] || continue + # expand known string vars + for i in "${!LVARS[@]}"; do raw="${raw//$i/${LVARS[$i]}}"; done + raw="${raw//\"/}"; raw="${raw//\'/}"; raw="${raw//../ }" + raw="$(printf '%s' "$raw" | tr -s ' ' | sed 's/^ *//;s/ *$//')" + # loop vars: try i/key/j/k = 0..9 when the expression still mentions one + local -a cands=("$raw") + if [[ "$raw" =~ (^|[^A-Za-z0-9_])(i|key|j|k)([^A-Za-z0-9_]|$) ]]; then + cands=() + local vname="${BASH_REMATCH[2]}" + local marked="${raw//${vname}/@@KB@@}" + for exp in 0 1 2 3 4 5 6 7 8 9; do + cands+=("${marked//@@KB@@/${exp}}") + done + fi + for exp in "${cands[@]}"; do + canon "$exp" || continue + c="$CANON_OUT" + if [ -n "${THEME_BY_KEY[$c]:-}" ]; then + CF_FILE+=("$f"); CF_LINE+=("$n") + CF_TEXT+=("$(printf '%s' "$ln" | sed 's/^[[:space:]]*//' | cut -c1-90)") + CF_COMBO+=("$c"); CF_TSRC+=("${THEME_SRC[$c]}"); CF_TLBL+=("${THEME_LABEL[$c]:-}") + break + fi + done + done < "$f" +} + +# user.lua is deliberately scanned by the theme itself (its binds win on +# purpose), so it never counts as a conflict source. same for files that +# aren't actually required by hyprland.lua — scanning dead configs just cries +# wolf. land/ only counts when the live hyprland.lua pulls it in. +scan_lua "$HYLUA" +if [ -f "$HYLUA" ] && grep -qE '^\s*(require|dofile)\(\s*"land\.' "$HYLUA"; then + for _lf in "$HYDIR"/land/*.lua; do [ -f "$_lf" ] && scan_lua "$_lf"; done +fi + +if [ "$MODE" = "theme" ]; then + for k in "${!THEME_BY_KEY[@]}"; do printf '%s\n' "$k"; done + exit 0 +fi + +total="${#CF_FILE[@]}" +if [ "$total" -eq 0 ]; then + [ "$MODE" = "check" ] && echo "OK" + exit 0 +fi + +emit() { + # file|line|combo|theme_src|line_text — one record per conflict + printf '%s|%s|%s|%s|%s|%s\n' "${CF_FILE[$1]}" "${CF_LINE[$1]}" "${CF_COMBO[$1]}" "${CF_TSRC[$1]}" "${CF_TEXT[$1]}" "${CF_TLBL[$1]}" +} + +if [ "$MODE" = "check" ]; then + for ((i = 0; i < total; i++)); do emit "$i"; done + exit 0 +fi + +# notify: one line to the HUD socket — "kbconflicts :: ::