diff --git a/modules/home-manager/ags/ags/assets/icons/arch-symbolic.svg b/modules/home-manager/ags/ags/assets/icons/arch-symbolic.svg
new file mode 100644
index 0000000..7de9094
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/icons/arch-symbolic.svg
@@ -0,0 +1,113 @@
+
+
+
+
diff --git a/modules/home-manager/ags/ags/assets/icons/cachyos-symbolic.svg b/modules/home-manager/ags/ags/assets/icons/cachyos-symbolic.svg
new file mode 100644
index 0000000..4a9db19
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/icons/cachyos-symbolic.svg
@@ -0,0 +1,318 @@
+
+
diff --git a/modules/home-manager/ags/ags/assets/icons/cloudflare-dns-symbolic.svg b/modules/home-manager/ags/ags/assets/icons/cloudflare-dns-symbolic.svg
new file mode 100644
index 0000000..bd48d3c
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/icons/cloudflare-dns-symbolic.svg
@@ -0,0 +1,10 @@
+
diff --git a/modules/home-manager/ags/ags/assets/icons/crosshair-symbolic.svg b/modules/home-manager/ags/ags/assets/icons/crosshair-symbolic.svg
new file mode 100644
index 0000000..2296749
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/icons/crosshair-symbolic.svg
@@ -0,0 +1,65 @@
+
+
+
+
diff --git a/modules/home-manager/ags/ags/assets/icons/debian-symbolic.svg b/modules/home-manager/ags/ags/assets/icons/debian-symbolic.svg
new file mode 100644
index 0000000..252f853
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/icons/debian-symbolic.svg
@@ -0,0 +1,91 @@
+
+
+
+
diff --git a/modules/home-manager/ags/ags/assets/icons/endeavouros-symbolic.svg b/modules/home-manager/ags/ags/assets/icons/endeavouros-symbolic.svg
new file mode 100644
index 0000000..3be4cc4
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/icons/endeavouros-symbolic.svg
@@ -0,0 +1,96 @@
+
+
diff --git a/modules/home-manager/ags/ags/assets/icons/fedora-symbolic.svg b/modules/home-manager/ags/ags/assets/icons/fedora-symbolic.svg
new file mode 100644
index 0000000..1a4e8c8
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/icons/fedora-symbolic.svg
@@ -0,0 +1,38 @@
+
+
diff --git a/modules/home-manager/ags/ags/assets/icons/flatpak-symbolic.svg b/modules/home-manager/ags/ags/assets/icons/flatpak-symbolic.svg
new file mode 100644
index 0000000..0c2bf62
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/icons/flatpak-symbolic.svg
@@ -0,0 +1,52 @@
+
+
diff --git a/modules/home-manager/ags/ags/assets/icons/github-symbolic.svg b/modules/home-manager/ags/ags/assets/icons/github-symbolic.svg
new file mode 100644
index 0000000..c1c9f19
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/icons/github-symbolic.svg
@@ -0,0 +1,40 @@
+
+
diff --git a/modules/home-manager/ags/ags/assets/icons/linux-symbolic.svg b/modules/home-manager/ags/ags/assets/icons/linux-symbolic.svg
new file mode 100644
index 0000000..63f9c7e
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/icons/linux-symbolic.svg
@@ -0,0 +1,113 @@
+
+
+
+
diff --git a/modules/home-manager/ags/ags/assets/icons/nixos-symbolic.svg b/modules/home-manager/ags/ags/assets/icons/nixos-symbolic.svg
new file mode 100644
index 0000000..b697b0d
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/icons/nixos-symbolic.svg
@@ -0,0 +1,77 @@
+
+
diff --git a/modules/home-manager/ags/ags/assets/icons/ollama-symbolic.svg b/modules/home-manager/ags/ags/assets/icons/ollama-symbolic.svg
new file mode 100644
index 0000000..0145481
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/icons/ollama-symbolic.svg
@@ -0,0 +1,60 @@
+
+
diff --git a/modules/home-manager/ags/ags/assets/icons/openrouter-symbolic.svg b/modules/home-manager/ags/ags/assets/icons/openrouter-symbolic.svg
new file mode 100644
index 0000000..32aaaf5
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/icons/openrouter-symbolic.svg
@@ -0,0 +1,39 @@
+
+
diff --git a/modules/home-manager/ags/ags/assets/icons/ubuntu-symbolic.svg b/modules/home-manager/ags/ags/assets/icons/ubuntu-symbolic.svg
new file mode 100644
index 0000000..07746c9
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/icons/ubuntu-symbolic.svg
@@ -0,0 +1,85 @@
+
+
+
+
diff --git a/modules/home-manager/ags/ags/assets/themes/sourceviewtheme-dark-monokai-license.txt b/modules/home-manager/ags/ags/assets/themes/sourceviewtheme-dark-monokai-license.txt
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/themes/sourceviewtheme-dark-monokai-license.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/modules/home-manager/ags/ags/assets/themes/sourceviewtheme-light.xml b/modules/home-manager/ags/ags/assets/themes/sourceviewtheme-light.xml
new file mode 100644
index 0000000..bf39bfb
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/themes/sourceviewtheme-light.xml
@@ -0,0 +1,95 @@
+
+
+ end_4
+ <_description>Catppuccin port but very random
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/home-manager/ags/ags/assets/themes/sourceviewtheme.xml b/modules/home-manager/ags/ags/assets/themes/sourceviewtheme.xml
new file mode 100644
index 0000000..248d7af
--- /dev/null
+++ b/modules/home-manager/ags/ags/assets/themes/sourceviewtheme.xml
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+ Leo Iannacone
+ <_description>Based on SublimeText Monokai Extended - Generated with tm2gtksw2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/home-manager/ags/ags/config.js b/modules/home-manager/ags/ags/config.js
new file mode 100644
index 0000000..73e57b5
--- /dev/null
+++ b/modules/home-manager/ags/ags/config.js
@@ -0,0 +1,81 @@
+"use strict";
+// Import
+import Gdk from 'gi://Gdk';
+import GLib from 'gi://GLib';
+import App from 'resource:///com/github/Aylur/ags/app.js'
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
+// Stuff
+import userOptions from './modules/.configuration/user_options.js';
+import { firstRunWelcome, startBatteryWarningService } from './services/messages.js';
+import { startAutoDarkModeService } from './services/darkmode.js';
+// Widgets
+import { Bar, BarCornerTopleft, BarCornerTopright } from './modules/bar/main.js';
+import Cheatsheet from './modules/cheatsheet/main.js';
+// import DesktopBackground from './modules/desktopbackground/main.js';
+import Dock from './modules/dock/main.js';
+import Corner from './modules/screencorners/main.js';
+import Crosshair from './modules/crosshair/main.js';
+import Indicator from './modules/indicators/main.js';
+import Osk from './modules/onscreenkeyboard/main.js';
+import Overview from './modules/overview/main.js';
+import Session from './modules/session/main.js';
+import SideLeft from './modules/sideleft/main.js';
+import SideRight from './modules/sideright/main.js';
+import { COMPILED_STYLE_DIR } from './init.js';
+
+const range = (length, start = 1) => Array.from({ length }, (_, i) => i + start);
+function forMonitors(widget) {
+ const n = Gdk.Display.get_default()?.get_n_monitors() || 1;
+ return range(n, 0).map(widget).flat(1);
+}
+function forMonitorsAsync(widget) {
+ const n = Gdk.Display.get_default()?.get_n_monitors() || 1;
+ return range(n, 0).forEach((n) => widget(n).catch(print))
+}
+
+// Start stuff
+handleStyles(true);
+startAutoDarkModeService().catch(print);
+firstRunWelcome().catch(print);
+startBatteryWarningService().catch(print)
+
+const Windows = () => [
+ // forMonitors(DesktopBackground),
+ forMonitors(Crosshair),
+ Overview(),
+ forMonitors(Indicator),
+ forMonitors(Cheatsheet),
+ SideLeft(),
+ SideRight(),
+ forMonitors(Osk),
+ forMonitors(Session),
+ ...(userOptions.dock.enabled ? [forMonitors(Dock)] : []),
+ ...(userOptions.appearance.fakeScreenRounding !== 0 ? [
+ forMonitors((id) => Corner(id, 'top left', true)),
+ forMonitors((id) => Corner(id, 'top right', true)),
+ forMonitors((id) => Corner(id, 'bottom left', true)),
+ forMonitors((id) => Corner(id, 'bottom right', true)),
+ ] : []),
+ ...(userOptions.appearance.barRoundCorners ? [
+ forMonitors(BarCornerTopleft),
+ forMonitors(BarCornerTopright),
+ ] : []),
+];
+
+const CLOSE_ANIM_TIME = 210; // Longer than actual anim time to make sure widgets animate fully
+const closeWindowDelays = {}; // For animations
+for (let i = 0; i < (Gdk.Display.get_default()?.get_n_monitors() || 1); i++) {
+ closeWindowDelays[`osk${i}`] = CLOSE_ANIM_TIME;
+}
+
+App.config({
+ css: `${COMPILED_STYLE_DIR}/style.css`,
+ stackTraceOnError: true,
+ closeWindowDelay: closeWindowDelays,
+ windows: Windows().flat(1),
+});
+
+// Stuff that don't need to be toggled. And they're async so ugh...
+forMonitorsAsync(Bar);
+// Bar().catch(print); // Use this to debug the bar. Single monitor only.
+
diff --git a/modules/home-manager/ags/ags/config_overviewOnly.js b/modules/home-manager/ags/ags/config_overviewOnly.js
new file mode 100644
index 0000000..9c42aaa
--- /dev/null
+++ b/modules/home-manager/ags/ags/config_overviewOnly.js
@@ -0,0 +1,24 @@
+// Want only the overview from my config? this is what you're looking for!
+// Remember to install: `dart-sass`, `ags`, `material-symbols`, and `xorg-xrandr`
+// To launch this, run the following
+// ags -c ~/.config/ags/config_overviewOnly.js
+// To toggle the overview, run:
+// ags -t overview
+// You might wanna add that as a keybind (in hyprland.conf)
+// bind = Super, Tab, exec, ags -t overview
+
+// Import
+import App from 'resource:///com/github/Aylur/ags/app.js'
+// Widgets
+import Overview from './modules/overview/main.js';
+import { COMPILED_STYLE_DIR } from './init.js';
+
+handleStyles(true);
+
+App.config({
+ css: `${COMPILED_STYLE_DIR}/style.css`,
+ stackTraceOnError: true,
+ windows: [
+ Overview(),
+ ],
+});
diff --git a/modules/home-manager/ags/ags/init.js b/modules/home-manager/ags/ags/init.js
new file mode 100644
index 0000000..07c837b
--- /dev/null
+++ b/modules/home-manager/ags/ags/init.js
@@ -0,0 +1,32 @@
+import GLib from 'gi://GLib';
+import App from 'resource:///com/github/Aylur/ags/app.js'
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
+import { darkMode } from './modules/.miscutils/system.js';
+
+export const COMPILED_STYLE_DIR = `${GLib.get_user_cache_dir()}/ags/user/generated`
+
+globalThis['handleStyles'] = (resetMusic) => {
+ // Reset
+ Utils.exec(`mkdir -p "${GLib.get_user_state_dir()}/ags/scss"`);
+ if (resetMusic) {
+ Utils.exec(`bash -c 'echo "" > ${GLib.get_user_state_dir()}/ags/scss/_musicwal.scss'`); // reset music styles
+ Utils.exec(`bash -c 'echo "" > ${GLib.get_user_state_dir()}/ags/scss/_musicmaterial.scss'`); // reset music styles
+ }
+ // Generate overrides
+ let lightdark = darkMode.value ? "dark" : "light";
+ Utils.writeFileSync(
+`@mixin symbolic-icon {
+ -gtk-icon-theme: '${userOptions.icons.symbolicIconTheme[lightdark]}';
+}
+`,
+ `${GLib.get_user_state_dir()}/ags/scss/_lib_mixins_overrides.scss`)
+ // Compile and apply
+ async function applyStyle() {
+ Utils.exec(`mkdir -p ${COMPILED_STYLE_DIR}`);
+ Utils.exec(`sass -I "${GLib.get_user_state_dir()}/ags/scss" -I "${App.configDir}/scss/fallback" "${App.configDir}/scss/main.scss" "${COMPILED_STYLE_DIR}/style.css"`);
+ App.resetCss();
+ App.applyCss(`${COMPILED_STYLE_DIR}/style.css`);
+ console.log('[LOG] Styles loaded')
+ }
+ applyStyle().catch(print);
+}
diff --git a/modules/home-manager/ags/ags/modules/.commondata/hyprlanddata.js b/modules/home-manager/ags/ags/modules/.commondata/hyprlanddata.js
new file mode 100644
index 0000000..7489365
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.commondata/hyprlanddata.js
@@ -0,0 +1,27 @@
+const { Gdk } = imports.gi;
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { execAsync, exec } = Utils;
+
+export let monitors;
+
+// Mixes with Gdk monitor size cuz it reports monitor size scaled
+async function updateStuff() {
+ monitors = JSON.parse(exec('hyprctl monitors -j'))
+ const display = Gdk.Display.get_default();
+ monitors.forEach((monitor, i) => {
+ const gdkMonitor = display.get_monitor(i);
+ monitor.realWidth = monitor.width;
+ monitor.realHeight = monitor.height;
+ if (userOptions.monitors.scaleMethod.toLowerCase == "gdk") {
+ monitor.width = gdkMonitor.get_geometry().width;
+ monitor.height = gdkMonitor.get_geometry().height;
+ }
+ else { // == "division"
+ monitor.width = Math.ceil(monitor.realWidth / monitor.scale);
+ monitor.height = Math.ceil(monitor.realHeight / monitor.scale);
+ }
+ });
+}
+
+updateStuff().catch(print);
+
diff --git a/modules/home-manager/ags/ags/modules/.commondata/quotes.js b/modules/home-manager/ags/ags/modules/.commondata/quotes.js
new file mode 100644
index 0000000..7cea990
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.commondata/quotes.js
@@ -0,0 +1,14 @@
+export const quotes = [
+ {
+ quote: 'Nvidia, fuck you',
+ author: 'Linus Torvalds',
+ },
+ {
+ quote: 'reproducible system? cock and vagina?',
+ author: 'vaxry',
+ },
+ {
+ quote: "haha pointers hee hee i love pointe-\\\nProcess Vaxry exited with signal SIGSEGV",
+ author: 'vaxry',
+ }
+];
diff --git a/modules/home-manager/ags/ags/modules/.commondata/weather.js b/modules/home-manager/ags/ags/modules/.commondata/weather.js
new file mode 100644
index 0000000..57c34fb
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.commondata/weather.js
@@ -0,0 +1,94 @@
+export const WWO_CODE = {
+ "113": "Sunny",
+ "116": "PartlyCloudy",
+ "119": "Cloudy",
+ "122": "VeryCloudy",
+ "143": "Fog",
+ "176": "LightShowers",
+ "179": "LightSleetShowers",
+ "182": "LightSleet",
+ "185": "LightSleet",
+ "200": "ThunderyShowers",
+ "227": "LightSnow",
+ "230": "HeavySnow",
+ "248": "Fog",
+ "260": "Fog",
+ "263": "LightShowers",
+ "266": "LightRain",
+ "281": "LightSleet",
+ "284": "LightSleet",
+ "293": "LightRain",
+ "296": "LightRain",
+ "299": "HeavyShowers",
+ "302": "HeavyRain",
+ "305": "HeavyShowers",
+ "308": "HeavyRain",
+ "311": "LightSleet",
+ "314": "LightSleet",
+ "317": "LightSleet",
+ "320": "LightSnow",
+ "323": "LightSnowShowers",
+ "326": "LightSnowShowers",
+ "329": "HeavySnow",
+ "332": "HeavySnow",
+ "335": "HeavySnowShowers",
+ "338": "HeavySnow",
+ "350": "LightSleet",
+ "353": "LightShowers",
+ "356": "HeavyShowers",
+ "359": "HeavyRain",
+ "362": "LightSleetShowers",
+ "365": "LightSleetShowers",
+ "368": "LightSnowShowers",
+ "371": "HeavySnowShowers",
+ "374": "LightSleetShowers",
+ "377": "LightSleet",
+ "386": "ThunderyShowers",
+ "389": "ThunderyHeavyRain",
+ "392": "ThunderySnowShowers",
+ "395": "HeavySnowShowers",
+}
+
+export const WEATHER_SYMBOL = {
+ "Unknown": "air",
+ "Cloudy": "cloud",
+ "Fog": "foggy",
+ "HeavyRain": "rainy",
+ "HeavyShowers": "rainy",
+ "HeavySnow": "snowing",
+ "HeavySnowShowers": "snowing",
+ "LightRain": "rainy",
+ "LightShowers": "rainy",
+ "LightSleet": "rainy",
+ "LightSleetShowers": "rainy",
+ "LightSnow": "cloudy_snowing",
+ "LightSnowShowers": "cloudy_snowing",
+ "PartlyCloudy": "partly_cloudy_day",
+ "Sunny": "clear_day",
+ "ThunderyHeavyRain": "thunderstorm",
+ "ThunderyShowers": "thunderstorm",
+ "ThunderySnowShowers": "thunderstorm",
+ "VeryCloudy": "cloud",
+}
+
+export const NIGHT_WEATHER_SYMBOL = {
+ "Unknown": "air",
+ "Cloudy": "cloud",
+ "Fog": "foggy",
+ "HeavyRain": "rainy",
+ "HeavyShowers": "rainy",
+ "HeavySnow": "snowing",
+ "HeavySnowShowers": "snowing",
+ "LightRain": "rainy",
+ "LightShowers": "rainy",
+ "LightSleet": "rainy",
+ "LightSleetShowers": "rainy",
+ "LightSnow": "cloudy_snowing",
+ "LightSnowShowers": "cloudy_snowing",
+ "PartlyCloudy": "partly_cloudy_night",
+ "Sunny": "clear_night",
+ "ThunderyHeavyRain": "thunderstorm",
+ "ThunderyShowers": "thunderstorm",
+ "ThunderySnowShowers": "thunderstorm",
+ "VeryCloudy": "cloud",
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/.commonwidgets/cairo_circularprogress.js b/modules/home-manager/ags/ags/modules/.commonwidgets/cairo_circularprogress.js
new file mode 100644
index 0000000..83c1842
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.commonwidgets/cairo_circularprogress.js
@@ -0,0 +1,106 @@
+const { Gtk } = imports.gi;
+const Lang = imports.lang;
+import Widget from 'resource:///com/github/Aylur/ags/widget.js'
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
+
+// -- Styling --
+// min-height for diameter
+// min-width for trough stroke
+// padding for space between trough and progress
+// margin for space between widget and parent
+// background-color for trough color
+// color for progress color
+// -- Usage --
+// font size for progress value (0-100px) (hacky i know, but i want animations)
+export const AnimatedCircProg = ({
+ initFrom = 0,
+ initTo = 0,
+ initAnimTime = 2900,
+ initAnimPoints = 1,
+ extraSetup = () => { },
+ ...rest
+}) => Widget.DrawingArea({
+ ...rest,
+ css: `${initFrom != initTo ? 'font-size: ' + initFrom + 'px; transition: ' + initAnimTime + 'ms linear;' : ''}`,
+ setup: (area) => {
+ const styleContext = area.get_style_context();
+ const width = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
+ const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
+ const padding = styleContext.get_padding(Gtk.StateFlags.NORMAL).left;
+ const marginLeft = styleContext.get_margin(Gtk.StateFlags.NORMAL).left;
+ const marginRight = styleContext.get_margin(Gtk.StateFlags.NORMAL).right;
+ const marginTop = styleContext.get_margin(Gtk.StateFlags.NORMAL).top;
+ const marginBottom = styleContext.get_margin(Gtk.StateFlags.NORMAL).bottom;
+ area.set_size_request(width + marginLeft + marginRight, height + marginTop + marginBottom);
+ area.connect('draw', Lang.bind(area, (area, cr) => {
+ const styleContext = area.get_style_context();
+ const width = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
+ const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
+ const padding = styleContext.get_padding(Gtk.StateFlags.NORMAL).left;
+ const marginLeft = styleContext.get_margin(Gtk.StateFlags.NORMAL).left;
+ const marginRight = styleContext.get_margin(Gtk.StateFlags.NORMAL).right;
+ const marginTop = styleContext.get_margin(Gtk.StateFlags.NORMAL).top;
+ const marginBottom = styleContext.get_margin(Gtk.StateFlags.NORMAL).bottom;
+ area.set_size_request(width + marginLeft + marginRight, height + marginTop + marginBottom);
+
+ const progressValue = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL) / 100.0;
+
+ const bg_stroke = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
+ const fg_stroke = bg_stroke - padding;
+ const radius = Math.min(width, height) / 2.0 - Math.max(bg_stroke, fg_stroke) / 2.0;
+ const center_x = width / 2.0 + marginLeft;
+ const center_y = height / 2.0 + marginTop;
+ const start_angle = -Math.PI / 2.0;
+ const end_angle = start_angle + (2 * Math.PI * progressValue);
+ const start_x = center_x + Math.cos(start_angle) * radius;
+ const start_y = center_y + Math.sin(start_angle) * radius;
+ const end_x = center_x + Math.cos(end_angle) * radius;
+ const end_y = center_y + Math.sin(end_angle) * radius;
+
+ // Draw background
+ const background_color = styleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
+ cr.setSourceRGBA(background_color.red, background_color.green, background_color.blue, background_color.alpha);
+ cr.arc(center_x, center_y, radius, 0, 2 * Math.PI);
+ cr.setLineWidth(bg_stroke);
+ cr.stroke();
+
+ if (progressValue == 0) return;
+
+ // Draw progress
+ const color = styleContext.get_property('color', Gtk.StateFlags.NORMAL);
+ cr.setSourceRGBA(color.red, color.green, color.blue, color.alpha);
+ cr.arc(center_x, center_y, radius, start_angle, end_angle);
+ cr.setLineWidth(fg_stroke);
+ cr.stroke();
+
+ // Draw rounded ends for progress arcs
+ cr.setLineWidth(0);
+ cr.arc(start_x, start_y, fg_stroke / 2, 0, 0 - 0.01);
+ cr.fill();
+ cr.arc(end_x, end_y, fg_stroke / 2, 0, 0 - 0.01);
+ cr.fill();
+ }));
+
+ // Init animation
+ if (initFrom != initTo) {
+ area.css = `font-size: ${initFrom}px; transition: ${initAnimTime}ms linear;`;
+ Utils.timeout(20, () => {
+ area.css = `font-size: ${initTo}px;`;
+ }, area)
+ const transitionDistance = initTo - initFrom;
+ const oneStep = initAnimTime / initAnimPoints;
+ area.css = `
+ font-size: ${initFrom}px;
+ transition: ${oneStep}ms linear;
+ `;
+ for (let i = 0; i < initAnimPoints; i++) {
+ Utils.timeout(Math.max(10, i * oneStep), () => {
+ if(!area) return;
+ area.css = `${initFrom != initTo ? 'font-size: ' + (initFrom + (transitionDistance / initAnimPoints * (i + 1))) + 'px;' : ''}`;
+ });
+ }
+ }
+ else area.css = 'font-size: 0px;';
+ extraSetup(area);
+ },
+})
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/.commonwidgets/cairo_navigationindicator.js b/modules/home-manager/ags/ags/modules/.commonwidgets/cairo_navigationindicator.js
new file mode 100644
index 0000000..fd8d599
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.commonwidgets/cairo_navigationindicator.js
@@ -0,0 +1,71 @@
+const { Gtk } = imports.gi;
+const Lang = imports.lang;
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+
+// min-height/min-width for height/width
+// background-color/color for background/indicator color
+// padding for pad of indicator
+// font-size for selected index (0-based)
+export const NavigationIndicator = ({count, vertical, ...props}) => Widget.DrawingArea({
+ ...props,
+ setup: (area) => {
+ const styleContext = area.get_style_context();
+ const width = Math.max(styleContext.get_property('min-width', Gtk.StateFlags.NORMAL), area.get_allocated_width());
+ const height = Math.max(styleContext.get_property('min-height', Gtk.StateFlags.NORMAL), area.get_allocated_height());
+ area.set_size_request(width, height);
+
+ area.connect('draw', Lang.bind(area, (area, cr) => {
+ const styleContext = area.get_style_context();
+ const width = Math.max(styleContext.get_property('min-width', Gtk.StateFlags.NORMAL), area.get_allocated_width());
+ const height = Math.max(styleContext.get_property('min-height', Gtk.StateFlags.NORMAL), area.get_allocated_height());
+ // console.log('allocated width/height:', area.get_allocated_width(), '/', area.get_allocated_height())
+ area.set_size_request(width, height);
+ const paddingLeft = styleContext.get_padding(Gtk.StateFlags.NORMAL).left;
+ const paddingRight = styleContext.get_padding(Gtk.StateFlags.NORMAL).right;
+ const paddingTop = styleContext.get_padding(Gtk.StateFlags.NORMAL).top;
+ const paddingBottom = styleContext.get_padding(Gtk.StateFlags.NORMAL).bottom;
+
+ const selectedCell = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL);
+
+ let cellWidth = width;
+ let cellHeight = height;
+ if (vertical) cellHeight /= count;
+ else cellWidth /= count;
+ const indicatorWidth = cellWidth - paddingLeft - paddingRight;
+ const indicatorHeight = cellHeight - paddingTop - paddingBottom;
+
+ const background_color = styleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
+ const color = styleContext.get_property('color', Gtk.StateFlags.NORMAL);
+ cr.setLineWidth(2);
+ // Background
+ cr.setSourceRGBA(background_color.red, background_color.green, background_color.blue, background_color.alpha);
+ cr.rectangle(0, 0, width, height);
+ cr.fill();
+
+ // The indicator line
+ cr.setSourceRGBA(color.red, color.green, color.blue, color.alpha);
+ if (vertical) {
+ cr.rectangle(paddingLeft, paddingTop + cellHeight * selectedCell + indicatorWidth / 2, indicatorWidth, indicatorHeight - indicatorWidth);
+ cr.stroke();
+ cr.rectangle(paddingLeft, paddingTop + cellHeight * selectedCell + indicatorWidth / 2, indicatorWidth, indicatorHeight - indicatorWidth);
+ cr.fill();
+ cr.arc(paddingLeft + indicatorWidth / 2, paddingTop + cellHeight * selectedCell + indicatorWidth / 2, indicatorWidth / 2, Math.PI, 2 * Math.PI);
+ cr.fill();
+ cr.arc(paddingLeft + indicatorWidth / 2, paddingTop + cellHeight * selectedCell + indicatorHeight - indicatorWidth / 2, indicatorWidth / 2, 0, Math.PI);
+ cr.fill();
+ }
+ else {
+ cr.rectangle(paddingLeft + cellWidth * selectedCell + indicatorHeight / 2, paddingTop, indicatorWidth - indicatorHeight, indicatorHeight);
+ cr.stroke();
+ cr.rectangle(paddingLeft + cellWidth * selectedCell + indicatorHeight / 2, paddingTop, indicatorWidth - indicatorHeight, indicatorHeight);
+ cr.fill();
+ cr.arc(paddingLeft + cellWidth * selectedCell + indicatorHeight / 2, paddingTop + indicatorHeight / 2, indicatorHeight / 2, 0.5 * Math.PI, 1.5 * Math.PI);
+ cr.fill();
+ cr.arc(paddingLeft + cellWidth * selectedCell + indicatorWidth - indicatorHeight / 2, paddingTop + indicatorHeight / 2, indicatorHeight / 2, -0.5 * Math.PI, 0.5 * Math.PI);
+ cr.fill();
+ }
+ }))
+ },
+})
+
+
diff --git a/modules/home-manager/ags/ags/modules/.commonwidgets/cairo_roundedcorner.js b/modules/home-manager/ags/ags/modules/.commonwidgets/cairo_roundedcorner.js
new file mode 100644
index 0000000..90c1cf0
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.commonwidgets/cairo_roundedcorner.js
@@ -0,0 +1,50 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+const { Gtk } = imports.gi;
+const Lang = imports.lang;
+
+export const RoundedCorner = (place, props) => Widget.DrawingArea({
+ ...props,
+ hpack: place.includes('left') ? 'start' : 'end',
+ vpack: place.includes('top') ? 'start' : 'end',
+ setup: (widget) => Utils.timeout(1, () => {
+ const c = widget.get_style_context().get_property('background-color', Gtk.StateFlags.NORMAL);
+ const r = widget.get_style_context().get_property('border-radius', Gtk.StateFlags.NORMAL);
+ widget.set_size_request(r, r);
+ widget.connect('draw', Lang.bind(widget, (widget, cr) => {
+ const c = widget.get_style_context().get_property('background-color', Gtk.StateFlags.NORMAL);
+ const r = widget.get_style_context().get_property('border-radius', Gtk.StateFlags.NORMAL);
+ // const borderColor = widget.get_style_context().get_property('color', Gtk.StateFlags.NORMAL);
+ // const borderWidth = widget.get_style_context().get_border(Gtk.StateFlags.NORMAL).left; // ur going to write border-width: something anyway
+ widget.set_size_request(r, r);
+
+ switch (place) {
+ case 'topleft':
+ cr.arc(r, r, r, Math.PI, 3 * Math.PI / 2);
+ cr.lineTo(0, 0);
+ break;
+
+ case 'topright':
+ cr.arc(0, r, r, 3 * Math.PI / 2, 2 * Math.PI);
+ cr.lineTo(r, 0);
+ break;
+
+ case 'bottomleft':
+ cr.arc(r, 0, r, Math.PI / 2, Math.PI);
+ cr.lineTo(0, r);
+ break;
+
+ case 'bottomright':
+ cr.arc(0, 0, r, 0, Math.PI / 2);
+ cr.lineTo(r, r);
+ break;
+ }
+
+ cr.closePath();
+ cr.setSourceRGBA(c.red, c.green, c.blue, c.alpha);
+ cr.fill();
+ // cr.setLineWidth(borderWidth);
+ // cr.setSourceRGBA(borderColor.red, borderColor.green, borderColor.blue, borderColor.alpha);
+ // cr.stroke();
+ }));
+ }),
+});
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/.commonwidgets/cairo_slider.js b/modules/home-manager/ags/ags/modules/.commonwidgets/cairo_slider.js
new file mode 100644
index 0000000..eff9085
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.commonwidgets/cairo_slider.js
@@ -0,0 +1,49 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+const { Gtk } = imports.gi;
+const Lang = imports.lang;
+
+export const AnimatedSlider = ({
+ className,
+ value,
+ ...rest
+}) => {
+ return Widget.DrawingArea({
+ className: `${className}`,
+ setup: (self) => {
+ self.connect('draw', Lang.bind(self, (self, cr) => {
+ const styleContext = self.get_style_context();
+ const allocatedWidth = self.get_allocated_width();
+ const allocatedHeight = self.get_allocated_height();
+ console.log(allocatedHeight, allocatedWidth)
+ const minWidth = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
+ const minHeight = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
+ const radius = styleContext.get_property('border-radius', Gtk.StateFlags.NORMAL);
+ const bg = styleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
+ const fg = styleContext.get_property('color', Gtk.StateFlags.NORMAL);
+ const value = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL) / 100;
+ self.set_size_request(-1, minHeight);
+ const width = allocatedHeight;
+ const height = minHeight;
+
+ cr.arc(radius, radius, radius, -1 * Math.PI, -0.5 * Math.PI); // Top-left
+ cr.arc(width - radius, radius, radius, -0.5 * Math.PI, 0); // Top-right
+ cr.arc(width - radius, height - radius, radius, 0, 0.5 * Math.PI); // Bottom-left
+ cr.arc(radius, height - radius, radius, 0.5 * Math.PI, 1 * Math.PI); // Bottom-right
+ cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
+ cr.closePath();
+ cr.fill();
+
+ // const valueWidth = width * value;
+ // cr.arc(radius, radius, radius, -1 * Math.PI, -0.5 * Math.PI); // Top-left
+ // cr.arc(valueWidth - radius, radius, radius, -0.5 * Math.PI, 0); // Top-right
+ // cr.arc(valueWidth - radius, height - radius, radius, 0, 0.5 * Math.PI); // Bottom-left
+ // cr.arc(radius, height - radius, radius, 0.5 * Math.PI, 1 * Math.PI); // Bottom-right
+ // cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha);
+ // cr.closePath();
+ // cr.fill();
+
+ }));
+ },
+ ...rest,
+ })
+}
diff --git a/modules/home-manager/ags/ags/modules/.commonwidgets/clickcloseregion.js b/modules/home-manager/ags/ags/modules/.commonwidgets/clickcloseregion.js
new file mode 100644
index 0000000..e0aae79
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.commonwidgets/clickcloseregion.js
@@ -0,0 +1,23 @@
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import { monitors } from '../.commondata/hyprlanddata.js';
+const { Box, EventBox } = Widget;
+
+export const clickCloseRegion = ({ name, multimonitor = true, monitor = 0, expand = true, fillMonitor = '' }) => {
+ return EventBox({
+ child: Box({
+ expand: expand,
+ css: `
+ min-width: ${fillMonitor.includes('h') ? monitors[monitor].width : 0}px;
+ min-height: ${fillMonitor.includes('v') ? monitors[monitor].height : 0}px;
+ `,
+ }),
+ setup: (self) => self.on('button-press-event', (self, event) => { // Any mouse button
+ if (multimonitor) closeWindowOnAllMonitors(name);
+ else App.closeWindow(name);
+ }),
+ })
+}
+
+export default clickCloseRegion;
+
diff --git a/modules/home-manager/ags/ags/modules/.commonwidgets/configwidgets.js b/modules/home-manager/ags/ags/modules/.commonwidgets/configwidgets.js
new file mode 100644
index 0000000..e98f5ab
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.commonwidgets/configwidgets.js
@@ -0,0 +1,219 @@
+import Variable from 'resource:///com/github/Aylur/ags/variable.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+import { MaterialIcon } from './materialicon.js';
+import { setupCursorHover } from '../.widgetutils/cursorhover.js';
+const { Box, Button, Label, Revealer, SpinButton } = Widget;
+
+export const ConfigToggle = ({
+ icon, name, desc = '', initValue,
+ expandWidget = true,
+ onChange = () => { }, extraSetup = () => { },
+ ...rest
+}) => {
+ const enabled = Variable(initValue);
+ const toggleIcon = Label({
+ className: `icon-material txt-bold ${enabled.value ? '' : 'txt-poof'}`,
+ label: `${enabled.value ? 'check' : ''}`,
+ setup: (self) => self.hook(enabled, (self) => {
+ self.toggleClassName('switch-fg-toggling-false', false);
+ if (!enabled.value) {
+ self.label = '';
+ self.toggleClassName('txt-poof', true);
+ }
+ else Utils.timeout(1, () => {
+ toggleIcon.label = 'check';
+ toggleIcon.toggleClassName('txt-poof', false);
+ })
+ }),
+ })
+ const toggleButtonIndicator = Box({
+ className: `switch-fg ${enabled.value ? 'switch-fg-true' : ''}`,
+ vpack: 'center',
+ hpack: 'start',
+ homogeneous: true,
+ children: [toggleIcon,],
+ setup: (self) => self.hook(enabled, (self) => {
+ self.toggleClassName('switch-fg-true', enabled.value);
+ }),
+ });
+ const toggleButton = Box({
+ hpack: 'end',
+ className: `switch-bg ${enabled.value ? 'switch-bg-true' : ''}`,
+ homogeneous: true,
+ children: [toggleButtonIndicator],
+ setup: (self) => self.hook(enabled, (self) => {
+ self.toggleClassName('switch-bg-true', enabled.value);
+ }),
+ });
+ const widgetContent = Box({
+ tooltipText: desc,
+ className: 'txt spacing-h-5 configtoggle-box',
+ children: [
+ ...(icon !== undefined ? [MaterialIcon(icon, 'norm')] : []),
+ ...(name !== undefined ? [Label({
+ className: 'txt txt-small',
+ label: name,
+ })] : []),
+ ...(expandWidget ? [Box({ hexpand: true })] : []),
+ toggleButton,
+ ]
+ });
+ const interactionWrapper = Button({
+ attribute: {
+ enabled: enabled,
+ toggle: (newValue) => {
+ enabled.value = !enabled.value;
+ onChange(interactionWrapper, enabled.value);
+ }
+ },
+ child: widgetContent,
+ onClicked: (self) => self.attribute.toggle(self),
+ setup: (self) => {
+ setupCursorHover(self);
+ self.connect('pressed', () => { // mouse down
+ toggleIcon.toggleClassName('txt-poof', true);
+ toggleIcon.toggleClassName('switch-fg-true', false);
+ if (!enabled.value) toggleIcon.toggleClassName('switch-fg-toggling-false', true);
+ });
+ extraSetup(self)
+ },
+ ...rest,
+ });
+ interactionWrapper.enabled = enabled;
+ return interactionWrapper;
+}
+
+export const ConfigSegmentedSelection = ({
+ icon, name, desc = '',
+ options = [{ name: 'Option 1', value: 0 }, { name: 'Option 2', value: 1 }],
+ initIndex = 0,
+ onChange,
+ ...rest
+}) => {
+ let lastSelected = initIndex;
+ let value = options[initIndex].value;
+ const widget = Box({
+ tooltipText: desc,
+ className: 'segment-container',
+ // homogeneous: true,
+ children: options.map((option, id) => {
+ const selectedIcon = Revealer({
+ revealChild: id == initIndex,
+ transition: 'slide_right',
+ transitionDuration: userOptions.animations.durationSmall,
+ child: MaterialIcon('check', 'norm')
+ });
+ return Button({
+ setup: setupCursorHover,
+ className: `segment-btn ${id == initIndex ? 'segment-btn-enabled' : ''}`,
+ child: Box({
+ hpack: 'center',
+ className: 'spacing-h-5',
+ children: [
+ selectedIcon,
+ Label({
+ label: option.name,
+ })
+ ]
+ }),
+ onClicked: (self) => {
+ value = option.value;
+ const kids = widget.get_children();
+ kids[lastSelected].toggleClassName('segment-btn-enabled', false);
+ kids[lastSelected].get_children()[0].get_children()[0].revealChild = false;
+ lastSelected = id;
+ self.toggleClassName('segment-btn-enabled', true);
+ selectedIcon.revealChild = true;
+ onChange(option.value, option.name);
+ }
+ })
+ }),
+ ...rest,
+ });
+ return widget;
+
+}
+
+export const ConfigMulipleSelection = ({
+ icon, name, desc = '',
+ optionsArr = [
+ [{ name: 'Option 1', value: 0 }, { name: 'Option 2', value: 1 }],
+ [{ name: 'Option 3', value: 0 }, { name: 'Option 4', value: 1 }],
+ ],
+ initIndex = [0, 0],
+ onChange,
+ ...rest
+}) => {
+ let lastSelected = initIndex;
+ const widget = Box({
+ tooltipText: desc,
+ className: 'multipleselection-container spacing-v-3',
+ vertical: true,
+ children: optionsArr.map((options, grp) => Box({
+ className: 'spacing-h-5',
+ hpack: 'center',
+ children: options.map((option, id) => Button({
+ setup: setupCursorHover,
+ className: `multipleselection-btn ${id == initIndex[1] && grp == initIndex[0] ? 'multipleselection-btn-enabled' : ''}`,
+ label: option.name,
+ onClicked: (self) => {
+ const kidsg = widget.get_children();
+ const kids = kidsg.flatMap(widget => widget.get_children());
+ kids.forEach(kid => {
+ kid.toggleClassName('multipleselection-btn-enabled', false);
+ });
+ lastSelected = id;
+ self.toggleClassName('multipleselection-btn-enabled', true);
+ onChange(option.value, option.name);
+ }
+ })),
+ })),
+ ...rest,
+ });
+ return widget;
+
+}
+
+export const ConfigGap = ({ vertical = true, size = 5, ...rest }) => Box({
+ className: `gap-${vertical ? 'v' : 'h'}-${size}`,
+ ...rest,
+})
+
+export const ConfigSpinButton = ({
+ icon, name, desc = '', initValue,
+ minValue = 0, maxValue = 100, step = 1,
+ expandWidget = true,
+ onChange = () => { }, extraSetup = () => { },
+ ...rest
+}) => {
+ const value = Variable(initValue);
+ const spinButton = SpinButton({
+ className: 'spinbutton',
+ range: [minValue, maxValue],
+ increments: [step, step],
+ onValueChanged: ({ value: newValue }) => {
+ value.value = newValue;
+ onChange(spinButton, newValue);
+ },
+ });
+ spinButton.value = value.value;
+ const widgetContent = Box({
+ tooltipText: desc,
+ className: 'txt spacing-h-5 configtoggle-box',
+ children: [
+ ...(icon !== undefined ? [MaterialIcon(icon, 'norm')] : []),
+ ...(name !== undefined ? [Label({
+ className: 'txt txt-small',
+ label: name,
+ })] : []),
+ ...(expandWidget ? [Box({ hexpand: true })] : []),
+ spinButton,
+ ],
+ setup: (self) => {
+ extraSetup(self);
+ },
+ ...rest,
+ });
+ return widgetContent;
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/.commonwidgets/materialicon.js b/modules/home-manager/ags/ags/modules/.commonwidgets/materialicon.js
new file mode 100644
index 0000000..029f0ed
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.commonwidgets/materialicon.js
@@ -0,0 +1,7 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+
+export const MaterialIcon = (icon, size, props = {}) => Widget.Label({
+ className: `icon-material txt-${size}`,
+ label: icon,
+ ...props,
+})
diff --git a/modules/home-manager/ags/ags/modules/.commonwidgets/notification.js b/modules/home-manager/ags/ags/modules/.commonwidgets/notification.js
new file mode 100644
index 0000000..57840ff
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.commonwidgets/notification.js
@@ -0,0 +1,462 @@
+// This file is for the actual widget for each single notification
+const { GLib, Gdk, Gtk } = imports.gi;
+import Widget from 'resource:///com/github/Aylur/ags/widget.js'
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
+const { Box, EventBox, Icon, Overlay, Label, Button, Revealer } = Widget;
+import { MaterialIcon } from './materialicon.js';
+import { setupCursorHover } from "../.widgetutils/cursorhover.js";
+import { AnimatedCircProg } from "./cairo_circularprogress.js";
+
+function guessMessageType(summary) {
+ const str = summary.toLowerCase();
+ if (str.includes('reboot')) return 'restart_alt';
+ if (str.includes('recording')) return 'screen_record';
+ if (str.includes('battery') || summary.includes('power')) return 'power';
+ if (str.includes('screenshot')) return 'screenshot_monitor';
+ if (str.includes('welcome')) return 'waving_hand';
+ if (str.includes('time')) return 'scheduleb';
+ if (str.includes('installed')) return 'download';
+ if (str.includes('update')) return 'update';
+ if (str.startsWith('file')) return 'folder_copy';
+ return 'chat';
+}
+
+function exists(widget) {
+ return widget !== null;
+}
+
+const getFriendlyNotifTimeString = (timeObject) => {
+ const messageTime = GLib.DateTime.new_from_unix_local(timeObject);
+ const oneMinuteAgo = GLib.DateTime.new_now_local().add_seconds(-60);
+ if (messageTime.compare(oneMinuteAgo) > 0)
+ return 'Now';
+ else if (messageTime.get_day_of_year() == GLib.DateTime.new_now_local().get_day_of_year())
+ return messageTime.format(userOptions.time.format);
+ else if (messageTime.get_day_of_year() == GLib.DateTime.new_now_local().get_day_of_year() - 1)
+ return 'Yesterday';
+ else
+ return messageTime.format(userOptions.time.dateFormat);
+}
+
+const NotificationIcon = (notifObject) => {
+ // { appEntry, appIcon, image }, urgency = 'normal'
+ if (notifObject.image) {
+ return Box({
+ valign: Gtk.Align.CENTER,
+ hexpand: false,
+ className: 'notif-icon',
+ css: `
+ background-image: url("${notifObject.image}");
+ background-size: auto 100%;
+ background-repeat: no-repeat;
+ background-position: center;
+ `,
+ });
+ }
+
+ let icon = 'NO_ICON';
+ if (Utils.lookUpIcon(notifObject.appIcon))
+ icon = notifObject.appIcon;
+ if (Utils.lookUpIcon(notifObject.appEntry))
+ icon = notifObject.appEntry;
+
+ return Box({
+ vpack: 'center',
+ hexpand: false,
+ className: `notif-icon notif-icon-material-${notifObject.urgency}`,
+ homogeneous: true,
+ children: [
+ (icon != 'NO_ICON' ?
+ Icon({
+ vpack: 'center',
+ icon: icon,
+ })
+ :
+ MaterialIcon(`${notifObject.urgency == 'critical' ? 'release_alert' : guessMessageType(notifObject.summary.toLowerCase())}`, 'hugerass', {
+ hexpand: true,
+ })
+ )
+ ],
+ });
+};
+
+export default ({
+ notifObject,
+ isPopup = false,
+ props = {},
+} = {}) => {
+ const popupTimeout = notifObject.timeout || (notifObject.urgency == 'critical' ? 8000 : 3000);
+ const command = (isPopup ?
+ () => notifObject.dismiss() :
+ () => notifObject.close()
+ )
+ const destroyWithAnims = () => {
+ widget.sensitive = false;
+ notificationBox.setCss(middleClickClose);
+ Utils.timeout(userOptions.animations.durationSmall, () => {
+ if (wholeThing) wholeThing.revealChild = false;
+ }, wholeThing);
+ Utils.timeout(userOptions.animations.durationSmall * 2, () => {
+ command();
+ if (wholeThing) {
+ wholeThing.destroy();
+ wholeThing = null;
+ }
+ }, wholeThing);
+ }
+ const widget = EventBox({
+ onHover: (self) => {
+ self.window.set_cursor(Gdk.Cursor.new_from_name(display, 'grab'));
+ if (!wholeThing.attribute.hovered)
+ wholeThing.attribute.hovered = true;
+ },
+ onHoverLost: (self) => {
+ self.window.set_cursor(null);
+ if (wholeThing.attribute.hovered)
+ wholeThing.attribute.hovered = false;
+ if (isPopup) {
+ command();
+ }
+ },
+ onMiddleClick: (self) => {
+ destroyWithAnims();
+ },
+ setup: (self) => {
+ self.on("button-press-event", () => {
+ wholeThing.attribute.held = true;
+ notificationContent.toggleClassName(`${isPopup ? 'popup-' : ''}notif-clicked-${notifObject.urgency}`, true);
+ Utils.timeout(800, () => {
+ if (wholeThing?.attribute.held) {
+ Utils.execAsync(['wl-copy', `${notifObject.body}`]).catch(print);
+ notifTextSummary.label = notifObject.summary + " (copied)";
+ Utils.timeout(3000, () => notifTextSummary.label = notifObject.summary)
+ }
+ })
+ }).on("button-release-event", () => {
+ wholeThing.attribute.held = false;
+ notificationContent.toggleClassName(`${isPopup ? 'popup-' : ''}notif-clicked-${notifObject.urgency}`, false);
+ })
+ }
+ });
+ let wholeThing = Revealer({
+ attribute: {
+ 'close': undefined,
+ 'destroyWithAnims': destroyWithAnims,
+ 'dragging': false,
+ 'held': false,
+ 'hovered': false,
+ 'id': notifObject.id,
+ },
+ revealChild: false,
+ transition: 'slide_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Box({ // Box to make sure css-based spacing works
+ homogeneous: true,
+ }),
+ });
+
+ const display = Gdk.Display.get_default();
+ const notifTextPreview = Revealer({
+ transition: 'slide_down',
+ transitionDuration: userOptions.animations.durationSmall,
+ revealChild: true,
+ child: Label({
+ xalign: 0,
+ className: `txt-smallie notif-body-${notifObject.urgency}`,
+ useMarkup: true,
+ xalign: 0,
+ justify: Gtk.Justification.LEFT,
+ maxWidthChars: 1,
+ truncate: 'end',
+ label: notifObject.body.split("\n")[0],
+ }),
+ });
+ const notifTextExpanded = Revealer({
+ transition: 'slide_up',
+ transitionDuration: userOptions.animations.durationSmall,
+ revealChild: false,
+ child: Box({
+ vertical: true,
+ className: 'spacing-v-10',
+ children: [
+ Label({
+ xalign: 0,
+ className: `txt-smallie notif-body-${notifObject.urgency}`,
+ useMarkup: true,
+ xalign: 0,
+ justify: Gtk.Justification.LEFT,
+ maxWidthChars: 1,
+ wrap: true,
+ label: notifObject.body,
+ }),
+ Box({
+ className: 'notif-actions spacing-h-5',
+ children: [
+ Button({
+ hexpand: true,
+ className: `notif-action notif-action-${notifObject.urgency}`,
+ onClicked: () => destroyWithAnims(),
+ setup: setupCursorHover,
+ child: Label({
+ label: 'Close',
+ }),
+ }),
+ ...notifObject.actions.map(action => Widget.Button({
+ hexpand: true,
+ className: `notif-action notif-action-${notifObject.urgency}`,
+ onClicked: () => notifObject.invoke(action.id),
+ setup: setupCursorHover,
+ child: Label({
+ label: action.label,
+ }),
+ }))
+ ],
+ })
+ ]
+ }),
+ });
+ const notifIcon = Box({
+ vpack: 'start',
+ homogeneous: true,
+ children: [
+ Overlay({
+ child: NotificationIcon(notifObject),
+ overlays: isPopup ? [AnimatedCircProg({
+ className: `notif-circprog-${notifObject.urgency}`,
+ vpack: 'center', hpack: 'center',
+ initFrom: (isPopup ? 100 : 0),
+ initTo: 0,
+ initAnimTime: popupTimeout,
+ })] : [],
+ }),
+ ]
+ });
+
+ const notifTextSummary = Label({
+ xalign: 0,
+ className: 'txt-small txt-semibold titlefont',
+ justify: Gtk.Justification.LEFT,
+ hexpand: true,
+ maxWidthChars: 1,
+ truncate: 'end',
+ ellipsize: 3,
+ useMarkup: notifObject.summary.startsWith('<'),
+ label: notifObject.summary,
+ });
+ const initTimeString = getFriendlyNotifTimeString(notifObject.time);
+ const notifTextBody = Label({
+ vpack: 'center',
+ justification: 'right',
+ className: 'txt-smaller txt-semibold',
+ label: initTimeString,
+ setup: initTimeString == 'Now' ? (self) => {
+ let id = Utils.timeout(60000, () => {
+ self.label = getFriendlyNotifTimeString(notifObject.time);
+ id = null;
+ });
+ self.connect('destroy', () => { if (id) GLib.source_remove(id) });
+ } : () => { },
+ });
+ const notifText = Box({
+ valign: Gtk.Align.CENTER,
+ vertical: true,
+ hexpand: true,
+ children: [
+ Box({
+ children: [
+ notifTextSummary,
+ notifTextBody,
+ ]
+ }),
+ notifTextPreview,
+ notifTextExpanded,
+ ]
+ });
+ const notifExpandButton = Button({
+ vpack: 'start',
+ className: 'notif-expand-btn',
+ onClicked: (self) => {
+ if (notifTextPreview.revealChild) { // Expanding...
+ notifTextPreview.revealChild = false;
+ notifTextExpanded.revealChild = true;
+ self.child.label = 'expand_less';
+ expanded = true;
+ }
+ else {
+ notifTextPreview.revealChild = true;
+ notifTextExpanded.revealChild = false;
+ self.child.label = 'expand_more';
+ expanded = false;
+ }
+ },
+ child: MaterialIcon('expand_more', 'norm', {
+ vpack: 'center',
+ }),
+ setup: setupCursorHover,
+ });
+ const notificationContent = Box({
+ ...props,
+ className: `${isPopup ? 'popup-' : ''}notif-${notifObject.urgency} spacing-h-10`,
+ children: [
+ notifIcon,
+ Box({
+ className: 'spacing-h-5',
+ children: [
+ notifText,
+ notifExpandButton,
+ ]
+ })
+ ]
+ })
+
+ // Gesture stuff
+ const gesture = Gtk.GestureDrag.new(widget);
+ var initDirX = 0;
+ var initDirVertical = -1; // -1: unset, 0: horizontal, 1: vertical
+ var expanded = false;
+ // in px
+ const startMargin = 0;
+ const MOVE_THRESHOLD = 10;
+ const DRAG_CONFIRM_THRESHOLD = 100;
+ // in rem
+ const maxOffset = 10.227;
+ const endMargin = 20.455;
+ const disappearHeight = 6.818;
+ const leftAnim1 = `transition: ${userOptions.animations.durationSmall}ms cubic-bezier(0.05, 0.7, 0.1, 1);
+ margin-left: -${Number(maxOffset + endMargin)}rem;
+ margin-right: ${Number(maxOffset + endMargin)}rem;
+ opacity: 0;`;
+
+ const rightAnim1 = `transition: ${userOptions.animations.durationSmall}ms cubic-bezier(0.05, 0.7, 0.1, 1);
+ margin-left: ${Number(maxOffset + endMargin)}rem;
+ margin-right: -${Number(maxOffset + endMargin)}rem;
+ opacity: 0;`;
+
+ const middleClickClose = `transition: ${userOptions.animations.durationSmall}ms cubic-bezier(0.85, 0, 0.15, 1);
+ margin-left: ${Number(maxOffset + endMargin)}rem;
+ margin-right: -${Number(maxOffset + endMargin)}rem;
+ opacity: 0;`;
+
+ const notificationBox = Box({
+ attribute: {
+ 'leftAnim1': leftAnim1,
+ 'rightAnim1': rightAnim1,
+ 'middleClickClose': middleClickClose,
+ 'ready': false,
+ },
+ homogeneous: true,
+ children: [notificationContent],
+ setup: (self) => self
+ .hook(gesture, self => {
+ var offset_x = gesture.get_offset()[1];
+ var offset_y = gesture.get_offset()[2];
+ // Which dir?
+ if (initDirVertical == -1) {
+ if (Math.abs(offset_y) > MOVE_THRESHOLD)
+ initDirVertical = 1;
+ if (initDirX == 0 && Math.abs(offset_x) > MOVE_THRESHOLD) {
+ initDirVertical = 0;
+ initDirX = (offset_x > 0 ? 1 : -1);
+ }
+ }
+ // Horizontal drag
+ if (initDirVertical == 0 && offset_x > MOVE_THRESHOLD) {
+ if (initDirX < 0)
+ self.setCss(`margin-left: 0px; margin-right: 0px;`);
+ else
+ self.setCss(`
+ margin-left: ${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
+ margin-right: -${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
+ `);
+ }
+ else if (initDirVertical == 0 && offset_x < -MOVE_THRESHOLD) {
+ if (initDirX > 0)
+ self.setCss(`margin-left: 0px; margin-right: 0px;`);
+ else {
+ offset_x = Math.abs(offset_x);
+ self.setCss(`
+ margin-right: ${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
+ margin-left: -${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
+ `);
+ }
+ }
+ // Update dragging
+ wholeThing.attribute.dragging = Math.abs(offset_x) > MOVE_THRESHOLD;
+ if (Math.abs(offset_x) > MOVE_THRESHOLD ||
+ Math.abs(offset_y) > MOVE_THRESHOLD) wholeThing.attribute.held = false;
+ widget.window?.set_cursor(Gdk.Cursor.new_from_name(display, 'grabbing'));
+ // Vertical drag
+ if (initDirVertical == 1 && offset_y > MOVE_THRESHOLD && !expanded) {
+ notifTextPreview.revealChild = false;
+ notifTextExpanded.revealChild = true;
+ expanded = true;
+ notifExpandButton.child.label = 'expand_less';
+ }
+ else if (initDirVertical == 1 && offset_y < -MOVE_THRESHOLD && expanded) {
+ notifTextPreview.revealChild = true;
+ notifTextExpanded.revealChild = false;
+ expanded = false;
+ notifExpandButton.child.label = 'expand_more';
+ }
+
+ }, 'drag-update')
+ .hook(gesture, self => {
+ if (!self.attribute.ready) {
+ wholeThing.revealChild = true;
+ self.attribute.ready = true;
+ return;
+ }
+ const offset_h = gesture.get_offset()[1];
+
+ if (Math.abs(offset_h) > DRAG_CONFIRM_THRESHOLD && offset_h * initDirX > 0) {
+ if (offset_h > 0) {
+ self.setCss(rightAnim1);
+ widget.sensitive = false;
+ }
+ else {
+ self.setCss(leftAnim1);
+ widget.sensitive = false;
+ }
+ Utils.timeout(userOptions.animations.durationSmall, () => {
+ if (wholeThing) wholeThing.revealChild = false;
+ }, wholeThing);
+ Utils.timeout(userOptions.animations.durationSmall * 2, () => {
+ command();
+ if (wholeThing) {
+ wholeThing.destroy();
+ wholeThing = null;
+ }
+ }, wholeThing);
+ }
+ else {
+ self.setCss(`transition: margin 200ms cubic-bezier(0.05, 0.7, 0.1, 1), opacity 200ms cubic-bezier(0.05, 0.7, 0.1, 1);
+ margin-left: ${startMargin}px;
+ margin-right: ${startMargin}px;
+ margin-bottom: unset; margin-top: unset;
+ opacity: 1;`);
+ if (widget.window)
+ widget.window.set_cursor(Gdk.Cursor.new_from_name(display, 'grab'));
+
+ wholeThing.attribute.dragging = false;
+ }
+ initDirX = 0;
+ initDirVertical = -1;
+ }, 'drag-end')
+ ,
+ })
+ widget.add(notificationBox);
+ wholeThing.child.children = [widget];
+ if (isPopup) Utils.timeout(popupTimeout, () => {
+ if (wholeThing) {
+ wholeThing.revealChild = false;
+ Utils.timeout(userOptions.animations.durationSmall, () => {
+ if (wholeThing) {
+ wholeThing.destroy();
+ wholeThing = null;
+ }
+ command();
+ }, wholeThing);
+ }
+ })
+ return wholeThing;
+}
diff --git a/modules/home-manager/ags/ags/modules/.commonwidgets/statusicons.js b/modules/home-manager/ags/ags/modules/.commonwidgets/statusicons.js
new file mode 100644
index 0000000..eacc444
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.commonwidgets/statusicons.js
@@ -0,0 +1,306 @@
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+
+import { MaterialIcon } from './materialicon.js';
+import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
+import Network from 'resource:///com/github/Aylur/ags/service/network.js';
+import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
+import { languages } from './statusicons_languages.js';
+
+// A guessing func to try to support langs not listed in data/languages.js
+function isLanguageMatch(abbreviation, word) {
+ const lowerAbbreviation = abbreviation.toLowerCase();
+ const lowerWord = word.toLowerCase();
+ let j = 0;
+ for (let i = 0; i < lowerWord.length; i++) {
+ if (lowerWord[i] === lowerAbbreviation[j]) {
+ j++;
+ }
+ if (j === lowerAbbreviation.length) {
+ return true;
+ }
+ }
+ return false;
+}
+
+export const MicMuteIndicator = () => Widget.Revealer({
+ transition: 'slide_left',
+ transitionDuration: userOptions.animations.durationSmall,
+ revealChild: false,
+ setup: (self) => self.hook(Audio, (self) => {
+ self.revealChild = Audio.microphone?.stream?.isMuted;
+ }),
+ child: MaterialIcon('mic_off', 'norm'),
+});
+
+export const NotificationIndicator = (notifCenterName = 'sideright') => {
+ const widget = Widget.Revealer({
+ transition: 'slide_left',
+ transitionDuration: userOptions.animations.durationSmall,
+ revealChild: false,
+ setup: (self) => self
+ .hook(Notifications, (self, id) => {
+ if (!id || Notifications.dnd) return;
+ if (!Notifications.getNotification(id)) return;
+ self.revealChild = true;
+ }, 'notified')
+ .hook(App, (self, currentName, visible) => {
+ if (visible && currentName === notifCenterName) {
+ self.revealChild = false;
+ }
+ })
+ ,
+ child: Widget.Box({
+ children: [
+ MaterialIcon('notifications', 'norm'),
+ Widget.Label({
+ className: 'txt-small titlefont',
+ attribute: {
+ unreadCount: 0,
+ update: (self) => self.label = `${self.attribute.unreadCount}`,
+ },
+ setup: (self) => self
+ .hook(Notifications, (self, id) => {
+ if (!id || Notifications.dnd) return;
+ if (!Notifications.getNotification(id)) return;
+ self.attribute.unreadCount++;
+ self.attribute.update(self);
+ }, 'notified')
+ .hook(App, (self, currentName, visible) => {
+ if (visible && currentName === notifCenterName) {
+ self.attribute.unreadCount = 0;
+ self.attribute.update(self);
+ }
+ })
+ ,
+ })
+ ]
+ })
+ });
+ return widget;
+}
+
+export const BluetoothIndicator = () => Widget.Stack({
+ transition: 'slide_up_down',
+ transitionDuration: userOptions.animations.durationSmall,
+ children: {
+ 'false': Widget.Label({ className: 'txt-norm icon-material', label: 'bluetooth_disabled' }),
+ 'true': Widget.Label({ className: 'txt-norm icon-material', label: 'bluetooth' }),
+ },
+ setup: (self) => self
+ .hook(Bluetooth, stack => {
+ stack.shown = String(Bluetooth.enabled);
+ })
+ ,
+});
+
+const BluetoothDevices = () => Widget.Box({
+ className: 'spacing-h-5',
+ setup: self => self.hook(Bluetooth, self => {
+ self.children = Bluetooth.connected_devices.map((device) => {
+ return Widget.Box({
+ className: 'bar-bluetooth-device spacing-h-5',
+ vpack: 'center',
+ tooltipText: device.name,
+ children: [
+ Widget.Icon(`${device.iconName}-symbolic`),
+ ...(device.batteryPercentage ? [Widget.Label({
+ className: 'txt-smallie',
+ label: `${device.batteryPercentage}`,
+ setup: (self) => {
+ self.hook(device, (self) => {
+ self.label = `${device.batteryPercentage}`;
+ }, 'notify::batteryPercentage')
+ }
+ })] : []),
+ ]
+ });
+ });
+ self.visible = Bluetooth.connected_devices.length > 0;
+ }, 'notify::connected-devices'),
+})
+
+const NetworkWiredIndicator = () => Widget.Stack({
+ transition: 'slide_up_down',
+ transitionDuration: userOptions.animations.durationSmall,
+ children: {
+ 'fallback': SimpleNetworkIndicator(),
+ 'unknown': Widget.Label({ className: 'txt-norm icon-material', label: 'wifi_off' }),
+ 'disconnected': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_off' }),
+ 'connected': Widget.Label({ className: 'txt-norm icon-material', label: 'lan' }),
+ 'connecting': Widget.Label({ className: 'txt-norm icon-material', label: 'settings_ethernet' }),
+ },
+ setup: (self) => self.hook(Network, stack => {
+ if (!Network.wired)
+ return;
+
+ const { internet } = Network.wired;
+ if (['connecting', 'connected'].includes(internet))
+ stack.shown = internet;
+ else if (Network.connectivity !== 'full')
+ stack.shown = 'disconnected';
+ else
+ stack.shown = 'fallback';
+ }),
+});
+
+const SimpleNetworkIndicator = () => Widget.Icon({
+ setup: (self) => self.hook(Network, self => {
+ const icon = Network[Network.primary || 'wifi']?.iconName;
+ self.icon = icon || '';
+ self.visible = icon;
+ }),
+});
+
+const NetworkWifiIndicator = () => Widget.Stack({
+ transition: 'slide_up_down',
+ transitionDuration: userOptions.animations.durationSmall,
+ children: {
+ 'disabled': Widget.Label({ className: 'txt-norm icon-material', label: 'wifi_off' }),
+ 'disconnected': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_off' }),
+ 'connecting': Widget.Label({ className: 'txt-norm icon-material', label: 'settings_ethernet' }),
+ '0': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_0_bar' }),
+ '1': Widget.Label({ className: 'txt-norm icon-material', label: 'network_wifi_1_bar' }),
+ '2': Widget.Label({ className: 'txt-norm icon-material', label: 'network_wifi_2_bar' }),
+ '3': Widget.Label({ className: 'txt-norm icon-material', label: 'network_wifi_3_bar' }),
+ '4': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_4_bar' }),
+ },
+ setup: (self) => self.hook(Network, (stack) => {
+ if (!Network.wifi) {
+ return;
+ }
+ if (Network.wifi.internet == 'connected') {
+ stack.shown = String(Math.ceil(Network.wifi.strength / 25));
+ }
+ else if (["disconnected", "connecting"].includes(Network.wifi.internet)) {
+ stack.shown = Network.wifi.internet;
+ }
+ }),
+});
+
+export const NetworkIndicator = () => Widget.Stack({
+ transition: 'slide_up_down',
+ transitionDuration: userOptions.animations.durationSmall,
+ children: {
+ 'fallback': SimpleNetworkIndicator(),
+ 'wifi': NetworkWifiIndicator(),
+ 'wired': NetworkWiredIndicator(),
+ },
+ setup: (self) => self.hook(Network, stack => {
+ if (!Network.primary) {
+ stack.shown = 'wifi';
+ return;
+ }
+ const primary = Network.primary || 'fallback';
+ if (['wifi', 'wired'].includes(primary))
+ stack.shown = primary;
+ else
+ stack.shown = 'fallback';
+ }),
+});
+
+const HyprlandXkbKeyboardLayout = async ({ useFlag } = {}) => {
+ try {
+ const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
+ var languageStackArray = [];
+
+ const updateCurrentKeyboards = () => {
+ var initLangs = [];
+ JSON.parse(Utils.exec('hyprctl -j devices')).keyboards
+ .forEach(keyboard => {
+ initLangs.push(...keyboard.layout.split(',').map(lang => lang.trim()));
+ });
+ initLangs = [...new Set(initLangs)];
+ languageStackArray = Array.from({ length: initLangs.length }, (_, i) => {
+ const lang = languages.find(lang => lang.layout == initLangs[i]);
+ // if (!lang) return [
+ // initLangs[i],
+ // Widget.Label({ label: initLangs[i] })
+ // ];
+ // return [
+ // lang.layout,
+ // Widget.Label({ label: (useFlag ? lang.flag : lang.layout) })
+ // ];
+ // Object
+ if (!lang) return {
+ [initLangs[i]]: Widget.Label({ label: initLangs[i] })
+ };
+ return {
+ [lang.layout]: Widget.Label({ label: (useFlag ? lang.flag : lang.layout) })
+ };
+ });
+ };
+ updateCurrentKeyboards();
+ const widgetRevealer = Widget.Revealer({
+ transition: 'slide_left',
+ transitionDuration: userOptions.animations.durationSmall,
+ revealChild: languageStackArray.length > 1,
+ });
+ const widgetKids = {
+ ...languageStackArray.reduce((obj, lang) => {
+ return { ...obj, ...lang };
+ }, {}),
+ 'undef': Widget.Label({ label: '?' }),
+ }
+ const widgetContent = Widget.Stack({
+ transition: 'slide_up_down',
+ transitionDuration: userOptions.animations.durationSmall,
+ children: widgetKids,
+ setup: (self) => self.hook(Hyprland, (stack, kbName, layoutName) => {
+ if (!kbName) {
+ return;
+ }
+ var lang = languages.find(lang => layoutName.includes(lang.name));
+ if (lang) {
+ widgetContent.shown = lang.layout;
+ }
+ else { // Attempt to support langs not listed
+ lang = languageStackArray.find(lang => isLanguageMatch(lang[0], layoutName));
+ if (!lang) stack.shown = 'undef';
+ else stack.shown = lang[0];
+ }
+ }, 'keyboard-layout'),
+ });
+ widgetRevealer.child = widgetContent;
+ return widgetRevealer;
+ } catch {
+ return null;
+ }
+}
+
+const OptionalKeyboardLayout = async () => {
+ try {
+ return await HyprlandXkbKeyboardLayout({ useFlag: userOptions.appearance.keyboardUseFlag });
+ } catch {
+ return null;
+ }
+};
+const createKeyboardLayoutInstances = async () => {
+ const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
+ const monitorsCount = Hyprland.monitors.length
+ const instances = await Promise.all(
+ Array.from({ length: monitorsCount }, () => OptionalKeyboardLayout())
+ );
+
+ return instances;
+};
+const optionalKeyboardLayoutInstances = await createKeyboardLayoutInstances()
+
+export const StatusIcons = (props = {}, monitor = 0) => Widget.Box({
+ ...props,
+ child: Widget.Box({
+ className: 'spacing-h-15',
+ children: [
+ MicMuteIndicator(),
+ optionalKeyboardLayoutInstances[monitor],
+ NotificationIndicator(),
+ NetworkIndicator(),
+ Widget.Box({
+ className: 'spacing-h-5',
+ children: [BluetoothIndicator(), BluetoothDevices()]
+ })
+ ]
+ })
+});
diff --git a/modules/home-manager/ags/ags/modules/.commonwidgets/statusicons_languages.js b/modules/home-manager/ags/ags/modules/.commonwidgets/statusicons_languages.js
new file mode 100644
index 0000000..219533c
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.commonwidgets/statusicons_languages.js
@@ -0,0 +1,67 @@
+// For keyboard layout in statusicons.js
+// This list is not exhaustive. It just includes known/possible languages of users of my dotfiles
+// Add your language here if you use multi-lang xkb input. Else, ignore
+// Note that something like "French (Canada)" should go before "French"
+// and "English (US)" should go before "English"
+export const languages = [
+ {
+ layout: 'us',
+ name: 'English (US)',
+ flag: '🇺🇸'
+ },
+ {
+ layout: 'fi',
+ name: 'Finnish',
+ flag: '🇫🇮'
+ },
+ {
+ layout: 'ru',
+ name: 'Russian',
+ flag: '🇷🇺',
+ },
+ {
+ layout: 'pl',
+ name: 'Polish',
+ flag: '🇷🇵🇵🇱',
+ },
+ {
+ layout: 'ro',
+ name: 'Romanian',
+ flag: '🇷🇴',
+ },
+ {
+ layout: 'ca',
+ name: 'French (Canada)',
+ flag: '🇫🇷',
+ },
+ {
+ layout: 'fr',
+ name: 'French',
+ flag: '🇫🇷',
+ },
+ {
+ layout: 'tr',
+ name: 'Turkish',
+ flag: '🇹🇷',
+ },
+ {
+ layout: 'jp',
+ name: 'Japanese',
+ flag: '🇯🇵',
+ },
+ {
+ layout: 'cn',
+ name: 'Chinese',
+ flag: '🇨🇳',
+ },
+ {
+ layout: 'vn',
+ name: 'Vietnamese',
+ flag: '🇻🇳',
+ },
+ {
+ layout: 'undef',
+ name: 'Undefined',
+ flag: '🧐',
+ },
+]
diff --git a/modules/home-manager/ags/ags/modules/.commonwidgets/tabcontainer.js b/modules/home-manager/ags/ags/modules/.commonwidgets/tabcontainer.js
new file mode 100644
index 0000000..876875d
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.commonwidgets/tabcontainer.js
@@ -0,0 +1,279 @@
+import Variable from 'resource:///com/github/Aylur/ags/variable.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+const { Box, Button, EventBox, Label, Overlay, Stack } = Widget;
+import { MaterialIcon } from './materialicon.js';
+import { NavigationIndicator } from './cairo_navigationindicator.js';
+import { setupCursorHover } from '../.widgetutils/cursorhover.js';
+import { DoubleRevealer } from '../.widgethacks/advancedrevealers.js';
+
+export const TabContainer = ({ icons, names, children, className = '', setup = () => { }, ...rest }) => {
+ const shownIndex = Variable(0);
+ let previousShownIndex = 0;
+ const count = Math.min(icons.length, names.length, children.length);
+ const tabs = Box({
+ homogeneous: true,
+ children: Array.from({ length: count }, (_, i) => Button({ // Tab button
+ className: 'tab-btn',
+ onClicked: () => shownIndex.value = i,
+ setup: setupCursorHover,
+ child: Box({
+ hpack: 'center',
+ vpack: 'center',
+ className: 'spacing-h-5 txt-small',
+ children: [
+ MaterialIcon(icons[i], 'norm'),
+ Label({
+ label: names[i],
+ })
+ ]
+ })
+ })),
+ setup: (self) => self.hook(shownIndex, (self) => {
+ self.children[previousShownIndex].toggleClassName('tab-btn-active', false);
+ self.children[shownIndex.value].toggleClassName('tab-btn-active', true);
+ previousShownIndex = shownIndex.value;
+ }),
+ });
+ const tabIndicatorLine = Box({
+ vertical: true,
+ homogeneous: true,
+ setup: (self) => self.hook(shownIndex, (self) => {
+ self.children[0].css = `font-size: ${shownIndex.value}px;`;
+ }),
+ children: [NavigationIndicator({
+ className: 'tab-indicator',
+ count: count,
+ css: `font-size: ${shownIndex.value}px;`,
+ })],
+ });
+ const tabSection = Box({
+ homogeneous: true,
+ children: [EventBox({
+ onScrollUp: () => mainBox.prevTab(),
+ onScrollDown: () => mainBox.nextTab(),
+ child: Box({
+ vertical: true,
+ children: [
+ tabs,
+ tabIndicatorLine
+ ]
+ })
+ })]
+ });
+ const contentStack = Stack({
+ transition: 'slide_left_right',
+ children: children.reduce((acc, currentValue, index) => {
+ acc[index] = currentValue;
+ return acc;
+ }, {}),
+ setup: (self) => self.hook(shownIndex, (self) => {
+ self.shown = `${shownIndex.value}`;
+ }),
+ });
+ const mainBox = Box({
+ attribute: {
+ children: children,
+ shown: shownIndex,
+ names: names,
+ },
+ vertical: true,
+ className: `spacing-v-5 ${className}`,
+ setup: (self) => {
+ self.pack_start(tabSection, false, false, 0);
+ self.pack_end(contentStack, true, true, 0);
+ setup(self);
+ },
+ ...rest,
+ });
+ mainBox.nextTab = () => shownIndex.value = Math.min(shownIndex.value + 1, count - 1);
+ mainBox.prevTab = () => shownIndex.value = Math.max(shownIndex.value - 1, 0);
+ mainBox.cycleTab = () => shownIndex.value = (shownIndex.value + 1) % count;
+
+ return mainBox;
+}
+
+
+export const IconTabContainer = ({
+ iconWidgets, names, children, className = '',
+ setup = () => { }, onChange = () => { },
+ tabsHpack = 'center', tabSwitcherClassName = '',
+ ...rest
+}) => {
+ const shownIndex = Variable(0);
+ let previousShownIndex = 0;
+ const count = Math.min(iconWidgets.length, names.length, children.length);
+ const tabs = Box({
+ hpack: tabsHpack,
+ className: `spacing-h-5 ${tabSwitcherClassName}`,
+ children: iconWidgets.map((icon, i) => Button({
+ className: 'tab-icon',
+ tooltipText: names[i],
+ child: icon,
+ setup: setupCursorHover,
+ onClicked: () => shownIndex.value = i,
+ })),
+ setup: (self) => self.hook(shownIndex, (self) => {
+ self.children[previousShownIndex].toggleClassName('tab-icon-active', false);
+ self.children[shownIndex.value].toggleClassName('tab-icon-active', true);
+ previousShownIndex = shownIndex.value;
+ }),
+ });
+ const tabSection = Box({
+ homogeneous: true,
+ children: [EventBox({
+ onScrollUp: () => mainBox.prevTab(),
+ onScrollDown: () => mainBox.nextTab(),
+ child: Box({
+ vertical: true,
+ hexpand: true,
+ children: [
+ tabs,
+ ]
+ })
+ })]
+ });
+ const contentStack = Stack({
+ transition: 'slide_left_right',
+ children: children.reduce((acc, currentValue, index) => {
+ acc[index] = currentValue;
+ return acc;
+ }, {}),
+ setup: (self) => self.hook(shownIndex, (self) => {
+ self.shown = `${shownIndex.value}`;
+ }),
+ });
+ const mainBox = Box({
+ attribute: {
+ children: children,
+ shown: shownIndex,
+ names: names,
+ },
+ vertical: true,
+ className: `spacing-v-5 ${className}`,
+ setup: (self) => {
+ self.pack_start(tabSection, false, false, 0);
+ self.pack_end(contentStack, true, true, 0);
+ setup(self);
+ self.hook(shownIndex, (self) => onChange(self, shownIndex.value));
+ },
+ ...rest,
+ });
+ mainBox.nextTab = () => shownIndex.value = Math.min(shownIndex.value + 1, count - 1);
+ mainBox.prevTab = () => shownIndex.value = Math.max(shownIndex.value - 1, 0);
+ mainBox.cycleTab = () => shownIndex.value = (shownIndex.value + 1) % count;
+ mainBox.shown = shownIndex;
+
+ return mainBox;
+}
+
+export const ExpandingIconTabContainer = ({
+ icons, names, children, className = '',
+ setup = () => { }, onChange = () => { },
+ tabsHpack = 'center', tabSwitcherClassName = '',
+ transitionDuration = userOptions.animations.durationLarge,
+ ...rest
+}) => {
+ const shownIndex = Variable(0);
+ let previousShownIndex = 0;
+ const count = Math.min(icons.length, names.length, children.length);
+ const tabs = Box({
+ hpack: tabsHpack,
+ className: `spacing-h-5 ${tabSwitcherClassName}`,
+ children: icons.map((icon, i) => {
+ const tabIcon = MaterialIcon(icon, 'norm', { hexpand: true });
+ const tabName = DoubleRevealer({
+ transition1: 'slide_right',
+ transition2: 'crossfade',
+ duration1: 0,
+ duration2: 0,
+ // duration1: userOptions.animations.durationSmall,
+ // duration2: userOptions.animations.durationSmall,
+ child: Label({
+ className: 'margin-left-5 txt-small',
+ label: names[i],
+ }),
+ revealChild: i === shownIndex.value,
+ })
+ const button = Button({
+ className: 'tab-icon-expandable',
+ tooltipText: names[i],
+ child: Box({
+ homogeneous: true,
+ children: [Box({
+ hpack: 'center',
+ children: [
+ tabIcon,
+ tabName,
+ ]
+ })],
+ }),
+ setup: setupCursorHover,
+ onClicked: () => shownIndex.value = i,
+ });
+ button.toggleFocus = (value) => {
+ tabIcon.hexpand = !value;
+ button.toggleClassName('tab-icon-expandable-active', value);
+ tabName.toggleRevealChild(value);
+ }
+ return button;
+ }),
+ setup: (self) => self.hook(shownIndex, (self) => {
+ self.children[previousShownIndex].toggleFocus(false);
+ self.children[shownIndex.value].toggleFocus(true);
+ previousShownIndex = shownIndex.value;
+ }),
+ });
+ const tabSection = Box({
+ homogeneous: true,
+ children: [EventBox({
+ onScrollUp: () => mainBox.prevTab(),
+ onScrollDown: () => mainBox.nextTab(),
+ child: Box({
+ vertical: true,
+ hexpand: true,
+ children: [
+ tabs,
+ ]
+ })
+ })]
+ });
+ const contentStack = Stack({
+ transition: 'slide_left_right',
+ transitionDuration: transitionDuration,
+ children: children.reduce((acc, currentValue, index) => {
+ acc[index] = currentValue;
+ return acc;
+ }, {}),
+ setup: (self) => self.hook(shownIndex, (self) => {
+ self.shown = `${shownIndex.value}`;
+ }),
+ });
+ const mainBox = Box({
+ attribute: {
+ children: children,
+ shown: shownIndex,
+ names: names,
+ },
+ vertical: true,
+ className: `spacing-v-5 ${className}`,
+ setup: (self) => {
+ self.pack_start(tabSection, false, false, 0);
+ self.pack_end(contentStack, true, true, 0);
+ setup(self);
+ self.hook(shownIndex, (self) => onChange(self, shownIndex.value));
+ },
+ ...rest,
+ });
+ mainBox.nextTab = () => shownIndex.value = Math.min(shownIndex.value + 1, count - 1);
+ mainBox.prevTab = () => shownIndex.value = Math.max(shownIndex.value - 1, 0);
+ mainBox.cycleTab = () => shownIndex.value = (shownIndex.value + 1) % count;
+ mainBox.focusName = (name) => {
+ const focusIndex = names.indexOf(name);
+ if (focusIndex !== -1) {
+ shownIndex.value = focusIndex;
+ }
+ }
+ mainBox.shown = shownIndex;
+
+ return mainBox;
+}
diff --git a/modules/home-manager/ags/ags/modules/.configuration/user_options.js b/modules/home-manager/ags/ags/modules/.configuration/user_options.js
new file mode 100644
index 0000000..e3ca6d3
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.configuration/user_options.js
@@ -0,0 +1,256 @@
+import GLib from 'gi://GLib';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
+import userOverrides from '../../user_options.js';
+
+// Default options.
+// Add overrides in ~/.config/ags/user_options.js
+let configOptions = {
+ // General stuff
+ 'ai': {
+ 'defaultGPTProvider': "ollama",
+ 'defaultTemperature': 0.9,
+ 'enhancements': true,
+ 'useHistory': true,
+ 'writingCursor': " ...", // Warning: Using weird characters can mess up Markdown rendering
+ 'proxyUrl': null, // Can be "socks5://127.0.0.1:9050" or "http://127.0.0.1:8080" for example. Leave it blank if you don't need it.
+ },
+ 'animations': {
+ 'choreographyDelay': 35,
+ 'durationSmall': 110,
+ 'durationLarge': 180,
+ },
+ 'appearance': {
+ 'autoDarkMode': { // Turns on dark mode in certain hours. Time in 24h format
+ 'enabled': false,
+ 'from': "18:10",
+ 'to': "6:10",
+ },
+ 'keyboardUseFlag': false, // Use flag emoji instead of abbreviation letters
+ 'layerSmoke': false,
+ 'layerSmokeStrength': 0.2,
+ 'barRoundedCorners': 1,
+ 'fakeScreenRounding': 1, // 0: None | 1: Always | 2: When not fullscreen
+ },
+ 'apps': {
+ 'bluetooth': "blueberry",
+ 'imageViewer': "loupe",
+ 'network': "XDG_CURRENT_DESKTOP=\"gnome\" gnome-control-center wifi",
+ 'settings': "XDG_CURRENT_DESKTOP=\"gnome\" gnome-control-center",
+ 'taskManager': "gnome-usage",
+ 'terminal': "foot", // This is only for shell actions
+ },
+ 'battery': {
+ 'low': 20,
+ 'critical': 10,
+ 'warnLevels': [20, 15, 5],
+ 'warnTitles': ["Low battery", "Very low battery", 'Critical Battery'],
+ 'warnMessages': ["Plug in the charger", "You there?", 'PLUG THE CHARGER ALREADY'],
+ 'suspendThreshold': 3,
+ },
+ 'brightness': {
+ // Object of controller names for each monitor, either "brightnessctl" or "ddcutil" or "auto"
+ // 'default' one will be used if unspecified
+ // Examples
+ // 'eDP-1': "brightnessctl",
+ // 'DP-1': "ddcutil",
+ 'controllers': {
+ 'default': "ddcutil",
+ },
+ },
+ 'cheatsheet': {
+ 'keybinds': {
+ 'configPath': "" // Path to hyprland keybind config file. Leave empty for default (~/.config/hypr/hyprland/keybinds.conf)
+ }
+ },
+ 'gaming': {
+ 'crosshair': {
+ 'size': 20,
+ 'color': 'rgba(113,227,32,0.9)',
+ },
+ },
+ 'monitors': {
+ 'scaleMethod': "division", // Either "division" [default] or "gdk"
+ },
+ 'music': {
+ 'preferredPlayer': "plasma-browser-integration",
+ },
+ 'onScreenKeyboard': {
+ 'layout': "qwerty_full", // See modules/onscreenkeyboard/onscreenkeyboard.js for available layouts
+ },
+ 'overview': {
+ 'scale': 0.18, // Relative to screen size
+ 'numOfRows': 2,
+ 'numOfCols': 5,
+ 'wsNumScale': 0.09,
+ 'wsNumMarginScale': 0.07,
+ },
+ 'sidebar': {
+ 'ai': {
+ 'extraGptModels': {
+ 'oxygen3': {
+ 'name': 'Oxygen (GPT-3.5)',
+ 'logo_name': 'ai-oxygen-symbolic',
+ 'description': 'An API from Tornado Softwares\nPricing: Free: 100/day\nRequires you to join their Discord for a key',
+ 'base_url': 'https://app.oxyapi.uk/v1/chat/completions',
+ 'key_get_url': 'https://discord.com/invite/kM6MaCqGKA',
+ 'key_file': 'oxygen_key.txt',
+ 'model': 'gpt-3.5-turbo',
+ },
+ }
+ },
+ 'image': {
+ 'columns': 2,
+ 'batchCount': 20,
+ 'allowNsfw': true,
+ },
+ 'pages': {
+ 'order': ["apis", "tools"],
+ 'apis': {
+ 'order': ["gpt", "waifu", "booru"],
+ }
+ },
+ },
+ 'search': {
+ 'enableFeatures': {
+ 'actions': true,
+ 'commands': true,
+ 'mathResults': true,
+ 'directorySearch': true,
+ 'aiSearch': false,
+ 'webSearch': false,
+ },
+ 'engineBaseUrl': "https://www.google.com/search?q=",
+ 'excludedSites': ["quora.com"],
+ },
+ 'time': {
+ // See https://docs.gtk.org/glib/method.DateTime.format.html
+ // Here's the 12h format: "%I:%M%P"
+ // For seconds, add "%S" and set interval to 1000
+ 'format': "%H:%M",
+ 'interval': 5000,
+ 'dateFormatLong': "%A, %d/%m", // On bar
+ 'dateInterval': 5000,
+ 'dateFormat': "%d/%m", // On notif time
+ },
+ 'weather': {
+ 'city': "",
+ 'preferredUnit': "C", // Either C or F
+ },
+ 'workspaces': {
+ 'shown': 10,
+ },
+ 'dock': {
+ 'enabled': false,
+ 'hiddenThickness': 5,
+ 'pinnedApps': ['floorp', 'org.gnome.Nautilus'],
+ 'layer': 'top',
+ 'monitorExclusivity': true, // Dock will move to other monitor along with focus if enabled
+ 'searchPinnedAppIcons': false, // Try to search for the correct icon if the app class isn't an icon name
+ 'trigger': ['client-added', 'client-removed'], // client_added, client_move, workspace_active, client_active
+ // Automatically hide dock after `interval` ms since trigger
+ 'autoHide': [
+ {
+ 'trigger': 'client-added',
+ 'interval': 500,
+ },
+ {
+ 'trigger': 'client-removed',
+ 'interval': 500,
+ },
+ ],
+ },
+ // Longer stuff
+ 'icons': {
+ // Find the window's icon by its class with levenshteinDistance
+ // The file names are processed at startup, so if there
+ // are too many files in the search path it'll affect performance
+ // Example: ['/usr/share/icons/Tela-nord/scalable/apps']
+ 'searchPaths': [''],
+ 'symbolicIconTheme': {
+ "dark": "Adwaita",
+ "light": "Adwaita",
+ },
+ substitutions: {
+ 'code-url-handler': "visual-studio-code",
+ 'Code': "visual-studio-code",
+ 'GitHub Desktop': "github-desktop",
+ 'Minecraft* 1.20.1': "minecraft",
+ 'gnome-tweaks': "org.gnome.tweaks",
+ 'pavucontrol-qt': "pavucontrol",
+ 'wps': "wps-office2019-kprometheus",
+ 'wpsoffice': "wps-office2019-kprometheus",
+ '': "image-missing",
+ },
+ regexSubstitutions: [
+ {
+ regex: /^steam_app_(\d+)$/,
+ replace: "steam_icon_$1",
+ }
+ ]
+ },
+ 'keybinds': {
+ // Format: Mod1+Mod2+key. CaSe SeNsItIvE!
+ // Modifiers: Shift Ctrl Alt Hyper Meta
+ // See https://docs.gtk.org/gdk3/index.html#constants for the other keys (they are listed as KEY_key)
+ 'overview': {
+ 'altMoveLeft': "Ctrl+b",
+ 'altMoveRight': "Ctrl+f",
+ 'deleteToEnd': "Ctrl+k",
+ },
+ 'sidebar': {
+ 'apis': {
+ 'nextTab': "Page_Down",
+ 'prevTab': "Page_Up",
+ },
+ 'options': { // Right sidebar
+ 'nextTab': "Page_Down",
+ 'prevTab': "Page_Up",
+ },
+ 'pin': "Ctrl+p",
+ 'cycleTab': "Ctrl+Tab",
+ 'nextTab': "Ctrl+Page_Down",
+ 'prevTab': "Ctrl+Page_Up",
+ },
+ 'cheatsheet': {
+ 'keybinds': {
+ 'nextTab': "Page_Down",
+ 'prevTab': "Page_Up",
+ },
+ 'nextTab': "Ctrl+Page_Down",
+ 'prevTab': "Ctrl+Page_Up",
+ 'cycleTab': "Ctrl+Tab",
+ }
+ },
+ 'bar': {
+ // Array of bar modes for each monitor. Hit Ctrl+Alt+Slash to cycle.
+ // Modes: "normal", "focus" (workspace indicator only), "nothing"
+ // Example for four monitors: ["normal", "focus", "normal", "nothing"]
+ 'modes': ["normal"]
+ },
+}
+
+// Override defaults with user's options
+let optionsOkay = true;
+function overrideConfigRecursive(userOverrides, configOptions = {}, check = true) {
+ for (const [key, value] of Object.entries(userOverrides)) {
+ if (configOptions[key] === undefined && check) {
+ optionsOkay = false;
+ }
+ else if (typeof value === 'object' && !(value instanceof Array)) {
+ if (key === "substitutions" || key === "regexSubstitutions" || key === "extraGptModels") {
+ overrideConfigRecursive(value, configOptions[key], false);
+ } else overrideConfigRecursive(value, configOptions[key]);
+ } else {
+ configOptions[key] = value;
+ }
+ }
+}
+overrideConfigRecursive(userOverrides, configOptions);
+if (!optionsOkay) Utils.timeout(2000, () => Utils.execAsync(['notify-send',
+ 'Update your user options',
+ 'One or more config options don\'t exist',
+ '-a', 'ags',
+]).catch(print))
+
+globalThis['userOptions'] = configOptions;
+export default configOptions;
diff --git a/modules/home-manager/ags/ags/modules/.miscutils/files.js b/modules/home-manager/ags/ags/modules/.miscutils/files.js
new file mode 100644
index 0000000..00c17b9
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.miscutils/files.js
@@ -0,0 +1,14 @@
+const { Gio, GLib, Gtk } = imports.gi;
+
+export function fileExists(filePath) {
+ let file = Gio.File.new_for_path(filePath);
+ return file.query_exists(null);
+}
+
+export function expandTilde(path) {
+ if (path.startsWith('~')) {
+ return GLib.get_home_dir() + path.slice(1);
+ } else {
+ return path;
+ }
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/.miscutils/icons.js b/modules/home-manager/ags/ags/modules/.miscutils/icons.js
new file mode 100644
index 0000000..66c09ed
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.miscutils/icons.js
@@ -0,0 +1,28 @@
+const { Gtk } = imports.gi;
+
+export function iconExists(iconName) {
+ let iconTheme = Gtk.IconTheme.get_default();
+ return iconTheme.has_icon(iconName);
+}
+
+export function substitute(str) {
+ // Normal substitutions
+ if (userOptions.icons.substitutions[str])
+ return userOptions.icons.substitutions[str];
+
+ // Regex substitutions
+ for (let i = 0; i < userOptions.icons.regexSubstitutions.length; i++) {
+ const substitution = userOptions.icons.regexSubstitutions[i];
+ const replacedName = str.replace(
+ substitution.regex,
+ substitution.replace,
+ );
+ if (replacedName != str) return replacedName;
+ }
+
+ // Guess: convert to kebab case
+ if (!iconExists(str)) str = str.toLowerCase().replace(/\s+/g, "-");
+
+ // Original string
+ return str;
+}
diff --git a/modules/home-manager/ags/ags/modules/.miscutils/mathfuncs.js b/modules/home-manager/ags/ags/modules/.miscutils/mathfuncs.js
new file mode 100644
index 0000000..ba1c0b5
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.miscutils/mathfuncs.js
@@ -0,0 +1,4 @@
+
+export function clamp(x, min, max) {
+ return Math.min(Math.max(x, min), max);
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/.miscutils/md2pango.js b/modules/home-manager/ags/ags/modules/.miscutils/md2pango.js
new file mode 100644
index 0000000..afa91ad
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.miscutils/md2pango.js
@@ -0,0 +1,78 @@
+// Converts from Markdown to Pango. This does not support code blocks.
+// For illogical-impulse, code blocks are treated separately, in their own GtkSourceView widgets.
+// Partly inherited from https://github.com/ubunatic/md2pango
+
+const monospaceFonts = 'JetBrains Mono NF, JetBrains Mono Nerd Font, JetBrains Mono NL, SpaceMono NF, SpaceMono Nerd Font, monospace';
+
+const replacements = {
+ 'indents': [
+ { name: 'BULLET', re: /^(\s*)([\*\-]\s)(.*)(\s*)$/, sub: ' $1- $3' },
+ { name: 'NUMBERING', re: /^(\s*[0-9]+\.\s)(.*)(\s*)$/, sub: ' $1 $2' },
+ ],
+ 'escapes': [
+ { name: 'COMMENT', re: //, sub: '' },
+ { name: 'AMPERSTAND', re: /&/g, sub: '&' },
+ { name: 'LESSTHAN', re: //g, sub: '>' },
+ ],
+ 'sections': [
+ { name: 'H1', re: /^(#\s+)(.*)(\s*)$/, sub: '$2' },
+ { name: 'H2', re: /^(##\s+)(.*)(\s*)$/, sub: '$2' },
+ { name: 'H3', re: /^(###\s+)(.*)(\s*)$/, sub: '$2' },
+ { name: 'H4', re: /^(####\s+)(.*)(\s*)$/, sub: '$2' },
+ { name: 'H5', re: /^(#####\s+)(.*)(\s*)$/, sub: '$2' },
+ ],
+ 'styles': [
+ { name: 'BOLD', re: /(\*\*)(\S[\s\S]*?\S)(\*\*)/g, sub: "$2" },
+ { name: 'UND', re: /(__)(\S[\s\S]*?\S)(__)/g, sub: "$2" },
+ { name: 'EMPH', re: /\*(\S.*?\S)\*/g, sub: "$1" },
+ // { name: 'EMPH', re: /_(\S.*?\S)_/g, sub: "$1" },
+ { name: 'HEXCOLOR', re: /#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/g, sub: '#$1' },
+ { name: 'INLCODE', re: /(`)([^`]*)(`)/g, sub: '$2' },
+ // { name: 'UND', re: /(__|\*\*)(\S[\s\S]*?\S)(__|\*\*)/g, sub: "$2" },
+ ],
+}
+
+const replaceCategory = (text, replaces) => {
+ for (const type of replaces) {
+ text = text.replace(type.re, type.sub);
+ }
+ return text;
+}
+
+// Main function
+
+export default (text) => {
+ let lines = text.split('\n')
+ let output = [];
+ // Replace
+ for (const line of lines) {
+ let result = line;
+ result = replaceCategory(result, replacements.indents);
+ result = replaceCategory(result, replacements.escapes);
+ result = replaceCategory(result, replacements.sections);
+ result = replaceCategory(result, replacements.styles);
+ output.push(result)
+ }
+ // Remove trailing whitespaces
+ output = output.map(line => line.replace(/ +$/, ''))
+ return output.join('\n');
+}
+
+export const markdownTest = `## Inline formatting
+- **Bold** *Italics* __Underline__
+- \`Monospace text\` 🤓
+- Colors
+ - Nvidia green #7ABB08
+ - Soundcloud orange #FF5500
+## Code block
+\`\`\`cpp
+#include
+const std::string GREETING="UwU";
+int main() { std::cout << GREETING; }
+\`\`\`
+## LaTeX
+\`\`\`latex
+\\frac{d}{dx} \\left( \\frac{x-438}{x^2+23x-7} \\right) = \\frac{-x^2 + 869}{(x^2+23x-7)^2} \\\\ → \\\\ cos(2x) = 2cos^2(x) - 1 = 1 - 2sin^2(x) = cos^2(x) - sin^2(x)
+\`\`\`
+`;
diff --git a/modules/home-manager/ags/ags/modules/.miscutils/system.js b/modules/home-manager/ags/ags/modules/.miscutils/system.js
new file mode 100644
index 0000000..377463b
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.miscutils/system.js
@@ -0,0 +1,61 @@
+const { GLib } = imports.gi;
+import Variable from 'resource:///com/github/Aylur/ags/variable.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { execAsync, exec } = Utils;
+
+export const distroID = exec(`bash -c 'cat /etc/os-release | grep "^ID=" | cut -d "=" -f 2 | sed "s/\\"//g"'`).trim();
+export const isDebianDistro = (distroID == 'linuxmint' || distroID == 'ubuntu' || distroID == 'debian' || distroID == 'zorin' || distroID == 'popos' || distroID == 'raspbian' || distroID == 'kali');
+export const isArchDistro = (distroID == 'arch' || distroID == 'endeavouros' || distroID == 'cachyos');
+export const hasFlatpak = !!exec(`bash -c 'command -v flatpak'`);
+
+const LIGHTDARK_FILE_LOCATION = `${GLib.get_user_state_dir()}/ags/user/colormode.txt`;
+export const darkMode = Variable(!(Utils.readFile(LIGHTDARK_FILE_LOCATION).split('\n')[0].trim() == 'light'));
+darkMode.connect('changed', ({ value }) => {
+ let lightdark = value ? "dark" : "light";
+ execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_state_dir()}/ags/user && sed -i "1s/.*/${lightdark}/" ${GLib.get_user_state_dir()}/ags/user/colormode.txt`])
+ .then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchcolor.sh`]))
+ .then(execAsync(['bash', '-c', `command -v darkman && darkman set ${lightdark}`])) // Optional darkman integration
+ .catch(print);
+});
+globalThis['darkMode'] = darkMode;
+export const hasPlasmaIntegration = !!Utils.exec('bash -c "command -v plasma-browser-integration-host"');
+
+export const getDistroIcon = () => {
+ // Arches
+ if(distroID == 'arch') return 'arch-symbolic';
+ if(distroID == 'endeavouros') return 'endeavouros-symbolic';
+ if(distroID == 'cachyos') return 'cachyos-symbolic';
+ // Funny flake
+ if(distroID == 'nixos') return 'nixos-symbolic';
+ // Cool thing
+ if(distroID == 'fedora') return 'fedora-symbolic';
+ // Debians
+ if(distroID == 'linuxmint') return 'ubuntu-symbolic';
+ if(distroID == 'ubuntu') return 'ubuntu-symbolic';
+ if(distroID == 'debian') return 'debian-symbolic';
+ if(distroID == 'zorin') return 'ubuntu-symbolic';
+ if(distroID == 'popos') return 'ubuntu-symbolic';
+ if(distroID == 'raspbian') return 'debian-symbolic';
+ if(distroID == 'kali') return 'debian-symbolic';
+ return 'linux-symbolic';
+}
+
+export const getDistroName = () => {
+ // Arches
+ if(distroID == 'arch') return 'Arch Linux';
+ if(distroID == 'endeavouros') return 'EndeavourOS';
+ if(distroID == 'cachyos') return 'CachyOS';
+ // Funny flake
+ if(distroID == 'nixos') return 'NixOS';
+ // Cool thing
+ if(distroID == 'fedora') return 'Fedora';
+ // Debians
+ if(distroID == 'linuxmint') return 'Linux Mint';
+ if(distroID == 'ubuntu') return 'Ubuntu';
+ if(distroID == 'debian') return 'Debian';
+ if(distroID == 'zorin') return 'Zorin';
+ if(distroID == 'popos') return 'Pop!_OS';
+ if(distroID == 'raspbian') return 'Raspbian';
+ if(distroID == 'kali') return 'Kali Linux';
+ return 'Linux';
+}
diff --git a/modules/home-manager/ags/ags/modules/.widgethacks/advancedrevealers.js b/modules/home-manager/ags/ags/modules/.widgethacks/advancedrevealers.js
new file mode 100644
index 0000000..3f12793
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.widgethacks/advancedrevealers.js
@@ -0,0 +1,86 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+
+const { Revealer, Scrollable } = Widget;
+
+export const MarginRevealer = ({
+ transition = 'slide_down',
+ child,
+ revealChild,
+ showClass = 'element-show', // These are for animation curve, they don't really hide
+ hideClass = 'element-hide', // Don't put margins in these classes!
+ extraSetup = () => { },
+ ...rest
+}) => {
+ const widget = Scrollable({
+ ...rest,
+ attribute: {
+ 'revealChild': true, // It'll be set to false after init if it's supposed to hide
+ 'transition': transition,
+ 'show': () => {
+ if (widget.attribute.revealChild) return;
+ widget.hscroll = 'never';
+ widget.vscroll = 'never';
+ child.toggleClassName(hideClass, false);
+ child.toggleClassName(showClass, true);
+ widget.attribute.revealChild = true;
+ child.css = 'margin: 0px;';
+ },
+ 'hide': () => {
+ if (!widget.attribute.revealChild) return;
+ child.toggleClassName(hideClass, true);
+ child.toggleClassName(showClass, false);
+ widget.attribute.revealChild = false;
+ if (widget.attribute.transition == 'slide_left')
+ child.css = `margin-right: -${child.get_allocated_width()}px;`;
+ else if (widget.attribute.transition == 'slide_right')
+ child.css = `margin-left: -${child.get_allocated_width()}px;`;
+ else if (widget.attribute.transition == 'slide_up')
+ child.css = `margin-bottom: -${child.get_allocated_height()}px;`;
+ else if (widget.attribute.transition == 'slide_down')
+ child.css = `margin-top: -${child.get_allocated_height()}px;`;
+ },
+ 'toggle': () => {
+ if (widget.attribute.revealChild) widget.attribute.hide();
+ else widget.attribute.show();
+ },
+ },
+ child: child,
+ hscroll: `${revealChild ? 'never' : 'always'}`,
+ vscroll: `${revealChild ? 'never' : 'always'}`,
+ setup: (self) => {
+ extraSetup(self);
+ }
+ });
+ child.toggleClassName(`${revealChild ? showClass : hideClass}`, true);
+ return widget;
+}
+
+// TODO: Allow reveal update. Currently this just helps at declaration
+export const DoubleRevealer = ({
+ transition1 = 'slide_right',
+ transition2 = 'slide_left',
+ duration1 = 150,
+ duration2 = 150,
+ child,
+ revealChild,
+ ...rest
+}) => {
+ const r2 = Revealer({
+ transition: transition2,
+ transitionDuration: duration2,
+ revealChild: revealChild,
+ child: child,
+ });
+ const r1 = Revealer({
+ transition: transition1,
+ transitionDuration: duration1,
+ revealChild: revealChild,
+ child: r2,
+ ...rest,
+ })
+ r1.toggleRevealChild = (value) => {
+ r1.revealChild = value;
+ r2.revealChild = value;
+ }
+ return r1;
+}
diff --git a/modules/home-manager/ags/ags/modules/.widgethacks/popupwindow.js b/modules/home-manager/ags/ags/modules/.widgethacks/popupwindow.js
new file mode 100644
index 0000000..5c07c0e
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.widgethacks/popupwindow.js
@@ -0,0 +1,36 @@
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+const { Box, Window } = Widget;
+
+
+export default ({
+ name,
+ child,
+ showClassName = "",
+ hideClassName = "",
+ ...props
+}) => {
+ return Window({
+ name,
+ visible: false,
+ layer: 'overlay',
+ ...props,
+
+ child: Box({
+ setup: (self) => {
+ self.keybind("Escape", () => closeEverything());
+ if (showClassName != "" && hideClassName !== "") {
+ self.hook(App, (self, currentName, visible) => {
+ if (currentName === name) {
+ self.toggleClassName(hideClassName, !visible);
+ }
+ });
+
+ if (showClassName !== "" && hideClassName !== "")
+ self.className = `${showClassName} ${hideClassName}`;
+ }
+ },
+ child: child,
+ }),
+ });
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/.widgetutils/clickthrough.js b/modules/home-manager/ags/ags/modules/.widgetutils/clickthrough.js
new file mode 100644
index 0000000..505f141
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.widgetutils/clickthrough.js
@@ -0,0 +1,4 @@
+import Cairo from 'gi://cairo?version=1.0';
+
+export const dummyRegion = new Cairo.Region();
+export const enableClickthrough = (self) => self.input_shape_combine_region(dummyRegion);
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/.widgetutils/cursorhover.js b/modules/home-manager/ags/ags/modules/.widgetutils/cursorhover.js
new file mode 100644
index 0000000..89be913
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.widgetutils/cursorhover.js
@@ -0,0 +1,57 @@
+const { Gdk } = imports.gi;
+
+export function setupCursorHover(button) { // Hand pointing cursor on hover
+ const display = Gdk.Display.get_default();
+ button.connect('enter-notify-event', () => {
+ const cursor = Gdk.Cursor.new_from_name(display, 'pointer');
+ button.get_window().set_cursor(cursor);
+ });
+
+ button.connect('leave-notify-event', () => {
+ const cursor = Gdk.Cursor.new_from_name(display, 'default');
+ button.get_window().set_cursor(cursor);
+ });
+
+}
+
+export function setupCursorHoverAim(button) { // Crosshair cursor on hover
+ button.connect('enter-notify-event', () => {
+ const display = Gdk.Display.get_default();
+ const cursor = Gdk.Cursor.new_from_name(display, 'crosshair');
+ button.get_window().set_cursor(cursor);
+ });
+
+ button.connect('leave-notify-event', () => {
+ const display = Gdk.Display.get_default();
+ const cursor = Gdk.Cursor.new_from_name(display, 'default');
+ button.get_window().set_cursor(cursor);
+ });
+}
+
+export function setupCursorHoverGrab(button) { // Hand ready to grab on hover
+ button.connect('enter-notify-event', () => {
+ const display = Gdk.Display.get_default();
+ const cursor = Gdk.Cursor.new_from_name(display, 'grab');
+ button.get_window().set_cursor(cursor);
+ });
+
+ button.connect('leave-notify-event', () => {
+ const display = Gdk.Display.get_default();
+ const cursor = Gdk.Cursor.new_from_name(display, 'default');
+ button.get_window().set_cursor(cursor);
+ });
+}
+
+export function setupCursorHoverInfo(button) { // "?" mark cursor on hover
+ const display = Gdk.Display.get_default();
+ button.connect('enter-notify-event', () => {
+ const cursor = Gdk.Cursor.new_from_name(display, 'help');
+ button.get_window().set_cursor(cursor);
+ });
+
+ button.connect('leave-notify-event', () => {
+ const cursor = Gdk.Cursor.new_from_name(display, 'default');
+ button.get_window().set_cursor(cursor);
+ });
+}
+
diff --git a/modules/home-manager/ags/ags/modules/.widgetutils/keybind.js b/modules/home-manager/ags/ags/modules/.widgetutils/keybind.js
new file mode 100644
index 0000000..eda7877
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/.widgetutils/keybind.js
@@ -0,0 +1,25 @@
+const { Gdk } = imports.gi;
+
+const MODS = {
+ 'Shift': Gdk.ModifierType.SHIFT_MASK,
+ 'Ctrl': Gdk.ModifierType.CONTROL_MASK,
+ 'Alt': Gdk.ModifierType.ALT_MASK,
+ 'Hyper': Gdk.ModifierType.HYPER_MASK,
+ 'Meta': Gdk.ModifierType.META_MASK
+}
+
+export const checkKeybind = (event, keybind) => {
+ const pressedModMask = event.get_state()[1];
+ const pressedKey = event.get_keyval()[1];
+ const keys = keybind.split('+');
+ for (let i = 0; i < keys.length; i++) {
+ if (keys[i] in MODS) {
+ if (!(pressedModMask & MODS[keys[i]])) {
+ return false;
+ }
+ } else if (pressedKey !== Gdk[`KEY_${keys[i]}`]) {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/modules/home-manager/ags/ags/modules/bar/focus/workspaces_hyprland.js b/modules/home-manager/ags/ags/modules/bar/focus/workspaces_hyprland.js
new file mode 100644
index 0000000..77fe479
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/bar/focus/workspaces_hyprland.js
@@ -0,0 +1,213 @@
+const { GLib, Gdk, Gtk } = imports.gi;
+const Lang = imports.lang;
+const Cairo = imports.cairo;
+const Pango = imports.gi.Pango;
+const PangoCairo = imports.gi.PangoCairo;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+const { Box, DrawingArea, EventBox } = Widget;
+import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
+
+const dummyWs = Box({ className: 'bar-ws-focus' }); // Not shown. Only for getting size props
+const dummyActiveWs = Box({ className: 'bar-ws-focus bar-ws-focus-active' }); // Not shown. Only for getting size props
+const dummyOccupiedWs = Box({ className: 'bar-ws-focus bar-ws-focus-occupied' }); // Not shown. Only for getting size props
+
+const WS_TAKEN_WIDTH_MULTIPLIER = 1.4;
+const floor = Math.floor;
+const ceil = Math.ceil;
+
+// Font size = workspace id
+const WorkspaceContents = (count = 10) => {
+ return DrawingArea({
+ className: 'menu-decel',
+ attribute: {
+ lastImmediateActiveWs: 0,
+ immediateActiveWs: 0,
+ initialized: false,
+ workspaceMask: 0,
+ workspaceGroup: 0,
+ updateMask: (self) => {
+ const offset = Math.floor((Hyprland.active.workspace.id - 1) / count) * userOptions.workspaces.shown;
+ // if (self.attribute.initialized) return; // We only need this to run once
+ const workspaces = Hyprland.workspaces;
+ let workspaceMask = 0;
+ for (let i = 0; i < workspaces.length; i++) {
+ const ws = workspaces[i];
+ if (ws.id <= offset || ws.id > offset + count) continue; // Out of range, ignore
+ if (workspaces[i].windows > 0)
+ workspaceMask |= (1 << (ws.id - offset));
+ }
+ // console.log('Mask:', workspaceMask.toString(2));
+ self.attribute.workspaceMask = workspaceMask;
+ // self.attribute.initialized = true;
+ self.queue_draw();
+ },
+ toggleMask: (self, occupied, name) => {
+ if (occupied) self.attribute.workspaceMask |= (1 << parseInt(name));
+ else self.attribute.workspaceMask &= ~(1 << parseInt(name));
+ self.queue_draw();
+ },
+ },
+ setup: (area) => area
+ .hook(Hyprland.active.workspace, (self) => {
+ const newActiveWs = (Hyprland.active.workspace.id - 1) % count + 1;
+ self.setCss(`font-size: ${newActiveWs}px;`);
+ self.attribute.lastImmediateActiveWs = self.attribute.immediateActiveWs;
+ self.attribute.immediateActiveWs = newActiveWs;
+ const previousGroup = self.attribute.workspaceGroup;
+ const currentGroup = Math.floor((Hyprland.active.workspace.id - 1) / count);
+ if (currentGroup !== previousGroup) {
+ self.attribute.updateMask(self);
+ self.attribute.workspaceGroup = currentGroup;
+ }
+ })
+ .hook(Hyprland, (self) => self.attribute.updateMask(self), 'notify::workspaces')
+ .on('draw', Lang.bind(area, (area, cr) => {
+ const offset = Math.floor((Hyprland.active.workspace.id - 1) / count) * userOptions.workspaces.shown;
+
+ const allocation = area.get_allocation();
+ const { width, height } = allocation;
+
+ const workspaceStyleContext = dummyWs.get_style_context();
+ const workspaceDiameter = workspaceStyleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
+ const workspaceRadius = workspaceDiameter / 2;
+ const wsbg = workspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
+
+ const occupiedWorkspaceStyleContext = dummyOccupiedWs.get_style_context();
+ const occupiedbg = occupiedWorkspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
+
+ const activeWorkspaceStyleContext = dummyActiveWs.get_style_context();
+ const activeWorkspaceWidth = activeWorkspaceStyleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
+ // const activeWorkspaceWidth = 100;
+ const activebg = activeWorkspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
+
+ const widgetStyleContext = area.get_style_context();
+ const activeWs = widgetStyleContext.get_property('font-size', Gtk.StateFlags.NORMAL);
+ const lastImmediateActiveWs = area.attribute.lastImmediateActiveWs;
+ const immediateActiveWs = area.attribute.immediateActiveWs;
+
+ // Draw
+ area.set_size_request(workspaceDiameter * WS_TAKEN_WIDTH_MULTIPLIER * (count - 1) + activeWorkspaceWidth, -1);
+ for (let i = 1; i <= count; i++) {
+ if (i == immediateActiveWs) continue;
+ let colors = {};
+ if (area.attribute.workspaceMask & (1 << i)) colors = occupiedbg;
+ else colors = wsbg;
+
+ // if ((i == immediateActiveWs + 1 && immediateActiveWs < activeWs) ||
+ // (i == immediateActiveWs + 1 && immediateActiveWs < activeWs)) {
+ // const widthPercentage = (i == immediateActiveWs - 1) ?
+ // 1 - (immediateActiveWs - activeWs) :
+ // activeWs - immediateActiveWs;
+ // cr.setSourceRGBA(colors.red * widthPercentage + activebg.red * (1 - widthPercentage),
+ // colors.green * widthPercentage + activebg.green * (1 - widthPercentage),
+ // colors.blue * widthPercentage + activebg.blue * (1 - widthPercentage),
+ // colors.alpha);
+ // }
+ // else
+ cr.setSourceRGBA(colors.red, colors.green, colors.blue, colors.alpha)
+
+ const centerX = (i <= activeWs) ?
+ (-workspaceRadius + (workspaceDiameter * WS_TAKEN_WIDTH_MULTIPLIER * i))
+ : -workspaceRadius + workspaceDiameter * WS_TAKEN_WIDTH_MULTIPLIER * (count - 1) + activeWorkspaceWidth - ((count - i) * workspaceDiameter * WS_TAKEN_WIDTH_MULTIPLIER);
+ cr.arc(centerX, height / 2, workspaceRadius, 0, 2 * Math.PI);
+ cr.fill();
+ // What if shrinking
+ if (i == floor(activeWs) && immediateActiveWs > activeWs) { // To right
+ const widthPercentage = 1 - (ceil(activeWs) - activeWs);
+ const leftX = centerX;
+ const wsWidth = (activeWorkspaceWidth - (workspaceDiameter * 1.5)) * (1 - widthPercentage);
+ cr.rectangle(leftX, height / 2 - workspaceRadius, wsWidth, workspaceDiameter);
+ cr.fill();
+ cr.arc(leftX + wsWidth, height / 2, workspaceRadius, 0, Math.PI * 2);
+ cr.fill();
+ }
+ else if (i == ceil(activeWs) && immediateActiveWs < activeWs) { // To left
+ const widthPercentage = activeWs - floor(activeWs);
+ const rightX = centerX;
+ const wsWidth = (activeWorkspaceWidth - (workspaceDiameter * 1.5)) * widthPercentage;
+ const leftX = rightX - wsWidth;
+ cr.rectangle(leftX, height / 2 - workspaceRadius, wsWidth, workspaceDiameter);
+ cr.fill();
+ cr.arc(leftX, height / 2, workspaceRadius, 0, Math.PI * 2);
+ cr.fill();
+ }
+ }
+
+ let widthPercentage, leftX, rightX, activeWsWidth;
+ cr.setSourceRGBA(activebg.red, activebg.green, activebg.blue, activebg.alpha);
+ if (immediateActiveWs > activeWs) { // To right
+ const immediateActiveWs = ceil(activeWs);
+ widthPercentage = immediateActiveWs - activeWs;
+ rightX = -workspaceRadius + workspaceDiameter * WS_TAKEN_WIDTH_MULTIPLIER * (count - 1) + activeWorkspaceWidth - ((count - immediateActiveWs) * workspaceDiameter * WS_TAKEN_WIDTH_MULTIPLIER);
+ activeWsWidth = (activeWorkspaceWidth - (workspaceDiameter * 1.5)) * (1 - widthPercentage);
+ leftX = rightX - activeWsWidth;
+
+ cr.arc(leftX, height / 2, workspaceRadius, 0, Math.PI * 2); // Should be 0.5 * Math.PI, 1.5 * Math.PI in theory but it leaves a weird 1px gap
+ cr.fill();
+ cr.rectangle(leftX, height / 2 - workspaceRadius, activeWsWidth, workspaceDiameter);
+ cr.fill();
+ cr.arc(leftX + activeWsWidth, height / 2, workspaceRadius, 0, Math.PI * 2);
+ cr.fill();
+ }
+ else { // To left
+ const immediateActiveWs = floor(activeWs);
+ widthPercentage = 1 - (activeWs - immediateActiveWs);
+ leftX = -workspaceRadius + (workspaceDiameter * WS_TAKEN_WIDTH_MULTIPLIER * immediateActiveWs);
+ activeWsWidth = (activeWorkspaceWidth - (workspaceDiameter * 1.5)) * widthPercentage
+
+ cr.arc(leftX, height / 2, workspaceRadius, 0, Math.PI * 2); // Should be 0.5 * Math.PI, 1.5 * Math.PI in theory but it leaves a weird 1px gap
+ cr.fill();
+ cr.rectangle(leftX, height / 2 - workspaceRadius, activeWsWidth, workspaceDiameter);
+ cr.fill();
+ cr.arc(leftX + activeWsWidth, height / 2, workspaceRadius, 0, Math.PI * 2);
+ cr.fill();
+ }
+ }))
+ ,
+ })
+}
+
+export default () => EventBox({
+ onScrollUp: () => Hyprland.messageAsync(`dispatch workspace -1`).catch(print),
+ onScrollDown: () => Hyprland.messageAsync(`dispatch workspace +1`).catch(print),
+ onMiddleClick: () => toggleWindowOnAllMonitors('osk'),
+ onSecondaryClick: () => App.toggleWindow('overview'),
+ attribute: {
+ clicked: false,
+ ws_group: 0,
+ },
+ child: Box({
+ homogeneous: true,
+ // className: 'bar-group-margin',
+ children: [Box({
+ // className: 'bar-group bar-group-standalone bar-group-pad',
+ css: 'min-width: 2px;',
+ children: [WorkspaceContents(userOptions.workspaces.shown)],
+ })]
+ }),
+ setup: (self) => {
+ self.add_events(Gdk.EventMask.POINTER_MOTION_MASK);
+ self.on('motion-notify-event', (self, event) => {
+ if (!self.attribute.clicked) return;
+ const [_, cursorX, cursorY] = event.get_coords();
+ const widgetWidth = self.get_allocation().width;
+ const wsId = Math.ceil(cursorX * userOptions.workspaces.shown / widgetWidth);
+ Utils.execAsync([`${App.configDir}/scripts/hyprland/workspace_action.sh`, 'workspace', `${wsId}`])
+ .catch(print);
+ })
+ self.on('button-press-event', (self, event) => {
+ if (!(event.get_button()[1] === 1)) return; // We're only interested in left-click here
+ self.attribute.clicked = true;
+ const [_, cursorX, cursorY] = event.get_coords();
+ const widgetWidth = self.get_allocation().width;
+ // const wsId = Math.ceil(cursorX * NUM_OF_WORKSPACES_PER_GROUP / widgetWidth) + self.attribute.ws_group * NUM_OF_WORKSPACES_PER_GROUP;
+ // Hyprland.messageAsync(`dispatch workspace ${wsId}`).catch(print);
+ const wsId = Math.ceil(cursorX * userOptions.workspaces.shown / widgetWidth);
+ Utils.execAsync([`${App.configDir}/scripts/hyprland/workspace_action.sh`, 'workspace', `${wsId}`])
+ .catch(print);
+ })
+ self.on('button-release-event', (self) => self.attribute.clicked = false);
+ }
+})
diff --git a/modules/home-manager/ags/ags/modules/bar/main.js b/modules/home-manager/ags/ags/modules/bar/main.js
new file mode 100644
index 0000000..86e5d06
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/bar/main.js
@@ -0,0 +1,121 @@
+const { Gtk } = imports.gi;
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import Battery from 'resource:///com/github/Aylur/ags/service/battery.js';
+
+import WindowTitle from "./normal/spaceleft.js";
+import Indicators from "./normal/spaceright.js";
+import Music from "./normal/music.js";
+import System from "./normal/system.js";
+import { enableClickthrough } from "../.widgetutils/clickthrough.js";
+import { RoundedCorner } from "../.commonwidgets/cairo_roundedcorner.js";
+import { currentShellMode } from '../../variables.js';
+
+const NormalOptionalWorkspaces = async () => {
+ try {
+ return (await import('./normal/workspaces_hyprland.js')).default();
+ } catch {
+ return null;
+ }
+};
+
+const FocusOptionalWorkspaces = async () => {
+ try {
+ return (await import('./focus/workspaces_hyprland.js')).default();
+ } catch {
+ return null;
+ }
+};
+
+export const Bar = async (monitor = 0) => {
+ const SideModule = (children) => Widget.Box({
+ className: 'bar-sidemodule',
+ children: children,
+ });
+ const normalBarContent = Widget.CenterBox({
+ className: 'bar-bg',
+ setup: (self) => {
+ const styleContext = self.get_style_context();
+ const minHeight = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
+ // execAsync(['bash', '-c', `hyprctl keyword monitor ,addreserved,${minHeight},0,0,0`]).catch(print);
+ },
+ startWidget: (await WindowTitle(monitor)),
+ centerWidget: Widget.Box({
+ className: 'spacing-h-4',
+ children: [
+ SideModule([Music()]),
+ Widget.Box({
+ homogeneous: true,
+ children: [await NormalOptionalWorkspaces()],
+ }),
+ SideModule([System()]),
+ ]
+ }),
+ endWidget: Indicators(monitor),
+ });
+ const focusedBarContent = Widget.CenterBox({
+ className: 'bar-bg-focus',
+ startWidget: Widget.Box({}),
+ centerWidget: Widget.Box({
+ className: 'spacing-h-4',
+ children: [
+ SideModule([]),
+ Widget.Box({
+ homogeneous: true,
+ children: [await FocusOptionalWorkspaces()],
+ }),
+ SideModule([]),
+ ]
+ }),
+ endWidget: Widget.Box({}),
+ setup: (self) => {
+ self.hook(Battery, (self) => {
+ if (!Battery.available) return;
+ self.toggleClassName('bar-bg-focus-batterylow', Battery.percent <= userOptions.battery.low);
+ })
+ }
+ });
+ const nothingContent = Widget.Box({
+ className: 'bar-bg-nothing',
+ })
+ return Widget.Window({
+ monitor,
+ name: `bar${monitor}`,
+ anchor: ['top', 'left', 'right'],
+ exclusivity: 'exclusive',
+ visible: true,
+ child: Widget.Stack({
+ homogeneous: false,
+ transition: 'slide_up_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ children: {
+ 'normal': normalBarContent,
+ 'focus': focusedBarContent,
+ 'nothing': nothingContent,
+ },
+ setup: (self) => self.hook(currentShellMode, (self) => {
+ self.shown = currentShellMode.value[monitor];
+ })
+ }),
+ });
+}
+
+export const BarCornerTopleft = (monitor = 0) => Widget.Window({
+ monitor,
+ name: `barcornertl${monitor}`,
+ layer: 'top',
+ anchor: ['top', 'left'],
+ exclusivity: 'normal',
+ visible: true,
+ child: RoundedCorner('topleft', { className: 'corner', }),
+ setup: enableClickthrough,
+});
+export const BarCornerTopright = (monitor = 0) => Widget.Window({
+ monitor,
+ name: `barcornertr${monitor}`,
+ layer: 'top',
+ anchor: ['top', 'right'],
+ exclusivity: 'normal',
+ visible: true,
+ child: RoundedCorner('topright', { className: 'corner', }),
+ setup: enableClickthrough,
+});
diff --git a/modules/home-manager/ags/ags/modules/bar/normal/music.js b/modules/home-manager/ags/ags/modules/bar/normal/music.js
new file mode 100644
index 0000000..93c0ea1
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/bar/normal/music.js
@@ -0,0 +1,230 @@
+const { GLib } = imports.gi;
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
+const { Box, Button, EventBox, Label, Overlay, Revealer, Scrollable } = Widget;
+const { execAsync, exec } = Utils;
+import { AnimatedCircProg } from "../../.commonwidgets/cairo_circularprogress.js";
+import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
+import { showMusicControls } from '../../../variables.js';
+
+const CUSTOM_MODULE_CONTENT_INTERVAL_FILE = `${GLib.get_user_cache_dir()}/ags/user/scripts/custom-module-interval.txt`;
+const CUSTOM_MODULE_CONTENT_SCRIPT = `${GLib.get_user_cache_dir()}/ags/user/scripts/custom-module-poll.sh`;
+const CUSTOM_MODULE_LEFTCLICK_SCRIPT = `${GLib.get_user_cache_dir()}/ags/user/scripts/custom-module-leftclick.sh`;
+const CUSTOM_MODULE_RIGHTCLICK_SCRIPT = `${GLib.get_user_cache_dir()}/ags/user/scripts/custom-module-rightclick.sh`;
+const CUSTOM_MODULE_MIDDLECLICK_SCRIPT = `${GLib.get_user_cache_dir()}/ags/user/scripts/custom-module-middleclick.sh`;
+const CUSTOM_MODULE_SCROLLUP_SCRIPT = `${GLib.get_user_cache_dir()}/ags/user/scripts/custom-module-scrollup.sh`;
+const CUSTOM_MODULE_SCROLLDOWN_SCRIPT = `${GLib.get_user_cache_dir()}/ags/user/scripts/custom-module-scrolldown.sh`;
+
+function trimTrackTitle(title) {
+ if (!title) return '';
+ const cleanPatterns = [
+ /【[^】]*】/, // Touhou n weeb stuff
+ " [FREE DOWNLOAD]", // F-777
+ ];
+ cleanPatterns.forEach((expr) => title = title.replace(expr, ''));
+ return title;
+}
+
+const BarGroup = ({ child }) => Box({
+ className: 'bar-group-margin bar-sides',
+ children: [
+ Box({
+ className: 'bar-group bar-group-standalone bar-group-pad-system',
+ children: [child],
+ }),
+ ]
+});
+
+const BarResource = (name, icon, command, circprogClassName = 'bar-batt-circprog', textClassName = 'txt-onSurfaceVariant', iconClassName = 'bar-batt') => {
+ const resourceCircProg = AnimatedCircProg({
+ className: `${circprogClassName}`,
+ vpack: 'center',
+ hpack: 'center',
+ });
+ const resourceProgress = Box({
+ homogeneous: true,
+ children: [Overlay({
+ child: Box({
+ vpack: 'center',
+ className: `${iconClassName}`,
+ homogeneous: true,
+ children: [
+ MaterialIcon(icon, 'small'),
+ ],
+ }),
+ overlays: [resourceCircProg]
+ })]
+ });
+ const resourceLabel = Label({
+ className: `txt-smallie ${textClassName}`,
+ });
+ const widget = Button({
+ onClicked: () => Utils.execAsync(['bash', '-c', `${userOptions.apps.taskManager}`]).catch(print),
+ child: Box({
+ className: `spacing-h-4 ${textClassName}`,
+ children: [
+ resourceProgress,
+ resourceLabel,
+ ],
+ setup: (self) => self.poll(5000, () => execAsync(['bash', '-c', command])
+ .then((output) => {
+ resourceCircProg.css = `font-size: ${Number(output)}px;`;
+ resourceLabel.label = `${Math.round(Number(output))}%`;
+ widget.tooltipText = `${name}: ${Math.round(Number(output))}%`;
+ }).catch(print))
+ ,
+ })
+ });
+ return widget;
+}
+
+const TrackProgress = () => {
+ const _updateProgress = (circprog) => {
+ const mpris = Mpris.getPlayer('');
+ if (!mpris) return;
+ // Set circular progress value
+ circprog.css = `font-size: ${Math.max(mpris.position / mpris.length * 100, 0)}px;`
+ }
+ return AnimatedCircProg({
+ className: 'bar-music-circprog',
+ vpack: 'center', hpack: 'center',
+ extraSetup: (self) => self
+ .hook(Mpris, _updateProgress)
+ .poll(3000, _updateProgress)
+ ,
+ })
+}
+
+const switchToRelativeWorkspace = async (self, num) => {
+ try {
+ const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
+ Hyprland.messageAsync(`dispatch workspace ${num > 0 ? '+' : ''}${num}`).catch(print);
+ } catch {
+ return null;
+ }
+}
+
+export default () => {
+ // TODO: use cairo to make button bounce smaller on click, if that's possible
+ const playingState = Box({ // Wrap a box cuz overlay can't have margins itself
+ homogeneous: true,
+ children: [Overlay({
+ child: Box({
+ vpack: 'center',
+ className: 'bar-music-playstate',
+ homogeneous: true,
+ children: [Label({
+ vpack: 'center',
+ className: 'bar-music-playstate-txt',
+ justification: 'center',
+ setup: (self) => self.hook(Mpris, label => {
+ const mpris = Mpris.getPlayer('');
+ label.label = `${mpris !== null && mpris.playBackStatus == 'Playing' ? 'pause' : 'play_arrow'}`;
+ }),
+ })],
+ setup: (self) => self.hook(Mpris, label => {
+ const mpris = Mpris.getPlayer('');
+ if (!mpris) return;
+ label.toggleClassName('bar-music-playstate-playing', mpris !== null && mpris.playBackStatus == 'Playing');
+ label.toggleClassName('bar-music-playstate', mpris !== null || mpris.playBackStatus == 'Paused');
+ }),
+ }),
+ overlays: [
+ TrackProgress(),
+ ]
+ })]
+ });
+ const trackTitle = Label({
+ hexpand: true,
+ className: 'txt-smallie bar-music-txt',
+ truncate: 'end',
+ maxWidthChars: 1, // Doesn't matter, just needs to be non negative
+ setup: (self) => self.hook(Mpris, label => {
+ const mpris = Mpris.getPlayer('');
+ if (mpris)
+ label.label = `${trimTrackTitle(mpris.trackTitle)} • ${mpris.trackArtists.join(', ')}`;
+ else
+ label.label = 'No media';
+ }),
+ })
+ const musicStuff = Box({
+ className: 'spacing-h-10',
+ hexpand: true,
+ children: [
+ playingState,
+ trackTitle,
+ ]
+ })
+ const SystemResourcesOrCustomModule = () => {
+ // Check if $XDG_CACHE_HOME/ags/user/scripts/custom-module-poll.sh exists
+ if (GLib.file_test(CUSTOM_MODULE_CONTENT_SCRIPT, GLib.FileTest.EXISTS)) {
+ const interval = Number(Utils.readFile(CUSTOM_MODULE_CONTENT_INTERVAL_FILE)) || 5000;
+ return BarGroup({
+ child: Button({
+ child: Label({
+ className: 'txt-smallie txt-onSurfaceVariant',
+ useMarkup: true,
+ setup: (self) => Utils.timeout(1, () => {
+ self.label = exec(CUSTOM_MODULE_CONTENT_SCRIPT);
+ self.poll(interval, (self) => {
+ const content = exec(CUSTOM_MODULE_CONTENT_SCRIPT);
+ self.label = content;
+ })
+ })
+ }),
+ onPrimaryClickRelease: () => execAsync(CUSTOM_MODULE_LEFTCLICK_SCRIPT).catch(print),
+ onSecondaryClickRelease: () => execAsync(CUSTOM_MODULE_RIGHTCLICK_SCRIPT).catch(print),
+ onMiddleClickRelease: () => execAsync(CUSTOM_MODULE_MIDDLECLICK_SCRIPT).catch(print),
+ onScrollUp: () => execAsync(CUSTOM_MODULE_SCROLLUP_SCRIPT).catch(print),
+ onScrollDown: () => execAsync(CUSTOM_MODULE_SCROLLDOWN_SCRIPT).catch(print),
+ })
+ });
+ } else return BarGroup({
+ child: Box({
+ children: [
+ BarResource('RAM Usage', 'memory', `LANG=C free | awk '/^Mem/ {printf("%.2f\\n", ($3/$2) * 100)}'`,
+ 'bar-ram-circprog', 'bar-ram-txt', 'bar-ram-icon'),
+ Revealer({
+ revealChild: true,
+ transition: 'slide_left',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Box({
+ className: 'spacing-h-10 margin-left-10',
+ children: [
+ BarResource('Swap Usage', 'swap_horiz', `LANG=C free | awk '/^Swap/ {if ($2 > 0) printf("%.2f\\n", ($3/$2) * 100); else print "0";}'`,
+ 'bar-swap-circprog', 'bar-swap-txt', 'bar-swap-icon'),
+ BarResource('CPU Usage', 'settings_motion_mode', `LANG=C top -bn1 | grep Cpu | sed 's/\\,/\\./g' | awk '{print $2}'`,
+ 'bar-cpu-circprog', 'bar-cpu-txt', 'bar-cpu-icon'),
+ ]
+ }),
+ setup: (self) => self.hook(Mpris, label => {
+ const mpris = Mpris.getPlayer('');
+ self.revealChild = (!mpris);
+ }),
+ })
+ ],
+ })
+ });
+ }
+ return EventBox({
+ onScrollUp: (self) => switchToRelativeWorkspace(self, -1),
+ onScrollDown: (self) => switchToRelativeWorkspace(self, +1),
+ child: Box({
+ className: 'spacing-h-4',
+ children: [
+ SystemResourcesOrCustomModule(),
+ EventBox({
+ child: BarGroup({ child: musicStuff }),
+ onPrimaryClick: () => showMusicControls.setValue(!showMusicControls.value),
+ onSecondaryClick: () => execAsync(['bash', '-c', 'playerctl next || playerctl position `bc <<< "100 * $(playerctl metadata mpris:length) / 1000000 / 100"` &']).catch(print),
+ onMiddleClick: () => execAsync('playerctl play-pause').catch(print),
+ setup: (self) => self.on('button-press-event', (self, event) => {
+ if (event.get_button()[1] === 8) // Side button
+ execAsync('playerctl previous').catch(print)
+ }),
+ })
+ ]
+ })
+ });
+}
diff --git a/modules/home-manager/ags/ags/modules/bar/normal/spaceleft.js b/modules/home-manager/ags/ags/modules/bar/normal/spaceleft.js
new file mode 100644
index 0000000..7bfb40d
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/bar/normal/spaceleft.js
@@ -0,0 +1,78 @@
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import Brightness from '../../../services/brightness.js';
+import Indicator from '../../../services/indicator.js';
+
+const WindowTitle = async () => {
+ try {
+ const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
+ return Widget.Scrollable({
+ hexpand: true, vexpand: true,
+ hscroll: 'automatic', vscroll: 'never',
+ child: Widget.Box({
+ vertical: true,
+ children: [
+ Widget.Label({
+ xalign: 0,
+ truncate: 'end',
+ maxWidthChars: 1, // Doesn't matter, just needs to be non negative
+ className: 'txt-smaller bar-wintitle-topdesc txt',
+ setup: (self) => self.hook(Hyprland.active.client, label => { // Hyprland.active.client
+ label.label = Hyprland.active.client.class.length === 0 ? 'Desktop' : Hyprland.active.client.class;
+ }),
+ }),
+ Widget.Label({
+ xalign: 0,
+ truncate: 'end',
+ maxWidthChars: 1, // Doesn't matter, just needs to be non negative
+ className: 'txt-smallie bar-wintitle-txt',
+ setup: (self) => self.hook(Hyprland.active.client, label => { // Hyprland.active.client
+ label.label = Hyprland.active.client.title.length === 0 ? `Workspace ${Hyprland.active.workspace.id}` : Hyprland.active.client.title;
+ }),
+ })
+ ]
+ })
+ });
+ } catch {
+ return null;
+ }
+}
+
+
+export default async (monitor = 0) => {
+ const optionalWindowTitleInstance = await WindowTitle();
+ return Widget.EventBox({
+ onScrollUp: () => {
+ Indicator.popup(1); // Since the brightness and speaker are both on the same window
+ Brightness[monitor].screen_value += 0.05;
+ },
+ onScrollDown: () => {
+ Indicator.popup(1); // Since the brightness and speaker are both on the same window
+ Brightness[monitor].screen_value -= 0.05;
+ },
+ onPrimaryClick: () => {
+ App.toggleWindow('sideleft');
+ },
+ child: Widget.Box({
+ homogeneous: false,
+ children: [
+ Widget.Box({ className: 'bar-corner-spacing' }),
+ Widget.Overlay({
+ overlays: [
+ Widget.Box({ hexpand: true }),
+ Widget.Box({
+ className: 'bar-sidemodule', hexpand: true,
+ children: [Widget.Box({
+ vertical: true,
+ className: 'bar-space-button',
+ children: [
+ optionalWindowTitleInstance,
+ ]
+ })]
+ }),
+ ]
+ })
+ ]
+ })
+ });
+}
diff --git a/modules/home-manager/ags/ags/modules/bar/normal/spaceright.js b/modules/home-manager/ags/ags/modules/bar/normal/spaceright.js
new file mode 100644
index 0000000..6266638
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/bar/normal/spaceright.js
@@ -0,0 +1,91 @@
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+
+import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
+import SystemTray from 'resource:///com/github/Aylur/ags/service/systemtray.js';
+const { execAsync } = Utils;
+import Indicator from '../../../services/indicator.js';
+import { StatusIcons } from '../../.commonwidgets/statusicons.js';
+import { Tray } from "./tray.js";
+
+const SeparatorDot = () => Widget.Revealer({
+ transition: 'slide_left',
+ revealChild: false,
+ attribute: {
+ 'count': SystemTray.items.length,
+ 'update': (self, diff) => {
+ self.attribute.count += diff;
+ self.revealChild = (self.attribute.count > 0);
+ }
+ },
+ child: Widget.Box({
+ vpack: 'center',
+ className: 'separator-circle',
+ }),
+ setup: (self) => self
+ .hook(SystemTray, (self) => self.attribute.update(self, 1), 'added')
+ .hook(SystemTray, (self) => self.attribute.update(self, -1), 'removed')
+ ,
+});
+
+export default (monitor = 0) => {
+ const barTray = Tray();
+ const barStatusIcons = StatusIcons({
+ className: 'bar-statusicons',
+ setup: (self) => self.hook(App, (self, currentName, visible) => {
+ if (currentName === 'sideright') {
+ self.toggleClassName('bar-statusicons-active', visible);
+ }
+ }),
+ }, monitor);
+ const SpaceRightDefaultClicks = (child) => Widget.EventBox({
+ onHover: () => { barStatusIcons.toggleClassName('bar-statusicons-hover', true) },
+ onHoverLost: () => { barStatusIcons.toggleClassName('bar-statusicons-hover', false) },
+ onPrimaryClick: () => App.toggleWindow('sideright'),
+ onSecondaryClick: () => execAsync(['bash', '-c', 'playerctl next || playerctl position `bc <<< "100 * $(playerctl metadata mpris:length) / 1000000 / 100"` &']).catch(print),
+ onMiddleClick: () => execAsync('playerctl play-pause').catch(print),
+ setup: (self) => self.on('button-press-event', (self, event) => {
+ if (event.get_button()[1] === 8)
+ execAsync('playerctl previous').catch(print)
+ }),
+ child: child,
+ });
+ const emptyArea = SpaceRightDefaultClicks(Widget.Box({ hexpand: true, }));
+ const indicatorArea = SpaceRightDefaultClicks(Widget.Box({
+ children: [
+ SeparatorDot(),
+ barStatusIcons
+ ],
+ }));
+ const actualContent = Widget.Box({
+ hexpand: true,
+ className: 'spacing-h-5 bar-spaceright',
+ children: [
+ emptyArea,
+ barTray,
+ indicatorArea
+ ],
+ });
+
+ return Widget.EventBox({
+ onScrollUp: () => {
+ if (!Audio.speaker) return;
+ if (Audio.speaker.volume <= 0.09) Audio.speaker.volume += 0.01;
+ else Audio.speaker.volume += 0.03;
+ Indicator.popup(1);
+ },
+ onScrollDown: () => {
+ if (!Audio.speaker) return;
+ if (Audio.speaker.volume <= 0.09) Audio.speaker.volume -= 0.01;
+ else Audio.speaker.volume -= 0.03;
+ Indicator.popup(1);
+ },
+ child: Widget.Box({
+ children: [
+ actualContent,
+ SpaceRightDefaultClicks(Widget.Box({ className: 'bar-corner-spacing' })),
+ ]
+ })
+ });
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/bar/normal/system.js b/modules/home-manager/ags/ags/modules/bar/normal/system.js
new file mode 100644
index 0000000..058fd62
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/bar/normal/system.js
@@ -0,0 +1,236 @@
+// This is for the right pills of the bar.
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { Box, Label, Button, Overlay, Revealer, Scrollable, Stack, EventBox } = Widget;
+const { exec, execAsync } = Utils;
+const { GLib } = imports.gi;
+import Battery from 'resource:///com/github/Aylur/ags/service/battery.js';
+import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
+import { AnimatedCircProg } from "../../.commonwidgets/cairo_circularprogress.js";
+import { WWO_CODE, WEATHER_SYMBOL, NIGHT_WEATHER_SYMBOL } from '../../.commondata/weather.js';
+
+const WEATHER_CACHE_FOLDER = `${GLib.get_user_cache_dir()}/ags/weather`;
+Utils.exec(`mkdir -p ${WEATHER_CACHE_FOLDER}`);
+
+const BarBatteryProgress = () => {
+ const _updateProgress = (circprog) => { // Set circular progress value
+ circprog.css = `font-size: ${Math.abs(Battery.percent)}px;`
+
+ circprog.toggleClassName('bar-batt-circprog-low', Battery.percent <= userOptions.battery.low);
+ circprog.toggleClassName('bar-batt-circprog-full', Battery.charged);
+ }
+ return AnimatedCircProg({
+ className: 'bar-batt-circprog',
+ vpack: 'center', hpack: 'center',
+ extraSetup: (self) => self
+ .hook(Battery, _updateProgress)
+ ,
+ })
+}
+
+const time = Variable('', {
+ poll: [
+ userOptions.time.interval,
+ () => GLib.DateTime.new_now_local().format(userOptions.time.format),
+ ],
+})
+
+const date = Variable('', {
+ poll: [
+ userOptions.time.dateInterval,
+ () => GLib.DateTime.new_now_local().format(userOptions.time.dateFormatLong),
+ ],
+})
+
+const BarClock = () => Widget.Box({
+ vpack: 'center',
+ className: 'spacing-h-4 bar-clock-box',
+ children: [
+ Widget.Label({
+ className: 'bar-time',
+ label: time.bind(),
+ }),
+ Widget.Label({
+ className: 'txt-norm txt-onLayer1',
+ label: '•',
+ }),
+ Widget.Label({
+ className: 'txt-smallie bar-date',
+ label: date.bind(),
+ }),
+ ],
+});
+
+const UtilButton = ({ name, icon, onClicked }) => Button({
+ vpack: 'center',
+ tooltipText: name,
+ onClicked: onClicked,
+ className: 'bar-util-btn icon-material txt-norm',
+ label: `${icon}`,
+})
+
+const Utilities = () => Box({
+ hpack: 'center',
+ className: 'spacing-h-4',
+ children: [
+ UtilButton({
+ name: 'Screen snip', icon: 'screenshot_region', onClicked: () => {
+ Utils.execAsync(`${App.configDir}/scripts/grimblast.sh copy area`)
+ .catch(print)
+ }
+ }),
+ UtilButton({
+ name: 'Color picker', icon: 'colorize', onClicked: () => {
+ Utils.execAsync(['hyprpicker', '-a']).catch(print)
+ }
+ }),
+ UtilButton({
+ name: 'Toggle on-screen keyboard', icon: 'keyboard', onClicked: () => {
+ toggleWindowOnAllMonitors('osk');
+ }
+ }),
+ ]
+})
+
+const BarBattery = () => Box({
+ className: 'spacing-h-4 bar-batt-txt',
+ children: [
+ Revealer({
+ transitionDuration: userOptions.animations.durationSmall,
+ revealChild: false,
+ transition: 'slide_right',
+ child: MaterialIcon('bolt', 'norm', { tooltipText: "Charging" }),
+ setup: (self) => self.hook(Battery, revealer => {
+ self.revealChild = Battery.charging;
+ }),
+ }),
+ Label({
+ className: 'txt-smallie',
+ setup: (self) => self.hook(Battery, label => {
+ label.label = `${Number.parseFloat(Battery.percent.toFixed(1))}%`;
+ }),
+ }),
+ Overlay({
+ child: Widget.Box({
+ vpack: 'center',
+ className: 'bar-batt',
+ homogeneous: true,
+ children: [
+ MaterialIcon('battery_full', 'small'),
+ ],
+ setup: (self) => self.hook(Battery, box => {
+ box.toggleClassName('bar-batt-low', Battery.percent <= userOptions.battery.low);
+ box.toggleClassName('bar-batt-full', Battery.charged);
+ }),
+ }),
+ overlays: [
+ BarBatteryProgress(),
+ ]
+ }),
+ ]
+});
+
+const BarGroup = ({ child }) => Widget.Box({
+ className: 'bar-group-margin bar-sides',
+ children: [
+ Widget.Box({
+ className: 'bar-group bar-group-standalone bar-group-pad-system',
+ children: [child],
+ }),
+ ]
+});
+const BatteryModule = () => Stack({
+ transition: 'slide_up_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ children: {
+ 'laptop': Box({
+ className: 'spacing-h-4', children: [
+ BarGroup({ child: Utilities() }),
+ BarGroup({ child: BarBattery() }),
+ ]
+ }),
+ 'desktop': BarGroup({
+ child: Box({
+ hexpand: true,
+ hpack: 'center',
+ className: 'spacing-h-4 txt-onSurfaceVariant',
+ children: [
+ MaterialIcon('device_thermostat', 'small'),
+ Label({
+ label: 'Weather',
+ })
+ ],
+ setup: (self) => self.poll(900000, async (self) => {
+ const WEATHER_CACHE_PATH = WEATHER_CACHE_FOLDER + '/wttr.in.txt';
+ const updateWeatherForCity = (city) => execAsync(`curl https://wttr.in/${city.replace(/ /g, '%20')}?format=j1`)
+ .then(output => {
+ const weather = JSON.parse(output);
+ Utils.writeFile(JSON.stringify(weather), WEATHER_CACHE_PATH)
+ .catch(print);
+ const weatherCode = weather.current_condition[0].weatherCode;
+ const weatherDesc = weather.current_condition[0].weatherDesc[0].value;
+ const temperature = weather.current_condition[0][`temp_${userOptions.weather.preferredUnit}`];
+ const feelsLike = weather.current_condition[0][`FeelsLike${userOptions.weather.preferredUnit}`];
+ const weatherSymbol = WEATHER_SYMBOL[WWO_CODE[weatherCode]];
+ self.children[0].label = weatherSymbol;
+ self.children[1].label = `${temperature}°${userOptions.weather.preferredUnit} • Feels like ${feelsLike}°${userOptions.weather.preferredUnit}`;
+ self.tooltipText = weatherDesc;
+ }).catch((err) => {
+ try { // Read from cache
+ const weather = JSON.parse(
+ Utils.readFile(WEATHER_CACHE_PATH)
+ );
+ const weatherCode = weather.current_condition[0].weatherCode;
+ const weatherDesc = weather.current_condition[0].weatherDesc[0].value;
+ const temperature = weather.current_condition[0][`temp_${userOptions.weather.preferredUnit}`];
+ const feelsLike = weather.current_condition[0][`FeelsLike${userOptions.weather.preferredUnit}`];
+ const weatherSymbol = WEATHER_SYMBOL[WWO_CODE[weatherCode]];
+ self.children[0].label = weatherSymbol;
+ self.children[1].label = `${temperature}°${userOptions.weather.preferredUnit} • Feels like ${feelsLike}°${userOptions.weather.preferredUnit}`;
+ self.tooltipText = weatherDesc;
+ } catch (err) {
+ print(err);
+ }
+ });
+ if (userOptions.weather.city != '' && userOptions.weather.city != null) {
+ updateWeatherForCity(userOptions.weather.city.replace(/ /g, '%20'));
+ }
+ else {
+ Utils.execAsync('curl ipinfo.io')
+ .then(output => {
+ return JSON.parse(output)['city'].toLowerCase();
+ })
+ .then(updateWeatherForCity)
+ .catch(print)
+ }
+ }),
+ })
+ }),
+ },
+ setup: (stack) => Utils.timeout(10, () => {
+ if (!Battery.available) stack.shown = 'desktop';
+ else stack.shown = 'laptop';
+ })
+})
+
+const switchToRelativeWorkspace = async (self, num) => {
+ try {
+ const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
+ Hyprland.messageAsync(`dispatch workspace ${num > 0 ? '+' : ''}${num}`).catch(print);
+ } catch {
+ return null;
+ }
+}
+
+export default () => Widget.EventBox({
+ onScrollUp: (self) => switchToRelativeWorkspace(self, -1),
+ onScrollDown: (self) => switchToRelativeWorkspace(self, +1),
+ onPrimaryClick: () => App.toggleWindow('sideright'),
+ child: Widget.Box({
+ className: 'spacing-h-4',
+ children: [
+ BarGroup({ child: BarClock() }),
+ BatteryModule(),
+ ]
+ })
+});
diff --git a/modules/home-manager/ags/ags/modules/bar/normal/tray.js b/modules/home-manager/ags/ags/modules/bar/normal/tray.js
new file mode 100644
index 0000000..d568bc7
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/bar/normal/tray.js
@@ -0,0 +1,36 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import SystemTray from 'resource:///com/github/Aylur/ags/service/systemtray.js';
+const { Box, Icon, Button, Revealer } = Widget;
+const { Gravity } = imports.gi.Gdk;
+
+const SysTrayItem = (item) => item.id !== null ? Button({
+ className: 'bar-systray-item',
+ child: Icon({ hpack: 'center' }).bind('icon', item, 'icon'),
+ setup: (self) => self
+ .hook(item, (self) => self.tooltipMarkup = item['tooltip-markup'])
+ ,
+ onPrimaryClick: (_, event) => item.activate(event),
+ onSecondaryClick: (btn, event) => item.menu.popup_at_widget(btn, Gravity.SOUTH, Gravity.NORTH, null),
+}) : null;
+
+export const Tray = (props = {}) => {
+ const trayContent = Box({
+ className: 'margin-right-5 spacing-h-15',
+ setup: (self) => self
+ .hook(SystemTray, (self) => {
+ self.children = SystemTray.items.map(SysTrayItem);
+ self.show_all();
+ })
+ ,
+ });
+ const trayRevealer = Widget.Revealer({
+ revealChild: true,
+ transition: 'slide_left',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: trayContent,
+ });
+ return Box({
+ ...props,
+ children: [trayRevealer],
+ });
+}
diff --git a/modules/home-manager/ags/ags/modules/bar/normal/workspaces_hyprland.js b/modules/home-manager/ags/ags/modules/bar/normal/workspaces_hyprland.js
new file mode 100644
index 0000000..2f9b373
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/bar/normal/workspaces_hyprland.js
@@ -0,0 +1,224 @@
+const { GLib, Gdk, Gtk } = imports.gi;
+const Lang = imports.lang;
+const Cairo = imports.cairo;
+const Pango = imports.gi.Pango;
+const PangoCairo = imports.gi.PangoCairo;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+const { Box, DrawingArea, EventBox } = Widget;
+import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
+
+const dummyWs = Box({ className: 'bar-ws' }); // Not shown. Only for getting size props
+const dummyActiveWs = Box({ className: 'bar-ws bar-ws-active' }); // Not shown. Only for getting size props
+const dummyOccupiedWs = Box({ className: 'bar-ws bar-ws-occupied' }); // Not shown. Only for getting size props
+
+const mix = (value1, value2, perc) => {
+ return value1 * perc + value2 * (1 - perc);
+}
+
+const getFontWeightName = (weight) => {
+ switch (weight) {
+ case Pango.Weight.ULTRA_LIGHT:
+ return 'UltraLight';
+ case Pango.Weight.LIGHT:
+ return 'Light';
+ case Pango.Weight.NORMAL:
+ return 'Normal';
+ case Pango.Weight.BOLD:
+ return 'Bold';
+ case Pango.Weight.ULTRA_BOLD:
+ return 'UltraBold';
+ case Pango.Weight.HEAVY:
+ return 'Heavy';
+ default:
+ return 'Normal';
+ }
+}
+
+// Font size = workspace id
+const WorkspaceContents = (count = 10) => {
+ return DrawingArea({
+ className: 'bar-ws-container',
+ attribute: {
+ initialized: false,
+ workspaceMask: 0,
+ workspaceGroup: 0,
+ updateMask: (self) => {
+ const offset = Math.floor((Hyprland.active.workspace.id - 1) / count) * userOptions.workspaces.shown;
+ // if (self.attribute.initialized) return; // We only need this to run once
+ const workspaces = Hyprland.workspaces;
+ let workspaceMask = 0;
+ for (let i = 0; i < workspaces.length; i++) {
+ const ws = workspaces[i];
+ if (ws.id <= offset || ws.id > offset + count) continue; // Out of range, ignore
+ if (workspaces[i].windows > 0)
+ workspaceMask |= (1 << (ws.id - offset));
+ }
+ // console.log('Mask:', workspaceMask.toString(2));
+ self.attribute.workspaceMask = workspaceMask;
+ // self.attribute.initialized = true;
+ self.queue_draw();
+ },
+ toggleMask: (self, occupied, name) => {
+ if (occupied) self.attribute.workspaceMask |= (1 << parseInt(name));
+ else self.attribute.workspaceMask &= ~(1 << parseInt(name));
+ self.queue_draw();
+ },
+ },
+ setup: (area) => area
+ .hook(Hyprland.active.workspace, (self) => {
+ self.setCss(`font-size: ${(Hyprland.active.workspace.id - 1) % count + 1}px;`);
+ const previousGroup = self.attribute.workspaceGroup;
+ const currentGroup = Math.floor((Hyprland.active.workspace.id - 1) / count);
+ if (currentGroup !== previousGroup) {
+ self.attribute.updateMask(self);
+ self.attribute.workspaceGroup = currentGroup;
+ }
+ })
+ .hook(Hyprland, (self) => self.attribute.updateMask(self), 'notify::workspaces')
+ .on('draw', Lang.bind(area, (area, cr) => {
+ const offset = Math.floor((Hyprland.active.workspace.id - 1) / count) * userOptions.workspaces.shown;
+
+ const allocation = area.get_allocation();
+ const { width, height } = allocation;
+
+ const workspaceStyleContext = dummyWs.get_style_context();
+ const workspaceDiameter = workspaceStyleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
+ const workspaceRadius = workspaceDiameter / 2;
+ const workspaceFontSize = workspaceStyleContext.get_property('font-size', Gtk.StateFlags.NORMAL) / 4 * 3;
+ const workspaceFontFamily = workspaceStyleContext.get_property('font-family', Gtk.StateFlags.NORMAL);
+ const workspaceFontWeight = workspaceStyleContext.get_property('font-weight', Gtk.StateFlags.NORMAL);
+ const wsbg = workspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
+ const wsfg = workspaceStyleContext.get_property('color', Gtk.StateFlags.NORMAL);
+
+ const occupiedWorkspaceStyleContext = dummyOccupiedWs.get_style_context();
+ const occupiedbg = occupiedWorkspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
+ const occupiedfg = occupiedWorkspaceStyleContext.get_property('color', Gtk.StateFlags.NORMAL);
+
+ const activeWorkspaceStyleContext = dummyActiveWs.get_style_context();
+ const activebg = activeWorkspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
+ const activefg = activeWorkspaceStyleContext.get_property('color', Gtk.StateFlags.NORMAL);
+ area.set_size_request(workspaceDiameter * count, -1);
+ const widgetStyleContext = area.get_style_context();
+ const activeWs = widgetStyleContext.get_property('font-size', Gtk.StateFlags.NORMAL);
+
+ const activeWsCenterX = -(workspaceDiameter / 2) + (workspaceDiameter * activeWs);
+ const activeWsCenterY = height / 2;
+
+ // Font
+ const layout = PangoCairo.create_layout(cr);
+ const fontDesc = Pango.font_description_from_string(`${workspaceFontFamily[0]} ${getFontWeightName(workspaceFontWeight)} ${workspaceFontSize}`);
+ layout.set_font_description(fontDesc);
+ cr.setAntialias(Cairo.Antialias.BEST);
+ // Get kinda min radius for number indicators
+ layout.set_text("0".repeat(count.toString().length), -1);
+ const [layoutWidth, layoutHeight] = layout.get_pixel_size();
+ const indicatorRadius = Math.max(layoutWidth, layoutHeight) / 2 * 1.15; // smaller than sqrt(2)*radius
+ const indicatorGap = workspaceRadius - indicatorRadius;
+
+ for (let i = 1; i <= count; i++) {
+ if (area.attribute.workspaceMask & (1 << i)) {
+ // Draw bg highlight
+ cr.setSourceRGBA(occupiedbg.red, occupiedbg.green, occupiedbg.blue, occupiedbg.alpha);
+ const wsCenterX = -(workspaceRadius) + (workspaceDiameter * i);
+ const wsCenterY = height / 2;
+ if (!(area.attribute.workspaceMask & (1 << (i - 1)))) { // Left
+ cr.arc(wsCenterX, wsCenterY, workspaceRadius, 0.5 * Math.PI, 1.5 * Math.PI);
+ cr.fill();
+ }
+ else {
+ cr.rectangle(wsCenterX - workspaceRadius, wsCenterY - workspaceRadius, workspaceRadius, workspaceRadius * 2)
+ cr.fill();
+ }
+ if (!(area.attribute.workspaceMask & (1 << (i + 1)))) { // Right
+ cr.arc(wsCenterX, wsCenterY, workspaceRadius, -0.5 * Math.PI, 0.5 * Math.PI);
+ cr.fill();
+ }
+ else {
+ cr.rectangle(wsCenterX, wsCenterY - workspaceRadius, workspaceRadius, workspaceRadius * 2)
+ cr.fill();
+ }
+ }
+ }
+
+ // Draw active ws
+ cr.setSourceRGBA(activebg.red, activebg.green, activebg.blue, activebg.alpha);
+ cr.arc(activeWsCenterX, activeWsCenterY, indicatorRadius, 0, 2 * Math.PI);
+ cr.fill();
+
+ // Draw workspace numbers
+ for (let i = 1; i <= count; i++) {
+ const inactivecolors = area.attribute.workspaceMask & (1 << i) ? occupiedfg : wsfg;
+ if (i == activeWs) {
+ cr.setSourceRGBA(activefg.red, activefg.green, activefg.blue, activefg.alpha);
+ }
+ // Moving to
+ else if ((i == Math.floor(activeWs) && Hyprland.active.workspace.id < activeWs) || (i == Math.ceil(activeWs) && Hyprland.active.workspace.id > activeWs)) {
+ cr.setSourceRGBA(mix(activefg.red, inactivecolors.red, 1 - Math.abs(activeWs - i)), mix(activefg.green, inactivecolors.green, 1 - Math.abs(activeWs - i)), mix(activefg.blue, inactivecolors.blue, 1 - Math.abs(activeWs - i)), activefg.alpha);
+ }
+ // Moving from
+ else if ((i == Math.floor(activeWs) && Hyprland.active.workspace.id > activeWs) || (i == Math.ceil(activeWs) && Hyprland.active.workspace.id < activeWs)) {
+ cr.setSourceRGBA(mix(activefg.red, inactivecolors.red, 1 - Math.abs(activeWs - i)), mix(activefg.green, inactivecolors.green, 1 - Math.abs(activeWs - i)), mix(activefg.blue, inactivecolors.blue, 1 - Math.abs(activeWs - i)), activefg.alpha);
+ }
+ // Inactive
+ else
+ cr.setSourceRGBA(inactivecolors.red, inactivecolors.green, inactivecolors.blue, inactivecolors.alpha);
+
+ layout.set_text(`${i + offset}`, -1);
+ const [layoutWidth, layoutHeight] = layout.get_pixel_size();
+ const x = -workspaceRadius + (workspaceDiameter * i) - (layoutWidth / 2);
+ const y = (height - layoutHeight) / 2;
+ cr.moveTo(x, y);
+ PangoCairo.show_layout(cr, layout);
+ cr.stroke();
+ }
+ }))
+ ,
+ })
+}
+
+export default () => EventBox({
+ onScrollUp: () => Hyprland.messageAsync(`dispatch workspace -1`).catch(print),
+ onScrollDown: () => Hyprland.messageAsync(`dispatch workspace +1`).catch(print),
+ onMiddleClick: () => toggleWindowOnAllMonitors('osk'),
+ onSecondaryClick: () => App.toggleWindow('overview'),
+ attribute: {
+ clicked: false,
+ ws_group: 0,
+ },
+ child: Box({
+ homogeneous: true,
+ className: 'bar-group-margin',
+ children: [Box({
+ className: 'bar-group bar-group-standalone bar-group-pad',
+ css: 'min-width: 2px;',
+ children: [WorkspaceContents(userOptions.workspaces.shown)],
+ })]
+ }),
+ setup: (self) => {
+ self.add_events(Gdk.EventMask.POINTER_MOTION_MASK);
+ self.on('motion-notify-event', (self, event) => {
+ if (!self.attribute.clicked) return;
+ const [_, cursorX, cursorY] = event.get_coords();
+ const widgetWidth = self.get_allocation().width;
+ const wsId = Math.ceil(cursorX * userOptions.workspaces.shown / widgetWidth);
+ Utils.execAsync([`${App.configDir}/scripts/hyprland/workspace_action.sh`, 'workspace', `${wsId}`])
+ .catch(print);
+ })
+ self.on('button-press-event', (self, event) => {
+ if (event.get_button()[1] === 1) {
+ self.attribute.clicked = true;
+ const [_, cursorX, cursorY] = event.get_coords();
+ const widgetWidth = self.get_allocation().width;
+ const wsId = Math.ceil(cursorX * userOptions.workspaces.shown / widgetWidth);
+ Utils.execAsync([`${App.configDir}/scripts/hyprland/workspace_action.sh`, 'workspace', `${wsId}`])
+ .catch(print);
+ }
+ else if (event.get_button()[1] === 8) {
+ Hyprland.messageAsync(`dispatch togglespecialworkspace`).catch(print);
+ }
+ })
+ self.on('button-release-event', (self) => self.attribute.clicked = false);
+ }
+})
diff --git a/modules/home-manager/ags/ags/modules/cheatsheet/data_keybinds.js b/modules/home-manager/ags/ags/modules/cheatsheet/data_keybinds.js
new file mode 100644
index 0000000..5631c49
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/cheatsheet/data_keybinds.js
@@ -0,0 +1,122 @@
+export const keybindList = [[
+ {
+ "icon": "pin_drop",
+ "name": "Workspaces: navigation",
+ "binds": [
+ { "keys": ["", "+", "#"], "action": "Go to workspace #" },
+ { "keys": ["", "+", "S"], "action": "Toggle special workspace" },
+ { "keys": ["", "+", "(Scroll ↑↓)"], "action": "Go to workspace -1/+1" },
+ { "keys": ["Ctrl", "", "+", "←"], "action": "Go to workspace on the left" },
+ { "keys": ["Ctrl", "", "+", "→"], "action": "Go to workspace on the right" },
+ { "keys": ["", "+", "PageUp"], "action": "Go to workspace on the left" },
+ { "keys": ["", "+", "PageDown"], "action": "Go to workspace on the right" }
+ ],
+ "id": 1
+ },
+ {
+ "icon": "overview_key",
+ "name": "Workspaces: management",
+ "binds": [
+ { "keys": ["", "Alt", "+", "#"], "action": "Move window to workspace #" },
+ { "keys": ["", "Alt", "+", "S"], "action": "Move window to special workspace" },
+ { "keys": ["", "Alt", "+", "PageUp"], "action": "Move window to workspace on the left" },
+ { "keys": ["", "Alt", "+", "PageDown"], "action": "Move window to workspace on the right" }
+ ],
+ "id": 2
+ },
+ {
+ "icon": "move_group",
+ "name": "Windows",
+ "binds": [
+ { "keys": ["", "+", "←↑→↓"], "action": "Focus window in direction" },
+ { "keys": ["", "Shift", "+", "←↑→↓"], "action": "Swap window in direction" },
+ { "keys": ["", "+", ";"], "action": "Split ratio -" },
+ { "keys": ["", "+", "'"], "action": "Split ratio +" },
+ { "keys": ["", "+", "Lmb"], "action": "Move window" },
+ { "keys": ["", "+", "Rmb"], "action": "Resize window" },
+ { "keys": ["", "Alt", "+", "Space"], "action": "Float window" },
+ { "keys": ["", "+", "F"], "action": "Fullscreen" },
+ { "keys": ["", "Alt", "+", "F"], "action": "Fake fullscreen" }
+ ],
+ "id": 3
+ }
+],
+[
+ {
+ "icon": "widgets",
+ "name": "Widgets (AGS)",
+ "binds": [
+ { "keys": ["", "OR", "", "+", "Tab"], "action": "Toggle overview/launcher" },
+ { "keys": ["Ctrl", "", "+", "R"], "action": "Restart AGS" },
+ { "keys": ["", "+", "/"], "action": "Toggle this cheatsheet" },
+ { "keys": ["", "+", "N"], "action": "Toggle system sidebar" },
+ { "keys": ["", "+", "B", "OR", "", "+", "O"], "action": "Toggle utilities sidebar" },
+ { "keys": ["", "+", "K"], "action": "Toggle virtual keyboard" },
+ { "keys": ["Ctrl", "Alt", "+", "Del"], "action": "Power/Session menu" },
+
+ { "keys": ["Esc"], "action": "Exit a window" },
+ { "keys": ["rightCtrl"], "action": "Dismiss/close sidebar" },
+
+ { "keys": ["Ctrl", "", "+", "T"], "action": "Change wallpaper+colorscheme" },
+
+ // { "keys": ["", "+", "B"], "action": "Toggle left sidebar" },
+ // { "keys": ["", "+", "N"], "action": "Toggle right sidebar" },
+ // { "keys": ["", "+", "G"], "action": "Toggle volume mixer" },
+ // { "keys": ["", "+", "M"], "action": "Toggle useless audio visualizer" },
+ // { "keys": ["(right)Ctrl"], "action": "Dismiss notification & close menus" }
+ ],
+ "id": 4
+ },
+ {
+ "icon": "construction",
+ "name": "Utilities",
+ "binds": [
+ { "keys": ["PrtSc"], "action": "Screenshot >> clipboard" },
+ { "keys": ["Ctrl", "PrtSc"], "action": "Screenshot >> file + clipboard" },
+ { "keys": ["", "Shift", "+", "S"], "action": "Screen snip >> clipboard" },
+ { "keys": ["", "Shift", "+", "T"], "action": "Image to text >> clipboard" },
+ { "keys": ["", "Shift", "+", "C"], "action": "Color picker" },
+ { "keys": ["", "Alt", "+", "R"], "action": "Record region" },
+ { "keys": ["Ctrl", "Alt", "+", "R"], "action": "Record region with sound" },
+ { "keys": ["", "Shift", "Alt", "+", "R"], "action": "Record screen with sound" }
+ ],
+ "id": 5
+ },
+],
+[
+ {
+ "icon": "apps",
+ "name": "Apps",
+ "binds": [
+ { "keys": ["", "+", "T"], "action": "Launch terminal: foot" },
+ { "keys": ["", "+", "W"], "action": "Launch browser: Firefox" },
+ { "keys": ["", "+", "C"], "action": "Launch editor: vscode" },
+ { "keys": ["", "+", "X"], "action": "Launch editor: GNOME Text Editor" },
+ { "keys": ["", "+", "I"], "action": "Launch settings: GNOME Control center" }
+ ],
+ "id": 6
+ },
+ {
+ "icon": "keyboard",
+ "name": "Typing",
+ "binds": [
+ { "keys": ["", "+", "V"], "action": "Clipboard history >> clipboard" },
+ { "keys": ["", "+", "."], "action": "Emoji picker >> clipboard" },
+ ],
+ "id": 7
+ },
+ {
+ "icon": "terminal",
+ "name": "Launcher actions",
+ "binds": [
+ { "keys": [">raw"], "action": "Toggle mouse acceleration" },
+ { "keys": [">img"], "action": "Select wallpaper and generate colorscheme" },
+ { "keys": [">light"], "action": "Switch to light theme" },
+ { "keys": [">dark"], "action": "Switch to dark theme" },
+ { "keys": [">badapple"], "action": "Apply black n' white colorscheme" },
+ { "keys": [">color"], "action": "Pick acccent color" },
+ { "keys": [">todo"], "action": "Type something after that to add a To-do item" },
+ ],
+ "id": 8
+ }
+]];
diff --git a/modules/home-manager/ags/ags/modules/cheatsheet/data_periodictable.js b/modules/home-manager/ags/ags/modules/cheatsheet/data_periodictable.js
new file mode 100644
index 0000000..83121a2
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/cheatsheet/data_periodictable.js
@@ -0,0 +1,195 @@
+export const periodicTable = [
+ [
+ { name: 'Hydrogen', symbol: 'H', number: 1, weight: 1.01, type: 'nonmetal' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: 'Helium', symbol: 'He', number: 2, weight: 4.00, type: 'noblegas' },
+ ],
+ [
+ { name: 'Lithium', symbol: 'Li', number: 3, weight: 6.94, type: 'metal' },
+ { name: 'Beryllium', symbol: 'Be', number: 4, weight: 9.01, type: 'metal' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: 'Boron', symbol: 'B', number: 5, weight: 10.81, type: 'nonmetal' },
+ { name: 'Carbon', symbol: 'C', number: 6, weight: 12.01, type: 'nonmetal' },
+ { name: 'Nitrogen', symbol: 'N', number: 7, weight: 14.01, type: 'nonmetal' },
+ { name: 'Oxygen', symbol: 'O', number: 8, weight: 16, type: 'nonmetal' },
+ { name: 'Fluorine', symbol: 'F', number: 9, weight: 19, type: 'nonmetal' },
+ { name: 'Neon', symbol: 'Ne', number: 10, weight: 20.18, type: 'noblegas' },
+
+
+ ],
+ [
+ { name: 'Sodium', symbol: 'Na', number: 11, weight: 22.99, type: 'metal' },
+ { name: 'Magnesium', symbol: 'Mg', number: 12, weight: 24.31, type: 'metal' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: 'Aluminum', symbol: 'Al', number: 13, weight: 26.98, type: 'metal' },
+ { name: 'Silicon', symbol: 'Si', number: 14, weight: 28.09, type: 'nonmetal' },
+ { name: 'Phosphorus', symbol: 'P', number: 15, weight: 30.97, type: 'nonmetal' },
+ { name: 'Sulfur', symbol: 'S', number: 16, weight: 32.07, type: 'nonmetal' },
+ { name: 'Chlorine', symbol: 'Cl', number: 17, weight: 35.45, type: 'nonmetal' },
+ { name: 'Argon', symbol: 'Ar', number: 18, weight: 39.95, type: 'noblegas' },
+ ],
+ [
+ { name: 'Kalium', symbol: 'K', number: 19, weight: 39.098, type: 'metal' },
+ { name: 'Calcium', symbol: 'Ca', number: 20, weight: 40.078, type: 'metal' },
+ { name: 'Scandium', symbol: 'Sc', number: 21, weight: 44.956, type: 'metal' },
+ { name: 'Titanium', symbol: 'Ti', number: 22, weight: 47.87, type: 'metal' },
+ { name: 'Vanadium', symbol: 'V', number: 23, weight: 50.94, type: 'metal' },
+ { name: 'Chromium', symbol: 'Cr', number: 24, weight: 52, type: 'metal', icon: 'chromium-browser' },
+ { name: 'Manganese', symbol: 'Mn', number: 25, weight: 54.94, type: 'metal' },
+ { name: 'Iron', symbol: 'Fe', number: 26, weight: 55.85, type: 'metal' },
+ { name: 'Cobalt', symbol: 'Co', number: 27, weight: 58.93, type: 'metal' },
+ { name: 'Nickel', symbol: 'Ni', number: 28, weight: 58.69, type: 'metal' },
+ { name: 'Copper', symbol: 'Cu', number: 29, weight: 63.55, type: 'metal' },
+ { name: 'Zinc', symbol: 'Zn', number: 30, weight: 65.38, type: 'metal' },
+ { name: 'Gallium', symbol: 'Ga', number: 31, weight: 69.72, type: 'metal' },
+ { name: 'Germanium', symbol: 'Ge', number: 32, weight: 72.63, type: 'metal' },
+ { name: 'Arsenic', symbol: 'As', number: 33, weight: 74.92, type: 'nonmetal' },
+ { name: 'Selenium', symbol: 'Se', number: 34, weight: 78.96, type: 'nonmetal' },
+ { name: 'Bromine', symbol: 'Br', number: 35, weight: 79.904, type: 'nonmetal' },
+ { name: 'Krypton', symbol: 'Kr', number: 36, weight: 83.8, type: 'noblegas' },
+ ],
+ [
+ { name: 'Rubidium', symbol: 'Rb', number: 37, weight: 85.47, type: 'metal' },
+ { name: 'Strontium', symbol: 'Sr', number: 38, weight: 87.62, type: 'metal' },
+ { name: 'Yttrium', symbol: 'Y', number: 39, weight: 88.91, type: 'metal' },
+ { name: 'Zirconium', symbol: 'Zr', number: 40, weight: 91.22, type: 'metal' },
+ { name: 'Niobium', symbol: 'Nb', number: 41, weight: 92.91, type: 'metal' },
+ { name: 'Molybdenum', symbol: 'Mo', number: 42, weight: 95.94, type: 'metal' },
+ { name: 'Technetium', symbol: 'Tc', number: 43, weight: 98, type: 'metal' },
+ { name: 'Ruthenium', symbol: 'Ru', number: 44, weight: 101.07, type: 'metal' },
+ { name: 'Rhodium', symbol: 'Rh', number: 45, weight: 102.91, type: 'metal' },
+ { name: 'Palladium', symbol: 'Pd', number: 46, weight: 106.42, type: 'metal' },
+ { name: 'Silver', symbol: 'Ag', number: 47, weight: 107.87, type: 'metal' },
+ { name: 'Cadmium', symbol: 'Cd', number: 48, weight: 112.41, type: 'metal' },
+ { name: 'Indium', symbol: 'In', number: 49, weight: 114.82, type: 'metal' },
+ { name: 'Tin', symbol: 'Sn', number: 50, weight: 118.71, type: 'metal' },
+ { name: 'Antimony', symbol: 'Sb', number: 51, weight: 121.76, type: 'metal' },
+ { name: 'Tellurium', symbol: 'Te', number: 52, weight: 127.6, type: 'nonmetal' },
+ { name: 'Iodine', symbol: 'I', number: 53, weight: 126.9, type: 'nonmetal' },
+ { name: 'Xenon', symbol: 'Xe', number: 54, weight: 131.29, type: 'noblegas' },
+ ],
+ [
+ { name: 'Cesium', symbol: 'Cs', number: 55, weight: 132.91, type: 'metal' },
+ { name: 'Barium', symbol: 'Ba', number: 56, weight: 137.33, type: 'metal' },
+ { name: 'Lanthanum', symbol: 'La', number: 57, weight: 138.91, type: 'lanthanum' },
+ { name: 'Hafnium', symbol: 'Hf', number: 72, weight: 178.49, type: 'metal' },
+ { name: 'Tantalum', symbol: 'Ta', number: 73, weight: 180.95, type: 'metal' },
+ { name: 'Tungsten', symbol: 'W', number: 74, weight: 183.84, type: 'metal' },
+ { name: 'Rhenium', symbol: 'Re', number: 75, weight: 186.21, type: 'metal' },
+ { name: 'Osmium', symbol: 'Os', number: 76, weight: 190.23, type: 'metal' },
+ { name: 'Iridium', symbol: 'Ir', number: 77, weight: 192.22, type: 'metal' },
+ { name: 'Platinum', symbol: 'Pt', number: 78, weight: 195.09, type: 'metal' },
+ { name: 'Gold', symbol: 'Au', number: 79, weight: 196.97, type: 'metal' },
+ { name: 'Mercury', symbol: 'Hg', number: 80, weight: 200.59, type: 'metal' },
+ { name: 'Thallium', symbol: 'Tl', number: 81, weight: 204.38, type: 'metal' },
+ { name: 'Lead', symbol: 'Pb', number: 82, weight: 207.2, type: 'metal' },
+ { name: 'Bismuth', symbol: 'Bi', number: 83, weight: 208.98, type: 'metal' },
+ { name: 'Polonium', symbol: 'Po', number: 84, weight: 209, type: 'metal' },
+ { name: 'Astatine', symbol: 'At', number: 85, weight: 210, type: 'nonmetal' },
+ { name: 'Radon', symbol: 'Rn', number: 86, weight: 222, type: 'noblegas' },
+ ],
+ [
+ { name: 'Francium', symbol: 'Fr', number: 87, weight: 223, type: 'metal' },
+ { name: 'Radium', symbol: 'Ra', number: 88, weight: 226, type: 'metal' },
+ { name: 'Actinium', symbol: 'Ac', number: 89, weight: 227, type: 'actinium' },
+ { name: 'Rutherfordium', symbol: 'Rf', number: 104, weight: 267, type: 'metal' },
+ { name: 'Dubnium', symbol: 'Db', number: 105, weight: 268, type: 'metal' },
+ { name: 'Seaborgium', symbol: 'Sg', number: 106, weight: 271, type: 'metal' },
+ { name: 'Bohrium', symbol: 'Bh', number: 107, weight: 272, type: 'metal' },
+ { name: 'Hassium', symbol: 'Hs', number: 108, weight: 277, type: 'metal' },
+ { name: 'Meitnerium', symbol: 'Mt', number: 109, weight: 278, type: 'metal' },
+ { name: 'Darmstadtium', symbol: 'Ds', number: 110, weight: 281, type: 'metal' },
+ { name: 'Roentgenium', symbol: 'Rg', number: 111, weight: 280, type: 'metal' },
+ { name: 'Copernicium', symbol: 'Cn', number: 112, weight: 285, type: 'metal' },
+ { name: 'Nihonium', symbol: 'Nh', number: 113, weight: 286, type: 'metal' },
+ { name: 'Flerovium', symbol: 'Fl', number: 114, weight: 289, type: 'metal' },
+ { name: 'Moscovium', symbol: 'Mc', number: 115, weight: 290, type: 'metal' },
+ { name: 'Livermorium', symbol: 'Lv', number: 116, weight: 293, type: 'metal' },
+ { name: 'Tennessine', symbol: 'Ts', number: 117, weight: 294, type: 'metal' },
+ { name: 'Oganesson', symbol: 'Og', number: 118, weight: 294, type: 'noblegas' },
+ ],
+]
+
+export const series = [
+ [
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: 'Cerium', symbol: 'Ce', number: 58, weight: 140.12, type: 'lanthanum' },
+ { name: 'Praseodymium', symbol: 'Pr', number: 59, weight: 140.91, type: 'lanthanum' },
+ { name: 'Neodymium', symbol: 'Nd', number: 60, weight: 144.24, type: 'lanthanum' },
+ { name: 'Promethium', symbol: 'Pm', number: 61, weight: 145, type: 'lanthanum' },
+ { name: 'Samarium', symbol: 'Sm', number: 62, weight: 150.36, type: 'lanthanum' },
+ { name: 'Europium', symbol: 'Eu', number: 63, weight: 151.96, type: 'lanthanum' },
+ { name: 'Gadolinium', symbol: 'Gd', number: 64, weight: 157.25, type: 'lanthanum' },
+ { name: 'Terbium', symbol: 'Tb', number: 65, weight: 158.93, type: 'lanthanum' },
+ { name: 'Dysprosium', symbol: 'Dy', number: 66, weight: 162.5, type: 'lanthanum' },
+ { name: 'Holmium', symbol: 'Ho', number: 67, weight: 164.93, type: 'lanthanum' },
+ { name: 'Erbium', symbol: 'Er', number: 68, weight: 167.26, type: 'lanthanum' },
+ { name: 'Thulium', symbol: 'Tm', number: 69, weight: 168.93, type: 'lanthanum' },
+ { name: 'Ytterbium', symbol: 'Yb', number: 70, weight: 173.04, type: 'lanthanum' },
+ { name: 'Lutetium', symbol: 'Lu', number: 71, weight: 174.97, type: 'lanthanum' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ ],
+ [
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ { name: 'Thorium', symbol: 'Th', number: 90, weight: 232.04, type: 'actinium' },
+ { name: 'Protactinium', symbol: 'Pa', number: 91, weight: 231.04, type: 'actinium' },
+ { name: 'Uranium', symbol: 'U', number: 92, weight: 238.03, type: 'actinium' },
+ { name: 'Neptunium', symbol: 'Np', number: 93, weight: 237, type: 'actinium' },
+ { name: 'Plutonium', symbol: 'Pu', number: 94, weight: 244, type: 'actinium' },
+ { name: 'Americium', symbol: 'Am', number: 95, weight: 243, type: 'actinium' },
+ { name: 'Curium', symbol: 'Cm', number: 96, weight: 247, type: 'actinium' },
+ { name: 'Berkelium', symbol: 'Bk', number: 97, weight: 247, type: 'actinium' },
+ { name: 'Californium', symbol: 'Cf', number: 98, weight: 251, type: 'actinium' },
+ { name: 'Einsteinium', symbol: 'Es', number: 99, weight: 252, type: 'actinium' },
+ { name: 'Fermium', symbol: 'Fm', number: 100, weight: 257, type: 'actinium' },
+ { name: 'Mendelevium', symbol: 'Md', number: 101, weight: 258, type: 'actinium' },
+ { name: 'Nobelium', symbol: 'No', number: 102, weight: 259, type: 'actinium' },
+ { name: 'Lawrencium', symbol: 'Lr', number: 103, weight: 262, type: 'actinium' },
+ { name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
+ ],
+];
+
+export const niceTypes = {
+ 'metal': "Metal",
+ 'nonmetal': "Nonmetal",
+ 'noblegas': "Noble gas",
+ 'lanthanum': "Lanthanum",
+ 'actinium': "Actinium"
+}
diff --git a/modules/home-manager/ags/ags/modules/cheatsheet/keybinds.js b/modules/home-manager/ags/ags/modules/cheatsheet/keybinds.js
new file mode 100644
index 0000000..09cd100
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/cheatsheet/keybinds.js
@@ -0,0 +1,126 @@
+const { GLib, Gtk } = imports.gi;
+import App from "resource:///com/github/Aylur/ags/app.js";
+import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
+import Widget from "resource:///com/github/Aylur/ags/widget.js";
+import { IconTabContainer } from "../.commonwidgets/tabcontainer.js";
+const { Box, Label, Scrollable } = Widget;
+
+const HYPRLAND_KEYBIND_CONFIG_FILE = userOptions.cheatsheet.keybinds.configPath ?
+ userOptions.cheatsheet.keybinds.configPath : `${GLib.get_user_config_dir()}/hypr/hyprland/keybinds.conf`;
+const KEYBIND_SECTIONS_PER_PAGE = 3;
+const getKeybindList = () => {
+ let data = Utils.exec(`${App.configDir}/scripts/hyprland/get_keybinds.py --path ${HYPRLAND_KEYBIND_CONFIG_FILE}`);
+ if (data == "\"error\"") {
+ Utils.timeout(2000, () => Utils.execAsync(['notify-send',
+ 'Update path to keybinds',
+ 'Keybinds hyprland config file not found. Check your user options.',
+ '-a', 'ags',
+ ]).catch(print))
+ return { children: [] };
+ }
+ return JSON.parse(data);
+};
+const keybindList = getKeybindList();
+
+const keySubstitutions = {
+ "Super": "",
+ "mouse_up": "Scroll ↓", // ikr, weird
+ "mouse_down": "Scroll ↑", // trust me bro
+ "mouse:272": "LMB",
+ "mouse:273": "RMB",
+ "mouse:275": "MouseBack",
+ "Slash": "/",
+ "Hash": "#"
+}
+
+const substituteKey = (key) => {
+ return keySubstitutions[key] || key;
+}
+
+const Keybind = (keybindData, type) => { // type: either "keys" or "actions"
+ const Key = (key) => Label({ // Specific keys
+ vpack: 'center',
+ className: `${['OR', '+'].includes(key) ? 'cheatsheet-key-notkey' : 'cheatsheet-key'} txt-small`,
+ label: substituteKey(key),
+ });
+ const Action = (text) => Label({ // Binds
+ xalign: 0,
+ label: text,
+ className: "txt txt-small cheatsheet-action",
+ })
+ return Widget.Box({
+ className: "spacing-h-10 cheatsheet-bind-lineheight",
+ children: type == "keys" ? [
+ ...(keybindData.mods.length > 0 ? [
+ ...keybindData.mods.map(Key),
+ Key("+"),
+ ] : []),
+ Key(keybindData.key),
+ ] : [Action(keybindData.comment)],
+ })
+}
+
+const Section = (sectionData, scope) => {
+ const keys = Box({
+ vertical: true,
+ className: 'spacing-v-5',
+ children: sectionData.keybinds.map((data) => Keybind(data, "keys"))
+ })
+ const actions = Box({
+ vertical: true,
+ className: 'spacing-v-5',
+ children: sectionData.keybinds.map((data) => Keybind(data, "actions"))
+ })
+ const name = Label({
+ xalign: 0,
+ className: "cheatsheet-category-title txt margin-bottom-10",
+ label: sectionData.name,
+ })
+ const binds = Box({
+ className: 'spacing-h-10',
+ children: [
+ keys,
+ actions,
+ ]
+ })
+ const childrenSections = Box({
+ vertical: true,
+ className: 'spacing-v-15',
+ children: sectionData.children.map((data) => Section(data, scope + 1))
+ })
+ return Box({
+ vertical: true,
+ children: [
+ ...((sectionData.name && sectionData.name.length > 0) ? [name] : []),
+ Box({
+ className: 'spacing-v-10',
+ children: [
+ binds,
+ childrenSections,
+ ]
+ })
+ ]
+ })
+};
+
+export default () => {
+ const numOfTabs = Math.ceil(keybindList.children.length / KEYBIND_SECTIONS_PER_PAGE);
+ const keybindPages = Array.from({ length: numOfTabs }, (_, i) => ({
+ iconWidget: Label({
+ className: "txt txt-small",
+ label: `${i + 1}`,
+ }),
+ name: `${i + 1}`,
+ child: Box({
+ className: 'spacing-h-30',
+ children: keybindList.children.slice(
+ KEYBIND_SECTIONS_PER_PAGE * i, 0 + KEYBIND_SECTIONS_PER_PAGE * (i + 1),
+ ).map(data => Section(data, 1)),
+ }),
+ }));
+ return IconTabContainer({
+ iconWidgets: keybindPages.map((kbp) => kbp.iconWidget),
+ names: keybindPages.map((kbp) => kbp.name),
+ children: keybindPages.map((kbp) => kbp.child),
+ });
+};
diff --git a/modules/home-manager/ags/ags/modules/cheatsheet/main.js b/modules/home-manager/ags/ags/modules/cheatsheet/main.js
new file mode 100644
index 0000000..65fe7c9
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/cheatsheet/main.js
@@ -0,0 +1,146 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import { setupCursorHover } from "../.widgetutils/cursorhover.js";
+import PopupWindow from '../.widgethacks/popupwindow.js';
+import Keybinds from "./keybinds.js";
+import PeriodicTable from "./periodictable.js";
+import { ExpandingIconTabContainer } from '../.commonwidgets/tabcontainer.js';
+import { checkKeybind } from '../.widgetutils/keybind.js';
+import clickCloseRegion from '../.commonwidgets/clickcloseregion.js';
+
+const cheatsheets = [
+ {
+ name: 'Keybinds',
+ materialIcon: 'keyboard',
+ contentWidget: Keybinds,
+ },
+ {
+ name: 'Periodic table',
+ materialIcon: 'experiment',
+ contentWidget: PeriodicTable,
+ },
+];
+
+const CheatsheetHeader = () => Widget.CenterBox({
+ vertical: false,
+ startWidget: Widget.Box({}),
+ centerWidget: Widget.Box({
+ vertical: true,
+ className: "spacing-h-15",
+ children: [
+ Widget.Box({
+ hpack: 'center',
+ className: 'spacing-h-5 cheatsheet-title',
+ children: [
+ Widget.Label({
+ hpack: 'center',
+ css: 'margin-right: 0.682rem;',
+ className: 'txt-title',
+ label: 'Cheat sheet',
+ }),
+ Widget.Label({
+ vpack: 'center',
+ className: "cheatsheet-key txt-small",
+ label: "",
+ }),
+ Widget.Label({
+ vpack: 'center',
+ className: "cheatsheet-key-notkey txt-small",
+ label: "+",
+ }),
+ Widget.Label({
+ vpack: 'center',
+ className: "cheatsheet-key txt-small",
+ label: "/",
+ })
+ ]
+ }),
+ ]
+ }),
+ endWidget: Widget.Button({
+ vpack: 'start',
+ hpack: 'end',
+ className: "cheatsheet-closebtn icon-material txt txt-hugeass",
+ onClicked: () => {
+ closeWindowOnAllMonitors('cheatsheet');
+ },
+ child: Widget.Label({
+ className: 'icon-material txt txt-hugeass',
+ label: 'close'
+ }),
+ setup: setupCursorHover,
+ }),
+});
+
+const sheetContents = [];
+const SheetContent = (id) => {
+ sheetContents[id] = ExpandingIconTabContainer({
+ tabsHpack: 'center',
+ tabSwitcherClassName: 'sidebar-icontabswitcher',
+ transitionDuration: userOptions.animations.durationLarge * 1.4,
+ icons: cheatsheets.map((api) => api.materialIcon),
+ names: cheatsheets.map((api) => api.name),
+ children: cheatsheets.map((api) => api.contentWidget()),
+ onChange: (self, id) => {
+ self.shown = cheatsheets[id].name;
+ }
+ });
+ return sheetContents[id];
+}
+
+export default (id) => {
+ const sheets = SheetContent(id);
+ const widgetContent = Widget.Box({
+ vertical: true,
+ className: "cheatsheet-bg spacing-v-5",
+ children: [
+ CheatsheetHeader(),
+ sheets,
+ ]
+ });
+ return PopupWindow({
+ monitor: id,
+ name: `cheatsheet${id}`,
+ layer: 'overlay',
+ keymode: 'on-demand',
+ visible: false,
+ anchor: ['top', 'bottom', 'left', 'right'],
+ child: Widget.Box({
+ vertical: true,
+ children: [
+ clickCloseRegion({ name: 'cheatsheet' }),
+ Widget.Box({
+ children: [
+ clickCloseRegion({ name: 'cheatsheet' }),
+ widgetContent,
+ clickCloseRegion({ name: 'cheatsheet' }),
+ ]
+ }),
+ clickCloseRegion({ name: 'cheatsheet' }),
+ ],
+ setup: (self) => self.on('key-press-event', (widget, event) => { // Typing
+ // Whole sheet
+ if (checkKeybind(event, userOptions.keybinds.cheatsheet.nextTab))
+ sheetContents.forEach(tab => tab.nextTab())
+ else if (checkKeybind(event, userOptions.keybinds.cheatsheet.prevTab))
+ sheetContents.forEach(tab => tab.prevTab())
+ else if (checkKeybind(event, userOptions.keybinds.cheatsheet.cycleTab))
+ sheetContents.forEach(tab => tab.cycleTab())
+ // Keybinds
+ if (sheets.attribute.names[sheets.attribute.shown.value] == 'Keybinds') { // If Keybinds tab is focused
+ if (checkKeybind(event, userOptions.keybinds.cheatsheet.keybinds.nextTab)) {
+ sheetContents.forEach((sheet) => {
+ const toSwitchTab = sheet.attribute.children[sheet.attribute.shown.value];
+ toSwitchTab.nextTab();
+ })
+ }
+ else if (checkKeybind(event, userOptions.keybinds.cheatsheet.keybinds.prevTab)) {
+ sheetContents.forEach((sheet) => {
+ const toSwitchTab = sheet.attribute.children[sheet.attribute.shown.value];
+ toSwitchTab.prevTab();
+ })
+ }
+ }
+ })
+ })
+ });
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/cheatsheet/periodictable.js b/modules/home-manager/ags/ags/modules/cheatsheet/periodictable.js
new file mode 100644
index 0000000..92a85b9
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/cheatsheet/periodictable.js
@@ -0,0 +1,94 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import { niceTypes, periodicTable, series } from "./data_periodictable.js";
+const { Box, Button, Icon, Label, Revealer } = Widget;
+
+export default () => {
+ const ElementTile = (element) => {
+ return Box({
+ vertical: true,
+ tooltipText: element.electronConfig ? `${element.electronConfig}` : null,
+ className: `cheatsheet-periodictable-${element.type}`,
+ children: element.name == '' ? null : [
+ Box({
+ className: 'padding-left-8 padding-right-8 padding-top-8',
+ children: [
+ Label({
+ label: `${element.number}`,
+ className: "cheatsheet-periodictable-elementnum txt-tiny txt-bold",
+ }),
+ Box({ hexpand: true }),
+ Label({
+ label: `${element.weight}`,
+ className: "txt-smaller",
+ })
+ ]
+ }),
+ element.icon ? Icon({
+ icon: element.icon,
+ className: "txt-hugerass txt-bold",
+ }) : Label({
+ label: `${element.symbol}`,
+ className: "cheatsheet-periodictable-elementsymbol",
+ }),
+ Label({
+ label: `${element.name}`,
+ className: "txt-tiny",
+ })
+ ]
+ })
+ }
+ const BoardColor = (type) => Box({
+ className: 'spacing-h-5',
+ children: [
+ Box({
+ homogeneous: true,
+ className: `cheatsheet-periodictable-legend-color-wrapper`,
+ children: [Box({
+ className: `cheatsheet-periodictable-legend-color-${type}`,
+ })]
+ }),
+ Label({
+ label: `${niceTypes[type]}`,
+ className: "txt txt-small",
+ })
+ ]
+ })
+ const mainBoard = Box({
+ hpack: 'center',
+ vertical: true,
+ className: "spacing-v-3",
+ children: periodicTable.map((row, _) => Box({ // Rows
+ className: "spacing-h-5",
+ children: row.map((element, _) => ElementTile(element))
+ })),
+ });
+ const seriesBoard = Box({
+ hpack: 'center',
+ vertical: true,
+ className: "spacing-v-3",
+ children: series.map((row, _) => Box({ // Rows
+ className: "spacing-h-5",
+ children: row.map((element, _) => ElementTile(element))
+ })),
+ });
+ const legend = Box({
+ hpack: 'center',
+ className: 'spacing-h-20',
+ children: [
+ BoardColor('metal'),
+ BoardColor('nonmetal'),
+ BoardColor('noblegas'),
+ BoardColor('lanthanum'),
+ BoardColor('actinium'),
+ ]
+ })
+ return Box({
+ vertical: true,
+ className: 'spacing-v-20',
+ children: [
+ mainBoard,
+ seriesBoard,
+ legend
+ ]
+ })
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/crosshair/main.js b/modules/home-manager/ags/ags/modules/crosshair/main.js
new file mode 100644
index 0000000..390a971
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/crosshair/main.js
@@ -0,0 +1,22 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import { enableClickthrough } from "../.widgetutils/clickthrough.js";
+import { RoundedCorner } from "../.commonwidgets/cairo_roundedcorner.js";
+
+export default (monitor = 0, ) => {
+ return Widget.Window({
+ monitor,
+ name: `crosshair${monitor}`,
+ layer: 'overlay',
+ exclusivity: 'ignore',
+ visible: false,
+ child: Widget.Icon({
+ icon: 'crosshair-symbolic',
+ css: `
+ font-size: ${userOptions.gaming.crosshair.size}px;
+ color: ${userOptions.gaming.crosshair.color};
+ `,
+ }),
+ setup: enableClickthrough,
+ });
+}
+
diff --git a/modules/home-manager/ags/ags/modules/desktopbackground/data_quicklaunches.js b/modules/home-manager/ags/ags/modules/desktopbackground/data_quicklaunches.js
new file mode 100644
index 0000000..a5a9562
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/desktopbackground/data_quicklaunches.js
@@ -0,0 +1,14 @@
+export const quickLaunchItems = [
+ {
+ "name": "GitHub + Files×2",
+ "command": "github-desktop & nautilus --new-window & nautilus --new-window &"
+ },
+ {
+ "name": "Terminal×2",
+ "command": "foot & foot &"
+ },
+ {
+ "name": "Discord + Youtube + Github",
+ "command": "xdg-open 'https://discord.com/app' && xdg-open 'https://youtube.com/' && xdg-open 'https://github.com/' &"
+ },
+]
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/desktopbackground/main.js b/modules/home-manager/ags/ags/modules/desktopbackground/main.js
new file mode 100644
index 0000000..9600e15
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/desktopbackground/main.js
@@ -0,0 +1,24 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+
+import WallpaperImage from './wallpaper.js';
+import TimeAndLaunchesWidget from './timeandlaunches.js'
+import SystemWidget from './system.js'
+
+export default (monitor) => Widget.Window({
+ name: `desktopbackground${monitor}`,
+ // anchor: ['top', 'bottom', 'left', 'right'],
+ layer: 'background',
+ exclusivity: 'ignore',
+ visible: true,
+ child: Widget.Overlay({
+ child: WallpaperImage(monitor),
+ // child: Widget.Box({}),
+ overlays: [
+ TimeAndLaunchesWidget(),
+ SystemWidget(),
+ ],
+ setup: (self) => {
+ self.set_overlay_pass_through(self.get_children()[1], true);
+ },
+ }),
+});
diff --git a/modules/home-manager/ags/ags/modules/desktopbackground/system.js b/modules/home-manager/ags/ags/modules/desktopbackground/system.js
new file mode 100644
index 0000000..a65402b
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/desktopbackground/system.js
@@ -0,0 +1,161 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { execAsync, exec } = Utils;
+const { Box, EventBox, Label, Revealer, Overlay } = Widget;
+import { AnimatedCircProg } from "../.commonwidgets/cairo_circularprogress.js";
+import { MaterialIcon } from '../.commonwidgets/materialicon.js';
+
+const ResourceValue = (name, icon, interval, valueUpdateCmd, displayFunc, props = {}) => Box({
+ ...props,
+ className: 'bg-system-bg txt',
+ children: [
+ Revealer({
+ transition: 'slide_left',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Box({
+ vpack: 'center',
+ vertical: true,
+ className: 'margin-right-15',
+ children: [
+ Label({
+ xalign: 1,
+ className: 'txt-small txt',
+ label: `${name}`,
+ }),
+ Label({
+ xalign: 1,
+ className: 'titlefont txt-norm txt-onSecondaryContainer',
+ setup: (self) => self
+ .poll(interval, (label) => displayFunc(label))
+ ,
+ })
+ ]
+ })
+ }),
+ Overlay({
+ child: AnimatedCircProg({
+ className: 'bg-system-circprog',
+ extraSetup: (self) => self
+ .poll(interval, (self) => {
+ execAsync(['bash', '-c', `${valueUpdateCmd}`]).then((newValue) => {
+ self.css = `font-size: ${Math.round(newValue)}px;`
+ }).catch(print);
+ })
+ ,
+ }),
+ overlays: [
+ MaterialIcon(`${icon}`, 'hugeass'),
+ ],
+ setup: self => self.set_overlay_pass_through(self.get_children()[1], true),
+ }),
+ ]
+})
+
+const resources = Box({
+ vpack: 'fill',
+ vertical: true,
+ className: 'spacing-v-15',
+ children: [
+ ResourceValue('Memory', 'memory', 10000, `free | awk '/^Mem/ {printf("%.2f\\n", ($3/$2) * 100)}'`,
+ (label) => {
+ execAsync(['bash', '-c', `free -h | awk '/^Mem/ {print $3 " / " $2}' | sed 's/Gi/Gib/g'`])
+ .then((output) => {
+ label.label = `${output}`
+ }).catch(print);
+ }, { hpack: 'end' }),
+ ResourceValue('Swap', 'swap_horiz', 10000, `free | awk '/^Swap/ {if ($2 > 0) printf("%.2f\\n", ($3/$2) * 100); else print "0";}'`,
+ (label) => {
+ execAsync(['bash', '-c', `free -h | awk '/^Swap/ {if ($2 != "0") print $3 " / " $2; else print "No swap"}' | sed 's/Gi/Gib/g'`])
+ .then((output) => {
+ label.label = `${output}`
+ }).catch(print);
+ }, { hpack: 'end' }),
+ ResourceValue('Disk space', 'hard_drive_2', 3600000, `echo $(df --output=pcent / | tr -dc '0-9')`,
+ (label) => {
+ execAsync(['bash', '-c', `df -h --output=avail / | awk 'NR==2{print $1}'`])
+ .then((output) => {
+ label.label = `${output} available`
+ }).catch(print);
+ }, { hpack: 'end' }),
+ ]
+});
+
+const distroAndVersion = Box({
+ vertical: true,
+ children: [
+ Box({
+ hpack: 'end',
+ children: [
+ Label({
+ className: 'bg-distro-txt',
+ xalign: 0,
+ label: 'Hyping on ',
+ }),
+ Label({
+ className: 'bg-distro-name',
+ xalign: 0,
+ label: '',
+ setup: (label) => {
+ execAsync([`grep`, `-oP`, `PRETTY_NAME="\\K[^"]+`, `/etc/os-release`]).then(distro => {
+ label.label = distro;
+ }).catch(print);
+ },
+ }),
+ ]
+ }),
+ Box({
+ hpack: 'end',
+ children: [
+ Label({
+ className: 'bg-distro-txt',
+ xalign: 0,
+ label: 'with ',
+ }),
+ Label({
+ className: 'bg-distro-name',
+ xalign: 0,
+ label: 'An environment idk',
+ setup: (label) => {
+ // hyprctl will return unsuccessfully if Hyprland isn't running
+ execAsync([`bash`, `-c`, `hyprctl version | grep -oP "Tag: v\\K\\d+\\.\\d+\\.\\d+"`]).then(version => {
+ label.label = `Hyprland ${version}`;
+ }).catch(() => execAsync([`bash`, `-c`, `sway -v | cut -d'-' -f1 | sed 's/sway version /v/'`]).then(version => {
+ label.label = `Sway ${version}`;
+ }).catch(print));
+ },
+ }),
+ ]
+ })
+ ]
+})
+
+export default () => Box({
+ hpack: 'end',
+ vpack: 'end',
+ children: [
+ EventBox({
+ child: Box({
+ hpack: 'end',
+ vpack: 'end',
+ className: 'bg-distro-box spacing-v-20',
+ vertical: true,
+ children: [
+ resources,
+ distroAndVersion,
+ ]
+ }),
+ onPrimaryClickRelease: () => {
+ const kids = resources.get_children();
+ for (let i = 0; i < kids.length; i++) {
+ const child = kids[i];
+ const firstChild = child.get_children()[0];
+ firstChild.revealChild = !firstChild.revealChild;
+ }
+
+ },
+ })
+ ],
+})
+
+
+
diff --git a/modules/home-manager/ags/ags/modules/desktopbackground/timeandlaunches.js b/modules/home-manager/ags/ags/modules/desktopbackground/timeandlaunches.js
new file mode 100644
index 0000000..f0e0214
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/desktopbackground/timeandlaunches.js
@@ -0,0 +1,74 @@
+const { GLib } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import Service from 'resource:///com/github/Aylur/ags/service.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+
+import Variable from 'resource:///com/github/Aylur/ags/variable.js';
+const { execAsync, exec } = Utils;
+const { Box, Label, Button, Revealer, EventBox } = Widget;
+import { setupCursorHover } from '../.widgetutils/cursorhover.js';
+import { quickLaunchItems } from './data_quicklaunches.js'
+
+const TimeAndDate = () => Box({
+ vertical: true,
+ className: 'spacing-v--5',
+ children: [
+ Label({
+ className: 'bg-time-clock',
+ xalign: 0,
+ label: GLib.DateTime.new_now_local().format(userOptions.time.format),
+ setup: (self) => self.poll(userOptions.time.interval, label => {
+ label.label = GLib.DateTime.new_now_local().format(userOptions.time.format);
+ }),
+ }),
+ Label({
+ className: 'bg-time-date',
+ xalign: 0,
+ label: GLib.DateTime.new_now_local().format(userOptions.time.dateFormatLong),
+ setup: (self) => self.poll(userOptions.time.dateInterval, (label) => {
+ label.label = GLib.DateTime.new_now_local().format(userOptions.time.dateFormatLong);
+ }),
+ }),
+ ]
+})
+
+const QuickLaunches = () => Box({
+ vertical: true,
+ className: 'spacing-v-10',
+ children: [
+ Label({
+ xalign: 0,
+ className: 'bg-quicklaunch-title',
+ label: 'Quick Launches',
+ }),
+ Box({
+ hpack: 'start',
+ className: 'spacing-h-5',
+ children: quickLaunchItems.map((item, i) => Button({
+ onClicked: () => {
+ execAsync(['bash', '-c', `${item["command"]}`]).catch(print);
+ },
+ className: 'bg-quicklaunch-btn',
+ child: Label({
+ label: `${item["name"]}`,
+ }),
+ setup: (self) => {
+ setupCursorHover(self);
+ }
+ })),
+ })
+ ]
+})
+
+export default () => Box({
+ hpack: 'start',
+ vpack: 'end',
+ vertical: true,
+ className: 'bg-time-box spacing-h--10',
+ children: [
+ TimeAndDate(),
+ // QuickLaunches(),
+ ],
+})
+
diff --git a/modules/home-manager/ags/ags/modules/desktopbackground/wallpaper.js b/modules/home-manager/ags/ags/modules/desktopbackground/wallpaper.js
new file mode 100644
index 0000000..779bb23
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/desktopbackground/wallpaper.js
@@ -0,0 +1,119 @@
+const { Gdk, GdkPixbuf, Gio, GLib, Gtk } = imports.gi;
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { exec, execAsync } = Utils;
+const { Box, Button, Label, Stack } = Widget;
+import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
+
+import Wallpaper from '../../services/wallpaper.js';
+import { setupCursorHover } from '../.widgetutils/cursorhover.js';
+import { clamp } from '../.miscutils/mathfuncs.js';
+import { monitors } from '../.commondata/hyprlanddata.js';
+
+const DISABLE_AGS_WALLPAPER = true;
+
+const SWITCHWALL_SCRIPT_PATH = `${App.configDir}/scripts/color_generation/switchwall.sh`;
+const WALLPAPER_ZOOM_SCALE = 1.25; // For scrolling when we switch workspace
+const MAX_WORKSPACES = 10;
+
+export default (monitor = 0) => {
+ const WALLPAPER_OFFSCREEN_X = (WALLPAPER_ZOOM_SCALE - 1) * monitors[monitor].width;
+ const WALLPAPER_OFFSCREEN_Y = (WALLPAPER_ZOOM_SCALE - 1) * monitors[monitor].height;
+ const wallpaperImage = Widget.DrawingArea({
+ attribute: {
+ pixbuf: undefined,
+ workspace: 1,
+ sideleft: 0,
+ sideright: 0,
+ updatePos: (self) => {
+ self.setCss(`font-size: ${self.attribute.workspace - self.attribute.sideleft + self.attribute.sideright}px;`)
+ },
+ },
+ className: 'bg-wallpaper-transition',
+ setup: (self) => {
+ self.set_size_request(monitors[monitor].width, monitors[monitor].height);
+ self
+ // TODO: reduced updates using timeouts to reduce lag
+ // .hook(Hyprland.active.workspace, (self) => {
+ // self.attribute.workspace = Hyprland.active.workspace.id
+ // self.attribute.updatePos(self);
+ // })
+ // .hook(App, (box, name, visible) => { // Update on open
+ // if (self.attribute[name] === undefined) return;
+ // self.attribute[name] = (visible ? 1 : 0);
+ // self.attribute.updatePos(self);
+ // })
+ .on('draw', (self, cr) => {
+ if (!self.attribute.pixbuf) return;
+ const styleContext = self.get_style_context();
+ const workspace = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL);
+ // Draw
+ Gdk.cairo_set_source_pixbuf(cr, self.attribute.pixbuf,
+ -(WALLPAPER_OFFSCREEN_X / (MAX_WORKSPACES + 1) * (clamp(workspace, 0, MAX_WORKSPACES + 1))),
+ -WALLPAPER_OFFSCREEN_Y / 2);
+ cr.paint();
+ })
+ .hook(Wallpaper, (self) => {
+ if (DISABLE_AGS_WALLPAPER) return;
+ const wallPath = Wallpaper.get(monitor);
+ if (!wallPath || wallPath === "") return;
+ self.attribute.pixbuf = GdkPixbuf.Pixbuf.new_from_file(wallPath);
+
+ const scale_x = monitors[monitor].width * WALLPAPER_ZOOM_SCALE / self.attribute.pixbuf.get_width();
+ const scale_y = monitors[monitor].height * WALLPAPER_ZOOM_SCALE / self.attribute.pixbuf.get_height();
+ const scale_factor = Math.max(scale_x, scale_y);
+
+ self.attribute.pixbuf = self.attribute.pixbuf.scale_simple(
+ Math.round(self.attribute.pixbuf.get_width() * scale_factor),
+ Math.round(self.attribute.pixbuf.get_height() * scale_factor),
+ GdkPixbuf.InterpType.BILINEAR
+ );
+ self.queue_draw();
+ }, 'updated');
+ ;
+ }
+ ,
+ });
+ const wallpaperPrompt = Box({
+ hpack: 'center',
+ vpack: 'center',
+ vertical: true,
+ className: 'spacing-v-10',
+ children: [
+ Label({
+ hpack: 'center',
+ justification: 'center',
+ className: 'txt-large',
+ label: `No wallpaper loaded.\nAn image ≥ ${monitors[monitor].width * WALLPAPER_ZOOM_SCALE} × ${monitors[monitor].height * WALLPAPER_ZOOM_SCALE} is recommended.`,
+ }),
+ Button({
+ hpack: 'center',
+ className: 'btn-primary',
+ label: `Select one`,
+ setup: setupCursorHover,
+ onClicked: (self) => Utils.execAsync([SWITCHWALL_SCRIPT_PATH]).catch(print),
+ }),
+ ]
+ });
+ const stack = Stack({
+ transition: 'crossfade',
+ transitionDuration: userOptions.animations.durationLarge,
+ children: {
+ 'disabled': Box({}),
+ 'image': wallpaperImage,
+ 'prompt': wallpaperPrompt,
+ },
+ setup: (self) => self
+ .hook(Wallpaper, (self) => {
+ if (DISABLE_AGS_WALLPAPER) {
+ self.shown = 'disabled';
+ return;
+ }
+ const wallPath = Wallpaper.get(monitor);
+ self.shown = ((wallPath && wallPath != "") ? 'image' : 'prompt');
+ }, 'updated')
+ ,
+ })
+ return stack;
+ // return wallpaperImage;
+}
diff --git a/modules/home-manager/ags/ags/modules/dock/dock.js b/modules/home-manager/ags/ags/modules/dock/dock.js
new file mode 100755
index 0000000..8e31841
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/dock/dock.js
@@ -0,0 +1,300 @@
+const { Gtk, GLib } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { EventBox, Button } = Widget;
+
+import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
+import Applications from 'resource:///com/github/Aylur/ags/service/applications.js';
+const { execAsync, exec } = Utils;
+const { Box, Revealer } = Widget;
+import { setupCursorHover } from '../.widgetutils/cursorhover.js';
+import { getAllFiles, searchIcons } from './icons.js'
+import { MaterialIcon } from '../.commonwidgets/materialicon.js';
+import { substitute } from '../.miscutils/icons.js';
+
+const icon_files = userOptions.icons.searchPaths.map(e => getAllFiles(e)).flat(1)
+
+let isPinned = false
+let cachePath = new Map()
+
+let timers = []
+
+function clearTimes() {
+ timers.forEach(e => GLib.source_remove(e))
+ timers = []
+}
+
+function ExclusiveWindow(client) {
+ const fn = [
+ (client) => !(client !== null && client !== undefined),
+ // Jetbrains
+ (client) => client.title.includes("win"),
+ // Vscode
+ (client) => client.title === '' && client.class === ''
+ ]
+
+ for (const item of fn) { if (item(client)) { return true } }
+ return false
+}
+
+const focus = ({ address }) => Utils.execAsync(`hyprctl dispatch focuswindow address:${address}`).catch(print);
+
+const DockSeparator = (props = {}) => Box({
+ ...props,
+ className: 'dock-separator',
+})
+
+const PinButton = () => Widget.Button({
+ className: 'dock-app-btn dock-app-btn-animate',
+ tooltipText: 'Pin Dock',
+ child: Widget.Box({
+ homogeneous: true,
+ className: 'dock-app-icon txt',
+ child: MaterialIcon('push_pin', 'hugeass')
+ }),
+ onClicked: (self) => {
+ isPinned = !isPinned
+ self.className = `${isPinned ? "pinned-dock-app-btn" : "dock-app-btn animate"} dock-app-btn-animate`
+ },
+ setup: setupCursorHover,
+})
+
+const LauncherButton = () => Widget.Button({
+ className: 'dock-app-btn dock-app-btn-animate',
+ tooltipText: 'Open launcher',
+ child: Widget.Box({
+ homogeneous: true,
+ className: 'dock-app-icon txt',
+ child: MaterialIcon('apps', 'hugerass')
+ }),
+ onClicked: (self) => {
+ App.toggleWindow('overview');
+ },
+ setup: setupCursorHover,
+})
+
+const AppButton = ({ icon, ...rest }) => Widget.Revealer({
+ attribute: {
+ 'workspace': 0
+ },
+ revealChild: false,
+ transition: 'slide_right',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Widget.Button({
+ ...rest,
+ className: 'dock-app-btn dock-app-btn-animate',
+ child: Widget.Box({
+ child: Widget.Overlay({
+ child: Widget.Box({
+ homogeneous: true,
+ className: 'dock-app-icon',
+ child: Widget.Icon({
+ icon: icon,
+ }),
+ }),
+ overlays: [Widget.Box({
+ class_name: 'indicator',
+ vpack: 'end',
+ hpack: 'center',
+ })],
+ }),
+ }),
+ setup: (button) => {
+ setupCursorHover(button);
+ }
+ })
+});
+
+const Taskbar = (monitor) => Widget.Box({
+ className: 'dock-apps',
+ attribute: {
+ monitor: monitor,
+ 'map': new Map(),
+ 'clientSortFunc': (a, b) => {
+ return a.attribute.workspace > b.attribute.workspace;
+ },
+ 'update': (box, monitor) => {
+ for (let i = 0; i < Hyprland.clients.length; i++) {
+ const client = Hyprland.clients[i];
+ if (client["pid"] == -1) return;
+ const appClass = substitute(client.class);
+ // for (const appName of userOptions.dock.pinnedApps) {
+ // if (appClass.includes(appName.toLowerCase()))
+ // return null;
+ // }
+ let appClassLower = appClass.toLowerCase()
+ let path = ''
+ if (cachePath[appClassLower]) { path = cachePath[appClassLower] }
+ else {
+ path = searchIcons(appClass.toLowerCase(), icon_files)
+ cachePath[appClassLower] = path
+ }
+ if (path === '') { path = substitute(appClass) }
+ const newButton = AppButton({
+ icon: path,
+ tooltipText: `${client.title} (${appClass})`,
+ onClicked: () => focus(client),
+ });
+ newButton.attribute.workspace = client.workspace.id;
+ newButton.revealChild = true;
+ box.attribute.map.set(client.address, newButton);
+ }
+ box.children = Array.from(box.attribute.map.values());
+ },
+ 'add': (box, address, monitor) => {
+ if (!address) { // First active emit is undefined
+ box.attribute.update(box);
+ return;
+ }
+ const newClient = Hyprland.clients.find(client => {
+ return client.address == address;
+ });
+ if (ExclusiveWindow(newClient)) { return }
+ let appClass = newClient.class
+ let appClassLower = appClass.toLowerCase()
+ let path = ''
+ if (cachePath[appClassLower]) { path = cachePath[appClassLower] }
+ else {
+ path = searchIcons(appClassLower, icon_files)
+ cachePath[appClassLower] = path
+ }
+ if (path === '') { path = substitute(appClass) }
+ const newButton = AppButton({
+ icon: path,
+ tooltipText: `${newClient.title} (${appClass})`,
+ onClicked: () => focus(newClient),
+ })
+ newButton.attribute.workspace = newClient.workspace.id;
+ box.attribute.map.set(address, newButton);
+ box.children = Array.from(box.attribute.map.values());
+ newButton.revealChild = true;
+ },
+ 'remove': (box, address) => {
+ if (!address) return;
+
+ const removedButton = box.attribute.map.get(address);
+ if (!removedButton) return;
+ removedButton.revealChild = false;
+
+ Utils.timeout(userOptions.animations.durationLarge, () => {
+ removedButton.destroy();
+ box.attribute.map.delete(address);
+ box.children = Array.from(box.attribute.map.values());
+ })
+ },
+ },
+ setup: (self) => {
+ self.hook(Hyprland, (box, address) => box.attribute.add(box, address, self.monitor), 'client-added')
+ .hook(Hyprland, (box, address) => box.attribute.remove(box, address, self.monitor), 'client-removed')
+ Utils.timeout(100, () => self.attribute.update(self));
+ },
+});
+
+const PinnedApps = () => Widget.Box({
+ class_name: 'dock-apps',
+ homogeneous: true,
+ children: userOptions.dock.pinnedApps
+ .map(term => ({ app: Applications.query(term)?.[0], term }))
+ .filter(({ app }) => app)
+ .map(({ app, term = true }) => {
+ const newButton = AppButton({
+ // different icon, emm...
+ icon: userOptions.dock.searchPinnedAppIcons ?
+ searchIcons(app.name, icon_files) :
+ app.icon_name,
+ onClicked: () => {
+ for (const client of Hyprland.clients) {
+ if (client.class.toLowerCase().includes(term))
+ return focus(client);
+ }
+
+ app.launch();
+ },
+ onMiddleClick: () => app.launch(),
+ tooltipText: app.name,
+ setup: (self) => {
+ self.revealChild = true;
+ self.hook(Hyprland, button => {
+ const running = Hyprland.clients
+ .find(client => client.class.toLowerCase().includes(term)) || false;
+
+ button.toggleClassName('notrunning', !running);
+ button.toggleClassName('focused', Hyprland.active.client.address == running.address);
+ button.set_tooltip_text(running ? running.title : app.name);
+ }, 'notify::clients')
+ },
+ })
+ newButton.revealChild = true;
+ return newButton;
+ }),
+});
+
+export default (monitor = 0) => {
+ const dockContent = Box({
+ className: 'dock-bg spacing-h-5',
+ children: [
+ PinButton(),
+ PinnedApps(),
+ DockSeparator(),
+ Taskbar(),
+ LauncherButton(),
+ ]
+ })
+ const dockRevealer = Revealer({
+ attribute: {
+ 'updateShow': self => { // I only use mouse to resize. I don't care about keyboard resize if that's a thing
+ if (userOptions.dock.monitorExclusivity)
+ self.revealChild = Hyprland.active.monitor.id === monitor;
+ else
+ self.revealChild = true;
+
+ return self.revealChild
+ }
+ },
+ revealChild: false,
+ transition: 'slide_up',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: dockContent,
+ setup: (self) => {
+ const callback = (self, trigger) => {
+ if (!userOptions.dock.trigger.includes(trigger)) return
+ const flag = self.attribute.updateShow(self)
+
+ if (flag) clearTimes();
+
+ const hidden = userOptions.dock.autoHide.find(e => e["trigger"] === trigger)
+
+ if (hidden) {
+ let id = Utils.timeout(hidden.interval, () => {
+ if (!isPinned) { self.revealChild = false }
+ timers = timers.filter(e => e !== id)
+ })
+ timers.push(id)
+ }
+ }
+
+ self
+ // .hook(Hyprland, (self) => self.attribute.updateShow(self))
+ .hook(Hyprland.active.workspace, self => callback(self, "workspace-active"))
+ .hook(Hyprland.active.client, self => callback(self, "client-active"))
+ .hook(Hyprland, self => callback(self, "client-added"), "client-added")
+ .hook(Hyprland, self => callback(self, "client-removed"), "client-removed")
+ },
+ })
+ return EventBox({
+ onHover: () => {
+ dockRevealer.revealChild = true;
+ clearTimes()
+ },
+ child: Box({
+ homogeneous: true,
+ css: `min-height: ${userOptions.dock.hiddenThickness}px;`,
+ children: [dockRevealer],
+ }),
+ setup: self => self.on("leave-notify-event", () => {
+ if (!isPinned) dockRevealer.revealChild = false;
+ clearTimes()
+ })
+ })
+}
diff --git a/modules/home-manager/ags/ags/modules/dock/icons.js b/modules/home-manager/ags/ags/modules/dock/icons.js
new file mode 100644
index 0000000..60f01a3
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/dock/icons.js
@@ -0,0 +1,63 @@
+const { Gio, GLib } = imports.gi
+
+const exists = (path) => Gio.File.new_for_path(path).query_exists(null);
+
+export const levenshteinDistance = (a, b) => {
+ if (!a.length) { return b.length }
+ if (!b.length) { return a.length }
+
+ let f = Array.from(new Array(a.length + 1),
+ () => new Array(b.length + 1).fill(0))
+
+ for (let i = 0; i <= b.length; i++) { f[0][i] = i; }
+ for (let i = 0; i <= a.length; i++) { f[i][0] = i; }
+
+ for (let i = 1; i <= a.length; i++) {
+ for (let j = 1; j <= b.length; j++) {
+ if (a.charAt(i - 1) === b.charAt(j - 1)) {
+ f[i][j] = f[i-1][j-1]
+ } else {
+ f[i][j] = Math.min(f[i-1][j-1], Math.min(f[i][j-1], f[i-1][j])) + 1
+ }
+ }
+ }
+
+ return f[a.length][b.length]
+}
+
+export const getAllFiles = (dir, files = []) => {
+ if (!exists(dir)) { return [] }
+ const file = Gio.File.new_for_path(dir);
+ const enumerator = file.enumerate_children('standard::name,standard::type',
+ Gio.FileQueryInfoFlags.NONE, null);
+
+ for (const info of enumerator) {
+ if (info.get_file_type() === Gio.FileType.DIRECTORY) {
+ files.push(getAllFiles(`${dir}/${info.get_name()}`))
+ } else {
+ files.push(`${dir}/${info.get_name()}`)
+ }
+ }
+
+ return files.flat(1);
+}
+
+export const searchIcons = (appClass, files) => {
+ appClass = appClass.toLowerCase()
+
+ if (!files.length) { return "" }
+
+ let appro = 0x3f3f3f3f
+ let path = ""
+
+ for (const item of files) {
+ let score = levenshteinDistance(item.split("/").pop().toLowerCase().split(".")[0], appClass)
+
+ if (score < appro) {
+ appro = score
+ path = item
+ }
+ }
+
+ return path
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/dock/main.js b/modules/home-manager/ags/ags/modules/dock/main.js
new file mode 100644
index 0000000..77d394d
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/dock/main.js
@@ -0,0 +1,12 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import Dock from './dock.js';
+
+export default (monitor = 0) => Widget.Window({
+ monitor,
+ name: `dock${monitor}`,
+ layer: userOptions.dock.layer,
+ anchor: ['bottom'],
+ exclusivity: 'normal',
+ visible: true,
+ child: Dock(monitor),
+});
diff --git a/modules/home-manager/ags/ags/modules/indicators/colorscheme.js b/modules/home-manager/ags/ags/modules/indicators/colorscheme.js
new file mode 100644
index 0000000..b91375b
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/indicators/colorscheme.js
@@ -0,0 +1,264 @@
+const { Gio, GLib } = imports.gi;
+import Variable from 'resource:///com/github/Aylur/ags/variable.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import { ConfigToggle, ConfigMulipleSelection } from '../.commonwidgets/configwidgets.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { execAsync } = Utils;
+import { setupCursorHover } from '../.widgetutils/cursorhover.js';
+import { showColorScheme } from '../../variables.js';
+import { MaterialIcon } from '../.commonwidgets/materialicon.js';
+import { darkMode } from '../.miscutils/system.js';
+
+const ColorBox = ({
+ name = 'Color',
+ ...rest
+}) => Widget.Box({
+ ...rest,
+ homogeneous: true,
+ children: [
+ Widget.Label({
+ label: `${name}`,
+ })
+ ]
+})
+
+const ColorSchemeSettingsRevealer = () => {
+ const headerButtonIcon = MaterialIcon('expand_more', 'norm');
+ const header = Widget.Button({
+ className: 'osd-settings-btn-arrow',
+ onClicked: () => {
+ content.revealChild = !content.revealChild;
+ headerButtonIcon.label = content.revealChild ? 'expand_less' : 'expand_more';
+ },
+ setup: setupCursorHover,
+ hpack: 'end',
+ child: headerButtonIcon,
+ });
+ const content = Widget.Revealer({
+ revealChild: false,
+ transition: 'slide_down',
+ transitionDuration: 200,
+ child: ColorSchemeSettings(),
+ setup: (self) => self.hook(isHoveredColorschemeSettings, (revealer) => {
+ if (isHoveredColorschemeSettings.value == false) {
+ setTimeout(() => {
+ if (isHoveredColorschemeSettings.value == false)
+ revealer.revealChild = false;
+ headerButtonIcon.label = 'expand_more';
+ }, 1500);
+ }
+ }),
+ });
+ return Widget.EventBox({
+ onHover: (self) => {
+ isHoveredColorschemeSettings.setValue(true);
+ },
+ onHoverLost: (self) => {
+ isHoveredColorschemeSettings.setValue(false);
+ },
+ child: Widget.Box({
+ vertical: true,
+ children: [
+ header,
+ content,
+ ]
+ }),
+ });
+}
+
+function calculateSchemeInitIndex(optionsArr, searchValue = 'vibrant') {
+ if (searchValue == '')
+ searchValue = 'vibrant';
+ const flatArray = optionsArr.flatMap(subArray => subArray);
+ const result = flatArray.findIndex(element => element.value === searchValue);
+ const rowIndex = Math.floor(result / optionsArr[0].length);
+ const columnIndex = result % optionsArr[0].length;
+ return [rowIndex, columnIndex];
+}
+
+const schemeOptionsArr = [
+ [
+ { name: 'Tonal Spot', value: 'tonalspot' },
+ { name: 'Fruit Salad', value: 'fruitsalad' },
+ { name: 'Fidelity', value: 'fidelity' },
+ { name: 'Rainbow', value: 'rainbow' },
+ ],
+ [
+ { name: 'Neutral', value: 'neutral' },
+ { name: 'Monochrome', value: 'monochrome' },
+ { name: 'Expressive', value: 'expressive' },
+ { name: 'Vibrant', value: 'vibrant' },
+ ],
+ [
+ { name: 'Vibrant+', value: 'morevibrant' },
+ ],
+ //[
+ // { name: 'Content', value: 'content' },
+ //]
+];
+
+const LIGHTDARK_FILE_LOCATION = `${GLib.get_user_state_dir()}/ags/user/colormode.txt`;
+const initTransparency = Utils.exec(`bash -c "sed -n \'2p\' ${LIGHTDARK_FILE_LOCATION}"`);
+const initTransparencyVal = (initTransparency == "transparent") ? 1 : 0;
+const initScheme = Utils.exec(`bash -c "sed -n \'3p\' ${LIGHTDARK_FILE_LOCATION}"`);
+const initSchemeIndex = calculateSchemeInitIndex(schemeOptionsArr, initScheme);
+
+const ColorSchemeSettings = () => Widget.Box({
+ className: 'osd-colorscheme-settings spacing-v-5 margin-20',
+ vertical: true,
+ vpack: 'center',
+ children: [
+ Widget.Box({
+ vertical: true,
+ children: [
+ Widget.Label({
+ xalign: 0,
+ className: 'txt-norm titlefont txt',
+ label: 'Options',
+ hpack: 'center',
+ }),
+ //////////////////
+ ConfigToggle({
+ icon: 'dark_mode',
+ name: 'Dark Mode',
+ desc: 'Ya should go to sleep!',
+ initValue: darkMode.value,
+ onChange: (_, newValue) => {
+ darkMode.value = !!newValue;
+ },
+ extraSetup: (self) => self.hook(darkMode, (self) => {
+ self.enabled.value = darkMode.value;
+ }),
+ }),
+ ConfigToggle({
+ icon: 'border_clear',
+ name: 'Transparency',
+ desc: 'Make shell elements transparent',
+ initValue: initTransparencyVal,
+ onChange: (self, newValue) => {
+ let transparency = newValue == 0 ? "opaque" : "transparent";
+ execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_state_dir()}/ags/user && sed -i "2s/.*/${transparency}/" ${GLib.get_user_state_dir()}/ags/user/colormode.txt`])
+ .then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchcolor.sh`]))
+ .catch(print);
+ },
+ }),
+ Widget.Box({
+ tooltipText: 'Theme GTK apps using accent color\n(drawback: dark/light mode switching requires restart)',
+ className: 'txt spacing-h-5 configtoggle-box',
+ children: [
+ MaterialIcon('imagesearch_roller', 'norm'),
+ Widget.Label({
+ className: 'txt txt-small',
+ label: 'Use Gradience',
+ }),
+ Widget.Box({ hexpand: true }),
+ ConfigMulipleSelection({
+ hpack: 'center',
+ vpack: 'center',
+ optionsArr: [
+ [{ name: 'Off', value: 0 }, { name: 'On', value: 1 }],
+ ],
+ initIndex: [-1, -1],
+ onChange: (value, name) => {
+ const ADWAITA_BLUE = "#3584E4";
+ if (value) execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh - --yes-gradience`, `&`])
+ .catch(print);
+ else execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh "${ADWAITA_BLUE}" --no-gradience`, `&`])
+ .catch(print);
+
+ },
+ }),
+ ]
+ }),
+ ]
+ }),
+ Widget.Box({
+ vertical: true,
+ className: 'spacing-v-5',
+ children: [
+ Widget.Label({
+ xalign: 0,
+ className: 'txt-norm titlefont txt margin-top-5',
+ label: 'Scheme styles',
+ hpack: 'center',
+ }),
+ //////////////////
+ ConfigMulipleSelection({
+ hpack: 'center',
+ vpack: 'center',
+ optionsArr: schemeOptionsArr,
+ initIndex: initSchemeIndex,
+ onChange: (value, name) => {
+ execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_state_dir()}/ags/user && sed -i "3s/.*/${value}/" ${GLib.get_user_state_dir()}/ags/user/colormode.txt`])
+ .then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchcolor.sh`]))
+ .catch(print);
+ },
+ }),
+ ]
+ })
+ ]
+});
+
+const ColorschemeContent = () => Widget.Box({
+ className: 'osd-colorscheme spacing-v-5',
+ vertical: true,
+ hpack: 'center',
+ children: [
+ Widget.Label({
+ xalign: 0,
+ className: 'txt-norm titlefont txt',
+ label: 'Color scheme',
+ hpack: 'center',
+ }),
+ Widget.Box({
+ className: 'spacing-h-5',
+ hpack: 'center',
+ children: [
+ ColorBox({ name: 'P', className: 'osd-color osd-color-primary' }),
+ ColorBox({ name: 'S', className: 'osd-color osd-color-secondary' }),
+ ColorBox({ name: 'T', className: 'osd-color osd-color-tertiary' }),
+ ColorBox({ name: 'Sf', className: 'osd-color osd-color-surface' }),
+ ColorBox({ name: 'Sf-i', className: 'osd-color osd-color-inverseSurface' }),
+ ColorBox({ name: 'E', className: 'osd-color osd-color-error' }),
+ ]
+ }),
+ Widget.Box({
+ className: 'spacing-h-5',
+ hpack: 'center',
+ children: [
+ ColorBox({ name: 'P-c', className: 'osd-color osd-color-primaryContainer' }),
+ ColorBox({ name: 'S-c', className: 'osd-color osd-color-secondaryContainer' }),
+ ColorBox({ name: 'T-c', className: 'osd-color osd-color-tertiaryContainer' }),
+ ColorBox({ name: 'Sf-c', className: 'osd-color osd-color-surfaceContainer' }),
+ ColorBox({ name: 'Sf-v', className: 'osd-color osd-color-surfaceVariant' }),
+ ColorBox({ name: 'E-c', className: 'osd-color osd-color-errorContainer' }),
+ ]
+ }),
+ ColorSchemeSettingsRevealer(),
+ ]
+});
+
+const isHoveredColorschemeSettings = Variable(false);
+
+export default () => Widget.Revealer({
+ transition: 'slide_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: ColorschemeContent(),
+ setup: (self) => {
+ self
+ .hook(showColorScheme, (revealer) => {
+ if (showColorScheme.value == true)
+ revealer.revealChild = true;
+ else
+ revealer.revealChild = isHoveredColorschemeSettings.value;
+ })
+ .hook(isHoveredColorschemeSettings, (revealer) => {
+ if (isHoveredColorschemeSettings.value == false) {
+ setTimeout(() => {
+ if (isHoveredColorschemeSettings.value == false)
+ revealer.revealChild = showColorScheme.value;
+ }, 2000);
+ }
+ })
+ },
+})
diff --git a/modules/home-manager/ags/ags/modules/indicators/indicatorvalues.js b/modules/home-manager/ags/ags/modules/indicators/indicatorvalues.js
new file mode 100644
index 0000000..4fde189
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/indicators/indicatorvalues.js
@@ -0,0 +1,124 @@
+// This file is for brightness/volume indicators
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
+const { Box, Label, ProgressBar } = Widget;
+import { MarginRevealer } from '../.widgethacks/advancedrevealers.js';
+import Brightness from '../../services/brightness.js';
+import Indicator from '../../services/indicator.js';
+
+const OsdValue = ({
+ name, nameSetup = undefined, labelSetup, progressSetup,
+ extraClassName = '', extraProgressClassName = '',
+ ...rest
+}) => {
+ const valueName = Label({
+ xalign: 0, yalign: 0, hexpand: true,
+ className: 'osd-label',
+ label: `${name}`,
+ setup: nameSetup,
+ });
+ const valueNumber = Label({
+ hexpand: false, className: 'osd-value-txt',
+ setup: labelSetup,
+ });
+ return Box({ // Volume
+ vertical: true,
+ hexpand: true,
+ className: `osd-bg osd-value ${extraClassName}`,
+ attribute: {
+ 'disable': () => {
+ valueNumber.label = '';
+ }
+ },
+ children: [
+ Box({
+ vexpand: true,
+ children: [
+ valueName,
+ valueNumber,
+ ]
+ }),
+ ProgressBar({
+ className: `osd-progress ${extraProgressClassName}`,
+ hexpand: true,
+ vertical: false,
+ setup: progressSetup,
+ })
+ ],
+ ...rest,
+ });
+}
+
+export default (monitor = 0) => {
+ const brightnessIndicator = OsdValue({
+ name: 'Brightness',
+ extraClassName: 'osd-brightness',
+ extraProgressClassName: 'osd-brightness-progress',
+ labelSetup: (self) => self.hook(Brightness[monitor], self => {
+ self.label = `${Math.round(Brightness[monitor].screen_value * 100)}`;
+ }, 'notify::screen-value'),
+ progressSetup: (self) => self.hook(Brightness[monitor], (progress) => {
+ const updateValue = Brightness[monitor].screen_value;
+ if (updateValue !== progress.value) Indicator.popup(1);
+ progress.value = updateValue;
+ }, 'notify::screen-value'),
+ });
+
+ const volumeIndicator = OsdValue({
+ name: 'Volume',
+ extraClassName: 'osd-volume',
+ extraProgressClassName: 'osd-volume-progress',
+ attribute: { headphones: undefined , device: undefined},
+ nameSetup: (self) => Utils.timeout(1, () => {
+ const updateAudioDevice = (self) => {
+ const usingHeadphones = (Audio.speaker?.stream?.port)?.toLowerCase().includes('headphone');
+ if (volumeIndicator.attribute.headphones === undefined ||
+ volumeIndicator.attribute.headphones !== usingHeadphones) {
+ volumeIndicator.attribute.headphones = usingHeadphones;
+ self.label = usingHeadphones ? 'Headphones' : 'Speakers';
+ // Indicator.popup(1);
+ }
+ }
+ self.hook(Audio, updateAudioDevice);
+ Utils.timeout(1000, updateAudioDevice);
+ }),
+ labelSetup: (self) => self.hook(Audio, (label) => {
+ const newDevice = (Audio.speaker?.name);
+ const updateValue = Math.round(Audio.speaker?.volume * 100);
+ if (!isNaN(updateValue)) {
+ if (newDevice === volumeIndicator.attribute.device && updateValue != label.label) {
+ Indicator.popup(1);
+ }
+ }
+ volumeIndicator.attribute.device = newDevice;
+ label.label = `${updateValue}`;
+ }),
+ progressSetup: (self) => self.hook(Audio, (progress) => {
+ const updateValue = Audio.speaker?.volume;
+ if (!isNaN(updateValue)) {
+ if (updateValue > 1) progress.value = 1;
+ else progress.value = updateValue;
+ }
+ }),
+ });
+ return MarginRevealer({
+ transition: 'slide_down',
+ showClass: 'osd-show',
+ hideClass: 'osd-hide',
+ extraSetup: (self) => self
+ .hook(Indicator, (revealer, value) => {
+ if (value > -1) revealer.attribute.show();
+ else revealer.attribute.hide();
+ }, 'popup')
+ ,
+ child: Box({
+ hpack: 'center',
+ vertical: false,
+ className: 'spacing-h--10',
+ children: [
+ brightnessIndicator,
+ volumeIndicator,
+ ]
+ })
+ });
+}
diff --git a/modules/home-manager/ags/ags/modules/indicators/main.js b/modules/home-manager/ags/ags/modules/indicators/main.js
new file mode 100644
index 0000000..1674193
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/indicators/main.js
@@ -0,0 +1,32 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import Indicator from '../../services/indicator.js';
+import IndicatorValues from './indicatorvalues.js';
+import MusicControls from './musiccontrols.js';
+import ColorScheme from './colorscheme.js';
+import NotificationPopups from './notificationpopups.js';
+
+export default (monitor = 0) => Widget.Window({
+ name: `indicator${monitor}`,
+ monitor,
+ className: 'indicator',
+ layer: 'overlay',
+ // exclusivity: 'ignore',
+ visible: true,
+ anchor: ['top'],
+ child: Widget.EventBox({
+ onHover: () => { //make the widget hide when hovering
+ Indicator.popup(-1);
+ },
+ child: Widget.Box({
+ vertical: true,
+ className: 'osd-window',
+ css: 'min-height: 2px;',
+ children: [
+ IndicatorValues(monitor),
+ MusicControls(),
+ NotificationPopups(),
+ ColorScheme(),
+ ]
+ })
+ }),
+});
diff --git a/modules/home-manager/ags/ags/modules/indicators/musiccontrols.js b/modules/home-manager/ags/ags/modules/indicators/musiccontrols.js
new file mode 100644
index 0000000..6384cf9
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/indicators/musiccontrols.js
@@ -0,0 +1,408 @@
+const { GLib } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
+const { exec, execAsync } = Utils;
+const { Box, EventBox, Icon, Scrollable, Label, Button, Revealer } = Widget;
+
+import { fileExists } from '../.miscutils/files.js';
+import { AnimatedCircProg } from "../.commonwidgets/cairo_circularprogress.js";
+import { showMusicControls } from '../../variables.js';
+import { darkMode, hasPlasmaIntegration } from '../.miscutils/system.js';
+
+const COMPILED_STYLE_DIR = `${GLib.get_user_cache_dir()}/ags/user/generated`
+const LIGHTDARK_FILE_LOCATION = `${GLib.get_user_state_dir()}/ags/user/colormode.txt`;
+const colorMode = Utils.exec(`bash -c "sed -n \'1p\' '${LIGHTDARK_FILE_LOCATION}'"`);
+const lightDark = (colorMode == "light") ? '-l' : '';
+const COVER_COLORSCHEME_SUFFIX = '_colorscheme.css';
+var lastCoverPath = '';
+
+function isRealPlayer(player) {
+ return (
+ // Remove unecessary native buses from browsers if there's plasma integration
+ !(hasPlasmaIntegration && player.busName.startsWith('org.mpris.MediaPlayer2.firefox')) &&
+ !(hasPlasmaIntegration && player.busName.startsWith('org.mpris.MediaPlayer2.chromium')) &&
+ // playerctld just copies other buses and we don't need duplicates
+ !player.busName.startsWith('org.mpris.MediaPlayer2.playerctld') &&
+ // Non-instance mpd bus
+ !(player.busName.endsWith('.mpd') && !player.busName.endsWith('MediaPlayer2.mpd'))
+ );
+}
+
+export const getPlayer = (name = userOptions.music.preferredPlayer) => Mpris.getPlayer(name) || Mpris.players[0] || null;
+function lengthStr(length) {
+ const min = Math.floor(length / 60);
+ const sec = Math.floor(length % 60);
+ const sec0 = sec < 10 ? '0' : '';
+ return `${min}:${sec0}${sec}`;
+}
+
+function detectMediaSource(link) {
+ if (link.startsWith("file://")) {
+ if (link.includes('firefox-mpris'))
+ return ' Firefox'
+ return " File";
+ }
+ let url = link.replace(/(^\w+:|^)\/\//, '');
+ let domain = url.match(/(?:[a-z]+\.)?([a-z]+\.[a-z]+)/i)[1];
+ if (domain == 'ytimg.com') return ' Youtube';
+ if (domain == 'discordapp.net') return ' Discord';
+ if (domain == 'sndcdn.com') return ' SoundCloud';
+ return domain;
+}
+
+const DEFAULT_MUSIC_FONT = 'Gabarito, sans-serif';
+function getTrackfont(player) {
+ const title = player.trackTitle;
+ const artists = player.trackArtists.join(' ');
+ if (artists.includes('TANO*C') || artists.includes('USAO') || artists.includes('Kobaryo'))
+ return 'Chakra Petch'; // Rigid square replacement
+ if (title.includes('東方'))
+ return 'Crimson Text, serif'; // Serif for Touhou stuff
+ return DEFAULT_MUSIC_FONT;
+}
+function trimTrackTitle(title) {
+ if (!title) return '';
+ const cleanPatterns = [
+ /【[^】]*】/, // Touhou n weeb stuff
+ " [FREE DOWNLOAD]", // F-777
+ ];
+ cleanPatterns.forEach((expr) => title = title.replace(expr, ''));
+ return title;
+}
+
+const TrackProgress = ({ player, ...rest }) => {
+ const _updateProgress = (circprog) => {
+ // const player = Mpris.getPlayer();
+ if (!player) return;
+ // Set circular progress (see definition of AnimatedCircProg for explanation)
+ circprog.css = `font-size: ${Math.max(player.position / player.length * 100, 0)}px;`
+ }
+ return AnimatedCircProg({
+ ...rest,
+ className: 'osd-music-circprog',
+ vpack: 'center',
+ extraSetup: (self) => self
+ .hook(Mpris, _updateProgress)
+ .poll(3000, _updateProgress)
+ ,
+ })
+}
+
+const TrackTitle = ({ player, ...rest }) => Label({
+ ...rest,
+ label: 'No music playing',
+ xalign: 0,
+ truncate: 'end',
+ // wrap: true,
+ className: 'osd-music-title',
+ setup: (self) => self.hook(player, (self) => {
+ // Player name
+ self.label = player.trackTitle.length > 0 ? trimTrackTitle(player.trackTitle) : 'No media';
+ // Font based on track/artist
+ const fontForThisTrack = getTrackfont(player);
+ self.css = `font-family: ${fontForThisTrack}, ${DEFAULT_MUSIC_FONT};`;
+ }, 'notify::track-title'),
+});
+
+const TrackArtists = ({ player, ...rest }) => Label({
+ ...rest,
+ xalign: 0,
+ className: 'osd-music-artists',
+ truncate: 'end',
+ setup: (self) => self.hook(player, (self) => {
+ self.label = player.trackArtists.length > 0 ? player.trackArtists.join(', ') : '';
+ }, 'notify::track-artists'),
+})
+
+const CoverArt = ({ player, ...rest }) => {
+ const fallbackCoverArt = Box({ // Fallback
+ className: 'osd-music-cover-fallback',
+ homogeneous: true,
+ children: [Label({
+ className: 'icon-material txt-gigantic txt-thin',
+ label: 'music_note',
+ })]
+ });
+ // const coverArtDrawingArea = Widget.DrawingArea({ className: 'osd-music-cover-art' });
+ // const coverArtDrawingAreaStyleContext = coverArtDrawingArea.get_style_context();
+ const realCoverArt = Box({
+ className: 'osd-music-cover-art',
+ homogeneous: true,
+ // children: [coverArtDrawingArea],
+ attribute: {
+ 'pixbuf': null,
+ // 'showImage': (self, imagePath) => {
+ // const borderRadius = coverArtDrawingAreaStyleContext.get_property('border-radius', Gtk.StateFlags.NORMAL);
+ // const frameHeight = coverArtDrawingAreaStyleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
+ // const frameWidth = coverArtDrawingAreaStyleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
+ // let imageHeight = frameHeight;
+ // let imageWidth = frameWidth;
+ // // Get image dimensions
+ // execAsync(['identify', '-format', '{"w":%w,"h":%h}', imagePath])
+ // .then((output) => {
+ // const imageDimensions = JSON.parse(output);
+ // const imageAspectRatio = imageDimensions.w / imageDimensions.h;
+ // const displayedAspectRatio = imageWidth / imageHeight;
+ // if (imageAspectRatio >= displayedAspectRatio) {
+ // imageWidth = imageHeight * imageAspectRatio;
+ // } else {
+ // imageHeight = imageWidth / imageAspectRatio;
+ // }
+ // // Real stuff
+ // // TODO: fix memory leak(?)
+ // // if (self.attribute.pixbuf) {
+ // // self.attribute.pixbuf.unref();
+ // // self.attribute.pixbuf = null;
+ // // }
+ // self.attribute.pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(imagePath, imageWidth, imageHeight);
+
+ // coverArtDrawingArea.set_size_request(frameWidth, frameHeight);
+ // coverArtDrawingArea.connect("draw", (widget, cr) => {
+ // // Clip a rounded rectangle area
+ // cr.arc(borderRadius, borderRadius, borderRadius, Math.PI, 1.5 * Math.PI);
+ // cr.arc(frameWidth - borderRadius, borderRadius, borderRadius, 1.5 * Math.PI, 2 * Math.PI);
+ // cr.arc(frameWidth - borderRadius, frameHeight - borderRadius, borderRadius, 0, 0.5 * Math.PI);
+ // cr.arc(borderRadius, frameHeight - borderRadius, borderRadius, 0.5 * Math.PI, Math.PI);
+ // cr.closePath();
+ // cr.clip();
+ // // Paint image as bg, centered
+ // Gdk.cairo_set_source_pixbuf(cr, self.attribute.pixbuf,
+ // frameWidth / 2 - imageWidth / 2,
+ // frameHeight / 2 - imageHeight / 2
+ // );
+ // cr.paint();
+ // });
+ // }).catch(print)
+ // },
+ 'updateCover': (self) => {
+ // const player = Mpris.getPlayer(); // Maybe no need to re-get player.. can't remember why I had this
+ // Player closed
+ // Note that cover path still remains, so we're checking title
+ if (!player || player.trackTitle == "" || !player.coverPath) {
+ self.css = `background-image: none;`; // CSS image
+ App.applyCss(`${COMPILED_STYLE_DIR}/style.css`);
+ return;
+ }
+
+ const coverPath = player.coverPath;
+ const stylePath = `${player.coverPath}${darkMode.value ? '' : '-l'}${COVER_COLORSCHEME_SUFFIX}`;
+ if (player.coverPath == lastCoverPath) { // Since 'notify::cover-path' emits on cover download complete
+ Utils.timeout(200, () => {
+ // self.attribute.showImage(self, coverPath);
+ self.css = `background-image: url('${coverPath}');`; // CSS image
+ });
+ }
+ lastCoverPath = player.coverPath;
+
+ // If a colorscheme has already been generated, skip generation
+ if (fileExists(stylePath)) {
+ // self.attribute.showImage(self, coverPath)
+ self.css = `background-image: url('${coverPath}');`; // CSS image
+ App.applyCss(stylePath);
+ return;
+ }
+
+ // Generate colors
+ execAsync(['bash', '-c',
+ `${App.configDir}/scripts/color_generation/generate_colors_material.py --path '${coverPath}' --mode ${darkMode.value ? 'dark' : 'light'} > ${GLib.get_user_state_dir()}/ags/scss/_musicmaterial.scss`])
+ .then(() => {
+ exec(`wal -i "${player.coverPath}" -n -t -s -e -q ${darkMode.value ? '' : '-l'}`)
+ exec(`cp ${GLib.get_user_cache_dir()}/wal/colors.scss ${GLib.get_user_state_dir()}/ags/scss/_musicwal.scss`);
+ exec(`sass -I "${GLib.get_user_state_dir()}/ags/scss" -I "${App.configDir}/scss/fallback" "${App.configDir}/scss/_music.scss" "${stylePath}"`);
+ Utils.timeout(200, () => {
+ // self.attribute.showImage(self, coverPath)
+ self.css = `background-image: url('${coverPath}');`; // CSS image
+ });
+ App.applyCss(`${stylePath}`);
+ })
+ .catch(print);
+ },
+ },
+ setup: (self) => self
+ .hook(player, (self) => {
+ self.attribute.updateCover(self);
+ }, 'notify::cover-path')
+ ,
+ });
+ return Box({
+ ...rest,
+ className: 'osd-music-cover',
+ children: [
+ Widget.Overlay({
+ child: fallbackCoverArt,
+ overlays: [realCoverArt],
+ })
+ ],
+ })
+}
+
+const TrackControls = ({ player, ...rest }) => Widget.Revealer({
+ revealChild: false,
+ transition: 'slide_right',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Widget.Box({
+ ...rest,
+ vpack: 'center',
+ className: 'osd-music-controls spacing-h-3',
+ children: [
+ Button({
+ className: 'osd-music-controlbtn',
+ onClicked: () => player.previous(),
+ child: Label({
+ className: 'icon-material osd-music-controlbtn-txt',
+ label: 'skip_previous',
+ })
+ }),
+ Button({
+ className: 'osd-music-controlbtn',
+ onClicked: () => player.next(),
+ child: Label({
+ className: 'icon-material osd-music-controlbtn-txt',
+ label: 'skip_next',
+ })
+ }),
+ ],
+ }),
+ setup: (self) => self.hook(Mpris, (self) => {
+ // const player = Mpris.getPlayer();
+ if (!player)
+ self.revealChild = false;
+ else
+ self.revealChild = true;
+ }, 'notify::play-back-status'),
+});
+
+const TrackSource = ({ player, ...rest }) => Widget.Revealer({
+ revealChild: false,
+ transition: 'slide_left',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Widget.Box({
+ ...rest,
+ className: 'osd-music-pill spacing-h-5',
+ homogeneous: true,
+ children: [
+ Label({
+ hpack: 'fill',
+ justification: 'center',
+ className: 'icon-nerd',
+ setup: (self) => self.hook(player, (self) => {
+ self.label = detectMediaSource(player.trackCoverUrl);
+ }, 'notify::cover-path'),
+ }),
+ ],
+ }),
+ setup: (self) => self.hook(Mpris, (self) => {
+ const mpris = Mpris.getPlayer('');
+ if (!mpris)
+ self.revealChild = false;
+ else
+ self.revealChild = true;
+ }),
+});
+
+const TrackTime = ({ player, ...rest }) => {
+ return Widget.Revealer({
+ revealChild: false,
+ transition: 'slide_left',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Widget.Box({
+ ...rest,
+ vpack: 'center',
+ className: 'osd-music-pill spacing-h-5',
+ children: [
+ Label({
+ setup: (self) => self.poll(1000, (self) => {
+ // const player = Mpris.getPlayer();
+ if (!player) return;
+ self.label = lengthStr(player.position);
+ }),
+ }),
+ Label({ label: '/' }),
+ Label({
+ setup: (self) => self.hook(Mpris, (self) => {
+ // const player = Mpris.getPlayer();
+ if (!player) return;
+ self.label = lengthStr(player.length);
+ }),
+ }),
+ ],
+ }),
+ setup: (self) => self.hook(Mpris, (self) => {
+ if (!player) self.revealChild = false;
+ else self.revealChild = true;
+ }),
+ })
+}
+
+const PlayState = ({ player }) => {
+ var position = 0;
+ const trackCircProg = TrackProgress({ player: player });
+ return Widget.Button({
+ className: 'osd-music-playstate',
+ child: Widget.Overlay({
+ child: trackCircProg,
+ overlays: [
+ Widget.Button({
+ className: 'osd-music-playstate-btn',
+ onClicked: () => player.playPause(),
+ child: Widget.Label({
+ justification: 'center',
+ hpack: 'fill',
+ vpack: 'center',
+ setup: (self) => self.hook(player, (label) => {
+ label.label = `${player.playBackStatus == 'Playing' ? 'pause' : 'play_arrow'}`;
+ }, 'notify::play-back-status'),
+ }),
+ }),
+ ],
+ passThrough: true,
+ })
+ });
+}
+
+const MusicControlsWidget = (player) => Box({
+ className: 'osd-music spacing-h-20 test',
+ children: [
+ CoverArt({ player: player, vpack: 'center' }),
+ Box({
+ vertical: true,
+ className: 'spacing-v-5 osd-music-info',
+ children: [
+ Box({
+ vertical: true,
+ vpack: 'center',
+ hexpand: true,
+ children: [
+ TrackTitle({ player: player }),
+ TrackArtists({ player: player }),
+ ]
+ }),
+ Box({ vexpand: true }),
+ Box({
+ className: 'spacing-h-10',
+ setup: (box) => {
+ box.pack_start(TrackControls({ player: player }), false, false, 0);
+ box.pack_end(PlayState({ player: player }), false, false, 0);
+ if(hasPlasmaIntegration || player.busName.startsWith('org.mpris.MediaPlayer2.chromium')) box.pack_end(TrackTime({ player: player }), false, false, 0)
+ // box.pack_end(TrackSource({ vpack: 'center', player: player }), false, false, 0);
+ }
+ })
+ ]
+ })
+ ]
+})
+
+export default () => Revealer({
+ transition: 'slide_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ revealChild: false,
+ child: Box({
+ children: Mpris.bind("players")
+ .as(players => players.map((player) => (isRealPlayer(player) ? MusicControlsWidget(player) : null)))
+ }),
+ setup: (self) => self.hook(showMusicControls, (revealer) => {
+ revealer.revealChild = showMusicControls.value;
+ }),
+})
diff --git a/modules/home-manager/ags/ags/modules/indicators/notificationpopups.js b/modules/home-manager/ags/ags/modules/indicators/notificationpopups.js
new file mode 100644
index 0000000..0c38a67
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/indicators/notificationpopups.js
@@ -0,0 +1,45 @@
+// This file is for popup notifications
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
+const { Box } = Widget;
+import Notification from '../.commonwidgets/notification.js';
+
+export default () => Box({
+ vertical: true,
+ hpack: 'center',
+ className: 'osd-notifs spacing-v-5-revealer',
+ attribute: {
+ 'map': new Map(),
+ 'dismiss': (box, id, force = false) => {
+ if (!id || !box.attribute.map.has(id))
+ return;
+ const notifWidget = box.attribute.map.get(id);
+ if (notifWidget == null || notifWidget.attribute.hovered && !force)
+ return; // cuz already destroyed
+
+ notifWidget.revealChild = false;
+ notifWidget.attribute.destroyWithAnims();
+ box.attribute.map.delete(id);
+ },
+ 'notify': (box, id) => {
+ if (!id || Notifications.dnd) return;
+ if (!Notifications.getNotification(id)) return;
+
+ box.attribute.map.delete(id);
+
+ const notif = Notifications.getNotification(id);
+ const newNotif = Notification({
+ notifObject: notif,
+ isPopup: true,
+ });
+ box.attribute.map.set(id, newNotif);
+ box.pack_end(box.attribute.map.get(id), false, false, 0);
+ box.show_all();
+ },
+ },
+ setup: (self) => self
+ .hook(Notifications, (box, id) => box.attribute.notify(box, id), 'notified')
+ .hook(Notifications, (box, id) => box.attribute.dismiss(box, id), 'dismissed')
+ .hook(Notifications, (box, id) => box.attribute.dismiss(box, id, true), 'closed')
+ ,
+});
diff --git a/modules/home-manager/ags/ags/modules/onscreenkeyboard/data_keyboardlayouts.js b/modules/home-manager/ags/ags/modules/onscreenkeyboard/data_keyboardlayouts.js
new file mode 100644
index 0000000..1a67b7a
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/onscreenkeyboard/data_keyboardlayouts.js
@@ -0,0 +1,218 @@
+// We're going to use ydotool
+// See /usr/include/linux/input-event-codes.h for keycodes
+
+export const DEFAULT_OSK_LAYOUT = "qwerty_full"
+export const oskLayouts = {
+ qwerty_full: {
+ name: "QWERTY - Full",
+ name_short: "US",
+ comment: "Like physical keyboard",
+ // A key looks like this: { k: "a", ks: "A", t: "normal" } (key, key-shift, type)
+ // key types are: normal, tab, caps, shift, control, fn (normal w/ half height), space, expand
+ // keys: [
+ // [{ k: "Esc", t: "fn" }, { k: "F1", t: "fn" }, { k: "F2", t: "fn" }, { k: "F3", t: "fn" }, { k: "F4", t: "fn" }, { k: "F5", t: "fn" }, { k: "F6", t: "fn" }, { k: "F7", t: "fn" }, { k: "F8", t: "fn" }, { k: "F9", t: "fn" }, { k: "F10", t: "fn" }, { k: "F11", t: "fn" }, { k: "F12", t: "fn" }, { k: "PrtSc", t: "fn" }, { k: "Del", t: "fn" }],
+ // [{ k: "`", ks: "~", t: "normal" }, { k: "1", ks: "!", t: "normal" }, { k: "2", ks: "@", t: "normal" }, { k: "3", ks: "#", t: "normal" }, { k: "4", ks: "$", t: "normal" }, { k: "5", ks: "%", t: "normal" }, { k: "6", ks: "^", t: "normal" }, { k: "7", ks: "&", t: "normal" }, { k: "8", ks: "*", t: "normal" }, { k: "9", ks: "(", t: "normal" }, { k: "0", ks: ")", t: "normal" }, { k: "-", ks: "_", t: "normal" }, { k: "=", ks: "+", t: "normal" }, { k: "Backspace", t: "shift" }],
+ // [{ k: "Tab", t: "tab" }, { k: "q", ks: "Q", t: "normal" }, { k: "w", ks: "W", t: "normal" }, { k: "e", ks: "E", t: "normal" }, { k: "r", ks: "R", t: "normal" }, { k: "t", ks: "T", t: "normal" }, { k: "y", ks: "Y", t: "normal" }, { k: "u", ks: "U", t: "normal" }, { k: "i", ks: "I", t: "normal" }, { k: "o", ks: "O", t: "normal" }, { k: "p", ks: "P", t: "normal" }, { k: "[", ks: "{", t: "normal" }, { k: "]", ks: "}", t: "normal" }, { k: "\\", ks: "|", t: "expand" }],
+ // [{ k: "Caps", t: "caps" }, { k: "a", ks: "A", t: "normal" }, { k: "s", ks: "S", t: "normal" }, { k: "d", ks: "D", t: "normal" }, { k: "f", ks: "F", t: "normal" }, { k: "g", ks: "G", t: "normal" }, { k: "h", ks: "H", t: "normal" }, { k: "j", ks: "J", t: "normal" }, { k: "k", ks: "K", t: "normal" }, { k: "l", ks: "L", t: "normal" }, { k: ";", ks: ":", t: "normal" }, { k: "'", ks: '"', t: "normal" }, { k: "Enter", t: "expand" }],
+ // [{ k: "Shift", t: "shift" }, { k: "z", ks: "Z", t: "normal" }, { k: "x", ks: "X", t: "normal" }, { k: "c", ks: "C", t: "normal" }, { k: "v", ks: "V", t: "normal" }, { k: "b", ks: "B", t: "normal" }, { k: "n", ks: "N", t: "normal" }, { k: "m", ks: "M", t: "normal" }, { k: ",", ks: "<", t: "normal" }, { k: ".", ks: ">", t: "normal" }, { k: "/", ks: "?", t: "normal" }, { k: "Shift", t: "expand" }],
+ // [{ k: "Ctrl", t: "control" }, { k: "Fn", t: "normal" }, { k: "Win", t: "normal" }, { k: "Alt", t: "normal" }, { k: "Space", t: "space" }, { k: "Alt", t: "normal" }, { k: "Menu", t: "normal" }, { k: "Ctrl", t: "control" }]
+ // ]
+ // A normal key looks like this: {label: "a", labelShift: "A", shape: "normal", keycode: 30, type: "normal"}
+ // A modkey looks like this: {label: "Ctrl", shape: "control", keycode: 29, type: "modkey"}
+ // key types are: normal, tab, caps, shift, control, fn (normal w/ half height), space, expand
+ keys: [
+ [
+ { keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
+ { keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
+ { keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
+ { keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
+ { keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
+ { keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
+ { keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
+ { keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
+ { keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
+ { keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
+ { keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
+ { keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
+ { keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
+ { keytype: "normal", label: "PrtSc", shape: "fn", keycode: 99 },
+ { keytype: "normal", label: "Del", shape: "fn", keycode: 111 }
+ ],
+ [
+ { keytype: "normal", label: "`", labelShift: "~", shape: "normal", keycode: 41 },
+ { keytype: "normal", label: "1", labelShift: "!", shape: "normal", keycode: 2 },
+ { keytype: "normal", label: "2", labelShift: "@", shape: "normal", keycode: 3 },
+ { keytype: "normal", label: "3", labelShift: "#", shape: "normal", keycode: 4 },
+ { keytype: "normal", label: "4", labelShift: "$", shape: "normal", keycode: 5 },
+ { keytype: "normal", label: "5", labelShift: "%", shape: "normal", keycode: 6 },
+ { keytype: "normal", label: "6", labelShift: "^", shape: "normal", keycode: 7 },
+ { keytype: "normal", label: "7", labelShift: "&", shape: "normal", keycode: 8 },
+ { keytype: "normal", label: "8", labelShift: "*", shape: "normal", keycode: 9 },
+ { keytype: "normal", label: "9", labelShift: "(", shape: "normal", keycode: 10 },
+ { keytype: "normal", label: "0", labelShift: ")", shape: "normal", keycode: 11 },
+ { keytype: "normal", label: "-", labelShift: "_", shape: "normal", keycode: 12 },
+ { keytype: "normal", label: "=", labelShift: "+", shape: "normal", keycode: 13 },
+ { keytype: "normal", label: "Backspace", shape: "expand", keycode: 14 }
+ ],
+ [
+ { keytype: "normal", label: "Tab", shape: "tab", keycode: 15 },
+ { keytype: "normal", label: "q", labelShift: "Q", shape: "normal", keycode: 16 },
+ { keytype: "normal", label: "w", labelShift: "W", shape: "normal", keycode: 17 },
+ { keytype: "normal", label: "e", labelShift: "E", shape: "normal", keycode: 18 },
+ { keytype: "normal", label: "r", labelShift: "R", shape: "normal", keycode: 19 },
+ { keytype: "normal", label: "t", labelShift: "T", shape: "normal", keycode: 20 },
+ { keytype: "normal", label: "y", labelShift: "Y", shape: "normal", keycode: 21 },
+ { keytype: "normal", label: "u", labelShift: "U", shape: "normal", keycode: 22 },
+ { keytype: "normal", label: "i", labelShift: "I", shape: "normal", keycode: 23 },
+ { keytype: "normal", label: "o", labelShift: "O", shape: "normal", keycode: 24 },
+ { keytype: "normal", label: "p", labelShift: "P", shape: "normal", keycode: 25 },
+ { keytype: "normal", label: "[", labelShift: "{", shape: "normal", keycode: 26 },
+ { keytype: "normal", label: "]", labelShift: "}", shape: "normal", keycode: 27 },
+ { keytype: "normal", label: "\\", labelShift: "|", shape: "expand", keycode: 43 }
+ ],
+ [
+ //{ keytype: "normal", label: "Caps", shape: "caps", keycode: 58 }, // not needed as double-pressing shift does that
+ { keytype: "spacer", label: "", shape: "empty" },
+ { keytype: "spacer", label: "", shape: "empty" },
+ { keytype: "normal", label: "a", labelShift: "A", shape: "normal", keycode: 30 },
+ { keytype: "normal", label: "s", labelShift: "S", shape: "normal", keycode: 31 },
+ { keytype: "normal", label: "d", labelShift: "D", shape: "normal", keycode: 32 },
+ { keytype: "normal", label: "f", labelShift: "F", shape: "normal", keycode: 33 },
+ { keytype: "normal", label: "g", labelShift: "G", shape: "normal", keycode: 34 },
+ { keytype: "normal", label: "h", labelShift: "H", shape: "normal", keycode: 35 },
+ { keytype: "normal", label: "j", labelShift: "J", shape: "normal", keycode: 36 },
+ { keytype: "normal", label: "k", labelShift: "K", shape: "normal", keycode: 37 },
+ { keytype: "normal", label: "l", labelShift: "L", shape: "normal", keycode: 38 },
+ { keytype: "normal", label: ";", labelShift: ":", shape: "normal", keycode: 39 },
+ { keytype: "normal", label: "'", labelShift: '"', shape: "normal", keycode: 40 },
+ { keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
+ ],
+ [
+ { keytype: "modkey", label: "Shift", labelShift: "Shift ⇧", labelCaps: "Locked ⇩", shape: "shift", keycode: 42 },
+ { keytype: "normal", label: "z", labelShift: "Z", shape: "normal", keycode: 44 },
+ { keytype: "normal", label: "x", labelShift: "X", shape: "normal", keycode: 45 },
+ { keytype: "normal", label: "c", labelShift: "C", shape: "normal", keycode: 46 },
+ { keytype: "normal", label: "v", labelShift: "V", shape: "normal", keycode: 47 },
+ { keytype: "normal", label: "b", labelShift: "B", shape: "normal", keycode: 48 },
+ { keytype: "normal", label: "n", labelShift: "N", shape: "normal", keycode: 49 },
+ { keytype: "normal", label: "m", labelShift: "M", shape: "normal", keycode: 50 },
+ { keytype: "normal", label: ",", labelShift: "<", shape: "normal", keycode: 51 },
+ { keytype: "normal", label: ".", labelShift: ">", shape: "normal", keycode: 52 },
+ { keytype: "normal", label: "/", labelShift: "?", shape: "normal", keycode: 53 },
+ { keytype: "modkey", label: "Shift", labelShift: "Shift ⇧", labelCaps: "Locked ⇩", shape: "expand", keycode: 54 } // optional
+ ],
+ [
+ { keytype: "modkey", label: "Ctrl", shape: "control", keycode: 29 },
+ // { label: "Super", shape: "normal", keycode: 125 }, // dangerous
+ { keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
+ { keytype: "normal", label: "Space", shape: "space", keycode: 57 },
+ { keytype: "modkey", label: "Alt", shape: "normal", keycode: 100 },
+ // { label: "Super", shape: "normal", keycode: 126 }, // dangerous
+ { keytype: "normal", label: "Menu", shape: "normal", keycode: 139 },
+ { keytype: "modkey", label: "Ctrl", shape: "control", keycode: 97 }
+ ]
+ ]
+ },
+ qwertz_full: {
+ name: "QWERTZ - Full",
+ name_short: "DE",
+ comment: "Keyboard layout commonly used in German-speaking countries",
+ keys: [
+ [
+ { keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
+ { keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
+ { keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
+ { keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
+ { keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
+ { keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
+ { keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
+ { keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
+ { keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
+ { keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
+ { keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
+ { keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
+ { keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
+ { keytype: "normal", label: "Druck", shape: "fn", keycode: 99 },
+ { keytype: "normal", label: "Entf", shape: "fn", keycode: 111 }
+ ],
+ [
+ { keytype: "normal", label: "^", labelShift: "°", labelAlt: "′", shape: "normal", keycode: 41 },
+ { keytype: "normal", label: "1", labelShift: "!", labelAlt: "¹", shape: "normal", keycode: 2 },
+ { keytype: "normal", label: "2", labelShift: "\"", labelAlt: "²", shape: "normal", keycode: 3 },
+ { keytype: "normal", label: "3", labelShift: "§", labelAlt: "³", shape: "normal", keycode: 4 },
+ { keytype: "normal", label: "4", labelShift: "$", labelAlt: "¼", shape: "normal", keycode: 5 },
+ { keytype: "normal", label: "5", labelShift: "%", labelAlt: "½", shape: "normal", keycode: 6 },
+ { keytype: "normal", label: "6", labelShift: "&", labelAlt: "¬", shape: "normal", keycode: 7 },
+ { keytype: "normal", label: "7", labelShift: "/", labelAlt: "{", shape: "normal", keycode: 8 },
+ { keytype: "normal", label: "8", labelShift: "(", labelAlt: "[", shape: "normal", keycode: 9 },
+ { keytype: "normal", label: "9", labelShift: ")", labelAlt: "]", shape: "normal", keycode: 10 },
+ { keytype: "normal", label: "0", labelShift: "=", labelAlt: "}", shape: "normal", keycode: 11 },
+ { keytype: "normal", label: "ß", labelShift: "?", labelAlt: "\\", shape: "normal", keycode: 12 },
+ { keytype: "normal", label: "´", labelShift: "`", labelAlt: "¸", shape: "normal", keycode: 13 },
+ { keytype: "normal", label: "⟵", shape: "expand", keycode: 14 }
+ ],
+ [
+ { keytype: "normal", label: "Tab ⇆", shape: "tab", keycode: 15 },
+ { keytype: "normal", label: "q", labelShift: "Q", labelAlt: "@", shape: "normal", keycode: 16 },
+ { keytype: "normal", label: "w", labelShift: "W", labelAlt: "ſ", shape: "normal", keycode: 17 },
+ { keytype: "normal", label: "e", labelShift: "E", labelAlt: "€", shape: "normal", keycode: 18 },
+ { keytype: "normal", label: "r", labelShift: "R", labelAlt: "¶", shape: "normal", keycode: 19 },
+ { keytype: "normal", label: "t", labelShift: "T", labelAlt: "ŧ", shape: "normal", keycode: 20 },
+ { keytype: "normal", label: "z", labelShift: "Z", labelAlt: "←", shape: "normal", keycode: 21 },
+ { keytype: "normal", label: "u", labelShift: "U", labelAlt: "↓", shape: "normal", keycode: 22 },
+ { keytype: "normal", label: "i", labelShift: "I", labelAlt: "→", shape: "normal", keycode: 23 },
+ { keytype: "normal", label: "o", labelShift: "O", labelAlt: "ø", shape: "normal", keycode: 24 },
+ { keytype: "normal", label: "p", labelShift: "P", labelAlt: "þ", shape: "normal", keycode: 25 },
+ { keytype: "normal", label: "ü", labelShift: "Ü", labelAlt: "¨", shape: "normal", keycode: 26 },
+ { keytype: "normal", label: "+", labelShift: "*", labelAlt: "~", shape: "normal", keycode: 27 },
+ { keytype: "normal", label: "↵", shape: "expand", keycode: 28 }
+ ],
+ [
+ //{ keytype: "normal", label: "Umschalt ⇩", shape: "caps", keycode: 58 },
+ { keytype: "spacer", label: "", shape: "empty" },
+ { keytype: "spacer", label: "", shape: "empty" },
+ { keytype: "normal", label: "a", labelShift: "A", labelAlt: "æ", shape: "normal", keycode: 30 },
+ { keytype: "normal", label: "s", labelShift: "S", labelAlt: "ſ", shape: "normal", keycode: 31 },
+ { keytype: "normal", label: "d", labelShift: "D", labelAlt: "ð", shape: "normal", keycode: 32 },
+ { keytype: "normal", label: "f", labelShift: "F", labelAlt: "đ", shape: "normal", keycode: 33 },
+ { keytype: "normal", label: "g", labelShift: "G", labelAlt: "ŋ", shape: "normal", keycode: 34 },
+ { keytype: "normal", label: "h", labelShift: "H", labelAlt: "ħ", shape: "normal", keycode: 35 },
+ { keytype: "normal", label: "j", labelShift: "J", labelAlt: "", shape: "normal", keycode: 36 },
+ { keytype: "normal", label: "k", labelShift: "K", labelAlt: "ĸ", shape: "normal", keycode: 37 },
+ { keytype: "normal", label: "l", labelShift: "L", labelAlt: "ł", shape: "normal", keycode: 38 },
+ { keytype: "normal", label: "ö", labelShift: "Ö", labelAlt: "˝", shape: "normal", keycode: 39 },
+ { keytype: "normal", label: "ä", labelShift: 'Ä', labelAlt: "^", shape: "normal", keycode: 40 },
+ { keytype: "normal", label: "#", labelShift: '\'', labelAlt: "’", shape: "normal", keycode: 43 },
+ { keytype: "spacer", label: "", shape: "empty" },
+ //{ keytype: "normal", label: "↵", shape: "expand", keycode: 28 }
+ ],
+ [
+ { keytype: "modkey", label: "Shift", labelShift: "Shift ⇧", labelCaps: "Locked ⇩", shape: "shift", keycode: 42 },
+ { keytype: "normal", label: "<", labelShift: ">", labelAlt: "|", shape: "normal", keycode: 86 },
+ { keytype: "normal", label: "y", labelShift: "Y", labelAlt: "»", shape: "normal", keycode: 44 },
+ { keytype: "normal", label: "x", labelShift: "X", labelAlt: "«", shape: "normal", keycode: 45 },
+ { keytype: "normal", label: "c", labelShift: "C", labelAlt: "¢", shape: "normal", keycode: 46 },
+ { keytype: "normal", label: "v", labelShift: "V", labelAlt: "„", shape: "normal", keycode: 47 },
+ { keytype: "normal", label: "b", labelShift: "B", labelAlt: "“", shape: "normal", keycode: 48 },
+ { keytype: "normal", label: "n", labelShift: "N", labelAlt: "”", shape: "normal", keycode: 49 },
+ { keytype: "normal", label: "m", labelShift: "M", labelAlt: "µ", shape: "normal", keycode: 50 },
+ { keytype: "normal", label: ",", labelShift: ";", labelAlt: "·", shape: "normal", keycode: 51 },
+ { keytype: "normal", label: ".", labelShift: ":", labelAlt: "…", shape: "normal", keycode: 52 },
+ { keytype: "normal", label: "-", labelShift: "_", labelAlt: "–", shape: "normal", keycode: 53 },
+ { keytype: "modkey", label: "Shift", labelShift: "Shift ⇧", labelCaps: "Locked ⇩", shape: "expand", keycode: 54 }, // optional
+ ],
+ [
+ { keytype: "modkey", label: "Strg", shape: "control", keycode: 29 },
+ //{ keytype: "normal", label: "", shape: "normal", keycode: 125 }, // dangerous
+ { keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
+ { keytype: "normal", label: "Leertaste", shape: "space", keycode: 57 },
+ { keytype: "modkey", label: "Alt Gr", shape: "normal", keycode: 100 },
+ // { label: "Super", shape: "normal", keycode: 126 }, // dangerous
+ //{ keytype: "normal", label: "Menu", shape: "normal", keycode: 139 }, // doesn't work?
+ { keytype: "modkey", label: "Strg", shape: "control", keycode: 97 },
+ { keytype: "normal", label: "⇦", shape: "normal", keycode: 105 },
+ { keytype: "normal", label: "⇨", shape: "normal", keycode: 106 },
+ ]
+ ]
+ }
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/onscreenkeyboard/main.js b/modules/home-manager/ags/ags/modules/onscreenkeyboard/main.js
new file mode 100644
index 0000000..2204ebf
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/onscreenkeyboard/main.js
@@ -0,0 +1,11 @@
+import PopupWindow from '../.widgethacks/popupwindow.js';
+import OnScreenKeyboard from "./onscreenkeyboard.js";
+
+export default (id) => PopupWindow({
+ monitor: id,
+ anchor: ['bottom'],
+ name: `osk${id}`,
+ showClassName: 'osk-show',
+ hideClassName: 'osk-hide',
+ child: OnScreenKeyboard({ id: id }),
+});
diff --git a/modules/home-manager/ags/ags/modules/onscreenkeyboard/onscreenkeyboard.js b/modules/home-manager/ags/ags/modules/onscreenkeyboard/onscreenkeyboard.js
new file mode 100644
index 0000000..9280da7
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/onscreenkeyboard/onscreenkeyboard.js
@@ -0,0 +1,267 @@
+const { Gtk } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+
+const { Box, EventBox, Button, Revealer } = Widget;
+const { execAsync } = Utils;
+import { MaterialIcon } from '../.commonwidgets/materialicon.js';
+import { DEFAULT_OSK_LAYOUT, oskLayouts } from './data_keyboardlayouts.js';
+import { setupCursorHoverGrab } from '../.widgetutils/cursorhover.js';
+
+const keyboardLayout = oskLayouts[userOptions.onScreenKeyboard.layout] ? userOptions.onScreenKeyboard.layout : DEFAULT_OSK_LAYOUT;
+const keyboardJson = oskLayouts[keyboardLayout];
+
+async function startYdotoolIfNeeded() {
+ const running = exec('pidof ydotool')
+ if (!running) execAsync(['ydotoold']).catch(print);
+}
+
+function releaseAllKeys() {
+ const keycodes = Array.from(Array(249).keys());
+ execAsync([`ydotool`, `key`, ...keycodes.map(keycode => `${keycode}:0`)])
+ .then(console.log('[OSK] Released all keys'))
+ .catch(print);
+}
+class ShiftMode {
+ static Off = new ShiftMode('Off');
+ static Normal = new ShiftMode('Normal');
+ static Locked = new ShiftMode('Locked');
+
+ constructor(name) {
+ this.name = name;
+ }
+ toString() {
+ return `ShiftMode.${this.name}`;
+ }
+}
+var modsPressed = false;
+
+const TopDecor = () => Box({
+ vertical: true,
+ children: [
+ Box({
+ hpack: 'center',
+ className: 'osk-dragline',
+ homogeneous: true,
+ children: [EventBox({
+ setup: setupCursorHoverGrab,
+ })]
+ })
+ ]
+});
+
+const KeyboardControlButton = (icon, text, runFunction) => Button({
+ className: 'osk-control-button spacing-h-10',
+ onClicked: () => runFunction(),
+ child: Widget.Box({
+ children: [
+ MaterialIcon(icon, 'norm'),
+ Widget.Label({
+ label: `${text}`,
+ }),
+ ]
+ })
+})
+
+const KeyboardControls = () => Box({
+ vertical: true,
+ className: 'spacing-v-5',
+ children: [
+ Button({
+ className: 'osk-control-button txt-norm icon-material',
+ onClicked: () => {
+ releaseAllKeys();
+ toggleWindowOnAllMonitors('osk');
+ },
+ label: 'keyboard_hide',
+ }),
+ Button({
+ className: 'osk-control-button txt-norm',
+ label: `${keyboardJson['name_short']}`,
+ }),
+ Button({
+ className: 'osk-control-button txt-norm icon-material',
+ onClicked: () => { // TODO: Proper clipboard widget, since fuzzel doesn't receive mouse inputs
+ execAsync([`bash`, `-c`, "pkill fuzzel || cliphist list | fuzzel --no-fuzzy --dmenu | cliphist decode | wl-copy"]).catch(print);
+ },
+ label: 'assignment',
+ }),
+ ]
+})
+
+var shiftMode = ShiftMode.Off;
+var shiftButton;
+var rightShiftButton;
+var allButtons = [];
+const KeyboardItself = (kbJson) => {
+ return Box({
+ vertical: true,
+ className: 'spacing-v-5',
+ children: kbJson.keys.map(row => Box({
+ vertical: false,
+ className: 'spacing-h-5',
+ children: row.map(key => {
+ return Button({
+ className: `osk-key osk-key-${key.shape}`,
+ hexpand: ["space", "expand"].includes(key.shape),
+ label: key.label,
+ attribute:
+ { key: key },
+ setup: (button) => {
+ let pressed = false;
+ allButtons = allButtons.concat(button);
+ if (key.keytype == "normal") {
+ button.connect('pressed', () => { // mouse down
+ execAsync(`ydotool key ${key.keycode}:1`).catch(print);
+ });
+ button.connect('clicked', () => { // release
+ execAsync(`ydotool key ${key.keycode}:0`).catch(print);
+
+ if (shiftMode == ShiftMode.Normal) {
+ shiftMode = ShiftMode.Off;
+ if (typeof shiftButton !== 'undefined') {
+ execAsync(`ydotool key 42:0`).catch(print);
+ shiftButton.toggleClassName('osk-key-active', false);
+ }
+ if (typeof rightShiftButton !== 'undefined') {
+ execAsync(`ydotool key 54:0`).catch(print);
+ rightShiftButton.toggleClassName('osk-key-active', false);
+ }
+ allButtons.forEach(button => {
+ if (typeof button.attribute.key.labelShift !== 'undefined') button.label = button.attribute.key.label;
+ })
+ }
+ });
+ }
+ else if (key.keytype == "modkey") {
+ button.connect('pressed', () => { // release
+ if (pressed) {
+ execAsync(`ydotool key ${key.keycode}:0`).catch(print);
+ button.toggleClassName('osk-key-active', false);
+ pressed = false;
+ if (key.keycode == 100) { // Alt Gr button
+ allButtons.forEach(button => { if (typeof button.attribute.key.labelAlt !== 'undefined') button.label = button.attribute.key.label; });
+ }
+ }
+ else {
+ execAsync(`ydotool key ${key.keycode}:1`).catch(print);
+ button.toggleClassName('osk-key-active', true);
+ if (!(key.keycode == 42 || key.keycode == 54)) pressed = true;
+ else switch (shiftMode.name) { // This toggles the shift button state
+ case "Off": {
+ shiftMode = ShiftMode.Normal;
+ allButtons.forEach(button => { if (typeof button.attribute.key.labelShift !== 'undefined') button.label = button.attribute.key.labelShift; })
+ if (typeof shiftButton !== 'undefined') {
+ shiftButton.toggleClassName('osk-key-active', true);
+ }
+ if (typeof rightShiftButton !== 'undefined') {
+ rightShiftButton.toggleClassName('osk-key-active', true);
+ }
+ } break;
+ case "Normal": {
+ shiftMode = ShiftMode.Locked;
+ if (typeof shiftButton !== 'undefined') shiftButton.label = key.labelCaps;
+ if (typeof rightShiftButton !== 'undefined') rightShiftButton.label = key.labelCaps;
+ } break;
+ case "Locked": {
+ shiftMode = ShiftMode.Off;
+ if (typeof shiftButton !== 'undefined') {
+ shiftButton.label = key.label;
+ shiftButton.toggleClassName('osk-key-active', false);
+ }
+ if (typeof rightShiftButton !== 'undefined') {
+ rightShiftButton.label = key.label;
+ rightShiftButton.toggleClassName('osk-key-active', false);
+ }
+ execAsync(`ydotool key ${key.keycode}:0`).catch(print);
+
+ allButtons.forEach(button => { if (typeof button.attribute.key.labelShift !== 'undefined') button.label = button.attribute.key.label; }
+ )
+ };
+ }
+ if (key.keycode == 100) { // Alt Gr button
+ allButtons.forEach(button => { if (typeof button.attribute.key.labelAlt !== 'undefined') button.label = button.attribute.key.labelAlt; });
+ }
+ modsPressed = true;
+ }
+ });
+ if (key.keycode == 42) shiftButton = button;
+ else if (key.keycode == 54) rightShiftButton = button;
+ }
+ }
+ })
+ })
+ }))
+ })
+}
+
+const KeyboardWindow = () => Box({
+ vexpand: true,
+ hexpand: true,
+ vertical: true,
+ className: 'osk-window spacing-v-5',
+ children: [
+ TopDecor(),
+ Box({
+ className: 'osk-body spacing-h-10',
+ children: [
+ KeyboardControls(),
+ Widget.Box({ className: 'separator-line' }),
+ KeyboardItself(keyboardJson),
+ ],
+ })
+ ],
+ setup: (self) => self.hook(App, (self, name, visible) => { // Update on open
+ if (!name) return;
+ if (name.startsWith('osk') && visible) {
+ self.setCss(`margin-bottom: -0px;`);
+ }
+ }),
+});
+
+export default ({ id }) => {
+ const kbWindow = KeyboardWindow();
+ const gestureEvBox = EventBox({ child: kbWindow })
+ const gesture = Gtk.GestureDrag.new(gestureEvBox);
+ gesture.connect('drag-begin', async () => {
+ try {
+ const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
+ Hyprland.messageAsync('j/cursorpos').then((out) => {
+ gesture.startY = JSON.parse(out).y;
+ }).catch(print);
+ } catch {
+ return;
+ }
+ });
+ gesture.connect('drag-update', async () => {
+ try {
+ const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
+ Hyprland.messageAsync('j/cursorpos').then((out) => {
+ const currentY = JSON.parse(out).y;
+ const offset = gesture.startY - currentY;
+
+ if (offset > 0) return;
+
+ kbWindow.setCss(`
+ margin-bottom: ${offset}px;
+ `);
+ }).catch(print);
+ } catch {
+ return;
+ }
+ });
+ gesture.connect('drag-end', () => {
+ var offset = gesture.get_offset()[2];
+ if (offset > 50) {
+ App.closeWindow(`osk${id}`);
+ }
+ else {
+ kbWindow.setCss(`
+ transition: margin-bottom 170ms cubic-bezier(0.05, 0.7, 0.1, 1);
+ margin-bottom: 0px;
+ `);
+ }
+ })
+ return gestureEvBox;
+};
diff --git a/modules/home-manager/ags/ags/modules/overview/actions.js b/modules/home-manager/ags/ags/modules/overview/actions.js
new file mode 100644
index 0000000..766cf45
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/overview/actions.js
@@ -0,0 +1,28 @@
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
+
+function moveClientToWorkspace(address, workspace) {
+ Utils.execAsync(['bash', '-c', `hyprctl dispatch movetoworkspacesilent ${workspace},address:${address} &`]);
+}
+
+export function dumpToWorkspace(from, to) {
+ if (from == to) return;
+ Hyprland.clients.forEach(client => {
+ if (client.workspace.id == from) {
+ moveClientToWorkspace(client.address, to);
+ }
+ });
+}
+
+export function swapWorkspace(workspaceA, workspaceB) {
+ if (workspaceA == workspaceB) return;
+ const clientsA = [];
+ const clientsB = [];
+ Hyprland.clients.forEach(client => {
+ if (client.workspace.id == workspaceA) clientsA.push(client.address);
+ if (client.workspace.id == workspaceB) clientsB.push(client.address);
+ });
+
+ clientsA.forEach((address) => moveClientToWorkspace(address, workspaceB));
+ clientsB.forEach((address) => moveClientToWorkspace(address, workspaceA));
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/overview/main.js b/modules/home-manager/ags/ags/modules/overview/main.js
new file mode 100644
index 0000000..ee9a3d2
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/overview/main.js
@@ -0,0 +1,28 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import { SearchAndWindows } from "./windowcontent.js";
+import PopupWindow from '../.widgethacks/popupwindow.js';
+import { clickCloseRegion } from '../.commonwidgets/clickcloseregion.js';
+
+export default (id = '') => PopupWindow({
+ name: `overview${id}`,
+ // exclusivity: 'ignore',
+ keymode: 'on-demand',
+ visible: false,
+ anchor: ['top', 'bottom', 'left', 'right'],
+ layer: 'overlay',
+ child: Widget.Box({
+ vertical: true,
+ children: [
+ clickCloseRegion({ name: 'overview', multimonitor: false, expand: false }),
+ Widget.Box({
+ children: [
+ clickCloseRegion({ name: 'overview', multimonitor: false }),
+ SearchAndWindows(),
+ clickCloseRegion({ name: 'overview', multimonitor: false }),
+ ]
+ }),
+ clickCloseRegion({ name: 'overview', multimonitor: false }),
+ ]
+ }),
+})
+
diff --git a/modules/home-manager/ags/ags/modules/overview/miscfunctions.js b/modules/home-manager/ags/ags/modules/overview/miscfunctions.js
new file mode 100644
index 0000000..903ba72
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/overview/miscfunctions.js
@@ -0,0 +1,165 @@
+const { Gio, GLib } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { execAsync, exec } = Utils;
+import Todo from "../../services/todo.js";
+import { darkMode } from '../.miscutils/system.js';
+
+export function hasUnterminatedBackslash(inputString) {
+ // Use a regular expression to match a trailing odd number of backslashes
+ const regex = /\\+$/;
+ return regex.test(inputString);
+}
+
+export function launchCustomCommand(command) {
+ const args = command.toLowerCase().split(' ');
+ if (args[0] == '>raw') { // Mouse raw input
+ Utils.execAsync('hyprctl -j getoption input:accel_profile')
+ .then((output) => {
+ const value = JSON.parse(output)["str"].trim();
+ if (value != "[[EMPTY]]" && value != "") {
+ execAsync(['bash', '-c', `hyprctl keyword input:accel_profile '[[EMPTY]]'`]).catch(print);
+ }
+ else {
+ execAsync(['bash', '-c', `hyprctl keyword input:accel_profile flat`]).catch(print);
+ }
+ })
+ }
+ else if (args[0] == '>img') { // Change wallpaper
+ execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchwall.sh`, `&`]).catch(print);
+ }
+ else if (args[0] == '>color') { // Generate colorscheme from color picker
+ if (!args[1])
+ execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh --pick`, `&`]).catch(print);
+ else if (args[1][0] === '#')
+ execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh "${args[1]}"`, `&`]).catch(print);
+ }
+ else if (args[0] == '>light') { // Light mode
+ darkMode.value = false;
+ }
+ else if (args[0] == '>dark') { // Dark mode
+ darkMode.value = true;
+ }
+ else if (args[0] == '>badapple') { // Black and white
+ execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_state_dir()}/ags/user && sed -i "3s/.*/monochrome/" ${GLib.get_user_state_dir()}/ags/user/colormode.txt`])
+ .then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchcolor.sh`]))
+ .catch(print);
+ }
+ else if (args[0] == '>adw' || args[0] == '>adwaita') {
+ const ADWAITA_BLUE = "#3584E4";
+ execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh "${ADWAITA_BLUE}" --no-gradience`, `&`])
+ .catch(print);
+ }
+ else if (args[0] == '>grad' || args[0] == '>gradience') {
+ execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh - --yes-gradience`, `&`])
+ .catch(print);
+ }
+ else if (args[0] == '>nograd' || args[0] == '>nogradience') {
+ execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh - --no-gradience`, `&`])
+ .catch(print);
+ }
+ else if (args[0] == '>material') { // Use material colors
+ execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_state_dir()}/ags/user && echo "material" > ${GLib.get_user_state_dir()}/ags/user/colorbackend.txt`]).catch(print)
+ .then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchwall.sh --noswitch`]).catch(print))
+ .catch(print);
+ }
+ else if (args[0] == '>pywal') { // Use Pywal (ik it looks shit but I'm not removing)
+ execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_state_dir()}/ags/user && echo "pywal" > ${GLib.get_user_state_dir()}/ags/user/colorbackend.txt`]).catch(print)
+ .then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchwall.sh --noswitch`]).catch(print))
+ .catch(print);
+ }
+ else if (args[0] == '>todo') { // Todo
+ Todo.add(args.slice(1).join(' '));
+ }
+ else if (args[0] == '>shutdown') { // Shut down
+ execAsync([`bash`, `-c`, `systemctl poweroff || loginctl poweroff`]).catch(print);
+ }
+ else if (args[0] == '>reboot') { // Reboot
+ execAsync([`bash`, `-c`, `systemctl reboot || loginctl reboot`]).catch(print);
+ }
+ else if (args[0] == '>sleep') { // Sleep
+ execAsync([`bash`, `-c`, `systemctl suspend || loginctl suspend`]).catch(print);
+ }
+ else if (args[0] == '>logout') { // Log out
+ execAsync([`bash`, `-c`, `pkill Hyprland`]).catch(print);
+ }
+}
+
+export function execAndClose(command, terminal) {
+ App.closeWindow('overview');
+ if (terminal) {
+ execAsync([`bash`, `-c`, `${userOptions.apps.terminal} fish -C "${command}"`, `&`]).catch(print);
+ }
+ else
+ execAsync(command).catch(print);
+}
+
+export function couldBeMath(str) {
+ const regex = /^[0-9.+*/-]/;
+ return regex.test(str);
+}
+
+export function expandTilde(path) {
+ if (path.startsWith('~')) {
+ return GLib.get_home_dir() + path.slice(1);
+ } else {
+ return path;
+ }
+}
+
+function getFileIcon(fileInfo) {
+ let icon = fileInfo.get_icon();
+ if (icon) {
+ // Get the icon's name
+ return icon.get_names()[0];
+ } else {
+ // Default icon for files
+ return 'text-x-generic';
+ }
+}
+
+export function ls({ path = '~', silent = false }) {
+ let contents = [];
+ try {
+ let expandedPath = expandTilde(path);
+ if (expandedPath.endsWith('/'))
+ expandedPath = expandedPath.slice(0, -1);
+ let folder = Gio.File.new_for_path(expandedPath);
+
+ let enumerator = folder.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null);
+ let fileInfo;
+ while ((fileInfo = enumerator.next_file(null)) !== null) {
+ let fileName = fileInfo.get_display_name();
+ let fileType = fileInfo.get_file_type();
+
+ let item = {
+ parentPath: expandedPath,
+ name: fileName,
+ type: fileType === Gio.FileType.DIRECTORY ? 'folder' : 'file',
+ icon: getFileIcon(fileInfo),
+ };
+
+ // Add file extension for files
+ if (fileType === Gio.FileType.REGULAR) {
+ let fileExtension = fileName.split('.').pop();
+ item.type = `${fileExtension}`;
+ }
+
+ contents.push(item);
+ contents.sort((a, b) => {
+ const aIsFolder = a.type.startsWith('folder');
+ const bIsFolder = b.type.startsWith('folder');
+ if (aIsFolder && !bIsFolder) {
+ return -1;
+ } else if (!aIsFolder && bIsFolder) {
+ return 1;
+ } else {
+ return a.name.localeCompare(b.name); // Sort alphabetically within folders and files
+ }
+ });
+ }
+ } catch (e) {
+ if (!silent) console.log(e);
+ }
+ return contents;
+}
diff --git a/modules/home-manager/ags/ags/modules/overview/overview_hyprland.js b/modules/home-manager/ags/ags/modules/overview/overview_hyprland.js
new file mode 100644
index 0000000..e9eab42
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/overview/overview_hyprland.js
@@ -0,0 +1,431 @@
+// TODO
+// - Make client destroy/create not destroy and recreate the whole thing
+// - Active ws hook optimization: only update when moving to next group
+//
+const { Gdk, Gtk } = imports.gi;
+const { Gravity } = imports.gi.Gdk;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Variable from 'resource:///com/github/Aylur/ags/variable.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+
+import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
+const { execAsync, exec } = Utils;
+import { setupCursorHoverGrab } from '../.widgetutils/cursorhover.js';
+import { dumpToWorkspace, swapWorkspace } from "./actions.js";
+import { iconExists, substitute } from "../.miscutils/icons.js";
+import { monitors } from '../.commondata/hyprlanddata.js';
+import { MaterialIcon } from '../.commonwidgets/materialicon.js';
+
+const NUM_OF_WORKSPACES_SHOWN = userOptions.overview.numOfCols * userOptions.overview.numOfRows;
+const TARGET = [Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags.SAME_APP, 0)];
+
+const overviewTick = Variable(false);
+
+export default (overviewMonitor = 0) => {
+ const clientMap = new Map();
+ const ContextMenuWorkspaceArray = ({ label, actionFunc, thisWorkspace }) => Widget.MenuItem({
+ label: `${label}`,
+ setup: (menuItem) => {
+ let submenu = new Gtk.Menu();
+ submenu.className = 'menu';
+
+ const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
+ const startWorkspace = offset + 1;
+ const endWorkspace = startWorkspace + NUM_OF_WORKSPACES_SHOWN - 1;
+ for (let i = startWorkspace; i <= endWorkspace; i++) {
+ let button = new Gtk.MenuItem({
+ label: `Workspace ${i}`
+ });
+ button.connect("activate", () => {
+ // execAsync([`${onClickBinary}`, `${thisWorkspace}`, `${i}`]).catch(print);
+ actionFunc(thisWorkspace, i);
+ overviewTick.setValue(!overviewTick.value);
+ });
+ submenu.append(button);
+ }
+ menuItem.set_reserve_indicator(true);
+ menuItem.set_submenu(submenu);
+ }
+ })
+
+ const Window = ({ address, at: [x, y], size: [w, h], workspace: { id, name }, class: c, initialClass, monitor, title, xwayland }, screenCoords) => {
+ const revealInfoCondition = (Math.min(w, h) * userOptions.overview.scale > 70);
+ if (w <= 0 || h <= 0 || (c === '' && title === '')) return null;
+ // Non-primary monitors
+ if (screenCoords.x != 0) x -= screenCoords.x;
+ if (screenCoords.y != 0) y -= screenCoords.y;
+ // Other offscreen adjustments
+ if (x + w <= 0) x += (Math.floor(x / monitors[monitor].width) * monitors[monitor].width);
+ else if (x < 0) { w = x + w; x = 0; }
+ if (y + h <= 0) x += (Math.floor(y / monitors[monitor].height) * monitors[monitor].height);
+ else if (y < 0) { h = y + h; y = 0; }
+ // Truncate if offscreen
+ if (x + w > monitors[monitor].width) w = monitors[monitor].width - x;
+ if (y + h > monitors[monitor].height) h = monitors[monitor].height - y;
+
+ if(c.length == 0) c = initialClass;
+ const iconName = substitute(c);
+ const appIcon = iconExists(iconName) ? Widget.Icon({
+ icon: iconName,
+ size: Math.min(w, h) * userOptions.overview.scale / 2.5,
+ }) : MaterialIcon('terminal', 'gigantic', {
+ css: `font-size: ${Math.min(w, h) * userOptions.overview.scale / 2.5}px`,
+ });
+ return Widget.Button({
+ attribute: {
+ address, x, y, w, h, ws: id,
+ updateIconSize: (self) => {
+ appIcon.size = Math.min(self.attribute.w, self.attribute.h) * userOptions.overview.scale / 2.5;
+ },
+ },
+ className: 'overview-tasks-window',
+ hpack: 'start',
+ vpack: 'start',
+ css: `
+ margin-left: ${Math.round(x * userOptions.overview.scale)}px;
+ margin-top: ${Math.round(y * userOptions.overview.scale)}px;
+ margin-right: -${Math.round((x + w) * userOptions.overview.scale)}px;
+ margin-bottom: -${Math.round((y + h) * userOptions.overview.scale)}px;
+ `,
+ onClicked: (self) => {
+ Hyprland.messageAsync(`dispatch focuswindow address:${address}`);
+ App.closeWindow('overview');
+ },
+ onMiddleClickRelease: () => Hyprland.messageAsync(`dispatch closewindow address:${address}`),
+ onSecondaryClick: (button) => {
+ button.toggleClassName('overview-tasks-window-selected', true);
+ const menu = Widget.Menu({
+ className: 'menu',
+ children: [
+ Widget.MenuItem({
+ child: Widget.Label({
+ xalign: 0,
+ label: "Close (Middle-click)",
+ }),
+ onActivate: () => Hyprland.messageAsync(`dispatch closewindow address:${address}`),
+ }),
+ ContextMenuWorkspaceArray({
+ label: "Dump windows to workspace",
+ actionFunc: dumpToWorkspace,
+ thisWorkspace: Number(id)
+ }),
+ ContextMenuWorkspaceArray({
+ label: "Swap windows with workspace",
+ actionFunc: swapWorkspace,
+ thisWorkspace: Number(id)
+ }),
+ ],
+ });
+ menu.connect("deactivate", () => {
+ button.toggleClassName('overview-tasks-window-selected', false);
+ })
+ menu.connect("selection-done", () => {
+ button.toggleClassName('overview-tasks-window-selected', false);
+ })
+ menu.popup_at_widget(button.get_parent(), Gravity.SOUTH, Gravity.NORTH, null); // Show menu below the button
+ button.connect("destroy", () => menu.destroy());
+ },
+ child: Widget.Box({
+ homogeneous: true,
+ child: Widget.Box({
+ vertical: true,
+ vpack: 'center',
+ children: [
+ appIcon,
+ // TODO: Add xwayland tag instead of just having italics
+ Widget.Revealer({
+ transition: 'slide_right',
+ revealChild: revealInfoCondition,
+ child: Widget.Revealer({
+ transition: 'slide_down',
+ revealChild: revealInfoCondition,
+ child: Widget.Label({
+ maxWidthChars: 1, // Doesn't matter what number
+ truncate: 'end',
+ className: `margin-top-5 ${xwayland ? 'txt txt-italic' : 'txt'}`,
+ css: `
+ font-size: ${Math.min(monitors[monitor].width, monitors[monitor].height) * userOptions.overview.scale / 14.6}px;
+ margin: 0px ${Math.min(monitors[monitor].width, monitors[monitor].height) * userOptions.overview.scale / 10}px;
+ `,
+ // If the title is too short, include the class
+ label: (title.length <= 1 ? `${c}: ${title}` : title),
+ })
+ })
+ })
+ ]
+ })
+ }),
+ tooltipText: `${c}: ${title}`,
+ setup: (button) => {
+ setupCursorHoverGrab(button);
+
+ button.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, TARGET, Gdk.DragAction.MOVE);
+ button.drag_source_set_icon_name(substitute(c));
+
+ button.connect('drag-begin', (button) => { // On drag start, add the dragging class
+ button.toggleClassName('overview-tasks-window-dragging', true);
+ });
+ button.connect('drag-data-get', (_w, _c, data) => { // On drag finish, give address
+ data.set_text(address, address.length);
+ button.toggleClassName('overview-tasks-window-dragging', false);
+ });
+ },
+ });
+ }
+
+ const Workspace = (index) => {
+ // const fixed = Widget.Fixed({
+ // attribute: {
+ // put: (widget, x, y) => {
+ // fixed.put(widget, x, y);
+ // },
+ // move: (widget, x, y) => {
+ // fixed.move(widget, x, y);
+ // },
+ // }
+ // });
+ const fixed = Widget.Box({
+ attribute: {
+ put: (widget, x, y) => {
+ if (!widget.attribute) return;
+ // Note: x and y are already multiplied by userOptions.overview.scale
+ const newCss = `
+ margin-left: ${Math.round(x)}px;
+ margin-top: ${Math.round(y)}px;
+ margin-right: -${Math.round(x + (widget.attribute.w * userOptions.overview.scale))}px;
+ margin-bottom: -${Math.round(y + (widget.attribute.h * userOptions.overview.scale))}px;
+ `;
+ widget.css = newCss;
+ fixed.pack_start(widget, false, false, 0);
+ },
+ move: (widget, x, y) => {
+ if (!widget) return;
+ if (!widget.attribute) return;
+ // Note: x and y are already multiplied by userOptions.overview.scale
+ const newCss = `
+ margin-left: ${Math.round(x)}px;
+ margin-top: ${Math.round(y)}px;
+ margin-right: -${Math.round(x + (widget.attribute.w * userOptions.overview.scale))}px;
+ margin-bottom: -${Math.round(y + (widget.attribute.h * userOptions.overview.scale))}px;
+ `;
+ widget.css = newCss;
+ },
+ }
+ })
+ const WorkspaceNumber = ({ index, ...rest }) => Widget.Label({
+ className: 'overview-tasks-workspace-number',
+ label: `${index}`,
+ css: `
+ margin: ${Math.min(monitors[overviewMonitor].width, monitors[overviewMonitor].height) * userOptions.overview.scale * userOptions.overview.wsNumMarginScale}px;
+ font-size: ${monitors[overviewMonitor].height * userOptions.overview.scale * userOptions.overview.wsNumScale}px;
+ `,
+ setup: (self) => self.hook(Hyprland.active.workspace, (self) => {
+ // Update when going to new ws group
+ const currentGroup = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN);
+ self.label = `${currentGroup * NUM_OF_WORKSPACES_SHOWN + index}`;
+ }),
+ ...rest,
+ })
+ const widget = Widget.Box({
+ className: 'overview-tasks-workspace',
+ vpack: 'center',
+ // Rounding and adding 1px to minimum width/height to work around scaling inaccuracy:
+ css: `
+ min-width: ${1 + Math.round(monitors[overviewMonitor].width * userOptions.overview.scale)}px;
+ min-height: ${1 + Math.round(monitors[overviewMonitor].height * userOptions.overview.scale)}px;
+ `,
+ children: [Widget.EventBox({
+ hexpand: true,
+ onPrimaryClick: () => {
+ Hyprland.messageAsync(`dispatch workspace ${index}`);
+ App.closeWindow('overview');
+ },
+ setup: (eventbox) => {
+ eventbox.drag_dest_set(Gtk.DestDefaults.ALL, TARGET, Gdk.DragAction.COPY);
+ eventbox.connect('drag-data-received', (_w, _c, _x, _y, data) => {
+ const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
+ Hyprland.messageAsync(`dispatch movetoworkspacesilent ${index + offset},address:${data.get_text()}`)
+ overviewTick.setValue(!overviewTick.value);
+ });
+ },
+ child: Widget.Overlay({
+ child: Widget.Box({}),
+ overlays: [
+ WorkspaceNumber({ index: index, hpack: 'start', vpack: 'start' }),
+ fixed
+ ]
+ }),
+ })],
+ });
+ const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
+ fixed.attribute.put(WorkspaceNumber(offset + index), 0, 0);
+ widget.clear = () => {
+ const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
+ clientMap.forEach((client, address) => {
+ if (!client) return;
+ if ((client.attribute.ws <= offset || client.attribute.ws > offset + NUM_OF_WORKSPACES_SHOWN) ||
+ (client.attribute.ws == offset + index)) {
+ client.destroy();
+ client = null;
+ clientMap.delete(address);
+ }
+ });
+ }
+ widget.set = (clientJson, screenCoords) => {
+ let c = clientMap.get(clientJson.address);
+ if (c) {
+ if (c.attribute?.ws !== clientJson.workspace.id) {
+ c.destroy();
+ c = null;
+ clientMap.delete(clientJson.address);
+ }
+ else if (c) {
+ c.attribute.w = clientJson.size[0];
+ c.attribute.h = clientJson.size[1];
+ c.attribute.updateIconSize(c);
+ fixed.attribute.move(c,
+ Math.max(0, clientJson.at[0] * userOptions.overview.scale),
+ Math.max(0, clientJson.at[1] * userOptions.overview.scale)
+ );
+ return;
+ }
+ }
+ const newWindow = Window(clientJson, screenCoords);
+ if (newWindow === null) return;
+ // clientMap.set(clientJson.address, newWindow);
+ fixed.attribute.put(newWindow,
+ Math.max(0, newWindow.attribute.x * userOptions.overview.scale),
+ Math.max(0, newWindow.attribute.y * userOptions.overview.scale)
+ );
+ clientMap.set(clientJson.address, newWindow);
+ };
+ widget.unset = (clientAddress) => {
+ const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
+ let c = clientMap.get(clientAddress);
+ if (!c) return;
+ c.destroy();
+ c = null;
+ clientMap.delete(clientAddress);
+ };
+ widget.show = () => {
+ fixed.show_all();
+ }
+ return widget;
+ };
+
+ const arr = (s, n) => {
+ const array = [];
+ for (let i = 0; i < n; i++)
+ array.push(s + i);
+
+ return array;
+ };
+
+ const OverviewRow = ({ startWorkspace, workspaces, windowName = 'overview' }) => Widget.Box({
+ children: arr(startWorkspace, workspaces).map(Workspace),
+ attribute: {
+ workspaceGroup: Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN),
+ monitorMap: [],
+ getMonitorMap: (box) => {
+ execAsync('hyprctl -j monitors').then(monitors => {
+ box.attribute.monitorMap = JSON.parse(monitors).reduce((acc, item) => {
+ acc[item.id] = { x: item.x, y: item.y };
+ return acc;
+ }, {});
+ });
+ },
+ update: (box) => {
+ const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
+ Hyprland.messageAsync('j/clients').then(clients => {
+ const allClients = JSON.parse(clients);
+ const kids = box.get_children();
+ kids.forEach(kid => kid.clear());
+ for (let i = 0; i < allClients.length; i++) {
+ const client = allClients[i];
+ const childID = client.workspace.id - (offset + startWorkspace);
+ if (offset + startWorkspace <= client.workspace.id &&
+ client.workspace.id <= offset + startWorkspace + workspaces) {
+ const screenCoords = box.attribute.monitorMap[client.monitor];
+ if (kids[childID]) {
+ kids[childID].set(client, screenCoords);
+ }
+ continue;
+ }
+ }
+ kids.forEach(kid => kid.show());
+ }).catch(print);
+ },
+ updateWorkspace: (box, id) => {
+ const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
+ if (!( // Not in range, ignore
+ offset + startWorkspace <= id &&
+ id <= offset + startWorkspace + workspaces
+ )) return;
+ // if (!App.getWindow(windowName)?.visible) return;
+ Hyprland.messageAsync('j/clients').then(clients => {
+ const allClients = JSON.parse(clients);
+ const kids = box.get_children();
+ for (let i = 0; i < allClients.length; i++) {
+ const client = allClients[i];
+ if (client.workspace.id != id) continue;
+ const screenCoords = box.attribute.monitorMap[client.monitor];
+ kids[id - (offset + startWorkspace)]?.set(client, screenCoords);
+ }
+ kids[id - (offset + startWorkspace)]?.show();
+ }).catch(print);
+ },
+ },
+ setup: (box) => {
+ box.attribute.getMonitorMap(box);
+ box
+ .hook(overviewTick, (box) => box.attribute.update(box))
+ .hook(Hyprland, (box, clientAddress) => {
+ const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
+ const kids = box.get_children();
+ const client = Hyprland.getClient(clientAddress);
+ if (!client) return;
+ const id = client.workspace.id;
+
+ box.attribute.updateWorkspace(box, id);
+ kids[id - (offset + startWorkspace)]?.unset(clientAddress);
+ }, 'client-removed')
+ .hook(Hyprland, (box, clientAddress) => {
+ const client = Hyprland.getClient(clientAddress);
+ if (!client) return;
+ box.attribute.updateWorkspace(box, client.workspace.id);
+ }, 'client-added')
+ .hook(Hyprland.active.workspace, (box) => {
+ // Full update when going to new ws group
+ const previousGroup = box.attribute.workspaceGroup;
+ const currentGroup = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN);
+ if (currentGroup !== previousGroup) {
+ if (!App.getWindow(windowName) || !App.getWindow(windowName).visible) return;
+ box.attribute.update(box);
+ box.attribute.workspaceGroup = currentGroup;
+ }
+ })
+ .hook(App, (box, name, visible) => { // Update on open
+ if (name == 'overview' && visible) box.attribute.update(box);
+ })
+ },
+ });
+
+ return Widget.Revealer({
+ revealChild: true,
+ // hpack to prevent unneeded expansion in overview-tasks-workspace:
+ hpack: 'center',
+ transition: 'slide_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Widget.Box({
+ vertical: true,
+ className: 'overview-tasks',
+ children: Array.from({ length: userOptions.overview.numOfRows }, (_, index) =>
+ OverviewRow({
+ startWorkspace: 1 + index * userOptions.overview.numOfCols,
+ workspaces: userOptions.overview.numOfCols,
+ })
+ )
+ }),
+ });
+}
diff --git a/modules/home-manager/ags/ags/modules/overview/searchbuttons.js b/modules/home-manager/ags/ags/modules/overview/searchbuttons.js
new file mode 100644
index 0000000..ee2bd13
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/overview/searchbuttons.js
@@ -0,0 +1,176 @@
+const { Gtk } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { execAsync, exec } = Utils;
+import { searchItem } from './searchitem.js';
+import { execAndClose, couldBeMath, launchCustomCommand } from './miscfunctions.js';
+
+export const NoResultButton = () => searchItem({
+ materialIconName: 'Error',
+ name: "Search invalid",
+ content: "No results found!",
+ onActivate: () => {
+ App.closeWindow('overview');
+ },
+});
+
+export const DirectoryButton = ({ parentPath, name, type, icon }) => {
+ const actionText = Widget.Revealer({
+ revealChild: false,
+ transition: "crossfade",
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Widget.Label({
+ className: 'overview-search-results-txt txt txt-small txt-action',
+ label: 'Open',
+ })
+ });
+ const actionTextRevealer = Widget.Revealer({
+ revealChild: false,
+ transition: "slide_left",
+ transitionDuration: userOptions.animations.durationSmall,
+ child: actionText,
+ });
+ return Widget.Button({
+ className: 'overview-search-result-btn',
+ onClicked: () => {
+ App.closeWindow('overview');
+ execAsync(['bash', '-c', `xdg-open '${parentPath}/${name}'`, `&`]).catch(print);
+ },
+ child: Widget.Box({
+ children: [
+ Widget.Box({
+ vertical: false,
+ children: [
+ Widget.Box({
+ className: 'overview-search-results-icon',
+ homogeneous: true,
+ child: Widget.Icon({
+ icon: icon,
+ }),
+ }),
+ Widget.Label({
+ className: 'overview-search-results-txt txt txt-norm',
+ label: name,
+ }),
+ Widget.Box({ hexpand: true }),
+ actionTextRevealer,
+ ]
+ })
+ ]
+ }),
+ setup: (self) => self
+ .on('focus-in-event', (button) => {
+ actionText.revealChild = true;
+ actionTextRevealer.revealChild = true;
+ })
+ .on('focus-out-event', (button) => {
+ actionText.revealChild = false;
+ actionTextRevealer.revealChild = false;
+ })
+ ,
+ })
+}
+
+export const CalculationResultButton = ({ result, text }) => searchItem({
+ materialIconName: 'calculate',
+ name: `Math result`,
+ actionName: "Copy",
+ content: `${result}`,
+ onActivate: () => {
+ App.closeWindow('overview');
+ execAsync(['wl-copy', `${result}`]).catch(print);
+ },
+});
+
+export const DesktopEntryButton = (app) => {
+ const actionText = Widget.Revealer({
+ revealChild: false,
+ transition: "crossfade",
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Widget.Label({
+ className: 'overview-search-results-txt txt txt-small txt-action',
+ label: 'Launch',
+ })
+ });
+ const actionTextRevealer = Widget.Revealer({
+ revealChild: false,
+ transition: "slide_left",
+ transitionDuration: userOptions.animations.durationSmall,
+ child: actionText,
+ });
+ return Widget.Button({
+ className: 'overview-search-result-btn',
+ onClicked: () => {
+ App.closeWindow('overview');
+ app.launch();
+ },
+ child: Widget.Box({
+ children: [
+ Widget.Box({
+ vertical: false,
+ children: [
+ Widget.Box({
+ className: 'overview-search-results-icon',
+ homogeneous: true,
+ child: Widget.Icon({
+ icon: app.iconName,
+ }),
+ }),
+ Widget.Label({
+ className: 'overview-search-results-txt txt txt-norm',
+ label: app.name,
+ }),
+ Widget.Box({ hexpand: true }),
+ actionTextRevealer,
+ ]
+ })
+ ]
+ }),
+ setup: (self) => self
+ .on('focus-in-event', (button) => {
+ actionText.revealChild = true;
+ actionTextRevealer.revealChild = true;
+ })
+ .on('focus-out-event', (button) => {
+ actionText.revealChild = false;
+ actionTextRevealer.revealChild = false;
+ })
+ ,
+ })
+}
+
+export const ExecuteCommandButton = ({ command, terminal = false }) => searchItem({
+ materialIconName: `${terminal ? 'terminal' : 'settings_b_roll'}`,
+ name: `Run command`,
+ actionName: `Execute ${terminal ? 'in terminal' : ''}`,
+ content: `${command}`,
+ onActivate: () => execAndClose(command, terminal),
+ extraClassName: 'techfont',
+})
+
+export const CustomCommandButton = ({ text = '' }) => searchItem({
+ materialIconName: 'settings_suggest',
+ name: 'Action',
+ actionName: 'Run',
+ content: `${text}`,
+ onActivate: () => {
+ App.closeWindow('overview');
+ launchCustomCommand(text);
+ },
+});
+
+export const SearchButton = ({ text = '' }) => searchItem({
+ materialIconName: 'travel_explore',
+ name: 'Search the web',
+ actionName: 'Go',
+ content: `${text}`,
+ onActivate: () => {
+ App.closeWindow('overview');
+ let search = userOptions.search.engineBaseUrl + text;
+ for (let site of userOptions.search.excludedSites) {
+ if (site) search += ` -site:${site}`;
+ }
+ execAsync(['bash', '-c', `xdg-open '${search}' &`]).catch(print);
+ },
+});
diff --git a/modules/home-manager/ags/ags/modules/overview/searchitem.js b/modules/home-manager/ags/ags/modules/overview/searchitem.js
new file mode 100644
index 0000000..2a3303a
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/overview/searchitem.js
@@ -0,0 +1,65 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+
+export const searchItem = ({ materialIconName, name, actionName, content, onActivate, extraClassName = '', ...rest }) => {
+ const actionText = Widget.Revealer({
+ revealChild: false,
+ transition: "crossfade",
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Widget.Label({
+ className: 'overview-search-results-txt txt txt-small txt-action',
+ label: `${actionName}`,
+ })
+ });
+ const actionTextRevealer = Widget.Revealer({
+ revealChild: false,
+ transition: "slide_left",
+ transitionDuration: userOptions.animations.durationSmall,
+ child: actionText,
+ })
+ return Widget.Button({
+ className: `overview-search-result-btn txt ${extraClassName}`,
+ onClicked: onActivate,
+ child: Widget.Box({
+ children: [
+ Widget.Box({
+ vertical: false,
+ children: [
+ Widget.Label({
+ className: `icon-material overview-search-results-icon`,
+ label: `${materialIconName}`,
+ }),
+ Widget.Box({
+ vertical: true,
+ children: [
+ Widget.Label({
+ hpack: 'start',
+ className: 'overview-search-results-txt txt-smallie txt-subtext',
+ label: `${name}`,
+ truncate: "end",
+ }),
+ Widget.Label({
+ hpack: 'start',
+ className: 'overview-search-results-txt txt-norm',
+ label: `${content}`,
+ truncate: "end",
+ }),
+ ]
+ }),
+ Widget.Box({ hexpand: true }),
+ actionTextRevealer,
+ ],
+ })
+ ]
+ }),
+ setup: (self) => self
+ .on('focus-in-event', (button) => {
+ actionText.revealChild = true;
+ actionTextRevealer.revealChild = true;
+ })
+ .on('focus-out-event', (button) => {
+ actionText.revealChild = false;
+ actionTextRevealer.revealChild = false;
+ })
+ ,
+ });
+}
diff --git a/modules/home-manager/ags/ags/modules/overview/windowcontent.js b/modules/home-manager/ags/ags/modules/overview/windowcontent.js
new file mode 100644
index 0000000..3d436b3
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/overview/windowcontent.js
@@ -0,0 +1,212 @@
+const { Gdk, Gtk } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+
+import Applications from 'resource:///com/github/Aylur/ags/service/applications.js';
+const { execAsync, exec } = Utils;
+import { execAndClose, expandTilde, hasUnterminatedBackslash, couldBeMath, launchCustomCommand, ls } from './miscfunctions.js';
+import {
+ CalculationResultButton, CustomCommandButton, DirectoryButton,
+ DesktopEntryButton, ExecuteCommandButton, SearchButton,
+} from './searchbuttons.js';
+import { checkKeybind } from '../.widgetutils/keybind.js';
+
+// Add math funcs
+const { abs, sin, cos, tan, cot, asin, acos, atan, acot } = Math;
+const pi = Math.PI;
+// trigonometric funcs for deg
+const sind = x => sin(x * pi / 180);
+const cosd = x => cos(x * pi / 180);
+const tand = x => tan(x * pi / 180);
+const cotd = x => cot(x * pi / 180);
+const asind = x => asin(x) * 180 / pi;
+const acosd = x => acos(x) * 180 / pi;
+const atand = x => atan(x) * 180 / pi;
+const acotd = x => acot(x) * 180 / pi;
+
+const MAX_RESULTS = 10;
+const OVERVIEW_SCALE = 0.18; // = overview workspace box / screen size
+const OVERVIEW_WS_NUM_SCALE = 0.09;
+const OVERVIEW_WS_NUM_MARGIN_SCALE = 0.07;
+const TARGET = [Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags.SAME_APP, 0)];
+
+function iconExists(iconName) {
+ let iconTheme = Gtk.IconTheme.get_default();
+ return iconTheme.has_icon(iconName);
+}
+
+const OptionalOverview = async () => {
+ try {
+ return (await import('./overview_hyprland.js')).default();
+ } catch {
+ return Widget.Box({});
+ // return (await import('./overview_hyprland.js')).default();
+ }
+};
+
+const overviewContent = await OptionalOverview();
+
+export const SearchAndWindows = () => {
+ var _appSearchResults = [];
+
+ const resultsBox = Widget.Box({
+ className: 'overview-search-results',
+ vertical: true,
+ });
+ const resultsRevealer = Widget.Revealer({
+ transitionDuration: userOptions.animations.durationLarge,
+ revealChild: false,
+ transition: 'slide_down',
+ // duration: 200,
+ hpack: 'center',
+ child: resultsBox,
+ });
+ const entryPromptRevealer = Widget.Revealer({
+ transition: 'crossfade',
+ transitionDuration: userOptions.animations.durationLarge,
+ revealChild: true,
+ hpack: 'center',
+ child: Widget.Label({
+ className: 'overview-search-prompt txt-small txt',
+ label: 'Type to search'
+ }),
+ });
+
+ const entryIconRevealer = Widget.Revealer({
+ transition: 'crossfade',
+ transitionDuration: userOptions.animations.durationLarge,
+ revealChild: false,
+ hpack: 'end',
+ child: Widget.Label({
+ className: 'txt txt-large icon-material overview-search-icon',
+ label: 'search',
+ }),
+ });
+
+ const entryIcon = Widget.Box({
+ className: 'overview-search-prompt-box',
+ setup: box => box.pack_start(entryIconRevealer, true, true, 0),
+ });
+
+ const entry = Widget.Entry({
+ className: 'overview-search-box txt-small txt',
+ hpack: 'center',
+ onAccept: (self) => { // This is when you hit Enter
+ resultsBox.children[0].onClicked();
+ },
+ onChange: (entry) => { // this is when you type
+ const isAction = entry.text[0] == '>';
+ const isDir = (['/', '~'].includes(entry.text[0]));
+ resultsBox.get_children().forEach(ch => ch.destroy());
+
+ // check empty if so then dont do stuff
+ if (entry.text == '') {
+ resultsRevealer.revealChild = false;
+ overviewContent.revealChild = true;
+ entryPromptRevealer.revealChild = true;
+ entryIconRevealer.revealChild = false;
+ entry.toggleClassName('overview-search-box-extended', false);
+ return;
+ }
+ const text = entry.text;
+ resultsRevealer.revealChild = true;
+ overviewContent.revealChild = false;
+ entryPromptRevealer.revealChild = false;
+ entryIconRevealer.revealChild = true;
+ entry.toggleClassName('overview-search-box-extended', true);
+ _appSearchResults = Applications.query(text);
+
+ // Calculate
+ if (userOptions.search.enableFeatures.mathResults && couldBeMath(text)) { // Eval on typing is dangerous; this is a small workaround.
+ try {
+ const fullResult = eval(text.replace(/\^/g, "**"));
+ resultsBox.add(CalculationResultButton({ result: fullResult, text: text }));
+ } catch (e) {
+ // console.log(e);
+ }
+ }
+ if (userOptions.search.enableFeatures.directorySearch && isDir) {
+ var contents = [];
+ contents = ls({ path: text, silent: true });
+ contents.forEach((item) => {
+ resultsBox.add(DirectoryButton(item));
+ })
+ }
+ if (userOptions.search.enableFeatures.actions && isAction) { // Eval on typing is dangerous, this is a workaround.
+ resultsBox.add(CustomCommandButton({ text: entry.text }));
+ }
+ // Add application entries
+ let appsToAdd = MAX_RESULTS;
+ _appSearchResults.forEach(app => {
+ if (appsToAdd == 0) return;
+ resultsBox.add(DesktopEntryButton(app));
+ appsToAdd--;
+ });
+
+ // Fallbacks
+ // if the first word is an actual command
+ if (userOptions.search.enableFeatures.commands && !isAction && !hasUnterminatedBackslash(text) && exec(`bash -c "command -v ${text.split(' ')[0]}"`) != '') {
+ resultsBox.add(ExecuteCommandButton({ command: entry.text, terminal: entry.text.startsWith('sudo') }));
+ }
+
+ // Add fallback: search
+ if (userOptions.search.enableFeatures.aiSearch)
+ resultsBox.add(AiButton({ text: entry.text }));
+ if (userOptions.search.enableFeatures.webSearch)
+ resultsBox.add(SearchButton({ text: entry.text }));
+ if (resultsBox.children.length == 0) resultsBox.add(NoResultButton());
+ resultsBox.show_all();
+ },
+ });
+ return Widget.Box({
+ vertical: true,
+ children: [
+ Widget.Box({
+ hpack: 'center',
+ children: [
+ entry,
+ Widget.Box({
+ className: 'overview-search-icon-box',
+ setup: (box) => {
+ box.pack_start(entryPromptRevealer, true, true, 0)
+ },
+ }),
+ entryIcon,
+ ]
+ }),
+ overviewContent,
+ resultsRevealer,
+ ],
+ setup: (self) => self
+ .hook(App, (_b, name, visible) => {
+ if (name == 'overview' && !visible) {
+ resultsBox.children = [];
+ entry.set_text('');
+ }
+ })
+ .on('key-press-event', (widget, event) => { // Typing
+ const keyval = event.get_keyval()[1];
+ const modstate = event.get_state()[1];
+ if (checkKeybind(event, userOptions.keybinds.overview.altMoveLeft))
+ entry.set_position(Math.max(entry.get_position() - 1, 0));
+ else if (checkKeybind(event, userOptions.keybinds.overview.altMoveRight))
+ entry.set_position(Math.min(entry.get_position() + 1, entry.get_text().length));
+ else if (checkKeybind(event, userOptions.keybinds.overview.deleteToEnd)) {
+ const text = entry.get_text();
+ const pos = entry.get_position();
+ const newText = text.slice(0, pos);
+ entry.set_text(newText);
+ entry.set_position(newText.length);
+ }
+ else if (!(modstate & Gdk.ModifierType.CONTROL_MASK)) { // Ctrl not held
+ if (keyval >= 32 && keyval <= 126 && widget != entry) {
+ Utils.timeout(1, () => entry.grab_focus());
+ entry.set_text(entry.text + String.fromCharCode(keyval));
+ entry.set_position(-1);
+ }
+ }
+ })
+ ,
+ });
+};
diff --git a/modules/home-manager/ags/ags/modules/screencorners/main.js b/modules/home-manager/ags/ags/modules/screencorners/main.js
new file mode 100644
index 0000000..83e09c0
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/screencorners/main.js
@@ -0,0 +1,38 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
+import { enableClickthrough } from "../.widgetutils/clickthrough.js";
+import { RoundedCorner } from "../.commonwidgets/cairo_roundedcorner.js";
+
+if(userOptions.appearance.fakeScreenRounding === 2) Hyprland.connect('event', (service, name, data) => {
+ if (name == 'fullscreen') {
+ const monitor = Hyprland.active.monitor.id;
+ if (data == '1') {
+ for (const window of App.windows) {
+ if (window.name.startsWith("corner") && window.name.endsWith(monitor)) {
+ App.closeWindow(window.name);
+ }
+ }
+ } else {
+ for (const window of App.windows) {
+ if (window.name.startsWith("corner") && window.name.endsWith(monitor)) {
+ App.openWindow(window.name);
+ }
+ }
+ }
+ }
+})
+
+export default (monitor = 0, where = 'bottom left', useOverlayLayer = true) => {
+ const positionString = where.replace(/\s/, ""); // remove space
+ return Widget.Window({
+ monitor,
+ name: `corner${positionString}${monitor}`,
+ layer: useOverlayLayer ? 'overlay' : 'top',
+ anchor: where.split(' '),
+ exclusivity: 'ignore',
+ visible: true,
+ child: RoundedCorner(positionString, { className: 'corner-black', }),
+ setup: enableClickthrough,
+ });
+}
+
diff --git a/modules/home-manager/ags/ags/modules/session/main.js b/modules/home-manager/ags/ags/modules/session/main.js
new file mode 100644
index 0000000..30c5ab6
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/session/main.js
@@ -0,0 +1,14 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import SessionScreen from "./sessionscreen.js";
+import PopupWindow from '../.widgethacks/popupwindow.js';
+
+export default (id = 0) => PopupWindow({ // On-screen keyboard
+ monitor: id,
+ name: `session${id}`,
+ visible: false,
+ keymode: 'on-demand',
+ layer: 'overlay',
+ exclusivity: 'ignore',
+ anchor: ['top', 'bottom', 'left', 'right'],
+ child: SessionScreen({ id: id }),
+})
diff --git a/modules/home-manager/ags/ags/modules/session/sessionscreen.js b/modules/home-manager/ags/ags/modules/session/sessionscreen.js
new file mode 100644
index 0000000..64a3b84
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/session/sessionscreen.js
@@ -0,0 +1,134 @@
+// This is for the cool memory indicator on the sidebar
+// For the right pill of the bar, see system.js
+const { Gdk, Gtk } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+import { monitors } from '../.commondata/hyprlanddata.js';
+
+const { exec, execAsync } = Utils;
+
+const SessionButton = (name, icon, command, props = {}, colorid = 0) => {
+ const buttonDescription = Widget.Revealer({
+ vpack: 'end',
+ transitionDuration: userOptions.animations.durationSmall,
+ transition: 'slide_down',
+ revealChild: false,
+ child: Widget.Label({
+ className: 'txt-smaller session-button-desc',
+ label: name,
+ }),
+ });
+ return Widget.Button({
+ onClicked: command,
+ className: `session-button session-color-${colorid}`,
+ child: Widget.Overlay({
+ className: 'session-button-box',
+ child: Widget.Label({
+ vexpand: true,
+ className: 'icon-material',
+ label: icon,
+ }),
+ overlays: [
+ buttonDescription,
+ ]
+ }),
+ onHover: (button) => {
+ const display = Gdk.Display.get_default();
+ const cursor = Gdk.Cursor.new_from_name(display, 'pointer');
+ button.get_window().set_cursor(cursor);
+ buttonDescription.revealChild = true;
+ },
+ onHoverLost: (button) => {
+ const display = Gdk.Display.get_default();
+ const cursor = Gdk.Cursor.new_from_name(display, 'default');
+ button.get_window().set_cursor(cursor);
+ buttonDescription.revealChild = false;
+ },
+ setup: (self) => self
+ .on('focus-in-event', (self) => {
+ buttonDescription.revealChild = true;
+ self.toggleClassName('session-button-focused', true);
+ })
+ .on('focus-out-event', (self) => {
+ buttonDescription.revealChild = false;
+ self.toggleClassName('session-button-focused', false);
+ })
+ ,
+ ...props,
+ });
+}
+
+export default ({ id = 0 }) => {
+ // lock, logout, sleep
+ const lockButton = SessionButton('Lock', 'lock', () => { closeWindowOnAllMonitors('session'); execAsync(['loginctl', 'lock-session']).catch(print) }, {}, 1);
+ const logoutButton = SessionButton('Logout', 'logout', () => { closeWindowOnAllMonitors('session'); execAsync(['bash', '-c', 'pkill Hyprland || loginctl terminate-user $USER']).catch(print) }, {}, 2);
+ const sleepButton = SessionButton('Sleep', 'sleep', () => { closeWindowOnAllMonitors('session'); execAsync(['bash', '-c', 'systemctl suspend || loginctl suspend']).catch(print) }, {}, 3);
+ // hibernate, shutdown, reboot
+ const hibernateButton = SessionButton('Hibernate', 'downloading', () => { closeWindowOnAllMonitors('session'); execAsync(['bash', '-c', 'systemctl hibernate || loginctl hibernate']).catch(print) }, {}, 4);
+ const shutdownButton = SessionButton('Shutdown', 'power_settings_new', () => { closeWindowOnAllMonitors('session'); execAsync(['bash', '-c', 'systemctl poweroff || loginctl poweroff']).catch(print) }, {}, 5);
+ const rebootButton = SessionButton('Reboot', 'restart_alt', () => { closeWindowOnAllMonitors('session'); execAsync(['bash', '-c', 'systemctl reboot || loginctl reboot']).catch(print) }, {}, 6);
+ const cancelButton = SessionButton('Cancel', 'close', () => closeWindowOnAllMonitors('session'), { className: 'session-button-cancel' }, 7);
+
+ const sessionDescription = Widget.Box({
+ vertical: true,
+ css: 'margin-bottom: 0.682rem;',
+ children: [
+ Widget.Label({
+ className: 'txt-title txt',
+ label: 'Session',
+ }),
+ Widget.Label({
+ justify: Gtk.Justification.CENTER,
+ className: 'txt-small txt',
+ label: 'Use arrow keys to navigate.\nEnter to select, Esc to cancel.'
+ }),
+ ]
+ });
+ const SessionButtonRow = (children) => Widget.Box({
+ hpack: 'center',
+ className: 'spacing-h-15',
+ children: children,
+ });
+ const sessionButtonRows = [
+ SessionButtonRow([lockButton, logoutButton, sleepButton]),
+ SessionButtonRow([hibernateButton, shutdownButton, rebootButton]),
+ SessionButtonRow([cancelButton]),
+ ]
+ return Widget.Box({
+ className: 'session-bg',
+ css: `
+ min-width: ${monitors[id].width}px;
+ min-height: ${monitors[id].height}px;
+ `, // idk why but height = screen height doesn't fill
+ vertical: true,
+ children: [
+ Widget.EventBox({
+ onPrimaryClick: () => closeWindowOnAllMonitors('session'),
+ onSecondaryClick: () => closeWindowOnAllMonitors('session'),
+ onMiddleClick: () => closeWindowOnAllMonitors('session'),
+ }),
+ Widget.Box({
+ hpack: 'center',
+ vexpand: true,
+ vertical: true,
+ children: [
+ Widget.Box({
+ vpack: 'center',
+ vertical: true,
+ className: 'spacing-v-15',
+ children: [
+ sessionDescription,
+ ...sessionButtonRows,
+ ]
+ })
+ ]
+ })
+ ],
+ setup: (self) => self
+ .hook(App, (_b, name, visible) => {
+ if (visible) lockButton.grab_focus(); // Lock is the default option
+ })
+ ,
+ });
+}
diff --git a/modules/home-manager/ags/ags/modules/sideleft/apis/ai_chatmessage.js b/modules/home-manager/ags/ags/modules/sideleft/apis/ai_chatmessage.js
new file mode 100644
index 0000000..4b3a702
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideleft/apis/ai_chatmessage.js
@@ -0,0 +1,365 @@
+const { Gdk, Gio, GLib, Gtk } = imports.gi;
+import GtkSource from "gi://GtkSource?version=3.0";
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { Box, Button, Label, Icon, Scrollable, Stack } = Widget;
+const { execAsync, exec } = Utils;
+import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
+import md2pango from '../../.miscutils/md2pango.js';
+import { darkMode } from "../../.miscutils/system.js";
+
+const LATEX_DIR = `${GLib.get_user_cache_dir()}/ags/media/latex`;
+const CUSTOM_SOURCEVIEW_SCHEME_PATH = `${App.configDir}/assets/themes/sourceviewtheme${darkMode.value ? '' : '-light'}.xml`;
+const CUSTOM_SCHEME_ID = `custom${darkMode.value ? '' : '-light'}`;
+const USERNAME = GLib.get_user_name();
+
+/////////////////////// Custom source view colorscheme /////////////////////////
+
+function loadCustomColorScheme(filePath) {
+ // Read the XML file content
+ const file = Gio.File.new_for_path(filePath);
+ const [success, contents] = file.load_contents(null);
+
+ if (!success) {
+ logError('Failed to load the XML file.');
+ return;
+ }
+
+ // Parse the XML content and set the Style Scheme
+ const schemeManager = GtkSource.StyleSchemeManager.get_default();
+ schemeManager.append_search_path(file.get_parent().get_path());
+}
+loadCustomColorScheme(CUSTOM_SOURCEVIEW_SCHEME_PATH);
+
+//////////////////////////////////////////////////////////////////////////////
+
+function substituteLang(str) {
+ const subs = [
+ { from: 'javascript', to: 'js' },
+ { from: 'bash', to: 'sh' },
+ ];
+ for (const { from, to } of subs) {
+ if (from === str) return to;
+ }
+ return str;
+}
+
+const HighlightedCode = (content, lang) => {
+ const buffer = new GtkSource.Buffer();
+ const sourceView = new GtkSource.View({
+ buffer: buffer,
+ wrap_mode: Gtk.WrapMode.NONE
+ });
+ const langManager = GtkSource.LanguageManager.get_default();
+ let displayLang = langManager.get_language(substituteLang(lang)); // Set your preferred language
+ if (displayLang) {
+ buffer.set_language(displayLang);
+ }
+ const schemeManager = GtkSource.StyleSchemeManager.get_default();
+ buffer.set_style_scheme(schemeManager.get_scheme(CUSTOM_SCHEME_ID));
+ buffer.set_text(content, -1);
+ return sourceView;
+}
+
+const TextBlock = (content = '') => Label({
+ hpack: 'fill',
+ className: 'txt sidebar-chat-txtblock sidebar-chat-txt',
+ useMarkup: true,
+ xalign: 0,
+ wrap: true,
+ selectable: true,
+ label: content,
+});
+
+Utils.execAsync(['bash', '-c', `rm -rf ${LATEX_DIR}`])
+ .then(() => Utils.execAsync(['bash', '-c', `mkdir -p ${LATEX_DIR}`]))
+ .catch(print);
+const Latex = (content = '') => {
+ const latexViewArea = Box({
+ // vscroll: 'never',
+ // hscroll: 'automatic',
+ // homogeneous: true,
+ attribute: {
+ render: async (self, text) => {
+ if (text.length == 0) return;
+ const styleContext = self.get_style_context();
+ const fontSize = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL);
+
+ const timeSinceEpoch = Date.now();
+ const fileName = `${timeSinceEpoch}.tex`;
+ const outFileName = `${timeSinceEpoch}-symbolic.svg`;
+ const outIconName = `${timeSinceEpoch}-symbolic`;
+ const scriptFileName = `${timeSinceEpoch}-render.sh`;
+ const filePath = `${LATEX_DIR}/${fileName}`;
+ const outFilePath = `${LATEX_DIR}/${outFileName}`;
+ const scriptFilePath = `${LATEX_DIR}/${scriptFileName}`;
+
+ Utils.writeFile(text, filePath).catch(print);
+ // Since MicroTex doesn't support file path input properly, we gotta cat it
+ // And escaping such a command is a fucking pain so I decided to just generate a script
+ // Note: MicroTex doesn't support `&=`
+ // You can add this line in the middle for debugging: echo "$text" > ${filePath}.tmp
+ const renderScript = `#!/usr/bin/env bash
+text=$(cat ${filePath} | sed 's/$/ \\\\\\\\/g' | sed 's/&=/=/g')
+cd /opt/MicroTeX
+./LaTeX -headless -input="$text" -output=${outFilePath} -textsize=${fontSize * 1.1} -padding=0 -maxwidth=${latexViewArea.get_allocated_width() * 0.85} > /dev/null 2>&1
+sed -i 's/fill="rgb(0%, 0%, 0%)"/style="fill:#000000"/g' ${outFilePath}
+sed -i 's/stroke="rgb(0%, 0%, 0%)"/stroke="${darkMode.value ? '#ffffff' : '#000000'}"/g' ${outFilePath}
+`;
+ Utils.writeFile(renderScript, scriptFilePath).catch(print);
+ Utils.exec(`chmod a+x ${scriptFilePath}`)
+ Utils.timeout(100, () => {
+ Utils.exec(`bash ${scriptFilePath}`);
+ Gtk.IconTheme.get_default().append_search_path(LATEX_DIR);
+
+ self.child?.destroy();
+ self.child = Gtk.Image.new_from_icon_name(outIconName, 0);
+ })
+ }
+ },
+ setup: (self) => self.attribute.render(self, content).catch(print),
+ });
+ const wholeThing = Box({
+ className: 'sidebar-chat-latex',
+ homogeneous: true,
+ attribute: {
+ 'updateText': (text) => {
+ latexViewArea.attribute.render(latexViewArea, text).catch(print);
+ }
+ },
+ children: [Scrollable({
+ vscroll: 'never',
+ hscroll: 'automatic',
+ child: latexViewArea
+ })]
+ })
+ return wholeThing;
+}
+
+const CodeBlock = (content = '', lang = 'txt') => {
+ if (lang == 'tex' || lang == 'latex') {
+ return Latex(content);
+ }
+ const topBar = Box({
+ className: 'sidebar-chat-codeblock-topbar',
+ children: [
+ Label({
+ label: lang,
+ className: 'sidebar-chat-codeblock-topbar-txt',
+ }),
+ Box({
+ hexpand: true,
+ }),
+ Button({
+ className: 'sidebar-chat-codeblock-topbar-btn',
+ child: Box({
+ className: 'spacing-h-5',
+ children: [
+ MaterialIcon('content_copy', 'small'),
+ Label({
+ label: 'Copy',
+ })
+ ]
+ }),
+ onClicked: (self) => {
+ const buffer = sourceView.get_buffer();
+ const copyContent = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), false); // TODO: fix this
+ execAsync([`wl-copy`, `${copyContent}`]).catch(print);
+ },
+ }),
+ ]
+ })
+ // Source view
+ const sourceView = HighlightedCode(content, lang);
+
+ const codeBlock = Box({
+ attribute: {
+ 'updateText': (text) => {
+ sourceView.get_buffer().set_text(text, -1);
+ }
+ },
+ className: 'sidebar-chat-codeblock',
+ vertical: true,
+ children: [
+ topBar,
+ Box({
+ className: 'sidebar-chat-codeblock-code',
+ homogeneous: true,
+ children: [Scrollable({
+ vscroll: 'never',
+ hscroll: 'automatic',
+ child: sourceView,
+ })],
+ })
+ ]
+ })
+
+ // const schemeIds = styleManager.get_scheme_ids();
+
+ // print("Available Style Schemes:");
+ // for (let i = 0; i < schemeIds.length; i++) {
+ // print(schemeIds[i]);
+ // }
+ return codeBlock;
+}
+
+const Divider = () => Box({
+ className: 'sidebar-chat-divider',
+})
+
+const MessageContent = (content) => {
+ const contentBox = Box({
+ vertical: true,
+ attribute: {
+ 'fullUpdate': (self, content, useCursor = false) => {
+ // Clear and add first text widget
+ const children = contentBox.get_children();
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i];
+ child.destroy();
+ }
+ contentBox.add(TextBlock())
+ // Loop lines. Put normal text in markdown parser
+ // and put code into code highlighter (TODO)
+ let lines = content.split('\n');
+ let lastProcessed = 0;
+ let inCode = false;
+ for (const [index, line] of lines.entries()) {
+ // Code blocks
+ const codeBlockRegex = /^\s*```([a-zA-Z0-9]+)?\n?/;
+ if (codeBlockRegex.test(line)) {
+ const kids = self.get_children();
+ const lastLabel = kids[kids.length - 1];
+ const blockContent = lines.slice(lastProcessed, index).join('\n');
+ if (!inCode) {
+ lastLabel.label = md2pango(blockContent);
+ contentBox.add(CodeBlock('', codeBlockRegex.exec(line)[1]));
+ }
+ else {
+ lastLabel.attribute.updateText(blockContent);
+ contentBox.add(TextBlock());
+ }
+
+ lastProcessed = index + 1;
+ inCode = !inCode;
+ }
+ // Breaks
+ const dividerRegex = /^\s*---/;
+ if (!inCode && dividerRegex.test(line)) {
+ const kids = self.get_children();
+ const lastLabel = kids[kids.length - 1];
+ const blockContent = lines.slice(lastProcessed, index).join('\n');
+ lastLabel.label = md2pango(blockContent);
+ contentBox.add(Divider());
+ contentBox.add(TextBlock());
+ lastProcessed = index + 1;
+ }
+ }
+ if (lastProcessed < lines.length) {
+ const kids = self.get_children();
+ const lastLabel = kids[kids.length - 1];
+ let blockContent = lines.slice(lastProcessed, lines.length).join('\n');
+ if (!inCode)
+ lastLabel.label = `${md2pango(blockContent)}${useCursor ? userOptions.ai.writingCursor : ''}`;
+ else
+ lastLabel.attribute.updateText(blockContent);
+ }
+ // Debug: plain text
+ // contentBox.add(Label({
+ // hpack: 'fill',
+ // className: 'txt sidebar-chat-txtblock sidebar-chat-txt',
+ // useMarkup: false,
+ // xalign: 0,
+ // wrap: true,
+ // selectable: true,
+ // label: '------------------------------\n' + md2pango(content),
+ // }))
+ contentBox.show_all();
+ }
+ }
+ });
+ contentBox.attribute.fullUpdate(contentBox, content, false);
+ return contentBox;
+}
+
+export const ChatMessage = (message, modelName = 'Model') => {
+ const TextSkeleton = (extraClassName = '') => Box({
+ className: `sidebar-chat-message-skeletonline ${extraClassName}`,
+ })
+ const messageContentBox = MessageContent(message.content);
+ const messageLoadingSkeleton = Box({
+ vertical: true,
+ className: 'spacing-v-5',
+ children: Array.from({ length: 3 }, (_, id) => TextSkeleton(`sidebar-chat-message-skeletonline-offset${id}`)),
+ })
+ const messageArea = Stack({
+ homogeneous: message.role !== 'user',
+ transition: 'crossfade',
+ transitionDuration: userOptions.animations.durationLarge,
+ children: {
+ 'thinking': messageLoadingSkeleton,
+ 'message': messageContentBox,
+ },
+ shown: message.thinking ? 'thinking' : 'message',
+ });
+ const thisMessage = Box({
+ className: 'sidebar-chat-message',
+ homogeneous: true,
+ children: [
+ Box({
+ vertical: true,
+ children: [
+ Label({
+ hpack: 'start',
+ xalign: 0,
+ className: `txt txt-bold sidebar-chat-name sidebar-chat-name-${message.role == 'user' ? 'user' : 'bot'}`,
+ wrap: true,
+ useMarkup: true,
+ label: (message.role == 'user' ? USERNAME : modelName),
+ }),
+ Box({
+ homogeneous: true,
+ className: 'sidebar-chat-messagearea',
+ children: [messageArea]
+ })
+ ],
+ setup: (self) => self
+ .hook(message, (self, isThinking) => {
+ messageArea.shown = message.thinking ? 'thinking' : 'message';
+ }, 'notify::thinking')
+ .hook(message, (self) => { // Message update
+ messageContentBox.attribute.fullUpdate(messageContentBox, message.content, message.role != 'user');
+ }, 'notify::content')
+ .hook(message, (label, isDone) => { // Remove the cursor
+ messageContentBox.attribute.fullUpdate(messageContentBox, message.content, false);
+ }, 'notify::done')
+ ,
+ })
+ ]
+ });
+ return thisMessage;
+}
+
+export const SystemMessage = (content, commandName, scrolledWindow) => {
+ const messageContentBox = MessageContent(content);
+ const thisMessage = Box({
+ className: 'sidebar-chat-message',
+ children: [
+ Box({
+ vertical: true,
+ children: [
+ Label({
+ xalign: 0,
+ hpack: 'start',
+ className: 'txt txt-bold sidebar-chat-name sidebar-chat-name-system',
+ wrap: true,
+ label: `System • ${commandName}`,
+ }),
+ messageContentBox,
+ ],
+ })
+ ],
+ });
+ return thisMessage;
+}
diff --git a/modules/home-manager/ags/ags/modules/sideleft/apis/booru.js b/modules/home-manager/ags/ags/modules/sideleft/apis/booru.js
new file mode 100644
index 0000000..340c5d3
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideleft/apis/booru.js
@@ -0,0 +1,523 @@
+const { Gdk, GdkPixbuf, Gio, GLib, Gtk } = imports.gi;
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { Box, Button, EventBox, Label, Overlay, Revealer, Scrollable, Stack } = Widget;
+const { execAsync, exec } = Utils;
+import { fileExists } from '../../.miscutils/files.js';
+import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
+import { MarginRevealer } from '../../.widgethacks/advancedrevealers.js';
+import { setupCursorHover, setupCursorHoverInfo } from '../../.widgetutils/cursorhover.js';
+import BooruService from '../../../services/booru.js';
+import { chatEntry } from '../apiwidgets.js';
+import { ConfigToggle } from '../../.commonwidgets/configwidgets.js';
+import { SystemMessage } from './ai_chatmessage.js';
+
+const IMAGE_REVEAL_DELAY = 13; // Some wait for inits n other weird stuff
+const USER_CACHE_DIR = GLib.get_user_cache_dir();
+
+// Create cache folder and clear pics from previous session
+Utils.exec(`bash -c 'mkdir -p ${USER_CACHE_DIR}/ags/media/waifus'`);
+Utils.exec(`bash -c 'rm ${USER_CACHE_DIR}/ags/media/waifus/*'`);
+
+const TagButton = (command) => Button({
+ className: 'sidebar-chat-chip sidebar-chat-chip-action txt txt-small',
+ onClicked: () => { chatEntry.buffer.text += `${command} ` },
+ setup: setupCursorHover,
+ label: command,
+});
+
+const CommandButton = (command, displayName = command) => Button({
+ className: 'sidebar-chat-chip sidebar-chat-chip-action txt txt-small',
+ onClicked: () => sendMessage(command),
+ setup: setupCursorHover,
+ label: displayName,
+});
+
+export const booruTabIcon = Box({
+ hpack: 'center',
+ homogeneous: true,
+ children: [
+ MaterialIcon('gallery_thumbnail', 'norm'),
+ ]
+});
+
+const BooruInfo = () => {
+ const booruLogo = Label({
+ hpack: 'center',
+ className: 'sidebar-chat-welcome-logo',
+ label: 'gallery_thumbnail',
+ })
+ return Box({
+ vertical: true,
+ vexpand: true,
+ className: 'spacing-v-15',
+ children: [
+ booruLogo,
+ Label({
+ className: 'txt txt-title-small sidebar-chat-welcome-txt',
+ wrap: true,
+ justify: Gtk.Justification.CENTER,
+ label: 'Anime booru',
+ }),
+ Box({
+ className: 'spacing-h-5',
+ hpack: 'center',
+ children: [
+ Label({
+ className: 'txt-smallie txt-subtext',
+ wrap: true,
+ justify: Gtk.Justification.CENTER,
+ label: 'Powered by yande.re and konachan',
+ }),
+ Button({
+ className: 'txt-subtext txt-norm icon-material',
+ label: 'info',
+ tooltipText: 'An image booru. May contain NSFW content.\nWatch your back.\n\nDisclaimer: Not affiliated with the provider\nnor responsible for any of its content.',
+ setup: setupCursorHoverInfo,
+ }),
+ ]
+ }),
+ ]
+ });
+}
+
+export const BooruSettings = () => MarginRevealer({
+ transition: 'slide_down',
+ revealChild: true,
+ child: Box({
+ vertical: true,
+ className: 'sidebar-chat-settings',
+ children: [
+ Box({
+ vertical: true,
+ hpack: 'fill',
+ className: 'sidebar-chat-settings-toggles',
+ children: [
+ ConfigToggle({
+ icon: 'menstrual_health',
+ name: 'Lewds',
+ desc: `Shows naughty stuff when enabled.\nYa like those? Add this to user_options.js:
+'sidebar': {
+ 'image': {
+ 'allowNsfw': true,
+ }
+},`,
+ initValue: BooruService.nsfw,
+ onChange: (self, newValue) => {
+ BooruService.nsfw = newValue;
+ },
+ extraSetup: (self) => self.hook(BooruService, (self) => {
+ self.attribute.enabled.value = BooruService.nsfw;
+ }, 'notify::nsfw')
+ }),
+ ConfigToggle({
+ icon: 'sell',
+ name: 'Save in folder by tags',
+ desc: 'Saves images in folders by their tags',
+ initValue: userOptions.sidebar.image.saveInFolderByTags,
+ onChange: (self, newValue) => {
+ userOptions.sidebar.image.saveInFolderByTags = newValue;
+ },
+ }),
+ ]
+ })
+ ]
+ })
+});
+
+const booruWelcome = Box({
+ vexpand: true,
+ homogeneous: true,
+ child: Box({
+ className: 'spacing-v-15',
+ vpack: 'center',
+ vertical: true,
+ children: [
+ BooruInfo(),
+ BooruSettings(),
+ ]
+ })
+});
+
+const BooruPage = (taglist, serviceName = 'Booru') => {
+ const PageState = (icon, name) => Box({
+ className: 'spacing-h-5 txt',
+ children: [
+ Label({
+ className: 'sidebar-waifu-txt txt-smallie',
+ xalign: 0,
+ label: name,
+ }),
+ MaterialIcon(icon, 'norm'),
+ ]
+ })
+ const ImageAction = ({ name, icon, action }) => Button({
+ className: 'sidebar-waifu-image-action txt-norm icon-material',
+ tooltipText: name,
+ label: icon,
+ onClicked: action,
+ setup: setupCursorHover,
+ })
+ const PreviewImage = (data, delay = 0) => {
+ const imageArea = Widget.DrawingArea({
+ className: 'sidebar-booru-image-drawingarea',
+ });
+ const imageBox = Box({
+ className: 'sidebar-booru-image',
+ // css: `background-image: url('${data.preview_url}');`,
+ attribute: {
+ 'update': (self, data, force = false) => {
+ const imagePath = `${USER_CACHE_DIR}/ags/media/waifus/${data.md5}.${data.file_ext}`;
+ const widgetStyleContext = imageArea.get_style_context();
+ const widgetWidth = widgetStyleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
+ const widgetHeight = widgetWidth / data.aspect_ratio;
+ imageArea.set_size_request(widgetWidth, widgetHeight);
+ const showImage = () => {
+ // const pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(imagePath, widgetWidth, widgetHeight);
+ const pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(imagePath, widgetWidth, widgetHeight, false);
+ imageArea.connect("draw", (widget, cr) => {
+ const borderRadius = widget.get_style_context().get_property('border-radius', Gtk.StateFlags.NORMAL);
+
+ // Draw a rounded rectangle
+ cr.arc(borderRadius, borderRadius, borderRadius, Math.PI, 1.5 * Math.PI);
+ cr.arc(widgetWidth - borderRadius, borderRadius, borderRadius, 1.5 * Math.PI, 2 * Math.PI);
+ cr.arc(widgetWidth - borderRadius, widgetHeight - borderRadius, borderRadius, 0, 0.5 * Math.PI);
+ cr.arc(borderRadius, widgetHeight - borderRadius, borderRadius, 0.5 * Math.PI, Math.PI);
+ cr.closePath();
+ cr.clip();
+
+ // Paint image as bg
+ Gdk.cairo_set_source_pixbuf(cr, pixbuf, (widgetWidth - widgetWidth) / 2, (widgetHeight - widgetHeight) / 2);
+ cr.paint();
+ });
+ self.queue_draw();
+ imageRevealer.revealChild = true;
+ }
+ // Show
+ // const downloadCommand = `wget -O '${imagePath}' '${data.preview_url}'`;
+ const downloadCommand = `curl -L -o '${imagePath}' '${data.preview_url}'`;
+ if (!force && fileExists(imagePath)) showImage();
+ else Utils.timeout(delay, () => Utils.execAsync(['bash', '-c', downloadCommand])
+ .then(showImage)
+ .catch(print)
+ );
+ },
+ },
+ child: imageArea,
+ setup: (self) => {
+ Utils.timeout(1000, () => self.attribute.update(self, data));
+ }
+ });
+ const imageActions = Revealer({
+ transition: 'crossfade',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Box({
+ vpack: 'start',
+ className: 'sidebar-booru-image-actions spacing-h-3',
+ children: [
+ Box({ hexpand: true }),
+ ImageAction({
+ name: 'Go to file url',
+ icon: 'file_open',
+ action: () => execAsync(['xdg-open', `${data.file_url}`]).catch(print),
+ }),
+ ImageAction({
+ name: 'Go to source',
+ icon: 'open_in_new',
+ action: () => execAsync(['xdg-open', `${data.source}`]).catch(print),
+ }),
+ ImageAction({
+ name: 'Save image',
+ icon: 'save',
+ action: (self) => {
+ const currentTags = BooruService.queries.at(-1).realTagList.filter(tag => !tag.includes('rating:'));
+ const tagDirectory = currentTags.join('+');
+ let fileExtension = data.file_ext || 'jpg';
+ const saveCommand = `mkdir -p $(xdg-user-dir PICTURES)/homework/${data.is_nsfw ? '🌶️/' : ''}${userOptions.sidebar.image.saveInFolderByTags ? tagDirectory : ''} && curl -L -o $(xdg-user-dir PICTURES)/homework/${data.is_nsfw ? '🌶️/' : ''}${userOptions.sidebar.image.saveInFolderByTags ? (tagDirectory + '/') : ''}${data.md5}.${fileExtension} '${data.file_url}'`;
+ execAsync(['bash', '-c', saveCommand])
+ .then(() => self.label = 'done')
+ .catch(print);
+ },
+ }),
+ ]
+ })
+ });
+ const imageOverlay = Overlay({
+ passThrough: true,
+ child: imageBox,
+ overlays: [imageActions]
+ });
+ const imageRevealer = Revealer({
+ transition: 'slide_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: EventBox({
+ onHover: () => { imageActions.revealChild = true },
+ onHoverLost: () => { imageActions.revealChild = false },
+ child: imageOverlay,
+ })
+ })
+ return imageRevealer;
+ }
+ const downloadState = Stack({
+ homogeneous: false,
+ transition: 'slide_up_down',
+ transitionDuration: userOptions.animations.durationSmall,
+ children: {
+ 'api': PageState('api', 'Calling API'),
+ 'download': PageState('downloading', 'Downloading image'),
+ 'done': PageState('done', 'Finished!'),
+ 'error': PageState('error', 'Error'),
+ },
+ });
+ const downloadIndicator = MarginRevealer({
+ vpack: 'center',
+ transition: 'slide_left',
+ revealChild: true,
+ child: downloadState,
+ });
+ const pageHeading = Box({
+ vertical: true,
+ children: [
+ Box({
+ children: [
+ Label({
+ hpack: 'start',
+ className: `sidebar-booru-provider`,
+ label: `${serviceName}`,
+ truncate: 'end',
+ maxWidthChars: 20,
+ }),
+ Box({ hexpand: true }),
+ downloadIndicator,
+ ]
+ }),
+ Box({
+ children: [
+ Scrollable({
+ hexpand: true,
+ vscroll: 'never',
+ hscroll: 'automatic',
+ child: Box({
+ hpack: 'fill',
+ className: 'spacing-h-5',
+ children: [
+ ...taglist.map((tag) => TagButton(tag)),
+ Box({ hexpand: true }),
+ ]
+ })
+ }),
+ ]
+ })
+ ]
+ });
+ const pageImages = Box({
+ hpack: 'start',
+ homogeneous: true,
+ className: 'sidebar-booru-imagegrid',
+ })
+ const pageImageRevealer = Revealer({
+ transition: 'slide_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ revealChild: false,
+ child: pageImages,
+ });
+ const thisPage = Box({
+ homogeneous: true,
+ className: 'sidebar-chat-message',
+ attribute: {
+ 'imagePath': '',
+ 'isNsfw': false,
+ 'update': (data, force = false) => { // TODO: Use columns. Sort min to max h/w ratio then greedily put em in...
+ // Sort by .aspect_ratio
+ data = data.sort(
+ (a, b) => a.aspect_ratio - b.aspect_ratio
+ );
+ if (data.length == 0) {
+ downloadState.shown = 'error';
+ return;
+ }
+ const imageColumns = userOptions.sidebar.image.columns;
+ const imageRows = data.length / imageColumns;
+
+ // Init cols
+ pageImages.children = Array.from(
+ { length: imageColumns },
+ (_, i) => Box({
+ attribute: { height: 0 },
+ vertical: true,
+ })
+ );
+ // Greedy add O(n^2) 😭
+ for (let i = 0; i < data.length; i++) {
+ // Find column with lowest length
+ let minHeight = Infinity;
+ let minIndex = -1;
+ for (let j = 0; j < imageColumns; j++) {
+ const height = pageImages.children[j].attribute.height;
+ if (height < minHeight) {
+ minHeight = height;
+ minIndex = j;
+ }
+ }
+ // Add image to it
+ pageImages.children[minIndex].pack_start(PreviewImage(data[i], minIndex), false, false, 0)
+ pageImages.children[minIndex].attribute.height += 1 / data[i].aspect_ratio; // we want height/width
+ }
+ pageImages.show_all();
+
+ // Reveal stuff
+ Utils.timeout(IMAGE_REVEAL_DELAY,
+ () => pageImageRevealer.revealChild = true
+ );
+ downloadIndicator.attribute.hide();
+ },
+ },
+ children: [Box({
+ vertical: true,
+ children: [
+ pageHeading,
+ Box({
+ vertical: true,
+ children: [pageImageRevealer],
+ })
+ ]
+ })],
+ });
+ return thisPage;
+}
+
+const booruContent = Box({
+ className: 'spacing-v-15',
+ vertical: true,
+ attribute: {
+ 'map': new Map(),
+ },
+ setup: (self) => self
+ .hook(BooruService, (box, id) => {
+ if (id === undefined) return;
+ const newPage = BooruPage(BooruService.queries[id].taglist, BooruService.queries[id].providerName);
+ box.add(newPage);
+ box.show_all();
+ box.attribute.map.set(id, newPage);
+ }, 'newResponse')
+ .hook(BooruService, (box, id) => {
+ if (id === undefined) return;
+ if (!BooruService.responses[id]) return;
+ box.attribute.map.get(id)?.attribute.update(BooruService.responses[id]);
+ }, 'updateResponse')
+ ,
+});
+
+export const booruView = Scrollable({
+ className: 'sidebar-chat-viewport',
+ vexpand: true,
+ child: Box({
+ vertical: true,
+ children: [
+ booruWelcome,
+ booruContent,
+ ]
+ }),
+ setup: (scrolledWindow) => {
+ // Show scrollbar
+ scrolledWindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
+ const vScrollbar = scrolledWindow.get_vscrollbar();
+ vScrollbar.get_style_context().add_class('sidebar-scrollbar');
+ // Avoid click-to-scroll-widget-to-view behavior
+ Utils.timeout(1, () => {
+ const viewport = scrolledWindow.child;
+ viewport.set_focus_vadjustment(new Gtk.Adjustment(undefined));
+ })
+ // Scroll to bottom with new content if chat entry not focused
+ const adjustment = scrolledWindow.get_vadjustment();
+ adjustment.connect("changed", () => {
+ if (!chatEntry.hasFocus) return;
+ adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size());
+ })
+ }
+});
+
+const booruTags = Revealer({
+ revealChild: false,
+ transition: 'crossfade',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Box({
+ className: 'spacing-h-5',
+ children: [
+ Scrollable({
+ vscroll: 'never',
+ hscroll: 'automatic',
+ hexpand: true,
+ child: Box({
+ className: 'spacing-h-5',
+ children: [
+ TagButton('( * )'),
+ TagButton('hololive'),
+ ]
+ })
+ }),
+ Box({ className: 'separator-line' }),
+ ]
+ })
+});
+
+export const booruCommands = Box({
+ className: 'spacing-h-5',
+ setup: (self) => {
+ self.pack_end(CommandButton('/clear'), false, false, 0);
+ self.pack_end(CommandButton('/next'), false, false, 0);
+ self.pack_start(Button({
+ className: 'sidebar-chat-chip-toggle',
+ setup: setupCursorHover,
+ label: 'Tags →',
+ onClicked: () => {
+ booruTags.revealChild = !booruTags.revealChild;
+ }
+ }), false, false, 0);
+ self.pack_start(booruTags, true, true, 0);
+ }
+});
+
+const clearChat = () => { // destroy!!
+ booruContent.attribute.map.forEach((value, key, map) => {
+ value.destroy();
+ value = null;
+ });
+}
+
+export const sendMessage = (text) => {
+ // Commands
+ if (text.startsWith('+')) { // Next page
+ const lastQuery = BooruService.queries.at(-1);
+ BooruService.fetch(`${lastQuery.realTagList.join(' ')} ${lastQuery.page + 1}`)
+ }
+ else if (text.startsWith('/')) {
+ if (text.startsWith('/clear')) clearChat();
+ else if (text.startsWith('/safe')) {
+ BooruService.nsfw = false;
+ const message = SystemMessage(`Switched to safe mode`, '/safe', booruView)
+ booruContent.add(message);
+ booruContent.show_all();
+ booruContent.attribute.map.set(Date.now(), message);
+ }
+ else if (text.startsWith('/lewd')) {
+ BooruService.nsfw = true;
+ const message = SystemMessage(`Tiddies enabled`, '/lewd', booruView)
+ booruContent.add(message);
+ booruContent.show_all();
+ booruContent.attribute.map.set(Date.now(), message);
+ }
+ else if (text.startsWith('/mode')) {
+ const mode = text.slice(text.indexOf(' ') + 1);
+ BooruService.mode = mode;
+ const message = SystemMessage(`Changed provider to ${BooruService.providerName}`, '/mode', booruView)
+ booruContent.add(message);
+ booruContent.show_all();
+ booruContent.attribute.map.set(Date.now(), message);
+ }
+ else if (text.startsWith('/next')) {
+ sendMessage('+')
+ }
+ }
+ else BooruService.fetch(text);
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/sideleft/apis/chatgpt.js b/modules/home-manager/ags/ags/modules/sideleft/apis/chatgpt.js
new file mode 100644
index 0000000..09fc5a2
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideleft/apis/chatgpt.js
@@ -0,0 +1,355 @@
+const { Gtk } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+
+const { Box, Button, Icon, Label, Revealer, Scrollable } = Widget;
+import GPTService from '../../../services/gpt.js';
+import { setupCursorHover, setupCursorHoverInfo } from '../../.widgetutils/cursorhover.js';
+import { SystemMessage, ChatMessage } from "./ai_chatmessage.js";
+import { ConfigToggle, ConfigSegmentedSelection, ConfigGap } from '../../.commonwidgets/configwidgets.js';
+import { markdownTest } from '../../.miscutils/md2pango.js';
+import { MarginRevealer } from '../../.widgethacks/advancedrevealers.js';
+import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
+import { chatEntry } from '../apiwidgets.js';
+
+export const chatGPTTabIcon = Icon({
+ hpack: 'center',
+ icon: `ollama-symbolic`,
+});
+
+const ProviderSwitcher = () => {
+ const ProviderChoice = (id, provider) => {
+ const providerSelected = MaterialIcon('check', 'norm', {
+ setup: (self) => self.hook(GPTService, (self) => {
+ self.toggleClassName('invisible', GPTService.providerID !== id);
+ }, 'providerChanged')
+ });
+ return Button({
+ tooltipText: provider.description,
+ onClicked: () => {
+ GPTService.providerID = id;
+ providerList.revealChild = false;
+ indicatorChevron.label = 'expand_more';
+ },
+ child: Box({
+ className: 'spacing-h-10 txt',
+ children: [
+ Icon({
+ icon: provider['logo_name'],
+ className: 'txt-large'
+ }),
+ Label({
+ hexpand: true,
+ xalign: 0,
+ className: 'txt-small',
+ label: provider.name,
+ }),
+ providerSelected
+ ],
+ }),
+ setup: setupCursorHover,
+ });
+ }
+ const indicatorChevron = MaterialIcon('expand_more', 'norm');
+ const indicatorButton = Button({
+ tooltipText: 'Select ChatGPT-compatible API provider',
+ child: Box({
+ className: 'spacing-h-10 txt',
+ children: [
+ MaterialIcon('cloud', 'norm'),
+ Label({
+ hexpand: true,
+ xalign: 0,
+ className: 'txt-small',
+ label: GPTService.providerID,
+ setup: (self) => self.hook(GPTService, (self) => {
+ self.label = `${GPTService.providers[GPTService.providerID]['name']}`;
+ }, 'providerChanged')
+ }),
+ indicatorChevron,
+ ]
+ }),
+ onClicked: () => {
+ providerList.revealChild = !providerList.revealChild;
+ indicatorChevron.label = (providerList.revealChild ? 'expand_less' : 'expand_more');
+ },
+ setup: setupCursorHover,
+ });
+ const providerList = Revealer({
+ revealChild: false,
+ transition: 'slide_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Box({
+ vertical: true, className: 'spacing-v-5 sidebar-chat-providerswitcher-list',
+ children: [
+ Box({ className: 'separator-line margin-top-5 margin-bottom-5' }),
+ Box({
+ className: 'spacing-v-5',
+ vertical: true,
+ setup: (self) => self.hook(GPTService, (self) => {
+ self.children = Object.entries(GPTService.providers)
+ .map(([id, provider]) => ProviderChoice(id, provider));
+ }, 'initialized'),
+ })
+ ]
+ })
+ })
+ return Box({
+ hpack: 'center',
+ vertical: true,
+ className: 'sidebar-chat-providerswitcher',
+ children: [
+ indicatorButton,
+ providerList,
+ ]
+ })
+}
+
+const GPTInfo = () => {
+ const openAiLogo = Icon({
+ hpack: 'center',
+ className: 'sidebar-chat-welcome-logo',
+ icon: `ollama-symbolic`,
+ });
+ return Box({
+ vertical: true,
+ className: 'spacing-v-15',
+ children: [
+ openAiLogo,
+ Label({
+ className: 'txt txt-title-small sidebar-chat-welcome-txt',
+ wrap: true,
+ justify: Gtk.Justification.CENTER,
+ label: 'Assistant',
+ }),
+ Box({
+ className: 'spacing-h-5',
+ hpack: 'center',
+ children: [
+ Label({
+ className: 'txt-smallie txt-subtext',
+ wrap: true,
+ justify: Gtk.Justification.CENTER,
+ label: 'Provider shown above',
+ }),
+ Button({
+ className: 'txt-subtext txt-norm icon-material',
+ label: 'info',
+ tooltipText: 'Uses gpt-3.5-turbo.\nNot affiliated, endorsed, or sponsored by OpenAI.\n\nPrivacy: OpenAI claims they do not use your data\nwhen you use their API. Idk about others.',
+ setup: setupCursorHoverInfo,
+ }),
+ ]
+ }),
+ ]
+ });
+}
+
+const GPTSettings = () => MarginRevealer({
+ transition: 'slide_down',
+ revealChild: true,
+ extraSetup: (self) => self
+ .hook(GPTService, (self) => Utils.timeout(200, () => {
+ self.attribute.hide();
+ }), 'newMsg')
+ .hook(GPTService, (self) => Utils.timeout(200, () => {
+ self.attribute.show();
+ }), 'clear')
+ ,
+ child: Box({
+ vertical: true,
+ className: 'sidebar-chat-settings',
+ children: [
+ ConfigSegmentedSelection({
+ hpack: 'center',
+ icon: 'casino',
+ name: 'Randomness',
+ desc: 'The model\'s temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1',
+ options: [
+ { value: 0.00, name: 'Precise', },
+ { value: 0.50, name: 'Balanced', },
+ { value: 1.00, name: 'Creative', },
+ ],
+ initIndex: 2,
+ onChange: (value, name) => {
+ GPTService.temperature = value;
+ },
+ }),
+ ConfigGap({ vertical: true, size: 10 }), // Note: size can only be 5, 10, or 15
+ Box({
+ vertical: true,
+ hpack: 'fill',
+ className: 'sidebar-chat-settings-toggles',
+ children: [
+ ConfigToggle({
+ icon: 'model_training',
+ name: 'Enhancements',
+ desc: 'Tells the model:\n- It\'s a Linux sidebar assistant\n- Be brief and use bullet points',
+ initValue: GPTService.assistantPrompt,
+ onChange: (self, newValue) => {
+ GPTService.assistantPrompt = newValue;
+ },
+ }),
+ ]
+ })
+ ]
+ })
+});
+
+export const OpenaiApiKeyInstructions = () => Box({
+ homogeneous: true,
+ children: [Revealer({
+ transition: 'slide_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ setup: (self) => self
+ .hook(GPTService, (self, hasKey) => {
+ self.revealChild = (GPTService.key.length == 0);
+ }, 'hasKey')
+ ,
+ child: Button({
+ child: Label({
+ useMarkup: true,
+ wrap: true,
+ className: 'txt sidebar-chat-welcome-txt',
+ justify: Gtk.Justification.CENTER,
+ label: 'An API key is required\nYou can grab one here, then enter it below'
+ }),
+ setup: setupCursorHover,
+ onClicked: () => {
+ Utils.execAsync(['bash', '-c', `xdg-open ${GPTService.getKeyUrl}`]);
+ }
+ })
+ })]
+});
+
+const GPTWelcome = () => Box({
+ vexpand: true,
+ homogeneous: true,
+ child: Box({
+ className: 'spacing-v-15',
+ vpack: 'center',
+ vertical: true,
+ children: [
+ GPTInfo(),
+ OpenaiApiKeyInstructions(),
+ GPTSettings(),
+ ]
+ })
+});
+
+export const chatContent = Box({
+ className: 'spacing-v-5',
+ vertical: true,
+ setup: (self) => self
+ .hook(GPTService, (box, id) => {
+ const message = GPTService.messages[id];
+ if (!message) return;
+ box.add(ChatMessage(message, `Model (${GPTService.providers[GPTService.providerID]['name']})`))
+ }, 'newMsg')
+ ,
+});
+
+const clearChat = () => {
+ GPTService.clear();
+ const children = chatContent.get_children();
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i];
+ child.destroy();
+ }
+}
+
+const CommandButton = (command) => Button({
+ className: 'sidebar-chat-chip sidebar-chat-chip-action txt txt-small',
+ onClicked: () => sendMessage(command),
+ setup: setupCursorHover,
+ label: command,
+});
+
+export const chatGPTCommands = Box({
+ className: 'spacing-h-5',
+ children: [
+ Box({ hexpand: true }),
+ CommandButton('/key'),
+ CommandButton('/model'),
+ CommandButton('/clear'),
+ ]
+});
+
+export const sendMessage = (text) => {
+ // Check if text or API key is empty
+ if (text.length == 0) return;
+ if (GPTService.key.length == 0) {
+ GPTService.key = text;
+ chatContent.add(SystemMessage(`Key saved to\n\`${GPTService.keyPath}\``, 'API Key', chatGPTView));
+ text = '';
+ return;
+ }
+ // Commands
+ if (text.startsWith('/')) {
+ if (text.startsWith('/clear')) clearChat();
+ else if (text.startsWith('/model')) chatContent.add(SystemMessage(`Currently using \`${GPTService.modelName}\``, '/model', chatGPTView))
+ else if (text.startsWith('/prompt')) {
+ const firstSpaceIndex = text.indexOf(' ');
+ const prompt = text.slice(firstSpaceIndex + 1);
+ if (firstSpaceIndex == -1 || prompt.length < 1) {
+ chatContent.add(SystemMessage(`Usage: \`/prompt MESSAGE\``, '/prompt', chatGPTView))
+ }
+ else {
+ GPTService.addMessage('user', prompt)
+ }
+ }
+ else if (text.startsWith('/key')) {
+ const parts = text.split(' ');
+ if (parts.length == 1) chatContent.add(SystemMessage(
+ `Key stored in:\n\`${GPTService.keyPath}\`\nTo update this key, type \`/key YOUR_API_KEY\``,
+ '/key',
+ chatGPTView));
+ else {
+ GPTService.key = parts[1];
+ chatContent.add(SystemMessage(`Updated API Key at\n\`${GPTService.keyPath}\``, '/key', chatGPTView));
+ }
+ }
+ else if (text.startsWith('/test'))
+ chatContent.add(SystemMessage(markdownTest, `Markdown test`, chatGPTView));
+ else
+ chatContent.add(SystemMessage(`Invalid command.`, 'Error', chatGPTView))
+ }
+ else {
+ GPTService.send(text);
+ }
+}
+
+export const chatGPTView = Box({
+ vertical: true,
+ children: [
+ ProviderSwitcher(),
+ Scrollable({
+ className: 'sidebar-chat-viewport',
+ vexpand: true,
+ child: Box({
+ vertical: true,
+ children: [
+ GPTWelcome(),
+ chatContent,
+ ]
+ }),
+ setup: (scrolledWindow) => {
+ // Show scrollbar
+ scrolledWindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
+ const vScrollbar = scrolledWindow.get_vscrollbar();
+ vScrollbar.get_style_context().add_class('sidebar-scrollbar');
+ // Avoid click-to-scroll-widget-to-view behavior
+ Utils.timeout(1, () => {
+ const viewport = scrolledWindow.child;
+ viewport.set_focus_vadjustment(new Gtk.Adjustment(undefined));
+ })
+ // Always scroll to bottom with new content
+ const adjustment = scrolledWindow.get_vadjustment();
+ adjustment.connect("changed", () => {
+ if(!chatEntry.hasFocus) return;
+ adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size());
+ })
+ }
+ })
+ ]
+});
diff --git a/modules/home-manager/ags/ags/modules/sideleft/apis/gemini.js b/modules/home-manager/ags/ags/modules/sideleft/apis/gemini.js
new file mode 100644
index 0000000..8fd8923
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideleft/apis/gemini.js
@@ -0,0 +1,288 @@
+const { Gtk } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+
+const { Box, Button, Icon, Label, Revealer, Scrollable } = Widget;
+import GeminiService from '../../../services/gemini.js';
+import { setupCursorHover, setupCursorHoverInfo } from '../../.widgetutils/cursorhover.js';
+import { SystemMessage, ChatMessage } from "./ai_chatmessage.js";
+import { ConfigToggle, ConfigSegmentedSelection, ConfigGap } from '../../.commonwidgets/configwidgets.js';
+import { markdownTest } from '../../.miscutils/md2pango.js';
+import { MarginRevealer } from '../../.widgethacks/advancedrevealers.js';
+import { chatEntry } from '../apiwidgets.js';
+
+const MODEL_NAME = `Gemini`;
+
+export const geminiTabIcon = Icon({
+ hpack: 'center',
+ icon: `google-gemini-symbolic`,
+})
+
+const GeminiInfo = () => {
+ const geminiLogo = Icon({
+ hpack: 'center',
+ className: 'sidebar-chat-welcome-logo',
+ icon: `google-gemini-symbolic`,
+ });
+ return Box({
+ vertical: true,
+ className: 'spacing-v-15',
+ children: [
+ geminiLogo,
+ Label({
+ className: 'txt txt-title-small sidebar-chat-welcome-txt',
+ wrap: true,
+ justify: Gtk.Justification.CENTER,
+ label: 'Assistant (Gemini)',
+ }),
+ Box({
+ className: 'spacing-h-5',
+ hpack: 'center',
+ children: [
+ Label({
+ className: 'txt-smallie txt-subtext',
+ wrap: true,
+ justify: Gtk.Justification.CENTER,
+ label: 'Powered by Google',
+ }),
+ Button({
+ className: 'txt-subtext txt-norm icon-material',
+ label: 'info',
+ tooltipText: 'Uses gemini-pro.\nNot affiliated, endorsed, or sponsored by Google.\n\nPrivacy: Chat messages aren\'t linked to your account,\n but will be read by human reviewers to improve the model.',
+ setup: setupCursorHoverInfo,
+ }),
+ ]
+ }),
+ ]
+ });
+}
+
+export const GeminiSettings = () => MarginRevealer({
+ transition: 'slide_down',
+ revealChild: true,
+ extraSetup: (self) => self
+ .hook(GeminiService, (self) => Utils.timeout(200, () => {
+ self.attribute.hide();
+ }), 'newMsg')
+ .hook(GeminiService, (self) => Utils.timeout(200, () => {
+ self.attribute.show();
+ }), 'clear')
+ ,
+ child: Box({
+ vertical: true,
+ className: 'sidebar-chat-settings',
+ children: [
+ ConfigSegmentedSelection({
+ hpack: 'center',
+ icon: 'casino',
+ name: 'Randomness',
+ desc: 'Gemini\'s temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1',
+ options: [
+ { value: 0.00, name: 'Precise', },
+ { value: 0.50, name: 'Balanced', },
+ { value: 1.00, name: 'Creative', },
+ ],
+ initIndex: 2,
+ onChange: (value, name) => {
+ GeminiService.temperature = value;
+ },
+ }),
+ ConfigGap({ vertical: true, size: 10 }), // Note: size can only be 5, 10, or 15
+ Box({
+ vertical: true,
+ hpack: 'fill',
+ className: 'sidebar-chat-settings-toggles',
+ children: [
+ ConfigToggle({
+ icon: 'model_training',
+ name: 'Enhancements',
+ desc: 'Tells Gemini:\n- It\'s a Linux sidebar assistant\n- Be brief and use bullet points',
+ initValue: GeminiService.assistantPrompt,
+ onChange: (self, newValue) => {
+ GeminiService.assistantPrompt = newValue;
+ },
+ }),
+ ConfigToggle({
+ icon: 'shield',
+ name: 'Safety',
+ desc: 'When turned off, tells the API (not the model) \nto not block harmful/explicit content',
+ initValue: GeminiService.safe,
+ onChange: (self, newValue) => {
+ GeminiService.safe = newValue;
+ },
+ }),
+ ConfigToggle({
+ icon: 'history',
+ name: 'History',
+ desc: 'Saves chat history\nMessages in previous chats won\'t show automatically, but they are there',
+ initValue: GeminiService.useHistory,
+ onChange: (self, newValue) => {
+ GeminiService.useHistory = newValue;
+ },
+ }),
+ ]
+ })
+ ]
+ })
+});
+
+export const GoogleAiInstructions = () => Box({
+ homogeneous: true,
+ children: [Revealer({
+ transition: 'slide_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ setup: (self) => self
+ .hook(GeminiService, (self, hasKey) => {
+ self.revealChild = (GeminiService.key.length == 0);
+ }, 'hasKey')
+ ,
+ child: Button({
+ child: Label({
+ useMarkup: true,
+ wrap: true,
+ className: 'txt sidebar-chat-welcome-txt',
+ justify: Gtk.Justification.CENTER,
+ label: 'A Google AI API key is required\nYou can grab one here, then enter it below',
+ // setup: self => self.set_markup("This is a test link")
+ }),
+ setup: setupCursorHover,
+ onClicked: () => {
+ Utils.execAsync(['bash', '-c', `xdg-open https://makersuite.google.com/app/apikey &`]);
+ }
+ })
+ })]
+});
+
+const geminiWelcome = Box({
+ vexpand: true,
+ homogeneous: true,
+ child: Box({
+ className: 'spacing-v-15',
+ vpack: 'center',
+ vertical: true,
+ children: [
+ GeminiInfo(),
+ GoogleAiInstructions(),
+ GeminiSettings(),
+ ]
+ })
+});
+
+export const chatContent = Box({
+ className: 'spacing-v-5',
+ vertical: true,
+ setup: (self) => self
+ .hook(GeminiService, (box, id) => {
+ const message = GeminiService.messages[id];
+ if (!message) return;
+ box.add(ChatMessage(message, MODEL_NAME))
+ }, 'newMsg')
+ ,
+});
+
+const clearChat = () => {
+ GeminiService.clear();
+ const children = chatContent.get_children();
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i];
+ child.destroy();
+ }
+}
+
+const CommandButton = (command) => Button({
+ className: 'sidebar-chat-chip sidebar-chat-chip-action txt txt-small',
+ onClicked: () => sendMessage(command),
+ setup: setupCursorHover,
+ label: command,
+});
+
+export const geminiCommands = Box({
+ className: 'spacing-h-5',
+ children: [
+ Box({ hexpand: true }),
+ CommandButton('/key'),
+ CommandButton('/model'),
+ CommandButton('/clear'),
+ ]
+});
+
+export const sendMessage = (text) => {
+ // Check if text or API key is empty
+ if (text.length == 0) return;
+ if (GeminiService.key.length == 0) {
+ GeminiService.key = text;
+ chatContent.add(SystemMessage(`Key saved to\n\`${GeminiService.keyPath}\``, 'API Key', geminiView));
+ text = '';
+ return;
+ }
+ // Commands
+ if (text.startsWith('/')) {
+ if (text.startsWith('/clear')) clearChat();
+ else if (text.startsWith('/load')) {
+ clearChat();
+ GeminiService.loadHistory();
+ }
+ else if (text.startsWith('/model')) chatContent.add(SystemMessage(`Currently using \`${GeminiService.modelName}\``, '/model', geminiView))
+ else if (text.startsWith('/prompt')) {
+ const firstSpaceIndex = text.indexOf(' ');
+ const prompt = text.slice(firstSpaceIndex + 1);
+ if (firstSpaceIndex == -1 || prompt.length < 1) {
+ chatContent.add(SystemMessage(`Usage: \`/prompt MESSAGE\``, '/prompt', geminiView))
+ }
+ else {
+ GeminiService.addMessage('user', prompt)
+ }
+ }
+ else if (text.startsWith('/key')) {
+ const parts = text.split(' ');
+ if (parts.length == 1) chatContent.add(SystemMessage(
+ `Key stored in:\n\`${GeminiService.keyPath}\`\nTo update this key, type \`/key YOUR_API_KEY\``,
+ '/key',
+ geminiView));
+ else {
+ GeminiService.key = parts[1];
+ chatContent.add(SystemMessage(`Updated API Key at\n\`${GeminiService.keyPath}\``, '/key', geminiView));
+ }
+ }
+ else if (text.startsWith('/test'))
+ chatContent.add(SystemMessage(markdownTest, `Markdown test`, geminiView));
+ else
+ chatContent.add(SystemMessage(`Invalid command.`, 'Error', geminiView))
+ }
+ else {
+ GeminiService.send(text);
+ }
+}
+
+export const geminiView = Box({
+ homogeneous: true,
+ children: [Scrollable({
+ className: 'sidebar-chat-viewport',
+ vexpand: true,
+ child: Box({
+ vertical: true,
+ children: [
+ geminiWelcome,
+ chatContent,
+ ]
+ }),
+ setup: (scrolledWindow) => {
+ // Show scrollbar
+ scrolledWindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
+ const vScrollbar = scrolledWindow.get_vscrollbar();
+ vScrollbar.get_style_context().add_class('sidebar-scrollbar');
+ // Avoid click-to-scroll-widget-to-view behavior
+ Utils.timeout(1, () => {
+ const viewport = scrolledWindow.child;
+ viewport.set_focus_vadjustment(new Gtk.Adjustment(undefined));
+ })
+ // Always scroll to bottom with new content
+ const adjustment = scrolledWindow.get_vadjustment();
+ adjustment.connect("changed", () => Utils.timeout(1, () => {
+ if(!chatEntry.hasFocus) return;
+ adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size());
+ }))
+ }
+ })]
+});
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/sideleft/apis/waifu.js b/modules/home-manager/ags/ags/modules/sideleft/apis/waifu.js
new file mode 100644
index 0000000..d7bc524
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideleft/apis/waifu.js
@@ -0,0 +1,419 @@
+// TODO: execAsync(['identify', '-format', '{"w":%w,"h":%h}', imagePath])
+// to detect img dimensions
+
+const { Gdk, GdkPixbuf, Gio, GLib, Gtk } = imports.gi;
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { Box, Button, Label, Overlay, Revealer, Scrollable, Stack } = Widget;
+const { execAsync, exec } = Utils;
+import { fileExists } from '../../.miscutils/files.js';
+import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
+import { MarginRevealer } from '../../.widgethacks/advancedrevealers.js';
+import { setupCursorHover, setupCursorHoverInfo } from '../../.widgetutils/cursorhover.js';
+import WaifuService from '../../../services/waifus.js';
+import { darkMode } from '../../.miscutils/system.js';
+import { chatEntry } from '../apiwidgets.js';
+
+async function getImageViewerApp(preferredApp) {
+ Utils.execAsync(['bash', '-c', `command -v ${preferredApp}`])
+ .then((output) => {
+ if (output != '') return preferredApp;
+ else return 'xdg-open';
+ })
+ .catch(print);
+}
+
+const IMAGE_REVEAL_DELAY = 13; // Some wait for inits n other weird stuff
+const IMAGE_VIEWER_APP = getImageViewerApp(userOptions.apps.imageViewer); // Gnome's image viewer cuz very comfortable zooming
+const USER_CACHE_DIR = GLib.get_user_cache_dir();
+
+// Create cache folder and clear pics from previous session
+Utils.exec(`bash -c 'mkdir -p ${USER_CACHE_DIR}/ags/media/waifus'`);
+Utils.exec(`bash -c 'rm ${USER_CACHE_DIR}/ags/media/waifus/*'`);
+
+const CommandButton = (command) => Button({
+ className: 'sidebar-chat-chip sidebar-chat-chip-action txt txt-small',
+ onClicked: () => sendMessage(command),
+ setup: setupCursorHover,
+ label: command,
+});
+
+export const waifuTabIcon = Box({
+ hpack: 'center',
+ children: [
+ MaterialIcon('photo', 'norm'),
+ ]
+});
+
+const WaifuInfo = () => {
+ const waifuLogo = Label({
+ hpack: 'center',
+ className: 'sidebar-chat-welcome-logo',
+ label: 'photo',
+ })
+ return Box({
+ vertical: true,
+ vexpand: true,
+ className: 'spacing-v-15',
+ children: [
+ waifuLogo,
+ Label({
+ className: 'txt txt-title-small sidebar-chat-welcome-txt',
+ wrap: true,
+ justify: Gtk.Justification.CENTER,
+ label: 'Waifus',
+ }),
+ Box({
+ className: 'spacing-h-5',
+ hpack: 'center',
+ children: [
+ Label({
+ className: 'txt-smallie txt-subtext',
+ wrap: true,
+ justify: Gtk.Justification.CENTER,
+ label: 'Powered by waifu.im + other APIs',
+ }),
+ Button({
+ className: 'txt-subtext txt-norm icon-material',
+ label: 'info',
+ tooltipText: 'Type tags for a random pic.\nNSFW content will not be returned unless\nyou explicitly request such a tag.\n\nDisclaimer: Not affiliated with the providers\nnor responsible for any of their content.',
+ setup: setupCursorHoverInfo,
+ }),
+ ]
+ }),
+ ]
+ });
+}
+
+const waifuWelcome = Box({
+ vexpand: true,
+ homogeneous: true,
+ child: Box({
+ className: 'spacing-v-15',
+ vpack: 'center',
+ vertical: true,
+ children: [
+ WaifuInfo(),
+ ]
+ })
+});
+
+const WaifuImage = (taglist) => {
+ const ImageState = (icon, name) => Box({
+ className: 'spacing-h-5 txt',
+ children: [
+ Box({ hexpand: true }),
+ Label({
+ className: 'sidebar-waifu-txt txt-smallie',
+ xalign: 0,
+ label: name,
+ }),
+ MaterialIcon(icon, 'norm'),
+ ]
+ })
+ const ImageAction = ({ name, icon, action }) => Button({
+ className: 'sidebar-waifu-image-action txt-norm icon-material',
+ tooltipText: name,
+ label: icon,
+ onClicked: action,
+ setup: setupCursorHover,
+ })
+ const downloadState = Stack({
+ homogeneous: false,
+ transition: 'slide_up_down',
+ transitionDuration: userOptions.animations.durationSmall,
+ children: {
+ 'api': ImageState('api', 'Calling API'),
+ 'download': ImageState('downloading', 'Downloading image'),
+ 'done': ImageState('done', 'Finished!'),
+ 'error': ImageState('error', 'Error'),
+ 'notfound': ImageState('error', 'Not found!'),
+ },
+ });
+ const downloadIndicator = MarginRevealer({
+ vpack: 'center',
+ transition: 'slide_left',
+ revealChild: true,
+ child: downloadState,
+ });
+ const blockHeading = Box({
+ hpack: 'fill',
+ className: 'spacing-h-5',
+ children: [
+ ...taglist.map((tag) => CommandButton(tag)),
+ Box({ hexpand: true }),
+ downloadIndicator,
+ ]
+ });
+ const blockImageActions = Revealer({
+ transition: 'crossfade',
+ revealChild: false,
+ child: Box({
+ vertical: true,
+ children: [
+ Box({
+ className: 'sidebar-waifu-image-actions spacing-h-3',
+ children: [
+ Box({ hexpand: true }),
+ ImageAction({
+ name: 'Go to source',
+ icon: 'link',
+ action: () => execAsync(['xdg-open', `${thisBlock.attribute.imageData.source}`]).catch(print),
+ }),
+ ImageAction({
+ name: 'Hoard',
+ icon: 'save',
+ action: (self) => {
+ execAsync(['bash', '-c', `mkdir -p $(xdg-user-dir PICTURES)/homework${thisBlock.attribute.isNsfw ? '/🌶️' : ''} && cp ${thisBlock.attribute.imagePath} $(xdg-user-dir PICTURES)/homework${thisBlock.attribute.isNsfw ? '/🌶️/' : ''}`])
+ .then(() => self.label = 'done')
+ .catch(print);
+ },
+ }),
+ ImageAction({
+ name: 'Open externally',
+ icon: 'open_in_new',
+ action: () => execAsync([IMAGE_VIEWER_APP, `${thisBlock.attribute.imagePath}`]).catch(print),
+ }),
+ ]
+ })
+ ],
+ })
+ })
+ const blockImage = Widget.DrawingArea({
+ className: 'sidebar-waifu-image',
+ });
+ const blockImageRevealer = Revealer({
+ transition: 'slide_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ revealChild: false,
+ child: Box({
+ className: 'margin-top-5',
+ children: [Overlay({
+ child: Box({
+ homogeneous: true,
+ className: 'sidebar-waifu-image',
+ children: [blockImage],
+ }),
+ overlays: [blockImageActions],
+ })]
+ }),
+ });
+ const thisBlock = Box({
+ className: 'sidebar-chat-message',
+ attribute: {
+ 'imagePath': '',
+ 'isNsfw': false,
+ 'imageData': '',
+ 'update': (imageData, force = false) => {
+ thisBlock.attribute.imageData = imageData;
+ const { status, signature, url, extension, source, dominant_color, is_nsfw, width, height, tags } = thisBlock.attribute.imageData;
+ thisBlock.attribute.isNsfw = is_nsfw;
+ if (status == 404) {
+ downloadState.shown = 'notfound';
+ return;
+ }
+ if (status != 200) {
+ downloadState.shown = 'error';
+ return;
+ }
+ thisBlock.attribute.imagePath = `${USER_CACHE_DIR}/ags/media/waifus/${signature}${extension}`;
+ downloadState.shown = 'download';
+ // Width/height
+ const widgetWidth = Math.min(Math.floor(waifuContent.get_allocated_width() * 0.85), width);
+ const widgetHeight = Math.ceil(widgetWidth * height / width);
+ blockImage.set_size_request(widgetWidth, widgetHeight);
+ const showImage = () => {
+ downloadState.shown = 'done';
+ const pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(thisBlock.attribute.imagePath, widgetWidth, widgetHeight);
+ // const pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(thisBlock.attribute.imagePath, widgetWidth, widgetHeight, false);
+
+ blockImage.set_size_request(widgetWidth, widgetHeight);
+ blockImage.connect("draw", (widget, cr) => {
+ const borderRadius = widget.get_style_context().get_property('border-radius', Gtk.StateFlags.NORMAL);
+
+ // Draw a rounded rectangle
+ cr.arc(borderRadius, borderRadius, borderRadius, Math.PI, 1.5 * Math.PI);
+ cr.arc(widgetWidth - borderRadius, borderRadius, borderRadius, 1.5 * Math.PI, 2 * Math.PI);
+ cr.arc(widgetWidth - borderRadius, widgetHeight - borderRadius, borderRadius, 0, 0.5 * Math.PI);
+ cr.arc(borderRadius, widgetHeight - borderRadius, borderRadius, 0.5 * Math.PI, Math.PI);
+ cr.closePath();
+ cr.clip();
+
+ // Paint image as bg
+ Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0);
+ cr.paint();
+ });
+
+ // Reveal stuff
+ Utils.timeout(IMAGE_REVEAL_DELAY, () => {
+ blockImageRevealer.revealChild = true;
+ })
+ Utils.timeout(IMAGE_REVEAL_DELAY + blockImageRevealer.transitionDuration,
+ () => blockImageActions.revealChild = true
+ );
+ downloadIndicator.attribute.hide();
+ }
+ // Show
+ if (!force && fileExists(thisBlock.attribute.imagePath)) showImage();
+ else Utils.execAsync(['bash', '-c', `wget -O '${thisBlock.attribute.imagePath}' '${url}'`])
+ .then(showImage)
+ .catch(print);
+ thisBlock.css = `background-color: mix(${darkMode.value ? 'black' : 'white'}, ${dominant_color}, 0.97);`;
+ },
+ },
+ children: [
+ Box({
+ vertical: true,
+ children: [
+ blockHeading,
+ Box({
+ vertical: true,
+ hpack: 'start',
+ children: [blockImageRevealer],
+ })
+ ]
+ })
+ ],
+ });
+ return thisBlock;
+}
+
+const waifuContent = Box({
+ className: 'spacing-v-15',
+ vertical: true,
+ attribute: {
+ 'map': new Map(),
+ },
+ setup: (self) => self
+ .hook(WaifuService, (box, id) => {
+ if (id === undefined) return;
+ const newImageBlock = WaifuImage(WaifuService.queries[id]);
+ box.add(newImageBlock);
+ box.show_all();
+ box.attribute.map.set(id, newImageBlock);
+ }, 'newResponse')
+ .hook(WaifuService, (box, id) => {
+ if (id === undefined) return;
+ const data = WaifuService.responses[id];
+ if (!data) return;
+ const imageBlock = box.attribute.map.get(id);
+ imageBlock?.attribute.update(data);
+ }, 'updateResponse')
+ ,
+});
+
+export const waifuView = Scrollable({
+ className: 'sidebar-chat-viewport',
+ vexpand: true,
+ child: Box({
+ vertical: true,
+ children: [
+ waifuWelcome,
+ waifuContent,
+ ]
+ }),
+ setup: (scrolledWindow) => {
+ // Show scrollbar
+ scrolledWindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
+ const vScrollbar = scrolledWindow.get_vscrollbar();
+ vScrollbar.get_style_context().add_class('sidebar-scrollbar');
+ // Avoid click-to-scroll-widget-to-view behavior
+ Utils.timeout(1, () => {
+ const viewport = scrolledWindow.child;
+ viewport.set_focus_vadjustment(new Gtk.Adjustment(undefined));
+ })
+ // Always scroll to bottom with new content
+ const adjustment = scrolledWindow.get_vadjustment();
+ adjustment.connect("changed", () => {
+ if (!chatEntry.hasFocus) return;
+ adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size());
+ })
+ }
+});
+
+const waifuTags = Revealer({
+ revealChild: false,
+ transition: 'crossfade',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Box({
+ className: 'spacing-h-5',
+ children: [
+ Scrollable({
+ vscroll: 'never',
+ hscroll: 'automatic',
+ hexpand: true,
+ child: Box({
+ className: 'spacing-h-5',
+ children: [
+ CommandButton('waifu'),
+ CommandButton('maid'),
+ CommandButton('uniform'),
+ CommandButton('oppai'),
+ CommandButton('selfies'),
+ CommandButton('marin-kitagawa'),
+ CommandButton('raiden-shogun'),
+ CommandButton('mori-calliope'),
+ ]
+ })
+ }),
+ Box({ className: 'separator-line' }),
+ ]
+ })
+});
+
+export const waifuCommands = Box({
+ className: 'spacing-h-5',
+ setup: (self) => {
+ self.pack_end(CommandButton('/clear'), false, false, 0);
+ self.pack_start(Button({
+ className: 'sidebar-chat-chip-toggle',
+ setup: setupCursorHover,
+ label: 'Tags →',
+ onClicked: () => {
+ waifuTags.revealChild = !waifuTags.revealChild;
+ }
+ }), false, false, 0);
+ self.pack_start(waifuTags, true, true, 0);
+ }
+});
+
+const clearChat = () => { // destroy!!
+ waifuContent.attribute.map.forEach((value, key, map) => {
+ value.destroy();
+ value = null;
+ });
+}
+
+function newSimpleImageCall(name, url, width, height, dominantColor = '#9392A6') {
+ const timeSinceEpoch = Date.now();
+ const newImage = WaifuImage([`/${name}`]);
+ waifuContent.add(newImage);
+ waifuContent.attribute.map.set(timeSinceEpoch, newImage);
+ Utils.timeout(IMAGE_REVEAL_DELAY, () => newImage?.attribute.update({
+ status: 200,
+ url: url,
+ extension: '',
+ signature: timeSinceEpoch,
+ source: url,
+ dominant_color: dominantColor,
+ is_nsfw: false,
+ width: width,
+ height: height,
+ tags: [`/${name}`],
+ }, true));
+}
+
+export const sendMessage = (text) => {
+ // Commands
+ if (text.startsWith('/')) {
+ if (text.startsWith('/clear')) clearChat();
+ else if (text.startsWith('/test'))
+ newSimpleImageCall('test', 'https://picsum.photos/600/400', 300, 200);
+ else if (text.startsWith('/chino'))
+ newSimpleImageCall('chino', 'https://chino.pages.dev/chino', 300, 400, '#B2AEF3');
+ else if (text.startsWith('/place'))
+ newSimpleImageCall('place', 'https://placewaifu.com/image/400/600', 400, 600, '#F0A235');
+
+ }
+ else WaifuService.fetch(text);
+}
diff --git a/modules/home-manager/ags/ags/modules/sideleft/apiwidgets.js b/modules/home-manager/ags/ags/modules/sideleft/apiwidgets.js
new file mode 100644
index 0000000..266c0d6
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideleft/apiwidgets.js
@@ -0,0 +1,223 @@
+const { Gtk, Gdk } = imports.gi;
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { Box, Button, CenterBox, Entry, EventBox, Icon, Label, Overlay, Revealer, Scrollable, Stack } = Widget;
+const { execAsync, exec } = Utils;
+import { setupCursorHover, setupCursorHoverInfo } from '../.widgetutils/cursorhover.js';
+// APIs
+import GPTService from '../../services/gpt.js';
+import Gemini from '../../services/gemini.js';
+import { geminiView, geminiCommands, sendMessage as geminiSendMessage, geminiTabIcon } from './apis/gemini.js';
+import { chatGPTView, chatGPTCommands, sendMessage as chatGPTSendMessage, chatGPTTabIcon } from './apis/chatgpt.js';
+import { waifuView, waifuCommands, sendMessage as waifuSendMessage, waifuTabIcon } from './apis/waifu.js';
+import { booruView, booruCommands, sendMessage as booruSendMessage, booruTabIcon } from './apis/booru.js';
+import { enableClickthrough } from "../.widgetutils/clickthrough.js";
+import { checkKeybind } from '../.widgetutils/keybind.js';
+const TextView = Widget.subclass(Gtk.TextView, "AgsTextView");
+
+import { widgetContent } from './sideleft.js';
+import { IconTabContainer } from '../.commonwidgets/tabcontainer.js';
+
+const EXPAND_INPUT_THRESHOLD = 30;
+const APILIST = {
+ 'gpt': {
+ name: 'Assistant (GPTs)',
+ sendCommand: chatGPTSendMessage,
+ contentWidget: chatGPTView,
+ commandBar: chatGPTCommands,
+ tabIcon: chatGPTTabIcon,
+ placeholderText: 'Message the model...',
+ },
+ 'gemini': {
+ name: 'Assistant (Gemini Pro)',
+ sendCommand: geminiSendMessage,
+ contentWidget: geminiView,
+ commandBar: geminiCommands,
+ tabIcon: geminiTabIcon,
+ placeholderText: 'Message Gemini...',
+ },
+ 'waifu': {
+ name: 'Waifus',
+ sendCommand: waifuSendMessage,
+ contentWidget: waifuView,
+ commandBar: waifuCommands,
+ tabIcon: waifuTabIcon,
+ placeholderText: 'Enter tags',
+ },
+ 'booru': {
+ name: 'Booru',
+ sendCommand: booruSendMessage,
+ contentWidget: booruView,
+ commandBar: booruCommands,
+ tabIcon: booruTabIcon,
+ placeholderText: 'Enter tags',
+ },
+}
+const APIS = userOptions.sidebar.pages.apis.order.map((apiName) => APILIST[apiName]);
+let currentApiId = 0;
+
+function apiSendMessage(textView) {
+ // Get text
+ const buffer = textView.get_buffer();
+ const [start, end] = buffer.get_bounds();
+ const text = buffer.get_text(start, end, true).trimStart();
+ if (!text || text.length == 0) return;
+ // Send
+ APIS[currentApiId].sendCommand(text)
+ // Reset
+ buffer.set_text("", -1);
+ chatEntryWrapper.toggleClassName('sidebar-chat-wrapper-extended', false);
+ chatEntry.set_valign(Gtk.Align.CENTER);
+}
+
+export const chatEntry = TextView({
+ hexpand: true,
+ wrapMode: Gtk.WrapMode.WORD_CHAR,
+ acceptsTab: false,
+ className: 'sidebar-chat-entry txt txt-smallie',
+ setup: (self) => self
+ .hook(App, (self, currentName, visible) => {
+ if (visible && currentName === 'sideleft') {
+ self.grab_focus();
+ }
+ })
+ .hook(GPTService, (self) => {
+ if (APIS[currentApiId].name != 'Assistant (GPTs)') return;
+ self.placeholderText = (GPTService.key.length > 0 ? 'Message the model...' : 'Enter API Key...');
+ }, 'hasKey')
+ .hook(Gemini, (self) => {
+ if (APIS[currentApiId].name != 'Assistant (Gemini Pro)') return;
+ self.placeholderText = (Gemini.key.length > 0 ? 'Message Gemini...' : 'Enter Google AI API Key...');
+ }, 'hasKey')
+ .on("key-press-event", (widget, event) => {
+ // Don't send when Shift+Enter
+ if (event.get_keyval()[1] === Gdk.KEY_Return) {
+ if (event.get_state()[1] !== 17) {// SHIFT_MASK doesn't work but 17 should be shift
+ apiSendMessage(widget);
+ return true;
+ }
+ return false;
+ }
+ // Keybinds
+ if (checkKeybind(event, userOptions.keybinds.sidebar.cycleTab))
+ widgetContent.cycleTab();
+ else if (checkKeybind(event, userOptions.keybinds.sidebar.nextTab))
+ widgetContent.nextTab();
+ else if (checkKeybind(event, userOptions.keybinds.sidebar.prevTab))
+ widgetContent.prevTab();
+ else if (checkKeybind(event, userOptions.keybinds.sidebar.apis.nextTab)) {
+ apiWidgets.attribute.nextTab();
+ return true;
+ }
+ else if (checkKeybind(event, userOptions.keybinds.sidebar.apis.prevTab)) {
+ apiWidgets.attribute.prevTab();
+ return true;
+ }
+ })
+ ,
+});
+
+chatEntry.get_buffer().connect("changed", (buffer) => {
+ const bufferText = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), true);
+ chatSendButton.toggleClassName('sidebar-chat-send-available', bufferText.length > 0);
+ chatPlaceholderRevealer.revealChild = (bufferText.length == 0);
+ if (buffer.get_line_count() > 1 || bufferText.length > EXPAND_INPUT_THRESHOLD) {
+ chatEntryWrapper.toggleClassName('sidebar-chat-wrapper-extended', true);
+ chatEntry.set_valign(Gtk.Align.FILL);
+ chatPlaceholder.set_valign(Gtk.Align.FILL);
+ }
+ else {
+ chatEntryWrapper.toggleClassName('sidebar-chat-wrapper-extended', false);
+ chatEntry.set_valign(Gtk.Align.CENTER);
+ chatPlaceholder.set_valign(Gtk.Align.CENTER);
+ }
+});
+
+const chatEntryWrapper = Scrollable({
+ className: 'sidebar-chat-wrapper',
+ hscroll: 'never',
+ vscroll: 'always',
+ child: chatEntry,
+});
+
+const chatSendButton = Button({
+ className: 'txt-norm icon-material sidebar-chat-send',
+ vpack: 'end',
+ label: 'arrow_upward',
+ setup: setupCursorHover,
+ onClicked: (self) => {
+ APIS[currentApiId].sendCommand(chatEntry.get_buffer().text);
+ chatEntry.get_buffer().set_text("", -1);
+ },
+});
+
+const chatPlaceholder = Label({
+ className: 'txt-subtext txt-smallie margin-left-5',
+ hpack: 'start',
+ vpack: 'center',
+ label: APIS[currentApiId].placeholderText,
+});
+
+const chatPlaceholderRevealer = Revealer({
+ revealChild: true,
+ transition: 'crossfade',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: chatPlaceholder,
+ setup: enableClickthrough,
+});
+
+const textboxArea = Box({ // Entry area
+ className: 'sidebar-chat-textarea',
+ children: [
+ Overlay({
+ passThrough: true,
+ child: chatEntryWrapper,
+ overlays: [chatPlaceholderRevealer],
+ }),
+ Box({ className: 'width-10' }),
+ chatSendButton,
+ ]
+});
+
+const apiCommandStack = Stack({
+ transition: 'slide_up_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ children: APIS.reduce((acc, api) => {
+ acc[api.name] = api.commandBar;
+ return acc;
+ }, {}),
+})
+
+export const apiContentStack = IconTabContainer({
+ tabSwitcherClassName: 'sidebar-icontabswitcher',
+ className: 'margin-top-5',
+ iconWidgets: APIS.map((api) => api.tabIcon),
+ names: APIS.map((api) => api.name),
+ children: APIS.map((api) => api.contentWidget),
+ onChange: (self, id) => {
+ apiCommandStack.shown = APIS[id].name;
+ chatPlaceholder.label = APIS[id].placeholderText;
+ currentApiId = id;
+ }
+});
+
+function switchToTab(id) {
+ apiContentStack.shown.value = id;
+}
+
+const apiWidgets = Widget.Box({
+ attribute: {
+ 'nextTab': () => switchToTab(Math.min(currentApiId + 1, APIS.length - 1)),
+ 'prevTab': () => switchToTab(Math.max(0, currentApiId - 1)),
+ },
+ vertical: true,
+ className: 'spacing-v-10',
+ homogeneous: false,
+ children: [
+ apiContentStack,
+ apiCommandStack,
+ textboxArea,
+ ],
+});
+
+export default apiWidgets;
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/sideleft/main.js b/modules/home-manager/ags/ags/modules/sideleft/main.js
new file mode 100644
index 0000000..3127ea8
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideleft/main.js
@@ -0,0 +1,18 @@
+import PopupWindow from '../.widgethacks/popupwindow.js';
+import SidebarLeft from "./sideleft.js";
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+const { Box } = Widget;
+import clickCloseRegion from '../.commonwidgets/clickcloseregion.js';
+
+export default () => PopupWindow({
+ keymode: 'on-demand',
+ anchor: ['left', 'top', 'bottom'],
+ name: 'sideleft',
+ layer: 'overlay',
+ child: Box({
+ children: [
+ SidebarLeft(),
+ clickCloseRegion({ name: 'sideleft', multimonitor: false, fillMonitor: 'horizontal' }),
+ ]
+ })
+});
diff --git a/modules/home-manager/ags/ags/modules/sideleft/sideleft.js b/modules/home-manager/ags/ags/modules/sideleft/sideleft.js
new file mode 100644
index 0000000..a3cc934
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideleft/sideleft.js
@@ -0,0 +1,121 @@
+const { Gdk } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { Box, Button, EventBox, Label, Revealer, Scrollable, Stack } = Widget;
+const { execAsync, exec } = Utils;
+import { MaterialIcon } from '../.commonwidgets/materialicon.js';
+import { setupCursorHover } from '../.widgetutils/cursorhover.js';
+import toolBox from './toolbox.js';
+import apiWidgets from './apiwidgets.js';
+import { chatEntry } from './apiwidgets.js';
+import { TabContainer } from '../.commonwidgets/tabcontainer.js';
+import { checkKeybind } from '../.widgetutils/keybind.js';
+
+const SIDEBARTABS = {
+ 'apis': {
+ name: 'apis',
+ content: apiWidgets,
+ materialIcon: 'api',
+ friendlyName: 'APIs',
+ },
+ 'tools': {
+ name: 'tools',
+ content: toolBox,
+ materialIcon: 'home_repair_service',
+ friendlyName: 'Tools',
+ },
+}
+const CONTENTS = userOptions.sidebar.pages.order.map((tabName) => SIDEBARTABS[tabName])
+
+const pinButton = Button({
+ attribute: {
+ 'enabled': false,
+ 'toggle': (self) => {
+ self.attribute.enabled = !self.attribute.enabled;
+ self.toggleClassName('sidebar-pin-enabled', self.attribute.enabled);
+
+ const sideleftWindow = App.getWindow('sideleft');
+ const sideleftContent = sideleftWindow.get_children()[0].get_children()[0].get_children()[1];
+
+ sideleftContent.toggleClassName('sidebar-pinned', self.attribute.enabled);
+
+ if (self.attribute.enabled) {
+ sideleftWindow.exclusivity = 'on-demad';
+ }
+ else {
+ sideleftWindow.exclusivity = 'normal';
+ }
+ },
+ },
+ vpack: 'start',
+ className: 'sidebar-pin',
+ child: MaterialIcon('push_pin', 'larger'),
+ tooltipText: 'Pin sidebar (Ctrl+P)',
+ onClicked: (self) => self.attribute.toggle(self),
+ setup: (self) => {
+ setupCursorHover(self);
+ self.hook(App, (self, currentName, visible) => {
+ if (currentName === 'sideleft' && visible) self.grab_focus();
+ })
+ },
+})
+
+export const widgetContent = TabContainer({
+ icons: CONTENTS.map((item) => item.materialIcon),
+ names: CONTENTS.map((item) => item.friendlyName),
+ children: CONTENTS.map((item) => item.content),
+ className: 'sidebar-left spacing-v-10',
+ setup: (self) => self.hook(App, (self, currentName, visible) => {
+ if (currentName === 'sideleft')
+ self.toggleClassName('sidebar-pinned', pinButton.attribute.enabled && visible);
+ }),
+});
+
+export default () => Box({
+ // vertical: true,
+ vexpand: true,
+ css: 'min-width: 2px;',
+ children: [
+ widgetContent,
+ ],
+ setup: (self) => self
+ .on('key-press-event', (widget, event) => { // Handle keybinds
+ if (checkKeybind(event, userOptions.keybinds.sidebar.pin))
+ pinButton.attribute.toggle(pinButton);
+ else if (checkKeybind(event, userOptions.keybinds.sidebar.cycleTab))
+ widgetContent.cycleTab();
+ else if (checkKeybind(event, userOptions.keybinds.sidebar.nextTab))
+ widgetContent.nextTab();
+ else if (checkKeybind(event, userOptions.keybinds.sidebar.prevTab))
+ widgetContent.prevTab();
+
+ if (widgetContent.attribute.names[widgetContent.attribute.shown.value] == 'APIs') { // If api tab is focused
+ // Focus entry when typing
+ if ((
+ !(event.get_state()[1] & Gdk.ModifierType.CONTROL_MASK) &&
+ event.get_keyval()[1] >= 32 && event.get_keyval()[1] <= 126 &&
+ widget != chatEntry && event.get_keyval()[1] != Gdk.KEY_space)
+ ||
+ ((event.get_state()[1] & Gdk.ModifierType.CONTROL_MASK) &&
+ event.get_keyval()[1] === Gdk.KEY_v)
+ ) {
+ chatEntry.grab_focus();
+ const buffer = chatEntry.get_buffer();
+ buffer.set_text(buffer.text + String.fromCharCode(event.get_keyval()[1]), -1);
+ buffer.place_cursor(buffer.get_iter_at_offset(-1));
+ }
+ // Switch API type
+ else if (checkKeybind(event, userOptions.keybinds.sidebar.apis.nextTab)) {
+ const toSwitchTab = widgetContent.attribute.children[widgetContent.attribute.shown.value];
+ toSwitchTab.nextTab();
+ }
+ else if (checkKeybind(event, userOptions.keybinds.sidebar.apis.prevTab)) {
+ const toSwitchTab = widgetContent.attribute.children[widgetContent.attribute.shown.value];
+ toSwitchTab.prevTab();
+ }
+ }
+
+ })
+ ,
+});
diff --git a/modules/home-manager/ags/ags/modules/sideleft/toolbox.js b/modules/home-manager/ags/ags/modules/sideleft/toolbox.js
new file mode 100644
index 0000000..a21d195
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideleft/toolbox.js
@@ -0,0 +1,20 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+const { Box, Label, Scrollable } = Widget;
+import QuickScripts from './tools/quickscripts.js';
+import ColorPicker from './tools/colorpicker.js';
+import Name from './tools/name.js';
+
+export default Scrollable({
+ hscroll: "never",
+ vscroll: "automatic",
+ child: Box({
+ vertical: true,
+ className: 'spacing-v-10',
+ children: [
+ QuickScripts(),
+ ColorPicker(),
+ Box({ vexpand: true }),
+ Name(),
+ ]
+ })
+});
diff --git a/modules/home-manager/ags/ags/modules/sideleft/tools/changeres.sh b/modules/home-manager/ags/ags/modules/sideleft/tools/changeres.sh
new file mode 100644
index 0000000..333f098
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideleft/tools/changeres.sh
@@ -0,0 +1,99 @@
+#!/bin/bash
+
+# Function to get the current resolution
+get_current_resolution() {
+ local output
+ output=$(hyprctl monitors -j)
+ local width height refreshRate
+ width=$(echo "$output" | jq -r '.[0].width')
+ height=$(echo "$output" | jq -r '.[0].height')
+ refreshRate=$(echo "$output" | jq -r '.[0].refreshRate')
+ echo "$width $height $refreshRate"
+}
+
+# Function to update the Hyprland configuration with the new resolution
+update_resolution_config() {
+ local newWidth="$1"
+ local newHeight="$2"
+ local newRefreshRate="$3"
+ local currentRes
+ currentRes=$(get_current_resolution)
+ local width height refreshRate
+ width=${newWidth:-$(echo "$currentRes" | awk '{print $1}')}
+ height=${newHeight:-$(echo "$currentRes" | awk '{print $2}')}
+ refreshRate=${newRefreshRate:-$(echo "$currentRes" | awk '{print $3}')}
+
+ local modelineOutput
+ modelineOutput=$(gtf "$width" "$height" "$refreshRate")
+ local modeline
+ modeline=$(echo "$modelineOutput" | grep -oP 'Modeline "\K[^"]+')
+
+ if [ -z "$modeline" ]; then
+ echo "Failed to generate modeline"
+ exit 1
+ fi
+
+ # Extract the resolution and refresh rate from the modeline
+ local resolution
+ resolution=$(echo "$modeline" | grep -oP '^[0-9]+x[0-9]+')
+ local rate
+ rate=$(echo "$modeline" | grep -oP '[0-9]+.[0-9]+$')
+
+ if [ -z "$resolution" ] || [ -z "$rate" ]; then
+ echo "Failed to extract resolution or refresh rate from modeline"
+ exit 1
+ fi
+
+ local configPath="${HOME}/.config/hypr/hyprland/general.conf"
+ local newConfigContent
+ newConfigContent=$(sed "s/^monitor=.*$/monitor=eDP-1, $resolution@$rate, auto, 1/" "$configPath")
+
+ echo "$newConfigContent" > "$configPath"
+}
+
+# Main script
+echo "Welcome to the Resolution Configurator"
+echo ""
+echo " +---------------------------+"
+echo " | _____ |"
+echo " | | | |"
+echo " | | | |"
+echo " | |_____| |"
+echo " | |"
+echo " +---------------------------+"
+echo ""
+echo "Current resolution and refresh rate:"
+currentRes=$(get_current_resolution)
+width=$(echo "$currentRes" | awk '{print $1}')
+height=$(echo "$currentRes" | awk '{print $2}')
+refreshRate=$(echo "$currentRes" | awk '{print $3}')
+
+echo "Width: $width px"
+echo "Height: $height px"
+echo "Refresh Rate: $refreshRate Hz"
+
+echo ""
+
+read -p "Enter new width (or press Enter to keep current width): " newWidth
+read -p "Enter new height (or press Enter to keep current height): " newHeight
+read -p "Enter new refresh rate (or press Enter to keep current refresh rate): " newRefreshRate
+
+# Validate inputs (if provided)
+if [[ ! "$newWidth" =~ ^[0-9]+$ && -n "$newWidth" ]]; then
+ echo "Invalid width value."
+ exit 1
+fi
+
+if [[ ! "$newHeight" =~ ^[0-9]+$ && -n "$newHeight" ]]; then
+ echo "Invalid height value."
+ exit 1
+fi
+
+if [[ ! "$newRefreshRate" =~ ^[0-9]+$ && -n "$newRefreshRate" ]]; then
+ echo "Invalid refresh rate value."
+ exit 1
+fi
+
+update_resolution_config "$newWidth" "$newHeight" "$newRefreshRate"
+
+echo "Resolution updated successfully."
diff --git a/modules/home-manager/ags/ags/modules/sideleft/tools/color.js b/modules/home-manager/ags/ags/modules/sideleft/tools/color.js
new file mode 100644
index 0000000..568a04c
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideleft/tools/color.js
@@ -0,0 +1,198 @@
+// It's weird, I know
+const { Gio, GLib } = imports.gi;
+import Service from 'resource:///com/github/Aylur/ags/service.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { exec, execAsync } = Utils;
+import { clamp } from '../../.miscutils/mathfuncs.js';
+
+export class ColorPickerSelection extends Service {
+ static {
+ Service.register(this, {
+ 'picked': [],
+ 'assigned': ['int'],
+ 'hue': [],
+ 'sl': [],
+ });
+ }
+
+ _hue = 198;
+ _xAxis = 94;
+ _yAxis = 80;
+
+ get hue() { return this._hue; }
+ set hue(value) {
+ this._hue = clamp(value, 0, 360);
+ this.emit('hue');
+ this.emit('picked');
+ this.emit('changed');
+ }
+ get xAxis() { return this._xAxis; }
+ set xAxis(value) {
+ this._xAxis = clamp(value, 0, 100);
+ this.emit('sl');
+ this.emit('picked');
+ this.emit('changed');
+ }
+ get yAxis() { return this._yAxis; }
+ set yAxis(value) {
+ this._yAxis = clamp(value, 0, 100);
+ this.emit('sl');
+ this.emit('picked');
+ this.emit('changed');
+ }
+ setColorFromHex(hexString, id) {
+ const hsl = hexToHSL(hexString);
+ this._hue = hsl.hue;
+ this._xAxis = hsl.saturation;
+ // this._yAxis = hsl.lightness;
+ this._yAxis = (100 - hsl.saturation / 2) / 100 * hsl.lightness;
+ // console.log(this._hue, this._xAxis, this._yAxis)
+ this.emit('assigned', id);
+ this.emit('changed');
+ }
+
+ constructor() {
+ super();
+ this.emit('changed');
+ }
+}
+
+
+export function hslToRgbValues(h, s, l) {
+ h /= 360;
+ s /= 100;
+ l /= 100;
+ let r, g, b;
+ if (s === 0) {
+ r = g = b = l; // achromatic
+ } else {
+ const hue2rgb = (p, q, t) => {
+ if (t < 0) t += 1;
+ if (t > 1) t -= 1;
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
+ if (t < 1 / 2) return q;
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+ return p;
+ };
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ const p = 2 * l - q;
+ r = hue2rgb(p, q, h + 1 / 3);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1 / 3);
+ }
+ const to255 = x => Math.round(x * 255);
+ r = to255(r);
+ g = to255(g);
+ b = to255(b);
+ return `${Math.round(r)},${Math.round(g)},${Math.round(b)}`;
+ // return `rgb(${r},${g},${b})`;
+}
+export function hslToHex(h, s, l) {
+ h /= 360;
+ s /= 100;
+ l /= 100;
+ let r, g, b;
+ if (s === 0) {
+ r = g = b = l; // achromatic
+ } else {
+ const hue2rgb = (p, q, t) => {
+ if (t < 0) t += 1;
+ if (t > 1) t -= 1;
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
+ if (t < 1 / 2) return q;
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+ return p;
+ };
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ const p = 2 * l - q;
+ r = hue2rgb(p, q, h + 1 / 3);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1 / 3);
+ }
+ const toHex = x => {
+ const hex = Math.round(x * 255).toString(16);
+ return hex.length === 1 ? "0" + hex : hex;
+ };
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
+}
+
+// export function hexToHSL(hex) {
+// // Remove the '#' if present
+// hex = hex.replace(/^#/, '');
+// // Parse the hex value into RGB components
+// const bigint = parseInt(hex, 16);
+// const r = (bigint >> 16) & 255;
+// const g = (bigint >> 8) & 255;
+// const b = bigint & 255;
+// // Normalize RGB values to range [0, 1]
+// const normalizedR = r / 255;
+// const normalizedG = g / 255;
+// const normalizedB = b / 255;
+// // Find the maximum and minimum values
+// const max = Math.max(normalizedR, normalizedG, normalizedB);
+// const min = Math.min(normalizedR, normalizedG, normalizedB);
+// // Calculate the lightness
+// const lightness = (max + min) / 2;
+// // If the color is grayscale, set saturation to 0
+// if (max === min) {
+// return {
+// hue: 0,
+// saturation: 0,
+// lightness: lightness * 100 // Convert to percentage
+// };
+// }
+// // Calculate the saturation
+// const d = max - min;
+// const saturation = lightness > 0.5 ? d / (2 - max - min) : d / (max + min);
+// // Calculate the hue
+// let hue;
+// if (max === normalizedR) {
+// hue = ((normalizedG - normalizedB) / d + (normalizedG < normalizedB ? 6 : 0)) * 60;
+// } else if (max === normalizedG) {
+// hue = ((normalizedB - normalizedR) / d + 2) * 60;
+// } else {
+// hue = ((normalizedR - normalizedG) / d + 4) * 60;
+// }
+// return {
+// hue: Math.round(hue),
+// saturation: Math.round(saturation * 100), // Convert to percentage
+// lightness: Math.round(lightness * 100) // Convert to percentage
+// };
+// }
+
+export function hexToHSL(hex) {
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+
+ var r = parseInt(result[1], 16);
+ var g = parseInt(result[2], 16);
+ var b = parseInt(result[3], 16);
+
+ r /= 255, g /= 255, b /= 255;
+ var max = Math.max(r, g, b), min = Math.min(r, g, b);
+ var h, s, l = (max + min) / 2;
+
+ if (max == min) {
+ h = s = 0; // achromatic
+ } else {
+ var d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ switch (max) {
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break;
+ }
+ h /= 6;
+ }
+
+ s = s * 100;
+ s = Math.round(s);
+ l = l * 100;
+ l = Math.round(l);
+ h = Math.round(360 * h);
+
+ return {
+ hue: h,
+ saturation: s,
+ lightness: l
+ };
+}
diff --git a/modules/home-manager/ags/ags/modules/sideleft/tools/colorpicker.js b/modules/home-manager/ags/ags/modules/sideleft/tools/colorpicker.js
new file mode 100644
index 0000000..f8c0c85
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideleft/tools/colorpicker.js
@@ -0,0 +1,283 @@
+// TODO: Make selection update when entry changes
+const { Gtk } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Variable from 'resource:///com/github/Aylur/ags/variable.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { execAsync, exec } = Utils;
+const { Box, Button, Entry, EventBox, Icon, Label, Overlay, Scrollable } = Widget;
+import SidebarModule from './module.js';
+import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
+import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
+
+import { ColorPickerSelection, hslToHex, hslToRgbValues, hexToHSL } from './color.js';
+import { clamp } from '../../.miscutils/mathfuncs.js';
+
+export default () => {
+ const selectedColor = new ColorPickerSelection();
+ function shouldUseBlackColor() {
+ return ((selectedColor.xAxis < 40 || (45 <= selectedColor.hue && selectedColor.hue <= 195)) &&
+ selectedColor.yAxis > 60);
+ }
+ const colorBlack = 'rgba(0,0,0,0.9)';
+ const colorWhite = 'rgba(255,255,255,0.9)';
+ const hueRange = Box({
+ homogeneous: true,
+ className: 'sidebar-module-colorpicker-wrapper',
+ children: [Box({
+ className: 'sidebar-module-colorpicker-hue',
+ css: `background: linear-gradient(to bottom, #ff6666, #ffff66, #66dd66, #66ffff, #6666ff, #ff66ff, #ff6666);`,
+ })],
+ });
+ const hueSlider = Box({
+ vpack: 'start',
+ className: 'sidebar-module-colorpicker-cursorwrapper',
+ css: `margin-top: ${13.636 * selectedColor.hue / 360}rem;`,
+ homogeneous: true,
+ children: [Box({
+ className: 'sidebar-module-colorpicker-hue-cursor',
+ })],
+ setup: (self) => self.hook(selectedColor, () => {
+ const widgetHeight = hueRange.children[0].get_allocated_height();
+ self.setCss(`margin-top: ${13.636 * selectedColor.hue / 360}rem;`)
+ }),
+ });
+ const hueSelector = Box({
+ children: [EventBox({
+ child: Overlay({
+ child: hueRange,
+ overlays: [hueSlider],
+ }),
+ attribute: {
+ clicked: false,
+ setHue: (self, event) => {
+ const widgetHeight = hueRange.children[0].get_allocated_height();
+ const [_, cursorX, cursorY] = event.get_coords();
+ const cursorYPercent = clamp(cursorY / widgetHeight, 0, 1);
+ selectedColor.hue = Math.round(cursorYPercent * 360);
+ }
+ },
+ setup: (self) => self
+ .on('motion-notify-event', (self, event) => {
+ if (!self.attribute.clicked) return;
+ self.attribute.setHue(self, event);
+ })
+ .on('button-press-event', (self, event) => {
+ if (!(event.get_button()[1] === 1)) return; // We're only interested in left-click here
+ self.attribute.clicked = true;
+ self.attribute.setHue(self, event);
+ })
+ .on('button-release-event', (self) => self.attribute.clicked = false)
+ ,
+ })]
+ });
+ const saturationAndLightnessRange = Box({
+ homogeneous: true,
+ children: [Box({
+ className: 'sidebar-module-colorpicker-saturationandlightness',
+ attribute: {
+ update: (self) => {
+ // css: `background: linear-gradient(to right, #ffffff, color);`,
+ self.setCss(`background:
+ linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,1)),
+ linear-gradient(to right, #ffffff, ${hslToHex(selectedColor.hue, 100, 50)});
+ `);
+ },
+ },
+ setup: (self) => self
+ .hook(selectedColor, self.attribute.update, 'hue')
+ .hook(selectedColor, self.attribute.update, 'assigned')
+ ,
+ })],
+ });
+ const saturationAndLightnessCursor = Box({
+ className: 'sidebar-module-colorpicker-saturationandlightness-cursorwrapper',
+ children: [Box({
+ vpack: 'start',
+ hpack: 'start',
+ homogeneous: true,
+ css: `
+ margin-left: ${13.636 * selectedColor.xAxis / 100}rem;
+ margin-top: ${13.636 * (100 - selectedColor.yAxis) / 100}rem;
+ `, // Why 13.636rem? see class name in stylesheet
+ attribute: {
+ update: (self) => {
+ const allocation = saturationAndLightnessRange.children[0].get_allocation();
+ self.setCss(`
+ margin-left: ${13.636 * selectedColor.xAxis / 100}rem;
+ margin-top: ${13.636 * (100 - selectedColor.yAxis) / 100}rem;
+ `); // Why 13.636rem? see class name in stylesheet
+ }
+ },
+ setup: (self) => self
+ .hook(selectedColor, self.attribute.update, 'sl')
+ .hook(selectedColor, self.attribute.update, 'assigned')
+ ,
+ children: [Box({
+ className: 'sidebar-module-colorpicker-saturationandlightness-cursor',
+ css: `
+ background-color: ${hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))};
+ border-color: ${shouldUseBlackColor() ? colorBlack : colorWhite};
+ `,
+ attribute: {
+ update: (self) => {
+ self.setCss(`
+ background-color: ${hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))};
+ border-color: ${shouldUseBlackColor() ? colorBlack : colorWhite};
+ `);
+ }
+ },
+ setup: (self) => self
+ .hook(selectedColor, self.attribute.update, 'sl')
+ .hook(selectedColor, self.attribute.update, 'hue')
+ .hook(selectedColor, self.attribute.update, 'assigned')
+ ,
+ })],
+ })]
+ });
+ const saturationAndLightnessSelector = Box({
+ homogeneous: true,
+ className: 'sidebar-module-colorpicker-saturationandlightness-wrapper',
+ children: [EventBox({
+ child: Overlay({
+ child: saturationAndLightnessRange,
+ overlays: [saturationAndLightnessCursor],
+ }),
+ attribute: {
+ clicked: false,
+ setSaturationAndLightness: (self, event) => {
+ const allocation = saturationAndLightnessRange.children[0].get_allocation();
+ const [_, cursorX, cursorY] = event.get_coords();
+ const cursorXPercent = clamp(cursorX / allocation.width, 0, 1);
+ const cursorYPercent = clamp(cursorY / allocation.height, 0, 1);
+ selectedColor.xAxis = Math.round(cursorXPercent * 100);
+ selectedColor.yAxis = Math.round(100 - cursorYPercent * 100);
+ }
+ },
+ setup: (self) => self
+ .on('motion-notify-event', (self, event) => {
+ if (!self.attribute.clicked) return;
+ self.attribute.setSaturationAndLightness(self, event);
+ })
+ .on('button-press-event', (self, event) => {
+ if (!(event.get_button()[1] === 1)) return; // We're only interested in left-click here
+ self.attribute.clicked = true;
+ self.attribute.setSaturationAndLightness(self, event);
+ })
+ .on('button-release-event', (self) => self.attribute.clicked = false)
+ ,
+ })]
+ });
+ const resultColorBox = Box({
+ className: 'sidebar-module-colorpicker-result-box',
+ homogeneous: true,
+ css: `background-color: ${hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))};`,
+ children: [Label({
+ className: 'txt txt-small',
+ label: 'Result',
+ }),],
+ attribute: {
+ update: (self) => {
+ self.setCss(`background-color: ${hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))};`);
+ self.children[0].setCss(`color: ${shouldUseBlackColor() ? colorBlack : colorWhite};`)
+ }
+ },
+ setup: (self) => self
+ .hook(selectedColor, self.attribute.update, 'sl')
+ .hook(selectedColor, self.attribute.update, 'hue')
+ .hook(selectedColor, self.attribute.update, 'assigned')
+ ,
+ });
+ const ResultBox = ({ colorSystemName, updateCallback, copyCallback }) => Box({
+ children: [
+ Box({
+ vertical: true,
+ hexpand: true,
+ children: [
+ Label({
+ xalign: 0,
+ className: 'txt-tiny',
+ label: colorSystemName,
+ }),
+ Overlay({
+ child: Entry({
+ widthChars: 10,
+ className: 'txt-small techfont',
+ attribute: {
+ id: 0,
+ update: updateCallback,
+ },
+ setup: (self) => self
+ .hook(selectedColor, self.attribute.update, 'sl')
+ .hook(selectedColor, self.attribute.update, 'hue')
+ .hook(selectedColor, self.attribute.update, 'assigned')
+ // .on('activate', (self) => {
+ // const newColor = self.text;
+ // if (newColor.length != 7) return;
+ // selectedColor.setColorFromHex(self.text, self.attribute.id);
+ // })
+ ,
+ }),
+ })
+ ]
+ }),
+ Button({
+ child: MaterialIcon('content_copy', 'norm'),
+ onClicked: (self) => {
+ copyCallback(self);
+ self.child.label = 'done';
+ Utils.timeout(1000, () => self.child.label = 'content_copy');
+ },
+ setup: setupCursorHover,
+ })
+ ]
+ });
+ const resultHex = ResultBox({
+ colorSystemName: 'Hex',
+ updateCallback: (self, id) => {
+ if (id && self.attribute.id === id) return;
+ self.text = hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100));
+ },
+ copyCallback: () => Utils.execAsync(['wl-copy', `${hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))}`]),
+ })
+ const resultRgb = ResultBox({
+ colorSystemName: 'RGB',
+ updateCallback: (self, id) => {
+ if (id && self.attribute.id === id) return;
+ self.text = hslToRgbValues(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100));
+ },
+ copyCallback: () => Utils.execAsync(['wl-copy', `rgb(${hslToRgbValues(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))})`]),
+ })
+ const resultHsl = ResultBox({
+ colorSystemName: 'HSL',
+ updateCallback: (self, id) => {
+ if (id && self.attribute.id === id) return;
+ self.text = `${selectedColor.hue},${selectedColor.xAxis}%,${Math.round(selectedColor.yAxis / (1 + selectedColor.xAxis / 100))}%`;
+ },
+ copyCallback: () => Utils.execAsync(['wl-copy', `hsl(${selectedColor.hue},${selectedColor.xAxis}%,${Math.round(selectedColor.yAxis / (1 + selectedColor.xAxis / 100))}%)`]),
+ })
+ const result = Box({
+ className: 'sidebar-module-colorpicker-result-area spacing-v-5 txt',
+ hexpand: true,
+ vertical: true,
+ children: [
+ resultColorBox,
+ resultHex,
+ resultRgb,
+ resultHsl,
+ ]
+ })
+ return SidebarModule({
+ icon: MaterialIcon('colorize', 'norm'),
+ name: 'Inaccurate Color picker',
+ revealChild: false,
+ child: Box({
+ className: 'spacing-h-5',
+ children: [
+ hueSelector,
+ saturationAndLightnessSelector,
+ result,
+ ]
+ })
+ });
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/modules/sideleft/tools/module.js b/modules/home-manager/ags/ags/modules/sideleft/tools/module.js
new file mode 100644
index 0000000..2cba49e
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideleft/tools/module.js
@@ -0,0 +1,57 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
+import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
+const { Box, Button, Icon, Label, Revealer } = Widget;
+
+export default ({
+ icon,
+ name,
+ child,
+ revealChild = true,
+}) => {
+ const headerButtonIcon = MaterialIcon(revealChild ? 'expand_less' : 'expand_more', 'norm');
+ const header = Button({
+ onClicked: () => {
+ content.revealChild = !content.revealChild;
+ headerButtonIcon.label = content.revealChild ? 'expand_less' : 'expand_more';
+ },
+ setup: setupCursorHover,
+ child: Box({
+ className: 'txt spacing-h-10',
+ children: [
+ icon,
+ Label({
+ className: 'txt-norm',
+ label: `${name}`,
+ useMarkup: true,
+ }),
+ Box({
+ hexpand: true,
+ }),
+ Box({
+ className: 'sidebar-module-btn-arrow',
+ homogeneous: true,
+ children: [headerButtonIcon],
+ })
+ ]
+ })
+ });
+ const content = Revealer({
+ revealChild: revealChild,
+ transition: 'slide_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Box({
+ className: 'margin-top-5',
+ homogeneous: true,
+ children: [child],
+ }),
+ });
+ return Box({
+ className: 'sidebar-module',
+ vertical: true,
+ children: [
+ header,
+ content,
+ ]
+ });
+}
diff --git a/modules/home-manager/ags/ags/modules/sideleft/tools/name.js b/modules/home-manager/ags/ags/modules/sideleft/tools/name.js
new file mode 100644
index 0000000..72eb198
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideleft/tools/name.js
@@ -0,0 +1,26 @@
+const { Gtk } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
+const { execAsync, exec } = Utils;
+const { Box, Button, CenterBox, EventBox, Icon, Label, Scrollable } = Widget;
+
+export default () => Box({
+ className: 'txt sidebar-module techfont',
+ children: [
+ Label({
+ label: 'illogical-impulse'
+ }),
+ Box({ hexpand: true }),
+ Button({
+ className: 'sidebar-module-btn-arrow',
+ onClicked: () => execAsync(['xdg-open', 'https://github.com/end-4/dots-hyprland']).catch(print),
+ child: Icon({
+ className: 'txt txt-norm',
+ icon: 'github-symbolic',
+ }),
+ setup: setupCursorHover,
+ })
+ ]
+})
diff --git a/modules/home-manager/ags/ags/modules/sideleft/tools/quickscripts.js b/modules/home-manager/ags/ags/modules/sideleft/tools/quickscripts.js
new file mode 100644
index 0000000..f6ce465
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideleft/tools/quickscripts.js
@@ -0,0 +1,121 @@
+const { Gtk } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { execAsync, exec } = Utils;
+const { Box, Button, EventBox, Icon, Label, Scrollable } = Widget;
+import SidebarModule from './module.js';
+import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
+import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
+
+import { distroID, isArchDistro, isDebianDistro, hasFlatpak } from '../../.miscutils/system.js';
+
+const scripts = [
+ {
+ icon: 'desktop-symbolic',
+ name: 'Change screen resolution',
+ command: `bash ${App.configDir}/modules/sideleft/tools/changeres.sh`,
+ enabled: true,
+ },
+ {
+ icon: 'nixos-symbolic',
+ name: 'Trim system generations to 5',
+ command: `sudo ${App.configDir}/scripts/quickscripts/nixos-trim-generations.sh 5 0 system`,
+ enabled: distroID == 'nixos',
+ },
+ {
+ icon: 'nixos-symbolic',
+ name: 'Trim home manager generations to 5',
+ command: `${App.configDir}/scripts/quickscripts/nixos-trim-generations.sh 5 0 home-manager`,
+ enabled: distroID == 'nixos',
+ },
+ {
+ icon: 'ubuntu-symbolic',
+ name: 'Update packages',
+ command: `sudo apt update && sudo apt upgrade -y`,
+ enabled: isDebianDistro,
+ },
+ {
+ icon: 'fedora-symbolic',
+ name: 'Update packages',
+ command: `sudo dnf upgrade -y`,
+ enabled: distroID == 'fedora',
+ },
+ {
+ icon: 'arch-symbolic',
+ name: 'Update packages',
+ command: `sudo pacman -Syyu`,
+ enabled: isArchDistro,
+ },
+ {
+ icon: 'arch-symbolic',
+ name: 'Remove orphan packages',
+ command: `sudo pacman -R $(pacman -Qdtq)`,
+ enabled: isArchDistro,
+ },
+ {
+ icon: 'flatpak-symbolic',
+ name: 'Uninstall unused flatpak packages',
+ command: `flatpak uninstall --unused`,
+ enabled: hasFlatpak,
+ },
+ {
+ icon: 'arch-symbolic',
+ name: 'Save replay',
+ command: 'killall -SIGUSR1 gpu-screen-recorder',
+ enabled: true,
+ },
+ {
+ icon: 'arch-symbolic',
+ name: 'Stop recording',
+ command: 'killall -SIGINT gpu-screen-recorder',
+ enabled: true,
+ },
+ {
+ icon: 'arch-symbolic',
+ name: 'Pause/resume recording',
+ command: 'killall -SIGUSR2 gpu-screen-recorder',
+ enabled: true,
+ },
+];
+
+export default () => SidebarModule({
+ icon: MaterialIcon('code', 'norm'),
+ name: 'Quick scripts',
+ child: Box({
+ vertical: true,
+ className: 'spacing-v-5',
+ children: scripts.map((script) => {
+ if (!script.enabled) return null;
+ const scriptStateIcon = MaterialIcon('not_started', 'norm');
+ return Box({
+ className: 'spacing-h-5 txt',
+ children: [
+ Icon({
+ className: 'sidebar-module-btn-icon txt-large',
+ icon: script.icon,
+ }),
+ Label({
+ className: 'txt-small',
+ hpack: 'start',
+ hexpand: true,
+ label: script.name,
+ tooltipText: script.command,
+ }),
+ Button({
+ className: 'sidebar-module-scripts-button',
+ child: scriptStateIcon,
+ onClicked: () => {
+ closeEverything();
+ execAsync([`bash`, `-c`, `${userOptions.apps.terminal} fish -C "${script.command}"`]).catch(print)
+ .then(() => {
+ scriptStateIcon.label = 'done';
+ })
+ },
+ setup: setupCursorHover,
+ }),
+ ],
+ })
+ }),
+ })
+});
diff --git a/modules/home-manager/ags/ags/modules/sideright/calendar.js b/modules/home-manager/ags/ags/modules/sideright/calendar.js
new file mode 100644
index 0000000..db66df1
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideright/calendar.js
@@ -0,0 +1,203 @@
+const { Gio } = imports.gi;
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { Box, Button, Label } = Widget;
+import { MaterialIcon } from '../.commonwidgets/materialicon.js';
+import { setupCursorHover } from '../.widgetutils/cursorhover.js';
+
+import { TodoWidget } from "./todolist.js";
+import { getCalendarLayout } from "./calendar_layout.js";
+
+let calendarJson = getCalendarLayout(undefined, true);
+let monthshift = 0;
+
+function getDateInXMonthsTime(x) {
+ var currentDate = new Date(); // Get the current date
+ var targetMonth = currentDate.getMonth() + x; // Calculate the target month
+ var targetYear = currentDate.getFullYear(); // Get the current year
+
+ // Adjust the year and month if necessary
+ targetYear += Math.floor(targetMonth / 12);
+ targetMonth = (targetMonth % 12 + 12) % 12;
+
+ // Create a new date object with the target year and month
+ var targetDate = new Date(targetYear, targetMonth, 1);
+
+ // Set the day to the last day of the month to get the desired date
+ // targetDate.setDate(0);
+
+ return targetDate;
+}
+
+const weekDays = [ // MONDAY IS THE FIRST DAY OF THE WEEK :HESRIGHTYOUKNOW:
+ { day: 'Mo', today: 0 },
+ { day: 'Tu', today: 0 },
+ { day: 'We', today: 0 },
+ { day: 'Th', today: 0 },
+ { day: 'Fr', today: 0 },
+ { day: 'Sa', today: 0 },
+ { day: 'Su', today: 0 },
+]
+
+const CalendarDay = (day, today) => Widget.Button({
+ className: `sidebar-calendar-btn ${today == 1 ? 'sidebar-calendar-btn-today' : (today == -1 ? 'sidebar-calendar-btn-othermonth' : '')}`,
+ child: Widget.Overlay({
+ child: Box({}),
+ overlays: [Label({
+ hpack: 'center',
+ className: 'txt-smallie txt-semibold sidebar-calendar-btn-txt',
+ label: String(day),
+ })],
+ })
+})
+
+const CalendarWidget = () => {
+ const calendarMonthYear = Widget.Button({
+ className: 'txt txt-large sidebar-calendar-monthyear-btn',
+ onClicked: () => shiftCalendarXMonths(0),
+ setup: (button) => {
+ button.label = `${new Date().toLocaleString('default', { month: 'long' })} ${new Date().getFullYear()}`;
+ setupCursorHover(button);
+ }
+ });
+ const addCalendarChildren = (box, calendarJson) => {
+ const children = box.get_children();
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i];
+ child.destroy();
+ }
+ box.children = calendarJson.map((row, i) => Widget.Box({
+ className: 'spacing-h-5',
+ children: row.map((day, i) => CalendarDay(day.day, day.today)),
+ }))
+ }
+ function shiftCalendarXMonths(x) {
+ if (x == 0) monthshift = 0;
+ else monthshift += x;
+ var newDate;
+ if (monthshift == 0) newDate = new Date();
+ else newDate = getDateInXMonthsTime(monthshift);
+
+ calendarJson = getCalendarLayout(newDate, (monthshift == 0));
+ calendarMonthYear.label = `${monthshift == 0 ? '' : '• '}${newDate.toLocaleString('default', { month: 'long' })} ${newDate.getFullYear()}`;
+ addCalendarChildren(calendarDays, calendarJson);
+ }
+ const calendarHeader = Widget.Box({
+ className: 'spacing-h-5 sidebar-calendar-header',
+ setup: (box) => {
+ box.pack_start(calendarMonthYear, false, false, 0);
+ box.pack_end(Widget.Box({
+ className: 'spacing-h-5',
+ children: [
+ Button({
+ className: 'sidebar-calendar-monthshift-btn',
+ onClicked: () => shiftCalendarXMonths(-1),
+ child: MaterialIcon('chevron_left', 'norm'),
+ setup: setupCursorHover,
+ }),
+ Button({
+ className: 'sidebar-calendar-monthshift-btn',
+ onClicked: () => shiftCalendarXMonths(1),
+ child: MaterialIcon('chevron_right', 'norm'),
+ setup: setupCursorHover,
+ })
+ ]
+ }), false, false, 0);
+ }
+ })
+ const calendarDays = Widget.Box({
+ hexpand: true,
+ vertical: true,
+ className: 'spacing-v-5',
+ setup: (box) => {
+ addCalendarChildren(box, calendarJson);
+ }
+ });
+ return Widget.EventBox({
+ onScrollUp: () => shiftCalendarXMonths(-1),
+ onScrollDown: () => shiftCalendarXMonths(1),
+ child: Widget.Box({
+ hpack: 'center',
+ children: [
+ Widget.Box({
+ hexpand: true,
+ vertical: true,
+ className: 'spacing-v-5',
+ children: [
+ calendarHeader,
+ Widget.Box({
+ homogeneous: true,
+ className: 'spacing-h-5',
+ children: weekDays.map((day, i) => CalendarDay(day.day, day.today))
+ }),
+ calendarDays,
+ ]
+ })
+ ]
+ })
+ });
+};
+
+const defaultShown = 'calendar';
+const contentStack = Widget.Stack({
+ hexpand: true,
+ children: {
+ 'calendar': CalendarWidget(),
+ 'todo': TodoWidget(),
+ // 'stars': Widget.Label({ label: 'GitHub feed will be here' }),
+ },
+ transition: 'slide_up_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ setup: (stack) => Utils.timeout(1, () => {
+ stack.shown = defaultShown;
+ })
+})
+
+const StackButton = (stackItemName, icon, name) => Widget.Button({
+ className: 'button-minsize sidebar-navrail-btn txt-small spacing-h-5',
+ onClicked: (button) => {
+ contentStack.shown = stackItemName;
+ const kids = button.get_parent().get_children();
+ for (let i = 0; i < kids.length; i++) {
+ if (kids[i] != button) kids[i].toggleClassName('sidebar-navrail-btn-active', false);
+ else button.toggleClassName('sidebar-navrail-btn-active', true);
+ }
+ },
+ child: Box({
+ className: 'spacing-v-5',
+ vertical: true,
+ children: [
+ Label({
+ className: `txt icon-material txt-hugeass`,
+ label: icon,
+ }),
+ Label({
+ label: name,
+ className: 'txt txt-smallie',
+ }),
+ ]
+ }),
+ setup: (button) => Utils.timeout(1, () => {
+ setupCursorHover(button);
+ button.toggleClassName('sidebar-navrail-btn-active', defaultShown === stackItemName);
+ })
+});
+
+export const ModuleCalendar = () => Box({
+ className: 'sidebar-group spacing-h-5',
+ setup: (box) => {
+ box.pack_start(Box({
+ vpack: 'center',
+ homogeneous: true,
+ vertical: true,
+ className: 'sidebar-navrail spacing-v-10',
+ children: [
+ StackButton('calendar', 'calendar_month', 'Calendar'),
+ StackButton('todo', 'done_outline', 'To Do'),
+ // StackButton(box, 'stars', 'star', 'GitHub'),
+ ]
+ }), false, false, 0);
+ box.pack_end(contentStack, false, false, 0);
+ }
+})
+
diff --git a/modules/home-manager/ags/ags/modules/sideright/calendar_layout.js b/modules/home-manager/ags/ags/modules/sideright/calendar_layout.js
new file mode 100644
index 0000000..35b481d
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideright/calendar_layout.js
@@ -0,0 +1,85 @@
+function checkLeapYear(year) {
+ return (
+ year % 400 == 0 ||
+ (year % 4 == 0 && year % 100 != 0));
+}
+
+function getMonthDays(month, year) {
+ const leapYear = checkLeapYear(year);
+ if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 31;
+ if (month == 2 && leapYear) return 29;
+ if (month == 2 && !leapYear) return 28;
+ return 30;
+}
+
+function getNextMonthDays(month, year) {
+ const leapYear = checkLeapYear(year);
+ if (month == 1 && leapYear) return 29;
+ if (month == 1 && !leapYear) return 28;
+ if (month == 12) return 31;
+ if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 30;
+ return 31;
+}
+
+function getPrevMonthDays(month, year) {
+ const leapYear = checkLeapYear(year);
+ if (month == 3 && leapYear) return 29;
+ if (month == 3 && !leapYear) return 28;
+ if (month == 1) return 31;
+ if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 30;
+ return 31;
+}
+
+export function getCalendarLayout(dateObject, highlight) {
+ if (!dateObject) dateObject = new Date();
+ const weekday = (dateObject.getDay() + 6) % 7; // MONDAY IS THE FIRST DAY OF THE WEEK
+ const day = dateObject.getDate();
+ const month = dateObject.getMonth() + 1;
+ const year = dateObject.getFullYear();
+ const weekdayOfMonthFirst = (weekday + 35 - (day - 1)) % 7;
+ const daysInMonth = getMonthDays(month, year);
+ const daysInNextMonth = getNextMonthDays(month, year);
+ const daysInPrevMonth = getPrevMonthDays(month, year);
+
+ // Fill
+ var monthDiff = (weekdayOfMonthFirst == 0 ? 0 : -1);
+ var toFill, dim;
+ if(weekdayOfMonthFirst == 0) {
+ toFill = 1;
+ dim = daysInMonth;
+ }
+ else {
+ toFill = (daysInPrevMonth - (weekdayOfMonthFirst - 1));
+ dim = daysInPrevMonth;
+ }
+ var calendar = [...Array(6)].map(() => Array(7));
+ var i = 0, j = 0;
+ while (i < 6 && j < 7) {
+ calendar[i][j] = {
+ "day": toFill,
+ "today": ((toFill == day && monthDiff == 0 && highlight) ? 1 : (
+ monthDiff == 0 ? 0 :
+ -1
+ ))
+ };
+ // Increment
+ toFill++;
+ if (toFill > dim) { // Next month?
+ monthDiff++;
+ if (monthDiff == 0)
+ dim = daysInMonth;
+ else if (monthDiff == 1)
+ dim = daysInNextMonth;
+ toFill = 1;
+ }
+ // Next tile
+ j++;
+ if (j == 7) {
+ j = 0;
+ i++;
+ }
+
+ }
+ return calendar;
+}
+
diff --git a/modules/home-manager/ags/ags/modules/sideright/centermodules/audiocontrols.js b/modules/home-manager/ags/ags/modules/sideright/centermodules/audiocontrols.js
new file mode 100644
index 0000000..5fc4421
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideright/centermodules/audiocontrols.js
@@ -0,0 +1,216 @@
+import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
+import Variable from 'resource:///com/github/Aylur/ags/variable.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+const { Box, Button, Icon, Label, Revealer, Scrollable, Slider, Stack } = Widget;
+import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
+import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
+import { iconExists } from '../../.miscutils/icons.js';
+
+const AppVolume = (stream) => Box({
+ className: 'sidebar-volmixer-stream spacing-h-10',
+ children: [
+ Icon({
+ className: 'sidebar-volmixer-stream-appicon',
+ vpack: 'center',
+ tooltipText: stream.stream.name,
+ setup: (self) => {
+ self.hook(stream, (self) => {
+ self.icon = stream.stream.name.toLowerCase();
+ })
+ },
+ }),
+ Box({
+ hexpand: true,
+ vpack: 'center',
+ vertical: true,
+ className: 'spacing-v-5',
+ children: [
+ Label({
+ xalign: 0,
+ maxWidthChars: 1,
+ truncate: 'end',
+ label: stream.description,
+ className: 'txt-small',
+ setup: (self) => self.hook(stream, (self) => {
+ self.label = `${stream.stream.name} • ${stream.description}`
+ })
+ }),
+ Slider({
+ drawValue: false,
+ hpack: 'fill',
+ className: 'sidebar-volmixer-stream-slider',
+ value: stream.volume,
+ min: 0, max: 1,
+ onChange: ({ value }) => {
+ stream.volume = value;
+ },
+ setup: (self) => self.hook(stream, (self) => {
+ self.value = stream.volume;
+ })
+ }),
+ // Box({
+ // homogeneous: true,
+ // className: 'test',
+ // children: [AnimatedSlider({
+ // className: 'sidebar-volmixer-stream-slider',
+ // value: stream.volume,
+ // })],
+ // })
+ ]
+ })
+ ]
+});
+
+const AudioDevices = (input = false) => {
+ const dropdownShown = Variable(false);
+ const DeviceStream = (stream) => Button({
+ child: Box({
+ className: 'txt spacing-h-10',
+ children: [
+ iconExists(stream.iconName) ? Icon({
+ className: 'txt-norm symbolic-icon',
+ icon: stream.iconName,
+ }) : MaterialIcon(input ? 'mic_external_on' : 'media_output', 'norm'),
+ Label({
+ hexpand: true,
+ xalign: 0,
+ className: 'txt-small',
+ truncate: 'end',
+ maxWidthChars: 1,
+ label: stream.description,
+ }),
+ ],
+ }),
+ onClicked: (self) => {
+ if (input) Audio.microphone = stream;
+ else Audio.speaker = stream;
+ dropdownShown.value = false;
+ },
+ setup: setupCursorHover,
+ })
+ const activeDevice = Button({
+ onClicked: () => { dropdownShown.value = !dropdownShown.value; },
+ child: Box({
+ className: 'txt spacing-h-10',
+ children: [
+ MaterialIcon(input ? 'mic_external_on' : 'media_output', 'norm'),
+ Label({
+ hexpand: true,
+ xalign: 0,
+ className: 'txt-small',
+ truncate: 'end',
+ maxWidthChars: 1,
+ label: `${input ? '[In]' : '[Out]'}`,
+ setup: (self) => self.hook(Audio, (self) => {
+ self.label = `${input ? '[In]' : '[Out]'} ${input ? Audio.microphone.description : Audio.speaker.description}`;
+ })
+ }),
+ Label({
+ className: `icon-material txt-norm`,
+ setup: (self) => self.hook(dropdownShown, (self) => {
+ self.label = dropdownShown.value ? 'expand_less' : 'expand_more';
+ })
+ })
+ ],
+ }),
+ setup: setupCursorHover,
+ });
+ const deviceSelector = Revealer({
+ transition: 'slide_down',
+ revealChild: dropdownShown.bind("value"),
+ transitionDuration: userOptions.animations.durationSmall,
+ child: Box({
+ vertical: true,
+ children: [
+ Box({ className: 'separator-line margin-top-5 margin-bottom-5' }),
+ Box({
+ vertical: true,
+ className: 'spacing-v-5 margin-top-5',
+ attribute: {
+ 'updateStreams': (self) => {
+ const streams = input ? Audio.microphones : Audio.speakers;
+ self.children = streams.map(stream => DeviceStream(stream));
+ },
+ },
+ setup: (self) => self
+ .hook(Audio, self.attribute.updateStreams, 'stream-added')
+ .hook(Audio, self.attribute.updateStreams, 'stream-removed')
+ ,
+ }),
+ ]
+ })
+ })
+ return Box({
+ hpack: 'fill',
+ className: 'sidebar-volmixer-deviceselector',
+ vertical: true,
+ children: [
+ activeDevice,
+ deviceSelector,
+ ]
+ })
+}
+
+export default (props) => {
+ const emptyContent = Box({
+ homogeneous: true,
+ children: [Box({
+ vertical: true,
+ vpack: 'center',
+ className: 'txt spacing-v-10',
+ children: [
+ Box({
+ vertical: true,
+ className: 'spacing-v-5 txt-subtext',
+ children: [
+ MaterialIcon('brand_awareness', 'gigantic'),
+ Label({ label: 'No audio source', className: 'txt-small' }),
+ ]
+ }),
+ ]
+ })]
+ });
+ const appList = Scrollable({
+ vexpand: true,
+ child: Box({
+ attribute: {
+ 'updateStreams': (self) => {
+ const streams = Audio.apps;
+ self.children = streams.map(stream => AppVolume(stream));
+ },
+ },
+ vertical: true,
+ className: 'spacing-v-5',
+ setup: (self) => self
+ .hook(Audio, self.attribute.updateStreams, 'stream-added')
+ .hook(Audio, self.attribute.updateStreams, 'stream-removed')
+ ,
+ })
+ })
+ const devices = Box({
+ vertical: true,
+ className: 'spacing-v-5',
+ children: [
+ AudioDevices(false),
+ AudioDevices(true),
+ ]
+ })
+ const mainContent = Stack({
+ children: {
+ 'empty': emptyContent,
+ 'list': appList,
+ },
+ setup: (self) => self.hook(Audio, (self) => {
+ self.shown = (Audio.apps.length > 0 ? 'list' : 'empty')
+ }),
+ })
+ return Box({
+ ...props,
+ className: 'spacing-v-5',
+ vertical: true,
+ children: [
+ mainContent,
+ devices,
+ ]
+ });
+}
diff --git a/modules/home-manager/ags/ags/modules/sideright/centermodules/bluetooth.js b/modules/home-manager/ags/ags/modules/sideright/centermodules/bluetooth.js
new file mode 100644
index 0000000..b9ec738
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideright/centermodules/bluetooth.js
@@ -0,0 +1,160 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { Box, Button, Icon, Label, Scrollable, Slider, Stack, Overlay } = Widget;
+const { execAsync, exec } = Utils;
+import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
+import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
+import { ConfigToggle } from '../../.commonwidgets/configwidgets.js';
+
+// can't connect: sync_problem
+
+const USE_SYMBOLIC_ICONS = true;
+
+const BluetoothDevice = (device) => {
+ // console.log(device);
+ const deviceIcon = Icon({
+ className: 'sidebar-bluetooth-appicon',
+ vpack: 'center',
+ tooltipText: device.name,
+ setup: (self) => self.hook(device, (self) => {
+ self.icon = `${device.iconName}${USE_SYMBOLIC_ICONS ? '-symbolic' : ''}`;
+ }),
+ });
+ const deviceStatus = Box({
+ hexpand: true,
+ vpack: 'center',
+ vertical: true,
+ children: [
+ Label({
+ xalign: 0,
+ maxWidthChars: 1,
+ truncate: 'end',
+ label: device.name,
+ className: 'txt-small',
+ setup: (self) => self.hook(device, (self) => {
+ self.label = device.name;
+ }),
+ }),
+ Label({
+ xalign: 0,
+ maxWidthChars: 1,
+ truncate: 'end',
+ label: device.connected ? 'Connected' : (device.paired ? 'Paired' : ''),
+ className: 'txt-subtext',
+ setup: (self) => self.hook(device, (self) => {
+ self.label = device.connected ? 'Connected' : (device.paired ? 'Paired' : '');
+ }),
+ }),
+ ]
+ });
+ const deviceConnectButton = ConfigToggle({
+ vpack: 'center',
+ expandWidget: false,
+ desc: 'Toggle connection',
+ initValue: device.connected,
+ onChange: (self, newValue) => {
+ device.setConnection(newValue);
+ },
+ extraSetup: (self) => self.hook(device, (self) => {
+ Utils.timeout(200, () => self.enabled.value = device.connected);
+ }),
+ })
+ const deviceRemoveButton = Button({
+ vpack: 'center',
+ className: 'sidebar-bluetooth-device-remove',
+ child: MaterialIcon('delete', 'norm'),
+ tooltipText: 'Remove device',
+ setup: setupCursorHover,
+ onClicked: () => execAsync(['bluetoothctl', 'remove', device.address]).catch(print),
+ });
+ return Box({
+ className: 'sidebar-bluetooth-device spacing-h-10',
+ children: [
+ deviceIcon,
+ deviceStatus,
+ Box({
+ className: 'spacing-h-5',
+ children: [
+ deviceConnectButton,
+ deviceRemoveButton,
+ ]
+ })
+ ]
+ })
+}
+
+export default (props) => {
+ const emptyContent = Box({
+ homogeneous: true,
+ children: [Box({
+ vertical: true,
+ vpack: 'center',
+ className: 'txt spacing-v-10',
+ children: [
+ Box({
+ vertical: true,
+ className: 'spacing-v-5 txt-subtext',
+ children: [
+ MaterialIcon('bluetooth_disabled', 'gigantic'),
+ Label({ label: 'No Bluetooth devices', className: 'txt-small' }),
+ ]
+ }),
+ ]
+ })]
+ });
+ const deviceList = Overlay({
+ passThrough: true,
+ child: Scrollable({
+ vexpand: true,
+ child: Box({
+ attribute: {
+ 'updateDevices': (self) => {
+ const devices = Bluetooth.devices;
+ self.children = devices.map(d => BluetoothDevice(d));
+ },
+ },
+ vertical: true,
+ className: 'spacing-v-5 margin-bottom-15',
+ setup: (self) => self
+ .hook(Bluetooth, self.attribute.updateDevices, 'device-added')
+ .hook(Bluetooth, self.attribute.updateDevices, 'device-removed')
+ ,
+ })
+ }),
+ overlays: [Box({
+ className: 'sidebar-centermodules-scrollgradient-bottom'
+ })]
+ });
+ const mainContent = Stack({
+ children: {
+ 'empty': emptyContent,
+ 'list': deviceList,
+ },
+ setup: (self) => self.hook(Bluetooth, (self) => {
+ self.shown = (Bluetooth.devices.length > 0 ? 'list' : 'empty')
+ }),
+ })
+ const bottomBar = Box({
+ homogeneous: true,
+ children: [Button({
+ hpack: 'center',
+ className: 'txt-small txt sidebar-centermodules-bottombar-button',
+ onClicked: () => {
+ execAsync(['bash', '-c', userOptions.apps.bluetooth]).catch(print);
+ closeEverything();
+ },
+ label: 'More',
+ setup: setupCursorHover,
+ })],
+ })
+ return Box({
+ ...props,
+ className: 'spacing-v-5',
+ vertical: true,
+ children: [
+ mainContent,
+ bottomBar
+ ]
+ });
+}
diff --git a/modules/home-manager/ags/ags/modules/sideright/centermodules/configure.js b/modules/home-manager/ags/ags/modules/sideright/centermodules/configure.js
new file mode 100644
index 0000000..b53556c
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideright/centermodules/configure.js
@@ -0,0 +1,130 @@
+const { GLib } = imports.gi;
+import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { Box, Button, Icon, Label, Scrollable, Slider, Stack } = Widget;
+const { execAsync, exec } = Utils;
+import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
+import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
+import { ConfigGap, ConfigSpinButton, ConfigToggle } from '../../.commonwidgets/configwidgets.js';
+
+const HyprlandToggle = ({ icon, name, desc = null, option, enableValue = 1, disableValue = 0, extraOnChange = () => { } }) => ConfigToggle({
+ icon: icon,
+ name: name,
+ desc: desc,
+ initValue: JSON.parse(exec(`hyprctl getoption -j ${option}`))["int"] != 0,
+ onChange: (self, newValue) => {
+ execAsync(['hyprctl', 'keyword', option, `${newValue ? enableValue : disableValue}`]).catch(print);
+ extraOnChange(self, newValue);
+ }
+});
+
+const HyprlandSpinButton = ({ icon, name, desc = null, option, ...rest }) => ConfigSpinButton({
+ icon: icon,
+ name: name,
+ desc: desc,
+ initValue: Number(JSON.parse(exec(`hyprctl getoption -j ${option}`))["int"]),
+ onChange: (self, newValue) => {
+ execAsync(['hyprctl', 'keyword', option, `${newValue}`]).catch(print);
+ },
+ ...rest,
+});
+
+const Subcategory = (children) => Box({
+ className: 'margin-left-20',
+ vertical: true,
+ children: children,
+})
+
+export default (props) => {
+ const ConfigSection = ({ name, children }) => Box({
+ vertical: true,
+ className: 'spacing-v-5',
+ children: [
+ Label({
+ hpack: 'center',
+ className: 'txt txt-large margin-left-10',
+ label: name,
+ }),
+ Box({
+ className: 'margin-left-10 margin-right-10',
+ vertical: true,
+ children: children,
+ })
+ ]
+ })
+ const mainContent = Scrollable({
+ vexpand: true,
+ child: Box({
+ vertical: true,
+ className: 'spacing-v-10',
+ children: [
+ ConfigSection({
+ name: 'Effects', children: [
+ ConfigToggle({
+ icon: 'border_clear',
+ name: 'Transparency',
+ desc: '[AGS]\nMake shell elements transparent\nBlur is also recommended if you enable this',
+ initValue: exec(`bash -c "sed -n \'2p\' ${GLib.get_user_state_dir()}/ags/user/colormode.txt"`) == "transparent",
+ onChange: (self, newValue) => {
+ const transparency = newValue == 0 ? "opaque" : "transparent";
+ console.log(transparency);
+ execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_state_dir()}/ags/user && sed -i "2s/.*/${transparency}/" ${GLib.get_user_state_dir()}/ags/user/colormode.txt`])
+ .then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchcolor.sh`]))
+ .catch(print);
+ },
+ }),
+ HyprlandToggle({ icon: 'blur_on', name: 'Blur', desc: "[Hyprland]\nEnable blur on transparent elements\nDoesn't affect performance/power consumption unless you have transparent windows.", option: "decoration:blur:enabled" }),
+ Subcategory([
+ HyprlandToggle({ icon: 'stack_off', name: 'X-ray', desc: "[Hyprland]\nMake everything behind a window/layer except the wallpaper not rendered on its blurred surface\nRecommended to improve performance (if you don't abuse transparency/blur) ", option: "decoration:blur:xray" }),
+ HyprlandSpinButton({ icon: 'target', name: 'Size', desc: '[Hyprland]\nAdjust the blur radius. Generally doesn\'t affect performance\nHigher = more color spread', option: 'decoration:blur:size', minValue: 1, maxValue: 1000 }),
+ HyprlandSpinButton({ icon: 'repeat', name: 'Passes', desc: '[Hyprland] Adjust the number of runs of the blur algorithm\nMore passes = more spread and power consumption\n4 is recommended\n2- would look weird and 6+ would look lame.', option: 'decoration:blur:passes', minValue: 1, maxValue: 10 }),
+ ]),
+ ConfigGap({}),
+ HyprlandToggle({
+ icon: 'animation', name: 'Animations', desc: '[Hyprland] [GTK]\nEnable animations', option: 'animations:enabled',
+ extraOnChange: (self, newValue) => execAsync(['gsettings', 'set', 'org.gnome.desktop.interface', 'enable-animations', `${newValue}`])
+ }),
+ Subcategory([
+ ConfigSpinButton({
+ icon: 'clear_all',
+ name: 'Choreography delay',
+ desc: 'In milliseconds, the delay between animations of a series',
+ initValue: userOptions.animations.choreographyDelay,
+ step: 10, minValue: 0, maxValue: 1000,
+ onChange: (self, newValue) => {
+ userOptions.animations.choreographyDelay = newValue
+ },
+ })
+ ]),
+ ]
+ }),
+ ConfigSection({
+ name: 'Developer', children: [
+ HyprlandToggle({ icon: 'speed', name: 'Show FPS', desc: "[Hyprland]\nShow FPS overlay on top-left corner", option: "debug:overlay" }),
+ HyprlandToggle({ icon: 'sort', name: 'Log to stdout', desc: "[Hyprland]\nPrint LOG, ERR, WARN, etc. messages to the console", option: "debug:enable_stdout_logs" }),
+ HyprlandToggle({ icon: 'motion_sensor_active', name: 'Damage tracking', desc: "[Hyprland]\nEnable damage tracking\nGenerally, leave it on.\nTurn off only when a shader doesn't work", option: "debug:damage_tracking", enableValue: 2 }),
+ HyprlandToggle({ icon: 'destruction', name: 'Damage blink', desc: "[Hyprland] [Epilepsy warning!]\nShow screen damage flashes", option: "debug:damage_blink" }),
+ ]
+ }),
+ ]
+ })
+ });
+ const footNote = Box({
+ homogeneous: true,
+ children: [Label({
+ hpack: 'center',
+ className: 'txt txt-italic txt-subtext margin-5',
+ label: 'Not all changes are saved',
+ })]
+ })
+ return Box({
+ ...props,
+ className: 'spacing-v-5',
+ vertical: true,
+ children: [
+ mainContent,
+ footNote,
+ ]
+ });
+}
diff --git a/modules/home-manager/ags/ags/modules/sideright/centermodules/notificationlist.js b/modules/home-manager/ags/ags/modules/sideright/centermodules/notificationlist.js
new file mode 100644
index 0000000..8007034
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideright/centermodules/notificationlist.js
@@ -0,0 +1,176 @@
+// This file is for the notification list on the sidebar
+// For the popup notifications, see onscreendisplay.js
+// The actual widget for each single notification is in ags/modules/.commonwidgets/notification.js
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
+const { Box, Button, Label, Revealer, Scrollable, Stack } = Widget;
+import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
+import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
+import Notification from '../../.commonwidgets/notification.js';
+import { ConfigToggle } from '../../.commonwidgets/configwidgets.js';
+
+export default (props) => {
+ const notifEmptyContent = Box({
+ homogeneous: true,
+ children: [Box({
+ vertical: true,
+ vpack: 'center',
+ className: 'txt spacing-v-10',
+ children: [
+ Box({
+ vertical: true,
+ className: 'spacing-v-5 txt-subtext',
+ children: [
+ MaterialIcon('notifications_active', 'gigantic'),
+ Label({ label: 'No notifications', className: 'txt-small' }),
+ ]
+ }),
+ ]
+ })]
+ });
+ const notificationList = Box({
+ vertical: true,
+ vpack: 'start',
+ className: 'spacing-v-5-revealer',
+ setup: (self) => self
+ .hook(Notifications, (box, id) => {
+ if (box.get_children().length == 0) { // On init there's no notif, or 1st notif
+ Notifications.notifications
+ .forEach(n => {
+ box.pack_end(Notification({
+ notifObject: n,
+ isPopup: false,
+ }), false, false, 0)
+ });
+ box.show_all();
+ return;
+ }
+ // 2nd or later notif
+ const notif = Notifications.getNotification(id);
+ const NewNotif = Notification({
+ notifObject: notif,
+ isPopup: false,
+ });
+ if (NewNotif) {
+ box.pack_end(NewNotif, false, false, 0);
+ box.show_all();
+ }
+ }, 'notified')
+ .hook(Notifications, (box, id) => {
+ if (!id) return;
+ for (const ch of box.children) {
+ if (ch._id === id) {
+ ch.attribute.destroyWithAnims();
+ }
+ }
+ }, 'closed')
+ ,
+ });
+ const ListActionButton = (icon, name, action) => Button({
+ className: 'sidebar-centermodules-bottombar-button',
+ onClicked: action,
+ child: Box({
+ hpack: 'center',
+ className: 'spacing-h-5',
+ children: [
+ MaterialIcon(icon, 'norm'),
+ Label({
+ className: 'txt-small',
+ label: name,
+ })
+ ]
+ }),
+ setup: setupCursorHover,
+ });
+ const silenceButton = ListActionButton('notifications_paused', 'Silence', (self) => {
+ Notifications.dnd = !Notifications.dnd;
+ self.toggleClassName('notif-listaction-btn-enabled', Notifications.dnd);
+ });
+ // const silenceToggle = ConfigToggle({
+ // expandWidget: false,
+ // icon: 'do_not_disturb_on',
+ // name: 'Do Not Disturb',
+ // initValue: false,
+ // onChange: (self, newValue) => {
+ // Notifications.dnd = newValue;
+ // },
+ // })
+ const clearButton = Revealer({
+ transition: 'slide_right',
+ transitionDuration: userOptions.animations.durationSmall,
+ setup: (self) => self.hook(Notifications, (self) => {
+ self.revealChild = Notifications.notifications.length > 0;
+ }),
+ child: ListActionButton('clear_all', 'Clear', () => {
+ Notifications.clear();
+ const kids = notificationList.get_children();
+ for (let i = 0; i < kids.length; i++) {
+ const kid = kids[i];
+ Utils.timeout(userOptions.animations.choreographyDelay * i, () => kid.attribute.destroyWithAnims());
+ }
+ })
+ })
+ const notifCount = Label({
+ attribute: {
+ updateCount: (self) => {
+ const count = Notifications.notifications.length;
+ if (count > 0) self.label = `${count} notifications`;
+ else self.label = '';
+ },
+ },
+ hexpand: true,
+ xalign: 0,
+ className: 'txt-small margin-left-10',
+ label: `${Notifications.notifications.length}`,
+ setup: (self) => self
+ .hook(Notifications, (box, id) => self.attribute.updateCount(self), 'notified')
+ .hook(Notifications, (box, id) => self.attribute.updateCount(self), 'dismissed')
+ .hook(Notifications, (box, id) => self.attribute.updateCount(self), 'closed')
+ ,
+ });
+ const listTitle = Box({
+ vpack: 'start',
+ className: 'txt spacing-h-5',
+ children: [
+ notifCount,
+ silenceButton,
+ // silenceToggle,
+ // Box({ hexpand: true }),
+ clearButton,
+ ]
+ });
+ const notifList = Scrollable({
+ hexpand: true,
+ hscroll: 'never',
+ vscroll: 'automatic',
+ child: Box({
+ vexpand: true,
+ homogeneous: true,
+ children: [notificationList],
+ }),
+ setup: (self) => {
+ const vScrollbar = self.get_vscrollbar();
+ vScrollbar.get_style_context().add_class('sidebar-scrollbar');
+ }
+ });
+ const listContents = Stack({
+ transition: 'crossfade',
+ transitionDuration: userOptions.animations.durationLarge,
+ children: {
+ 'empty': notifEmptyContent,
+ 'list': notifList,
+ },
+ setup: (self) => self.hook(Notifications, (self) => {
+ self.shown = (Notifications.notifications.length > 0 ? 'list' : 'empty')
+ }),
+ });
+ return Box({
+ ...props,
+ className: 'spacing-v-5',
+ vertical: true,
+ children: [
+ listContents,
+ listTitle,
+ ]
+ });
+}
diff --git a/modules/home-manager/ags/ags/modules/sideright/centermodules/wifinetworks.js b/modules/home-manager/ags/ags/modules/sideright/centermodules/wifinetworks.js
new file mode 100644
index 0000000..4a68c91
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideright/centermodules/wifinetworks.js
@@ -0,0 +1,216 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import Network from "resource:///com/github/Aylur/ags/service/network.js";
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { Box, Button, Entry, Icon, Label, Revealer, Scrollable, Slider, Stack, Overlay } = Widget;
+const { execAsync, exec } = Utils;
+import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
+import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
+import { ConfigToggle } from '../../.commonwidgets/configwidgets.js';
+
+const MATERIAL_SYMBOL_SIGNAL_STRENGTH = {
+ 'network-wireless-signal-excellent-symbolic': "signal_wifi_4_bar",
+ 'network-wireless-signal-good-symbolic': "network_wifi_3_bar",
+ 'network-wireless-signal-ok-symbolic': "network_wifi_2_bar",
+ 'network-wireless-signal-weak-symbolic': "network_wifi_1_bar",
+ 'network-wireless-signal-none-symbolic': "signal_wifi_0_bar",
+}
+
+let connectAttempt = '';
+
+const WifiNetwork = (accessPoint) => {
+ const networkStrength = MaterialIcon(MATERIAL_SYMBOL_SIGNAL_STRENGTH[accessPoint.iconName], 'hugerass')
+ const networkName = Box({
+ vertical: true,
+ children: [
+ Label({
+ hpack: 'start',
+ label: accessPoint.ssid
+ }),
+ accessPoint.active ? Label({
+ hpack: 'start',
+ className: 'txt-smaller txt-subtext',
+ label: "Selected",
+ }) : null,
+ ]
+ });
+ return Button({
+ onClicked: accessPoint.active ? () => { } : () => execAsync(`nmcli device wifi connect ${accessPoint.bssid}`)
+ // .catch(e => {
+ // Utils.notify({
+ // summary: "Network",
+ // body: e,
+ // actions: { "Open network manager": () => execAsync("nm-connection-editor").catch(print) }
+ // });
+ // })
+ .catch(print),
+ child: Box({
+ className: 'sidebar-wifinetworks-network spacing-h-10',
+ children: [
+ networkStrength,
+ networkName,
+ Box({ hexpand: true }),
+ accessPoint.active ? MaterialIcon('check', 'large') : null,
+ ],
+ }),
+ setup: accessPoint.active ? () => { } : setupCursorHover,
+ })
+}
+
+const CurrentNetwork = () => {
+ let authLock = false;
+ // console.log(Network.wifi);
+ const bottomSeparator = Box({
+ className: 'separator-line',
+ });
+ const networkName = Box({
+ vertical: true,
+ hexpand: true,
+ children: [
+ Label({
+ hpack: 'start',
+ className: 'txt-smaller txt-subtext',
+ label: "Current network",
+ }),
+ Label({
+ hpack: 'start',
+ label: Network.wifi?.ssid,
+ setup: (self) => self.hook(Network, (self) => {
+ if (authLock) return;
+ self.label = Network.wifi?.ssid;
+ }),
+ }),
+ ]
+ });
+ const networkStatus = Box({
+ children: [Label({
+ vpack: 'center',
+ className: 'txt-subtext',
+ setup: (self) => self.hook(Network, (self) => {
+ if (authLock) return;
+ self.label = Network.wifi.state;
+ }),
+ })]
+ })
+ const networkAuth = Revealer({
+ transition: 'slide_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: Box({
+ className: 'margin-top-10 spacing-v-5',
+ vertical: true,
+ children: [
+ Label({
+ className: 'margin-left-5',
+ hpack: 'start',
+ label: "Authentication",
+ }),
+ Entry({
+ className: 'sidebar-wifinetworks-auth-entry',
+ visibility: false, // Password dots
+ onAccept: (self) => {
+ authLock = false;
+ networkAuth.revealChild = false;
+ execAsync(`nmcli device wifi connect '${connectAttempt}' password '${self.text}'`)
+ .catch(print);
+ }
+ })
+ ]
+ }),
+ setup: (self) => self.hook(Network, (self) => {
+ if (Network.wifi.state == 'failed' || Network.wifi.state == 'need_auth') {
+ authLock = true;
+ connectAttempt = Network.wifi.ssid;
+ self.revealChild = true;
+ }
+ }),
+ });
+ const actualContent = Box({
+ vertical: true,
+ className: 'spacing-v-10',
+ children: [
+ Box({
+ className: 'sidebar-wifinetworks-network',
+ vertical: true,
+ children: [
+ Box({
+ className: 'spacing-h-10',
+ children: [
+ MaterialIcon('language', 'hugerass'),
+ networkName,
+ networkStatus,
+
+ ]
+ }),
+ networkAuth,
+ ]
+ }),
+ bottomSeparator,
+ ]
+ });
+ return Box({
+ vertical: true,
+ children: [Revealer({
+ transition: 'slide_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ revealChild: Network.wifi,
+ child: actualContent,
+ })]
+ })
+}
+
+export default (props) => {
+ const networkList = Box({
+ vertical: true,
+ className: 'spacing-v-10',
+ children: [Overlay({
+ passThrough: true,
+ child: Scrollable({
+ vexpand: true,
+ child: Box({
+ attribute: {
+ 'updateNetworks': (self) => {
+ const accessPoints = Network.wifi?.access_points || [];
+ self.children = Object.values(accessPoints.reduce((a, accessPoint) => {
+ // Only keep max strength networks by ssid
+ if (!a[accessPoint.ssid] || a[accessPoint.ssid].strength < accessPoint.strength) {
+ a[accessPoint.ssid] = accessPoint;
+ a[accessPoint.ssid].active |= accessPoint.active;
+ }
+
+ return a;
+ }, {})).map(n => WifiNetwork(n));
+ },
+ },
+ vertical: true,
+ className: 'spacing-v-5 margin-bottom-15',
+ setup: (self) => self.hook(Network, self.attribute.updateNetworks),
+ })
+ }),
+ overlays: [Box({
+ className: 'sidebar-centermodules-scrollgradient-bottom'
+ })]
+ })]
+ });
+ const bottomBar = Box({
+ homogeneous: true,
+ children: [Button({
+ hpack: 'center',
+ className: 'txt-small txt sidebar-centermodules-bottombar-button',
+ onClicked: () => {
+ execAsync(['bash', '-c', userOptions.apps.network]).catch(print);
+ closeEverything();
+ },
+ label: 'More',
+ setup: setupCursorHover,
+ })],
+ })
+ return Box({
+ ...props,
+ className: 'spacing-v-10',
+ vertical: true,
+ children: [
+ CurrentNetwork(),
+ networkList,
+ bottomBar,
+ ]
+ });
+}
diff --git a/modules/home-manager/ags/ags/modules/sideright/main.js b/modules/home-manager/ags/ags/modules/sideright/main.js
new file mode 100644
index 0000000..69a58b2
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideright/main.js
@@ -0,0 +1,18 @@
+import PopupWindow from '../.widgethacks/popupwindow.js';
+import SidebarRight from "./sideright.js";
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+const { Box } = Widget;
+import clickCloseRegion from '../.commonwidgets/clickcloseregion.js';
+
+export default () => PopupWindow({
+ keymode: 'on-demand',
+ anchor: ['right', 'top', 'bottom'],
+ name: 'sideright',
+ layer: 'overlay',
+ child: Box({
+ children: [
+ clickCloseRegion({ name: 'sideright', multimonitor: false, fillMonitor: 'horizontal' }),
+ SidebarRight(),
+ ]
+ })
+});
diff --git a/modules/home-manager/ags/ags/modules/sideright/quicktoggles.js b/modules/home-manager/ags/ags/modules/sideright/quicktoggles.js
new file mode 100644
index 0000000..ea6a9a1
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideright/quicktoggles.js
@@ -0,0 +1,267 @@
+const { GLib } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+
+import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
+import Network from 'resource:///com/github/Aylur/ags/service/network.js';
+const { execAsync, exec } = Utils;
+import { BluetoothIndicator, NetworkIndicator } from '../.commonwidgets/statusicons.js';
+import { setupCursorHover } from '../.widgetutils/cursorhover.js';
+import { MaterialIcon } from '../.commonwidgets/materialicon.js';
+import { sidebarOptionsStack } from './sideright.js';
+
+export const ToggleIconWifi = (props = {}) => Widget.Button({
+ className: 'txt-small sidebar-iconbutton',
+ tooltipText: 'Wifi | Right-click to configure',
+ onClicked: () => Network.toggleWifi(),
+ onSecondaryClickRelease: () => {
+ execAsync(['bash', '-c', `${userOptions.apps.network}`]).catch(print);
+ closeEverything();
+ },
+ child: NetworkIndicator(),
+ setup: (self) => {
+ setupCursorHover(self);
+ self.hook(Network, button => {
+ button.toggleClassName('sidebar-button-active', [Network.wifi?.internet, Network.wired?.internet].includes('connected'))
+ button.tooltipText = (`${Network.wifi?.ssid} | Right-click to configure` || 'Unknown');
+ });
+ },
+ ...props,
+});
+
+export const ToggleIconBluetooth = (props = {}) => Widget.Button({
+ className: 'txt-small sidebar-iconbutton',
+ tooltipText: 'Bluetooth | Right-click to configure',
+ onClicked: () => {
+ const status = Bluetooth?.enabled;
+ if (status)
+ exec('rfkill block bluetooth');
+ else
+ exec('rfkill unblock bluetooth');
+ },
+ onSecondaryClickRelease: () => {
+ execAsync(['bash', '-c', `${userOptions.apps.bluetooth}`]).catch(print);
+ closeEverything();
+ },
+ child: BluetoothIndicator(),
+ setup: (self) => {
+ setupCursorHover(self);
+ self.hook(Bluetooth, button => {
+ button.toggleClassName('sidebar-button-active', Bluetooth?.enabled)
+ });
+ },
+ ...props,
+});
+
+export const HyprToggleIcon = async (icon, name, hyprlandConfigValue, props = {}) => {
+ try {
+ return Widget.Button({
+ className: 'txt-small sidebar-iconbutton',
+ tooltipText: `${name}`,
+ onClicked: (button) => {
+ // Set the value to 1 - value
+ Utils.execAsync(`hyprctl -j getoption ${hyprlandConfigValue}`).then((result) => {
+ const currentOption = JSON.parse(result).int;
+ execAsync(['bash', '-c', `hyprctl keyword ${hyprlandConfigValue} ${1 - currentOption} &`]).catch(print);
+ button.toggleClassName('sidebar-button-active', currentOption == 0);
+ }).catch(print);
+ },
+ child: MaterialIcon(icon, 'norm', { hpack: 'center' }),
+ setup: button => {
+ button.toggleClassName('sidebar-button-active', JSON.parse(Utils.exec(`hyprctl -j getoption ${hyprlandConfigValue}`)).int == 1);
+ setupCursorHover(button);
+ },
+ ...props,
+ })
+ } catch {
+ return null;
+ }
+}
+
+export const ModuleNightLight = async (props = {}) => {
+ if (!exec(`bash -c 'command -v gammastep'`)) return null;
+ return Widget.Button({
+ attribute: {
+ enabled: false,
+ },
+ className: 'txt-small sidebar-iconbutton',
+ tooltipText: 'Night Light',
+ onClicked: (self) => {
+ self.attribute.enabled = !self.attribute.enabled;
+ self.toggleClassName('sidebar-button-active', self.attribute.enabled);
+ if (self.attribute.enabled) Utils.execAsync('gammastep').catch(print)
+ else Utils.execAsync('pkill gammastep')
+ .then(() => {
+ // disable the button until fully terminated to avoid race
+ self.sensitive = false;
+ const source = setInterval(() => {
+ Utils.execAsync('pkill -0 gammastep')
+ .catch(() => {
+ self.sensitive = true;
+ source.destroy();
+ });
+ }, 500);
+ })
+ .catch(print);
+ },
+ child: MaterialIcon('nightlight', 'norm'),
+ setup: (self) => {
+ setupCursorHover(self);
+ self.attribute.enabled = !!exec('pidof gammastep');
+ self.toggleClassName('sidebar-button-active', self.attribute.enabled);
+ },
+ ...props,
+ });
+}
+
+export const ModuleCloudflareWarp = async (props = {}) => {
+ if (!exec(`bash -c 'command -v warp-cli'`)) return null;
+ return Widget.Button({
+ attribute: {
+ enabled: false,
+ },
+ className: 'txt-small sidebar-iconbutton',
+ tooltipText: 'Cloudflare WARP',
+ onClicked: (self) => {
+ self.attribute.enabled = !self.attribute.enabled;
+ self.toggleClassName('sidebar-button-active', self.attribute.enabled);
+ if (self.attribute.enabled) Utils.execAsync('warp-cli connect').catch(print)
+ else Utils.execAsync('warp-cli disconnect').catch(print);
+ },
+ child: Widget.Icon({
+ icon: 'cloudflare-dns-symbolic',
+ className: 'txt-norm',
+ }),
+ setup: (self) => {
+ setupCursorHover(self);
+ self.attribute.enabled = !exec(`bash -c 'warp-cli status | grep Disconnected'`);
+ self.toggleClassName('sidebar-button-active', self.attribute.enabled);
+ },
+ ...props,
+ });
+}
+
+export const ModuleInvertColors = async (props = {}) => {
+ try {
+ const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
+ return Widget.Button({
+ className: 'txt-small sidebar-iconbutton',
+ tooltipText: 'Color inversion',
+ onClicked: (button) => {
+ // const shaderPath = JSON.parse(exec('hyprctl -j getoption decoration:screen_shader')).str;
+ Hyprland.messageAsync('j/getoption decoration:screen_shader')
+ .then((output) => {
+ const shaderPath = JSON.parse(output)["str"].trim();
+ if (shaderPath != "[[EMPTY]]" && shaderPath != "") {
+ execAsync(['bash', '-c', `hyprctl keyword decoration:screen_shader '[[EMPTY]]'`]).catch(print);
+ button.toggleClassName('sidebar-button-active', false);
+ }
+ else {
+ Hyprland.messageAsync(`j/keyword decoration:screen_shader ${GLib.get_user_config_dir()}/hypr/shaders/invert.frag`)
+ .catch(print);
+ button.toggleClassName('sidebar-button-active', true);
+ }
+ })
+ },
+ child: MaterialIcon('invert_colors', 'norm'),
+ setup: setupCursorHover,
+ ...props,
+ })
+ } catch {
+ return null;
+ };
+}
+
+export const ModuleRawInput = async (props = {}) => {
+ try {
+ const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
+ return Widget.Button({
+ className: 'txt-small sidebar-iconbutton',
+ tooltipText: 'Raw input',
+ onClicked: (button) => {
+ Hyprland.messageAsync('j/getoption input:accel_profile')
+ .then((output) => {
+ const value = JSON.parse(output)["str"].trim();
+ if (value != "[[EMPTY]]" && value != "") {
+ execAsync(['bash', '-c', `hyprctl keyword input:accel_profile '[[EMPTY]]'`]).catch(print);
+ button.toggleClassName('sidebar-button-active', false);
+ }
+ else {
+ Hyprland.messageAsync(`j/keyword input:accel_profile flat`)
+ .catch(print);
+ button.toggleClassName('sidebar-button-active', true);
+ }
+ })
+ },
+ child: MaterialIcon('mouse', 'norm'),
+ setup: setupCursorHover,
+ ...props,
+ })
+ } catch {
+ return null;
+ };
+}
+
+export const ModuleIdleInhibitor = (props = {}) => Widget.Button({ // TODO: Make this work
+ attribute: {
+ enabled: false,
+ },
+ className: 'txt-small sidebar-iconbutton',
+ tooltipText: 'Keep system awake',
+ onClicked: (self) => {
+ self.attribute.enabled = !self.attribute.enabled;
+ self.toggleClassName('sidebar-button-active', self.attribute.enabled);
+ if (self.attribute.enabled) Utils.execAsync(['bash', '-c', `pidof wayland-idle-inhibitor.py || ${App.configDir}/scripts/wayland-idle-inhibitor.py`]).catch(print)
+ else Utils.execAsync('pkill -f wayland-idle-inhibitor.py').catch(print);
+ },
+ child: MaterialIcon('coffee', 'norm'),
+ setup: (self) => {
+ setupCursorHover(self);
+ self.attribute.enabled = !!exec('pidof wayland-idle-inhibitor.py');
+ self.toggleClassName('sidebar-button-active', self.attribute.enabled);
+ },
+ ...props,
+});
+
+export const ModuleReloadIcon = (props = {}) => Widget.Button({
+ ...props,
+ className: 'txt-small sidebar-iconbutton',
+ tooltipText: 'Reload Environment config',
+ onClicked: () => {
+ execAsync(['bash', '-c', 'hyprctl reload &']);
+ App.closeWindow('sideright');
+ },
+ child: MaterialIcon('refresh', 'norm'),
+ setup: button => {
+ setupCursorHover(button);
+ }
+})
+
+export const ModuleSettingsIcon = (props = {}) => Widget.Button({
+ ...props,
+ className: 'txt-small sidebar-iconbutton',
+ tooltipText: 'Open Settings',
+ onClicked: () => {
+ execAsync(['bash', '-c', `${userOptions.apps.settings}`, '&']);
+ App.closeWindow('sideright');
+ },
+ child: MaterialIcon('settings', 'norm'),
+ setup: button => {
+ setupCursorHover(button);
+ }
+})
+
+export const ModulePowerIcon = (props = {}) => Widget.Button({
+ ...props,
+ className: 'txt-small sidebar-iconbutton',
+ tooltipText: 'Session',
+ onClicked: () => {
+ closeEverything();
+ Utils.timeout(1, () => openWindowOnAllMonitors('session'));
+ },
+ child: MaterialIcon('power_settings_new', 'norm'),
+ setup: button => {
+ setupCursorHover(button);
+ }
+})
diff --git a/modules/home-manager/ags/ags/modules/sideright/sideright.js b/modules/home-manager/ags/ags/modules/sideright/sideright.js
new file mode 100644
index 0000000..a19624e
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideright/sideright.js
@@ -0,0 +1,185 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { execAsync, exec } = Utils;
+const { Box, EventBox } = Widget;
+import {
+ ToggleIconBluetooth,
+ ToggleIconWifi,
+ HyprToggleIcon,
+ ModuleNightLight,
+ ModuleInvertColors,
+ ModuleIdleInhibitor,
+ ModuleReloadIcon,
+ ModuleSettingsIcon,
+ ModulePowerIcon,
+ ModuleRawInput,
+ ModuleCloudflareWarp
+} from "./quicktoggles.js";
+import ModuleNotificationList from "./centermodules/notificationlist.js";
+import ModuleAudioControls from "./centermodules/audiocontrols.js";
+import ModuleWifiNetworks from "./centermodules/wifinetworks.js";
+import ModuleBluetooth from "./centermodules/bluetooth.js";
+import ModuleConfigure from "./centermodules/configure.js";
+import { ModuleCalendar } from "./calendar.js";
+import { getDistroIcon } from '../.miscutils/system.js';
+import { MaterialIcon } from '../.commonwidgets/materialicon.js';
+import { ExpandingIconTabContainer } from '../.commonwidgets/tabcontainer.js';
+import { checkKeybind } from '../.widgetutils/keybind.js';
+
+const centerWidgets = [
+ {
+ name: 'Notifications',
+ materialIcon: 'notifications',
+ contentWidget: ModuleNotificationList,
+ },
+ {
+ name: 'Audio controls',
+ materialIcon: 'volume_up',
+ contentWidget: ModuleAudioControls,
+ },
+ {
+ name: 'Bluetooth',
+ materialIcon: 'bluetooth',
+ contentWidget: ModuleBluetooth,
+ },
+ {
+ name: 'Wifi networks',
+ materialIcon: 'wifi',
+ contentWidget: ModuleWifiNetworks,
+ onFocus: () => execAsync('nmcli dev wifi list').catch(print),
+ },
+ {
+ name: 'Live config',
+ materialIcon: 'tune',
+ contentWidget: ModuleConfigure,
+ },
+];
+
+const timeRow = Box({
+ className: 'spacing-h-10 sidebar-group-invisible-morehorizpad',
+ children: [
+ Widget.Icon({
+ icon: getDistroIcon(),
+ className: 'txt txt-larger',
+ }),
+ Widget.Label({
+ hpack: 'center',
+ className: 'txt-small txt',
+ setup: (self) => {
+ const getUptime = async () => {
+ try {
+ await execAsync(['bash', '-c', 'uptime -p']);
+ return execAsync(['bash', '-c', `uptime -p | sed -e 's/...//;s/ day\\| days/d/;s/ hour\\| hours/h/;s/ minute\\| minutes/m/;s/,[^,]*//2'`]);
+ } catch {
+ return execAsync(['bash', '-c', 'uptime']).then(output => {
+ const uptimeRegex = /up\s+((\d+)\s+days?,\s+)?((\d+):(\d+)),/;
+ const matches = uptimeRegex.exec(output);
+
+ if (matches) {
+ const days = matches[2] ? parseInt(matches[2]) : 0;
+ const hours = matches[4] ? parseInt(matches[4]) : 0;
+ const minutes = matches[5] ? parseInt(matches[5]) : 0;
+
+ let formattedUptime = '';
+
+ if (days > 0) {
+ formattedUptime += `${days} d `;
+ }
+ if (hours > 0) {
+ formattedUptime += `${hours} h `;
+ }
+ formattedUptime += `${minutes} m`;
+
+ return formattedUptime;
+ } else {
+ throw new Error('Failed to parse uptime output');
+ }
+ });
+ }
+ };
+
+ self.poll(5000, label => {
+ getUptime().then(upTimeString => {
+ label.label = `Uptime: ${upTimeString}`;
+ }).catch(err => {
+ console.error(`Failed to fetch uptime: ${err}`);
+ });
+ });
+ },
+ }),
+ Widget.Box({ hexpand: true }),
+ ModuleReloadIcon({ hpack: 'end' }),
+ // ModuleSettingsIcon({ hpack: 'end' }), // Button does work, gnome-control-center is kinda broken
+ ModulePowerIcon({ hpack: 'end' }),
+ ]
+});
+
+const togglesBox = Widget.Box({
+ hpack: 'center',
+ className: 'sidebar-togglesbox spacing-h-5',
+ children: [
+ ToggleIconWifi(),
+ ToggleIconBluetooth(),
+ await ModuleNightLight(),
+ await ModuleInvertColors(),
+ ModuleIdleInhibitor(),
+ await ModuleCloudflareWarp(),
+ ]
+})
+
+export const sidebarOptionsStack = ExpandingIconTabContainer({
+ tabsHpack: 'center',
+ tabSwitcherClassName: 'sidebar-icontabswitcher',
+ icons: centerWidgets.map((api) => api.materialIcon),
+ names: centerWidgets.map((api) => api.name),
+ children: centerWidgets.map((api) => api.contentWidget()),
+ onChange: (self, id) => {
+ self.shown = centerWidgets[id].name;
+ if (centerWidgets[id].onFocus) centerWidgets[id].onFocus();
+ }
+});
+
+export default () => Box({
+ vexpand: true,
+ hexpand: true,
+ css: 'min-width: 2px;',
+ children: [
+ EventBox({
+ onPrimaryClick: () => App.closeWindow('sideright'),
+ onSecondaryClick: () => App.closeWindow('sideright'),
+ onMiddleClick: () => App.closeWindow('sideright'),
+ }),
+ Box({
+ vertical: true,
+ vexpand: true,
+ className: 'sidebar-right spacing-v-15',
+ children: [
+ Box({
+ vertical: true,
+ className: 'spacing-v-5',
+ children: [
+ timeRow,
+ togglesBox,
+ ]
+ }),
+ Box({
+ className: 'sidebar-group',
+ children: [
+ sidebarOptionsStack,
+ ],
+ }),
+ ModuleCalendar(),
+ ]
+ }),
+ ],
+ setup: (self) => self
+ .on('key-press-event', (widget, event) => { // Handle keybinds
+ if (checkKeybind(event, userOptions.keybinds.sidebar.options.nextTab)) {
+ sidebarOptionsStack.nextTab();
+ }
+ else if (checkKeybind(event, userOptions.keybinds.sidebar.options.prevTab)) {
+ sidebarOptionsStack.prevTab();
+ }
+ })
+ ,
+});
diff --git a/modules/home-manager/ags/ags/modules/sideright/todolist.js b/modules/home-manager/ags/ags/modules/sideright/todolist.js
new file mode 100644
index 0000000..30008a3
--- /dev/null
+++ b/modules/home-manager/ags/ags/modules/sideright/todolist.js
@@ -0,0 +1,224 @@
+import Widget from 'resource:///com/github/Aylur/ags/widget.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { Box, Button, Label, Revealer } = Widget;
+import { MaterialIcon } from '../.commonwidgets/materialicon.js';
+import { TabContainer } from '../.commonwidgets/tabcontainer.js';
+import Todo from "../../services/todo.js";
+import { setupCursorHover } from '../.widgetutils/cursorhover.js';
+
+const TodoListItem = (task, id, isDone, isEven = false) => {
+ const taskName = Widget.Label({
+ hexpand: true,
+ xalign: 0,
+ wrap: true,
+ className: 'txt txt-small sidebar-todo-txt',
+ label: task.content,
+ selectable: true,
+ });
+ const actions = Box({
+ hpack: 'end',
+ className: 'spacing-h-5 sidebar-todo-actions',
+ children: [
+ Widget.Button({ // Check/Uncheck
+ vpack: 'center',
+ className: 'txt sidebar-todo-item-action',
+ child: MaterialIcon(`${isDone ? 'remove_done' : 'check'}`, 'norm', { vpack: 'center' }),
+ onClicked: (self) => {
+ const contentWidth = todoContent.get_allocated_width();
+ crosser.toggleClassName('sidebar-todo-crosser-crossed', true);
+ crosser.css = `margin-left: -${contentWidth}px;`;
+ Utils.timeout(200, () => {
+ widgetRevealer.revealChild = false;
+ })
+ Utils.timeout(350, () => {
+ if (isDone)
+ Todo.uncheck(id);
+ else
+ Todo.check(id);
+ })
+ },
+ setup: setupCursorHover,
+ }),
+ Widget.Button({ // Remove
+ vpack: 'center',
+ className: 'txt sidebar-todo-item-action',
+ child: MaterialIcon('delete_forever', 'norm', { vpack: 'center' }),
+ onClicked: () => {
+ const contentWidth = todoContent.get_allocated_width();
+ crosser.toggleClassName('sidebar-todo-crosser-removed', true);
+ crosser.css = `margin-left: -${contentWidth}px;`;
+ Utils.timeout(200, () => {
+ widgetRevealer.revealChild = false;
+ })
+ Utils.timeout(350, () => {
+ Todo.remove(id);
+ })
+ },
+ setup: setupCursorHover,
+ }),
+ ]
+ })
+ const crosser = Widget.Box({
+ className: 'sidebar-todo-crosser',
+ });
+ const todoContent = Widget.Box({
+ className: 'sidebar-todo-item spacing-h-5',
+ children: [
+ Widget.Box({
+ vertical: true,
+ children: [
+ taskName,
+ actions,
+ ]
+ }),
+ crosser,
+ ]
+ });
+ const widgetRevealer = Widget.Revealer({
+ revealChild: true,
+ transition: 'slide_down',
+ transitionDuration: userOptions.animations.durationLarge,
+ child: todoContent,
+ })
+ return Box({
+ homogeneous: true,
+ children: [widgetRevealer]
+ });
+}
+
+const todoItems = (isDone) => Widget.Scrollable({
+ hscroll: 'never',
+ vscroll: 'automatic',
+ child: Widget.Box({
+ vertical: true,
+ className: 'spacing-v-5',
+ setup: (self) => self
+ .hook(Todo, (self) => {
+ self.children = Todo.todo_json.map((task, i) => {
+ if (task.done != isDone) return null;
+ return TodoListItem(task, i, isDone);
+ })
+ if (self.children.length == 0) {
+ self.homogeneous = true;
+ self.children = [
+ Widget.Box({
+ hexpand: true,
+ vertical: true,
+ vpack: 'center',
+ className: 'txt txt-subtext',
+ children: [
+ MaterialIcon(`${isDone ? 'checklist' : 'check_circle'}`, 'gigantic'),
+ Label({ label: `${isDone ? 'Finished tasks will go here' : 'Nothing here!'}` })
+ ]
+ })
+ ]
+ }
+ else self.homogeneous = false;
+ }, 'updated')
+ ,
+ }),
+ setup: (listContents) => {
+ const vScrollbar = listContents.get_vscrollbar();
+ vScrollbar.get_style_context().add_class('sidebar-scrollbar');
+ }
+});
+
+const UndoneTodoList = () => {
+ const newTaskButton = Revealer({
+ transition: 'slide_left',
+ transitionDuration: userOptions.animations.durationLarge,
+ revealChild: true,
+ child: Button({
+ className: 'txt-small sidebar-todo-new',
+ halign: 'end',
+ vpack: 'center',
+ label: '+ New task',
+ setup: setupCursorHover,
+ onClicked: (self) => {
+ newTaskButton.revealChild = false;
+ newTaskEntryRevealer.revealChild = true;
+ confirmAddTask.revealChild = true;
+ cancelAddTask.revealChild = true;
+ newTaskEntry.grab_focus();
+ }
+ })
+ });
+ const cancelAddTask = Revealer({
+ transition: 'slide_right',
+ transitionDuration: userOptions.animations.durationLarge,
+ revealChild: false,
+ child: Button({
+ className: 'txt-norm icon-material sidebar-todo-add',
+ halign: 'end',
+ vpack: 'center',
+ label: 'close',
+ setup: setupCursorHover,
+ onClicked: (self) => {
+ newTaskEntryRevealer.revealChild = false;
+ confirmAddTask.revealChild = false;
+ cancelAddTask.revealChild = false;
+ newTaskButton.revealChild = true;
+ newTaskEntry.text = '';
+ }
+ })
+ });
+ const newTaskEntry = Widget.Entry({
+ // hexpand: true,
+ vpack: 'center',
+ className: 'txt-small sidebar-todo-entry',
+ placeholderText: 'Add a task...',
+ onAccept: ({ text }) => {
+ if (text == '') return;
+ Todo.add(text)
+ newTaskEntry.text = '';
+ },
+ onChange: ({ text }) => confirmAddTask.child.toggleClassName('sidebar-todo-add-available', text != ''),
+ });
+ const newTaskEntryRevealer = Revealer({
+ transition: 'slide_right',
+ transitionDuration: userOptions.animations.durationLarge,
+ revealChild: false,
+ child: newTaskEntry,
+ });
+ const confirmAddTask = Revealer({
+ transition: 'slide_right',
+ transitionDuration: userOptions.animations.durationLarge,
+ revealChild: false,
+ child: Button({
+ className: 'txt-norm icon-material sidebar-todo-add',
+ halign: 'end',
+ vpack: 'center',
+ label: 'arrow_upward',
+ setup: setupCursorHover,
+ onClicked: (self) => {
+ if (newTaskEntry.text == '') return;
+ Todo.add(newTaskEntry.text);
+ newTaskEntry.text = '';
+ }
+ })
+ });
+ return Box({ // The list, with a New button
+ vertical: true,
+ className: 'spacing-v-5',
+ setup: (box) => {
+ box.pack_start(todoItems(false), true, true, 0);
+ box.pack_start(Box({
+ setup: (self) => {
+ self.pack_start(cancelAddTask, false, false, 0);
+ self.pack_start(newTaskEntryRevealer, true, true, 0);
+ self.pack_start(confirmAddTask, false, false, 0);
+ self.pack_start(newTaskButton, false, false, 0);
+ }
+ }), false, false, 0);
+ },
+ });
+}
+
+export const TodoWidget = () => TabContainer({
+ icons: ['format_list_bulleted', 'task_alt'],
+ names: ['Unfinished', 'Done'],
+ children: [
+ UndoneTodoList(),
+ todoItems(true),
+ ]
+})
diff --git a/modules/home-manager/ags/ags/scripts/README.md b/modules/home-manager/ags/ags/scripts/README.md
new file mode 100644
index 0000000..6d79a51
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/README.md
@@ -0,0 +1,3 @@
+# scripts folder
+- For ARM devices, you have to compile C++ files yourself. If there are none, yippee
+- It is advised to use services instead of listening/polling scripts, so everything here are just scripts for actions
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/scripts/color_generation/applycolor.sh b/modules/home-manager/ags/ags/scripts/color_generation/applycolor.sh
new file mode 100755
index 0000000..db056d4
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/color_generation/applycolor.sh
@@ -0,0 +1,182 @@
+#!/usr/bin/env bash
+
+XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
+XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
+XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}"
+CONFIG_DIR="$XDG_CONFIG_HOME/ags"
+CACHE_DIR="$XDG_CACHE_HOME/ags"
+STATE_DIR="$XDG_STATE_HOME/ags"
+
+term_alpha=100 #Set this to < 100 make all your terminals transparent
+# sleep 0 # idk i wanted some delay or colors dont get applied properly
+if [ ! -d "$CACHE_DIR"/user/generated ]; then
+ mkdir -p "$CACHE_DIR"/user/generated
+fi
+cd "$CONFIG_DIR" || exit
+
+colornames=''
+colorstrings=''
+colorlist=()
+colorvalues=()
+
+# wallpath=$(swww query | head -1 | awk -F 'image: ' '{print $2}')
+# wallpath_png="$CACHE_DIR/user/generated/hypr/lockscreen.png"
+# convert "$wallpath" "$wallpath_png"
+# wallpath_png=$(echo "$wallpath_png" | sed 's/\//\\\//g')
+# wallpath_png=$(sed 's/\//\\\\\//g' <<< "$wallpath_png")
+
+transparentize() {
+ local hex="$1"
+ local alpha="$2"
+ local red green blue
+
+ red=$((16#${hex:1:2}))
+ green=$((16#${hex:3:2}))
+ blue=$((16#${hex:5:2}))
+
+ printf 'rgba(%d, %d, %d, %.2f)\n' "$red" "$green" "$blue" "$alpha"
+}
+
+get_light_dark() {
+ lightdark=""
+ if [ ! -f "$STATE_DIR/user/colormode.txt" ]; then
+ echo "" > "$STATE_DIR/user/colormode.txt"
+ else
+ lightdark=$(sed -n '1p' "$STATE_DIR/user/colormode.txt")
+ fi
+ echo "$lightdark"
+}
+
+apply_fuzzel() {
+ # Check if scripts/templates/fuzzel/fuzzel.ini exists
+ if [ ! -f "scripts/templates/fuzzel/fuzzel.ini" ]; then
+ echo "Template file not found for Fuzzel. Skipping that."
+ return
+ fi
+ # Copy template
+ mkdir -p "$CACHE_DIR"/user/generated/fuzzel
+ cp "scripts/templates/fuzzel/fuzzel.ini" "$CACHE_DIR"/user/generated/fuzzel/fuzzel.ini
+ # Apply colors
+ for i in "${!colorlist[@]}"; do
+ sed -i "s/{{ ${colorlist[$i]} }}/${colorvalues[$i]#\#}/g" "$CACHE_DIR"/user/generated/fuzzel/fuzzel.ini
+ done
+
+ cp "$CACHE_DIR"/user/generated/fuzzel/fuzzel.ini "$XDG_CONFIG_HOME"/fuzzel/fuzzel.ini
+}
+
+apply_term() {
+ # Check if terminal escape sequence template exists
+ if [ ! -f "scripts/templates/terminal/sequences.txt" ]; then
+ echo "Template file not found for Terminal. Skipping that."
+ return
+ fi
+ # Copy template
+ mkdir -p "$CACHE_DIR"/user/generated/terminal
+ cp "scripts/templates/terminal/sequences.txt" "$CACHE_DIR"/user/generated/terminal/sequences.txt
+ # Apply colors
+ for i in "${!colorlist[@]}"; do
+ sed -i "s/${colorlist[$i]} #/${colorvalues[$i]#\#}/g" "$CACHE_DIR"/user/generated/terminal/sequences.txt
+ done
+
+ sed -i "s/\$alpha/$term_alpha/g" "$CACHE_DIR/user/generated/terminal/sequences.txt"
+
+ for file in /dev/pts/*; do
+ if [[ $file =~ ^/dev/pts/[0-9]+$ ]]; then
+ cat "$CACHE_DIR"/user/generated/terminal/sequences.txt > "$file"
+ fi
+ done
+}
+
+apply_hyprland() {
+ # Check if scripts/templates/hypr/hyprland/colors.conf exists
+ if [ ! -f "scripts/templates/hypr/hyprland/colors.conf" ]; then
+ echo "Template file not found for Hyprland colors. Skipping that."
+ return
+ fi
+ # Copy template
+ mkdir -p "$CACHE_DIR"/user/generated/hypr/hyprland
+ cp "scripts/templates/hypr/hyprland/colors.conf" "$CACHE_DIR"/user/generated/hypr/hyprland/colors.conf
+ # Apply colors
+ for i in "${!colorlist[@]}"; do
+ sed -i "s/{{ ${colorlist[$i]} }}/${colorvalues[$i]#\#}/g" "$CACHE_DIR"/user/generated/hypr/hyprland/colors.conf
+ done
+
+ cp "$CACHE_DIR"/user/generated/hypr/hyprland/colors.conf "$XDG_CONFIG_HOME"/hypr/hyprland/colors.conf
+}
+
+apply_hyprlock() {
+ # Check if scripts/templates/hypr/hyprlock.conf exists
+ if [ ! -f "scripts/templates/hypr/hyprlock.conf" ]; then
+ echo "Template file not found for hyprlock. Skipping that."
+ return
+ fi
+ # Copy template
+ mkdir -p "$CACHE_DIR"/user/generated/hypr/
+ cp "scripts/templates/hypr/hyprlock.conf" "$CACHE_DIR"/user/generated/hypr/hyprlock.conf
+ # Apply colors
+ # sed -i "s/{{ SWWW_WALL }}/${wallpath_png}/g" "$CACHE_DIR"/user/generated/hypr/hyprlock.conf
+ for i in "${!colorlist[@]}"; do
+ sed -i "s/{{ ${colorlist[$i]} }}/${colorvalues[$i]#\#}/g" "$CACHE_DIR"/user/generated/hypr/hyprlock.conf
+ done
+
+ cp "$CACHE_DIR"/user/generated/hypr/hyprlock.conf "$XDG_CONFIG_HOME"/hypr/hyprlock.conf
+}
+
+apply_lightdark() {
+ lightdark=$(get_light_dark)
+ if [ "$lightdark" = "light" ]; then
+ gsettings set org.gnome.desktop.interface color-scheme 'prefer-light'
+ else
+ gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'
+ fi
+}
+
+apply_gtk() { # Using gradience-cli
+ usegradience=$(sed -n '4p' "$STATE_DIR/user/colormode.txt")
+ if [[ "$usegradience" = "nogradience" ]]; then
+ rm "$XDG_CONFIG_HOME/gtk-3.0/gtk.css"
+ rm "$XDG_CONFIG_HOME/gtk-4.0/gtk.css"
+ return
+ fi
+
+ # Copy template
+ mkdir -p "$CACHE_DIR"/user/generated/gradience
+ cp "scripts/templates/gradience/preset.json" "$CACHE_DIR"/user/generated/gradience/preset.json
+
+ # Apply colors
+ for i in "${!colorlist[@]}"; do
+ sed -i "s/{{ ${colorlist[$i]} }}/${colorvalues[$i]}/g" "$CACHE_DIR"/user/generated/gradience/preset.json
+ done
+
+ mkdir -p "$XDG_CONFIG_HOME/presets" # create gradience presets folder
+ gradience-cli apply -p "$CACHE_DIR"/user/generated/gradience/preset.json --gtk both
+
+ # And set GTK theme manually as Gradience defaults to light adw-gtk3
+ # (which is unreadable when broken when you use dark mode)
+ lightdark=$(get_light_dark)
+ if [ "$lightdark" = "light" ]; then
+ gsettings set org.gnome.desktop.interface gtk-theme 'adw-gtk3'
+ else
+ gsettings set org.gnome.desktop.interface gtk-theme adw-gtk3-dark
+ fi
+}
+
+apply_ags() {
+ ags run-js "handleStyles(false);"
+ ags run-js 'openColorScheme.value = true; Utils.timeout(2000, () => openColorScheme.value = false);'
+}
+
+
+colornames=$(cat $STATE_DIR/scss/_material.scss | cut -d: -f1)
+colorstrings=$(cat $STATE_DIR/scss/_material.scss | cut -d: -f2 | cut -d ' ' -f2 | cut -d ";" -f1)
+IFS=$'\n'
+colorlist=( $colornames ) # Array of color names
+colorvalues=( $colorstrings ) # Array of color values
+
+apply_ags &
+apply_hyprland &
+apply_hyprlock &
+apply_lightdark &
+apply_gtk &
+apply_fuzzel &
+apply_term &
diff --git a/modules/home-manager/ags/ags/scripts/color_generation/colorgen.sh b/modules/home-manager/ags/ags/scripts/color_generation/colorgen.sh
new file mode 100755
index 0000000..9a19d8e
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/color_generation/colorgen.sh
@@ -0,0 +1,94 @@
+#!/usr/bin/env bash
+
+XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
+XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
+XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}"
+CONFIG_DIR="$XDG_CONFIG_HOME/ags"
+CACHE_DIR="$XDG_CACHE_HOME/ags"
+STATE_DIR="$XDG_STATE_HOME/ags"
+
+# check if no arguments
+if [ $# -eq 0 ]; then
+ echo "Usage: colorgen.sh /path/to/image (--apply)"
+ exit 1
+fi
+
+# check if the file $STATE_DIR/user/colormode.txt exists. if not, create it. else, read it to $lightdark
+colormodefile="$STATE_DIR/user/colormode.txt"
+lightdark="dark"
+transparency="opaque"
+materialscheme="vibrant"
+terminalscheme="$XDG_CONFIG_HOME/ags/scripts/templates/terminal/scheme-base.json"
+
+if [ ! -f $colormodefile ]; then
+ echo "dark" > $colormodefile
+ echo "opaque" >> $colormodefile
+ echo "vibrant" >> $colormodefile
+elif [[ $(wc -l < $colormodefile) -ne 4 || $(wc -w < $colormodefile) -ne 4 ]]; then
+ echo "dark" > $colormodefile
+ echo "opaque" >> $colormodefile
+ echo "vibrant" >> $colormodefile
+ echo "yesgradience" >> $colormodefile
+else
+ lightdark=$(sed -n '1p' $colormodefile)
+ transparency=$(sed -n '2p' $colormodefile)
+ materialscheme=$(sed -n '3p' $colormodefile)
+ if [ "$materialscheme" = "monochrome" ]; then
+ terminalscheme="$XDG_CONFIG_HOME/ags/scripts/templates/terminal/scheme-monochrome.json"
+ fi
+fi
+backend="material" # color generator backend
+if [ ! -f "$STATE_DIR/user/colorbackend.txt" ]; then
+ echo "material" > "$STATE_DIR/user/colorbackend.txt"
+else
+ backend=$(cat "$STATE_DIR/user/colorbackend.txt") # either "" or "-l"
+fi
+
+cd "$CONFIG_DIR/scripts/" || exit
+if [[ "$1" = "#"* ]]; then # this is a color
+ color_generation/generate_colors_material.py --color "$1" \
+ --mode "$lightdark" --scheme "$materialscheme" --transparency "$transparency" \
+ --termscheme $terminalscheme --blend_bg_fg \
+ > "$CACHE_DIR"/user/generated/material_colors.scss
+ if [ "$2" = "--apply" ]; then
+ cp "$CACHE_DIR"/user/generated/material_colors.scss "$STATE_DIR/scss/_material.scss"
+ color_generation/applycolor.sh
+ fi
+elif [ "$backend" = "material" ]; then
+ smartflag=''
+ if [ "$3" = "--smart" ]; then
+ smartflag='--smart'
+ fi
+ color_generation/generate_colors_material.py --path "$1" \
+ --mode "$lightdark" --scheme "$materialscheme" --transparency "$transparency" \
+ --termscheme $terminalscheme --blend_bg_fg \
+ --cache "$STATE_DIR/user/color.txt" $smartflag \
+ > "$CACHE_DIR"/user/generated/material_colors.scss
+ if [ "$2" = "--apply" ]; then
+ cp "$CACHE_DIR"/user/generated/material_colors.scss "$STATE_DIR/scss/_material.scss"
+ color_generation/applycolor.sh
+ fi
+elif [ "$backend" = "pywal" ]; then
+ # clear and generate
+ wal -c
+ wal -i "$1" -n $lightdark -q
+ # copy scss
+ cp "$XDG_CACHE_HOME/wal/colors.scss" "$CACHE_DIR"/user/generated/material_colors.scss
+
+ cat color_generation/pywal_to_material.scss >> "$CACHE_DIR"/user/generated/material_colors.scss
+ if [ "$2" = "--apply" ]; then
+ sass -I "$STATE_DIR/scss" -I "$CONFIG_DIR/scss/fallback" "$CACHE_DIR"/user/generated/material_colors.scss "$CACHE_DIR"/user/generated/colors_classes.scss --style compressed
+ sed -i "s/ { color//g" "$CACHE_DIR"/user/generated/colors_classes.scss
+ sed -i "s/\./$/g" "$CACHE_DIR"/user/generated/colors_classes.scss
+ sed -i "s/}//g" "$CACHE_DIR"/user/generated/colors_classes.scss
+ if [ "$lightdark" = "-l" ]; then
+ printf "\n""\$darkmode: false;""\n" >> "$CACHE_DIR"/user/generated/colors_classes.scss
+ else
+ printf "\n""\$darkmode: true;""\n" >> "$CACHE_DIR"/user/generated/colors_classes.scss
+ fi
+
+ cp "$CACHE_DIR"/user/generated/colors_classes.scss "$STATE_DIR/scss/_material.scss"
+
+ color_generation/applycolor.sh
+ fi
+fi
diff --git a/modules/home-manager/ags/ags/scripts/color_generation/generate_colors_material.py b/modules/home-manager/ags/ags/scripts/color_generation/generate_colors_material.py
new file mode 100755
index 0000000..714e959
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/color_generation/generate_colors_material.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python3
+import argparse
+import math
+import json
+from PIL import Image
+from materialyoucolor.quantize import QuantizeCelebi
+from materialyoucolor.score.score import Score
+from materialyoucolor.hct import Hct
+from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
+from materialyoucolor.utils.color_utils import (rgba_from_argb, argb_from_rgb, argb_from_rgba)
+from materialyoucolor.utils.math_utils import (sanitize_degrees_double, difference_degrees, rotation_direction)
+
+parser = argparse.ArgumentParser(description='Color generation script')
+parser.add_argument('--path', type=str, default=None, help='generate colorscheme from image')
+parser.add_argument('--size', type=int , default=128 , help='bitmap image size')
+parser.add_argument('--color', type=str, default=None, help='generate colorscheme from color')
+parser.add_argument('--mode', type=str, choices=['dark', 'light'], default='dark', help='dark or light mode')
+parser.add_argument('--scheme', type=str, default='vibrant', help='material scheme to use')
+parser.add_argument('--smart', action='store_true', default=False, help='decide scheme type based on image color')
+parser.add_argument('--transparency', type=str, choices=['opaque', 'transparent'], default='opaque', help='enable transparency')
+parser.add_argument('--termscheme', type=str, default=None, help='JSON file containg the terminal scheme for generating term colors')
+parser.add_argument('--harmony', type=float , default=0.8, help='(0-1) Color hue shift towards accent')
+parser.add_argument('--harmonize_threshold', type=float , default=100, help='(0-180) Max threshold angle to limit color hue shift')
+parser.add_argument('--term_fg_boost', type=float , default=0.35, help='Make terminal foreground more different from the background')
+parser.add_argument('--blend_bg_fg', action='store_true', default=False, help='Shift terminal background or foreground towards accent')
+parser.add_argument('--cache', type=str, default=None, help='file path to store the generated color')
+parser.add_argument('--debug', action='store_true', default=False, help='debug mode')
+args = parser.parse_args()
+
+rgba_to_hex = lambda rgba: "#{:02X}{:02X}{:02X}".format(rgba[0], rgba[1], rgba[2])
+argb_to_hex = lambda argb: "#{:02X}{:02X}{:02X}".format(*map(round, rgba_from_argb(argb)))
+hex_to_argb = lambda hex_code: argb_from_rgb(int(hex_code[1:3], 16), int(hex_code[3:5], 16), int(hex_code[5:], 16))
+display_color = lambda rgba : "\x1B[38;2;{};{};{}m{}\x1B[0m".format(rgba[0], rgba[1], rgba[2], "\x1b[7m \x1b[7m")
+
+def calculate_optimal_size (width: int, height: int, bitmap_size: int) -> (int, int):
+ image_area = width * height;
+ bitmap_area = bitmap_size ** 2
+ scale = math.sqrt(bitmap_area/image_area) if image_area > bitmap_area else 1
+ new_width = round(width * scale)
+ new_height = round(height * scale)
+ if new_width == 0:
+ new_width = 1
+ if new_height == 0:
+ new_height = 1
+ return new_width, new_height
+
+def harmonize (design_color: int, source_color: int, threshold: float = 35, harmony: float = 0.5) -> int:
+ from_hct = Hct.from_int(design_color)
+ to_hct = Hct.from_int(source_color)
+ difference_degrees_ = difference_degrees(from_hct.hue, to_hct.hue)
+ rotation_degrees = min(difference_degrees_ * harmony, threshold)
+ output_hue = sanitize_degrees_double(
+ from_hct.hue + rotation_degrees * rotation_direction(from_hct.hue, to_hct.hue)
+ )
+ return Hct.from_hct(output_hue, from_hct.chroma, from_hct.tone).to_int()
+
+def boost_chroma_tone (argb: int, chroma: float = 1, tone: float = 1) -> int:
+ hct = Hct.from_int(argb)
+ return Hct.from_hct(hct.hue, hct.chroma * chroma, hct.tone * tone).to_int()
+
+darkmode = (args.mode == 'dark')
+transparent = (args.transparency == 'transparent')
+
+if args.path is not None:
+ image = Image.open(args.path)
+
+ if image.format == "GIF":
+ image.seek(1)
+
+ wsize, hsize = image.size
+ wsize_new, hsize_new = calculate_optimal_size(wsize, hsize, args.size)
+ if wsize_new < wsize or hsize_new < hsize:
+ image = image.resize((wsize_new, hsize_new), Image.Resampling.BICUBIC)
+ colors = QuantizeCelebi(list(image.getdata()), 128)
+ argb = Score.score(colors)[0]
+
+ if args.cache is not None:
+ with open(args.cache, 'w') as file:
+ file.write(argb_to_hex(argb))
+ hct = Hct.from_int(argb)
+ if(args.smart):
+ if(hct.chroma < 20):
+ args.scheme = 'neutral'
+ if(hct.tone > 60):
+ darkmode = False
+elif args.color is not None:
+ argb = hex_to_argb(args.color)
+ hct = Hct.from_int(argb)
+
+if args.scheme == 'fruitsalad':
+ from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme
+elif args.scheme == 'expressive':
+ from materialyoucolor.scheme.scheme_expressive import SchemeExpressive as Scheme
+elif args.scheme == 'monochrome':
+ from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome as Scheme
+elif args.scheme == 'rainbow':
+ from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow as Scheme
+elif args.scheme == 'tonalspot':
+ from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot as Scheme
+elif args.scheme == 'neutral':
+ from materialyoucolor.scheme.scheme_neutral import SchemeNeutral as Scheme
+elif args.scheme == 'fidelity':
+ from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity as Scheme
+elif args.scheme == 'content':
+ from materialyoucolor.scheme.scheme_content import SchemeContent as Scheme
+elif args.scheme == 'vibrant':
+ from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant as Scheme
+else:
+ from schemes.scheme_morevibrant import SchemeMoreVibrant as Scheme
+
+# Generate
+scheme = Scheme(hct, darkmode, 0.0)
+
+material_colors = {}
+term_colors = {}
+
+for color in vars(MaterialDynamicColors).keys():
+ color_name = getattr(MaterialDynamicColors, color)
+ if hasattr(color_name, "get_hct"):
+ rgba = color_name.get_hct(scheme).to_rgba()
+ material_colors[color] = rgba_to_hex(rgba)
+
+# Extended material
+if darkmode == True:
+ material_colors['success'] = '#B5CCBA'
+ material_colors['onSuccess'] = '#213528'
+ material_colors['successContainer'] = '#374B3E'
+ material_colors['onSuccessContainer'] = '#D1E9D6'
+else:
+ material_colors['success'] = '#4F6354'
+ material_colors['onSuccess'] = '#FFFFFF'
+ material_colors['successContainer'] = '#D1E8D5'
+ material_colors['onSuccessContainer'] = '#0C1F13'
+
+# Terminal Colors
+if args.termscheme is not None:
+ with open(args.termscheme, 'r') as f:
+ json_termscheme = f.read()
+ term_source_colors = json.loads(json_termscheme)['dark' if darkmode else 'light']
+
+ primary_color_argb = hex_to_argb(material_colors['primary_paletteKeyColor'])
+ for color, val in term_source_colors.items():
+ if(args.scheme == 'monochrome') :
+ term_colors[color] = val
+ continue
+ if args.blend_bg_fg and color == "term0":
+ harmonized = boost_chroma_tone(hex_to_argb(material_colors['surfaceContainerLow']), 1.2, 0.95)
+ elif args.blend_bg_fg and color == "term15":
+ harmonized = boost_chroma_tone(hex_to_argb(material_colors['onSurface']), 3, 1)
+ else:
+ harmonized = harmonize(hex_to_argb(val), primary_color_argb, args.harmonize_threshold, args.harmony)
+ harmonized = boost_chroma_tone(harmonized, 1, 1 + (args.term_fg_boost * (1 if darkmode else -1)))
+ term_colors[color] = argb_to_hex(harmonized)
+
+if args.debug == False:
+ print(f"$darkmode: {darkmode};")
+ print(f"$transparent: {transparent};")
+ for color, code in material_colors.items():
+ print(f"${color}: {code};")
+ for color, code in term_colors.items():
+ print(f"${color}: {code};")
+else:
+ if args.path is not None:
+ print('\n--------------Image properties-----------------')
+ print(f"Image size: {wsize} x {hsize}")
+ print(f"Resized image: {wsize_new} x {hsize_new}")
+ print('\n---------------Selected color------------------')
+ print(f"Dark mode: {darkmode}")
+ print(f"Scheme: {args.scheme}")
+ print(f"Accent color: {display_color(rgba_from_argb(argb))} {argb_to_hex(argb)}")
+ print(f"HCT: {hct.hue:.2f} {hct.chroma:.2f} {hct.tone:.2f}")
+ print('\n---------------Material colors-----------------')
+ for color, code in material_colors.items():
+ rgba = rgba_from_argb(hex_to_argb(code))
+ print(f"{color.ljust(32)} : {display_color(rgba)} {code}")
+ print('\n----------Harmonize terminal colors------------')
+ for color, code in term_colors.items():
+ rgba = rgba_from_argb(hex_to_argb(code))
+ code_source = term_source_colors[color]
+ rgba_source = rgba_from_argb(hex_to_argb(code_source))
+ print(f"{color.ljust(6)} : {display_color(rgba_source)} {code_source} --> {display_color(rgba)} {code}")
+ print('-----------------------------------------------')
diff --git a/modules/home-manager/ags/ags/scripts/color_generation/pywal_to_material.scss b/modules/home-manager/ags/ags/scripts/color_generation/pywal_to_material.scss
new file mode 100644
index 0000000..ff049b2
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/color_generation/pywal_to_material.scss
@@ -0,0 +1,57 @@
+$primary: lighten($color4, 20%);
+$onPrimary: darken($color2, 20%);
+$primaryContainer: darken($color2, 10%);
+$onPrimaryContainer: lighten($color4, 10%);
+$secondary: desaturate(lighten($color5, 20%), 20%);
+$onSecondary: desaturate(darken($color3, 20%), 20%);
+$secondaryContainer: desaturate(darken($color3, 20%), 20%);
+$onSecondaryContainer: desaturate(lighten($color5, 20%), 20%);
+$tertiary: adjust-hue(lighten($color4, 20%), 30deg);
+$onTertiary: adjust-hue(darken($color2, 20%), 30deg);
+$tertiaryContainer: adjust-hue(darken($color2, 10%), 30deg);
+$tertiaryContainer: adjust-hue(lighten($color4, 10%), 30deg);
+$error: #ffb4a9;
+$onError: #680003;
+$errorContainer: #930006;
+$onErrorContainer: #ffb4a9;
+$colorbarbg: $color0;
+$background: $color0;
+$onBackground: $color7;
+$surface: $color0;
+$onSurface: $color7;
+$surfaceVariant: $color1;
+$onSurfaceVariant: $color7;
+$outline: $color7;
+$shadow: #000000;
+$inverseSurface: invert($surface);
+$inverseOnSurface: invert($onSurface);
+$inversePrimary: invert($primary);
+
+.primary { color: $primary; }
+.onPrimary { color: $onPrimary; }
+.primaryContainer { color: $primaryContainer; }
+.onPrimaryContainer { color: $onPrimaryContainer; }
+.secondary { color: $secondary; }
+.onSecondary { color: $onSecondary; }
+.secondaryContainer { color: $secondaryContainer; }
+.onSecondaryContainer { color: $onSecondaryContainer; }
+.tertiary { color: $tertiary; }
+.onTertiary { color: $onTertiary; }
+.tertiaryContainer { color: $tertiaryContainer; }
+.onTertiaryContainer { color: $tertiaryContainer; }
+.error { color: $error; }
+.onError { color: $onError; }
+.errorContainer { color: $errorContainer; }
+.onErrorContainer { color: $onErrorContainer; }
+.colorbarbg { color: $colorbarbg; }
+.background { color: $background; }
+.onBackground { color: $onBackground; }
+.surface { color: $surface; }
+.onSurface { color: $onSurface; }
+.surfaceVariant { color: $surfaceVariant; }
+.onSurfaceVariant { color: $onSurfaceVariant; }
+.outline { color: $outline; }
+.shadow { color: $shadow; }
+.inverseSurface { color: $inverseSurface; }
+.inverseOnSurface { color: $inverseOnSurface; }
+.inversePrimary { color: $inversePrimary; }
diff --git a/modules/home-manager/ags/ags/scripts/color_generation/randomwall.sh b/modules/home-manager/ags/ags/scripts/color_generation/randomwall.sh
new file mode 100755
index 0000000..f2b18b3
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/color_generation/randomwall.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
+CONFIG_DIR="$XDG_CONFIG_HOME/ags"
+$CONFIG_DIR/scripts/color_generation/switchwall.sh "$(fd . $(xdg-user-dir PICTURES)/wallpapers/ -e .png -e .jpg -e .svg | xargs shuf -n1 -e)"
diff --git a/modules/home-manager/ags/ags/scripts/color_generation/schemes/scheme_morevibrant.py b/modules/home-manager/ags/ags/scripts/color_generation/schemes/scheme_morevibrant.py
new file mode 100644
index 0000000..d67f783
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/color_generation/schemes/scheme_morevibrant.py
@@ -0,0 +1,44 @@
+from materialyoucolor.scheme.dynamic_scheme import DynamicSchemeOptions, DynamicScheme
+from materialyoucolor.scheme.variant import Variant
+from materialyoucolor.palettes.tonal_palette import TonalPalette
+
+
+class SchemeMoreVibrant(DynamicScheme):
+ hues = [0.0, 41.0, 61.0, 101.0, 131.0, 181.0, 251.0, 301.0, 360.0]
+ secondary_rotations = [18.0, 15.0, 10.0, 12.0, 15.0, 18.0, 15.0, 12.0, 12.0]
+ tertiary_rotations = [35.0, 30.0, 20.0, 25.0, 30.0, 35.0, 30.0, 25.0, 25.0]
+
+ def __init__(self, source_color_hct, is_dark, contrast_level):
+ super().__init__(
+ DynamicSchemeOptions(
+ source_color_argb=source_color_hct.to_int(),
+ variant=Variant.VIBRANT,
+ contrast_level=contrast_level,
+ is_dark=is_dark,
+ primary_palette=TonalPalette.from_hue_and_chroma(
+ source_color_hct.hue, 200.0
+ ),
+ secondary_palette=TonalPalette.from_hue_and_chroma(
+ DynamicScheme.get_rotated_hue(
+ source_color_hct,
+ SchemeMoreVibrant.hues,
+ SchemeMoreVibrant.secondary_rotations,
+ ),
+ 32.0,
+ ),
+ tertiary_palette=TonalPalette.from_hue_and_chroma(
+ DynamicScheme.get_rotated_hue(
+ source_color_hct,
+ SchemeMoreVibrant.hues,
+ SchemeMoreVibrant.tertiary_rotations,
+ ),
+ 32.0,
+ ),
+ neutral_palette=TonalPalette.from_hue_and_chroma(
+ source_color_hct.hue, 13.0
+ ),
+ neutral_variant_palette=TonalPalette.from_hue_and_chroma(
+ source_color_hct.hue, 15.0
+ ),
+ )
+ )
diff --git a/modules/home-manager/ags/ags/scripts/color_generation/specials/_material_badapple-l.scss b/modules/home-manager/ags/ags/scripts/color_generation/specials/_material_badapple-l.scss
new file mode 100644
index 0000000..12c047b
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/color_generation/specials/_material_badapple-l.scss
@@ -0,0 +1,29 @@
+$darkmode: false;
+$primary: #000000;
+$onPrimary: #FFFFFF ;
+$primaryContainer: #d4d4d4;
+$onPrimaryContainer: #000000;
+$secondary: #000000;
+$onSecondary: #FFFFFF ;
+$secondaryContainer: #bebebe;
+$onSecondaryContainer: #000000;
+$tertiary: #000000;
+$onTertiary: #FFFFFF ;
+$tertiaryContainer: #FFFFFF ;
+$onTertiaryContainer: #000000;
+$error: #000000;
+$onError: #FFFFFF ;
+$errorContainer: #FFFFFF ;
+$onErrorContainer: #000000;
+$colorbarbg: #FFFFFF ;
+$background: #FFFFFF ;
+$onBackground: #000000;
+$surface: #f0f0f0;
+$onSurface: #000000;
+$surfaceVariant: #dddddd;
+$onSurfaceVariant: #000000;
+$outline: #525252;
+$shadow: #000000 ;
+$inverseSurface: #000000;
+$inverseOnSurface: #FFFFFF;
+$inversePrimary: #000000;
diff --git a/modules/home-manager/ags/ags/scripts/color_generation/specials/_material_badapple.scss b/modules/home-manager/ags/ags/scripts/color_generation/specials/_material_badapple.scss
new file mode 100644
index 0000000..24dd73f
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/color_generation/specials/_material_badapple.scss
@@ -0,0 +1,29 @@
+$darkmode: true;
+$primary: #e2e2e2;
+$onPrimary: #000000;
+$primaryContainer: #6b6b6b;
+$onPrimaryContainer: #e2e2e2;
+$secondary: #e2e2e2;
+$onSecondary: #000000;
+$secondaryContainer: #313131;
+$onSecondaryContainer: #e2e2e2;
+$tertiary: #e2e2e2;
+$onTertiary: #000000;
+$tertiaryContainer: #000000;
+$onTertiaryContainer: #e2e2e2;
+$error: #e2e2e2;
+$onError: #000000;
+$errorContainer: #000000;
+$onErrorContainer: #e2e2e2;
+$colorbarbg: #000000;
+$background: #000000;
+$onBackground: #e2e2e2;
+$surface: #161616;
+$onSurface: #e2e2e2;
+$surfaceVariant: #242424;
+$onSurfaceVariant: #e2e2e2;
+$outline: #a1a1a1;
+$shadow: #000000;
+$inverseSurface: #e2e2e2;
+$inverseOnSurface: #000000;
+$inversePrimary: #e2e2e2;
diff --git a/modules/home-manager/ags/ags/scripts/color_generation/switchcolor.sh b/modules/home-manager/ags/ags/scripts/color_generation/switchcolor.sh
new file mode 100755
index 0000000..183efc7
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/color_generation/switchcolor.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+
+XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
+XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
+XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}"
+CONFIG_DIR="$XDG_CONFIG_HOME/ags"
+CACHE_DIR="$XDG_CACHE_HOME/ags"
+STATE_DIR="$XDG_STATE_HOME/ags"
+
+COLORMODE_FILE_DIR="$STATE_DIR/user/colormode.txt"
+
+if [ "$1" == "--pick" ]; then
+ color=$(hyprpicker --no-fancy)
+elif [[ "$1" = "#"* ]]; then # this is a color
+ color=$1
+else
+ color=$(cut -f1 "$STATE_DIR/user/color.txt")
+fi
+
+sed -i "1s/.*/$color/" "$STATE_DIR/user/color.txt"
+
+# Use Gradience?
+colormodelines=$(wc -l "$COLORMODE_FILE_DIR" | awk '{print $1}' )
+if [ "$2" == "--no-gradience" ]; then
+ if [ "$colormodelines" == "3" ]; then
+ echo 'nogradience' >> "$COLORMODE_FILE_DIR"
+ else
+ sed -i "4s/.*/nogradience/" "$COLORMODE_FILE_DIR"
+ fi
+elif [ "$2" == "--yes-gradience" ]; then
+ if [ "$colormodelines" == "3" ]; then
+ echo 'yesgradience' >> "$COLORMODE_FILE_DIR"
+ else
+ sed -i "4s/.*/yesgradience/" "$COLORMODE_FILE_DIR"
+ fi
+fi
+
+# Generate colors for ags n stuff
+"$CONFIG_DIR"/scripts/color_generation/colorgen.sh "${color}" --apply
diff --git a/modules/home-manager/ags/ags/scripts/color_generation/switchwall.sh b/modules/home-manager/ags/ags/scripts/color_generation/switchwall.sh
new file mode 100755
index 0000000..64413c6
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/color_generation/switchwall.sh
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+
+XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
+CONFIG_DIR="$XDG_CONFIG_HOME/ags"
+CURRENT="$HOME/Pictures/wallpapers/videos/current"
+
+switch() {
+ if [ "$1" == '' ]; then
+ echo 'Aborted'
+ exit 0
+ elif [[ "$1" =~ \.mp4$ ]]; then
+ pkill mpvpaper
+ mpvpaper DP-1 -f -o "loop panscan=1.0" "$1"
+ ln -s "$1" "$CURRENT"
+ imgpath=${1%.*}.jpg
+ else
+ pkill mpvpaper
+ if [ -f "$CURRENT" ]; then
+ rm $CURRENT
+ fi
+ imgpath=$1
+ fi
+ read scale screenx screeny screensizey < <(hyprctl monitors -j | jq '.[] | select(.focused) | .scale, .x, .y, .height' | xargs)
+ cursorposx=$(hyprctl cursorpos -j | jq '.x' 2>/dev/null) || cursorposx=960
+ cursorposx=$(bc <<< "scale=0; ($cursorposx - $screenx) * $scale / 1")
+ cursorposy=$(hyprctl cursorpos -j | jq '.y' 2>/dev/null) || cursorposy=540
+ cursorposy=$(bc <<< "scale=0; ($cursorposy - $screeny) * $scale / 1")
+ cursorposy_inverted=$((screensizey - cursorposy))
+
+ # ags run-js "wallpaper.set('')"
+ # sleep 0.1 && ags run-js "wallpaper.set('${imgpath}')" &
+ swww img "$imgpath" --transition-step 100 --transition-fps 120 \
+ --transition-type grow --transition-angle 30 --transition-duration 1 \
+ --transition-pos "$cursorposx, $cursorposy_inverted"
+}
+
+if [ "$1" == "--noswitch" ]; then
+ imgpath=$(swww query | awk -F 'image: ' '{print $2}')
+ # imgpath=$(ags run-js 'wallpaper.get(0)')
+elif [[ "$1" ]]; then
+ switch "$1"
+else
+ # Select and set image (hyprland)
+
+ cd "$(xdg-user-dir PICTURES)" || return 1
+ switch "$(yad --width 1200 --height 800 --file --add-preview --large-preview --title='Choose wallpaper')"
+fi
+
+# Generate colors for ags n stuff
+"$CONFIG_DIR"/scripts/color_generation/colorgen.sh "${imgpath}" --apply --smart
diff --git a/modules/home-manager/ags/ags/scripts/grimblast.sh b/modules/home-manager/ags/ags/scripts/grimblast.sh
new file mode 100755
index 0000000..a5602f2
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/grimblast.sh
@@ -0,0 +1,277 @@
+#!/usr/bin/env bash
+## Grimblast: a helper for screenshots within hyprland
+## Requirements:
+## - `grim`: screenshot utility for wayland
+## - `slurp`: to select an area
+## - `hyprctl`: to read properties of current window (provided by Hyprland)
+## - `hyprpicker`: to freeze the screen when selecting area
+## - `wl-copy`: clipboard utility (provided by wl-clipboard)
+## - `jq`: json utility to parse hyprctl output
+## - `notify-send`: to show notifications (provided by libnotify)
+## Those are needed to be installed, if unsure, run `grimblast check`
+##
+## See `man 1 grimblast` or `grimblast usage` for further details.
+
+## Author: Misterio (https://github.com/misterio77)
+
+## This tool is based on grimshot, with swaymsg commands replaced by their
+## hyprctl equivalents.
+## https://github.com/swaywm/sway/blob/master/contrib/grimshot
+getTargetDirectory() {
+ test -f "${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs" &&
+ . "${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs"
+
+ echo "${XDG_SCREENSHOTS_DIR:-${XDG_PICTURES_DIR:-$HOME}}"
+}
+
+tmp_editor_directory() {
+ echo "/tmp"
+}
+
+#Detect if $GRIMBLAST_EDITOR env exist
+env_editor_confirm() {
+ if [ -n "$GRIMBLAST_EDITOR" ]; then
+ echo "GRIMBLAST_EDITOR is set. Continuing..."
+ else
+ echo "GRIMBLAST_EDITOR is not set. Defaulting to gimp"
+ GRIMBLAST_EDITOR=gimp
+ fi
+}
+
+NOTIFY=no
+CURSOR=
+FREEZE=
+WAIT=no
+SCALE=
+HYPRPICKER_PID=-1
+
+while [ $# -gt 0 ]; do
+ key="$1"
+
+ case $key in
+ -n | --notify)
+ NOTIFY=yes
+ shift # past argument
+ ;;
+ -c | --cursor)
+ CURSOR=yes
+ shift # past argument
+ ;;
+ -f | --freeze)
+ FREEZE=yes
+ shift # past argument
+ ;;
+ -w | --wait)
+ shift
+ WAIT=$1
+ if echo "$WAIT" | grep "[^0-9]" -q; then
+ echo "Invalid value for wait '$WAIT'" >&2
+ exit 3
+ fi
+ shift
+ ;;
+ -s | --scale)
+ shift # past argument
+ if [ $# -gt 0 ]; then
+ SCALE="$1" # assign the next argument to SCALE
+ shift # past argument
+ else
+ echo "Error: Missing argument for --scale option."
+ exit 1
+ fi
+ ;;
+ *) # unknown option
+ break # done with parsing --flags
+ ;;
+ esac
+done
+
+ACTION=${1:-usage}
+SUBJECT=${2:-screen}
+FILE=${3:-$(getTargetDirectory)/$(date -Ins).png}
+FILE_EDITOR=${3:-$(tmp_editor_directory)/$(date -Ins).png}
+
+if [ "$ACTION" != "save" ] && [ "$ACTION" != "copy" ] && [ "$ACTION" != "edit" ] && [ "$ACTION" != "copysave" ] && [ "$ACTION" != "check" ]; then
+ echo "Usage:"
+ echo " grimblast [--notify] [--cursor] [--freeze] [--wait N] [--scale ] (copy|save|copysave|edit) [active|screen|output|area] [FILE|-]"
+ echo " grimblast check"
+ echo " grimblast usage"
+ echo ""
+ echo "Commands:"
+ echo " copy: Copy the screenshot data into the clipboard."
+ echo " save: Save the screenshot to a regular file or '-' to pipe to STDOUT."
+ echo " copysave: Combine the previous 2 options."
+ echo " edit: Open screenshot in the image editor of your choice (default is gimp). See man page for info."
+ echo " check: Verify if required tools are installed and exit."
+ echo " usage: Show this message and exit."
+ echo ""
+ echo "Targets:"
+ echo " active: Currently active window."
+ echo " screen: All visible outputs."
+ echo " output: Currently active output."
+ echo " area: Manually select a region or window."
+ exit
+fi
+
+notify() {
+ notify-send -t 3000 -a grimblast "$@"
+}
+
+notifyOk() {
+ [ "$NOTIFY" = "no" ] && return
+
+ notify "$@"
+}
+
+notifyError() {
+ if [ $NOTIFY = "yes" ]; then
+ TITLE=${2:-"Screenshot"}
+ MESSAGE=${1:-"Error taking screenshot with grim"}
+ notify -u critical "$TITLE" "$MESSAGE"
+ else
+ echo "$1"
+ fi
+}
+
+resetFade() {
+ if [[ -n $FADE && -n $FADEOUT ]]; then
+ hyprctl keyword animation "$FADE" >/dev/null
+ hyprctl keyword animation "$FADEOUT" >/dev/null
+ fi
+}
+
+killHyprpicker() {
+ if [ ! $HYPRPICKER_PID -eq -1 ]; then
+ kill $HYPRPICKER_PID
+ fi
+}
+
+die() {
+ killHyprpicker
+ MSG=${1:-Bye}
+ notifyError "Error: $MSG"
+ exit 2
+}
+
+check() {
+ COMMAND=$1
+ if command -v "$COMMAND" >/dev/null 2>&1; then
+ RESULT="OK"
+ else
+ RESULT="NOT FOUND"
+ fi
+ echo " $COMMAND: $RESULT"
+}
+
+takeScreenshot() {
+ FILE=$1
+ GEOM=$2
+ OUTPUT=$3
+ if [ -n "$OUTPUT" ]; then
+ grim ${CURSOR:+-c} ${SCALE:+-s "$SCALE"} -o "$OUTPUT" "$FILE" || die "Unable to invoke grim"
+ elif [ -z "$GEOM" ]; then
+ grim ${CURSOR:+-c} ${SCALE:+-s "$SCALE"} "$FILE" || die "Unable to invoke grim"
+ else
+ grim ${CURSOR:+-c} ${SCALE:+-s "$SCALE"} -g "$GEOM" "$FILE" || die "Unable to invoke grim"
+ resetFade
+ fi
+}
+
+wait() {
+ if [ "$WAIT" != "no" ]; then
+ sleep "$WAIT"
+ fi
+}
+
+if [ "$ACTION" = "check" ]; then
+ echo "Checking if required tools are installed. If something is missing, install it to your system and make it available in PATH..."
+ check grim
+ check slurp
+ check hyprctl
+ check hyprpicker
+ check wl-copy
+ check jq
+ check notify-send
+ exit
+elif [ "$SUBJECT" = "active" ]; then
+ wait
+ FOCUSED=$(hyprctl activewindow -j)
+ GEOM=$(echo "$FOCUSED" | jq -r '"\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"')
+ APP_ID=$(echo "$FOCUSED" | jq -r '.class')
+ WHAT="$APP_ID window"
+elif [ "$SUBJECT" = "screen" ]; then
+ wait
+ GEOM=""
+ WHAT="Screen"
+elif [ "$SUBJECT" = "output" ]; then
+ wait
+ GEOM=""
+ OUTPUT=$(hyprctl monitors -j | jq -r '.[] | select(.focused == true)' | jq -r '.name')
+ WHAT="$OUTPUT"
+elif [ "$SUBJECT" = "area" ]; then
+ if [ "$FREEZE" = "yes" ] && [ "$(command -v "hyprpicker")" ] >/dev/null 2>&1; then
+ hyprpicker -r -z &
+ sleep 0.2
+ HYPRPICKER_PID=$!
+ fi
+
+ # get fade & fadeOut animation and unset it
+ # this removes the black border seen around screenshots
+ FADE="$(hyprctl -j animations | jq -jr '.[0][] | select(.name == "fade") | .name, ",", (if .enabled == true then "1" else "0" end), ",", (.speed|floor), ",", .bezier')"
+ FADEOUT="$(hyprctl -j animations | jq -jr '.[0][] | select(.name == "fadeOut") | .name, ",", (if .enabled == true then "1" else "0" end), ",", (.speed|floor), ",", .bezier')"
+ hyprctl keyword animation 'fade,0,1,default' >/dev/null
+ hyprctl keyword animation 'fadeOut,0,1,default' >/dev/null
+
+ WORKSPACES="$(hyprctl monitors -j | jq -r 'map(.activeWorkspace.id)')"
+ WINDOWS="$(hyprctl clients -j | jq -r --argjson workspaces "$WORKSPACES" 'map(select([.workspace.id] | inside($workspaces)))')"
+ # shellcheck disable=2086 # if we don't split, spaces mess up slurp
+ GEOM=$(echo "$WINDOWS" | jq -r '.[] | "\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"' | slurp $SLURP_ARGS)
+
+ # Check if user exited slurp without selecting the area
+ if [ -z "$GEOM" ]; then
+ killHyprpicker
+ resetFade
+ exit 1
+ fi
+ WHAT="Area"
+ wait
+elif [ "$SUBJECT" = "window" ]; then
+ die "Subject 'window' is now included in 'area'"
+else
+ die "Unknown subject to take a screen shot from" "$SUBJECT"
+fi
+
+if [ "$ACTION" = "copy" ]; then
+ takeScreenshot - "$GEOM" "$OUTPUT" | wl-copy --type image/png || die "Clipboard error"
+ notifyOk "$WHAT copied to buffer"
+elif [ "$ACTION" = "save" ]; then
+ if takeScreenshot "$FILE" "$GEOM" "$OUTPUT"; then
+ TITLE="Screenshot of $SUBJECT"
+ MESSAGE=$(basename "$FILE")
+ notifyOk "$TITLE" "$MESSAGE" -i "$FILE"
+ echo "$FILE"
+ else
+ notifyError "Error taking screenshot with grim"
+ fi
+elif [ "$ACTION" = "edit" ]; then
+ env_editor_confirm
+ if takeScreenshot "$FILE_EDITOR" "$GEOM" "$OUTPUT"; then
+ TITLE="Screenshot of $SUBJECT"
+ MESSAGE="Open screenshot in image editor"
+ notifyOk "$TITLE" "$MESSAGE" -i "$FILE_EDITOR"
+ $GRIMBLAST_EDITOR "$FILE_EDITOR"
+ echo "$FILE_EDITOR"
+ else
+ notifyError "Error taking screenshot"
+ fi
+else
+ if [ "$ACTION" = "copysave" ]; then
+ takeScreenshot - "$GEOM" "$OUTPUT" | tee "$FILE" | wl-copy --type image/png || die "Clipboard error"
+ notifyOk "$WHAT copied to buffer and saved to $FILE" -i "$FILE"
+ echo "$FILE"
+ else
+ notifyError "Error taking screenshot with grim"
+ fi
+fi
+
+killHyprpicker
diff --git a/modules/home-manager/ags/ags/scripts/hyprland/get_keybinds.py b/modules/home-manager/ags/ags/scripts/hyprland/get_keybinds.py
new file mode 100755
index 0000000..9b784df
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/hyprland/get_keybinds.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+import argparse
+import re
+import os
+from os.path import expandvars as os_expandvars
+from typing import Dict, List
+
+TITLE_REGEX = "#+!"
+HIDE_COMMENT = "[hidden]"
+MOD_SEPARATORS = ['+', ' ']
+COMMENT_BIND_PATTERN = "#/#"
+
+parser = argparse.ArgumentParser(description='Hyprland keybind reader')
+parser.add_argument('--path', type=str, default="$HOME/.config/hypr/hyprland.conf", help='path to keybind file (sourcing isn\'t supported)')
+args = parser.parse_args()
+content_lines = []
+reading_line = 0
+
+# Little Parser made for hyprland keybindings conf file
+Variables: Dict[str, str] = {}
+
+
+class KeyBinding(dict):
+ def __init__(self, mods, key, dispatcher, params, comment) -> None:
+ self["mods"] = mods
+ self["key"] = key
+ self["dispatcher"] = dispatcher
+ self["params"] = params
+ self["comment"] = comment
+
+class Section(dict):
+ def __init__(self, children, keybinds, name) -> None:
+ self["children"] = children
+ self["keybinds"] = keybinds
+ self["name"] = name
+
+
+def read_content(path: str) -> str:
+ if (not os.access(os.path.expanduser(os.path.expandvars(path)), os.R_OK)):
+ return ("error")
+ with open(os.path.expanduser(os.path.expandvars(path)), "r") as file:
+ return file.read()
+
+
+def autogenerate_comment(dispatcher: str, params: str = "") -> str:
+ match dispatcher:
+
+ case "resizewindow":
+ return "Resize window"
+
+ case "movewindow":
+ if(params == ""):
+ return "Move window"
+ else:
+ return "Window: move in {} direction".format({
+ "l": "left",
+ "r": "right",
+ "u": "up",
+ "d": "down",
+ }.get(params, "null"))
+
+ case "pin":
+ return "Window: pin (show on all workspaces)"
+
+ case "splitratio":
+ return "Window split ratio {}".format(params)
+
+ case "togglefloating":
+ return "Float/unfloat window"
+
+ case "resizeactive":
+ return "Resize window by {}".format(params)
+
+ case "killactive":
+ return "Close window"
+
+ case "fullscreen":
+ return "Toggle {}".format(
+ {
+ "0": "fullscreen",
+ "1": "maximization",
+ "2": "fullscreen on Hyprland's side",
+ }.get(params, "null")
+ )
+
+ case "fakefullscreen":
+ return "Toggle fake fullscreen"
+
+ case "workspace":
+ if params == "+1":
+ return "Workspace: focus right"
+ elif params == "-1":
+ return "Workspace: focus left"
+ return "Focus workspace {}".format(params)
+
+ case "movefocus":
+ return "Window: move focus {}".format(
+ {
+ "l": "left",
+ "r": "right",
+ "u": "up",
+ "d": "down",
+ }.get(params, "null")
+ )
+
+ case "swapwindow":
+ return "Window: swap in {} direction".format(
+ {
+ "l": "left",
+ "r": "right",
+ "u": "up",
+ "d": "down",
+ }.get(params, "null")
+ )
+
+ case "movetoworkspace":
+ if params == "+1":
+ return "Window: move to right workspace (non-silent)"
+ elif params == "-1":
+ return "Window: move to left workspace (non-silent)"
+ return "Window: move to workspace {} (non-silent)".format(params)
+
+ case "movetoworkspacesilent":
+ if params == "+1":
+ return "Window: move to right workspace"
+ elif params == "-1":
+ return "Window: move to right workspace"
+ return "Window: move to workspace {}".format(params)
+
+ case "togglespecialworkspace":
+ return "Workspace: toggle special"
+
+ case "exec":
+ return "Execute: {}".format(params)
+
+ case _:
+ return ""
+
+def get_keybind_at_line(line_number, line_start = 0):
+ global content_lines
+ line = content_lines[line_number]
+ _, keys = line.split("=", 1)
+ keys, *comment = keys.split("#", 1)
+
+ mods, key, dispatcher, *params = list(map(str.strip, keys.split(",", 4)))
+ params = "".join(map(str.strip, params))
+
+ # Remove empty spaces
+ comment = list(map(str.strip, comment))
+ # Add comment if it exists, else generate it
+ if comment:
+ comment = comment[0]
+ if comment.startswith("[hidden]"):
+ return None
+ else:
+ comment = autogenerate_comment(dispatcher, params)
+
+ if mods:
+ modstring = mods + MOD_SEPARATORS[0] # Add separator at end to ensure last mod is read
+ mods = []
+ p = 0
+ for index, char in enumerate(modstring):
+ if(char in MOD_SEPARATORS):
+ if(index - p > 1):
+ mods.append(modstring[p:index])
+ p = index+1
+ else:
+ mods = []
+
+ return KeyBinding(mods, key, dispatcher, params, comment)
+
+def get_binds_recursive(current_content, scope):
+ global content_lines
+ global reading_line
+ # print("get_binds_recursive({0}, {1}) [@L{2}]".format(current_content, scope, reading_line + 1))
+ while reading_line < len(content_lines): # TODO: Adjust condition
+ line = content_lines[reading_line]
+ heading_search_result = re.search(TITLE_REGEX, line)
+ # print("Read line {0}: {1}\tisHeading: {2}".format(reading_line + 1, content_lines[reading_line], "[{0}, {1}, {2}]".format(heading_search_result.start(), heading_search_result.start() == 0, ((heading_search_result != None) and (heading_search_result.start() == 0))) if heading_search_result != None else "No"))
+ if ((heading_search_result != None) and (heading_search_result.start() == 0)): # Found title
+ # Determine scope
+ heading_scope = line.find('!')
+ # Lower? Return
+ if(heading_scope <= scope):
+ reading_line -= 1
+ return current_content
+
+ section_name = line[(heading_scope+1):].strip()
+ # print("[[ Found h{0} at line {1} ]] {2}".format(heading_scope, reading_line+1, content_lines[reading_line]))
+ reading_line += 1
+ current_content["children"].append(get_binds_recursive(Section([], [], section_name), heading_scope))
+
+ elif line.startswith(COMMENT_BIND_PATTERN):
+ keybind = get_keybind_at_line(reading_line, line_start=len(COMMENT_BIND_PATTERN))
+ if(keybind != None):
+ current_content["keybinds"].append(keybind)
+
+ elif line == "" or not line.lstrip().startswith("bind"): # Comment, ignore
+ pass
+
+ else: # Normal keybind
+ keybind = get_keybind_at_line(reading_line)
+ if(keybind != None):
+ current_content["keybinds"].append(keybind)
+
+ reading_line += 1
+
+ return current_content;
+
+def parse_keys(path: str) -> Dict[str, List[KeyBinding]]:
+ global content_lines
+ content_lines = read_content(path).splitlines()
+ if content_lines[0] == "error":
+ return "error"
+ return get_binds_recursive(Section([], [], ""), 0)
+
+
+if __name__ == "__main__":
+ import json
+
+ ParsedKeys = parse_keys(args.path)
+ print(json.dumps(ParsedKeys))
diff --git a/modules/home-manager/ags/ags/scripts/hyprland/workspace_action.sh b/modules/home-manager/ags/ags/scripts/hyprland/workspace_action.sh
new file mode 100755
index 0000000..a43badf
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/hyprland/workspace_action.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+hyprctl dispatch "$1" $(((($(hyprctl activeworkspace -j | jq -r .id) - 1) / 10) * 10 + $2))
diff --git a/modules/home-manager/ags/ags/scripts/quickscripts/nixos-trim-generations.sh b/modules/home-manager/ags/ags/scripts/quickscripts/nixos-trim-generations.sh
new file mode 100755
index 0000000..98b59be
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/quickscripts/nixos-trim-generations.sh
@@ -0,0 +1,243 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+## Defaults
+keepGensDef=30; keepDaysDef=30
+keepGens=$keepGensDef; keepDays=$keepDaysDef
+
+## Usage
+usage () {
+ printf "Usage:\n\t ./trim-generations.sh \n\n
+(defaults are: Keep-Gens=$keepGensDef Keep-Days=$keepDaysDef Profile=user)\n\n"
+ printf "If you enter any parameters, you must enter all three, or none to use defaults.\n"
+ printf "Example:\n\t trim-generations.sh 15 10 home-manager\n"
+ printf " this will work on the home-manager profile and keep all generations from the\n"
+ printf "last 10 days, and keep at least 15 generations no matter how old.\n"
+ printf "\nProfiles available are:\tuser, home-manager, channels, system (root)\n"
+ printf "\n-h or --help prints this help text."
+}
+
+if [ $# -eq 1 ]; then # if help requested
+ if [ $1 = "-h" ]; then
+ usage
+ exit 1;
+ fi
+ if [ $1 = "--help" ]; then
+ usage
+ exit 2;
+ fi
+ printf "Dont recognise your option exiting..\n\n"
+ usage
+ exit 3;
+
+ elif [ $# -eq 0 ]; then # print the defaults
+ printf "The current defaults are:\n Keep-Gens=$keepGensDef Keep-Days=$keepDaysDef \n\n"
+ read -p "Keep these defaults? (y/n):" answer
+
+ case "$answer" in
+ [yY1] )
+ printf "Using defaults..\n"
+ ;;
+ [nN0] ) printf "ok, doing nothing, exiting..\n"
+ exit 6;
+ ;;
+ * ) printf "%b" "Doing nothing, exiting.."
+ exit 7;
+ ;;
+ esac
+fi
+
+## Handle parameters (and change if root)
+if [[ $EUID -ne 0 ]]; then # if not root
+ profile=$(readlink /home/$USER/.nix-profile)
+else
+ if [ -d /nix/var/nix/profiles/system ]; then # maybe this or the other
+ profile="/nix/var/nix/profiles/system"
+ elif [ -d /nix/var/nix/profiles/default ]; then
+ profile="/nix/var/nix/profiles/default"
+ else
+ echo "Cant find profile for root. Exiting"
+ exit 8
+ fi
+fi
+if (( $# < 1 )); then
+ printf "Keeping default: $keepGensDef generations OR $keepDaysDef days, whichever is more\n"
+elif [[ $# -le 2 ]]; then
+ printf "\nError: Not enough arguments.\n\n" >&2
+ usage
+ exit 1
+elif (( $# > 4)); then
+ printf "\nError: Too many arguments.\n\n" >&2
+ usage
+ exit 2
+else
+ if [ $1 -lt 1 ]; then
+ printf "using Gen numbers less than 1 not recommended. Setting to min=1\n"
+ read -p "is that ok? (y/n): " asnwer
+ #printf "$asnwer"
+ case "$asnwer" in
+ [yY1] )
+ printf "ok, continuing..\n"
+ ;;
+ [nN0] )
+ printf "ok, doing nothing, exiting..\n"
+ exit 6;
+ ;;
+ * )
+ printf "%b" "Doing nothing, exiting.."
+ exit 7;
+ ;;
+ esac
+ fi
+ if [ $2 -lt 0 ]; then
+ printf "using negative days number not recommended. Setting to min=0\n"
+ read -p "is that ok? (y/n): " asnwer
+
+ case "$asnwer" in
+ [yY1] )
+ printf "ok, continuing..\n"
+ ;;
+ [nN0] )
+ printf "ok, doing nothing, exiting..\n"
+ exit 6;
+ ;;
+ * )
+ printf "%b" "Doing nothing, exiting.."
+ exit 7;
+ ;;
+ esac
+ fi
+ keepGens=$1; keepDays=$2;
+ (( keepGens < 1 )) && keepGens=1
+ (( keepDays < 0 )) && keepDays=0
+ if [[ $EUID -ne 0 ]]; then
+ if [[ $3 == "user" ]] || [[ $3 == "default" ]]; then
+ profile=$(readlink /home/$USER/.nix-profile)
+ elif [[ $3 == "home-manager" ]]; then
+ # home-manager defaults to $XDG_STATE_HOME; otherwise, use
+ # `home-manager generations` and `nix-store --query --roots
+ # /nix/store/...` to figure out what reference is keeping the old
+ # generations alive.
+ profile="${XDG_STATE_HOME:-$HOME/.local/state}/nix/profiles/home-manager"
+ elif [[ $3 == "channels" ]]; then
+ profile="/nix/var/nix/profiles/per-user/$USER/channels"
+ else
+ printf "\nError: Do not understand your third argument. Should be one of: (user / home-manager/ channels)\n\n"
+ usage
+ exit 3
+ fi
+ else
+ if [[ $3 == "system" ]]; then
+ profile="/nix/var/nix/profiles/system"
+ elif [[ $3 == "user" ]] || [[ $3 == "default" ]]; then
+ profile="/nix/var/nix/profiles/default"
+ else
+ printf "\nError: Do not understand your third argument. Should be one of: (user / system)\n\n"
+ usage
+ exit 3
+ fi
+ fi
+ printf "OK! \t Keep Gens = $keepGens \t Keep Days = $keepDays\n\n"
+fi
+
+printf "Operating on profile: \t $profile\n\n"
+
+## Runs at the end, to decide whether to delete profiles that match chosen parameters.
+choose () {
+ local default="$1"
+ local prompt="$2"
+ local answer
+
+ read -p "$prompt" answer
+ [ -z "$answer" ] && answer="$default"
+
+ case "$answer" in
+ [yY1] ) #printf "answered yes!\n"
+ nix-env --delete-generations -p $profile ${!gens[@]}
+ exit 0
+ ;;
+ [nN0] ) printf "Ok doing nothing exiting..\n"
+ exit 6;
+ ;;
+ * ) printf "%b" "Unexpected answer '$answer'!" >&2
+ exit 7;
+ ;;
+ esac
+} # end of function choose
+
+# printf "profile = $profile\n\n"
+## Query nix-env for generations list
+IFS=$'\n' nixGens=( $(nix-env --list-generations -p $profile | sed 's:^\s*::; s:\s*$::' | tr '\t' ' ' | tr -s ' ') )
+timeNow=$(date +%s)
+
+## Get info on oldest generation
+IFS=' ' read -r -a oldestGenArr <<< "${nixGens[0]}"
+oldestGen=${oldestGenArr[0]}
+oldestDate=${oldestGenArr[1]}
+printf "%-30s %s\n" "oldest generation:" $oldestGen
+#oldestDate=${nixGens[0]:3:19}
+printf "%-30s %s\n" "oldest generation created:" $oldestDate
+oldestTime=$(date -d "$oldestDate" +%s)
+oldestElapsedSecs=$((timeNow-oldestTime))
+oldestElapsedMins=$((oldestElapsedSecs/60))
+oldestElapsedHours=$((oldestElapsedMins/60))
+oldestElapsedDays=$((oldestElapsedHours/24))
+printf "%-30s %s\n" "minutes before now:" $oldestElapsedMins
+printf "%-30s %s\n" "hours before now:" $oldestElapsedHours
+printf "%-30s %s\n\n" "days before now:" $oldestElapsedDays
+
+## Get info on current generation
+for i in "${nixGens[@]}"; do
+ IFS=' ' read -r -a iGenArr <<< "$i"
+ genNumber=${iGenArr[0]}
+ genDate=${iGenArr[1]}
+ if [[ "$i" =~ current ]]; then
+ currentGen=$genNumber
+ printf "%-30s %s\n" "current generation:" $currentGen
+ currentDate=$genDate
+ printf "%-30s %s\n" "current generation created:" $currentDate
+ currentTime=$(date -d "$currentDate" +%s)
+ currentElapsedSecs=$((timeNow-currentTime))
+ currentElapsedMins=$((currentElapsedSecs/60))
+ currentElapsedHours=$((currentElapsedMins/60))
+ currentElapsedDays=$((currentElapsedHours/24))
+ printf "%-30s %s\n" "minutes before now:" $currentElapsedMins
+ printf "%-30s %s\n" "hours before now:" $currentElapsedHours
+ printf "%-30s %s\n\n" "days before now:" $currentElapsedDays
+ fi
+done
+
+## Compare oldest and current generations
+timeBetweenOldestAndCurrent=$((currentTime-oldestTime))
+elapsedDays=$((timeBetweenOldestAndCurrent/60/60/24))
+generationsDiff=$((currentGen-oldestGen))
+
+## Figure out what we should do, based on generations and options
+if [[ elapsedDays -le keepDays ]]; then
+ printf "All generations are no more than $keepDays days older than current generation. \nOldest gen days difference from current gen: $elapsedDays \n\n\tNothing to do!\n"
+ exit 4;
+elif [[ generationsDiff -lt keepGens ]]; then
+ printf "Oldest generation ($oldestGen) is only $generationsDiff generations behind current ($currentGen). \n\n\t Nothing to do!\n"
+ exit 5;
+else
+ printf "\tSomething to do...\n"
+ declare -a gens
+ for i in "${nixGens[@]}"; do
+ IFS=' ' read -r -a iGenArr <<< "$i"
+ genNumber=${iGenArr[0]}
+ genDiff=$((currentGen-genNumber))
+ genDate=${iGenArr[1]}
+ genTime=$(date -d "$genDate" +%s)
+ elapsedSecs=$((timeNow-genTime))
+ genDaysOld=$((elapsedSecs/60/60/24))
+ if [[ genDaysOld -gt keepDays ]] && [[ genDiff -ge keepGens ]]; then
+ gens["$genNumber"]="$genDate, $genDaysOld day(s) old"
+ fi
+ done
+ printf "\nFound the following generation(s) to delete:\n"
+ for K in "${!gens[@]}"; do
+ printf "generation $K \t ${gens[$K]}\n"
+ done
+ printf "\n"
+ choose "y" "Do you want to delete these? [Y/n]: "
+fi
diff --git a/modules/home-manager/ags/ags/scripts/record-script.sh b/modules/home-manager/ags/ags/scripts/record-script.sh
new file mode 100755
index 0000000..9fc7706
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/record-script.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+
+getdate() {
+ date '+%Y-%m-%d_%H.%M.%S'
+}
+getaudiooutput() {
+ pactl list sources | grep 'Name' | grep 'monitor' | cut -d ' ' -f2
+}
+getactivemonitor() {
+ hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .name'
+}
+
+mkdir -p "$(xdg-user-dir VIDEOS)"
+cd "$(xdg-user-dir VIDEOS)" || exit
+if pgrep wf-recorder > /dev/null; then
+ notify-send -t 1000 "Recording Stopped" "Stopped" -a 'record-script.sh' &
+ pkill wf-recorder &
+else
+ notify-send -t 750 "Starting recording" 'recording_'"$(getdate)"'.mp4' -a 'record-script.sh'
+ if [[ "$1" == "--sound" ]]; then
+ wf-recorder --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --geometry "$(slurp)" --audio="$(getaudiooutput)" & disown
+ elif [[ "$1" == "--fullscreen-sound" ]]; then
+ wf-recorder -o $(getactivemonitor) --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --audio="$(getaudiooutput)" & disown
+ elif [[ "$1" == "--fullscreen" ]]; then
+ wf-recorder -o $(getactivemonitor) --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t & disown
+ else
+ wf-recorder --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --geometry "$(slurp)" & disown
+ fi
+fi
diff --git a/modules/home-manager/ags/ags/scripts/templates/fuzzel/fuzzel.ini b/modules/home-manager/ags/ags/scripts/templates/fuzzel/fuzzel.ini
new file mode 100644
index 0000000..47f6abc
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/templates/fuzzel/fuzzel.ini
@@ -0,0 +1,21 @@
+font=Gabarito
+terminal=foot -e
+prompt=">> "
+layer=overlay
+
+[colors]
+background={{ $background }}ff
+text={{ $onBackground }}ff
+selection={{ $surfaceVariant }}ff
+selection-text={{ $onSurfaceVariant }}ff
+border={{ $surfaceVariant }}dd
+match={{ $primary }}ff
+selection-match={{ $primary }}ff
+
+
+[border]
+radius=17
+width=1
+
+[dmenu]
+exit-immediately-if-empty=yes
diff --git a/modules/home-manager/ags/ags/scripts/templates/gradience/preset.json b/modules/home-manager/ags/ags/scripts/templates/gradience/preset.json
new file mode 100644
index 0000000..aeb5748
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/templates/gradience/preset.json
@@ -0,0 +1,144 @@
+{
+ "name": "Material3_Generated",
+ "variables": {
+ "theme_fg_color": "#AEE5FA",
+ "theme_text_color": "#AEE5FA",
+ "theme_bg_color": "#1a1b26",
+ "theme_base_color": "#1a1b26",
+ "theme_selected_bg_color": "#AEE5FA",
+ "theme_selected_fg_color": "rgba(0, 0, 0, 0.87)",
+ "insensitive_bg_color": "#1a1b26",
+ "insensitive_fg_color": "rgba(192, 202, 245, 0.5)",
+ "insensitive_base_color": "#24283b",
+ "theme_unfocused_fg_color": "#AEE5FA",
+ "theme_unfocused_text_color": "#c0caf5",
+ "theme_unfocused_bg_color": "#1a1b26",
+ "theme_unfocused_base_color": "#1a1b26",
+ "theme_unfocused_selected_bg_color": "#a9b1d6",
+ "theme_unfocused_selected_fg_color": "rgba(0, 0, 0, 0.87)",
+ "unfocused_insensitive_color": "rgba(192, 202, 245, 0.5)",
+ "borders": "rgba(192, 202, 245, 0.12)",
+ "unfocused_borders": "rgba(192, 202, 245, 0.12)",
+ "warning_color": "#FDD633",
+ "error_color": "#BA1B1B",
+ "success_color": "#81C995",
+ "wm_title": "#AEE5FA",
+ "wm_unfocused_title": "rgba(192, 202, 245, 0.7)",
+ "wm_highlight": "rgba(192, 202, 245, 0.1)",
+ "wm_bg": "#1a1b26",
+ "wm_unfocused_bg": "#1a1b26",
+ "wm_button_close_icon": "#1a1b26",
+ "wm_button_close_hover_bg": "#a9b1d6",
+ "wm_button_close_active_bg": "#c7c7c7",
+ "content_view_bg": "#1a1b26",
+ "placeholder_text_color": "silver",
+ "text_view_bg": "#1d1d1d",
+ "budgie_tasklist_indicator_color": "#90D1F6",
+ "budgie_tasklist_indicator_color_active": "#90D1F6",
+ "budgie_tasklist_indicator_color_active_window": "#999999",
+ "budgie_tasklist_indicator_color_attention": "#FDD633",
+ "STRAWBERRY_100": "#FF9262",
+ "STRAWBERRY_300": "#FF793E",
+ "STRAWBERRY_500": "#F15D22",
+ "STRAWBERRY_700": "#CF3B00",
+ "STRAWBERRY_900": "#AC1800",
+ "ORANGE_100": "#FFDB91",
+ "ORANGE_300": "#FFCA40",
+ "ORANGE_500": "#FAA41A",
+ "ORANGE_700": "#DE8800",
+ "ORANGE_900": "#C26C00",
+ "BANANA_100": "#FFFFA8",
+ "BANANA_300": "#FFFA7D",
+ "BANANA_500": "#FFCE51",
+ "BANANA_700": "#D1A023",
+ "BANANA_900": "#A27100",
+ "LIME_100": "#A2F3BE",
+ "LIME_300": "#8ADBA6",
+ "LIME_500": "#73C48F",
+ "LIME_700": "#479863",
+ "LIME_900": "#1C6D38",
+ "BLUEBERRY_100": "#94A6FF",
+ "BLUEBERRY_300": "#6A7CE0",
+ "BLUEBERRY_500": "#3F51B5",
+ "BLUEBERRY_700": "#213397",
+ "BLUEBERRY_900": "#031579",
+ "GRAPE_100": "#D25DE6",
+ "GRAPE_300": "#B84ACB",
+ "GRAPE_500": "#9C27B0",
+ "GRAPE_700": "#830E97",
+ "GRAPE_900": "#6A007E",
+ "COCOA_100": "#9F9792",
+ "COCOA_300": "#7B736E",
+ "COCOA_500": "#574F4A",
+ "COCOA_700": "#463E39",
+ "COCOA_900": "#342C27",
+ "SILVER_100": "#EEE",
+ "SILVER_300": "#CCC",
+ "SILVER_500": "#AAA",
+ "SILVER_700": "#888",
+ "SILVER_900": "#666",
+ "SLATE_100": "#888",
+ "SLATE_300": "#666",
+ "SLATE_500": "#444",
+ "SLATE_700": "#222",
+ "SLATE_900": "#111",
+ "BLACK_100": "#474341",
+ "BLACK_300": "#403C3A",
+ "BLACK_500": "#393634",
+ "BLACK_700": "#33302F",
+ "BLACK_900": "#2B2928",
+ "accent_bg_color": "{{ $primary }}",
+ "accent_fg_color": "{{ $onPrimary }}",
+ "accent_color": "{{ $primary }}",
+ "destructive_bg_color": "{{ $error }}",
+ "destructive_fg_color": "{{ $onError }}",
+ "destructive_color": "{{ $error }}",
+ "success_bg_color": "#81C995",
+ "success_fg_color": "rgba(0, 0, 0, 0.87)",
+ "warning_bg_color": "#FDD633",
+ "warning_fg_color": "rgba(0, 0, 0, 0.87)",
+ "error_bg_color": "{{ $error }}",
+ "error_fg_color": "{{ $onError }}",
+ "window_bg_color": "{{ $background }}",
+ "window_fg_color": "{{ $onBackground }}",
+ "view_bg_color": "{{ $surface }}",
+ "view_fg_color": "{{ $onSurface }}",
+ "headerbar_bg_color": "mix(@dialog_bg_color, @window_bg_color, 0.5)",
+ "headerbar_fg_color": "{{ $onSecondaryContainer }}",
+ "headerbar_border_color": "{{ $secondaryContainer }}",
+ "headerbar_backdrop_color": "@headerbar_bg_color",
+ "headerbar_shade_color": "rgba(0, 0, 0, 0.09)",
+ "card_bg_color": "{{ $background }}",
+ "card_fg_color": "{{ $onSecondaryContainer }}",
+ "card_shade_color": "rgba(0, 0, 0, 0.09)",
+ "dialog_bg_color": "{{ $secondaryContainer }}",
+ "dialog_fg_color": "{{ $onSecondaryContainer }}",
+ "popover_bg_color": "{{ $secondaryContainer }}",
+ "popover_fg_color": "{{ $onSecondaryContainer }}",
+ "thumbnail_bg_color": "#1a1b26",
+ "thumbnail_fg_color": "#AEE5FA",
+ "shade_color": "rgba(0, 0, 0, 0.36)",
+ "scrollbar_outline_color": "rgba(0, 0, 0, 0.5)",
+
+ "sidebar_bg_color": "@window_bg_color",
+ "sidebar_fg_color":"@window_fg_color",
+ "sidebar_border_color": "@sidebar_bg_color",
+ "sidebar_backdrop_color": "@sidebar_bg_color"
+ },
+ "palette": {
+ "blue_": {},
+ "green_": {},
+ "yellow_": {},
+ "orange_": {},
+ "red_": {},
+ "purple_": {},
+ "brown_": {},
+ "light_": {},
+ "dark_": {}
+ },
+ "custom_css": {
+ "gtk4": "",
+ "gtk3": ""
+ },
+ "plugins": {}
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/scripts/templates/hypr/hyprland/colors.conf b/modules/home-manager/ags/ags/scripts/templates/hypr/hyprland/colors.conf
new file mode 100644
index 0000000..8c0689f
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/templates/hypr/hyprland/colors.conf
@@ -0,0 +1,34 @@
+# exec = export SLURP_ARGS='-d -c {{ $onSecondaryContainer }}BB -b {{ $secondaryContainer }}44 -s 00000000'
+
+general {
+ col.active_border = rgba({{ $onSurface }}39)
+ col.inactive_border = rgba({{ $outline }}30)
+}
+
+misc {
+ background_color = rgba({{ $surface }}FF)
+}
+
+plugin {
+ hyprbars {
+ # Honestly idk if it works like css, but well, why not
+ bar_text_font = Rubik, Geist, AR One Sans, Reddit Sans, Inter, Roboto, Ubuntu, Noto Sans, sans-serif
+ bar_height = 30
+ bar_padding = 10
+ bar_button_padding = 5
+ bar_precedence_over_border = true
+ bar_part_of_window = true
+
+ bar_color = rgba({{ $background }}FF)
+ col.text = rgba({{ $onBackground }}FF)
+
+
+ # example buttons (R -> L)
+ # hyprbars-button = color, size, on-click
+ hyprbars-button = rgb({{ $onBackground }}), 13, , hyprctl dispatch killactive
+ hyprbars-button = rgb({{ $onBackground }}), 13, , hyprctl dispatch fullscreen 1
+ hyprbars-button = rgb({{ $onBackground }}), 13, , hyprctl dispatch movetoworkspacesilent special
+ }
+}
+
+windowrulev2 = bordercolor rgba({{ $primary }}AA) rgba({{ $primary }}77),pinned:1
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/scripts/templates/hypr/hyprlock.conf b/modules/home-manager/ags/ags/scripts/templates/hypr/hyprlock.conf
new file mode 100644
index 0000000..d3de5d8
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/templates/hypr/hyprlock.conf
@@ -0,0 +1,102 @@
+# $text_color = rgba({{ $onBackground }}FF)
+# $entry_background_color = rgba({{ $background }}11)
+# $entry_border_color = rgba({{ $outline }}55)
+# $entry_color = rgba({{ $onSurfaceVariant }}FF)
+$text_color = rgba(FFFFFFFF)
+$entry_background_color = rgba(33333311)
+$entry_border_color = rgba(3B3B3B55)
+$entry_color = rgba(FFFFFFFF)
+$font_family = Rubik Light
+$font_family_clock = Rubik Light
+$font_material_symbols = Material Symbols Rounded
+
+background {
+ # color = rgba({{ $surfaceContainerLowest }}FF)
+ color = rgba(000000FF)
+ # path = {{ SWWW_WALL }}
+ # path = screenshot
+ # blur_size = 5
+ # blur_passes = 4
+}
+input-field {
+ monitor =
+ size = 250, 50
+ outline_thickness = 2
+ dots_size = 0.1
+ dots_spacing = 0.3
+ outer_color = $entry_border_color
+ inner_color = $entry_background_color
+ font_color = $entry_color
+ # fade_on_empty = true
+
+ position = 0, 20
+ halign = center
+ valign = center
+}
+
+label { # Clock
+ monitor =
+ text = $TIME
+ shadow_passes = 1
+ shadow_boost = 0.5
+ color = $text_color
+ font_size = 65
+ font_family = $font_family_clock
+
+ position = 0, 300
+ halign = center
+ valign = center
+}
+label { # Greeting
+ monitor =
+ text = hi $USER !!!
+ shadow_passes = 1
+ shadow_boost = 0.5
+ color = $text_color
+ font_size = 20
+ font_family = $font_family
+
+ position = 0, 240
+ halign = center
+ valign = center
+}
+label { # lock icon
+ monitor =
+ text = lock
+ shadow_passes = 1
+ shadow_boost = 0.5
+ color = $text_color
+ font_size = 21
+ font_family = $font_material_symbols
+
+ position = 0, 65
+ halign = center
+ valign = bottom
+}
+label { # "locked" text
+ monitor =
+ text = locked
+ shadow_passes = 1
+ shadow_boost = 0.5
+ color = $text_color
+ font_size = 14
+ font_family = $font_family
+
+ position = 0, 45
+ halign = center
+ valign = bottom
+}
+
+label { # Status
+ monitor =
+ text = cmd[update:5000] ${XDG_CONFIG_HOME:-$HOME/.config}/hypr/hyprlock/status.sh
+ shadow_passes = 1
+ shadow_boost = 0.5
+ color = $text_color
+ font_size = 14
+ font_family = $font_family
+
+ position = 30, -30
+ halign = left
+ valign = top
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/scripts/templates/terminal/scheme-base.json b/modules/home-manager/ags/ags/scripts/templates/terminal/scheme-base.json
new file mode 100644
index 0000000..e4b78e7
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/templates/terminal/scheme-base.json
@@ -0,0 +1,38 @@
+{
+ "dark": {
+ "term0" : "#282828",
+ "term1" : "#CC241D",
+ "term2" : "#98971A",
+ "term3" : "#D79921",
+ "term4" : "#458588",
+ "term5" : "#B16286",
+ "term6" : "#689D6A",
+ "term7" : "#A89984",
+ "term8" : "#928374",
+ "term9" : "#FB4934",
+ "term10" : "#B8BB26",
+ "term11" : "#FABD2F",
+ "term12" : "#83A598",
+ "term13" : "#D3869B",
+ "term14" : "#8EC07C",
+ "term15" : "#EBDBB2"
+ },
+ "light": {
+ "term0" : "#FDF9F3",
+ "term1" : "#FF6188",
+ "term2" : "#A9DC76",
+ "term3" : "#FC9867",
+ "term4" : "#FFD866",
+ "term5" : "#F47FD4",
+ "term6" : "#78DCE8",
+ "term7" : "#333034",
+ "term8" : "#121212",
+ "term9" : "#FF6188",
+ "term10" : "#A9DC76",
+ "term11" : "#FC9867",
+ "term12" : "#FFD866",
+ "term13" : "#F47FD4",
+ "term14" : "#78DCE8",
+ "term15" : "#333034"
+ }
+}
diff --git a/modules/home-manager/ags/ags/scripts/templates/terminal/scheme-monochrome.json b/modules/home-manager/ags/ags/scripts/templates/terminal/scheme-monochrome.json
new file mode 100644
index 0000000..5807a26
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/templates/terminal/scheme-monochrome.json
@@ -0,0 +1,36 @@
+{
+ "dark": {
+ "term0": "#000000",
+ "term1": "#FFFFFF",
+ "term2": "#CCCCCC",
+ "term3": "#8f8f8f",
+ "term4": "#FFFFFF",
+ "term5": "#111111",
+ "term6": "#CCCCCC",
+ "term7": "#FFFFFF",
+ "term8": "#404040",
+ "term9": "#CCCCCC",
+ "term10": "#FFFFFF",
+ "term11": "#909090",
+ "term12": "#CCCCCC",
+ "term13": "#808080",
+ "term14": "#CCCCCC",
+ "term15": "#FFFFFF"
+ },
+ "light": {
+ "term0": "#EAE9EA",
+ "term1": "#777777",
+ "term2": "#000000",
+ "term3": "#000000",
+ "term4": "#000000",
+ "term5": "#000000",
+ "term6": "#000000",
+ "term7": "#202020",
+ "term8": "#000000",
+ "term9": "#000000",
+ "term10": "#CCCCCC",
+ "term11": "#808080",
+ "term12": "#CCCCCC",
+ "term13": "#FFFFFF"
+ }
+}
diff --git a/modules/home-manager/ags/ags/scripts/templates/terminal/sequences.txt b/modules/home-manager/ags/ags/scripts/templates/terminal/sequences.txt
new file mode 100644
index 0000000..27fa997
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/templates/terminal/sequences.txt
@@ -0,0 +1 @@
+]4;0;#$term0 #\]4;1;#$term1 #\]4;2;#$term2 #\]4;3;#$term3 #\]4;4;#$term4 #\]4;5;#$term5 #\]4;6;#$term6 #\]4;7;#$term7 #\]4;8;#$term8 #\]4;9;#$term9 #\]4;10;#$term10 #\]4;11;#$term11 #\]4;12;#$term12 #\]4;13;#$term13 #\]4;14;#$term14 #\]4;15;#$term15 #\]10;#$term7 #\]11;[$alpha]#$term0 #\]12;#$term7 #\]13;#$term7 #\]17;#$term7 #\]19;#$term0 #\]4;232;#$term7 #\]4;256;#$term7 #\]708;[$alpha]#$term0 #\
diff --git a/modules/home-manager/ags/ags/scripts/wayland-idle-inhibitor.py b/modules/home-manager/ags/ags/scripts/wayland-idle-inhibitor.py
new file mode 100755
index 0000000..4a4f8f8
--- /dev/null
+++ b/modules/home-manager/ags/ags/scripts/wayland-idle-inhibitor.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+
+import sys
+from dataclasses import dataclass
+from signal import SIGINT, SIGTERM, signal
+from threading import Event
+import setproctitle
+
+from pywayland.client.display import Display
+from pywayland.protocol.idle_inhibit_unstable_v1.zwp_idle_inhibit_manager_v1 import (
+ ZwpIdleInhibitManagerV1,
+)
+from pywayland.protocol.wayland.wl_compositor import WlCompositor
+from pywayland.protocol.wayland.wl_registry import WlRegistryProxy
+from pywayland.protocol.wayland.wl_surface import WlSurface
+
+
+@dataclass
+class GlobalRegistry:
+ surface: WlSurface | None = None
+ inhibit_manager: ZwpIdleInhibitManagerV1 | None = None
+
+
+def handle_registry_global(
+ wl_registry: WlRegistryProxy, id_num: int, iface_name: str, version: int
+) -> None:
+ global_registry: GlobalRegistry = wl_registry.user_data or GlobalRegistry()
+
+ if iface_name == "wl_compositor":
+ compositor = wl_registry.bind(id_num, WlCompositor, version)
+ global_registry.surface = compositor.create_surface() # type: ignore
+ elif iface_name == "zwp_idle_inhibit_manager_v1":
+ global_registry.inhibit_manager = wl_registry.bind(
+ id_num, ZwpIdleInhibitManagerV1, version
+ )
+
+
+def main() -> None:
+ done = Event()
+ signal(SIGINT, lambda _, __: done.set())
+ signal(SIGTERM, lambda _, __: done.set())
+
+ global_registry = GlobalRegistry()
+
+ display = Display()
+ display.connect()
+
+ registry = display.get_registry() # type: ignore
+ registry.user_data = global_registry
+ registry.dispatcher["global"] = handle_registry_global
+
+ def shutdown() -> None:
+ display.dispatch()
+ display.roundtrip()
+ display.disconnect()
+
+ display.dispatch()
+ display.roundtrip()
+
+ if global_registry.surface is None or global_registry.inhibit_manager is None:
+ print("Wayland seems not to support idle_inhibit_unstable_v1 protocol.")
+ shutdown()
+ sys.exit(1)
+
+ inhibitor = global_registry.inhibit_manager.create_inhibitor( # type: ignore
+ global_registry.surface
+ )
+
+ display.dispatch()
+ display.roundtrip()
+
+ print("Inhibiting idle...")
+ done.wait()
+ print("Shutting down...")
+
+ inhibitor.destroy()
+
+ shutdown()
+
+
+if __name__ == "__main__":
+ setproctitle.setproctitle("wayland-idle-inhibitor.py")
+ main()
diff --git a/modules/home-manager/ags/ags/scss/_bar.scss b/modules/home-manager/ags/ags/scss/_bar.scss
new file mode 100644
index 0000000..37a5601
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_bar.scss
@@ -0,0 +1,415 @@
+// Made to be pixel-perfect with 11pt font size
+// 1rem = 11pt = 14.6666666667px
+$bar_ws_width: 1.774rem;
+$bar_ws_width_focus: 0.614rem;
+$bar_ws_width_focus_active: 2.045rem;
+
+@mixin bar-group-rounding {
+ @include small-rounding;
+}
+
+.bar-height {
+ min-height: 2.727rem;
+}
+
+.bar-bg {
+ background-color: $layer0;
+ min-height: 2.727rem;
+ margin-top: 5px;
+ margin-left: 5px;
+ margin-right: 5px;
+ @include small-rounding;
+}
+
+.bar-bg-focus {
+ background-color: $layer0;
+ min-height: 1.364rem;
+}
+
+.bar-bg-nothing {
+ background-color: $layer0;
+ min-height: 2px;
+}
+
+.bar-bg-focus-batterylow {
+ background-color: mix($layer0, $errorContainer, 80%);
+}
+
+.bar-sidespace {
+ min-width: 1.5rem;
+}
+
+.bar-group-margin {
+ padding: 0.273rem 0rem;
+}
+
+.bar-group {
+ background-color: $layer1;
+}
+
+.bar-group-pad {
+ padding: 0.205rem;
+}
+
+.bar-group-pad-less {
+ padding: 0rem 0.681rem;
+}
+
+.bar-group-pad-system {
+ padding: 0rem 0.341rem;
+}
+
+.bar-group-pad-music {
+ padding-right: 1.023rem;
+ padding-left: 0.341rem;
+}
+
+.bar-group-standalone {
+ @include bar-group-rounding;
+ -gtk-outline-radius: 1.364rem;
+}
+
+.bar-group-round {
+ border-radius: 10rem;
+ -gtk-outline-radius: 10rem;
+}
+
+.bar-group-middle {
+ border-radius: 0.477rem;
+ -gtk-outline-radius: 0.477rem;
+}
+
+.bar-group-left {
+ border-radius: 0.477rem;
+ -gtk-outline-radius: 0.477rem;
+ border-top-left-radius: 1.364rem;
+ border-bottom-left-radius: 1.364rem;
+}
+
+.bar-group-right {
+ border-radius: 0.477rem;
+ -gtk-outline-radius: 0.477rem;
+ border-top-right-radius: 1.364rem;
+ border-bottom-right-radius: 1.364rem;
+}
+
+.bar-sidemodule {
+ min-width: 26rem;
+}
+
+.bar-ws-width {
+ min-width: 18.341rem;
+}
+
+
+.bar-ws-container {
+ transition: 700ms cubic-bezier(0.1, 1, 0, 1);
+}
+
+.bar-ws {
+ font-size: 1.02rem;
+ font-weight: 600;
+ min-width: $bar_ws_width;
+ color: $workspaceOnLayer1Inactive;
+}
+
+.bar-ws-active {
+ background-color: $workspaceLayer3;
+ color: $workspaceOnLayer3;
+}
+
+.bar-ws-occupied {
+ background-color: $layer2;
+ color: $workspaceOnLayer2;
+}
+
+// Focus is the bar mode name, not the workspace state!
+
+.bar-ws-focus {
+ background-color: $surfaceVariant;
+ min-width: $bar_ws_width_focus;
+}
+
+.bar-ws-focus-active {
+ min-width: $bar_ws_width_focus_active;
+ background-color: $onLayer0;
+}
+
+.bar-ws-focus-occupied {
+ background-color: $secondaryContainer;
+}
+
+.bar-clock-box {
+ margin: 0rem 0.682rem;
+}
+
+.bar-time {
+ @include titlefont;
+ font-size: 1.2727rem;
+ color: $timeOnLayer1;
+}
+
+.bar-date {
+ color: $dateOnLayer1;
+}
+
+.bar-batt {
+ @include full-rounding;
+ @include element_decel;
+ min-height: 1.77rem;
+ min-width: 1.77rem;
+ border-radius: 10rem;
+ color: $battOnLayer2;
+}
+
+.bar-batt-txt {
+ color: $battOnLayer1;
+}
+
+.bar-batt-circprog {
+ @include fluent_decel_long;
+ min-width: 0.136rem; // line width
+ min-height: 1.636rem;
+ padding: 0rem;
+ background-color: $battLayer2;
+ color: $battOnLayer2;
+}
+
+.bar-batt-circprog-low {
+ background-color: $error;
+ color: $errorContainer;
+}
+
+
+.bar-batt-low {
+ background-color: $error;
+ color: $errorContainer;
+}
+
+.bar-ram-icon {
+ @include full-rounding;
+ @include element_decel;
+ min-height: 1.77rem;
+ min-width: 1.77rem;
+ border-radius: 10rem;
+ color: $ramOnLayer2;
+}
+
+.bar-ram-circprog {
+ @include fluent_decel_long;
+ min-width: 0.136rem; // line width
+ min-height: 1.636rem;
+ padding: 0rem;
+ background-color: $ramLayer2;
+ color: $ramOnLayer2;
+}
+
+.bar-ram-txt {
+ color: $ramOnLayer1;
+}
+
+.bar-swap-icon {
+ @include full-rounding;
+ @include element_decel;
+ min-height: 1.77rem;
+ min-width: 1.77rem;
+ border-radius: 10rem;
+ color: $swapOnLayer2;
+}
+
+.bar-swap-circprog {
+ @include fluent_decel_long;
+ min-width: 0.136rem; // line width
+ min-height: 1.636rem;
+ padding: 0rem;
+ background-color: $swapLayer2;
+ color: $swapOnLayer2;
+}
+
+.bar-swap-txt {
+ color: $swapOnLayer1;
+}
+
+.bar-cpu-icon {
+ @include full-rounding;
+ @include element_decel;
+ min-height: 1.77rem;
+ min-width: 1.77rem;
+ border-radius: 10rem;
+ color: $cpuOnLayer2;
+}
+
+.bar-cpu-circprog {
+ @include fluent_decel_long;
+ min-width: 0.136rem; // line width
+ min-height: 1.636rem;
+ padding: 0rem;
+ background-color: $cpuLayer2;
+ color: $cpuOnLayer2;
+}
+
+.bar-cpu-txt {
+ color: $cpuOnLayer1;
+}
+
+.bar-music-playstate {
+ @include element_decel;
+ min-height: 1.77rem;
+ min-width: 1.77rem;
+ border-radius: 10rem;
+ color: $musicOnLayer2;
+}
+
+.bar-music-circprog {
+ @include fluent_decel_long;
+ min-width: 0.136rem; // line width
+ min-height: 1.636rem;
+ padding: 0rem;
+ background-color: $musicLayer2;
+ color: $musicOnLayer2;
+}
+
+.bar-music-playstate-playing {
+ min-height: 1.77rem;
+ min-width: 1.77rem;
+ border-radius: 10rem;
+ color: $musicOnLayer2;
+}
+
+.bar-music-playstate-txt {
+ transition: 100ms cubic-bezier(0.05, 0.7, 0.1, 1);
+ @include icon-material;
+}
+
+.bar-music-txt {
+ color: $musicOnLayer1;
+}
+
+.bar-music-cover {
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 100% auto;
+ min-width: 11.932rem;
+}
+
+.bar-music-extended-bg {
+ border-radius: 1.364rem;
+ min-width: 34.091rem;
+}
+
+.bar-music-hide-false {
+ @include menu_decel;
+ transition-duration: 100ms;
+ opacity: 1;
+}
+
+.bar-music-hide-true {
+ @include menu_accel;
+ transition-duration: 100ms;
+ opacity: 0;
+}
+
+.bar-corner-spacing {
+ min-width: $rounding_large;
+ min-height: $rounding_large;
+}
+
+/*
+.corner {
+ background-color: $layer0;
+ @include large-rounding;
+}
+
+.corner-black {
+ background-color: $black; // Hard code: fake screen corner
+ @include large-rounding;
+}
+*/
+
+.bar-wintitle-topdesc {
+ margin-top: -0.136rem;
+ margin-bottom: -0.341rem;
+ color: $windowtitleOnLayer0Inactive;
+}
+
+.bar-wintitle-txt {
+ color: $windowtitleOnLayer0;
+}
+
+.bar-space-button {
+ padding: 0.341rem;
+}
+
+.bar-space-button>box:first-child {
+ @include full-rounding;
+ padding: 0rem 0.682rem;
+}
+
+.bar-space-button-leftmost {
+ box {
+ margin: 0rem 0.682rem;
+ }
+}
+
+.bar-space-area-rightmost>box {
+ padding-right: 2.386rem;
+}
+
+.bar-systray {
+ @include full-rounding;
+ margin: 0.137rem 0rem;
+ padding: 0rem 0.682rem;
+}
+
+.bar-systray-item {
+ @include full-rounding;
+ @include element_decel;
+ @include symbolic-icon;
+ min-height: 1.032rem;
+ min-width: 1.032rem;
+ font-size: 1.032rem;
+ color: $trayOnLayer0;
+}
+
+.bar-statusicons {
+ @include full-rounding;
+ @include element_decel;
+ margin: 0.273rem;
+ padding: 0rem 0.614rem;
+}
+
+.bar-statusicons-active {
+ background-color: $layer0Active;
+ color: $onLayer0Active;
+}
+
+.bar-util-btn {
+ @include full-rounding;
+ @include element_decel;
+ min-height: 1.77rem;
+ min-width: 1.77rem;
+ background-color: $utilsLayer2;
+ color: $utilsOnLayer2;
+}
+
+.bar-util-btn:hover,
+.bar-util-btn:focus {
+ background-color: $layer2Hover;
+}
+
+.bar-util-btn:active {
+ background-color: $layer2Active;
+}
+
+.bar-spaceright {
+ color: $barspacerightOnLayer0;
+}
+
+.bar-bluetooth-device {
+ @include full-rounding;
+ @include symbolic-icon;
+ min-height: 1.032rem;
+ min-width: 1.032rem;
+ font-size: 1.032rem;
+ padding: 0.205rem 0.341rem;
+}
diff --git a/modules/home-manager/ags/ags/scss/_cheatsheet.scss b/modules/home-manager/ags/ags/scss/_cheatsheet.scss
new file mode 100644
index 0000000..19c0c97
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_cheatsheet.scss
@@ -0,0 +1,168 @@
+.cheatsheet-bg {
+ @include large-rounding;
+ @include elevation-border;
+ @include elevation2;
+ margin-bottom: 0.682rem;
+ background-color: $layer0;
+ padding: 1.364rem;
+}
+
+.cheatsheet-title {
+ color: $cheatsheetTitle;
+}
+
+.cheatsheet-bind-lineheight {
+ min-height: 2.045rem;
+}
+
+.cheatsheet-key {
+ @include techfont;
+ min-height: 1.364rem;
+ min-width: 1.364rem;
+ margin: 0.17rem;
+ padding: 0.136rem 0.205rem;
+ -gtk-outline-radius: 0.409rem;
+ color: $cheatsheetTitle;
+ border-radius: 0.409rem;
+ border: 0.068rem solid $cheatsheetTitle;
+ box-shadow: 0rem 0.136rem 0rem $cheatsheetTitle;
+}
+
+.cheatsheet-key-notkey {
+ min-height: 1.364rem;
+ padding: 0.136rem 0.205rem;
+ margin: 0.17rem;
+ color: $onLayer0;
+}
+
+@for $i from 1 through 8 {
+ .cheatsheet-color-#{$i} {
+ color: nth($cheatsheetColors, $i);
+ border-color: nth($cheatsheetColors, $i);
+ box-shadow: 0rem 0.136rem 0rem nth($cheatsheetColors, $i);
+ }
+}
+
+// .cheatsheet-action {}
+
+.cheatsheet-closebtn {
+ @include element_decel;
+ @include full-rounding;
+ min-width: 2.386rem;
+ min-height: 2.386rem;
+}
+
+.cheatsheet-closebtn:hover,
+.cheatsheet-closebtn:focus {
+ background-color: $layer0Hover;
+}
+
+.cheatsheet-closebtn:active {
+ background-color: $layer0Active;
+}
+
+.cheatsheet-category-title {
+ @include titlefont;
+ font-size: 1.705rem;
+}
+
+@mixin cheatsheet-periodictable-element {
+ min-width: 5.455rem;
+ min-height: 5.455rem;
+ @include small-rounding;
+ background-color: $layer1;
+ color: $onLayer1;
+}
+
+.cheatsheet-periodictable-elementsymbol {
+ @include readingfont;
+ font-size: 1.705rem;
+ font-weight: bold;
+}
+
+.cheatsheet-periodictable-elementnum {
+ @include full-rounding;
+ min-width: 1.364rem;
+ min-height: 1.364rem;
+ background-color: $term0;
+ color: $onBackground;
+}
+
+$colormetal: $term2;
+$colornonmetal: $term4;
+$colornoblegas: $term5;
+$colorlanthanum: $term3;
+$coloractinium: $term7;
+
+.cheatsheet-periodictable-empty {
+ @include small-rounding;
+ min-width: 5.455rem;
+ min-height: 5.455rem;
+}
+
+.cheatsheet-periodictable-metal {
+ @include cheatsheet-periodictable-element;
+ background-color: $colormetal;
+ color: $term0;
+}
+
+.cheatsheet-periodictable-nonmetal {
+ @include cheatsheet-periodictable-element;
+ background-color: $colornonmetal;
+ color: $term0;
+}
+
+.cheatsheet-periodictable-noblegas {
+ @include cheatsheet-periodictable-element;
+ background-color: $colornoblegas;
+ color: $term0;
+}
+
+.cheatsheet-periodictable-lanthanum {
+ @include cheatsheet-periodictable-element;
+ background-color: $colorlanthanum;
+ color: $term0;
+}
+
+.cheatsheet-periodictable-actinium {
+ @include cheatsheet-periodictable-element;
+ background-color: $coloractinium;
+ color: $term0;
+}
+
+.cheatsheet-periodictable-legend-color-wrapper {
+ @include full-rounding;
+ padding: 0.273rem;
+ border: 0.136rem solid $onLayer0;
+}
+
+@mixin cheatsheet-periodictable-legend-color {
+ @include full-rounding;
+ min-width: 1.023rem;
+ min-height: 1.023rem;
+}
+
+.cheatsheet-periodictable-legend-color-metal {
+ @include cheatsheet-periodictable-legend-color;
+ background-color: $colormetal;
+}
+
+.cheatsheet-periodictable-legend-color-nonmetal {
+ @include cheatsheet-periodictable-legend-color;
+ background-color: $colornonmetal;
+}
+
+.cheatsheet-periodictable-legend-color-noblegas {
+ @include cheatsheet-periodictable-legend-color;
+ background-color: $colornoblegas;
+}
+
+.cheatsheet-periodictable-legend-color-lanthanum {
+ @include cheatsheet-periodictable-legend-color;
+ background-color: $colorlanthanum;
+}
+
+.cheatsheet-periodictable-legend-color-actinium {
+ @include cheatsheet-periodictable-legend-color;
+ background-color: $coloractinium;
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/scss/_colors.scss b/modules/home-manager/ags/ags/scss/_colors.scss
new file mode 100644
index 0000000..ddcdd02
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_colors.scss
@@ -0,0 +1,99 @@
+$rootTransparency: 0.22; // Transparency = 1 - opacity
+$transparency: 0.75;
+
+// Transparent versions
+@if $transparent == True {
+ $background: transparentize($background, $rootTransparency);
+ $surface: transparentize($surface, $transparency);
+ $surfaceDim: transparentize($surfaceDim, $transparency);
+ $surfaceBright: transparentize($surfaceBright, $transparency);
+ $surfaceContainerLowest: transparentize($surfaceContainerLowest, $transparency);
+ $surfaceContainerLow: transparentize($surfaceContainerLow, $transparency);
+ $surfaceContainer: transparentize($surfaceContainer, $transparency);
+ $surfaceContainerHigh: transparentize($surfaceContainerHigh, $transparency);
+ $surfaceContainerHighest: transparentize($surfaceContainerHighest, $transparency);
+ $surfaceVariant: transparentize($surfaceVariant, $transparency);
+ $inverseSurface: transparentize($inverseSurface, $transparency);
+ $surfaceTint: transparentize($surfaceTint, $transparency);
+}
+
+// Others
+$subtext: $outline;
+$actiontext: mix($onBackground, $background, 85%);
+$black: black;
+$white: white;
+
+/// Color mappings for more chaotic, dynamic colors like the average rice ///
+// General
+$layer0: $background;
+$onLayer0: $onBackground;
+$layer0Hover: mix($layer0, $onLayer0, 85%);
+$layer0Active: $surfaceContainerHigh;
+$onLayer0Active: $onSurface;
+$onLayer0Inactive: mix($onLayer0, $layer0, 70%);
+$layer1: $surfaceContainerLow;
+$onLayer1: $onSurface;
+$onLayer1Inactive: mix($onLayer1, $layer1, 45%);
+$onLayer1: $onSurfaceVariant;
+$layer2: mix($surfaceContainer, $surfaceContainerHigh, 55%);
+$onLayer2: $onSurface;
+$layer3: $surfaceContainerHigh;
+$onLayer3: $onSurface;
+$layer1Hover: mix($layer1, $onLayer1, 85%);
+$layer1Active: mix($layer1, $onLayer1, 70%);
+$layer2Hover: mix($layer2, $onLayer2, 90%);
+$layer2Active: mix($layer2, $onLayer2, 80%);
+$layer3Hover: mix($layer3, $onLayer3, 90%);
+$layer3Active: mix($layer3, $onLayer3, 80%);
+// Elements
+$windowtitleOnLayer0Inactive: $onLayer0Inactive;
+$windowtitleOnLayer0: $onLayer0;
+$barspacerightOnLayer0: $onLayer0;
+$timeOnLayer1: $onLayer1;
+$dateOnLayer1: $onLayer1;
+$ramOnLayer1: $onLayer1;
+$ramLayer2: $secondaryContainer;
+$ramOnLayer2: $onSecondaryContainer;
+$swapOnLayer1: $onLayer1;
+$swapLayer2: $secondaryContainer;
+$swapOnLayer2: $onSecondaryContainer;
+$cpuOnLayer1: $onLayer1;
+$cpuLayer2: $secondaryContainer;
+$cpuOnLayer2: $onSecondaryContainer;
+$musicOnLayer1: $onLayer1;
+$musicLayer2: $secondaryContainer;
+$musicOnLayer2: $onSecondaryContainer;
+$utilsLayer2: $layer2;
+$utilsOnLayer2: $onLayer2;
+$battOnLayer1: $onLayer1;
+$battLayer2: $secondaryContainer;
+$battOnLayer2: $onSecondaryContainer;
+$workspaceOnLayer1Inactive: $onLayer1Inactive;
+$workspaceLayer3: $primary;
+$workspaceOnLayer3: $onPrimary;
+$workspaceOnLayer2: $onSecondaryContainer;
+$trayOnLayer0: $onLayer0;
+$cheatsheetTitle: $onSecondaryContainer;
+$cheatsheetColors: (
+ $onSecondaryContainer,
+ $onSecondaryContainer,
+ $onSecondaryContainer,
+ $onSecondaryContainer,
+ $onSecondaryContainer,
+ $onSecondaryContainer,
+ $onSecondaryContainer,
+ $onSecondaryContainer
+);
+
+$sessionColors: (
+ $onLayer1,
+ $onLayer1,
+ $onLayer1,
+ $onLayer1,
+ $onLayer1,
+ $onLayer1,
+ $onLayer1
+);
+$brightnessOnLayer0: $onLayer0;
+$volumeOnLayer0: $onLayer0;
+
diff --git a/modules/home-manager/ags/ags/scss/_common.scss b/modules/home-manager/ags/ags/scss/_common.scss
new file mode 100644
index 0000000..f8d3b62
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_common.scss
@@ -0,0 +1,319 @@
+* {
+ selection {
+ background-color: $secondary;
+ color: $onSecondary;
+ }
+
+ caret-color: $onLayer2;
+}
+
+@keyframes appear {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+menu {
+ @include small-rounding;
+ border: 1px solid mix($surfaceContainer, $onBackground, 80%); // I hate this
+ padding: 0.681rem;
+ background: opacify($surfaceContainer, 1);
+ color: $onSurface;
+ -gtk-outline-radius: 1.159rem;
+
+ animation-name: appear;
+ animation-duration: 40ms;
+ animation-timing-function: ease-out;
+ animation-iteration-count: 1;
+}
+
+menubar>menuitem {
+ border-radius: 0.545rem;
+ -gtk-outline-radius: 0.545rem;
+ min-width: 13.636rem;
+ min-height: 2.727rem;
+}
+
+menu>menuitem {
+ padding: 0.4em 1.5rem;
+ background: transparent;
+ transition: 0.2s ease background-color;
+ border-radius: 0.545rem;
+ -gtk-outline-radius: 0.545rem;
+}
+
+menu>menuitem:hover,
+menu>menuitem:focus {
+ background-color: $layer2Hover;
+}
+
+menu>menuitem:active {
+ background-color: $layer2Active;
+}
+
+radio {
+ @include full-rounding;
+ margin: 0.273rem;
+ min-width: 15px;
+ min-height: 15px;
+ border: 0.068rem solid $outline;
+}
+
+// radio:first-child {
+// background-color: red;
+// }
+
+radio:checked {
+ min-width: 8px;
+ min-height: 8px;
+ background-color: $onPrimary;
+ border: 0.477rem solid $primary;
+}
+
+tooltip {
+ animation-name: appear;
+ animation-duration: 100ms;
+ animation-timing-function: ease-out;
+ animation-iteration-count: 1;
+ @include normal-rounding;
+ background-color: opacify($color: $inverseSurface, $amount: 1);
+ color: $inverseOnSurface;
+}
+
+/////////////////////////////////////////
+// Emoji Chooser structure
+// popover
+// ├── box.emoji-searchbar
+// │ ╰── entry.search
+// ╰── box.emoji-toolbar
+// ├── button.image-button.emoji-section
+// ├── ...
+// ╰── button.image-button.emoji-section
+
+popover {
+ @include elevation-border-softer;
+ padding: 0.681rem;
+ background: $surfaceContainerHigh;
+ color: $onSurface;
+ border-radius: 1.159rem;
+ -gtk-outline-radius: 1.159rem;
+
+ animation-name: appear;
+ animation-duration: 40ms;
+ animation-timing-function: ease-out;
+ animation-iteration-count: 1;
+}
+
+
+/////////////////////////////////////////
+
+.configtoggle-box {
+ padding: 0.205rem 0.341rem;
+}
+
+.switch-bg {
+ @include element_decel;
+ @include full-rounding;
+ border: 0.136rem solid $onSurface;
+ min-width: 2.864rem;
+ min-height: 1.637rem;
+}
+
+.switch-bg-true {
+ background-color: $primary;
+ border: 0.136rem solid $primary;
+}
+
+.switch-fg {
+ @include full-rounding;
+ @include menu_decel;
+ background-color: $onSurface;
+ color: $layer1;
+ min-width: 0.819rem;
+ min-height: 0.819rem;
+ margin-left: 0.477rem;
+}
+
+.switch-fg-true {
+ background-color: $onPrimary;
+ color: $primary;
+ min-width: 1.431rem;
+ min-height: 1.431rem;
+ margin-left: 1.431rem;
+}
+
+.switch-fg-toggling-false {
+ @include menu_decel;
+ min-width: 1.636rem;
+ min-height: 0.819rem;
+}
+
+.segment-container {
+ @include full-rounding;
+ border: 0.068rem solid $outline;
+}
+
+.segment-container>*:first-child {
+ border-top-left-radius: 9999px;
+ border-bottom-left-radius: 9999px;
+}
+
+.segment-container>* {
+ border-right: 0.068rem solid $outline;
+ padding: 0.341rem 0.682rem;
+}
+
+.segment-container>*:last-child {
+ border-right: 0rem solid transparent;
+ border-top-right-radius: 9999px;
+ border-bottom-right-radius: 9999px;
+}
+
+.segment-btn {
+ color: $onSurface;
+}
+
+.segment-btn:focus,
+.segment-btn:hover {
+ background-color: $layer0Hover;
+}
+
+.segment-btn-enabled {
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+}
+
+.segment-btn-enabled:hover,
+.segment-btn-enabled:focus {
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+}
+
+.multipleselection-container {}
+
+.multipleselection-btn {
+ @include small-rounding;
+ padding: 0rem 0.341rem;
+ border: 0.034rem solid $outline;
+ color: $onSurface;
+}
+
+.multipleselection-btn:focus,
+.multipleselection-btn:hover {
+ background-color: $layer0Hover;
+ color: $onSurface;
+}
+
+.multipleselection-btn-enabled {
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+}
+
+.multipleselection-btn-enabled:hover,
+.multipleselection-btn-enabled:focus {
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+}
+
+.gap-v-5 {
+ min-height: 0.341rem;
+}
+
+.gap-h-5 {
+ min-width: 0.341rem;
+}
+
+.gap-v-10 {
+ min-height: 0.682rem;
+}
+
+.gap-h-10 {
+ min-width: 0.682rem;
+}
+
+.gap-v-15 {
+ min-height: 1.023rem;
+}
+
+.gap-h-15 {
+ min-width: 1.023rem;
+}
+
+.tab-btn {
+ @include small-rounding;
+ @include element_decel;
+ min-height: 2.5rem;
+ color: $onLayer0;
+}
+
+.tab-btn:hover {
+ background-color: $layer0Hover;
+}
+
+.tab-btn:focus {
+ background-color: $surfaceContainerLow;
+}
+
+.tab-btn-active>box>label {
+ color: $primary;
+}
+
+.tab-indicator {
+ transition: 180ms ease-in-out; // Doesn't look that good, but it syncs with the GtkStack
+ min-height: 0.205rem;
+ padding: 0rem 1.023rem;
+ color: $primary;
+}
+
+.tab-icon {
+ @include element_decel;
+ @include full-rounding;
+ min-width: 2.25rem;
+ min-height: 2.25rem;
+ font-size: 1.406rem;
+ color: $onSurface;
+}
+
+.tab-icon-active {
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+}
+
+.tab-icon-expandable {
+ transition: 0ms;
+ @include full-rounding;
+ min-width: 2.25rem;
+ min-height: 2.25rem;
+ font-size: 1.406rem;
+ color: $onSurface;
+ padding: 0rem;
+}
+
+.tab-icon-expandable-active {
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+ padding: 0rem 0.545rem;
+ min-width: 9.545rem;
+}
+
+widget {
+ @include small-rounding;
+}
+
+.spinbutton {
+ @include small-rounding;
+ background-color: $layer2;
+ padding: 0.341rem;
+ entry {
+ color: $onLayer2;
+ margin: 0.136rem 0.273rem;
+ }
+ button {
+ margin-left: 0.205rem;
+ padding: 0.136rem;
+ }
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/scss/_desktopbackground.scss b/modules/home-manager/ags/ags/scss/_desktopbackground.scss
new file mode 100644
index 0000000..2dda9a1
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_desktopbackground.scss
@@ -0,0 +1,92 @@
+.bg-wallpaper-transition {
+ transition: 1000ms cubic-bezier(0.05, 0.7, 0.1, 1);
+ font-size: 1px;
+}
+
+@mixin bg-textshadow {
+ // text-shadow: mix($shadow, $secondaryContainer, 50%) 1px 0px 3px;
+}
+
+.bg-time-box {
+ @include large-rounding;
+ margin: 2.045rem;
+ padding: 0.682rem;
+}
+
+.bg-time-clock {
+ @include titlefont;
+ @include bg-textshadow;
+ font-size: 5.795rem;
+ color: $onLayer0;
+}
+
+.bg-time-date {
+ @include titlefont;
+ @include bg-textshadow;
+ font-size: 2.591rem;
+ color: $onLayer0;
+}
+
+.bg-distro-box {
+ @include large-rounding;
+ margin: 2.045rem;
+ padding: 0.682rem;
+}
+
+.bg-distro-txt {
+ @include titlefont;
+ @include bg-textshadow;
+ font-size: 1.432rem;
+ color: $onLayer0;
+}
+
+.bg-distro-name {
+ @include titlefont;
+ @include bg-textshadow;
+ font-size: 1.432rem;
+ color: $onSecondaryContainer;
+}
+
+.bg-graph {
+ color: rgba(255, 255, 255, 0.5);
+ border-radius: 0.614rem;
+ border: 0.682rem solid;
+}
+
+.bg-quicklaunch-title {
+ @include mainfont;
+ color: $onSurfaceVariant;
+}
+
+.bg-quicklaunch-btn {
+ @include mainfont;
+ @include full-rounding;
+ background-color: $layer2;
+ color: $onLayer2;
+ min-width: 4.432rem;
+ min-height: 2.045rem;
+ padding: 0.273rem 0.682rem;
+}
+
+.bg-quicklaunch-btn:hover,
+.bg-quicklaunch-btn:focus {
+ background-color: $layer2Hover;
+}
+
+.bg-quicklaunch-btn:active {
+ background-color: $layer2Active;
+}
+
+.bg-system-bg {
+ @include normal-rounding;
+ // background-color: $background;
+}
+
+.bg-system-circprog {
+ @include fluent_decel_long;
+ min-width: 0.205rem; // Trough stroke width
+ min-height: 4.091rem; // Diameter
+ font-size: 0px;
+ padding: 0rem;
+ background-color: $layer2;
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/scss/_dock.scss b/modules/home-manager/ags/ags/scss/_dock.scss
new file mode 100644
index 0000000..2e087ff
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_dock.scss
@@ -0,0 +1,42 @@
+.dock-bg {
+ @include large-rounding;
+ @include elevation2;
+ background-color: $layer0;
+ padding: 0.682rem;
+}
+
+.dock-app-btn-animate {
+ transition-property: background-color;
+ transition-duration: 0.5s;
+}
+
+.dock-app-btn {
+ @include normal-rounding;
+ padding: 0.273rem;
+}
+
+.pinned-dock-app-btn {
+ @include normal-rounding;
+ padding: 0.273rem;
+ background-color: $layer0Hover;
+}
+
+.dock-app-btn:hover,
+.dock-app-btn:focus {
+ background-color: $layer0Hover;
+}
+
+.dock-app-btn:active {
+ background-color: $layer0Active;
+}
+
+.dock-app-icon {
+ min-width: 3.409rem;
+ min-height: 3.409rem;
+ font-size: 3.409rem;
+}
+
+.dock-separator {
+ min-width: 0.068rem;
+ background-color: $outline;
+}
diff --git a/modules/home-manager/ags/ags/scss/_lib_classes.scss b/modules/home-manager/ags/ags/scss/_lib_classes.scss
new file mode 100644
index 0000000..7a0068c
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_lib_classes.scss
@@ -0,0 +1,514 @@
+.test {
+ background-image: linear-gradient(45deg, #f4d609 0%, #f4d609 10%, #212121 10%, #212121 20%, #f4d609 20%, #f4d609 30%, #212121 30%,
+ #212121 40%, #f4d609 40%, #f4d609 50%, #212121 50%, #212121 60%, #f4d609 60%,
+ #f4d609 70%, #212121 70%, #212121 80%, #f4d609 80%, #f4d609 90%, #212121 90%, #212121 100%);
+ background-repeat: repeat;
+}
+
+.test-size {
+ min-height: 3rem;
+ min-width: 3rem;
+}
+
+.txt-title {
+ @include titlefont;
+ font-size: 2.045rem;
+}
+
+.txt-title-small {
+ @include titlefont;
+ font-size: 1.364rem;
+}
+
+.techfont {
+ @include techfont;
+}
+
+.txt-reading {
+ @include readingfont;
+}
+
+.no-anim {
+ @include noanim;
+}
+
+.txt {
+ color: $onBackground;
+}
+
+.txt-primary {
+ color: $primary;
+}
+
+.txt-onSecondaryContainer {
+ color: $onSecondaryContainer;
+}
+
+.txt-onSurfaceVariant {
+ color: $onSurfaceVariant;
+}
+
+.txt-onLayer1 {
+ color: $onLayer1;
+}
+
+.txt-shadow {
+ text-shadow: 1px 2px 8px rgba(0, 0, 0, 0.69);
+ margin: 10px;
+}
+
+.txt-gigantic {
+ @include mainfont;
+ font-size: 3rem;
+}
+
+.txt-massive {
+ @include mainfont;
+ font-size: 2.7273rem;
+}
+
+.txt-hugerass {
+ @include mainfont;
+ font-size: 2.045rem;
+}
+
+.txt-hugeass {
+ @include mainfont;
+ font-size: 1.8182rem;
+}
+
+.txt-larger {
+ @include mainfont;
+ font-size: 1.6363rem;
+}
+
+.txt-large {
+ //16pt
+ @include mainfont;
+ font-size: 1.4545rem;
+}
+
+.txt-norm {
+ //14pt
+ @include mainfont;
+ font-size: 1.2727rem;
+}
+
+.txt-small {
+ //12pt
+ @include mainfont;
+ font-size: 1.0909rem;
+}
+
+.txt-smallie {
+ //11pt
+ @include mainfont;
+ font-size: 1rem;
+}
+
+.txt-smaller {
+ //10pt
+ @include mainfont;
+ font-size: 0.9091rem;
+}
+
+.txt-tiny {
+ @include mainfont;
+ font-size: 0.7273rem;
+}
+
+.txt-poof {
+ font-size: 0px;
+}
+
+.txt-subtext {
+ @include subtext;
+}
+
+.txt-action {
+ @include actiontext;
+}
+
+.txt-thin {
+ font-weight: 300;
+}
+
+.txt-semibold {
+ font-weight: 500;
+}
+
+.txt-bold {
+ font-weight: bold;
+}
+
+.txt-italic {
+ font-style: italic;
+}
+
+.btn-primary {
+ @include full-rounding;
+ background-color: $primary;
+ color: $onPrimary;
+ padding: 0.682rem 1.023rem;
+}
+
+.titlefont {
+ @include titlefont;
+}
+
+.mainfont {
+ @include mainfont;
+}
+
+.icon-material {
+ @include icon-material;
+}
+
+.icon-nerd {
+ @include icon-nerd;
+}
+
+.separator-line {
+ background-color: mix($subtext, $surface, 50%);
+ min-width: 0.068rem;
+ min-height: 0.068rem;
+}
+
+.separator-circle {
+ @include full-rounding;
+ background-color: $outline;
+ margin: 0rem 0.682rem;
+ min-width: 0.273rem;
+ min-height: 0.273rem;
+}
+
+.spacing-h-3>* {
+ margin-right: 0.205rem;
+}
+
+.spacing-h-3>*:last-child {
+ margin-right: 0rem;
+}
+
+.spacing-v-3>* {
+ margin-bottom: 0.205rem;
+}
+
+.spacing-v-3>*:last-child {
+ margin-bottom: 0rem;
+}
+
+.spacing-v-15>* {
+ margin-bottom: 1.023rem;
+}
+
+.spacing-v-15>*:last-child {
+ margin-bottom: 0rem;
+}
+
+.spacing-h-15>* {
+ margin-right: 1.023rem;
+}
+
+.spacing-h-15>*:last-child {
+ margin-right: 0rem;
+}
+
+.spacing-h-15>revealer>* {
+ margin-right: 1.023rem;
+}
+
+.spacing-h-15>revealer:last-child>* {
+ margin-right: 0rem;
+}
+
+.spacing-h-15>scrolledwindow>* {
+ margin-right: 1.023rem;
+}
+
+.spacing-h-15>scrolledwindow:last-child>* {
+ margin-right: 0rem;
+}
+
+.spacing-v-5>box {
+ margin-bottom: 0.341rem;
+}
+
+.spacing-v-5>box:last-child {
+ margin-bottom: 0rem;
+}
+
+.spacing-v-5>* {
+ margin-bottom: 0.341rem;
+}
+
+.spacing-v-5>*:last-child {
+ margin-bottom: 0rem;
+}
+
+.spacing-v-5-revealer>revealer>* {
+ margin-bottom: 0.341rem;
+}
+
+.spacing-v-5-revealer>revealer:last-child>* {
+ margin-bottom: 0rem;
+}
+
+.spacing-v-5-revealer>scrolledwindow>* {
+ margin-bottom: 0.341rem;
+}
+
+.spacing-v-5-revealer>scrolledwindow:last-child>* {
+ margin-bottom: 0rem;
+}
+
+.spacing-h-4>* {
+ margin-right: 0.273rem;
+}
+
+.spacing-h-4>*:last-child {
+ margin-right: 0rem;
+}
+
+.spacing-h-4>overlay>*:first-child {
+ margin-right: 0.273rem;
+}
+
+.spacing-h-4>overlay:last-child>* {
+ margin-right: 0rem;
+}
+
+.spacing-h-5>* {
+ margin-right: 0.341rem;
+}
+
+.spacing-h-5>*:last-child {
+ margin-right: 0rem;
+}
+
+.spacing-h-5>widget>* {
+ margin-right: 0.341rem;
+}
+
+.spacing-h-5>widget:last-child>* {
+ margin-right: 0rem;
+}
+
+.spacing-h-5>revealer>* {
+ margin-right: 0.341rem;
+}
+
+.spacing-h-5>revealer:last-child>* {
+ margin-right: 0rem;
+}
+
+.spacing-h-5>scrolledwindow>* {
+ margin-right: 0.341rem;
+}
+
+.spacing-h-5>scrolledwindow:last-child>* {
+ margin-right: 0rem;
+}
+
+.spacing-v-minus5>* {
+ margin-bottom: -0.341rem;
+}
+
+.spacing-v-minus5>*:last-child {
+ margin-bottom: 0rem;
+}
+
+.spacing-h-10>* {
+ margin-right: 0.682rem;
+}
+
+.spacing-h-10>*:last-child {
+ margin-right: 0rem;
+}
+
+.spacing-h-10>revealer>* {
+ margin-right: 0.682rem;
+}
+
+.spacing-h-10>revealer:last-child>* {
+ margin-right: 0rem;
+}
+
+.spacing-h-10>scrolledwindow>* {
+ margin-right: 0.682rem;
+}
+
+.spacing-h-10>scrolledwindow:last-child>* {
+ margin-right: 0rem;
+}
+
+.spacing-h-10>flowboxchild>* {
+ margin-right: 0.682rem;
+}
+
+.spacing-h-10>flowboxchild:last-child>* {
+ margin-right: 0rem;
+}
+
+.spacing-v-10>* {
+ margin-bottom: 0.682rem;
+}
+
+.spacing-v-10>*:last-child {
+ margin-bottom: 0rem;
+}
+
+.spacing-h-20>* {
+ margin-right: 1.364rem;
+}
+
+.spacing-h-20>*:last-child {
+ margin-right: 0rem;
+}
+
+.spacing-v-20>* {
+ margin-bottom: 1.364rem;
+}
+
+.spacing-v-20>*:last-child {
+ margin-bottom: 0rem;
+}
+
+.spacing-h-30>* {
+ margin-right: 1.364rem;
+}
+
+.spacing-h-30>*:last-child {
+ margin-right: 0rem;
+}
+
+.spacing-v-30>* {
+ margin-bottom: 1.364rem;
+}
+
+.spacing-v-30>*:last-child {
+ margin-bottom: 0rem;
+}
+
+.anim-enter {
+ @include anim-enter;
+}
+
+.anim-exit {
+ @include anim-exit;
+}
+
+.button-minsize {
+ @include button-minsize;
+}
+
+@each $spacing in (5, 8, 10, 15, 20) {
+ @each $dir in ("top", "bottom", "left", "right") {
+ .margin-#{$dir}-#{$spacing} {
+ margin-#{$dir}: 0.068rem * $spacing;
+ }
+ .padding-#{$dir}-#{$spacing} {
+ padding-#{$dir}: 0.068rem * $spacing;
+ }
+ }
+ .padding-#{$spacing} {
+ padding: 0.068rem * $spacing;
+ }
+ .margin-#{$spacing} {
+ padding: 0.068rem * $spacing;
+ }
+}
+
+.width-10 {
+ min-width: 0.682rem;
+}
+
+.height-10 {
+ min-width: 0.682rem;
+}
+
+.invisible {
+ opacity: 0;
+ background-color: transparent;
+ color: transparent;
+}
+
+.spacing-h--5>box {
+ margin-right: -0.341rem;
+}
+
+.spacing-h--5>box:last-child {
+ margin-right: 0rem;
+}
+
+.spacing-v--5>* {
+ margin-bottom: -0.341rem;
+}
+
+.spacing-v--5>*:last-child {
+ margin-bottom: 0rem;
+}
+
+.spacing-h--10>* {
+ margin-left: -1.364rem;
+}
+
+.spacing-h--10>*:first-child {
+ margin-left: 0rem;
+}
+
+.spacing-v--10>* {
+ margin-bottom: -0.682rem;
+}
+
+.spacing-v--10>*:last-child {
+ margin-bottom: 0rem;
+}
+
+.spacing-v--10>* {
+ margin-bottom: -0.682rem;
+}
+
+.spacing-v--10>*:last-child {
+ margin-bottom: 0rem;
+}
+
+.spacing-h--20>* {
+ margin-left: -1.364rem;
+}
+
+.spacing-h--20>*:first-child {
+ margin-left: 0rem;
+}
+
+.instant {
+ transition: 0ms;
+}
+
+.menu-decel {
+ @include menu_decel;
+}
+
+.element-show {
+ @include element_easeInOut;
+}
+
+.element-hide {
+ @include element_easeInOut;
+}
+
+.element-move {
+ @include element_easeInOut;
+}
+
+.element-decel {
+ @include element_decel;
+}
+
+.element-bounceout {
+ @include element_bounceOut;
+}
+
+.element-accel {
+ @include element_accel;
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/scss/_lib_mixins.scss b/modules/home-manager/ags/ags/scss/_lib_mixins.scss
new file mode 100644
index 0000000..548909c
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_lib_mixins.scss
@@ -0,0 +1,177 @@
+// Common colors
+$hovercolor: $surfaceContainerHigh;
+$activecolor: $surfaceContainerHighest;
+$rounding_verysmall: 0.477rem;
+$rounding_small: 0.818rem;
+$rounding_mediumsmall: 0.955rem;
+$rounding_medium: 1.159rem;
+$rounding_mediumlarge: 1.364rem;
+$rounding_large: 1.705rem;
+
+// Common rules
+@mixin small-rounding {
+ border-radius: $rounding_small;
+ -gtk-outline-radius: $rounding_small;
+}
+
+@mixin normal-rounding {
+ border-radius: $rounding_medium;
+ -gtk-outline-radius: $rounding_medium;
+}
+
+@mixin large-rounding {
+ border-radius: $rounding_large;
+ -gtk-outline-radius: $rounding_large;
+}
+
+@mixin full-rounding {
+ border-radius: 9999px;
+ -gtk-outline-radius: 9999px;
+}
+
+// @mixin symbolic-icon {
+// -gtk-icon-theme: 'Adwaita';
+// }
+
+@mixin titlefont {
+ // Geometric sans-serif
+ font-family: "Gabarito", "Poppins", "Readex Pro", "Lexend", sans-serif;
+}
+
+@mixin mainfont {
+ // Other clean sans-serif
+ font-family: "Rubik", "Geist", "AR One Sans", "Reddit Sans", "Inter",
+ "Roboto", "Ubuntu", "Noto Sans", sans-serif;
+ // font-weight: 500;
+}
+
+@mixin icon-material {
+ // Material Design Icons
+ font-family: "Material Symbols Rounded", "MaterialSymbolsRounded", "Material Symbols Outlined",
+ "Material Symbols Sharp";
+}
+
+@mixin icon-nerd {
+ // Nerd Fonts
+ font-family: "SpaceMono NF", "SpaceMono Nerd Font", "JetBrains Mono NF",
+ "JetBrains Mono Nerd Font", monospace;
+}
+
+@mixin techfont {
+ // Monospace for sys info n stuff. Doesn't have to be a nerd font, but it's cool.
+ font-family: "JetBrains Mono NF", "JetBrains Mono Nerd Font",
+ "JetBrains Mono NL", "SpaceMono NF", "SpaceMono Nerd Font", monospace;
+}
+
+@mixin readingfont {
+ // The most readable fonts, for a comfortable reading experience
+ // in stuff like AI chat on sidebar
+ font-family: "Readex Pro", "Lexend", "Noto Sans", sans-serif;
+ // font-weight: 500;
+}
+
+@mixin subtext {
+ color: $subtext;
+}
+
+@mixin actiontext {
+ color: $actiontext;
+}
+
+$elevation_margin: 0.476rem;
+
+@mixin elevation2 {
+ box-shadow: 0px 2px 3px transparentize($shadow, 0.55);
+ margin: $elevation_margin;
+}
+
+@mixin elevation2-margin {
+ margin: $elevation_margin;
+}
+
+@mixin elevation2-padding {
+ padding: $elevation_margin;
+}
+
+@mixin elevation3 {
+ box-shadow: 0px 2px 5px $shadow;
+ margin: $elevation_margin;
+}
+
+@mixin menu_decel {
+ transition: 300ms cubic-bezier(0.1, 1, 0, 1);
+}
+
+@mixin menu_decel_fast {
+ transition: 170ms cubic-bezier(0.1, 1, 0, 1);
+}
+
+@mixin menu_accel {
+ transition: 160ms cubic-bezier(0.38, 0.04, 1, 0.07);
+}
+
+@mixin menu_accel_fast {
+ transition: 100ms cubic-bezier(0.38, 0.04, 1, 0.07);
+}
+
+@mixin fluent_decel {
+ transition: 200ms cubic-bezier(0.1, 1, 0, 1);
+}
+
+@mixin fluent_decel_long {
+ transition: 1000ms cubic-bezier(0.1, 1, 0, 1);
+}
+
+@mixin fluent_accel {
+ transition: 150ms cubic-bezier(0.42, 0, 1, 1);
+}
+
+@mixin noanim {
+ transition: 0ms;
+}
+
+@mixin anim-enter {
+ transition: 200ms cubic-bezier(0.05, 0.7, 0.1, 1);
+}
+
+@mixin anim-exit {
+ transition: 150ms cubic-bezier(0.3, 0, 0.8, 0.15);
+}
+
+@mixin element_decel {
+ transition: 300ms cubic-bezier(0, 0.55, 0.45, 1);
+}
+
+@mixin element_bounceOut {
+ transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+@mixin element_accel {
+ transition: 300ms cubic-bezier(0.55, 0, 1, 0.45);
+}
+
+@mixin element_easeInOut {
+ transition: 300ms cubic-bezier(0.85, 0, 0.15, 1);
+}
+
+@mixin elevation-border-softer {
+ border-top: 1px solid mix($surface, $onSurface, 86%);
+ border-left: 1px solid mix($surface, $onSurface, 86%);
+ border-right: 1px solid mix($surface, $onSurface, 90%);
+ border-bottom: 1px solid mix($surface, $onSurface, 90%);
+}
+
+@mixin elevation-border {
+ border-top: 1px solid $surfaceContainer;
+ border-left: 1px solid $surfaceContainer;
+ border-right: 1px solid $surfaceContainer;
+ border-bottom: 1px solid $surfaceContainer;
+}
+
+@mixin button-minsize {
+ min-width: 2.727rem;
+ min-height: 2.727rem;
+}
+
+$white: white;
+$black: black;
diff --git a/modules/home-manager/ags/ags/scss/_music.scss b/modules/home-manager/ags/ags/scss/_music.scss
new file mode 100644
index 0000000..473a855
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_music.scss
@@ -0,0 +1,156 @@
+@import 'material';
+@import 'musicmaterial';
+@import './wal';
+@import 'musicwal';
+@import './colors';
+@import './lib_mixins';
+$music_transparentize_amount: 0.3;
+$music_gradient1: mix($color1, $background, 50%);
+
+// @if $darkmode == True {
+// $music_gradient1: mix($color1, $background, 30%);
+// }
+
+$music_gradient2: $color3;
+$music_gradient3: $color5;
+$music_colorstart_transparentize: 0.3;
+$music_extra_transparentize: 0.15;
+
+$secondaryContainer: transparentize(mix(mix($background, $color2, 50%), $color6, 80%), 0.5);
+$onSecondaryContainer: mix($color7, $color2, 90%);
+@if $darkmode == False {
+ $onSecondaryContainer: mix($onSecondaryContainer, black, 50%);
+}
+
+.osd-music {
+ @include menu_decel;
+ @include elevation2;
+ margin-top: 0.313rem;
+ @include normal-rounding;
+ // min-height: 7.159rem;
+ min-width: 29.659rem;
+ background-color: $layer0;
+ padding: 0rem 1.023rem;
+ background: // Inspired by Amberol
+ linear-gradient(127deg, transparentize($music_gradient1, $music_colorstart_transparentize), transparentize($music_gradient1, $music_colorstart_transparentize - $music_transparentize_amount + $music_extra_transparentize) 70.71%),
+ linear-gradient(217deg, transparentize($music_gradient2, $music_colorstart_transparentize), transparentize($music_gradient2, $music_colorstart_transparentize - $music_transparentize_amount + $music_extra_transparentize) 70.71%),
+ radial-gradient(circle at 0% 100%, $color4 13%, rgba(0, 0, 0, 0) 100%),
+ linear-gradient(336deg, transparentize($music_gradient3, $music_colorstart_transparentize), transparentize($music_gradient3, $music_colorstart_transparentize - $music_transparentize_amount + $music_extra_transparentize) 70.71%),
+ linear-gradient($background, $background), // We don't want wallpaper tint, so here's a fully opaque surface
+ ;
+}
+
+
+.osd-music-cover-fallback {
+ @include element_decel;
+ @include small-rounding;
+ // margin: 1.023rem;
+ min-width: 7.5rem;
+ min-height: 7.5rem;
+ background-color: $layer1;
+ color: $onSecondaryContainer;
+}
+
+.osd-music-cover {
+ @include small-rounding;
+ @include menu_decel;
+ // Must be -top and -bottom or it'll mess up horizontal spacing
+ margin-top: 1.023rem;
+ margin-bottom: 1.023rem;
+ min-width: 7.5rem;
+ min-height: 7.5rem;
+}
+
+.osd-music-cover-art {
+ @include small-rounding;
+ min-width: 7.5rem;
+ min-height: 7.5rem;
+ background-size: cover;
+ background-position: center;
+}
+
+.osd-music-info {
+ margin: 1.023rem 0rem;
+}
+
+.osd-music-title {
+ @include element_decel;
+ @include titlefont;
+ font-size: 1.364rem;
+ color: $onSecondaryContainer;
+}
+
+.osd-music-artists {
+ @include element_decel;
+ @include mainfont;
+ font-size: 0.955rem;
+ color: mix($onSecondaryContainer, $secondaryContainer, 80%);
+}
+
+.osd-music-pill {
+ @include element_decel;
+ @include full-rounding;
+ @include titlefont;
+ min-width: 1.833rem;
+ padding: 0.273rem 0.682rem;
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+}
+
+.osd-music-controls {
+ @include element_decel;
+ @include full-rounding;
+ @include titlefont;
+ min-width: 1.833rem;
+ padding: 0.205rem;
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+}
+
+.osd-music-controlbtn {
+ @include menu_decel;
+ min-width: 2.045rem;
+ min-height: 2.045rem;
+ @include full-rounding;
+}
+
+.osd-music-controlbtn:hover,
+.osd-music-controlbtn:focus {
+ background-color: mix($secondaryContainer, $onSecondaryContainer, 90%);
+}
+
+.osd-music-controlbtn:active {
+ background-color: mix($secondaryContainer, $onSecondaryContainer, 85%);
+}
+
+.osd-music-controlbtn-txt {
+ @include element_decel;
+ transition: 100ms cubic-bezier(0.05, 0.7, 0.1, 1);
+ @include icon-material;
+ font-size: 1.364rem;
+ margin: -0.1rem 0rem;
+}
+
+.osd-music-circprog {
+ @include fluent_decel_long;
+ min-width: 0.409rem; // width of progress
+ min-height: 3.068rem;
+ padding: 0.273rem;
+ color: $onSecondaryContainer;
+}
+
+.osd-music-playstate {
+ @include menu_decel;
+ min-height: 3.068rem;
+ min-width: 3.068rem;
+ border-radius: 10rem;
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+}
+
+.osd-music-playstate-btn>label {
+ transition: 50ms cubic-bezier(0.05, 0.7, 0.1, 1);
+ @include icon-material;
+ font-size: 1.364rem;
+ margin: -0.1rem 0rem;
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/scss/_notifications.scss b/modules/home-manager/ags/ags/scss/_notifications.scss
new file mode 100644
index 0000000..152ead0
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_notifications.scss
@@ -0,0 +1,255 @@
+@mixin notif-rounding {
+ @include normal-rounding;
+}
+
+.notif-low {
+ @include notif-rounding;
+ background-color: $layer2;
+ color: $onLayer2;
+ padding: $rounding_small;
+ padding-right: $rounding_small + 0.545rem;
+}
+
+.notif-normal {
+ @include notif-rounding;
+ background-color: $layer2;
+ color: $onLayer2;
+ padding: $rounding_small;
+ padding-right: $rounding_small + 0.545rem;
+}
+
+.notif-critical {
+ @include notif-rounding;
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+ padding: $rounding_small;
+ padding-right: $rounding_small + 0.545rem;
+}
+
+.notif-clicked-low {
+ background-color: $layer1Hover;
+}
+
+.notif-clicked-normal {
+ background-color: $layer1Hover;
+}
+
+.notif-clicked-critical {
+ background-color: $onSecondary;
+ color: $onSecondaryContainer;
+}
+
+.popup-notif-low {
+ @include notif-rounding;
+ min-width: 30.682rem;
+ background-color: $layer2;
+
+ @if $transparent ==True {
+ background-color: $layer0;
+ }
+
+ border: 0.034rem solid $outlineVariant;
+ color: $onLayer2;
+ padding: $rounding_small;
+ padding-right: $rounding_small + 0.545rem;
+}
+
+.popup-notif-normal {
+ @include notif-rounding;
+ min-width: 30.682rem;
+ background-color: $layer2;
+
+ @if $transparent ==True {
+ background-color: $layer0;
+ }
+
+ border: 0.034rem solid $outlineVariant;
+ color: $onLayer2;
+ padding: $rounding_small;
+ padding-right: $rounding_small + 0.545rem;
+}
+
+.popup-notif-critical {
+ @include notif-rounding;
+ min-width: 30.682rem;
+ background-color: $secondaryContainer;
+ border: 0.034rem solid $onSecondaryContainer;
+ color: $onSecondaryContainer;
+ padding: $rounding_small;
+ padding-right: $rounding_small + 0.545rem;
+}
+
+.popup-notif-clicked-low {
+ background-color: $surfaceContainerLow;
+}
+
+.popup-notif-clicked-normal {
+ background-color: $surfaceContainerLow;
+}
+
+.popup-notif-clicked-critical {
+ background-color: $onSecondary;
+ color: $onSecondaryContainer;
+}
+
+.notif-body-low {
+ color: $outline;
+}
+
+.notif-body-normal {
+ color: $outline;
+}
+
+.notif-body-critical {
+ color: mix($onSecondaryContainer, $secondaryContainer, 67%);
+}
+
+.notif-icon {
+ @include full-rounding;
+ min-width: 3.409rem;
+ min-height: 3.409rem;
+ font-size: 2.182rem;
+}
+
+.notif-icon-material {
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+}
+
+.notif-icon-material-low {
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+}
+
+.notif-icon-material-normal {
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+}
+
+.notif-icon-material-critical {
+ background-color: $secondary;
+ color: $onSecondary;
+}
+
+.notif-expand-btn {
+ @include notif-rounding;
+ min-width: 1.841rem;
+ min-height: 1.841rem;
+}
+
+.notif-expand-btn:hover,
+.notif-expand-btn:focus {
+ background: $layer2Hover;
+}
+
+.notif-expand-btn:active {
+ background: $layer2Active;
+}
+
+.notif-listaction-btn {
+ @include notif-rounding;
+ padding: 0.341rem 0.682rem;
+}
+
+.notif-listaction-btn:hover,
+.notif-listaction-btn:focus {
+ background-color: $layer2Hover;
+}
+
+.notif-listaction-btn:active {
+ background-color: $layer2Active;
+}
+
+.notif-listaction-btn-enabled {
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+}
+
+.notif-listaction-btn-enabled:hover,
+.notif-listaction-btn-enabled:focus {
+ background-color: mix($secondaryContainer, $onSecondaryContainer, 90%);
+}
+
+.notif-listaction-btn-enabled:active {
+ background-color: mix($secondaryContainer, $onSecondaryContainer, 75%);
+}
+
+.osd-notif {
+ @include notif-rounding;
+ background-color: $layer0;
+ min-width: 30.682rem;
+}
+
+.notif-circprog-low {
+ transition: 0ms linear;
+ min-width: 0.136rem; // line width
+ min-height: 3.136rem;
+ padding: 0rem;
+ color: $onSecondaryContainer;
+}
+
+.notif-circprog-normal {
+ transition: 0ms linear;
+ min-width: 0.136rem; // line width
+ min-height: 3.136rem;
+ padding: 0rem;
+ color: $onSecondaryContainer;
+}
+
+.notif-circprog-critical {
+ transition: 0ms linear;
+ min-width: 0.136rem; // line width
+ min-height: 3.136rem;
+ padding: 0rem;
+ color: $secondaryContainer;
+}
+
+.notif-actions {
+ min-height: 2.045rem;
+}
+
+.notif-action {
+ @include small-rounding;
+}
+
+.notif-action-low {
+ background-color: $surfaceContainerHighest;
+ color: $onLayer2;
+}
+
+.notif-action-low:focus,
+.notif-action-low:hover {
+ border: 0.040rem solid $outlineVariant;
+}
+
+.notif-action-low:active {
+ background-color: $surfaceBright;
+}
+
+.notif-action-normal {
+ background-color: $surfaceContainerHighest;
+ color: $onSurface;
+}
+
+.notif-action-normal:focus,
+.notif-action-normal:hover {
+ border: 0.040rem solid $outlineVariant;
+}
+
+.notif-action-normal:active {
+ background-color: $surfaceBright;
+}
+
+.notif-action-critical {
+ background-color: mix($secondary, $onSecondary, 30%);
+ color: $onSurfaceVariant;
+}
+
+.notif-action-critical:focus,
+.notif-action-critical:hover {
+ border: 0.040rem solid $outline;
+}
+
+.notif-action-critical:active {
+ background-color: mix($secondary, $onSecondary, 40%);
+}
diff --git a/modules/home-manager/ags/ags/scss/_osd.scss b/modules/home-manager/ags/ags/scss/_osd.scss
new file mode 100644
index 0000000..ef21f43
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_osd.scss
@@ -0,0 +1,205 @@
+// .osd-window {
+ // margin-top: 2.727rem;
+// }
+
+.osd-bg {
+ min-width: 8.864rem;
+ min-height: 3.409rem;
+}
+
+.osd-value {
+ @include elevation-border;
+ @include elevation2;
+ background-color: $layer0;
+ border-radius: 1.023rem;
+ padding: 0.625rem 1.023rem;
+ padding-top: 0.313rem;
+}
+
+.osd-progress {
+ min-height: 0.955rem;
+ min-width: 0.068rem;
+ padding: 0rem;
+ border-radius: 10rem;
+ @include fluent_decel;
+
+ trough {
+ min-height: 0.954rem;
+ min-width: 0.068rem;
+ border-radius: 10rem;
+ background-color: $layer2;
+ // border: 0.068rem solid $onSecondaryContainer;
+ }
+
+ progress {
+ @include fluent_decel;
+ min-height: 0.680rem;
+ min-width: 0.680rem;
+ margin: 0rem 0.137rem;
+ border-radius: 10rem;
+ background-color: $onLayer2;
+ }
+}
+
+.osd-label {
+ font-size: 1.023rem;
+ font-weight: 500;
+ margin-top: 0.341rem;
+}
+
+.osd-value-txt {
+ @include titlefont;
+ font-size: 1.688rem;
+ font-weight: 500;
+ color: $onLayer0;
+}
+
+.osd-brightness {
+ color: $brightnessOnLayer0;
+}
+.osd-brightness-progress {
+ progress {
+ background-color: $brightnessOnLayer0;
+ }
+}
+.osd-volume {
+ color: $volumeOnLayer0;
+}
+.osd-volume-progress {
+ progress {
+ background-color: $volumeOnLayer0;
+ }
+}
+
+.osd-notifs {
+ padding-top: 0.313rem;
+}
+
+.osd-colorscheme {
+ border-radius: 1.023rem;
+ background-color: $layer0;
+ padding: 0.313rem 0.626rem;
+ @include elevation2;
+}
+
+.osd-colorscheme-settings {
+ background-color: $layer1;
+ padding: 0.313rem 0.626rem;
+ @include small-rounding;
+}
+
+.osd-color {
+ border-radius: 0.650rem;
+ -gtk-outline-radius: 0.650rem;
+ min-width: 2.727rem;
+ min-height: 1.705rem;
+ padding: 0rem 0.341rem;
+ font-weight: bold;
+
+ box {
+ @include small-rounding;
+ margin: 0.409rem;
+ }
+}
+
+.osd-color-primary {
+ background-color: $primary;
+ color: $onPrimary;
+ box { background-color: $onPrimary; }
+}
+.osd-color-primaryContainer {
+ background-color: $primaryContainer;
+ color: $onPrimaryContainer;
+ box { background-color: $onPrimaryContainer; }
+}
+.osd-color-secondary {
+ background-color: $secondary;
+ color: $onSecondary;
+ box { background-color: $onSecondary; }
+}
+.osd-color-secondaryContainer {
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+ box { background-color: $onSecondaryContainer; }
+}
+
+.osd-color-tertiary {
+ background-color: $tertiary;
+ color: $onTertiary;
+ box { background-color: $onTertiary; }
+}
+.osd-color-tertiaryContainer {
+ background-color: $tertiaryContainer;
+ color: $onTertiaryContainer;
+ box { background-color: $onTertiaryContainer; }
+}
+
+.osd-color-error {
+ background-color: $error;
+ color: $onError;
+ box { background-color: $onError; }
+}
+.osd-color-errorContainer {
+ background-color: $errorContainer;
+ color: $onErrorContainer;
+ box { background-color: $onErrorContainer; }
+}
+
+.osd-color-surface {
+ background-color: $surface;
+ color: $onSurface;
+ border: 0.068rem solid $outlineVariant;
+ box { background-color: $onSurface; }
+}
+
+.osd-color-surfaceContainer {
+ background-color: $surfaceContainer;
+ color: $onSurface;
+ box { background-color: $onSurface; }
+}
+
+.osd-color-inverseSurface {
+ background-color: $inverseSurface;
+ color: $inverseOnSurface;
+ box { background-color: $onSurfaceVariant; }
+}
+
+.osd-color-surfaceVariant {
+ background-color: $surfaceVariant;
+ color: $onSurfaceVariant;
+ box { background-color: $onSurfaceVariant; }
+}
+.osd-color-L1 {
+ background-color: $layer1;
+ color: $onLayer1;
+ box { background-color: $onLayer1; }
+}
+
+.osd-color-layer0 {
+ background-color: $layer0;
+ color: $onLayer0;
+ box { background-color: $onLayer0; }
+}
+
+.osd-settings-btn-arrow {
+ @include full-rounding;
+ @include icon-material;
+ min-width: 1.705rem;
+ min-height: 1.705rem;
+ color: $onSurface;
+
+ &:hover {
+ background-color: $surfaceContainerHigh;
+ }
+ &:active {
+ background-color: $surfaceContainerHighest;
+ }
+}
+
+.osd-show {
+ transition: 200ms cubic-bezier(0.1, 1, 0, 1);
+}
+
+.osd-hide {
+ transition: 190ms cubic-bezier(0.85, 0, 0.15, 1);
+}
diff --git a/modules/home-manager/ags/ags/scss/_osk.scss b/modules/home-manager/ags/ags/scss/_osk.scss
new file mode 100644
index 0000000..6cefc4a
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_osk.scss
@@ -0,0 +1,119 @@
+$osk_key_height: 2.5rem;
+$osk_key_width: 2.5rem;
+$osk_key_padding: 0.188rem;
+$osk_key_rounding: 0.545rem;
+$osk_key_fontsize: 1.091rem;
+
+.osk-window {
+ // @include menu_decel_fast;
+ @include large-rounding;
+ @include elevation-border;
+ @include elevation2;
+ // min-height: 29.591rem;
+ // min-width: 50rem;
+ background-color: $layer0;
+}
+
+.osk-body {
+ padding: 1.023rem;
+ padding-top: 0rem;
+}
+
+.osk-show {
+ @include menu_decel_fast;
+}
+
+.osk-hide {
+ margin-top: 30.682rem;
+ margin-bottom: -30.682rem;
+ // opacity: 0;
+ @include menu_accel_fast;
+}
+
+.osk-dragline {
+ @include full-rounding;
+ background-color: $onLayer0Inactive;
+ min-height: 0.273rem;
+ min-width: 10.227rem;
+ margin-top: 0.545rem;
+ margin-bottom: 0.205rem;
+}
+
+.osk-key {
+ border-radius: $osk_key_rounding;
+ background-color: $layer1;
+ color: $onLayer1;
+ padding: $osk_key_padding;
+ font-weight: 500;
+ font-size: $osk_key_fontsize;
+}
+
+.osk-key:hover,
+.osk-key:focus {
+ background-color: $layer1Hover;
+}
+
+.osk-key:active {
+ background-color: $layer1Active;
+ font-size: $osk_key_fontsize;
+}
+
+.osk-key-active {
+ background-color: $layer1Active;
+}
+
+.osk-key-normal {
+ min-width: $osk_key_width;
+ min-height: $osk_key_height;
+}
+
+.osk-key-fn {
+ min-width: $osk_key_width * 1.005;
+ min-height: calc($osk_key_height / 2); // dart-sass
+ // min-height: $osk_key_height / 2; // sassc
+}
+
+.osk-key-tab {
+ min-width: $osk_key_width * 1.6;
+ min-height: $osk_key_height;
+}
+
+.osk-key-caps {
+ min-width: $osk_key_width * 1.9;
+ min-height: $osk_key_height;
+}
+
+.osk-key-shift {
+ min-width: $osk_key_width * 2.5;
+ min-height: $osk_key_height;
+}
+
+.osk-key-control {
+ min-width: $osk_key_width * 1.3;
+ min-height: $osk_key_height;
+}
+
+.osk-control-button {
+ border-radius: $osk_key_rounding;
+ background-color: $layer1;
+ color: $onLayer1;
+ font-weight: 500;
+ font-size: $osk_key_fontsize;
+ padding: 0.682rem;
+}
+
+.osk-control-button:hover,
+.osk-control-button:focus {
+ background-color: $layer1Hover;
+}
+
+.osk-control-button:active {
+ background-color: $layer1Active;
+ font-size: $osk_key_fontsize;
+}
+
+.osk-key-empty, .osk-key-empty:hover, .osk-key-empty:focus {
+ min-width: $osk_key_width;
+ min-height: $osk_key_height;
+ background-color: transparent;
+}
diff --git a/modules/home-manager/ags/ags/scss/_overview.scss b/modules/home-manager/ags/ags/scss/_overview.scss
new file mode 100644
index 0000000..1ba52c3
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_overview.scss
@@ -0,0 +1,139 @@
+.overview-window {
+ margin-top: 2.727rem;
+}
+
+.overview-search-box {
+ @include element_decel;
+ @include large-rounding;
+ @include elevation-border;
+ @include elevation2;
+ min-width: 13.636rem;
+ min-height: 3.409rem;
+ padding: 0rem 1.364rem;
+ padding-right: 2.864rem;
+ background-color: $background;
+ color: $onBackground;
+
+ selection {
+ background-color: $secondary;
+ color: $onSecondary;
+ }
+
+ caret-color: transparent;
+}
+
+.overview-search-box-extended {
+ min-width: 25.909rem;
+ caret-color: $onSecondaryContainer;
+}
+
+.overview-search-prompt {
+ color: $subtext;
+}
+
+.overview-search-icon {
+ margin: 0rem 1.023rem;
+}
+
+.overview-search-prompt-box {
+ margin-left: -18.545rem;
+ margin-right: $elevation_margin + 0.068rem;
+}
+
+.overview-search-icon-box {
+ margin-left: -18.545rem;
+ margin-right: $elevation_margin + 0.068rem;
+}
+
+.overview-search-results {
+ // min-height: 2.813rem;
+ // min-height: 37.5rem;
+ @include large-rounding;
+ @include elevation-border;
+ @include elevation2;
+ min-width: 28.773rem;
+ padding: 0.682rem;
+ background-color: $layer0;
+ color: $onLayer0;
+}
+
+.overview-search-results-icon {
+ margin: 0rem 0.682rem;
+ font-size: 2.386rem;
+ min-width: 2.386rem;
+ min-height: 2.386rem;
+}
+
+.overview-search-results-txt {
+ margin-right: 0.682rem;
+}
+
+.overview-search-results-txt-cmd {
+ margin-right: 0.682rem;
+ @include techfont;
+ font-size: 1.227rem;
+}
+
+.overview-search-result-btn {
+ @include normal-rounding;
+ padding: 0.341rem;
+ min-width: 2.386rem;
+ min-height: 2.386rem;
+
+ caret-color: transparent;
+}
+
+.overview-search-result-btn:hover,
+.overview-search-result-btn:focus {
+ background-color: $layer2;
+}
+
+.overview-search-result-btn:active {
+ background-color: $layer2Hover;
+}
+
+.overview-tasks {
+ @include large-rounding;
+ @include elevation-border;
+ @include elevation2;
+ padding: 0.341rem;
+ background-color: $background;
+ color: $onBackground;
+}
+
+.overview-tasks-workspace {
+ @include normal-rounding;
+ // @include elevation-border;
+ margin: 0.341rem;
+ background-color: $layer1;
+}
+
+.overview-tasks-workspace-number {
+ @include mainfont;
+ color: $onSurfaceVariant;
+}
+
+.overview-tasks-window {
+ @include normal-rounding;
+ @include menu_decel;
+ background-color: transparentize($layer3, 0.2);
+ color: $onSurface;
+ border: 0.068rem solid $surfaceContainerHighest;
+}
+
+.overview-tasks-window:hover,
+.overview-tasks-window:focus {
+ background-color: transparentize($secondaryContainer, 0.3);
+}
+
+.overview-tasks-window:active {
+ background-color: transparentize($secondaryContainer, 0);
+}
+
+.overview-tasks-window-selected {
+ background-color: transparentize($secondaryContainer, 0.3);
+}
+
+.overview-tasks-window-dragging {
+ opacity: 0.2;
+}
diff --git a/modules/home-manager/ags/ags/scss/_session.scss b/modules/home-manager/ags/ags/scss/_session.scss
new file mode 100644
index 0000000..1639082
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_session.scss
@@ -0,0 +1,40 @@
+.session-bg {
+ background-color: transparentize($color: $layer0, $amount: 0.4);
+}
+
+.session-button {
+ @include large-rounding;
+ min-width: 8.182rem;
+ min-height: 8.182rem;
+ background-color: $layer1;
+ color: $onLayer1;
+ font-size: 3rem;
+}
+
+.session-button-focused {
+ background-color: $layer1Hover;
+}
+
+.session-button-desc {
+ background-color: $layer2;
+ color: $onLayer2;
+ border-bottom-left-radius: $rounding_large;
+ border-bottom-right-radius: $rounding_large;
+ padding: 0.205rem 0.341rem;
+ font-weight: 700;
+}
+
+.session-button-cancel {
+ @include large-rounding;
+ min-width: 8.182rem;
+ min-height: 5.455rem;
+ background-color: $layer1;
+ color: $onLayer1;
+ font-size: 3rem;
+}
+
+@for $i from 1 through 7 {
+ .session-color-#{$i} {
+ color: nth($sessionColors, $i);
+ }
+}
diff --git a/modules/home-manager/ags/ags/scss/_sidebars.scss b/modules/home-manager/ags/ags/scss/_sidebars.scss
new file mode 100644
index 0000000..599a7a3
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_sidebars.scss
@@ -0,0 +1,999 @@
+$sidebar_chat_textboxareaColor: mix($onSurfaceVariant, $surfaceVariant, 40%);
+
+@mixin group-padding {
+ padding: 0.341rem;
+}
+
+.sidebar-right {
+ @include menu_decel;
+ @include elevation-border;
+ @include elevation2;
+ border-radius: $rounding_large - $elevation_margin + 0.068rem;
+ min-width: 27.818rem;
+ background-color: $background;
+ padding: 1.023rem;
+}
+
+.sidebar-left {
+ @include menu_decel;
+ @include elevation-border;
+ @include elevation2;
+ border-radius: $rounding_large - $elevation_margin + 0.068rem;
+ min-width: 27.818rem;
+ background-color: $background;
+ padding: 1.023rem;
+}
+
+.sidebar-group {
+ @include normal-rounding;
+ @include group-padding;
+ background-color: $layer1;
+}
+
+.sidebar-group-nopad {
+ @include normal-rounding;
+ background-color: $layer1;
+}
+
+.sidebar-group-invisible {
+ @include group-padding;
+}
+
+.sidebar-group-invisible-morehorizpad {
+ padding: 0.341rem 0.682rem;
+}
+
+.sidebar-togglesbox {
+ @include full-rounding;
+ @include group-padding;
+ background-color: $layer1;
+}
+
+.sidebar-iconbutton {
+ @include full-rounding;
+ @include element_decel;
+ color: $onSurface;
+ min-width: 2.727rem;
+ min-height: 2.727rem;
+}
+
+.sidebar-iconbutton:hover,
+.sidebar-iconbutton:focus {
+ background-color: $layer1Hover;
+}
+
+.sidebar-iconbutton:active {
+ background-color: $layer1Active;
+}
+
+.sidebar-button-active {
+ background-color: $primary;
+ color: $onPrimary;
+}
+
+.sidebar-button-active:hover,
+.sidebar-button-active:focus {
+ background-color: mix($primary, $layer1Hover, 70%);
+}
+
+.sidebar-button-active:active {
+ background-color: mix($primary, $layer1Active, 40%);
+}
+
+.sidebar-buttons-separator {
+ min-width: 0.068rem;
+ min-height: 0.068rem;
+ background-color: $onSurfaceVariant;
+}
+
+.sidebar-navrail {
+ padding: 0rem $rounding_medium;
+}
+
+.sidebar-navrail-btn>box>label {
+ @include full-rounding;
+ @include menu_decel;
+}
+
+.sidebar-navrail-btn:hover>box>label:first-child,
+.sidebar-navrail-btn:focus>box>label:first-child {
+ background-color: $layer1Hover;
+}
+
+.sidebar-navrail-btn:active>box>label:first-child {
+ background-color: $layer1Active;
+}
+
+.sidebar-navrail-btn-active>box>label:first-child {
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+}
+
+.sidebar-navrail-btn-active:hover>box>label:first-child,
+.sidebar-navrail-btn-active:focus>box>label:first-child {
+ background-color: mix($secondaryContainer, $layer1Hover, 90%);
+ color: mix($onSecondaryContainer, $layer1Hover, 90%);
+}
+
+.sidebar-sysinfo-grouppad {
+ padding: 1.159rem;
+}
+
+.sidebar-memory-ram-circprog {
+ @include fluent_decel_long;
+ min-width: $rounding_small;
+ min-height: 4.091rem;
+ padding: 0.409rem;
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+ font-size: 0px;
+}
+
+.sidebar-memory-swap-circprog {
+ @include fluent_decel_long;
+ min-width: $rounding_small;
+ min-height: 2.255rem;
+ padding: 0.409rem;
+ margin: 0.918rem;
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+ font-size: 0px;
+}
+
+.sidebar-cpu-circprog {
+ min-width: $rounding_small;
+ min-height: 3.409rem;
+ padding: 0.409rem;
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+ @include fluent_decel_long;
+ font-size: 0px;
+}
+
+.sidebar-scrollbar {
+ trough {
+ @include full-rounding;
+ min-width: 0.545rem;
+ background-color: transparent;
+ }
+
+ slider {
+ @include full-rounding;
+ @include element_decel;
+ min-width: 0.273rem;
+ min-height: 2.045rem;
+ background-color: transparentize($onSurfaceVariant, 0.7);
+ }
+
+ slider:hover,
+ slider:focus {
+ background-color: transparentize($onSurfaceVariant, 0.6);
+ }
+
+ slider:active {
+ background-color: transparentize($onSurface, 0.5);
+ }
+}
+
+.sidebar-calendar-btn {
+ @include full-rounding;
+ @include element_decel;
+ min-height: 2.523rem;
+ min-width: 2.523rem;
+ color: $onSurface;
+}
+
+.sidebar-calendar-btn:hover,
+.sidebar-calendar-btn:focus {
+ background-color: $hovercolor;
+}
+
+.sidebar-calendar-btn:active {
+ background-color: $activecolor;
+}
+
+.sidebar-calendar-btn-txt {
+ margin-left: -10.341rem;
+ margin-right: -10.341rem;
+}
+
+.sidebar-calendar-btn-today {
+ background-color: $primary;
+ color: $onPrimary;
+}
+
+.sidebar-calendar-btn-today:hover,
+.sidebar-calendar-btn-today:focus {
+ background-color: mix($primary, $hovercolor, 70%);
+}
+
+.sidebar-calendar-btn-today:active {
+ background-color: mix($primary, $hovercolor, 40%);
+}
+
+.sidebar-calendar-btn-othermonth {
+ color: $outline;
+}
+
+.sidebar-calendar-header {
+ margin: 0.341rem;
+}
+
+.sidebar-calendar-monthyear-btn {
+ @include full-rounding;
+ @include element_decel;
+ padding: 0rem 0.682rem;
+ background-color: $layer2;
+ color: $onSurface;
+}
+
+.sidebar-calendar-monthyear-btn:hover,
+.sidebar-calendar-monthyear-btn:focus {
+ background-color: $hovercolor;
+}
+
+.sidebar-calendar-monthyear-btn:active {
+ background-color: $activecolor;
+}
+
+.sidebar-calendar-monthshift-btn {
+ @include full-rounding;
+ @include element_decel;
+ min-width: 2.045rem;
+ min-height: 2.045rem;
+ background-color: $layer2;
+ color: $outline;
+}
+
+.sidebar-calendar-monthshift-btn:hover {
+ background-color: $hovercolor;
+}
+
+.sidebar-calendar-monthshift-btn:active {
+ background-color: $activecolor;
+}
+
+.sidebar-todo-item {
+ @include small-rounding;
+ margin-right: 0.545rem;
+ // padding: 0.341rem;
+ background-color: $layer2;
+ color: $onLayer2;
+}
+
+.sidebar-todo-txt {
+ margin: 0.682rem;
+ margin-bottom: 0rem;
+}
+
+.sidebar-todo-actions {
+ margin: 0.341rem;
+ margin-top: 0rem;
+ margin-right: 0rem;
+}
+
+.sidebar-todo-item-action {
+ @include element_decel;
+ border-radius: 9999px;
+ min-width: 1.705rem;
+ min-height: 1.705rem;
+}
+
+.sidebar-todo-item-action:hover,
+.sidebar-todo-item-action:focus {
+ background-color: $layer2Hover;
+}
+
+.sidebar-todo-item-action:active {
+ background-color: $layer2Active;
+}
+
+.sidebar-todo-crosser {
+ transition: margin 200ms cubic-bezier(0.1, 1, 0, 1), background-color 0ms;
+ min-width: 0rem;
+}
+
+.sidebar-todo-crosser-crossed {
+ background-color: $onBackground;
+}
+
+.sidebar-todo-crosser-removed {
+ background-color: $error;
+}
+
+.sidebar-todo-new {
+ @include full-rounding;
+ @include element_decel;
+ background-color: $layer2;
+ color: $onLayer2;
+ margin: 0.341rem;
+ padding: 0.205rem 0.545rem;
+}
+
+.sidebar-todo-new,
+.sidebar-todo-new:focus {
+ color: $onSecondaryContainer;
+ background-color: $secondaryContainer;
+}
+
+.sidebar-todo-new:active {
+ color: $onPrimaryContainer;
+ background-color: $primaryContainer;
+}
+
+.sidebar-todo-add {
+ @include element_decel;
+ @include small-rounding;
+ min-width: 1.705rem;
+ min-height: 1.705rem;
+ color: $onSecondaryContainer;
+ border: 0.068rem solid $onSurface;
+}
+
+.sidebar-todo-add:hover,
+.sidebar-todo-add:focus {
+ background-color: $surfaceContainerHigh;
+}
+
+.sidebar-todo-add:active {
+ background-color: $surfaceContainerHighest;
+}
+
+.sidebar-todo-add-available {
+ @include element_decel;
+ @include small-rounding;
+ min-width: 1.705rem;
+ min-height: 1.705rem;
+ background-color: $primary;
+ color: $onPrimary;
+ border: 0.068rem solid $primary;
+}
+
+.sidebar-todo-add-available:hover,
+.sidebar-todo-add-available:focus {
+ background-color: mix($primary, $hovercolor, 70%);
+}
+
+.sidebar-todo-add-available:active {
+ background-color: mix($primary, $hovercolor, 40%);
+}
+
+.sidebar-todo-entry {
+ @include element_decel;
+ @include small-rounding;
+ background-color: $surfaceVariant;
+ color: $onSurfaceVariant;
+ caret-color: $onSurfaceVariant;
+ margin: 0rem 0.341rem;
+ min-height: 1.773rem;
+ min-width: 0rem;
+ padding: 0.205rem 0.682rem;
+ border: 0.068rem solid $outline;
+}
+
+.sidebar-todo-entry:focus {
+ border: 0.068rem solid $onSurfaceVariant;
+}
+
+.sidebar-module {
+ @include normal-rounding;
+ @include group-padding;
+ background-color: $layer1;
+ padding: 0.682rem;
+}
+
+.sidebar-module-btn-arrow {
+ @include full-rounding;
+ @include icon-material;
+ background-color: $layer2;
+ min-width: 1.705rem;
+ min-height: 1.705rem;
+
+ &:hover,
+ &:focus {
+ background-color: $layer2Hover;
+ }
+
+ &:active {
+ background-color: $layer2Active;
+ }
+}
+
+.sidebar-module-scripts-button {
+ @include full-rounding;
+ @include icon-material;
+ background-color: $layer1;
+ min-width: 1.705rem;
+ min-height: 1.705rem;
+
+ &:hover,
+ &:focus {
+ background-color: $layer1Hover;
+ }
+
+ &:active {
+ background-color: $layer1Active;
+ }
+}
+
+$colorpicker_rounding: 0.341rem;
+
+.sidebar-module-colorpicker-wrapper {
+ padding: 0.341rem;
+}
+
+.sidebar-module-colorpicker-cursorwrapper {
+ padding: 0.341rem 0.136rem;
+}
+
+.sidebar-module-colorpicker-hue {
+ min-height: 13.636rem;
+ min-width: 1.091rem;
+ border-radius: $colorpicker_rounding;
+}
+
+.sidebar-module-colorpicker-hue-cursor {
+ background-color: $onBackground;
+ border: 0.136rem solid $onBackground;
+ min-height: 0.136rem;
+ margin-top: -0.136rem;
+ border-radius: $colorpicker_rounding;
+}
+
+.sidebar-module-colorpicker-saturationandlightness-wrapper {
+ padding: 0.341rem;
+}
+
+.sidebar-module-colorpicker-saturationandlightness {
+ min-height: 13.636rem;
+ min-width: 13.636rem;
+ border-radius: $colorpicker_rounding;
+}
+
+.sidebar-module-colorpicker-saturationandlightness-cursorwrapper {
+ padding: 0.341rem;
+ margin-top: -0.409rem;
+ margin-left: -0.409rem;
+}
+
+.sidebar-module-colorpicker-saturationandlightness-cursor {
+ @include full-rounding;
+ border: 0.136rem solid white;
+ min-width: 0.682rem;
+ min-height: 0.682rem;
+ margin-top: -0.409rem;
+ margin-left: -0.409rem;
+}
+
+.sidebar-module-colorpicker-result-area {
+ padding: 0.341rem;
+}
+
+.sidebar-module-colorpicker-result-box {
+ border-radius: $colorpicker_rounding;
+ min-width: 2.045rem;
+ min-height: 0.682rem;
+ padding: 0.341rem;
+}
+
+.sidebar-icontabswitcher {
+ @include full-rounding;
+ @include group-padding;
+ background-color: $layer1;
+}
+
+.sidebar-chat-providerswitcher {
+ @include small-rounding;
+ padding: 0.477rem 0.682rem;
+ background-color: $surfaceContainerHigh;
+ color: $onSurfaceVariant;
+}
+
+.sidebar-chat-viewport {
+ @include element_decel;
+ padding: 0.682rem 0rem;
+}
+
+.sidebar-chat-textarea {
+ @include normal-rounding;
+ background-color: $layer1;
+ color: $onLayer1;
+ padding: 0.682rem;
+}
+
+.sidebar-chat-entry {
+ color: $onSurfaceVariant;
+ caret-color: $onSurfaceVariant;
+ min-height: 1.773rem;
+ min-width: 0rem;
+}
+
+.sidebar-chat-wrapper {
+ transition: 400ms cubic-bezier(0.1, 1, 0, 1);
+}
+
+.sidebar-chat-wrapper-extended {
+ min-height: 7.500rem;
+}
+
+.sidebar-chat-send {
+ @include element_decel;
+ min-width: 1.705rem;
+ min-height: 1.705rem;
+ border-radius: $rounding_medium - 0.681rem;
+}
+
+.sidebar-chat-send:hover,
+.sidebar-chat-send:focus {
+ background-color: $surfaceBright;
+}
+
+.sidebar-chat-send:active {
+ background-color: $surfaceVariant;
+}
+
+.sidebar-chat-send-available {
+ background-color: $primary;
+ color: $onPrimary;
+}
+
+.sidebar-chat-send-available:hover,
+.sidebar-chat-send-available:focus {
+ background-color: mix($primary, $hovercolor, 70%);
+}
+
+.sidebar-chat-send-available:active {
+ background-color: mix($primary, $hovercolor, 40%);
+}
+
+.sidebar-chat-messagearea {
+ margin: 0.341rem;
+}
+
+.sidebar-chat-message {
+ @include normal-rounding;
+ @include group-padding;
+ background-color: $layer1;
+}
+
+$skeleton-accent: mix($secondary, $onSecondary, 50%);
+
+@keyframes sidebar-chat-message-skeletonline-anim {
+ 0% {
+ background-position: 175% 0%;
+ }
+
+ 100% {
+ background-position: 50% 0%;
+ }
+}
+
+.sidebar-chat-message-skeletonline {
+ border-radius: $rounding_verysmall;
+ min-height: 1.364rem;
+ background-color: $layer2;
+}
+
+.sidebar-chat-message-skeletonline-offset0 {
+ background-repeat: no-repeat;
+ background: linear-gradient(to right, $layer3 0%, $skeleton-accent 25%, $layer3 50%, $layer3 100%);
+ background-size: 500% 500%;
+ animation: sidebar-chat-message-skeletonline-anim 2s linear;
+ animation-iteration-count: infinite;
+}
+
+.sidebar-chat-message-skeletonline-offset1 {
+ background-repeat: no-repeat;
+ background: linear-gradient(to right, $layer3 0%, $layer3 50%, $skeleton-accent 75%, $layer3 100%);
+ background-size: 500% 500%;
+ animation: sidebar-chat-message-skeletonline-anim 2s linear;
+ animation-iteration-count: infinite;
+}
+
+.sidebar-chat-message-skeletonline-offset2 {
+ margin-right: 5.795rem;
+ background-repeat: no-repeat;
+ background: linear-gradient(to right, $layer3 0%, $layer3 25%, $skeleton-accent 50%, $layer3 75%, $layer3 100%);
+ background-size: 500% 500%;
+ animation: sidebar-chat-message-skeletonline-anim 2s linear;
+ animation-iteration-count: infinite;
+}
+
+.sidebar-chat-indicator {
+ @include element_decel;
+ @include full-rounding;
+ min-width: 0.136rem;
+}
+
+.sidebar-chat-indicator-waifu {
+ @include element_decel;
+ @include full-rounding;
+ min-width: 0.136rem;
+ background-color: $onBackground;
+}
+
+.sidebar-chat-name {
+ @include titlefont;
+ @include small-rounding;
+ padding: 0.341rem 0.818rem;
+ margin: 0.341rem;
+ background-color: $layer2;
+ color: $onLayer2;
+}
+
+.sidebar-chat-name-user {
+ background-color: $layer2;
+ color: $onLayer2;
+}
+
+.sidebar-chat-name-bot {
+ background-color: $secondary;
+ color: $onSecondary;
+}
+
+.sidebar-chat-name-system {
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+}
+
+.sidebar-chat-txtblock {
+ margin-left: -0.136rem;
+ padding: 0.341rem;
+ padding-left: 0.818rem;
+}
+
+.sidebar-chat-txt {
+ @include readingfont;
+}
+
+.sidebar-chat-latex {
+ @include small-rounding;
+ margin: 0rem 0.682rem;
+ padding: 0.682rem;
+ color: $onBackground;
+}
+
+.sidebar-chat-codeblock {
+ @include normal-rounding;
+ background-color: $layer2;
+ color: $onLayer2;
+ margin: 0rem 0.682rem;
+}
+
+.sidebar-chat-codeblock-topbar {
+ @include mainfont;
+ background-color: $layer3;
+ color: $onLayer3;
+ border-top-left-radius: $rounding_small;
+ border-top-right-radius: $rounding_small;
+ padding: 0.341rem 0.477rem;
+}
+
+.sidebar-chat-codeblock-topbar-txt {
+ @include full-rounding;
+ padding: 0.273rem;
+}
+
+.sidebar-chat-codeblock-topbar-btn {
+ @include full-rounding;
+ @include element_decel;
+ padding: 0.273rem 0.477rem;
+}
+
+.sidebar-chat-codeblock-topbar-btn:hover,
+.sidebar-chat-codeblock-topbar-btn:focus {
+ background-color: $surfaceBright;
+}
+
+.sidebar-chat-codeblock-topbar-btn:active {
+ background-color: $surfaceVariant;
+}
+
+.sidebar-chat-codeblock-code {
+ @include techfont;
+ padding: 0.682rem;
+}
+
+.sidebar-chat-divider {
+ min-height: 1px;
+ background-color: $sidebar_chat_textboxareaColor;
+ margin: 0rem 0.545rem;
+}
+
+.sidebar-chat-welcome-txt {
+ margin: 0rem 3.409rem;
+}
+
+.sidebar-chat-settings-toggles {
+ margin: 0rem 5.455rem;
+}
+
+.sidebar-chat-welcome-icon {
+ @include full-rounding;
+ font-size: 4rem;
+}
+
+.sidebar-chat-welcome-logo {
+ @include full-rounding;
+ @include element_decel;
+ @include icon-material;
+ min-height: 4.773rem;
+ min-width: 4.773rem;
+ font-size: 3.076rem;
+ background-color: $secondaryContainer;
+ color: $onSecondaryContainer;
+}
+
+.sidebar-chat-chip {
+ @include element_decel;
+ @include small-rounding;
+ padding: 0.341rem 0.477rem;
+}
+
+.sidebar-chat-chip-action {
+ @include element_decel;
+ background-color: $layer2;
+ color: $onSurfaceVariant;
+}
+
+.sidebar-chat-chip-action:hover,
+.sidebar-chat-chip-action:focus {
+ background-color: $hovercolor;
+}
+
+.sidebar-chat-chip-action:active {
+ background-color: $activecolor;
+}
+
+.sidebar-chat-chip-action-active {
+ color: $sidebar_chat_textboxareaColor;
+ border: 0.068rem solid $sidebar_chat_textboxareaColor;
+}
+
+.sidebar-chat-chip-toggle {
+ @include element_decel;
+ @include small-rounding;
+ padding: 0.341rem 0.477rem;
+ background-color: $layer3;
+ color: $onSurfaceVariant;
+}
+
+.sidebar-chat-chip-toggle:focus,
+.sidebar-chat-chip-toggle:hover {
+ background-color: $hovercolor;
+}
+
+.sidebar-chat-chip-toggle:active {
+ background-color: $activecolor;
+}
+
+.sidebar-pin {
+ @include small-rounding;
+ @include element_decel;
+ min-height: 2.386rem;
+ min-width: 2.386rem;
+ color: $onSurface;
+}
+
+.sidebar-pin:hover,
+.sidebar-pin:focus {
+ background-color: $hovercolor;
+}
+
+.sidebar-pin:active {
+ background-color: $activecolor;
+}
+
+.sidebar-pin-enabled {
+ background-color: $primary;
+
+ label {
+ color: $onPrimary;
+ }
+}
+
+.sidebar-pin-enabled:hover,
+.sidebar-pin-enabled:focus {
+ background-color: mix($primary, $hovercolor, 70%);
+}
+
+.sidebar-pin-enabled:active {
+ background-color: mix($primary, $hovercolor, 40%);
+}
+
+.sidebar-waifu-heading {
+ @include titlefont;
+ padding: 0.341rem;
+ margin-left: -0.136rem;
+ padding-left: 0.818rem;
+}
+
+.sidebar-waifu-txt {
+ @include mainfont;
+}
+
+.sidebar-waifu-image {
+ @include normal-rounding;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+}
+
+.sidebar-waifu-image-actions {
+ @include element_decel;
+ padding: 0.313rem;
+}
+
+$waifu_image_overlay_transparency: 0.7;
+
+.sidebar-waifu-image-action {
+ @include full-rounding;
+ min-width: 1.875rem;
+ min-height: 1.875rem;
+ background-color: rgba(0, 0, 0, $waifu_image_overlay_transparency ); // Fixed cuz on image
+ color: rgba(255, 255, 255, $waifu_image_overlay_transparency);
+}
+
+.sidebar-waifu-image-action:hover,
+.sidebar-waifu-image-action:focus {
+ background-color: rgba(30, 30, 30, $waifu_image_overlay_transparency);
+}
+
+.sidebar-waifu-image-action:active {
+ background-color: rgba(60, 60, 60, $waifu_image_overlay_transparency);
+}
+
+.sidebar-booru-provider {
+ @include titlefont;
+ @include small-rounding;
+ padding: 0.341rem 0.818rem;
+ margin: 0.341rem;
+ font-weight: bold;
+ background-color: $primary;
+ color: $onPrimary;
+}
+
+.sidebar-booru-imagegrid {
+ @include normal-rounding;
+}
+
+.sidebar-booru-image {
+ @include small-rounding;
+ margin: 0.273rem;
+ min-width: 11.932rem;
+ // background-color: rgba(100, 200, 0, 0.3);
+}
+
+.sidebar-booru-image-drawingarea {
+ // background-color: rgba(200, 100, 0, 0.3);
+ @include small-rounding;
+ min-width: 12.273rem;
+ min-height: 12.273rem;
+}
+
+.sidebar-booru-image-actions {
+ // background-color: rgba(100, 100, 0, 0.3);
+ @include element_decel;
+ margin: 0.545rem;
+}
+
+.sidebar-volmixer-stream {
+ border-bottom: 0.068rem solid $outlineVariant;
+ padding: 0.682rem;
+ color: $onSurface;
+}
+
+.sidebar-volmixer-stream-appicon {
+ font-size: 3.273rem;
+}
+
+.sidebar-volmixer-stream-slider {
+ trough {
+ border-radius: $rounding_verysmall;
+ min-height: 1.364rem;
+ min-width: 1.364rem;
+ background-color: $secondaryContainer;
+ }
+
+ highlight {
+ border-radius: $rounding_verysmall;
+ min-height: 1.364rem;
+ min-width: 1.364rem;
+ background-color: $primary;
+ }
+
+ slider {
+ border-radius: $rounding_verysmall;
+ min-height: 1.364rem;
+ min-width: 1.364rem;
+ }
+}
+
+.sidebar-volmixer-status {
+ color: $onSurface;
+ margin: 0rem 0.682rem;
+}
+
+.sidebar-volmixer-deviceselector {
+ @include small-rounding;
+ padding: 0.477rem 0.682rem;
+ background-color: $surfaceContainerHigh;
+ color: $onSurfaceVariant;
+}
+
+.sidebar-bluetooth-device {
+ padding: 0.682rem;
+ @include normal-rounding;
+ background-color: $layer2;
+ color: $onLayer2;
+}
+
+.sidebar-bluetooth-appicon {
+ @include symbolic-icon;
+ font-size: 2.045rem;
+}
+
+.sidebar-bluetooth-device-remove {
+ @include full-rounding;
+ min-width: 2.045rem;
+ min-height: 2.045rem;
+ // background-color: $layer3;
+ padding: 0.341rem;
+}
+
+.sidebar-bluetooth-device-remove:hover,
+.sidebar-bluetooth-device-remove:focus {
+ @include full-rounding;
+ background-color: $layer2Hover;
+ padding: 0.341rem;
+}
+
+.sidebar-wifinetworks-network {
+ padding: 0.682rem;
+ @include normal-rounding;
+ background-color: $layer2;
+ color: $onLayer2;
+}
+
+.sidebar-wifinetworks-network:hover,
+.sidebar-wifinetworks-network:focus {
+ background-color: $layer2Hover;
+}
+
+.sidebar-wifinetworks-network:active {
+ background-color: $layer2Active;
+}
+
+.sidebar-wifinetworks-signal {
+ @include symbolic-icon;
+ font-size: 2.045rem;
+}
+
+.sidebar-wifinetworks-auth-entry {
+ @include small-rounding;
+ background-color: $layer1;
+ color: $onLayer1;
+ padding: 0.682rem;
+}
+
+.sidebar-centermodules-bottombar-button {
+ @include full-rounding;
+ @include element_decel;
+ min-width: 6.818rem;
+ min-height: 2.25rem;
+ background-color: $layer2;
+ color: $onLayer2;
+}
+
+.sidebar-centermodules-bottombar-button:hover,
+.sidebar-centermodules-bottombar-button:focus {
+ background-color: $layer2Hover;
+}
+
+.sidebar-centermodules-bottombar-button:active {
+ background-color: $layer2Active;
+}
+
+.sidebar-centermodules-scrollgradient-bottom {
+ background: linear-gradient(to top, $layer1 0%, transparentize($layer1, 1) 1.023rem);
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/scss/_wal.scss b/modules/home-manager/ags/ags/scss/_wal.scss
new file mode 100644
index 0000000..50ec8e5
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/_wal.scss
@@ -0,0 +1,8 @@
+// To prevent errors
+$color1: $surfaceVariant;
+$color2: $surfaceVariant;
+$color3: $surfaceVariant;
+$color4: $secondaryContainer;
+$color5: $secondaryContainer;
+$color6: $secondaryContainer;
+$color7: $onBackground;
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/scss/fallback/_material.scss b/modules/home-manager/ags/ags/scss/fallback/_material.scss
new file mode 100644
index 0000000..5d20fc1
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/fallback/_material.scss
@@ -0,0 +1,76 @@
+$darkmode: True;
+$transparent: False;
+$primary_paletteKeyColor: #FE00A9;
+$secondary_paletteKeyColor: #9B6A74;
+$tertiary_paletteKeyColor: #A86465;
+$neutral_paletteKeyColor: #857279;
+$neutral_variant_paletteKeyColor: #887179;
+$background: #1C1015;
+$onBackground: #F4DCE4;
+$surface: #1C1015;
+$surfaceDim: #1C1015;
+$surfaceBright: #44353B;
+$surfaceContainerLowest: #160B10;
+$surfaceContainerLow: #25181D;
+$surfaceContainer: #291C21;
+$surfaceContainerHigh: #34262C;
+$surfaceContainerHighest: #403137;
+$onSurface: #F4DCE4;
+$surfaceVariant: #554149;
+$onSurfaceVariant: #DAC0C9;
+$inverseSurface: #F4DCE4;
+$inverseOnSurface: #3B2C32;
+$outline: #A28A93;
+$outlineVariant: #554149;
+$shadow: #000000;
+$scrim: #000000;
+$surfaceTint: #FFAFD2;
+$primary: #FFAFD2;
+$onPrimary: #63003F;
+$primaryContainer: #8B005A;
+$onPrimaryContainer: #FFD8E7;
+$inversePrimary: #B60077;
+$secondary: #F1B7C3;
+$onSecondary: #4B252E;
+$secondaryContainer: #673D47;
+$onSecondaryContainer: #FFDBE1;
+$tertiary: #FFB3B3;
+$onTertiary: #532022;
+$tertiaryContainer: #C57D7E;
+$onTertiaryContainer: #000000;
+$error: #FFB4AB;
+$onError: #690005;
+$errorContainer: #93000A;
+$onErrorContainer: #FFDAD6;
+$primaryFixed: #FFD8E7;
+$primaryFixedDim: #FFAFD2;
+$onPrimaryFixed: #3D0025;
+$onPrimaryFixedVariant: #8B005A;
+$secondaryFixed: #FFD9E0;
+$secondaryFixedDim: #F1B7C3;
+$onSecondaryFixed: #32101A;
+$onSecondaryFixedVariant: #643B45;
+$tertiaryFixed: #FFDAD9;
+$tertiaryFixedDim: #FFB3B3;
+$onTertiaryFixed: #380B0F;
+$onTertiaryFixedVariant: #6F3637;
+$success: #B5CCBA;
+$onSuccess: #213528;
+$successContainer: #374B3E;
+$onSuccessContainer: #D1E9D6;
+$term0: #26161C;
+$term1: #FF4899;
+$term2: #FFBABC;
+$term3: #FFDEE1;
+$term4: #B3A3D5;
+$term5: #E790B7;
+$term6: #FFBA92;
+$term7: #EED1D3;
+$term8: #CBB4B6;
+$term9: #FFA6C3;
+$term10: #FFFBFF;
+$term11: #FFFFFF;
+$term12: #D5DAF9;
+$term13: #FFCCDF;
+$term14: #FFF9F8;
+$term15: #FFD8E7;
diff --git a/modules/home-manager/ags/ags/scss/main.scss b/modules/home-manager/ags/ags/scss/main.scss
new file mode 100644
index 0000000..57938e8
--- /dev/null
+++ b/modules/home-manager/ags/ags/scss/main.scss
@@ -0,0 +1,42 @@
+// Reset
+// * {
+// all: unset;
+// }
+*:not(popover) { all: unset; }
+
+// Colors
+@import 'material'; // Material colors
+@import './colors'; // Global color definitions. Uses material colors as base.
+@import './lib_mixins';
+@import 'lib_mixins_overrides';
+@import './lib_classes';
+@import './common'; // Context menu n stuff
+
+// Components
+@import './bar';
+@import './cheatsheet';
+@import './desktopbackground';
+@import './dock';
+@import './osd';
+@import './overview';
+@import './osk';
+@import './sidebars';
+@import './session';
+@import './notifications';
+
+// Music is put last as it might mess stuff up with pywal
+@import './music'; // Everything related to music is here
+
+// Classes for interaction
+.growingRadial {
+ transition: 300ms cubic-bezier(0.2, 0.0, 0, 1.0);
+}
+.fadingRadial {
+ transition: 50ms cubic-bezier(0.2, 0.0, 0, 1.0);
+}
+.sidebar-pinned {
+ margin: 0rem;
+ border-radius: 0rem;
+ border-bottom-right-radius: $rounding_large;
+ border: 0rem solid;
+}
diff --git a/modules/home-manager/ags/ags/services/booru.js b/modules/home-manager/ags/ags/services/booru.js
new file mode 100644
index 0000000..e620d8c
--- /dev/null
+++ b/modules/home-manager/ags/ags/services/booru.js
@@ -0,0 +1,156 @@
+import Service from 'resource:///com/github/Aylur/ags/service.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+
+const APISERVICES = {
+ 'yandere': {
+ name: 'yande.re',
+ endpoint: 'https://yande.re/post.json',
+ },
+ 'konachan': {
+ name: 'Konachan',
+ endpoint: 'https://konachan.net/post.json',
+ },
+}
+
+const getWorkingImageSauce = (url) => {
+ if (url.includes('pximg.net')) {
+ return `https://www.pixiv.net/en/artworks/${url.substring(url.lastIndexOf('/') + 1).replace(/_p\d+\.(png|jpg|jpeg|gif)$/, '')}`;
+ }
+ return url;
+}
+
+function paramStringFromObj(params) {
+ return Object.entries(params)
+ .map(([key, value]) => {
+ if (Array.isArray(value)) { // If it's an array, repeat
+ if (value.length == 0) return '';
+ let thisKey = `${encodeURIComponent(key)}=${encodeURIComponent(value[0])}`
+ for (let i = 1; i < value.length; i++) {
+ thisKey += `&${encodeURIComponent(key)}=${encodeURIComponent(value[i])}`;
+ }
+ return thisKey;
+ }
+ return `${key}=${value}`;
+ })
+ .join('&');
+}
+
+class BooruService extends Service {
+ _baseUrl = 'https://yande.re/post.json';
+ _mode = 'yandere';
+ _nsfw = userOptions.sidebar.image.allowNsfw;
+ _responses = [];
+ _queries = [];
+
+ static {
+ Service.register(this, {
+ 'initialized': [],
+ 'clear': [],
+ 'newResponse': ['int'],
+ 'updateResponse': ['int'],
+ }, {
+ 'nsfw': ['boolean'],
+ });
+ }
+
+ constructor() {
+ super();
+ this.emit('initialized');
+ }
+
+ clear() {
+ this._responses = [];
+ this._queries = [];
+ this.emit('clear');
+ }
+
+ get nsfw() { return this._nsfw }
+ set nsfw(value) { this._nsfw = value; this.notify('nsfw'); }
+
+ get mode() { return this._mode }
+ set mode(value) {
+ this._mode = value;
+ this._baseUrl = APISERVICES[this._mode].endpoint;
+ }
+ get providerName() {
+ return APISERVICES[this._mode].name;
+ }
+ get queries() { return this._queries }
+ get responses() { return this._responses }
+
+ async fetch(msg) {
+ // Init
+ const userArgs = `${msg}${(!this._nsfw || msg.includes('safe')) ? ' rating:safe' : ''}`.split(/\s+/);
+ console.log(userArgs)
+
+ let taglist = [];
+ let page = 1;
+ // Construct body/headers
+ for (let i = 0; i < userArgs.length; i++) {
+ const thisArg = userArgs[i].trim();
+ if (thisArg.length == 0 || thisArg == '.' || thisArg.includes('*')) continue;
+ else if (!isNaN(thisArg)) page = parseInt(thisArg);
+ else taglist.push(thisArg);
+ }
+ const newMessageId = this._queries.length;
+ this._queries.push({
+ providerName: APISERVICES[this._mode].name,
+ taglist: taglist.length == 0 ? ['*', `${page}`] : [...taglist, `${page}`],
+ realTagList: taglist,
+ page: page,
+ });
+ this.emit('newResponse', newMessageId);
+ const params = {
+ 'tags': taglist.join('+'),
+ 'page': `${page}`,
+ 'limit': `${userOptions.sidebar.image.batchCount}`,
+ };
+ const paramString = paramStringFromObj(params);
+ // Fetch
+ // Note: body isn't included since passing directly to url is more reliable
+ const options = {
+ method: 'GET',
+ headers: APISERVICES[this._mode].headers,
+ };
+ let status = 0;
+ // console.log(`${APISERVICES[this._mode].endpoint}?${paramString}`);
+
+ Utils.fetch(`${APISERVICES[this._mode].endpoint}?${paramString}`, options)
+ .then(result => {
+ status = result.status;
+ return result.text();
+ })
+ .then((dataString) => { // Store interesting stuff and emit
+ // console.log(dataString);
+ const parsedData = JSON.parse(dataString);
+ // console.log(parsedData)
+ this._responses[newMessageId] = parsedData.map(obj => {
+ return {
+ aspect_ratio: obj.width / obj.height,
+ id: obj.id,
+ tags: obj.tags,
+ rating: obj.rating,
+ is_nsfw: (obj.rating != 's'),
+ md5: obj.md5,
+ preview_url: obj.preview_url,
+ preview_width: obj.preview_width,
+ preview_height: obj.preview_height,
+ sample_url: obj.sample_url,
+ sample_width: obj.sample_width,
+ sample_height: obj.sample_height,
+ file_url: obj.file_url,
+ file_ext: obj.file_ext,
+ file_width: obj.file_width,
+ file_height: obj.file_height,
+ source: getWorkingImageSauce(obj.source),
+ }
+ });
+ this.emit('updateResponse', newMessageId);
+ })
+ .catch(print);
+
+ }
+}
+
+export default new BooruService();
+
diff --git a/modules/home-manager/ags/ags/services/brightness.js b/modules/home-manager/ags/ags/services/brightness.js
new file mode 100644
index 0000000..07c60bb
--- /dev/null
+++ b/modules/home-manager/ags/ags/services/brightness.js
@@ -0,0 +1,143 @@
+import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
+import Service from 'resource:///com/github/Aylur/ags/service.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { exec, execAsync } = Utils;
+
+import { clamp } from '../modules/.miscutils/mathfuncs.js';
+
+class BrightnessServiceBase extends Service {
+ static {
+ Service.register(
+ this,
+ { 'screen-changed': ['float'], },
+ { 'screen-value': ['float', 'rw'], },
+ );
+ }
+
+ _screenValue = 0;
+
+ // the getter has to be in snake_case
+ get screen_value() { return this._screenValue; }
+
+ // the setter has to be in snake_case too
+ set screen_value(percent) {
+ percent = clamp(percent, 0, 1);
+ this._screenValue = percent;
+
+ Utils.execAsync(this.setBrightnessCmd(percent))
+ .then(() => {
+ // signals has to be explicity emitted
+ this.emit('screen-changed', percent);
+ this.notify('screen-value');
+
+ // or use Service.changed(propName: string) which does the above two
+ // this.changed('screen');
+ })
+ .catch(print);
+ }
+
+ // overwriting connectWidget method, lets you
+ // change the default event that widgets connect to
+ connectWidget(widget, callback, event = 'screen-changed') {
+ super.connectWidget(widget, callback, event);
+ }
+}
+
+class BrightnessCtlService extends BrightnessServiceBase {
+ static {
+ Service.register(this);
+ }
+
+ constructor() {
+ super();
+ const current = Number(exec('brightnessctl g'));
+ const max = Number(exec('brightnessctl m'));
+ this._screenValue = current / max;
+ }
+
+ setBrightnessCmd(percent) {
+ return `brightnessctl s ${percent * 100}% -q`;
+ }
+}
+
+class BrightnessDdcService extends BrightnessServiceBase {
+ static {
+ Service.register(this);
+ }
+
+ constructor(busNum) {
+ super();
+ this._busNum = busNum;
+ Utils.execAsync(`ddcutil getvcp 10 --brief`)
+ .then((out) => {
+ // only the last line is useful
+ out = out.split('\n');
+ out = out[out.length - 1];
+
+ out = out.split(' ');
+ const current = Number(out[3]);
+ const max = Number(out[4]);
+ this._screenValue = current / max;
+ })
+ .catch(print);
+ }
+
+ setBrightnessCmd(percent) {
+ return `ddcutil setvcp 10 ${Math.round(percent * 100)}`;
+ }
+}
+
+async function listDdcMonitorsSnBus() {
+ let ddcSnBus = {};
+ try {
+ const out = await Utils.execAsync('ddcutil detect --brief');
+ const displays = out.split('\n\n');
+ displays.forEach(display => {
+ const reg = /^Display \d+/;
+ if (!reg.test(display))
+ return;
+ const lines = display.split('\n');
+ const sn = lines[3].split(':')[3];
+ const busNum = lines[1].split('/dev/i2c-')[1];
+ ddcSnBus[sn] = busNum;
+ });
+ } catch (err) {
+ print(err);
+ }
+ return ddcSnBus;
+}
+
+// Service instance
+const numMonitors = Hyprland.monitors.length;
+const service = Array(numMonitors);
+const ddcSnBus = await listDdcMonitorsSnBus();
+for (let i = 0; i < service.length; i++) {
+ const monitorName = Hyprland.monitors[i].name;
+ const monitorSn = Hyprland.monitors[i].serial;
+ const preferredController = userOptions.brightness.controllers[monitorName]
+ || userOptions.brightness.controllers.default || "auto";
+ if (preferredController) {
+ switch (preferredController) {
+ case "brightnessctl":
+ service[i] = new BrightnessCtlService();
+ break;
+ case "ddcutil":
+ service[i] = new BrightnessDdcService(ddcSnBus[monitorSn]);
+ break;
+ case "auto":
+ if (monitorSn in ddcSnBus && !!exec(`bash -c 'command -v ddcutil'`))
+ service[i] = new BrightnessDdcService(ddcSnBus[monitorSn]);
+ else
+ service[i] = new BrightnessCtlService();
+ break;
+ default:
+ throw new Error(`Unknown brightness controller ${preferredController}`);
+ }
+ }
+}
+
+// make it global for easy use with cli
+globalThis.brightness = service[0];
+
+// export to use in other modules
+export default service;
diff --git a/modules/home-manager/ags/ags/services/darkmode.js b/modules/home-manager/ags/ags/services/darkmode.js
new file mode 100644
index 0000000..b0f54be
--- /dev/null
+++ b/modules/home-manager/ags/ags/services/darkmode.js
@@ -0,0 +1,39 @@
+const { Gio, GLib } = imports.gi;
+import Service from 'resource:///com/github/Aylur/ags/service.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+import { darkMode } from '../modules/.miscutils/system.js';
+const { exec, execAsync } = Utils;
+
+const timeBefore = (time1, time2) => { // Arrays of [hour, minute]
+ if (time1[0] == time2[0]) return time1[1] < time2[1];
+ return time1[0] < time2[0];
+}
+
+const timeSame = (time1, time2) => // Arrays of [hour, minute]
+ (time1[0] == time2[0] && time1[1] == time2[1]);
+
+const timeBeforeOrSame = (time1, time2) => // Arrays of [hour, minute]
+ (timeBefore(time1, time2) || timeSame(time1, time2));
+
+const timeInRange = (time, rangeStart, rangeEnd) => { // Arrays of [hour, minute]
+ if (timeBefore(rangeStart, rangeEnd))
+ return (timeBeforeOrSame(rangeStart, time) && timeBeforeOrSame(time, rangeEnd))
+ else { // rangeEnd < rangeStart, meaning it ends the following day
+ rangeEnd[0] += 24;
+ if (timeBefore(time, rangeStart)) time[0] += 24;
+ return (timeBeforeOrSame(rangeStart, time) && timeBeforeOrSame(time, rangeEnd))
+ }
+
+}
+
+export async function startAutoDarkModeService() {
+ Utils.interval(userOptions.time.interval, () => {
+ if ((!userOptions.appearance.autoDarkMode.enabled)) return;
+ const fromTime = (userOptions.appearance.autoDarkMode.from).split(':').map(Number);
+ const toTime = (userOptions.appearance.autoDarkMode.to).split(':').map(Number);
+ if (fromTime == toTime) return;
+ const currentDateTime = GLib.DateTime.new_now_local();
+ const currentTime = [currentDateTime.get_hour(), currentDateTime.get_minute()];
+ darkMode.value = timeInRange(currentTime, fromTime, toTime);
+ })
+}
diff --git a/modules/home-manager/ags/ags/services/gemini.js b/modules/home-manager/ags/ags/services/gemini.js
new file mode 100644
index 0000000..1a4815b
--- /dev/null
+++ b/modules/home-manager/ags/ags/services/gemini.js
@@ -0,0 +1,328 @@
+import Service from 'resource:///com/github/Aylur/ags/service.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import Soup from 'gi://Soup?version=3.0';
+import { fileExists } from '../modules/.miscutils/files.js';
+
+const HISTORY_DIR = `${GLib.get_user_state_dir()}/ags/user/ai/chats/`;
+const HISTORY_FILENAME = `gemini.txt`;
+const HISTORY_PATH = HISTORY_DIR + HISTORY_FILENAME;
+const initMessages =
+ [
+ { role: "user", parts: [{ text: "You are an assistant on a sidebar of a Wayland Linux desktop. Please always use a casual tone when answering your questions, unless requested otherwise or making writing suggestions. These are the steps you should take to respond to the user's queries:\n1. If it's a writing- or grammar-related question or a sentence in quotation marks, Please point out errors and correct when necessary using underlines, and make the writing more natural where appropriate without making too major changes. If you're given a sentence in quotes but is grammatically correct, explain briefly concepts that are uncommon.\n2. If it's a question about system tasks, give a bash command in a code block with brief explanation.\n3. Otherwise, when asked to summarize information or explaining concepts, you are should use bullet points and headings. For mathematics expressions, you *have to* use LaTeX within a code block with the language set as \"latex\". \nNote: Use casual language, be short, while ensuring the factual correctness of your response. If you are unsure or don’t have enough information to provide a confident answer, simply say “I don’t know” or “I’m not sure.”. \nThanks!" }], },
+ { role: "model", parts: [{ text: "Got it!" }], },
+ { role: "user", parts: [{ text: "\"He rushed to where the event was supposed to be hold, he didn't know it got calceled\"" }], },
+ { role: "model", parts: [{ text: "## Grammar correction\nErrors:\n\"He rushed to where the event was supposed to be __hold____,__ he didn't know it got calceled\"\nCorrection + minor improvements:\n\"He rushed to the place where the event was supposed to be __held____, but__ he didn't know that it got calceled\"" }], },
+ { role: "user", parts: [{ text: "raise volume by 5%" }], },
+ { role: "model", parts: [{ text: "## Volume +5```bash\nwpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+\n```\nThis command uses the `wpctl` utility to adjust the volume of the default sink." }], }, { role: "user", parts: [{ text: "main advantages of the nixos operating system" }], },
+ { role: "model", parts: [{ text: "## NixOS advantages\n- **Reproducible**: A config working on one device will also work on another\n- **Declarative**: One config language to rule them all. Effortlessly share them with others.\n- **Reliable**: Per-program software versioning. Mitigates the impact of software breakage" }], },
+ { role: "user", parts: [{ text: "whats skeumorphism" }], },
+ { role: "model", parts: [{ text: "## Skeuomorphism\n- A design philosophy- From early days of interface designing- Tries to imitate real-life objects- It's in fact still used by Apple in their icons until today." }], },
+ { role: "user", parts: [{ text: "\"ignorance is bliss\"" }], },
+ { role: "model", parts: [{ text: "## \"Ignorance is bliss\"\n- A Latin proverb that means being unaware of something negative can be a source of happiness\n- Often used to justify avoiding difficult truths or responsibilities\n- Can also be interpreted as a warning against seeking knowledge that may bring pain or sorrow" }], },
+ { role: "user", parts: [{ text: "find the derivative of (x-438)/(x^2+23x-7)+x^x" }], },
+ { role: "model", parts: [{ text: "## Derivative\n```latex\n\\[\n\\frac{d}{dx}\\left(\\frac{x - 438}{x^2 + 23x - 7} + x^x\\right) = \\frac{-(x^2+23x-7)-(x-438)(2x+23)}{(x^2+23x-7)^2} + x^x(\\ln(x) + 1)\n\\]\n```" }], },
+ { role: "user", parts: [{ text: "write the double angle formulas" }], },
+ { role: "model", parts: [{ text: "## Double angle formulas\n```latex\n\\[\n\\sin(2\theta) = 2\\sin(\\theta)\\cos(\\theta)\n\\]\n\\\\\n\\[\n\\cos(2\\theta) = \\cos^2(\\theta) - \\sin^2(\\theta)\n\\]\n\\\\\n\\[\n\\tan(2\theta) = \\frac{2\\tan(\\theta)}{1 - \\tan^2(\\theta)}\n\\]\n```" }], },
+ ];
+
+
+if (!fileExists(`${GLib.get_user_config_dir()}/gemini_history.json`)) {
+ Utils.execAsync([`bash`, `-c`, `touch ${GLib.get_user_config_dir()}/gemini_history.json`]).catch(print);
+ Utils.writeFile('[ ]', `${GLib.get_user_config_dir()}/gemini_history.json`).catch(print);
+}
+
+Utils.exec(`mkdir -p ${GLib.get_user_state_dir()}/ags/user/ai`);
+const KEY_FILE_LOCATION = `${GLib.get_user_state_dir()}/ags/user/ai/google_key.txt`;
+const APIDOM_FILE_LOCATION = `${GLib.get_user_state_dir()}/ags/user/ai/google_api_dom.txt`;
+function replaceapidom(URL) {
+ if (fileExists(APIDOM_FILE_LOCATION)) {
+ var contents = Utils.readFile(APIDOM_FILE_LOCATION).trim();
+ var URL = URL.toString().replace("generativelanguage.googleapis.com", contents);
+ }
+ return URL;
+}
+const CHAT_MODELS = ["gemini-1.5-flash"]
+const ONE_CYCLE_COUNT = 3;
+
+class GeminiMessage extends Service {
+ static {
+ Service.register(this,
+ {
+ 'delta': ['string'],
+ },
+ {
+ 'content': ['string'],
+ 'thinking': ['boolean'],
+ 'done': ['boolean'],
+ });
+ }
+
+ _role = '';
+ _parts = [{ text: '' }];
+ _thinking;
+ _done = false;
+ _rawData = '';
+
+ constructor(role, content, thinking = true, done = false) {
+ super();
+ this._role = role;
+ this._parts = [{ text: content }];
+ this._thinking = thinking;
+ this._done = done;
+ }
+
+ get rawData() { return this._rawData }
+ set rawData(value) { this._rawData = value }
+
+ get done() { return this._done }
+ set done(isDone) { this._done = isDone; this.notify('done') }
+
+ get role() { return this._role }
+ set role(role) { this._role = role; this.emit('changed') }
+
+ get content() {
+ return this._parts.map(part => part.text).join();
+ }
+ set content(content) {
+ this._parts = [{ text: content }];
+ this.notify('content')
+ this.emit('changed')
+ }
+
+ get parts() { return this._parts }
+
+ get label() { return this._parserState.parsed + this._parserState.stack.join('') }
+
+ get thinking() { return this._thinking }
+ set thinking(value) {
+ this._thinking = value;
+ this.notify('thinking')
+ this.emit('changed')
+ }
+
+ addDelta(delta) {
+ if (this.thinking) {
+ this.thinking = false;
+ this.content = delta;
+ }
+ else {
+ this.content += delta;
+ }
+ this.emit('delta', delta);
+ }
+
+ parseSection() {
+ if (this._thinking) {
+ this.thinking = false;
+ this._parts[0].text = '';
+ }
+ const parsedData = JSON.parse(this._rawData);
+ if (!parsedData.candidates)
+ this._parts[0].text += `Blocked: ${parsedData.promptFeedback.blockReason}`;
+ else {
+ const delta = parsedData.candidates[0].content.parts[0].text;
+ this._parts[0].text += delta;
+ }
+ // this.emit('delta', delta);
+ this.notify('content');
+ this._rawData = '';
+ }
+}
+
+class GeminiService extends Service {
+ static {
+ Service.register(this, {
+ 'initialized': [],
+ 'clear': [],
+ 'newMsg': ['int'],
+ 'hasKey': ['boolean'],
+ });
+ }
+
+ _assistantPrompt = userOptions.ai.enhancements;
+ _cycleModels = true;
+ _usingHistory = userOptions.ai.useHistory;
+ _key = '';
+ _requestCount = 0;
+ _safe = userOptions.ai.safety;
+ _temperature = userOptions.ai.defaultTemperature;
+ _messages = [];
+ _modelIndex = 0;
+ _decoder = new TextDecoder();
+
+ constructor() {
+ super();
+
+ if (fileExists(KEY_FILE_LOCATION)) this._key = Utils.readFile(KEY_FILE_LOCATION).trim();
+ else this.emit('hasKey', false);
+
+ // if (this._usingHistory) Utils.timeout(1000, () => this.loadHistory());
+ if (this._usingHistory) this.loadHistory();
+ else this._messages = this._assistantPrompt ? [...initMessages] : [];
+
+ this.emit('initialized');
+ }
+
+ get modelName() { return CHAT_MODELS[this._modelIndex] }
+
+ get keyPath() { return KEY_FILE_LOCATION }
+ get key() { return this._key }
+ set key(keyValue) {
+ this._key = keyValue;
+ Utils.writeFile(this._key, KEY_FILE_LOCATION)
+ .then(this.emit('hasKey', true))
+ .catch(print);
+ }
+
+ get cycleModels() { return this._cycleModels }
+ set cycleModels(value) {
+ this._cycleModels = value;
+ if (!value) this._modelIndex = 0;
+ else {
+ this._modelIndex = (this._requestCount - (this._requestCount % ONE_CYCLE_COUNT)) % CHAT_MODELS.length;
+ }
+ }
+
+ get useHistory() { return this._usingHistory; }
+ set useHistory(value) {
+ if (value && !this._usingHistory) this.loadHistory();
+ this._usingHistory = value;
+ }
+
+ get safe() { return this._safe }
+ set safe(value) { this._safe = value; }
+
+ get temperature() { return this._temperature }
+ set temperature(value) { this._temperature = value; }
+
+ get messages() { return this._messages }
+ get lastMessage() { return this._messages[this._messages.length - 1] }
+
+ saveHistory() {
+ Utils.exec(`bash -c 'mkdir -p ${HISTORY_DIR} && touch ${HISTORY_PATH}'`);
+ Utils.writeFile(JSON.stringify(this._messages.map(msg => {
+ let m = { role: msg.role, parts: msg.parts }; return m;
+ })), HISTORY_PATH);
+ }
+
+ loadHistory() {
+ this._messages = [];
+ this.appendHistory();
+ this._usingHistory = true;
+ }
+
+ appendHistory() {
+ if (fileExists(HISTORY_PATH)) {
+ const readfile = Utils.readFile(HISTORY_PATH);
+ JSON.parse(readfile).forEach(element => {
+ // this._messages.push(element);
+ this.addMessage(element.role, element.parts[0].text);
+ });
+ // console.log(this._messages)
+ // this._messages = this._messages.concat(JSON.parse(readfile));
+ // for (let index = 0; index < this._messages.length; index++) {
+ // this.emit('newMsg', index);
+ // }
+ }
+ else {
+ this._messages = this._assistantPrompt ? [...initMessages] : []
+ }
+ }
+
+ clear() {
+ this._messages = this._assistantPrompt ? [...initMessages] : [];
+ if (this._usingHistory) this.saveHistory();
+ this.emit('clear');
+ }
+
+ get assistantPrompt() { return this._assistantPrompt; }
+ set assistantPrompt(value) {
+ this._assistantPrompt = value;
+ if (value) this._messages = [...initMessages];
+ else this._messages = [];
+ }
+
+ readResponse(stream, aiResponse) {
+ stream.read_line_async(
+ 0, null,
+ (stream, res) => {
+ try {
+ const [bytes] = stream.read_line_finish(res);
+ const line = this._decoder.decode(bytes);
+ // console.log(line);
+ if (line == '[{') { // beginning of response
+ aiResponse._rawData += '{';
+ this.thinking = false;
+ }
+ else if (line == ',\u000d' || line == ']') { // end of stream pulse
+ aiResponse.parseSection();
+ }
+ else // Normal content
+ aiResponse._rawData += line;
+
+ this.readResponse(stream, aiResponse);
+ } catch {
+ aiResponse.done = true;
+ if (this._usingHistory) this.saveHistory();
+ return;
+ }
+ });
+ }
+
+ addMessage(role, message) {
+ this._messages.push(new GeminiMessage(role, message, false));
+ this.emit('newMsg', this._messages.length - 1);
+ }
+
+ send(msg) {
+ this._messages.push(new GeminiMessage('user', msg, false));
+ this.emit('newMsg', this._messages.length - 1);
+ const aiResponse = new GeminiMessage('model', 'thinking...', true, false)
+
+ const body =
+ {
+ "contents": this._messages.map(msg => { let m = { role: msg.role, parts: msg.parts }; return m; }),
+ "safetySettings": this._safe ? [] : [
+ // { category: "HARM_CATEGORY_DEROGATORY", threshold: "BLOCK_NONE", },
+ { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE", },
+ { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE", },
+ { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE", },
+ // { category: "HARM_CATEGORY_UNSPECIFIED", threshold: "BLOCK_NONE", },
+ ],
+ "generationConfig": {
+ "temperature": this._temperature,
+ },
+ // "key": this._key,
+ // "apiKey": this._key,
+ };
+ const proxyResolver = new Gio.SimpleProxyResolver({ 'default-proxy': userOptions.ai.proxyUrl });
+ const session = new Soup.Session({ 'proxy-resolver': proxyResolver });
+ const message = new Soup.Message({
+ method: 'POST',
+ uri: GLib.Uri.parse(replaceapidom(`https://generativelanguage.googleapis.com/v1/models/${this.modelName}:streamGenerateContent?key=${this._key}`), GLib.UriFlags.NONE),
+ });
+ message.request_headers.append('Content-Type', `application/json`);
+ message.set_request_body_from_bytes('application/json', new GLib.Bytes(JSON.stringify(body)));
+
+ session.send_async(message, GLib.DEFAULT_PRIORITY, null, (_, result) => {
+ const stream = session.send_finish(result);
+ this.readResponse(new Gio.DataInputStream({
+ close_base_stream: true,
+ base_stream: stream
+ }), aiResponse);
+ });
+ this._messages.push(aiResponse);
+ this.emit('newMsg', this._messages.length - 1);
+
+ if (this._cycleModels) {
+ this._requestCount++;
+ if (this._cycleModels)
+ this._modelIndex = (this._requestCount - (this._requestCount % ONE_CYCLE_COUNT)) % CHAT_MODELS.length;
+ }
+ }
+}
+
+export default new GeminiService();
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/services/gpt.js b/modules/home-manager/ags/ags/services/gpt.js
new file mode 100644
index 0000000..0accc89
--- /dev/null
+++ b/modules/home-manager/ags/ags/services/gpt.js
@@ -0,0 +1,253 @@
+import Service from 'resource:///com/github/Aylur/ags/service.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import Soup from 'gi://Soup?version=3.0';
+import { fileExists } from '../modules/.miscutils/files.js';
+
+const PROVIDERS = { // There's this list hmm https://github.com/zukixa/cool-ai-stuff/
+ 'ollama': {
+ 'name': 'Ollama (Llama 3)',
+ 'logo_name': 'ollama-symbolic',
+ 'description': 'Official Ollama API.\nPricing: Free.',
+ 'base_url': 'http://localhost:11434/v1/chat/completions',
+ 'key_get_url': 'it\'s just ollama',
+ 'key_file': 'ollama_key.txt',
+ 'model': 'llama3:instruct',
+ },
+ 'openai': {
+ 'name': 'OpenAI',
+ 'logo_name': 'openai-symbolic',
+ 'description': 'Official OpenAI API.\nPricing: Free for the first $5 or 3 months, whichever is less.',
+ 'base_url': 'https://api.openai.com/v1/chat/completions',
+ 'key_get_url': 'https://platform.openai.com/api-keys',
+ 'key_file': 'openai_key.txt',
+ 'model': 'gpt-3.5-turbo',
+ },
+
+}
+
+// Custom prompt
+const initMessages =
+ [
+ { role: "user", content: "You are an assistant on a sidebar of a Wayland Linux desktop. Please always use a casual tone when answering your questions, unless requested otherwise or making writing suggestions. These are the steps you should take to respond to the user's queries:\n1. If it's a writing- or grammar-related question or a sentence in quotation marks, Please point out errors and correct when necessary using underlines, and make the writing more natural where appropriate without making too major changes. If you're given a sentence in quotes but is grammatically correct, explain briefly concepts that are uncommon.\n2. If it's a question about system tasks, give a bash command in a code block with brief explanation.\n3. Otherwise, when asked to summarize information or explaining concepts, you are should use bullet points and headings. For mathematics expressions, you *have to* use LaTeX within a code block with the language set as \"latex\". \nNote: Use casual language, be short, while ensuring the factual correctness of your response. If you are unsure or don’t have enough information to provide a confident answer, simply say “I don’t know” or “I’m not sure.”. \nThanks!", },
+ { role: "assistant", content: "- Got it!", },
+ { role: "user", content: "\"He rushed to where the event was supposed to be hold, he didn't know it got calceled\"", },
+ { role: "assistant", content: "## Grammar correction\nErrors:\n\"He rushed to where the event was supposed to be __hold____,__ he didn't know it got calceled\"\nCorrection + minor improvements:\n\"He rushed to the place where the event was supposed to be __held____, but__ he didn't know that it got calceled\"", },
+ { role: "user", content: "raise volume by 5%", },
+ { role: "assistant", content: "## Volume +5```bash\nwpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+\n```\nThis command uses the `wpctl` utility to adjust the volume of the default sink.", },
+ { role: "user", content: "main advantages of the nixos operating system", },
+ { role: "assistant", content: "## NixOS advantages\n- **Reproducible**: A config working on one device will also work on another\n- **Declarative**: One config language to rule them all. Effortlessly share them with others.\n- **Reliable**: Per-program software versioning. Mitigates the impact of software breakage", },
+ { role: "user", content: "whats skeumorphism", },
+ { role: "assistant", content: "## Skeuomorphism\n- A design philosophy- From early days of interface designing- Tries to imitate real-life objects- It's in fact still used by Apple in their icons until today.", },
+ ];
+
+Utils.exec(`mkdir -p ${GLib.get_user_state_dir()}/ags/user/ai`);
+
+class GPTMessage extends Service {
+ static {
+ Service.register(this,
+ {
+ 'delta': ['string'],
+ },
+ {
+ 'content': ['string'],
+ 'thinking': ['boolean'],
+ 'done': ['boolean'],
+ });
+ }
+
+ _role = '';
+ _content = '';
+ _thinking;
+ _done = false;
+
+ constructor(role, content, thinking = true, done = false) {
+ super();
+ this._role = role;
+ this._content = content;
+ this._thinking = thinking;
+ this._done = done;
+ }
+
+ get done() { return this._done }
+ set done(isDone) { this._done = isDone; this.notify('done') }
+
+ get role() { return this._role }
+ set role(role) { this._role = role; this.emit('changed') }
+
+ get content() { return this._content }
+ set content(content) {
+ this._content = content;
+ this.notify('content')
+ this.emit('changed')
+ }
+
+ get label() { return this._parserState.parsed + this._parserState.stack.join('') }
+
+ get thinking() { return this._thinking }
+ set thinking(value) {
+ this._thinking = value;
+ this.notify('thinking')
+ this.emit('changed')
+ }
+
+ addDelta(delta) {
+ if (this.thinking) {
+ this.thinking = false;
+ this.content = delta;
+ }
+ else {
+ this.content += delta;
+ }
+ this.emit('delta', delta);
+ }
+}
+
+class GPTService extends Service {
+ static {
+ Service.register(this, {
+ 'initialized': [],
+ 'clear': [],
+ 'newMsg': ['int'],
+ 'hasKey': ['boolean'],
+ 'providerChanged': [],
+ });
+ }
+
+ _assistantPrompt = true;
+ _currentProvider = userOptions.ai.defaultGPTProvider;
+ _requestCount = 0;
+ _temperature = userOptions.ai.defaultTemperature;
+ _messages = [];
+ _key = '';
+ _key_file_location = `${GLib.get_user_state_dir()}/ags/user/ai/${PROVIDERS[this._currentProvider]['key_file']}`;
+ _url = GLib.Uri.parse(PROVIDERS[this._currentProvider]['base_url'], GLib.UriFlags.NONE);
+
+ _decoder = new TextDecoder();
+
+ _initChecks() {
+ this._key_file_location = `${GLib.get_user_state_dir()}/ags/user/ai/${PROVIDERS[this._currentProvider]['key_file']}`;
+ if (fileExists(this._key_file_location)) this._key = Utils.readFile(this._key_file_location).trim();
+ else this.emit('hasKey', false);
+ this._url = GLib.Uri.parse(PROVIDERS[this._currentProvider]['base_url'], GLib.UriFlags.NONE);
+ }
+
+ constructor() {
+ super();
+ this._initChecks();
+
+ if (this._assistantPrompt) this._messages = [...initMessages];
+ else this._messages = [];
+
+ this.emit('initialized');
+ }
+
+ get modelName() { return PROVIDERS[this._currentProvider]['model'] }
+ get getKeyUrl() { return PROVIDERS[this._currentProvider]['key_get_url'] }
+ get providerID() { return this._currentProvider }
+ set providerID(value) {
+ this._currentProvider = value;
+ this.emit('providerChanged');
+ this._initChecks();
+ }
+ get providers() { return PROVIDERS }
+
+ get keyPath() { return this._key_file_location }
+ get key() { return this._key }
+ set key(keyValue) {
+ this._key = keyValue;
+ Utils.writeFile(this._key, this._key_file_location)
+ .then(this.emit('hasKey', true))
+ .catch(print);
+ }
+
+ get temperature() { return this._temperature }
+ set temperature(value) { this._temperature = value; }
+
+ get messages() { return this._messages }
+ get lastMessage() { return this._messages[this._messages.length - 1] }
+
+ clear() {
+ if (this._assistantPrompt)
+ this._messages = [...initMessages];
+ else
+ this._messages = [];
+ this.emit('clear');
+ }
+
+ get assistantPrompt() { return this._assistantPrompt; }
+ set assistantPrompt(value) {
+ this._assistantPrompt = value;
+ if (value) this._messages = [...initMessages];
+ else this._messages = [];
+ }
+
+ readResponse(stream, aiResponse) {
+ aiResponse.thinking = false;
+ stream.read_line_async(
+ 0, null,
+ (stream, res) => {
+ if (!stream) return;
+ const [bytes] = stream.read_line_finish(res);
+ const line = this._decoder.decode(bytes);
+ if (line && line != '') {
+ let data = line.substr(6);
+ if (data == '[DONE]') return;
+ try {
+ const result = JSON.parse(data);
+ if (result.choices[0].finish_reason === 'stop') {
+ aiResponse.done = true;
+ return;
+ }
+ aiResponse.addDelta(result.choices[0].delta.content);
+ // print(result.choices[0])
+ }
+ catch {
+ aiResponse.addDelta(line + '\n');
+ }
+ }
+ this.readResponse(stream, aiResponse);
+ });
+ }
+
+ addMessage(role, message) {
+ this._messages.push(new GPTMessage(role, message));
+ this.emit('newMsg', this._messages.length - 1);
+ }
+
+ send(msg) {
+ this._messages.push(new GPTMessage('user', msg, false, true));
+ this.emit('newMsg', this._messages.length - 1);
+ const aiResponse = new GPTMessage('assistant', '', true, false)
+
+ const body = {
+ model: PROVIDERS[this._currentProvider]['model'],
+ messages: this._messages.map(msg => { let m = { role: msg.role, content: msg.content }; return m; }),
+ temperature: this._temperature,
+ // temperature: 2, // <- Nuts
+ stream: true,
+ };
+ const proxyResolver = new Gio.SimpleProxyResolver({ 'default-proxy': userOptions.ai.proxyUrl });
+ const session = new Soup.Session({ 'proxy-resolver': proxyResolver });
+ const message = new Soup.Message({
+ method: 'POST',
+ uri: this._url,
+ });
+ message.request_headers.append('Authorization', `Bearer ${this._key}`);
+ message.set_request_body_from_bytes('application/json', new GLib.Bytes(JSON.stringify(body)));
+
+ session.send_async(message, GLib.DEFAULT_PRIORITY, null, (_, result) => {
+ const stream = session.send_finish(result);
+ this.readResponse(new Gio.DataInputStream({
+ close_base_stream: true,
+ base_stream: stream
+ }), aiResponse);
+ });
+ this._messages.push(aiResponse);
+ this.emit('newMsg', this._messages.length - 1);
+ }
+}
+
+export default new GPTService();
diff --git a/modules/home-manager/ags/ags/services/indicator.js b/modules/home-manager/ags/ags/services/indicator.js
new file mode 100644
index 0000000..dfe141e
--- /dev/null
+++ b/modules/home-manager/ags/ags/services/indicator.js
@@ -0,0 +1,38 @@
+import Service from 'resource:///com/github/Aylur/ags/service.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+
+class IndicatorService extends Service {
+ static {
+ Service.register(
+ this,
+ { 'popup': ['double'], },
+ );
+ }
+
+ _delay = 1500;
+ _count = 0;
+
+ popup(value) {
+ this.emit('popup', value);
+ this._count++;
+ Utils.timeout(this._delay, () => {
+ this._count--;
+
+ if (this._count === 0)
+ this.emit('popup', -1);
+ });
+ }
+
+ connectWidget(widget, callback) {
+ connect(this, widget, callback, 'popup');
+ }
+}
+
+// the singleton instance
+const service = new IndicatorService();
+
+// make it global for easy use with cli
+globalThis['indicator'] = service;
+
+// export to use in other modules
+export default service;
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/services/messages.js b/modules/home-manager/ags/ags/services/messages.js
new file mode 100644
index 0000000..fb94c7b
--- /dev/null
+++ b/modules/home-manager/ags/ags/services/messages.js
@@ -0,0 +1,61 @@
+const { Notify, GLib, Gio } = imports.gi;
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+import Battery from 'resource:///com/github/Aylur/ags/service/battery.js';
+
+export function fileExists(filePath) {
+ let file = Gio.File.new_for_path(filePath);
+ return file.query_exists(null);
+}
+
+const FIRST_RUN_FILE = "firstrun.txt";
+const FIRST_RUN_PATH = `${GLib.get_user_state_dir()}/ags/user/${FIRST_RUN_FILE}`;
+const FIRST_RUN_FILE_CONTENT = "Just a file to confirm that you have been greeted ;)";
+const APP_NAME = "illogical-impulse";
+const FIRST_RUN_NOTIF_TITLE = "Welcome!";
+const FIRST_RUN_NOTIF_BODY = `First run? 👀 CTRL+SUPER+T to pick a wallpaper (or styles will break!)\nFor a list of keybinds, hit Super + /.`;
+
+var batteryWarned = false;
+async function batteryMessage() {
+ const perc = Battery.percent;
+ const charging = Battery.charging;
+ if (charging) {
+ batteryWarned = false;
+ return;
+ }
+ for (let i = userOptions.battery.warnLevels.length - 1; i >= 0; i--) {
+ if (perc <= userOptions.battery.warnLevels[i] && !charging && !batteryWarned) {
+ batteryWarned = true;
+ Utils.execAsync(['bash', '-c',
+ `notify-send "${userOptions.battery.warnTitles[i]}" "${userOptions.battery.warnMessages[i]}" -u critical -a '${APP_NAME}' -t 69420 &`
+ ]).catch(print);
+ break;
+ }
+ }
+ if (perc <= userOptions.battery.suspendThreshold) {
+ Utils.execAsync(['bash', '-c',
+ `notify-send "Suspending system" "Critical battery level (${perc}% remaining)" -u critical -a '${APP_NAME}' -t 69420 &`
+ ]).catch(print);
+ Utils.execAsync('systemctl suspend').catch(print);
+ }
+}
+
+export async function startBatteryWarningService() {
+ Utils.timeout(1, () => {
+ Battery.connect('changed', () => batteryMessage().catch(print));
+ })
+}
+
+export async function firstRunWelcome() {
+ GLib.mkdir_with_parents(`${GLib.get_user_state_dir()}/ags/user`, 755);
+ if (!fileExists(FIRST_RUN_PATH)) {
+ Utils.writeFile(FIRST_RUN_FILE_CONTENT, FIRST_RUN_PATH)
+ .then(() => {
+ // Note that we add a little delay to make sure the cool circular progress works
+ Utils.execAsync(['hyprctl', 'keyword', 'bind', "Super,Slash,exec,ags -t cheatsheet"]).catch(print);
+ Utils.execAsync(['bash', '-c',
+ `sleep 0.5; notify-send "Millis since epoch" "$(date +%s%N | cut -b1-13)"; sleep 0.5; notify-send '${FIRST_RUN_NOTIF_TITLE}' '${FIRST_RUN_NOTIF_BODY}' -a '${APP_NAME}' &`
+ ]).catch(print)
+ })
+ .catch(print);
+ }
+}
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/services/todo.js b/modules/home-manager/ags/ags/services/todo.js
new file mode 100644
index 0000000..d5384f3
--- /dev/null
+++ b/modules/home-manager/ags/ags/services/todo.js
@@ -0,0 +1,83 @@
+const { Gio, GLib } = imports.gi;
+import Service from 'resource:///com/github/Aylur/ags/service.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { exec, execAsync } = Utils;
+
+class TodoService extends Service {
+ static {
+ Service.register(
+ this,
+ { 'updated': [], },
+ );
+ }
+
+ _todoPath = '';
+ _todoJson = [];
+
+ refresh(value) {
+ this.emit('updated', value);
+ }
+
+ connectWidget(widget, callback) {
+ this.connect(widget, callback, 'updated');
+ }
+
+ get todo_json() {
+ return this._todoJson;
+ }
+
+ _save() {
+ Utils.writeFile(JSON.stringify(this._todoJson), this._todoPath)
+ .catch(print);
+ }
+
+ add(content) {
+ this._todoJson.push({ content, done: false });
+ this._save();
+ this.emit('updated');
+ }
+
+ check(index) {
+ this._todoJson[index].done = true;
+ this._save();
+ this.emit('updated');
+ }
+
+ uncheck(index) {
+ this._todoJson[index].done = false;
+ this._save();
+ this.emit('updated');
+ }
+
+ remove(index) {
+ this._todoJson.splice(index, 1);
+ Utils.writeFile(JSON.stringify(this._todoJson), this._todoPath)
+ .catch(print);
+ this.emit('updated');
+ }
+
+ constructor() {
+ super();
+ this._todoPath = `${GLib.get_user_state_dir()}/ags/user/todo.json`;
+ try {
+ const fileContents = Utils.readFile(this._todoPath);
+ this._todoJson = JSON.parse(fileContents);
+ }
+ catch {
+ Utils.exec(`bash -c 'mkdir -p ${GLib.get_user_cache_dir()}/ags/user'`);
+ Utils.exec(`touch ${this._todoPath}`);
+ Utils.writeFile("[]", this._todoPath).then(() => {
+ this._todoJson = JSON.parse(Utils.readFile(this._todoPath))
+ }).catch(print);
+ }
+ }
+}
+
+// the singleton instance
+const service = new TodoService();
+
+// make it global for easy use with cli
+globalThis.todo = service;
+
+// export to use in other modules
+export default service;
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/services/waifus.js b/modules/home-manager/ags/ags/services/waifus.js
new file mode 100644
index 0000000..8512ed5
--- /dev/null
+++ b/modules/home-manager/ags/ags/services/waifus.js
@@ -0,0 +1,150 @@
+import Service from 'resource:///com/github/Aylur/ags/service.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+
+// Note: this service is made mainly for waifu.im. Others might work but not as properly
+const APISERVICES = {
+ 'im': {
+ 'endpoint': 'https://api.waifu.im/search',
+ 'headers': { 'Accept-Version': 'v5' },
+ },
+ 'nekos': {
+ 'endpoint': 'https://nekos.life/api/neko',
+ 'headers': {},
+ },
+ 'pics': {
+ 'endpoint': 'https://api.waifu.pics/sfw/',
+ 'headers': {},
+ },
+};
+
+function paramStringFromObj(params) {
+ return Object.entries(params)
+ .map(([key, value]) => {
+ if (Array.isArray(value)) { // If it's an array, repeat
+ if (value.length == 0) return '';
+ let thisKey = `${encodeURIComponent(key)}=${encodeURIComponent(value[0])}`
+ for (let i = 1; i < value.length; i++) {
+ thisKey += `&${encodeURIComponent(key)}=${encodeURIComponent(value[i])}`;
+ }
+ return thisKey;
+ }
+ return `${key}=${value}`;
+ })
+ .join('&');
+}
+
+class WaifuService extends Service {
+ _baseUrl = 'https://api.waifu.im/search';
+ _mode = 'im'; // Allowed: im
+ _responses = [];
+ _queries = [];
+ _nsfw = false;
+ _minHeight = 600;
+
+ static {
+ Service.register(this, {
+ 'initialized': [],
+ 'clear': [],
+ 'newResponse': ['int'],
+ 'updateResponse': ['int'],
+ });
+ }
+
+ constructor() {
+ super();
+ this.emit('initialized');
+ }
+
+ clear() {
+ this._responses = [];
+ this._queries = [];
+ this.emit('clear');
+ }
+
+ get mode() { return this._mode }
+ set mode(value) {
+ this._mode = value;
+ this._baseUrl = APISERVICES[this._mode].endpoint;
+ }
+ get nsfw() { return this._nsfw }
+ set nsfw(value) { this._nsfw = value }
+ get queries() { return this._queries }
+ get responses() { return this._responses }
+
+ async fetch(msg) {
+ // Init
+ const userArgs = msg.split(/\s+/);
+
+ let taglist = [];
+ this._nsfw = false;
+ // Construct body/headers
+ for (let i = 0; i < userArgs.length; i++) {
+ const thisArg = userArgs[i].trim();
+ if (thisArg.length == 0) continue;
+ if (thisArg == '--im') this._mode = 'im';
+ else if (thisArg == '--nekos') this._mode = 'nekos';
+ else if (thisArg.includes('pics')) this._mode = 'pics';
+ else if (thisArg.includes('segs') || thisArg.includes('sex') || thisArg.includes('lewd')) this._nsfw = true;
+ else {
+ taglist.push(thisArg);
+ if (['ecchi', 'hentai', 'ero', 'ass', 'paizuri', 'oral', 'milf'].includes(thisArg)) this._nsfw = true;
+ }
+ }
+ const newMessageId = this._queries.length;
+ this._queries.push(taglist);
+ this.emit('newResponse', newMessageId);
+ const params = {
+ 'included_tags': taglist,
+ 'height': `>=${this._minHeight}`,
+ 'nsfw': this._nsfw,
+ };
+ const paramString = paramStringFromObj(params);
+ // Fetch
+ // Note: body isn't included since passing directly to url is more reliable
+ const options = {
+ method: 'GET',
+ headers: APISERVICES[this._mode].headers,
+ };
+ let status = 0;
+ Utils.fetch(`${APISERVICES[this._mode].endpoint}?${paramString}`, options)
+ .then(result => {
+ status = result.status;
+ return result.text();
+ })
+ .then((dataString) => { // Store interesting stuff and emit
+ const parsedData = JSON.parse(dataString);
+ if (!parsedData.images) this._responses.push({ // Failed
+ status: status,
+ signature: -1,
+ url: '',
+ extension: '',
+ source: '',
+ dominant_color: '#383A40',
+ is_nsfw: false,
+ width: 0,
+ height: 0,
+ tags: [],
+ });
+ else {
+ const imageData = parsedData.images[0];
+ this._responses.push({
+ status: status,
+ signature: imageData?.signature || -1,
+ url: imageData?.url || undefined,
+ extension: imageData.extension,
+ source: imageData?.source,
+ dominant_color: imageData?.dominant_color || '#9392A6',
+ is_nsfw: imageData?.is_nsfw || false,
+ width: imageData?.width || 0,
+ height: imageData?.height || 0,
+ tags: imageData?.tags.map(obj => obj["name"]) || [],
+ });
+ }
+ this.emit('updateResponse', newMessageId);
+ })
+ .catch(print);
+ }
+}
+
+export default new WaifuService();
+
diff --git a/modules/home-manager/ags/ags/services/wallpaper.js b/modules/home-manager/ags/ags/services/wallpaper.js
new file mode 100644
index 0000000..9fa2b83
--- /dev/null
+++ b/modules/home-manager/ags/ags/services/wallpaper.js
@@ -0,0 +1,70 @@
+const { Gdk, GLib } = imports.gi;
+import Service from 'resource:///com/github/Aylur/ags/service.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+
+const WALLPAPER_CONFIG_PATH = `${GLib.get_user_state_dir()}/ags/user/wallpaper.json`;
+
+class WallpaperService extends Service {
+ static {
+ Service.register(
+ this,
+ { 'updated': [], },
+ );
+ }
+
+ _wallPath = '';
+ _wallJson = [];
+ _monitorCount = 1;
+
+ _save() {
+ Utils.writeFile(JSON.stringify(this._wallJson), this._wallPath)
+ .catch(print);
+ }
+
+ add(path) {
+ this._wallJson.push(path);
+ this._save();
+ this.emit('updated');
+ }
+
+ set(path, monitor = -1) {
+ this._monitorCount = Gdk.Display.get_default()?.get_n_monitors() || 1;
+ if (this._wallJson.length < this._monitorCount) this._wallJson[this._monitorCount - 1] = "";
+ if (monitor == -1)
+ this._wallJson.fill(path);
+ else
+ this._wallJson[monitor] = path;
+
+ this._save();
+ this.emit('updated');
+ }
+
+ get(monitor = 0) {
+ return this._wallJson[monitor];
+ }
+
+ constructor() {
+ super();
+ // How many screens?
+ this._monitorCount = Gdk.Display.get_default()?.get_n_monitors() || 1;
+ // Read config
+ this._wallPath = WALLPAPER_CONFIG_PATH;
+ try {
+ const fileContents = Utils.readFile(this._wallPath);
+ this._wallJson = JSON.parse(fileContents);
+ }
+ catch {
+ Utils.exec(`bash -c 'mkdir -p ${GLib.get_user_cache_dir()}/ags/user'`);
+ Utils.exec(`touch ${this._wallPath}`);
+ Utils.writeFile('[]', this._wallPath).then(() => {
+ this._wallJson = JSON.parse(Utils.readFile(this._wallPath))
+ }).catch(print);
+ }
+ }
+}
+
+// instance
+const service = new WallpaperService();
+// make it global for easy use with cli
+globalThis['wallpaper'] = service;
+export default service;
\ No newline at end of file
diff --git a/modules/home-manager/ags/ags/user_options.js b/modules/home-manager/ags/ags/user_options.js
new file mode 100644
index 0000000..03e7620
--- /dev/null
+++ b/modules/home-manager/ags/ags/user_options.js
@@ -0,0 +1,9 @@
+// For every option, see ~/.config/ags/modules/.configuration/user_options.js
+// (vscode users ctrl+click this: file://./modules/.configuration/user_options.js)
+// (vim users: `:vsp` to split window, move cursor to this path, press `gf`. `Ctrl-w` twice to switch between)
+// options listed in this file will override the default ones in the above file
+
+const userConfigOptions = {
+}
+
+export default userConfigOptions;
diff --git a/modules/home-manager/ags/ags/variables.js b/modules/home-manager/ags/ags/variables.js
new file mode 100644
index 0000000..bf6a9ff
--- /dev/null
+++ b/modules/home-manager/ags/ags/variables.js
@@ -0,0 +1,82 @@
+const { Gdk, Gtk } = imports.gi;
+import App from 'resource:///com/github/Aylur/ags/app.js'
+import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
+import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
+import Variable from 'resource:///com/github/Aylur/ags/variable.js';
+import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
+const { exec, execAsync } = Utils;
+
+Gtk.IconTheme.get_default().append_search_path(`${App.configDir}/assets/icons`);
+
+// Global vars for external control (through keybinds)
+export const showMusicControls = Variable(false, {})
+export const showColorScheme = Variable(false, {})
+globalThis['openMusicControls'] = showMusicControls;
+globalThis['openColorScheme'] = showColorScheme;
+globalThis['mpris'] = Mpris;
+
+// load monitor shell modes from userOptions
+const initialMonitorShellModes = () => {
+ const numberOfMonitors = Gdk.Display.get_default()?.get_n_monitors() || 1;
+ const monitorBarConfigs = [];
+ for (let i = 0; i < numberOfMonitors; i++) {
+ if (userOptions.bar.modes[i]) {
+ monitorBarConfigs.push(userOptions.bar.modes[i])
+ } else {
+ monitorBarConfigs.push('normal')
+ }
+ }
+ return monitorBarConfigs;
+
+}
+export const currentShellMode = Variable(initialMonitorShellModes(), {}) // normal, focus
+
+// Mode switching
+const updateMonitorShellMode = (monitorShellModes, monitor, mode) => {
+ const newValue = [...monitorShellModes.value];
+ newValue[monitor] = mode;
+ monitorShellModes.value = newValue;
+}
+globalThis['currentMode'] = currentShellMode;
+globalThis['cycleMode'] = () => {
+ const monitor = Hyprland.active.monitor.id || 0;
+
+ if (currentShellMode.value[monitor] === 'normal') {
+ updateMonitorShellMode(currentShellMode, monitor, 'focus')
+ }
+ else if (currentShellMode.value[monitor] === 'focus') {
+ updateMonitorShellMode(currentShellMode, monitor, 'nothing')
+ }
+ else {
+ updateMonitorShellMode(currentShellMode, monitor, 'normal')
+ }
+}
+
+// Window controls
+const range = (length, start = 1) => Array.from({ length }, (_, i) => i + start);
+globalThis['toggleWindowOnAllMonitors'] = (name) => {
+ range(Gdk.Display.get_default()?.get_n_monitors() || 1, 0).forEach(id => {
+ App.toggleWindow(`${name}${id}`);
+ });
+}
+globalThis['closeWindowOnAllMonitors'] = (name) => {
+ range(Gdk.Display.get_default()?.get_n_monitors() || 1, 0).forEach(id => {
+ App.closeWindow(`${name}${id}`);
+ });
+}
+globalThis['openWindowOnAllMonitors'] = (name) => {
+ range(Gdk.Display.get_default()?.get_n_monitors() || 1, 0).forEach(id => {
+ App.openWindow(`${name}${id}`);
+ });
+}
+
+globalThis['closeEverything'] = () => {
+ const numMonitors = Gdk.Display.get_default()?.get_n_monitors() || 1;
+ for (let i = 0; i < numMonitors; i++) {
+ App.closeWindow(`cheatsheet${i}`);
+ App.closeWindow(`session${i}`);
+ }
+ App.closeWindow('sideleft');
+ App.closeWindow('sideright');
+ App.closeWindow('overview');
+};
diff --git a/modules/home-manager/ags/default.nix b/modules/home-manager/ags/default.nix
index a593cc5..e178ff0 100644
--- a/modules/home-manager/ags/default.nix
+++ b/modules/home-manager/ags/default.nix
@@ -1,8 +1,14 @@
-{ inputs, pkgs, ... }:
+{ inputs, pkgs, lib, ... }:
{
# add the home manager module
imports = [ inputs.ags.homeManagerModules.default ];
+ home.activation = {
+ linkAgs = lib.hm.dag.entryAfter ["writeBoundary"] ''
+ ln -sf ./ags ~/.config/ags
+ '';
+ };
+
programs.ags = {
enable = true;