diff --git a/.config/ags/assets/icons/ai-oxygen-symbolic.svg b/.config/ags/assets/icons/ai-oxygen-symbolic.svg
deleted file mode 100644
index 5e1cc1937..000000000
--- a/.config/ags/assets/icons/ai-oxygen-symbolic.svg
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/.config/ags/assets/icons/ai-zukijourney.png b/.config/ags/assets/icons/ai-zukijourney.png
deleted file mode 100644
index 917335e72..000000000
Binary files a/.config/ags/assets/icons/ai-zukijourney.png and /dev/null differ
diff --git a/.config/ags/assets/themes/sourceviewtheme-dark-monokai-license.txt b/.config/ags/assets/themes/sourceviewtheme-dark-monokai-license.txt
deleted file mode 100644
index d159169d1..000000000
--- a/.config/ags/assets/themes/sourceviewtheme-dark-monokai-license.txt
+++ /dev/null
@@ -1,339 +0,0 @@
- 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/.config/ags/assets/themes/sourceviewtheme-light.xml b/.config/ags/assets/themes/sourceviewtheme-light.xml
deleted file mode 100644
index c880404dc..000000000
--- a/.config/ags/assets/themes/sourceviewtheme-light.xml
+++ /dev/null
@@ -1,95 +0,0 @@
-
-
- end_4
- <_description>Catppuccin port but very random
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.config/ags/config.js b/.config/ags/config.js
deleted file mode 100644
index 73e57b57f..000000000
--- a/.config/ags/config.js
+++ /dev/null
@@ -1,81 +0,0 @@
-"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/.config/ags/config_overviewOnly.js b/.config/ags/config_overviewOnly.js
deleted file mode 100644
index 9c42aaa1b..000000000
--- a/.config/ags/config_overviewOnly.js
+++ /dev/null
@@ -1,24 +0,0 @@
-// 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/.config/ags/i18n/i18n.js b/.config/ags/i18n/i18n.js
deleted file mode 100644
index abb38a9b4..000000000
--- a/.config/ags/i18n/i18n.js
+++ /dev/null
@@ -1,55 +0,0 @@
-const Gio = imports.gi.Gio;
-const GLib = imports.gi.GLib;
-import configOptions from "../modules/.configuration/user_options.js";
-const { langCode, Extra_logs } = configOptions.i18n
-const translations = {};
-
-let currentLanguage = langCode || getLanguageCode();
-
-function getLanguageCode() {
- let langEnv = GLib.getenv('LANG') || GLib.getenv('LANGUAGE') || 'Default.';
- let langCode = langEnv.split('.')[0];
- return langCode;
-}
-
-// Load language file
-function loadLanguage(lang) {
- if (!translations[lang]) {
- try {
- let filePath = `~/.config/ags/i18n/locales/${lang}.json`;
- filePath = filePath.replace(/^~/, GLib.get_home_dir());
- let file = Gio.File.new_for_path(filePath);
- let [success, contents] = file.load_contents(null);
- if (success) {
- let decoder = new TextDecoder('utf-8');
- let jsonString = decoder.decode(contents);
- translations[lang] = JSON.parse(jsonString);
- }
- } catch (error) {
- if (Extra_logs || lang === "Default")
- console.warn(`Failed to load language file, language code: ${lang}:\n`, error);
- return;
- }
- }
- currentLanguage = currentLanguage || lang;
-}
-
-// Initialize default language
-function init() {
- try {
- loadLanguage(currentLanguage);
- if (Extra_logs)
- console.log(getString("Initialization complete!") || "Initialization complete!");
- loadLanguage("Default");
- } catch (error) {
- console.error('Failed to initialize default language:', error);
- }
-}
-
-// Get translation, if no corresponding value, return the key
-function getString(key) {
- if (key && !translations[currentLanguage]?.[key] && Extra_logs)
- console.warn(`${translations[currentLanguage]["Not found"] || "Not found"}:::${key}`);
- return translations[currentLanguage]?.[key] || translations['Default']?.[key] || key;
-}
-export { getString, init };
\ No newline at end of file
diff --git a/.config/ags/i18n/locales/Default.json b/.config/ags/i18n/locales/Default.json
deleted file mode 100644
index 4b1debd6c..000000000
--- a/.config/ags/i18n/locales/Default.json
+++ /dev/null
@@ -1,250 +0,0 @@
-{
- "No media": "No media",
- "Powered by Google": "Powered by Google",
- "Not affiliated, endorsed, or sponsored by Google.\n\nPrivacy: Chat messages aren't linked to your account,\nbut will be read by human reviewers to improve the model.": "Not affiliated, endorsed, or sponsored by Google.\n\nPrivacy: Chat messages aren't linked to your account,\nbut will be read by human reviewers to improve the model.",
- "Precise": "Precise",
- "Balanced": "Balanced",
- "Creative": "Creative",
- "Gemini's temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1": "Gemini's temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1",
- "Enhancements": "Enhancements",
- "Tells Gemini:\n- It's a Linux sidebar assistant\n- Be brief and use bullet points": "Tells Gemini:\n- It's a Linux sidebar assistant\n- Be brief and use bullet points",
- "Safety": "Safety",
- "When turned off, tells the API (not the model) \nto not block harmful/explicit content": "When turned off, tells the API (not the model) \nto not block harmful/explicit content",
- "History": "History",
- "Saves chat history\nMessages in previous chats won't show automatically, but they are there": "Saves chat history\nMessages in previous chats won't show automatically, but they are there",
- "Key stored in:": "Key stored in:",
- "To update this key, type": "To update this key, type",
- "Updated API Key at": "Updated API Key at",
- "Currently using": "Currently using",
- "Select ChatGPT-compatible API provider": "Select ChatGPT-compatible API provider",
- "Official OpenAI API.\nPricing: Free for the first $5 or 3 months, whichever is less.": "Official OpenAI API.\nPricing: Free for the first $5 or 3 months, whichever is less.",
- "Official Ollama API.\nPricing: Free.": "Official Ollama API.\nPricing: Free.",
- "A unified interface for LLMs": "A unified interface for LLMs",
- "An API from Tornado Softwares\nPricing: Free: 100/day\nRequires you to join their Discord for a key": "An API from Tornado Softwares\nPricing: Free: 100/day\nRequires you to join their Discord for a key",
- "An API from @zukixa on GitHub.\nNote: Keys are IP-locked so it's buggy sometimes\nPricing: Free: 10/min, 800/day.\nRequires you to join their Discord for a key": "An API from @zukixa on GitHub.\nNote: Keys are IP-locked so it's buggy sometimes\nPricing: Free: 10/min, 800/day.\nRequires you to join their Discord for a key",
- "Provider shown above": "Provider shown above",
- "Chat with models compatible with OpenAI's Chat Completions API.\nNot affiliated, endorsed, or sponsored by any of the providers.": "Chat with models compatible with OpenAI's Chat Completions API.\nNot affiliated, endorsed, or sponsored by any of the providers.",
- "The model's temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1": "The model's temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1",
- "An API key is required\nYou can grab one here , then enter it below": "An API key is required\nYou can grab one here , then enter it below",
- "Tells the model:\n- It's a Linux sidebar assistant\n- Be brief and use bullet points": "Tells the model:\n- It's a Linux sidebar assistant\n- Be brief and use bullet points",
- "Powered by waifu.im + other APIs": "Powered by waifu.im + other APIs",
- "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.": "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.",
- "Tags →": "Tags →",
- "Invalid command.": "Invalid command.",
- "Anime booru": "Anime booru",
- "Powered by yande.re and konachan": "Powered by yande.re and konachan",
- "An image booru. May contain NSFW content.\nWatch your back.\n\nDisclaimer: Not affiliated with the provider\nnor responsible for any of its content.": "An image booru. May contain NSFW content.\nWatch your back.\n\nDisclaimer: Not affiliated with the provider\nnor responsible for any of its content.",
- "Lewds": "Lewds",
- "Shows naughty stuff when enabled": "Shows naughty stuff when enabled",
- "Saves images in folders by their tags": "Saves images in folders by their tags",
- "Message Gemini...": "Message Gemini...",
- "Enter Google AI API Key...": "Enter Google AI API Key...",
- "Message the model...": "Message the model...",
- "Enter API Key...": "Enter API Key...",
- "Enter tags": "Enter tags",
- "Enter tags and/or page number": "Enter tags and/or page number",
- "No tag in mind? Type a page number": "No tag in mind? Type a page number",
- "Quick scripts": "Quick scripts",
- "Change screen resolution": "Change screen resolution",
- "Update packages": "Update packages",
- "Trim system generations to 5": "Trim system generations to 5",
- "Trim home manager generations to 5": "Trim home manager generations to 5",
- "Remove orphan packages": "Remove orphan packages",
- "Uninstall unused flatpak packages": "Uninstall unused flatpak packages",
- "Inaccurate Color picker": "Inaccurate Color picker",
- "Result": "Result",
- "Type to search": "Type to search",
- "illogical-impulse": "illogical-impulse",
- "RAM Usage": "RAM Usage",
- "Swap Usage": "Swap Usage",
- "CPU Usage": "CPU Usage",
- "Uptime:": "Uptime:",
- "Screen snip": "Screen snip",
- "Color picker": "Color picker",
- "Toggle on-screen keyboard": "Toggle on-screen keyboard",
- "Night Light": "Night Light",
- "Color inversion": "Color inversion",
- "Keep system awake": "Keep system awake",
- "Cloudflare WARP": "Cloudflare WARP",
- "Session": "Session",
- "Bluetooth | Right-click to configure": "Bluetooth | Right-click to configure",
- "Wifi | Right-click to configure": "Wifi | Right-click to configure",
- "Right-click to configure": "Right-click to configure",
- "Unknown": "Unknown",
- "Reload Environment config": "Reload Environment config",
- "Open Settings": "Open Settings",
- "Notifications": "Notifications",
- "Audio controls": "Audio controls",
- "Bluetooth": "Bluetooth",
- "Wifi networks": "Wifi networks",
- "Quick config": "Quick config",
- "Silence": "Silence",
- "Clear": "Clear",
- "No notifications": "No notifications",
- "notifications": "notifications",
- "Close": "Close",
- "Now": "Now",
- "Yesterday": "Yesterday",
- "No audio source": "No audio source",
- "Remove device": "Remove device",
- "Connected": "Connected",
- "Paired": "Paired",
- "More": "More",
- "Selected": "Selected",
- "Connecting to": "Connecting to",
- "Current network": "Current network",
- "Authentication": "Authentication",
- "Authentication failed": "Authentication failed",
- "Enter network password": "Enter network password",
- "Properties": "Properties",
- "Forget": "Forget",
- "Effects": "Effects",
- "Transparency": "Transparency",
- "[AGS]\nMake shell elements transparent\nBlur is also recommended if you enable this": "[AGS]\nMake shell elements transparent\nBlur is also recommended if you enable this",
- "Blur": "Blur",
- "[Hyprland]\nEnable blur on transparent elements\nDoesn't affect performance/power consumption unless you have transparent windows.": "[Hyprland]\nEnable blur on transparent elements\nDoesn't affect performance/power consumption unless you have transparent windows.",
- "X-ray": "X-ray",
- "[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) ": "[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) ",
- "Size": "Size",
- "[Hyprland]\nAdjust the blur radius. Generally doesn't affect performance\nHigher = more color spread": "[Hyprland]\nAdjust the blur radius. Generally doesn't affect performance\nHigher = more color spread",
- "Passes": "Passes",
- "[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.": "[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.",
- "Animations": "Animations",
- "[Hyprland] [GTK]\nEnable animations": "[Hyprland] [GTK]\nEnable animations",
- "Choreography delay": "Choreography delay",
- "In milliseconds, the delay between animations of a series": "In milliseconds, the delay between animations of a series",
- "Developer": "Developer",
- "Show FPS": "Show FPS",
- "[Hyprland]\nShow FPS overlay on top-left corner": "[Hyprland]\nShow FPS overlay on top-left corner",
- "Log to stdout": "Log to stdout",
- "[Hyprland]\nPrint LOG, ERR, WARN, etc. messages to the console": "[Hyprland]\nPrint LOG, ERR, WARN, etc. messages to the console",
- "Damage tracking": "Damage tracking",
- "[Hyprland]\nEnable damage tracking\nGenerally, leave it on.\nTurn off only when a shader doesn't work": "[Hyprland]\nEnable damage tracking\nGenerally, leave it on.\nTurn off only when a shader doesn't work",
- "Damage blink": "Damage blink",
- "[Hyprland] [Epilepsy warning!]\nShow screen damage flashes": "[Hyprland] [Epilepsy warning!]\nShow screen damage flashes",
- "Not all changes are saved": "Not all changes are saved",
- "Mo": "Mo",
- "Tu": "Tu",
- "We": "We",
- "Th": "Th",
- "Fr": "Fr",
- "Sa": "Sa",
- "Su": "Su",
- "Calendar": "Calendar",
- "To Do": "To Do",
- "Unfinished": "Unfinished",
- "Done": "Done",
- "Finished tasks will go here": "Finished tasks will go here",
- "Nothing here!": "Nothing here!",
- "+ New task": "+ New task",
- "Add a task...": "Add a task...",
- "Collapse calendar": "Collapse calendar",
- "Expand calendar": "Expand calendar",
- "To do tasks": "To do tasks",
- "Color scheme": "Color scheme",
- "Options": "Options",
- "Dark Mode": "Dark Mode",
- "Ya should go to sleep!": "Ya should go to sleep!",
- "Theme GTK apps using accent color\n(drawback: dark/light mode switching requires restart)": "Theme GTK apps using accent color\n(drawback: dark/light mode switching requires restart)",
- "Scheme styles": "Scheme styles",
- "Vibrant": "Vibrant",
- "Vibrant+": "Vibrant+",
- "Expressive": "Expressive",
- "Monochrome": "Monochrome",
- "Rainbow": "Rainbow",
- "Fidelity": "Fidelity",
- "Fruit Salad": "Fruit Salad",
- "Tonal Spot": "Tonal Spot",
- "Content": "Content",
- "Use arrow keys to navigate.\nEnter to select, Esc to cancel.": "Use arrow keys to navigate.\nEnter to select, Esc to cancel.",
- "Lock": "Lock",
- "Logout": "Logout",
- "Sleep": "Sleep",
- "Hibernate": "Hibernate",
- "Shutdown": "Shutdown",
- "Reboot": "Reboot",
- "Cancel": "Cancel",
- "Cheat sheet": "Cheat sheet",
- "Keybinds": "Keybinds",
- "Periodic table": "Periodic table",
- "Essentials for beginners": "Essentials for beginners",
- "Make shell elements transparent": "Make shell elements transparent",
- "Actions": "Actions",
- "Window management": "Window management",
- "Window arrangement": "Window arrangement",
- "Workspace management": "Workspace management",
- "Workspace navigation": "Workspace navigation",
- "Widgets": "Widgets",
- "Media": "Media",
- "Apps": "Apps",
- "Neutral": "Neutral",
- "Launch foot (terminal)": "Launch foot (terminal)",
- "Open app launcher": "Open app launcher",
- "Change wallpaper": "Change wallpaper",
- "Clipboard history >> clipboard": "Clipboard history >> clipboard",
- "Pick emoji >> clipboard": "Pick emoji >> clipboard",
- "Screen snip >> edit": "Screen snip >> edit",
- "Screen snip to text >> clipboard": "Screen snip to text >> clipboard",
- "Pick color (Hex) >> clipboard": "Pick color (Hex) >> clipboard",
- "Screenshot >> clipboard": "Screenshot >> clipboard",
- "Screenshot >> clipboard & file": "Screenshot >> clipboard & file",
- "Record region (no sound)": "Record region (no sound)",
- "Record screen (with sound)": "Record screen (with sound)",
- "Suspend system": "Suspend system",
- "Move focus in direction": "Move focus in direction",
- "Move window": "Move window",
- "Resize window": "Resize window",
- "Close window": "Close window",
- "Pick and kill a window": "Pick and kill a window",
- "Window: move in direction": "Window: move in direction",
- "Window: split ratio +/- 0.1": "Window: split ratio +/- 0.1",
- "Float/unfloat window": "Float/unfloat window",
- "Toggle fake fullscreen": "Toggle fake fullscreen",
- "Toggle fullscreen": "Toggle fullscreen",
- "Toggle maximization": "Toggle maximization",
- "Focus workspace # (1, 2, 3, 4, ...)": "Focus workspace # (1, 2, 3, 4, ...)",
- "Workspace: focus left/right": "Workspace: focus left/right",
- "Workspace: toggle special": "Workspace: toggle special",
- "Window: move to workspace # (1, 2, 3, 4, ...)": "Window: move to workspace # (1, 2, 3, 4, ...)",
- "Window: move to workspace left/right": "Window: move to workspace left/right",
- "Window: move to workspace special": "Window: move to workspace special",
- "Window: pin (show on all workspaces)": "Window: pin (show on all workspaces)",
- "Restart widgets": "Restart widgets",
- "Cycle bar mode (normal, focus)": "Cycle bar mode (normal, focus)",
- "Toggle overview/launcher": "Toggle overview/launcher",
- "Show cheatsheet": "Show cheatsheet",
- "Toggle left sidebar": "Toggle left sidebar",
- "Toggle right sidebar": "Toggle right sidebar",
- "Toggle music controls": "Toggle music controls",
- "View color scheme and options": "View color scheme and options",
- "Toggle power menu": "Toggle power menu",
- "Toggle crosshair": "Toggle crosshair",
- "Next track": "Next track",
- "Previous track": "Previous track",
- "Play/pause media": "Play/pause media",
- "Launch Zed (editor)": "Launch Zed (editor)",
- "Launch VSCode (editor)": "Launch VSCode (editor)",
- "Launch Nautilus (file manager)": "Launch Nautilus (file manager)",
- "Launch Firefox (browser)": "Launch Firefox (browser)",
- "Launch GNOME Text Editor": "Launch GNOME Text Editor",
- "Launch WPS Office": "Launch WPS Office",
- "Launch GNOME Settings": "Launch GNOME Settings",
- "Launch pavucontrol (volume mixer)": "Launch pavucontrol (volume mixer)",
- "Launch EasyEffects (equalizer & other audio effects)": "Launch EasyEffects (equalizer & other audio effects)",
- "Launch GNOME System monitor": "Launch GNOME System monitor",
- "Toggle fallback launcher: anyrun": "Toggle fallback launcher: anyrun",
- "Toggle fallback launcher: fuzzel": "Toggle fallback launcher: fuzzel",
- "Initialization complete!": "Initialization complete!",
- "Not found": "Not found:",
- "Calling API": "Calling API",
- "Downloading image": "Downloading image",
- "Finished!": "Finished!",
- "Error": "Error",
- "Not found!": "Not found!",
- "Go to file url": "Go to file url",
- "Save image": "Save image",
- "Hoard": "Hoard",
- "Open externally": "Open externally",
- "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!": "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!",
- "Feels like": "Feels like"
-}
diff --git a/.config/ags/i18n/locales/fa_IR.json b/.config/ags/i18n/locales/fa_IR.json
deleted file mode 100644
index 1ff44b553..000000000
--- a/.config/ags/i18n/locales/fa_IR.json
+++ /dev/null
@@ -1,238 +0,0 @@
-{
- "No media": "بدون رسانه",
- "Powered by Google": "قدرت گرفته از گوگل",
- "Not affiliated, endorsed, or sponsored by Google.\n\nPrivacy: Chat messages aren't linked to your account,\nbut will be read by human reviewers to improve the model.": "بدون وابستگی تأیید شده یا حمایت شده توسط گوگل.\n\nحریم خصوصی: پیامهای گفتگو به حساب شما مرتبط نیستند،\nاما توسط بازبینیکنندگان انسانی برای بهبود مدل خوانده خواهند شد.",
- "Precise": "دقیق",
- "Balanced": "متعادل",
- "Creative": "خلاق",
- "Gemini's temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1": "مقدار دما در Gemini.\n دقیق = 0\n متعادل = 0.5\n خلاق = 1",
- "Enhancements": "بهبودها",
- "Tells Gemini:\n- It's a Linux sidebar assistant\n- Be brief and use bullet points": "به Gemini میگوید:\n- این یک دستیار نوار کناری لینوکس است\n- مختصر باشید و از فهرست کردن کمک بگیرید",
- "Safety": "ایمنی",
- "When turned off, tells the API (not the model) \nto not block harmful/explicit content": "زمانی که خاموش باشد، به API (نه مدل) میگوید که \nمحتوای مضر/صریح را مسدود نکند",
- "History": "پیشینه",
- "Saves chat history\nMessages in previous chats won't show automatically, but they are there": "پیشینه گفتگو را نگهداری میکند\nپیامهای گفتگوهای پیشین بهطور خودکار نمایش داده نمیشوند، اما وجود دارند",
- "Key stored in:": "کلید نگهداری شده در:",
- "To update this key, type": "برای بهروزرسانی این کلید، بنویسید",
- "Updated API Key at": "کلید API بهروزرسانی شده در",
- "Currently using": "هماینک بکار گرفته میشود",
- "Select ChatGPT-compatible API provider": "انتخاب ارائهدهنده API سازگار با ChatGPT",
- "Official OpenAI API.\nPricing: Free for the first $5 or 3 months, whichever is less.": "API رسمی OpenAI.\nقیمتگذاری: رایگان برای اولین 5 دلار یا 3 ماه، هر کدام که کمتر باشد.",
- "Official Ollama API.\nPricing: Free.": "API رسمی Ollama.\nقیمتگذاری: رایگان.",
- "A unified interface for LLMs": "یک رابط یکپارچه برای LLMها",
- "An API from Tornado Softwares\nPricing: Free: 100/day\nRequires you to join their Discord for a key": "API از Tornado Softwares\nقیمتگذاری: رایگان: 100 در روز\nنیاز به پیوستن به دیسکورد آنها برای دریافت کلید دارد",
- "An API from @zukixa on GitHub.\nNote: Keys are IP-locked so it's buggy sometimes\nPricing: Free: 10/min, 800/day.\nRequires you to join their Discord for a key": "API از @zukixa در گیتهاب.\nتوجه: کلیدها قفل IP هستند بنابراین گاهی اوقات باگ دارند\nقیمتگذاری: رایگان: 10 در دقیقه، 800 در روز.\nنیاز به پیوستن به دیسکورد آنها برای دریافت کلید دارد",
- "Provider shown above": "ارائهدهنده در بالا نشان داده شده است",
- "The model's temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1": "مقدار دما در مدل.\n دقیق = 0\n متعادل = 0.5\n خلاق = 1",
- "An API key is required\nYou can grab one here , then enter it below": "یک کلید API مورد نیاز است\nشما میتوانید یکی را اینجا بگیرید، سپس آن را پایین وارد کنید",
- "Tells the model:\n- It's a Linux sidebar assistant\n- Be brief and use bullet points": "به مدل میگوید:\n- این یک دستیار نوار کناری لینوکس است\n- گزافهگو نباشید و نقاط فهرست بکار ببرید",
- "Powered by waifu.im + other APIs": "قدرت گرفته از waifu.im + سایر APIها",
- "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.": "برچسبها را برای یک تصویر تصادفی بنویسید.\nمحتوای NSFW بازگردانده نخواهد شد مگر اینکه\nشما بهطور صریح چنین برچسبی را درخواست کنید.\n\nتوجه: بدون وابستگی ارائهدهندگان\nو مسئول هیچیک از محتوای آنها نیست.",
- "Tags →": "برچسبها →",
- "Invalid command.": "دستور نامعتبر.",
- "Anime booru": "انیمه بورو",
- "Powered by yande.re and konachan": "قدرت گرفته از yande.re و konachan",
- "An image booru. May contain NSFW content.\nWatch your back.\n\nDisclaimer: Not affiliated with the provider\nnor responsible for any of its content.": "یک تصویر بورو. ممکن است محتوای NSFW داشته باشد.\nمواظب باشید.\n\nتوجه: بدون وابستگی به ارائهدهنده\nو مسئول هیچیک از محتوای آن نیست.",
- "Lewds": "محتوای نامناسب",
- "Shows naughty stuff when enabled": "محتوای نامناسب را زمانی که فعال باشد نشان میدهد",
- "Saves images in folders by their tags": "تصاویر را در پوشهها بر اساس برچسبهایشان نگهداری میکند",
- "Message Gemini...": "پیام به Gemini...",
- "Enter Google AI API Key...": "کلید API گوگل AI را وارد کنید...",
- "Message the model...": "پیام به مدل...",
- "Enter API Key...": "کلید API را وارد کنید...",
- "Enter tags": "برچسبها را وارد کنید",
- "Quick scripts": "اسکریپتهای سریع",
- "Change screen resolution": "تغییر وضوح صفحه",
- "Update packages": "بهروزرسانی بستهها",
- "Trim system generations to 5": "تعداد نسلهای سامانه را به 5 کاهش دهید",
- "Trim home manager generations to 5": "تعداد نسلهای مدیر خانه را به 5 کاهش دهید",
- "Remove orphan packages": "بستههای ناکارآمد را پاک کنید",
- "Uninstall unused flatpak packages": "بستههای فلتپک بکار گرفته نشده را پاک کنید",
- "Inaccurate Color picker": "نادرست انتخابگر رنگ",
- "Result": "نتیجه",
- "Type to search": "برای جستجو بنویسید",
- "illogical-impulse": "illogical-impulse",
- "RAM Usage": "کارکرد RAM",
- "Swap Usage": "کارکرد Swap",
- "CPU Usage": "کارکرد CPU",
- "Uptime:": "در حال کار:",
- "Screen snip": "برش صفحه",
- "Color picker": "انتخابگر رنگ",
- "Toggle on-screen keyboard": "کیبورد روی صفحه را فعال/غیرفعال کنید",
- "Night Light": "نور شب",
- "Color inversion": "وارونگی رنگ",
- "Keep system awake": "سامانه را بیدار نگهدارید",
- "Cloudflare WARP": "Cloudflare WARP",
- "Session": "نشست",
- "Bluetooth | Right-click to configure": "بلوتوث | برای پیکربندی راست کلیک کنید ",
- "Wifi | Right-click to configure": "وایفای | برای پیکربندی راست کلیک کنید ",
- "Right-click to configure": "برای پیکربندی راست کلیک کنید",
- "Unknown": "ناشناخته",
- "Reload Environment config": "پیکربندی محیط را دوباره بارگذاری کنید",
- "Open Settings": "تنظیمات را باز کنید",
- "Notifications": "آگاهسازها",
- "Audio controls": "کنترلهای صدا",
- "Bluetooth": "بلوتوث",
- "Wifi networks": "شبکههای وایفای",
- "Quick config": "پیکربندی زنده",
- "Silence": "سکوت",
- "Clear": "پاک کردن",
- "No notifications": "بدون آگاهساز",
- "notifications": "آگاهسازها",
- "Close": "بستن",
- "Now": "اینک",
- "Yesterday": "دیروز",
- "No audio source": "هیچ منبع صوتی",
- "Remove device": "پاککردن دستگاه",
- "Connected": "متصل",
- "Paired": "جفت شده",
- "More": "بیشتر",
- "Selected": "انتخاب شده",
- "Current network": "شبکه کنونی",
- "Authentication": "احراز هویت",
- "Effects": "جلوهها",
- "Transparency": "شفافیت",
- "[AGS]\nMake shell elements transparent\nBlur is also recommended if you enable this": "[AGS]\nعناصر شل را شفاف کنید\nهمچنین اگر این را فعال کنید، تاری نیز توصیه میشود",
- "Blur": "تاری",
- "[Hyprland]\nEnable blur on transparent elements\nDoesn't affect performance/power consumption unless you have transparent windows.": "[Hyprland]\nفعال کردن تاری بر روی عناصر شفاف\nبر عملکرد/مصرف برق تأثیر نمیگذارد مگر اینکه پنجرههای شفاف داشته باشید.",
- "X-ray": "اشعه ایکس",
- "[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) ": "[Hyprland]\nهمه چیز را پشت یک پنجره/لایه به جز پسزمینه بر روی سطح تاری آن رندر نکنید\nتوصیه میشود برای بهبود عملکرد (اگر از شفافیت/تاری سوءاستفاده نکنید)",
- "Size": "اندازه",
- "[Hyprland]\nAdjust the blur radius. Generally doesn't affect performance\nHigher = more color spread": "[Hyprland]\nشعاع تاری را تنظیم کنید. به طور کلی بر عملکرد تأثیر نمیگذارد\nبیشتر = پخش رنگ بیشتر",
- "Passes": "عبور",
- "[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.": "[Hyprland] تعداد عبورهای الگوریتم تاری را تنظیم کنید\nعبورهای بیشتر = پخش بیشتر و مصرف برق بیشتر\n4 توصیه میشود\n2- عجیب به نظر میرسد و 6+ بیمزه خواهد بود.",
- "Animations": "پویانماییها",
- "[Hyprland] [GTK]\nEnable animations": "[Hyprland] [GTK]\nفعال کردن پویانماییها",
- "Choreography delay": "درنگ در پویایی",
- "In milliseconds, the delay between animations of a series": "به میلیثانیه، درنگ بین پویانماییهای یک سری",
- "Developer": "توسعهدهنده",
- "Show FPS": "نمایش FPS",
- "[Hyprland]\nShow FPS overlay on top-left corner": "[Hyprland]\nنمایش پوشش FPS در گوشه بالا سمت چپ",
- "Log to stdout": "ثبت در stdout",
- "[Hyprland]\nPrint LOG, ERR, WARN, etc. messages to the console": "[Hyprland]\nپیامهای LOG، ERR، WARN و دیگر را به کنسول چاپ کنید",
- "Damage tracking": "ردیابی آسیب",
- "[Hyprland]\nEnable damage tracking\nGenerally, leave it on.\nTurn off only when a shader doesn't work": "[Hyprland]\nفعال کردن ردیابی آسیب\nبه طور کلی، آن را روشن بگذارید.\nفقط زمانی که یک سایهزن(شیدر) کار نمیکند، خاموش کنید",
- "Damage blink": "چشمک آسیب",
- "[Hyprland] [Epilepsy warning!]\nShow screen damage flashes": "[Hyprland] [هشدار صرع!]\nنمایش چشمکهای آسیب صفحه",
- "Not all changes are saved": "همه تغییرات نگهداری نشدهاند",
- "Mo": "دو",
- "Tu": "سه",
- "We": "چهار",
- "Th": "پنج",
- "Fr": "جمعه",
- "Sa": "شنبه",
- "Su": "یک",
- "Calendar": "تقویم",
- "To Do": "کارها",
- "Unfinished": "مانده",
- "Done": "پایان یافته",
- "Finished tasks will go here": "کارهای پایان یافته اینجا خواهند بود",
- "Nothing here!": "هیچ چیز اینجا نیست!",
- "+ New task": "+ کار جدید",
- "Add a task...": "افزودن یک کار...",
- "Color scheme": "طرح رنگ",
- "Options": "گزینهها",
- "Dark Mode": "حالت تاریک",
- "Ya should go to sleep!": "پاشو برو بخواب!",
- "Theme GTK apps using accent color\n(drawback: dark/light mode switching requires restart)": "برنامههای GTK را با بکارگیری رنگ تأکید پوسته گذاری کنید\n(معایب: تغییر حالت تاریک/روشن نیاز به باز راهاندازی دارد)",
- "Scheme styles": "سبکهای طرح",
- "Vibrant": "زنده",
- "Vibrant+": "زنده+",
- "Expressive": "بیانگر",
- "Monochrome": "تکرنگ",
- "Rainbow": "رنگین کمان",
- "Fidelity": "وفاداری",
- "Fruit Salad": "سالاد میوه",
- "Tonal Spot": "نقطه تنال",
- "Content": "محتوا",
- "Use arrow keys to navigate.\nEnter to select, Esc to cancel.": "برای ناوبری کلیدهای جهتدار را بکار ببرید.\nبرای انتخاب Enter و برای رد کردن Esc را بزنید.",
- "Lock": "قفل",
- "Logout": "خروج",
- "Sleep": "خواب",
- "Hibernate": "خواب زمستانی",
- "Shutdown": "خاموش",
- "Reboot": "باز راهاندازی",
- "Cancel": "رد کردن",
- "Cheat sheet": "برگه تقلب",
- "Keybinds": "کلیدهای میانبر",
- "Periodic table": "جدول تناوبی",
- "Essentials for beginners": "اساس برای مبتدیان",
- "Make shell elements transparent": "عناصر شل را شفاف کنید",
- "Actions": "کنش",
- "Window management": "مدیریت پنجره",
- "Window arrangement": "چیدمان پنجره",
- "Workspace management": "مدیریت فضای کاری",
- "Workspace navigation": "ناوبری فضای کاری",
- "Widgets": "ابزارکها",
- "Media": "رسانه",
- "Apps": "برنامهها",
- "Neutral": "خنثی",
- "Launch foot (terminal)": "اجرای foot (ترمینال)",
- "Open app launcher": "باز کردن راهانداز برنامه",
- "Change wallpaper": "تغییر پسزمینه",
- "Clipboard history >> clipboard": "پیشینه کلیپ بورد >> کلیپ بورد",
- "Pick emoji >> clipboard": "انتخاب ایموجی >> کلیپ بورد",
- "Screen snip >> edit": "برش صفحه >> ویرایش",
- "Screen snip to text >> clipboard": "برش صفحه به متن >> کلیپ بورد",
- "Pick color (Hex) >> clipboard": "انتخاب رنگ (Hex) >> کلیپ بورد",
- "Screenshot >> clipboard": "عکس صفحه >> کلیپ بورد",
- "Screenshot >> clipboard & file": "عکس صفحه >> کلیپ بورد و فایل",
- "Record region (no sound)": "ضبط منطقه (بدون صدا)",
- "Record screen (with sound)": "ضبط صفحه (با صدا)",
- "Suspend system": "تعلیق سامانه",
- "Move focus in direction": "جابهجایی تمرکز در جهت",
- "Move window": "جابهجایی پنجره",
- "Resize window": "تغییر اندازه پنجره",
- "Close window": "بستن پنجره",
- "Pick and kill a window": "انتخاب و بستن یک پنجره",
- "Window: move in direction": "پنجره: جابهجایی در جهت",
- "Window: split ratio +/- 0.1": "پنجره: نسبت تقسیم +/- 0.1",
- "Float/unfloat window": "پنجره را شناور/نا شناور کنید",
- "Toggle fake fullscreen": "تغییر حالت تمام صفحه ساختگی",
- "Toggle fullscreen": "تغییر حالت تمام صفحه",
- "Toggle maximization": "تغییر حالت بزرگنمایی",
- "Focus workspace # (1, 2, 3, 4, ...)": "تمرکز بر فضای کاری # (1، 2، 3، 4، ...)",
- "Workspace: focus left/right": "فضای کاری: تمرکز به چپ/راست",
- "Workspace: toggle special": "فضای کاری: تغییر حالت خاص",
- "Window: move to workspace # (1, 2, 3, 4, ...)": "پنجره: رفتن به فضای کاری # (1، 2، 3، 4، ...)",
- "Window: move to workspace left/right": "پنجره: رفتن به فضای کاری چپ/راست",
- "Window: move to workspace special": "پنجره: رفتن به فضای کاری خاص",
- "Window: pin (show on all workspaces)": "پنجره: سنجاق (نمایش در همه فضاهای کاری)",
- "Restart widgets": "باز راهاندازی ابزارکها",
- "Cycle bar mode (normal, focus)": "چرخش حالت نوار (عادی، تمرکز)",
- "Toggle overview/launcher": "تغییر حالت نمای کلی/راهانداز",
- "Show cheatsheet": "نمایش برگه تقلب",
- "Toggle left sidebar": "تغییر حالت نوار کناری چپ",
- "Toggle right sidebar": "تغییر حالت نوار کناری راست",
- "Toggle music controls": "تغییر حالت کنترلهای موسیقی",
- "View color scheme and options": "مشاهده طرح رنگ و گزینهها",
- "Toggle power menu": "تغییر حالت فهرست قدرت",
- "Toggle crosshair": "تغییر حالت نشانهگذاری",
- "Next track": "ترانه پسین",
- "Previous track": "ترانه پیشین",
- "Play/pause media": "پخش/مکث رسانه",
- "Launch Zed (editor)": "اجرای Zed (ویرایشگر)",
- "Launch VSCode (editor)": "اجرای VSCode (ویرایشگر)",
- "Launch Nautilus (file manager)": "اجرای Nautilus (مدیر فایل)",
- "Launch Firefox (browser)": "اجرای Firefox (مرورگر)",
- "Launch GNOME Text Editor": "اجرای ویرایشگر متن GNOME",
- "Launch WPS Office": "اجرای WPS Office",
- "Launch GNOME Settings": "اجرای تنظیمات GNOME",
- "Launch pavucontrol (volume mixer)": "اجرای pavucontrol (میکسر صدا)",
- "Launch EasyEffects (equalizer & other audio effects)": "اجرای EasyEffects (اکولایزر و سایر جلوههای صوتی)",
- "Launch GNOME System monitor": "اجرای مانیتور سامانه GNOME",
- "Toggle fallback launcher: anyrun": "بکارگیری راهانداز پشتیبان: anyrun",
- "Toggle fallback launcher: fuzzel": "بکارگیری راهانداز پشتیبان: fuzzel",
- "Initialization complete!": "راهاندازی کامل شد!",
- "Not found": "یافت نشد:",
- "Calling API": "در حال تماس با API",
- "Downloading image": "در حال دریافت تصویر",
- "Finished!": "پایان یافت!",
- "Error": "خطا",
- "Not found!": "یافت نشد!",
- "Go to file url": "رفتن به URL فایل",
- "Save image": "نگهداری تصویر",
- "Hoard": "نگهداری",
- "Open externally": "باز کردن در",
- "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!": "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.”. \nIf user talks to you in Persian and you can also respond in Persian, definitely respond in Persian. Thanks!"
-}
diff --git a/.config/ags/i18n/locales/fr_FR.json b/.config/ags/i18n/locales/fr_FR.json
deleted file mode 100644
index 4c9a8e80f..000000000
--- a/.config/ags/i18n/locales/fr_FR.json
+++ /dev/null
@@ -1,240 +0,0 @@
-{
- "No media": "Aucun média",
- "Powered by Google": "Fourni par Google",
- "Not affiliated, endorsed, or sponsored by Google.\n\nPrivacy: Chat messages aren't linked to your account,\nbut will be read by human reviewers to improve the model.": "Non affilié, approuvé ou sponsorisé par Google.\n\nConfidentialité : Les messages de chat ne sont pas liés à votre compte,\nmais seront lus par des examinateurs humains pour améliorer le modèle.",
- "Precise": "Précis",
- "Balanced": "Équilibré",
- "Creative": "Créatif",
- "Gemini's temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1": "Température de Gemini.\n Précis = 0\n Équilibré = 0.5\n Créatif = 1",
- "Enhancements": "Améliorations",
- "Tells Gemini:\n- It's a Linux sidebar assistant\n- Be brief and use bullet points": "Indique à Gemini :\n- C'est un assistant de panneau latéral pour Linux\n- Sois concis et utilise des puces",
- "Safety": "Sécurité",
- "When turned off, tells the API (not the model) \nto not block harmful/explicit content": "Quand désactivé, indique à l'API (et non au modèle)\nde ne pas bloquer le contenu nuisible/explicite",
- "History": "Historique",
- "Saves chat history\nMessages in previous chats won't show automatically, but they are there": "Sauvegarde l'historique des discussions\nLes messages des conversations précédentes ne s'affichent pas automatiquement, mais ils existent",
- "Key stored in:": "Clé stockée dans :",
- "To update this key, type": "Pour mettre à jour cette clé, tapez",
- "Updated API Key at": "Clé API mise à jour le",
- "Currently using": "Actuellement utilisé",
- "Select ChatGPT-compatible API provider": "Sélectionnez un fournisseur d'API compatible ChatGPT",
- "Official OpenAI API.\nPricing: Free for the first $5 or 3 months, whichever is less.": "API officielle d'OpenAI.\nTarification : Gratuit pour les premiers 5 $ ou 3 mois, selon le moindre des deux.",
- "Official Ollama API.\nPricing: Free.": "API officielle d'Ollama.\nTarification : Gratuit.",
- "A unified interface for LLMs": "Une interface unifiée pour les LLM",
- "An API from Tornado Softwares\nPricing: Free: 100/day\nRequires you to join their Discord for a key": "Une API de Tornado Softwares\nTarification : Gratuit : 100 par jour\nNécessite de rejoindre leur Discord pour obtenir une clé",
- "An API from @zukixa on GitHub.\nNote: Keys are IP-locked so it's buggy sometimes\nPricing: Free: 10/min, 800/day.\nRequires you to join their Discord for a key": "Une API de @zukixa sur GitHub.\nNote : Les clés sont verrouillées par IP, ce qui peut provoquer des bugs\nTarification : Gratuit : 10/min, 800/jour.\nNécessite de rejoindre leur Discord pour obtenir une clé",
- "Provider shown above": "Fournisseur indiqué ci-dessus",
- "The model's temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1": "La valeur de température du modèle.\n Précis = 0\n Équilibré = 0.5\n Créatif = 1",
- "An API key is required\nYou can grab one here , then enter it below": "Une clé API est requise\nVous pouvez en obtenir une ici , puis la saisir ci-dessous",
- "Tells the model:\n- It's a Linux sidebar assistant\n- Be brief and use bullet points": "Indique au modèle :\n- C'est un assistant de barre latérale pour Linux\n- Sois concis et utilise des puces",
- "Powered by waifu.im + other APIs": "Propulsé par waifu.im + d'autres API",
- "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.": "Tapez des tags pour une image aléatoire.\nLe contenu NSFW ne sera pas affiché à moins que\nvous ne demandiez explicitement un tel tag.\n\nAvertissement : Pas affilié aux fournisseurs\net pas responsable de leur contenu.",
- "Tags →": "Tags →",
- "Invalid command.": "Commande invalide.",
- "Anime booru": "Booru d'anime",
- "Powered by yande.re and konachan": "Propulsé par yande.re et konachan",
- "An image booru. May contain NSFW content.\nWatch your back.\n\nDisclaimer: Not affiliated with the provider\nnor responsible for any of its content.": "Un booru d'images. Peut contenir du contenu NSFW.\nFais attention.\n\nAvertissement : Pas affilié au fournisseur\net pas responsable de son contenu.",
- "Lewds": "Contenu osé",
- "Shows naughty stuff when enabled": "Affiche du contenu osé lorsqu'il est activé",
- "Saves images in folders by their tags": "Enregistre les images dans des dossiers selon leurs tags",
- "Message Gemini...": "Envoyer un message à Gemini...",
- "Enter Google AI API Key...": "Saisissez la clé API de Google AI...",
- "Message the model...": "Envoyer un message au modèle...",
- "Enter API Key...": "Saisissez la clé API...",
- "Enter tags": "Saisissez les tags",
- "Quick scripts": "Scripts rapides",
- "Change screen resolution": "Changer la résolution de l'écran",
- "Update packages": "Mettre à jour les paquets",
- "Trim system generations to 5": "Limiter les générations système à 5",
- "Trim home manager generations to 5": "Limiter les générations de home-manager à 5",
- "Remove orphan packages": "Supprimer les paquets orphelins",
- "Uninstall unused flatpak packages": "Désinstaller les paquets Flatpak inutilisés",
- "Inaccurate Color picker": "Inexact Sélecteur de couleur",
- "Result": "Résultat",
- "Type to search": "Tapez pour rechercher",
- "illogical-impulse": "illogical-impulse",
- "RAM Usage": "Utilisation de la RAM",
- "Swap Usage": "Utilisation du swap",
- "CPU Usage": "Utilisation du CPU",
- "Uptime:": "Temps de fonctionnement :",
- "Screen snip": "Capture d'écran",
- "Color picker": "Sélecteur de couleur",
- "Toggle on-screen keyboard": "Activer/désactiver le clavier virtuel",
- "Night Light": "Lumière nocturne",
- "Color inversion": "Inversion des couleurs",
- "Keep system awake": "Empêcher la mise en veille du système",
- "Cloudflare WARP": "Cloudflare WARP",
- "Session": "Session",
- "Bluetooth | Right-click to configure": "Bluetooth | Clic droit pour configurer",
- "Wifi | Right-click to configure": "Wifi | Clic droit pour configurer",
- "Right-click to configure": "Clic droit pour configurer",
- "Unknown": "Inconnu",
- "Reload Environment config": "Recharger la configuration de l'environnement",
- "Open Settings": "Ouvrir les paramètres",
- "Notifications": "Notifications",
- "Audio controls": "Contrôles audio",
- "Bluetooth": "Bluetooth",
- "Wifi networks": "Réseaux Wifi",
- "Quick config": "Configuration",
- "Silence": "Silence",
- "Clear": "Effacer",
- "No notifications": "Aucune notification",
- "notifications": "notifications",
- "Close": "Fermer",
- "Now": "Maintenant",
- "Yesterday": "Hier",
- "No audio source": "Aucune source audio",
- "Remove device": "Retirer l'appareil",
- "Connected": "Connecté",
- "Paired": "Appairé",
- "More": "Plus",
- "Selected": "Sélectionné",
- "Current network": "Réseau actuel",
- "Authentication": "Authentification",
- "Effects": "Effets",
- "Transparency": "Transparence",
- "[AGS]\nMake shell elements transparent\nBlur is also recommended if you enable this": "[AGS]\nRendre les éléments de l'interface transparents\nLe flou est également recommandé si vous l'activez.\nChoisissez un fond d'écran avant d'activer cette option.",
- "Blur": "Flou",
- "[Hyprland]\nEnable blur on transparent elements\nDoesn't affect performance/power consumption unless you have transparent windows.": "[Hyprland]\nActiver le flou sur les éléments transparents\nN'affecte pas les performances ou la consommation d'énergie, sauf en cas de fenêtres transparentes.",
- "X-ray": "Rayon X",
- "[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) ": "[Hyprland]\nNe pas afficher ce qui se trouve derrière une fenêtre ou une couche (sauf le fond d'écran) sur sa surface floue\nRecommandé pour améliorer les performances (si vous n'abusez pas de la transparence/flou) ",
- "Size": "Taille",
- "[Hyprland]\nAdjust the blur radius. Generally doesn't affect performance\nHigher = more color spread": "[Hyprland]\nAjustez le rayon du flou. En général, cela n'affecte pas les performances\nPlus le rayon est grand, plus la diffusion des couleurs est importante",
- "Passes": "Passes",
- "[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.": "[Hyprland] Ajustez le nombre d'exécutions de l'algorithme de flou\nPlus il y a de passes, plus la diffusion et la consommation d'énergie augmentent\n4 est recommandé\n2 ou moins paraîtraient étranges et 6 ou plus sembleraient médiocres.",
- "Animations": "Animations",
- "[Hyprland] [GTK]\nEnable animations": "[Hyprland] [GTK]\nActiver les animations",
- "Choreography delay": "Délai de chorégraphie",
- "In milliseconds, the delay between animations of a series": "En millisecondes, le délai entre les animations d'une série",
- "Developer": "Développeur",
- "Show FPS": "Afficher les FPS",
- "[Hyprland]\nShow FPS overlay on top-left corner": "[Hyprland]\nAfficher une superposition FPS en haut à gauche",
- "Log to stdout": "Journaliser vers stdout",
- "[Hyprland]\nPrint LOG, ERR, WARN, etc. messages to the console": "[Hyprland]\nAfficher les messages LOG, ERR, WARN, etc. dans la console",
- "Damage tracking": "Suivi des dégâts",
- "[Hyprland]\nEnable damage tracking\nGenerally, leave it on.\nTurn off only when a shader doesn't work": "[Hyprland]\nActiver le suivi des dégâts\nEn général, laissez-le activé.\nDésactivez-le uniquement si un shader ne fonctionne pas",
- "Damage blink": "Clignotement des dégâts",
- "[Hyprland] [Epilepsy warning!]\nShow screen damage flashes": "[Hyprland] [Avertissement épilepsie !]\nAfficher des flashs lors des modifications de l'écran",
- "Not all changes are saved": "Tous les changements ne sont pas enregistrés",
- "Mo": "Lu",
- "Tu": "Ma",
- "We": "Me",
- "Th": "Je",
- "Fr": "Ve",
- "Sa": "Sa",
- "Su": "Di",
- "Calendar": "Calendrier",
- "To Do": "À faire",
- "Unfinished": "Non terminé",
- "Done": "Terminé",
- "Finished tasks will go here": "Les tâches terminées apparaîtront ici",
- "Nothing here!": "Rien ici !",
- "+ New task": "+ Nouvelle tâche",
- "Add a task...": "Ajouter une tâche...",
- "Color scheme": "Schéma de couleurs",
- "Options": "Options",
- "Dark Mode": "Mode sombre",
- "Ya should go to sleep!": "Tu devrais aller dormir !",
- "Theme GTK apps using accent color\n(drawback: dark/light mode switching requires restart)": "Thématiser les applications GTK avec la couleur d'accent\n(inconvénient : le changement de mode sombre/clair nécessite un redémarrage)",
- "Scheme styles": "Styles de schéma",
- "Vibrant": "Vibrant",
- "Vibrant+": "Vibrant+",
- "Expressive": "Expressif",
- "Monochrome": "Monochrome",
- "Rainbow": "Arc-en-ciel",
- "Fidelity": "Fidélité",
- "Fruit Salad": "Salade de fruits",
- "Tonal Spot": "Tonal Spot",
- "Content": "Contenu",
- "Use arrow keys to navigate.\nEnter to select, Esc to cancel.": "Utilise les flèches pour naviguer.\nEntrée pour sélectionner, Échap pour annuler.",
- "Lock": "Verrouiller",
- "Logout": "Se déconnecter",
- "Sleep": "Mettre en veille",
- "Hibernate": "Hibernation",
- "Shutdown": "Éteindre",
- "Reboot": "Redémarrer",
- "Cancel": "Annuler",
- "Cheat sheet": "Aide-mémoire",
- "Keybinds": "Raccourcis clavier",
- "Periodic table": "Tableau périodique",
- "Essentials for beginners": "Notions essentielles pour débutants",
- "Make shell elements transparent": "Rendre les éléments de l'interface transparents",
- "Actions": "Actions",
- "Window management": "Gestion des fenêtres",
- "Window arrangement": "Disposition des fenêtres",
- "Workspace management": "Gestion des espaces de travail",
- "Workspace navigation": "Navigation entre espaces de travail",
- "Widgets": "Widgets",
- "Media": "Médias",
- "Apps": "Applications",
- "Neutral": "Neutre",
- "Launch foot (terminal)": "Lancer foot (terminal)",
- "Open app launcher": "Ouvrir le lanceur d'applications",
- "Change wallpaper": "Changer le fond d'écran",
- "Clipboard history >> clipboard": "Historique du presse-papiers >> presse-papiers",
- "Pick emoji >> clipboard": "Choisir un emoji >> presse-papiers",
- "Screen snip >> edit": "Capture d'écran >> éditer",
- "Screen snip to text >> clipboard": "Capture d'écran en texte >> presse-papiers",
- "Pick color (Hex) >> clipboard": "Choisir une couleur (Hex) >> presse-papiers",
- "Screenshot >> clipboard": "Capture d'écran >> presse-papiers",
- "Screenshot >> clipboard & file": "Capture d'écran >> presse-papiers & fichier",
- "Record region (no sound)": "Enregistrer une région (sans son)",
- "Record screen (with sound)": "Enregistrer l'écran (avec son)",
- "Suspend system": "Suspendre le système",
- "Move focus in direction": "Déplacer le focus dans la direction",
- "Move window": "Déplacer la fenêtre",
- "Resize window": "Redimensionner la fenêtre",
- "Close window": "Fermer la fenêtre",
- "Pick and kill a window": "Sélectionner et fermer une fenêtre",
- "Window: move in direction": "Fenêtre : déplacer dans la direction",
- "Window: split ratio +/- 0.1": "Fenêtre : ajuster le ratio de partage +/- 0.1",
- "Float/unfloat window": "Basculer la fenêtre en mode flottant/non flottant",
- "Toggle fake fullscreen": "Basculer en faux plein écran",
- "Toggle fullscreen": "Basculer le plein écran",
- "Toggle maximization": "Basculer la maximisation",
- "Focus workspace # (1, 2, 3, 4, ...)": "Sélectionner l'espace de travail n° (1, 2, 3, 4, ...)",
- "Workspace: focus left/right": "Espace de travail : se déplacer à gauche/droite",
- "Workspace: toggle special": "Espace de travail : basculer spécial",
- "Window: move to workspace # (1, 2, 3, 4, ...)": "Fenêtre : déplacer vers l'espace de travail n° (1, 2, 3, 4, ...)",
- "Window: move to workspace left/right": "Fenêtre : déplacer vers l'espace de travail à gauche/droite",
- "Window: move to workspace special": "Fenêtre : déplacer vers l'espace de travail spécial",
- "Window: pin (show on all workspaces)": "Fenêtre : épingler (afficher sur tous les espaces de travail)",
- "Restart widgets": "Redémarrer les widgets",
- "Cycle bar mode (normal, focus)": "Cycler le mode de la barre (normal, focus)",
- "Toggle overview/launcher": "Basculer l'aperçu/le lanceur",
- "Show cheatsheet": "Afficher l'aide-mémoire",
- "Toggle left sidebar": "Basculer la barre latérale gauche",
- "Toggle right sidebar": "Basculer la barre latérale droite",
- "Toggle music controls": "Basculer les contrôles de musique",
- "View color scheme and options": "Voir le schéma de couleurs et les options",
- "Toggle power menu": "Basculer le menu d'alimentation",
- "Toggle crosshair": "Basculer le viseur",
- "Next track": "Piste suivante",
- "Previous track": "Piste précédente",
- "Play/pause media": "Lire/metre en pause le média",
- "Launch Zed (editor)": "Lancer Zed (éditeur)",
- "Launch VSCode (editor)": "Lancer VSCode (éditeur)",
- "Launch Nautilus (file manager)": "Lancer Nautilus (gestionnaire de fichiers)",
- "Launch Firefox (browser)": "Lancer Firefox (navigateur)",
- "Launch GNOME Text Editor": "Lancer GNOME Text Editor",
- "Launch WPS Office": "Lancer WPS Office",
- "Launch GNOME Settings": "Lancer les paramètres GNOME",
- "Launch pavucontrol (volume mixer)": "Lancer pavucontrol (contrôleur de volume)",
- "Launch EasyEffects (equalizer & other audio effects)": "Lancer EasyEffects (égaliseur et autres effets audio)",
- "Launch GNOME System monitor": "Lancer le moniteur système GNOME",
- "Toggle fallback launcher: anyrun": "Basculer le lanceur de secours : anyrun",
- "Toggle fallback launcher: fuzzel": "Basculer le lanceur de secours : fuzzel",
- "Initialization complete!": "Initialisation terminée !",
- "Not found": "Non trouvé :",
- "Calling API": "Appel de l'API",
- "Downloading image": "Téléchargement de l'image",
- "Finished!": "Terminé !",
- "Error": "Erreur",
- "Not found!": "Non trouvé !",
- "Go to file url": "Aller à l'URL du fichier",
- "Save image": "Enregistrer l'image",
- "Hoard": "Accumuler",
- "Open externally": "Ouvrir avec une application externe",
- "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!": "Tu es un assistant dans un panneau latéral d'un bureau Linux sous Wayland. Veilles à toujours adopter un ton décontracté lorsque tu réponds, sauf indication contraire ou lors de suggestions d'écriture. Voici les étapes à suivre pour répondre aux requêtes de l'utilisateur :\n1. S'il s'agit d'une question liée à l'écriture ou à la grammaire, ou d'une phrase entre guillemets, signales les erreurs et corriges-les si nécessaire en utilisant des soulignements, et rends l'écriture plus naturelle lorsque cela est approprié sans apporter de modifications trop importantes. Si on te fournit une phrase entre guillemets mais grammaticalement correcte, expliques brièvement des concepts peu communs.\n2. S'il s'agit d'une question concernant des tâches système, fournis une commande bash dans un bloc de code avec une brève explication.\n3. Sinon, lorsqu'on te demande de résumer des informations ou d'expliquer des concepts, utilises des puces et des titres. Pour les expressions mathématiques, tu *dois* utiliser LaTeX dans un bloc de code avec la langue définie sur \"latex\".\nNote : Utilises un langage décontracté, sois bref, tout en garantissant l'exactitude des informations de ta réponse. Si tu n'es pas sûr ou si tu ne disposes pas d'assez d'informations pour fournir une réponse convaincante, dis simplement \"Je ne sais pas\" ou \"Je ne suis pas sûr\".\nMerci !",
- "Feels like": "Ressenti"
- }
-
\ No newline at end of file
diff --git a/.config/ags/i18n/locales/it_IT.json b/.config/ags/i18n/locales/it_IT.json
deleted file mode 100644
index b572d1b92..000000000
--- a/.config/ags/i18n/locales/it_IT.json
+++ /dev/null
@@ -1,238 +0,0 @@
-{
- "No media": "Nessun media",
- "Powered by Google": "Offerto da Google",
- "Not affiliated, endorsed, or sponsored by Google.\n\nPrivacy: Chat messages aren't linked to your account,\nbut will be read by human reviewers to improve the model.": "Non affiliato, approvato o sponsorizzato da Google.\n\nPrivacy: I messaggi della chat non sono collegati al tuo account,\n ma saranno letti da revisori umani per migliorare il modello.",
- "Precise": "Preciso",
- "Balanced": "Bilanciato",
- "Creative": "Creativo",
- "Gemini's temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1": "Valore di temperatura di Gemini.\n Preciso = 0\n Bilanciato = 0.5\n Creativo = 1",
- "Enhancements": "Miglioramenti",
- "Tells Gemini:\n- It's a Linux sidebar assistant\n- Be brief and use bullet points": "Dice a Gemini:\n- È un assistente laterale per Linux\n- Sii breve e usa punti elenco",
- "Safety": "Sicurezza",
- "When turned off, tells the API (not the model) \nto not block harmful/explicit content": "Quando disattivato, dice all'API (non al modello) \n di non bloccare contenuti dannosi/espliciti",
- "History": "Cronologia",
- "Saves chat history\nMessages in previous chats won't show automatically, but they are there": "Salva la cronologia della chat\nI messaggi nelle chat precedenti non verranno mostrati automaticamente, ma sono lì",
- "Key stored in:": "Chiave memorizzata in:",
- "To update this key, type": "Per aggiornare questa chiave, digita",
- "Updated API Key at": "Chiave API aggiornata alle",
- "Currently using": "Attualmente in uso",
- "Select ChatGPT-compatible API provider": "Seleziona un provider API compatibile con ChatGPT",
- "Official OpenAI API.\nPricing: Free for the first $5 or 3 months, whichever is less.": "API ufficiale di OpenAI.\nPrezzi: Gratuito per i primi $5 o 3 mesi, a seconda di quale sia inferiore.",
- "Official Ollama API.\nPricing: Free.": "API ufficiale di Ollama.\nPrezzi: Gratuito.",
- "A unified interface for LLMs": "Un'interfaccia unificata per LLM",
- "An API from Tornado Softwares\nPricing: Free: 100/day\nRequires you to join their Discord for a key": "Un'API di Tornado Softwares\nPrezzi: Gratuito: 100/giorno\nRichiede di unirsi al loro Discord per una chiave",
- "An API from @zukixa on GitHub.\nNote: Keys are IP-locked so it's buggy sometimes\nPricing: Free: 10/min, 800/day.\nRequires you to join their Discord for a key": "Un'API di @zukixa su GitHub.\nNota: Le chiavi sono bloccate per IP, quindi a volte è instabile\nPrezzi: Gratuito: 10/min, 800/giorno.\nRichiede di unirsi al loro Discord per una chiave",
- "Provider shown above": "Provider mostrato sopra",
- "The model's temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1": "Valore di temperatura del modello.\n Preciso = 0\n Bilanciato = 0.5\n Creativo = 1",
- "An API key is required\nYou can grab one here , then enter it below": "È necessaria una chiave API\nPuoi ottenerne una qui , quindi inserirla qui sotto",
- "Tells the model:\n- It's a Linux sidebar assistant\n- Be brief and use bullet points": "Dice al modello:\n- È un assistente laterale per Linux\n- Sii breve e usa punti elenco",
- "Powered by waifu.im + other APIs": "Offerto da waifu.im + altre API",
- "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.": "Digita i tag per un'immagine casuale.\nIl contenuto NSFW non verrà restituito a meno che\nnon richiedi esplicitamente un tale tag.\n\nDisclaimer: Non affiliato ai provider\nné responsabile per alcun loro contenuto.",
- "Tags →": "Tag →",
- "Invalid command.": "Comando non valido.",
- "Anime booru": "Anime booru",
- "Powered by yande.re and konachan": "Offerto da yande.re e konachan",
- "An image booru. May contain NSFW content.\nWatch your back.\n\nDisclaimer: Not affiliated with the provider\nnor responsible for any of its content.": "Un booru di immagini. Potrebbe contenere contenuti NSFW.\nFai attenzione.\n\nDisclaimer: Non affiliato al provider\nné responsabile per alcun suo contenuto.",
- "Lewds": "Contenuti osé",
- "Shows naughty stuff when enabled": "Mostra contenuti osé quando abilitato",
- "Saves images in folders by their tags": "Salva le immagini in cartelle in base ai loro tag",
- "Message Gemini...": "Messaggia Gemini...",
- "Enter Google AI API Key...": "Inserisci la chiave API di Google AI...",
- "Message the model...": "Messaggia il modello...",
- "Enter API Key...": "Inserisci la chiave API...",
- "Enter tags": "Inserisci i tag",
- "Quick scripts": "Script rapidi",
- "Change screen resolution": "Cambia risoluzione dello schermo",
- "Update packages": "Aggiorna i pacchetti",
- "Trim system generations to 5": "Riduci le generazioni del sistema a 5",
- "Trim home manager generations to 5": "Riduci le generazioni del gestore di casa a 5",
- "Remove orphan packages": "Rimuovi i pacchetti orfani",
- "Uninstall unused flatpak packages": "Disinstalla i pacchetti flatpak non utilizzati",
- "Inaccurate Color picker": "Impreciso Selettore di colore",
- "Result": "Risultato",
- "Type to search": "Digita per cercare",
- "illogical-impulse": "impulso illogico",
- "RAM Usage": "Utilizzo della RAM",
- "Swap Usage": "Utilizzo dello Swap",
- "CPU Usage": "Utilizzo della CPU",
- "Uptime:": "Tempo di attività:",
- "Screen snip": "Ritaglio dello schermo",
- "Color picker": "Selettore di colore",
- "Toggle on-screen keyboard": "Attiva/disattiva tastiera su schermo",
- "Night Light": "Luce notturna",
- "Color inversion": "Inversione di colore",
- "Keep system awake": "Mantieni il sistema sveglio",
- "Cloudflare WARP": "Cloudflare WARP",
- "Session": "Sessione",
- "Bluetooth | Right-click to configure": "Bluetooth | Clic destro per configurare",
- "Wifi | Right-click to configure": "Wifi | Clic destro per configurare",
- "Right-click to configure": "Clic destro per configurare",
- "Unknown": "Sconosciuto",
- "Reload Environment config": "Ricarica la configurazione dell'ambiente",
- "Open Settings": "Apri Impostazioni",
- "Notifications": "Notifiche",
- "Audio controls": "Controlli audio",
- "Bluetooth": "Bluetooth",
- "Wifi networks": "Reti Wifi",
- "Quick config": "Configurazione",
- "Silence": "Silenzio",
- "Clear": "Cancella",
- "No notifications": "Nessuna notifica",
- "notifications": "notifiche",
- "Close": "Chiudi",
- "Now": "Ora",
- "Yesterday": "Ieri",
- "No audio source": "Nessuna sorgente audio",
- "Remove device": "Rimuovi dispositivo",
- "Connected": "Connesso",
- "Paired": "Abbinato",
- "More": "Altro",
- "Selected": "Selezionato",
- "Current network": "Rete corrente",
- "Authentication": "Autenticazione",
- "Effects": "Effetti",
- "Transparency": "Trasparenza",
- "[AGS]\nMake shell elements transparent\nBlur is also recommended if you enable this": "[AGS]\nRendi trasparenti gli elementi della shell\nSi consiglia anche il blur se abiliti questa opzione",
- "Blur": "Blur",
- "[Hyprland]\nEnable blur on transparent elements\nDoesn't affect performance/power consumption unless you have transparent windows.": "[Hyprland]\nAbilita il blur sugli elementi trasparenti\nNon influisce sulle prestazioni/consumo energetico a meno che non si abbiano finestre trasparenti.",
- "X-ray": "Raggi X",
- "[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) ": "[Hyprland]\nRendi tutto dietro una finestra/livello, tranne lo sfondo, non renderizzato sulla sua superficie sfocata\nConsigliato per migliorare le prestazioni (se non abusi di trasparenza/blur) ",
- "Size": "Dimensione",
- "[Hyprland]\nAdjust the blur radius. Generally doesn't affect performance\nHigher = more color spread": "[Hyprland]\nRegola il raggio del blur. Generalmente non influisce sulle prestazioni\nPiù alto = più diffusione del colore",
- "Passes": "Passaggi",
- "[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.": "[Hyprland] Regola il numero di esecuzioni dell'algoritmo di blur\nPiù passaggi = più diffusione e consumo energetico\nSi consigliano 4\n2- sembrerebbe strano e 6+ sembrerebbe scadente.",
- "Animations": "Animazioni",
- "[Hyprland] [GTK]\nEnable animations": "[Hyprland] [GTK]\nAbilita le animazioni",
- "Choreography delay": "Ritardo della coreografia",
- "In milliseconds, the delay between animations of a series": "In millisecondi, il ritardo tra le animazioni di una serie",
- "Developer": "Sviluppatore",
- "Show FPS": "Mostra FPS",
- "[Hyprland]\nShow FPS overlay on top-left corner": "[Hyprland]\nMostra l'overlay degli FPS nell'angolo in alto a sinistra",
- "Log to stdout": "Log su stdout",
- "[Hyprland]\nPrint LOG, ERR, WARN, etc. messages to the console": "[Hyprland]\nStampa i messaggi LOG, ERR, WARN, ecc. sulla console",
- "Damage tracking": "Tracciamento dei danni",
- "[Hyprland]\nEnable damage tracking\nGenerally, leave it on.\nTurn off only when a shader doesn't work": "[Hyprland]\nAbilita il tracciamento dei danni\nGeneralmente, lascialo acceso.\nDisattivalo solo quando uno shader non funziona",
- "Damage blink": "Lampeggio dei danni",
- "[Hyprland] [Epilepsy warning!]\nShow screen damage flashes": "[Hyprland] [Avviso epilessia!]\nMostra i lampeggi dei danni dello schermo",
- "Not all changes are saved": "Non tutte le modifiche sono state salvate",
- "Mo": "Lu",
- "Tu": "Ma",
- "We": "Me",
- "Th": "Gi",
- "Fr": "Ve",
- "Sa": "Sa",
- "Su": "Do",
- "Calendar": "Calendario",
- "To Do": "Da fare",
- "Unfinished": "Incompleto",
- "Done": "Fatto",
- "Finished tasks will go here": "Le attività completate andranno qui",
- "Nothing here!": "Niente qui!",
- "+ New task": "+ Nuova attività",
- "Add a task...": "Aggiungi un'attività...",
- "Color scheme": "Schema di colori",
- "Options": "Opzioni",
- "Dark Mode": "Modalità scura",
- "Ya should go to sleep!": "Dovresti andare a dormire!",
- "Theme GTK apps using accent color\n(drawback: dark/light mode switching requires restart)": "Tema le app GTK usando il colore di accento\n(svantaggio: il passaggio tra modalità scura/chiara richiede un riavvio)",
- "Scheme styles": "Stili dello schema",
- "Vibrant": "Vivace",
- "Vibrant+": "Vivace+",
- "Expressive": "Espressivo",
- "Monochrome": "Monocromatico",
- "Rainbow": "Arcobaleno",
- "Fidelity": "Fedeltà",
- "Fruit Salad": "Macedonia di frutta",
- "Tonal Spot": "Punto tonale",
- "Content": "Contenuto",
- "Use arrow keys to navigate.\nEnter to select, Esc to cancel.": "Usa i tasti freccia per navigare.\nInvio per selezionare, Esc per annullare.",
- "Lock": "Blocca",
- "Logout": "Esci",
- "Sleep": "Sospendi",
- "Hibernate": "Iberna",
- "Shutdown": "Spegni",
- "Reboot": "Riavvia",
- "Cancel": "Annulla",
- "Cheat sheet": "Foglio di riferimento",
- "Keybinds": "Scorciatoie da tastiera",
- "Periodic table": "Tavola periodica",
- "Essentials for beginners": "Essenziali per principianti",
- "Make shell elements transparent": "Rendi trasparenti gli elementi della shell",
- "Actions": "Azioni",
- "Window management": "Gestione delle finestre",
- "Window arrangement": "Disposizione delle finestre",
- "Workspace management": "Gestione degli spazi di lavoro",
- "Workspace navigation": "Navigazione degli spazi di lavoro",
- "Widgets": "Widget",
- "Media": "Media",
- "Apps": "App",
- "Neutral": "Neutro",
- "Launch foot (terminal)": "Avvia foot (terminale)",
- "Open app launcher": "Apri il launcher delle app",
- "Change wallpaper": "Cambia lo sfondo",
- "Clipboard history >> clipboard": "Cronologia degli appunti >> appunti",
- "Pick emoji >> clipboard": "Scegli emoji >> appunti",
- "Screen snip >> edit": "Ritaglio dello schermo >> modifica",
- "Screen snip to text >> clipboard": "Ritaglio dello schermo in testo >> appunti",
- "Pick color (Hex) >> clipboard": "Scegli colore (Hex) >> appunti",
- "Screenshot >> clipboard": "Screenshot >> appunti",
- "Screenshot >> clipboard & file": "Screenshot >> appunti e file",
- "Record region (no sound)": "Registra regione (senza audio)",
- "Record screen (with sound)": "Registra schermo (con audio)",
- "Suspend system": "Sospendi il sistema",
- "Move focus in direction": "Sposta il focus nella direzione",
- "Move window": "Sposta la finestra",
- "Resize window": "Ridimensiona la finestra",
- "Close window": "Chiudi la finestra",
- "Pick and kill a window": "Scegli e chiudi una finestra",
- "Window: move in direction": "Finestra: sposta nella direzione",
- "Window: split ratio +/- 0.1": "Finestra: rapporto di divisione +/- 0.1",
- "Float/unfloat window": "Finestra libera/agganciata",
- "Toggle fake fullscreen": "Attiva/disattiva falso fullscreen",
- "Toggle fullscreen": "Attiva/disattiva fullscreen",
- "Toggle maximization": "Attiva/disattiva massimizzazione",
- "Focus workspace # (1, 2, 3, 4, ...)": "Focalizza spazio di lavoro # (1, 2, 3, 4, ...)",
- "Workspace: focus left/right": "Spazio di lavoro: focalizza sinistra/destra",
- "Workspace: toggle special": "Spazio di lavoro: attiva/disattiva speciale",
- "Window: move to workspace # (1, 2, 3, 4, ...)": "Finestra: sposta in spazio di lavoro # (1, 2, 3, 4, ...)",
- "Window: move to workspace left/right": "Finestra: sposta in spazio di lavoro sinistra/destra",
- "Window: move to workspace special": "Finestra: sposta in spazio di lavoro speciale",
- "Window: pin (show on all workspaces)": "Finestra: fissa (mostra su tutti gli spazi di lavoro)",
- "Restart widgets": "Riavvia widget",
- "Cycle bar mode (normal, focus)": "Cicla modalità barra (normale, focus)",
- "Toggle overview/launcher": "Attiva/disattiva panoramica/lanciatore",
- "Show cheatsheet": "Mostra cheatsheet",
- "Toggle left sidebar": "Attiva/disattiva barra laterale sinistra",
- "Toggle right sidebar": "Attiva/disattiva barra laterale destra",
- "Toggle music controls": "Attiva/disattiva controlli musicali",
- "View color scheme and options": "Visualizza schema colori e opzioni",
- "Toggle power menu": "Attiva/disattiva menu di spegnimento",
- "Toggle crosshair": "Attiva/disattiva mirino",
- "Next track": "Traccia successiva",
- "Previous track": "Traccia precedente",
- "Play/pause media": "Riproduci/pausa media",
- "Launch Zed (editor)": "Avvia Zed (editor)",
- "Launch VSCode (editor)": "Avvia VSCode (editor)",
- "Launch Nautilus (file manager)": "Avvia Nautilus (gestore file)",
- "Launch Firefox (browser)": "Avvia Firefox (browser)",
- "Launch GNOME Text Editor": "Avvia GNOME Text Editor",
- "Launch WPS Office": "Avvia WPS Office",
- "Launch GNOME Settings": "Avvia Impostazioni GNOME",
- "Launch pavucontrol (volume mixer)": "Avvia pavucontrol (mixer volume)",
- "Launch EasyEffects (equalizer & other audio effects)": "Avvia EasyEffects (equalizzatore & altri effetti audio)",
- "Launch GNOME System monitor": "Avvia monitor di sistema GNOME",
- "Toggle fallback launcher: anyrun": "Attiva/disattiva lanciatore di riserva: anyrun",
- "Toggle fallback launcher: fuzzel": "Attiva/disattiva lanciatore di riserva: fuzzel",
- "Initialization complete!": "Inizializzazione completata!",
- "Not found": "Non trovato",
- "Calling API": "Chiamata API",
- "Downloading image": "Scaricamento immagine",
- "Finished!": "Completato!",
- "Error": "Errore",
- "Not found!": "Non trovato!",
- "Go to file url": "Vai all'url del file",
- "Save image": "Salva immagine",
- "Hoard": "Accumula",
- "Open externally": "Apri esternamente",
- "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!": "Sei un assistente nella barra laterale di un desktop Wayland Linux. Usa sempre un tono informale quando rispondi alle domande, a meno che non venga richiesto diversamente o quando fai suggerimenti di scrittura. Ecco i passaggi che devi seguire per rispondere alle domande dell'utente:\n1. Se si tratta di una domanda sulla scrittura o sulla grammatica o di una frase tra virgolette, segnala gli errori e correggi quando necessario utilizzando sottolineature, e rendi la scrittura più naturale dove appropriato senza fare modifiche troppo importanti. Se ti viene data una frase tra virgolette ma è grammaticalmente corretta, spiega brevemente concetti che sono poco comuni.\n2. Se è una domanda su attività di sistema, fornisci un comando bash in un blocco di codice con una breve spiegazione.\n3. Altrimenti, quando ti viene chiesto di riassumere informazioni o spiegare concetti, usa punti elenco e titoli. Per espressioni matematiche, *devi* usare LaTeX all'interno di un blocco di codice con il linguaggio impostato su \"latex\". \nNota: usa un linguaggio informale, sii breve, assicurandoti della correttezza fattuale della tua risposta. Se non sei sicuro o non hai abbastanza informazioni per fornire una risposta sicura, semplicemente dici “Non lo so” o “Non sono sicuro”. \nGrazie!"
-}
\ No newline at end of file
diff --git a/.config/ags/i18n/locales/zh_CN.json b/.config/ags/i18n/locales/zh_CN.json
deleted file mode 100644
index 906090be5..000000000
--- a/.config/ags/i18n/locales/zh_CN.json
+++ /dev/null
@@ -1,239 +0,0 @@
-{
- "No media": "无媒体活动",
- "Powered by Google": "由 Google 提供技术支持",
- "Not affiliated, endorsed, or sponsored by Google.\n\nPrivacy: Chat messages aren't linked to your account,\nbut will be read by human reviewers to improve the model.": "不隶属于、不受 Google 赞助或支持。\n\n隐私:聊天信息不会与你的账户关联,\n但会被人类审阅者阅读,用于改进模型。",
- "Precise": "精确",
- "Balanced": "平衡",
- "Creative": "创意",
- "Gemini's temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1": "Gemini 的 temperature 值\n 精确 = 0\n 平衡 = 0.5\n 创意 = 1",
- "Enhancements": "增强功能",
- "Tells Gemini:\n- It's a Linux sidebar assistant\n- Be brief and use bullet points": "告诉 Gemini:\n- 它是一个 Linux 侧边栏助手\n- 保持简洁并使用项目符号",
- "Safety": "安全",
- "When turned off, tells the API (not the model) \nto not block harmful/explicit content": "当关闭时,告诉 API(而不是模型)\n不要屏蔽有害/显露的内容",
- "History": "历史",
- "Saves chat history\nMessages in previous chats won't show automatically, but they are there": "保存聊天历史\n以前聊天中的消息不会自动显示,但它们仍然存在",
- "Key stored in:": "密钥值储存在:",
- "To update this key, type": "要更新此密钥,请输入",
- "Updated API Key at": "更新了 API 密钥于",
- "Currently using": "当前使用",
- "Select ChatGPT-compatible API provider": "选择与 ChatGPT 兼容的 API 提供商",
- "Official OpenAI API.\nPricing: Free for the first $5 or 3 months, whichever is less.": "官方 OpenAI API。\n定价:前 $5 或前 3 个月免费,取较小者。",
- "Official Ollama API.\nPricing: Free.": "官方 Ollama API。\n定价:免费。",
- "A unified interface for LLMs": "LLM 的统一接口",
- "An API from Tornado Softwares\nPricing: Free: 100/day\nRequires you to join their Discord for a key": "来自 Tornado Softwares 的 API\n定价:免费:每天 100 次请求\n需要加入他们的 Discord 以获取密钥",
- "An API from @zukixa on GitHub.\nNote: Keys are IP-locked so it's buggy sometimes\nPricing: Free: 10/min, 800/day.\nRequires you to join their Discord for a key": "来自 GitHub 上的 @zukixa 的 API。\n注意:密钥与 IP 绑定,所以有时会出错。\n定价:免费:每分钟 10 次,每天 800 次。\n需要加入他们的 Discord 才能获得密钥。",
- "Provider shown above": "上述显示的提供商",
- "The model's temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1": "模型的 temperature 值。\n 精确 = 0\n 平衡 = 0.5\n 创意 = 1",
- "An API key is required\nYou can grab one here , then enter it below": "需要 API 密钥\n您可以在这里 获取一个,然后在下面输入",
- "Tells the model:\n- It's a Linux sidebar assistant\n- Be brief and use bullet points": "告诉模型:\n- 它是一个 Linux 侧边栏助手\n- 保持简洁并使用项目符号",
- "Powered by waifu.im + other APIs": "由 waifu.im + 其他 API 提供支持",
- "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.": "输入标签以获取随机图片。\n除非您明确请求,否则不会返回 NSFW 内容。\n\n免责声明:与提供商无关联\n我也不对他们的任何内容负责。",
- "Tags →": "标签 →",
- "Invalid command.": "无效命令。",
- "Anime booru": "动漫图库",
- "Powered by yande.re and konachan": "由 yande.re 和 konachan 提供支持",
- "An image booru. May contain NSFW content.\nWatch your back.\n\nDisclaimer: Not affiliated with the provider\nnor responsible for any of its content.": "一个图片图库。可能包含 NSFW 内容。\n小心。\n\n免责声明:与提供商无关联\n也不对它的任何内容负责。",
- "Lewds": "不雅内容",
- "Shows naughty stuff when enabled": "启用时显示不雅内容",
- "Saves images in folders by their tags": "按标签将图片保存到文件夹中",
- "Message Gemini...": "向 Gemini 发送消息...",
- "Enter Google AI API Key...": "输入 Google AI API 密钥...",
- "Message the model...": "向模型发送消息...",
- "Enter API Key...": "输入 API 密钥...",
- "Enter tags": "输入标签",
- "Quick scripts": "快速脚本",
- "Change screen resolution": "更改屏幕分辨率",
- "Update packages": "更新软件包",
- "Trim system generations to 5": "将系统代数修剪为 5",
- "Trim home manager generations to 5": "将 home manager 代数修剪为 5",
- "Remove orphan packages": "移除孤立软件包",
- "Uninstall unused flatpak packages": "卸载未使用的 Flatpak 软件包",
- "Inaccurate Color picker": "不准确 颜色选择器",
- "Result": "结果",
- "Type to search": "输入以搜索",
- "illogical-impulse": "illogical-impulse",
- "RAM Usage": "RAM 使用情况",
- "Swap Usage": "Swap 使用情况",
- "CPU Usage": "CPU 使用情况",
- "Uptime:": "运行时间:",
- "Screen snip": "屏幕截图",
- "Color picker": "颜色选择器",
- "Toggle on-screen keyboard": "切换屏幕键盘",
- "Night Light": "夜灯",
- "Color inversion": "颜色反转",
- "Keep system awake": "保持系统唤醒",
- "Cloudflare WARP": "Cloudflare WARP",
- "Session": "会话",
- "Bluetooth | Right-click to configure": "蓝牙 | 右键单击以配置",
- "Wifi | Right-click to configure": "Wi-Fi | 右键单击以配置",
- "Right-click to configure": "右键单击以配置",
- "Unknown": "未知",
- "Reload Environment config": "重新加载环境配置",
- "Open Settings": "打开设置",
- "Notifications": "通知",
- "Audio controls": "音频控制",
- "Bluetooth": "蓝牙",
- "Wifi networks": "Wi-Fi 网络",
- "Quick config": "实时配置",
- "Silence": "静音",
- "Clear": "清除",
- "No notifications": "无通知",
- "notifications": "条通知",
- "Close": "关闭",
- "Now": "现在",
- "Yesterday": "昨天",
- "No audio source": "没有音频源",
- "Remove device": "移除设备",
- "Connected": "已连接",
- "Paired": "已配对",
- "More": "更多",
- "Selected": "已选中",
- "Current network": "当前网络",
- "Authentication": "身份验证",
- "Effects": "效果",
- "Transparency": "透明度",
- "[AGS]\nMake shell elements transparent\nBlur is also recommended if you enable this": "[AGS]\n使外壳元素透明\n如果启用此功能,也建议使用模糊效果",
- "Blur": "模糊",
- "[Hyprland]\nEnable blur on transparent elements\nDoesn't affect performance/power consumption unless you have transparent windows.": "[Hyprland]\n在透明元素上启用模糊效果\n除非您有透明窗口,否则不会影响性能/功耗。",
- "X-ray": "X-ray",
- "[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) ": "[Hyprland]\n使窗口/图层后面的所有内容(除了壁纸)在其模糊表面上不渲染\n建议提高性能(如果您不滥用透明度/模糊)",
- "Size": "大小",
- "[Hyprland]\nAdjust the blur radius. Generally doesn't affect performance\nHigher = more color spread": "[Hyprland]\n调整模糊半径。通常不会影响性能\n数值越高 = 颜色扩散越大",
- "Passes": "次数",
- "[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.": "[Hyprland]\n调整模糊算法的运行次数\n次数越多 = 扩散越大,功耗越高\n建议使用 4 次\n2 次看起来很奇怪,6 次以上看起来很糟糕。",
- "Animations": "动画",
- "[Hyprland] [GTK]\nEnable animations": "[Hyprland] [GTK]\n启用动画",
- "Choreography delay": "间隔",
- "In milliseconds, the delay between animations of a series": "以毫秒为单位,一系列动画之间的延迟",
- "Developer": "开发者选项",
- "Show FPS": "显示 FPS",
- "[Hyprland]\nShow FPS overlay on top-left corner": "[Hyprland]\n在左上角显示 FPS 叠加层",
- "Log to stdout": "输出日志",
- "[Hyprland]\nPrint LOG, ERR, WARN, etc. messages to the console": "[Hyprland]\n将 LOG、ERR、WARN 等消息打印到控制台",
- "Damage tracking": "Damage tracking",
- "[Hyprland]\nEnable damage tracking\nGenerally, leave it on.\nTurn off only when a shader doesn't work": "[Hyprland]\n启用 Damage tracking \n通常情况下,保持启用状态\n仅当着色器无法正常工作时才禁用",
- "Damage blink": "显示视图更新",
- "[Hyprland] [Epilepsy warning!]\nShow screen damage flashes": "[Hyprland] [癫痫警告!]\n屏幕视图更新时闪烁",
- "Not all changes are saved": "并非所有更改都已保存",
- "Mo": "一",
- "Tu": "二",
- "We": "三",
- "Th": "四",
- "Fr": "五",
- "Sa": "六",
- "Su": "日",
- "Calendar": "日历",
- "To Do": "待办",
- "Unfinished": "未完成",
- "Done": "已完成",
- "Finished tasks will go here": "已完成的任务将显示在此处",
- "Nothing here!": "这里什么也没有!",
- "+ New task": "+ 新任务",
- "Add a task...": "添加任务...",
- "Color scheme": "配色方案",
- "Options": "选项",
- "Dark Mode": "深色模式",
- "Ya should go to sleep!": "你应该去睡觉!",
- "Theme GTK apps using accent color\n(drawback: dark/light mode switching requires restart)": "使用强调色对 GTK 应用程序进行主题化\n(缺点:深色/浅色模式切换需要重启)",
- "Scheme styles": "样式方案",
- "Vibrant": "鲜艳",
- "Vibrant+": "鲜艳+",
- "Expressive": "表现力",
- "Monochrome": "黑白",
- "Rainbow": "彩虹",
- "Fidelity": "保真度",
- "Fruit Salad": "水果沙拉",
- "Tonal Spot": "色调点",
- "Content": "内容",
- "Use arrow keys to navigate.\nEnter to select, Esc to cancel.": "使用箭头键导航。\n回车键选择,Esc 键取消。",
- "Lock": "锁屏",
- "Logout": "注销",
- "Sleep": "睡眠",
- "Hibernate": "休眠",
- "Shutdown": "关机",
- "Reboot": "重启",
- "Cancel": "取消",
- "Cheat sheet": "备忘单",
- "Keybinds": "按键绑定",
- "Periodic table": "元素周期表",
- "Essentials for beginners": "初学者必备",
- "Make shell elements transparent": "使外壳元素透明",
- "Actions": "操作",
- "Window management": "窗口管理",
- "Window arrangement": "窗口排列",
- "Workspace management": "工作区管理",
- "Workspace navigation": "工作区导航",
- "Widgets": "小部件",
- "Media": "媒体",
- "Apps": "应用程序",
- "Neutral": "中性",
- "Launch foot (terminal)": "启动终端(foot)",
- "Open app launcher": "打开应用程序启动器",
- "Change wallpaper": "更改壁纸",
- "Clipboard history >> clipboard": "剪贴板历史 >> 剪贴板",
- "Pick emoji >> clipboard": "选择表情符号 >> 剪贴板",
- "Screen snip >> edit": "屏幕截图 >> 编辑",
- "Screen snip to text >> clipboard": "屏幕截图转文字 >> 剪贴板",
- "Pick color (Hex) >> clipboard": "选择颜色(十六进制)>> 剪贴板",
- "Screenshot >> clipboard": "屏幕截图 >> 剪贴板",
- "Screenshot >> clipboard & file": "屏幕截图 >> 剪贴板和文件",
- "Record region (no sound)": "录制区域(无声音)",
- "Record screen (with sound)": "录制屏幕(带声音)",
- "Suspend system": "挂起系统",
- "Move focus in direction": "在方向上移动焦点",
- "Move window": "移动窗口",
- "Resize window": "调整窗口大小",
- "Close window": "关闭窗口",
- "Pick and kill a window": "选择并关闭一个窗口",
- "Window: move in direction": "窗口:在方向上移动",
- "Window: split ratio +/- 0.1": "窗口:分割比例 +/- 0.1",
- "Float/unfloat window": "浮动/取消浮动窗口",
- "Toggle fake fullscreen": "切换伪全屏",
- "Toggle fullscreen": "切换全屏",
- "Toggle maximization": "切换最大化",
- "Focus workspace # (1, 2, 3, 4, ...)": "聚焦工作区 #(1, 2, 3, 4, ...)",
- "Workspace: focus left/right": "工作区:聚焦左右",
- "Workspace: toggle special": "工作区:切换特殊工作区",
- "Window: move to workspace # (1, 2, 3, 4, ...)": "窗口:移动到工作区 #(1, 2, 3, 4, ...)",
- "Window: move to workspace left/right": "窗口:移动到左右工作区",
- "Window: move to workspace special": "窗口:移动到特殊工作区",
- "Window: pin (show on all workspaces)": "窗口:固定(在所有工作区显示)",
- "Restart widgets": "重启小部件",
- "Cycle bar mode (normal, focus)": "循环栏模式(正常,聚焦)",
- "Toggle overview/launcher": "切换概览/启动器",
- "Show cheatsheet": "显示快捷键表",
- "Toggle left sidebar": "切换左侧边栏",
- "Toggle right sidebar": "切换右侧边栏",
- "Toggle music controls": "切换音乐控制",
- "View color scheme and options": "查看配色方案和选项",
- "Toggle power menu": "切换电源菜单",
- "Toggle crosshair": "切换准星",
- "Next track": "下一曲目",
- "Previous track": "上一曲目",
- "Play/pause media": "播放/暂停媒体",
- "Launch Zed (editor)": "启动 Zed(编辑器)",
- "Launch VSCode (editor)": "启动 VSCode(编辑器)",
- "Launch Nautilus (file manager)": "启动 Nautilus(文件管理器)",
- "Launch Firefox (browser)": "启动 Firefox(浏览器)",
- "Launch GNOME Text Editor": "启动 GNOME 文本编辑器",
- "Launch WPS Office": "启动 WPS 办公软件",
- "Launch GNOME Settings": "启动 GNOME 设置",
- "Launch pavucontrol (volume mixer)": "启动 pavucontrol(音量混合器)",
- "Launch EasyEffects (equalizer & other audio effects)": "启动 EasyEffects(均衡器和其他音频效果)",
- "Launch GNOME System monitor": "启动 GNOME 系统监视器",
- "Toggle fallback launcher: anyrun": "切换备用启动器:anyrun",
- "Toggle fallback launcher: fuzzel": "切换备用启动器:fuzzel",
- "Initialization complete!": "初始化完成!",
- "Not found": "未找到",
- "Calling API": "调用 API",
- "Downloading image": "正在下载图片",
- "Finished!": "完成!",
- "Error": "错误",
- "Not found!": "未找到!",
- "Go to file url": "前往文件链接",
- "Save image": "保存图片",
- "Hoard": "保存",
- "Open externally": "在外部打开",
- "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!": "你是 Wayland Linux 桌面侧边栏上的助手。除非有其他要求或提供建议,否则请始终保持轻松的语气回答问题。这是你回答用户查询的步骤:\n1. 如果是写作或语法相关的问题,或者引号中的句子,请指出错误并在必要时进行更正,使用下划线,并在适当的地方使写作更自然,不要进行太大更改。如果你给出的句子在引号中但语法正确,请简要解释不常见概念。\n2. 如果是关于系统任务的问题,请给出bash命令,并在代码块中简要说明。\n3. 否则,在总结信息或解释概念时,你应该使用项目符号和标题。对于数学表达式,你必须在代码块中使用 LaTeX,并将语言设置为\"latex\"。\n注意:使用轻松的语言,简洁,同时确保回答的事实正确性。如果你不确定或没有足够的信息来提供自信的答案,只需说“我不知道”或“我不确定”。\n谢谢!",
- "Feels like": "体感温度"
-}
diff --git a/.config/ags/init.js b/.config/ags/init.js
deleted file mode 100644
index 023043541..000000000
--- a/.config/ags/init.js
+++ /dev/null
@@ -1,53 +0,0 @@
-const { Gio, GLib } = imports.gi;
-import GtkSource from "gi://GtkSource?version=3.0";
-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';
-
-const CUSTOM_SOURCEVIEW_SCHEME_PATH = `${App.configDir}/assets/themes/sourceviewtheme${darkMode.value ? '' : '-light'}.xml`;
-
-export const COMPILED_STYLE_DIR = `${GLib.get_user_cache_dir()}/ags/user/generated`
-
-function loadSourceViewColorScheme(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());
-}
-
-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().then(() => {
- loadSourceViewColorScheme(CUSTOM_SOURCEVIEW_SCHEME_PATH);
- }).catch(print);
-}
diff --git a/.config/ags/modules/.commondata/hyprlanddata.js b/.config/ags/modules/.commondata/hyprlanddata.js
deleted file mode 100644
index 395f2cd1b..000000000
--- a/.config/ags/modules/.commondata/hyprlanddata.js
+++ /dev/null
@@ -1,33 +0,0 @@
-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"
- if (monitor.transform % 2 == 1) { // Vertical monitors (or horizontal monitor that's vertical by default...)
- monitor.width = Math.floor(monitor.realHeight / monitor.scale);
- monitor.height = Math.floor(monitor.realWidth / monitor.scale);
- }
- else {
- monitor.width = Math.ceil(monitor.realWidth / monitor.scale);
- monitor.height = Math.ceil(monitor.realHeight / monitor.scale);
- }
- }
- });
-}
-
-updateStuff().catch(print);
-
diff --git a/.config/ags/modules/.commondata/quotes.js b/.config/ags/modules/.commondata/quotes.js
deleted file mode 100644
index 7cea99075..000000000
--- a/.config/ags/modules/.commondata/quotes.js
+++ /dev/null
@@ -1,14 +0,0 @@
-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/.config/ags/modules/.commondata/weather.js b/.config/ags/modules/.commondata/weather.js
deleted file mode 100644
index 57c34fb1b..000000000
--- a/.config/ags/modules/.commondata/weather.js
+++ /dev/null
@@ -1,94 +0,0 @@
-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/.config/ags/modules/.commonwidgets/cairo_circularprogress.js b/.config/ags/modules/.commonwidgets/cairo_circularprogress.js
deleted file mode 100644
index 83c184227..000000000
--- a/.config/ags/modules/.commonwidgets/cairo_circularprogress.js
+++ /dev/null
@@ -1,106 +0,0 @@
-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/.config/ags/modules/.commonwidgets/cairo_navigationindicator.js b/.config/ags/modules/.commonwidgets/cairo_navigationindicator.js
deleted file mode 100644
index fd8d5990f..000000000
--- a/.config/ags/modules/.commonwidgets/cairo_navigationindicator.js
+++ /dev/null
@@ -1,71 +0,0 @@
-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/.config/ags/modules/.commonwidgets/cairo_roundedcorner.js b/.config/ags/modules/.commonwidgets/cairo_roundedcorner.js
deleted file mode 100644
index 90c1cf07b..000000000
--- a/.config/ags/modules/.commonwidgets/cairo_roundedcorner.js
+++ /dev/null
@@ -1,50 +0,0 @@
-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/.config/ags/modules/.commonwidgets/cairo_slider.js b/.config/ags/modules/.commonwidgets/cairo_slider.js
deleted file mode 100644
index eff9085c3..000000000
--- a/.config/ags/modules/.commonwidgets/cairo_slider.js
+++ /dev/null
@@ -1,49 +0,0 @@
-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/.config/ags/modules/.commonwidgets/clickcloseregion.js b/.config/ags/modules/.commonwidgets/clickcloseregion.js
deleted file mode 100644
index e0aae79b0..000000000
--- a/.config/ags/modules/.commonwidgets/clickcloseregion.js
+++ /dev/null
@@ -1,23 +0,0 @@
-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/.config/ags/modules/.commonwidgets/configwidgets.js b/.config/ags/modules/.commonwidgets/configwidgets.js
deleted file mode 100644
index 2b49a7d2f..000000000
--- a/.config/ags/modules/.commonwidgets/configwidgets.js
+++ /dev/null
@@ -1,298 +0,0 @@
-const { Gtk } = imports.gi;
-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, setupCursorHoverHResize } from '../.widgetutils/cursorhover.js';
-const { Box, Button, EventBox, Label, Revealer, SpinButton } = Widget;
-
-// Basically M3 Switch
-// https://m3.material.io/components/switch/overview
-// onReset must be async
-export const ConfigToggle = ({
- icon, name, desc = '', initValue,
- expandWidget = true, resetButton = false,
- onChange = () => { }, extraSetup = () => { },
- onReset = () => { }, fetchValue = () => { },
- ...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',
- vpack: 'center',
- 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',
- children: [
- ...(icon !== undefined ? [MaterialIcon(icon, 'norm', {vpack: 'center'})] : []),
- ...(name !== undefined ? [Label({
- vpack: 'center',
- 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),
- onHoverLost: () => { // mouse away
- toggleIcon.toggleClassName('switch-fg-toggling-false', false);
- if (enabled.value) toggleIcon.toggleClassName('txt-poof', false);
- },
- 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,
- });
- const wholeThing = Box({
- attribute: {
- 'enabled': enabled,
- },
- className: 'configtoggle-box spacing-h-5',
- children: [
- interactionWrapper,
- ...(resetButton ? [Button({
- className: 'configtoggle-reset',
- onClicked: (self) => {
- onReset(self).then(() => {
- enabled.value = fetchValue();
- }).catch(print);
- },
- child: MaterialIcon('settings_backup_restore', 'small'),
- setup: setupCursorHover,
- })] : []),
- ]
- });
- wholeThing.enabled = enabled;
- return wholeThing;
-}
-
-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,
-})
-
-// Gtk SpinButton with value scrubbing gesture
-// scrubRatio is the ratio of changed value to drag distance in pixels
-// onReset must be async
-export const ConfigSpinButton = ({
- icon, name, desc = '', initValue,
- minValue = 0, maxValue = 100, step = 1,
- expandWidget = true, resetButton = false,
- scrubRatio = 1 / 20, roundValue = true,
- onChange = () => { }, extraSetup = () => { },
- onReset = () => { }, fetchValue = () => { },
- ...rest
-}) => {
- let resetLock = false;
- const value = Variable(initValue);
- const spinButton = SpinButton({
- className: 'spinbutton',
- range: [minValue, maxValue],
- increments: [step, step],
- onValueChanged: ({ value: newValue }) => {
- if (resetLock) return;
- value.value = newValue;
- onChange(spinButton, newValue);
- },
- // This funny line means: set value of the spinbutton to the value of the
- // Variable object called value that tracks the value of the widget
- 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,
- ...(resetButton ? [Button({
- className: 'spinbutton-reset',
- onClicked: (self) => {
- onReset(self).then(() => {
- resetLock = true;
- const newValue = fetchValue();
- spinButton.value = newValue;
- value.value = newValue;
- resetLock = false;
- }).catch(print);
- },
- child: MaterialIcon('settings_backup_restore', 'small'),
- setup: setupCursorHover,
- })] : []),
- ],
- setup: (self) => {
- extraSetup(self);
- },
- ...rest,
- });
- const interactionWrapper = EventBox({
- child: widgetContent,
- setup: setupCursorHoverHResize,
- })
- const gesture = Gtk.GestureDrag.new(interactionWrapper);
- let gestureValueOnDragBegin;
- const wholeThing = Box({
- children: [interactionWrapper],
- setup: (self) => self
- .hook(gesture, (self) => {
- gestureValueOnDragBegin = value.value;
- }, 'drag-begin')
- .hook(gesture, (self) => {
- var offset_x = gesture.get_offset()[1];
- var offset_y = gesture.get_offset()[2];
- let newValue = gestureValueOnDragBegin + (offset_x * scrubRatio);
- if (roundValue) newValue = Math.round(newValue);
- if (newValue !== spinButton.value) {
- spinButton.value = newValue;
- }
- }, 'drag-update')
- .hook(gesture, (self) => {
-
- }, 'drag-end')
- });
- wholeThing.enabled = value;
- return wholeThing;
-}
\ No newline at end of file
diff --git a/.config/ags/modules/.commonwidgets/configwidgets_apps.js b/.config/ags/modules/.commonwidgets/configwidgets_apps.js
deleted file mode 100644
index 3bf7ce712..000000000
--- a/.config/ags/modules/.commonwidgets/configwidgets_apps.js
+++ /dev/null
@@ -1,154 +0,0 @@
-const { GLib } = imports.gi;
-import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
-const { execAsync, exec } = Utils;
-
-import { getNestedProperty, updateNestedProperty } from "../.miscutils/objects.js";
-import { ConfigSpinButton, ConfigToggle } from "./configwidgets.js";
-
-const AGS_CONFIG_FILE = `${App.configDir}/user_options.jsonc`;
-const HYPRLAND_CONFIG_FILE = `${GLib.get_user_config_dir()}/hypr/custom/general.conf`;
-
-export const AgsToggle = ({
- icon, name, desc = null,
- option, resetButton = true, save = true,
- extraOnChange = () => { }, extraOnReset = () => { },
- ...rest
-}) => ConfigToggle({
- icon: icon,
- name: name,
- desc: `${desc}\n\n${option}\nEdit in ${AGS_CONFIG_FILE}`,
- resetButton: resetButton,
- initValue: getNestedProperty(userOptions, option),
- fetchValue: () => getNestedProperty(userOptions, option),
- onChange: (self, newValue) => {
- updateNestedProperty(userOptions, option, newValue);
- if (save) execAsync(['bash', '-c', `${App.configDir}/scripts/ags/agsconfigurator.py \
- --key ${option} \
- --value ${newValue} \
- --file ${AGS_CONFIG_FILE}`
- ]).catch(print);
- extraOnChange(self, newValue);
- },
- onReset: async (self) => {
- updateNestedProperty(userOptions, option,
- getNestedProperty(userOptionsDefaults, option));
- if (save) exec(`bash -c '${App.configDir}/scripts/ags/agsconfigurator.py \
- --key ${option} \
- --reset \
- --file ${AGS_CONFIG_FILE}'`);
- extraOnReset(self);
- },
- ...rest
-});
-
-export const AgsSpinButton = ({
- icon, name, desc = null,
- option, resetButton = true,
- save = true, extraOnChange = () => { }, extraOnReset = () => { },
- ...rest
-}) => ConfigSpinButton({
- icon: icon,
- name: name,
- desc: `${desc}\n\n${option}\nEdit in ${AGS_CONFIG_FILE}`,
- resetButton: resetButton,
- initValue: getNestedProperty(userOptions, option),
- fetchValue: () => getNestedProperty(userOptions, option),
- step: 10, minValue: 0, maxValue: 1000,
- onChange: (self, newValue) => {
- updateNestedProperty(userOptions, option, newValue);
- if (save) execAsync(['bash', '-c', `${App.configDir}/scripts/ags/agsconfigurator.py \
- --key ${option} \
- --value ${newValue} \
- --file ${AGS_CONFIG_FILE}`
- ]).catch(print);
- extraOnChange(self, newValue);
- },
- onReset: async () => {
- updateNestedProperty(userOptions, option,
- getNestedProperty(userOptionsDefaults, option));
- if (save) exec(`bash -c '${App.configDir}/scripts/ags/agsconfigurator.py \
- --key ${option} \
- --reset \
- --file ${AGS_CONFIG_FILE}'`);
- extraOnReset(self);
- },
- ...rest,
-});
-
-export const HyprlandToggle = ({
- icon, name, desc = null,
- option, resetButton = true,
- enableValue = 1, disableValue = 0,
- extraOnChange = () => { }, extraOnReset = () => { }, save = true
-}) => ConfigToggle({
- icon: icon,
- name: name,
- desc: `${desc}\n\n${option}\nEdit in ${HYPRLAND_CONFIG_FILE}`,
- resetButton: resetButton,
- initValue: JSON.parse(exec(`hyprctl getoption -j ${option}`))["int"] != 0,
- fetchValue: () => JSON.parse(exec(`hyprctl getoption -j ${option}`))["int"] != 0,
- onChange: (self, newValue) => {
- if (save)
- execAsync(['bash', '-c', `${App.configDir}/scripts/hyprland/hyprconfigurator.py \
- --key ${option} \
- --value ${newValue ? enableValue : disableValue} \
- --file ${HYPRLAND_CONFIG_FILE}`
- ]).catch(print);
-
- else
- execAsync(['hyprctl', 'keyword', option, `${newValue ? enableValue : disableValue}`]).catch(print);
-
- extraOnChange(self, newValue);
- },
- onReset: async (self) => {
- if (save)
- exec(`bash -c '${App.configDir}/scripts/hyprland/hyprconfigurator.py \
- --key ${option} \
- --reset \
- --file "${HYPRLAND_CONFIG_FILE}"'`);
-
- else
- exec('hyprctl reload');
- extraOnReset(self);
- },
-});
-
-export const HyprlandSpinButton = ({
- icon, name, desc = null,
- option, resetButton = true, save = true,
- extraOnChange = () => { }, extraOnReset = () => { },
- ...rest
-}) => ConfigSpinButton({
- icon: icon,
- name: name,
- desc: `${desc}\n\n${option}\nEdit in ${HYPRLAND_CONFIG_FILE}`,
- resetButton: resetButton,
- initValue: Number(JSON.parse(exec(`hyprctl getoption -j ${option}`))["int"]),
- fetchValue: () => Number(JSON.parse(exec(`hyprctl getoption -j ${option}`))["int"]),
- onChange: (self, newValue) => {
- if (save)
- execAsync(['bash', '-c', `${App.configDir}/scripts/hyprland/hyprconfigurator.py \
- --key ${option} \
- --value ${newValue} \
- --file ${HYPRLAND_CONFIG_FILE}`
- ]).catch(print);
-
- else
- execAsync(['hyprctl', 'keyword', option, `${newValue}`]).catch(print);
-
- extraOnChange(self, newValue);
- },
- onReset: async (self) => {
- if (save)
- exec(`bash -c '${App.configDir}/scripts/hyprland/hyprconfigurator.py \
- --key ${option} \
- --reset \
- --file "${HYPRLAND_CONFIG_FILE}"'`);
-
- else
- exec('hyprctl reload');
- extraOnReset(self);
- },
- ...rest,
-});
-
diff --git a/.config/ags/modules/.commonwidgets/materialicon.js b/.config/ags/modules/.commonwidgets/materialicon.js
deleted file mode 100644
index 029f0ede6..000000000
--- a/.config/ags/modules/.commonwidgets/materialicon.js
+++ /dev/null
@@ -1,7 +0,0 @@
-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/.config/ags/modules/.commonwidgets/notification.js b/.config/ags/modules/.commonwidgets/notification.js
deleted file mode 100644
index b9486a2f0..000000000
--- a/.config/ags/modules/.commonwidgets/notification.js
+++ /dev/null
@@ -1,505 +0,0 @@
-// 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 keywordsToTypes = {
- 'reboot': 'restart_alt',
- 'recording': 'screen_record',
- 'battery': 'power',
- 'power': 'power',
- 'screenshot': 'screenshot_monitor',
- 'welcome': 'waving_hand',
- 'time': 'scheduleb',
- 'installed': 'download',
- 'update': 'update',
- 'ai response': 'neurology',
- 'startswith:file': 'folder_copy', // Declarative startsWith check
- };
-
- const lowerSummary = summary.toLowerCase();
-
- for (const [keyword, type] of Object.entries(keywordsToTypes)) {
- if (keyword.startsWith('startswith:')) {
- const startsWithKeyword = keyword.replace('startswith:', '');
- if (lowerSummary.startsWith(startsWithKeyword)) {
- return type;
- }
- } else if (lowerSummary.includes(keyword)) {
- return type;
- }
- }
-
- return 'chat';
-}
-
-function processNotificationBody(body, appEntry) {
- let processedBody = body;
- if (appEntry?.toLowerCase().includes('chrome')) {
- processedBody = body.split('\n\n').slice(1).join('\n\n');
- }
- processedBody = processedBody.replace(/<[^>]*>/g, '');
- return processedBody;
-}
-
-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 getString('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 getString('Yesterday');
- else
- return messageTime.format(userOptions.time.dateFormat);
-}
-
-const NotificationIcon = (notifObject) => {
-
- if (notifObject.hints?.image_path?.deepUnpack) {
- const imagePath = notifObject.hints.image_path.deepUnpack();
- return Box({
- valign: Gtk.Align.CENTER,
- hexpand: false,
- className: 'notif-icon',
- css: `
- background-image: url("${imagePath}");
- background-size: auto 100%;
- background-repeat: no-repeat;
- background-position: center;
- `,
- });
- }
-
- 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();
- },
- onSecondaryClick: (self) => {
- expanded = !expanded;
- notifTextPreview.revealChild = !expanded;
- notifTextExpanded.revealChild = expanded;
- notifExpandButton.child.label = `expand_${expanded ? 'less' : 'more'}`;
- },
- 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: processNotificationBody(notifObject.body, notifObject.appEntry).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: processNotificationBody(notifObject.body, notifObject.appEntry)
- }),
- 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: getString('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: ${userOptions.animations.durationSmall}ms cubic-bezier(0.05, 0.7, 0.1, 1), opacity ${userOptions.animations.durationSmall}ms 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.attribute.hovered) {
- wholeThing.revealChild = false;
- Utils.timeout(userOptions.animations.durationSmall, () => {
- if (wholeThing) {
- wholeThing.destroy();
- wholeThing = null;
- }
- command();
- }, wholeThing);
- }
- })
- return wholeThing;
-}
diff --git a/.config/ags/modules/.commonwidgets/statusicons.js b/.config/ags/modules/.commonwidgets/statusicons.js
deleted file mode 100644
index 11b4e1769..000000000
--- a/.config/ags/modules/.commonwidgets/statusicons.js
+++ /dev/null
@@ -1,316 +0,0 @@
-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: {
- 'disabled': Widget.Label({ className: 'txt-norm icon-material', label: 'bluetooth_disabled' }),
- 'enabled': Widget.Label({ className: 'txt-norm icon-material', label: 'bluetooth' }),
- 'connected': Widget.Label({ className: 'txt-norm icon-material', label: 'bluetooth_connected' }),
- },
- setup: (self) =>
- self.hook(Bluetooth, (stack) => {
- if (!Bluetooth.enabled) {
- stack.shown = 'disabled';
- } else if (Bluetooth.connected_devices.length === 0) {
- stack.shown = 'enabled';
- } else if (Bluetooth.connected_devices.length > 0) {
- stack.shown = 'connected';
- }
- }),
-});
-
-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: 'signal_wifi_off' }),
- 'disconnected': Widget.Label({
- className: 'txt-norm icon-material',
- label: 'signal_wifi_statusbar_not_connected',
- }),
- '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.enabled) {
- stack.shown = 'disabled';
- } else 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/.config/ags/modules/.commonwidgets/statusicons_languages.js b/.config/ags/modules/.commonwidgets/statusicons_languages.js
deleted file mode 100644
index 560d9424e..000000000
--- a/.config/ags/modules/.commonwidgets/statusicons_languages.js
+++ /dev/null
@@ -1,62 +0,0 @@
-// 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: '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: '🧐',
- },
-]
\ No newline at end of file
diff --git a/.config/ags/modules/.commonwidgets/tabcontainer.js b/.config/ags/modules/.commonwidgets/tabcontainer.js
deleted file mode 100644
index 1e252f57e..000000000
--- a/.config/ags/modules/.commonwidgets/tabcontainer.js
+++ /dev/null
@@ -1,299 +0,0 @@
-import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
-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, initIndex = 0,
- className = '', setup = () => { },
- onChange = () => { },
- extraTabStripWidgets = [],
- ...rest
-}) => {
- const shownIndex = Variable(initIndex);
- 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({
- className: 'spacing-h-5',
- children: [
- Box({
- vertical: true,
- hexpand: true,
- children: [
- tabs,
- tabIndicatorLine
- ]
- }),
- ...extraTabStripWidgets,
- ]
- })
- })]
- });
-
- shownIndex.setValue(initIndex)
- 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;
-
- return mainBox;
-}
-
-
-export const IconTabContainer = ({
- iconWidgets, names, children, className = '',
- initIndex = 0,
- setup = () => { }, onChange = () => { },
- tabsHpack = 'center', tabSwitcherClassName = '',
- ...rest
-}) => {
- const shownIndex = Variable(initIndex);
- 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/.config/ags/modules/.configuration/default_options.jsonc b/.config/ags/modules/.configuration/default_options.jsonc
deleted file mode 100644
index ef19f14e4..000000000
--- a/.config/ags/modules/.configuration/default_options.jsonc
+++ /dev/null
@@ -1,291 +0,0 @@
-// This file is parsed with a custom JSONC parser.
-// Don't expect every JSONC feature in... say, vscode, to work.
-{
- // General stuff
- "ai": {
- "defaultGPTProvider": "ollama_llama_3_2",
- "defaultTemperature": 0.5,
- "enhancements": true,
- "charsEachUpdate": 50, // Lower = smoother update rate, but more update lag
- "keep_alive": -1, // For ollama. -1 means forever
- "useHistory": false,
- "safety": 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.
- "extraGptModels": {
- // Below is an example. Copy to user_options.jsonc and edit it
- // The base url is conveniently ollama's btw
- // "model_id": {
- // "name": "User-added model",
- // "logo_name": "ollama-symbolic",
- // "description": "A model added by the user",
- // "base_url": "http://localhost:11434/v1/chat/completions",
- // "key_get_url": "",
- // "requires_key": false,
- // "key_file": "api_key_file.txt",
- // "model": "model-name"
- // },
- }
- },
- "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"
- },
- "borderless": false, // Uhm experimental...
- "keyboardUseFlag": false, // Use flag emoji instead of abbreviation letters
- "layerSmoke": false,
- "layerSmokeStrength": 0.2,
- "barRoundCorners": 1, // 0: No, 1: Yes
- "fakeScreenRounding": 2 // 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
- },
- "bar": {
- // Whether to show Swap and CPU usage when there's media. RAM is always shown.
- "alwaysShowFullResources": false,
- // 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"
- ]
- },
- "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": "auto"
- }
- },
- "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)"
- }
- },
- "i18n": {
- "langCode": "", //Customize the locale, such as zh_CN,Optional value references "~/.config/ags/i18n/locales/"
- "extraLogs": false
- },
- "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": {
- "image": {
- "columns": 2,
- "batchCount": 20,
- "allowNsfw": false
- },
- "pages": {
- "order": [
- "apis",
- "tools"
- ],
- "defaultPage": "apis",
- "apis": {
- "order": [
- "gemini",
- "gpt",
- "waifu",
- "booru"
- ],
- "defaultPage": "gemini"
- }
- },
- "quickToggles": {
- "order": [
- "wifi",
- "bluetooth",
- "nightlight",
- "gamemode",
- "idleinhibitor",
- "cloudflarewarp"
- ]
- },
- "calendar": {
- "expandByDefault": true
- }
- },
- "search": {
- "enableFeatures": {
- "actions": true,
- "commands": true,
- "mathResults": true,
- "directorySearch": true,
- "aiSearch": true,
- "webSearch": true
- },
- "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
- "calendarDateFormat": "%d %B %Y"
- },
- "weather": {
- "city": "",
- "preferredUnit": "C" // Either C or F
- },
- "workspaces": {
- "shown": 10
- },
- "dock": {
- "enabled": false,
- "hiddenThickness": 5,
- "pinnedApps": [
- "firefox",
- "org.gnome.Nautilus"
- ],
- "ignoredAppsRegex": [],
- "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",
- "footclient": "foot",
- "zen": "zen-browser",
- "": "image-missing"
- },
- "regexSubstitutions": [
- {
- "regex": "/^steam_app_(\\d+)$/",
- "replace": "steam_icon_$1"
- }
- ]
- },
- "keybinds": {
- // Format: "Modifier_1+...+Modifier_n+key". The key is CaSe SeNsItIvE!
- // Modifiers: Shift Ctrl Alt Hyper Meta
- // See https://docs.gtk.org/gdk3/index.html#constants for keys (listed as KEY_key)
- // You can assign multiple keybinds for the same action. Just split them with a comma
- // Example: "Ctrl+Page_Down, ctrl+Tab"
- "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"
- },
- "expand": "Ctrl+E",
- "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"
- }
- }
-}
\ No newline at end of file
diff --git a/.config/ags/modules/.configuration/user_options.js b/.config/ags/modules/.configuration/user_options.js
deleted file mode 100644
index 878070b66..000000000
--- a/.config/ags/modules/.configuration/user_options.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import App from 'resource:///com/github/Aylur/ags/app.js';
-import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
-import { parseJSONC } from '../.miscutils/jsonc.js';
-
-function overrideConfigRecursive(userOverrides, configOptions = {}) {
- for (const [key, value] of Object.entries(userOverrides)) {
- if (typeof value === 'object'
- && !(value instanceof Array)
- && configOptions[key]) {
- overrideConfigRecursive(value, configOptions[key]);
- }
- else {
- configOptions[key] = value;
- }
- }
-}
-
-// Load default options from ~/.config/ags/modules/.configuration/default_options.jsonc
-const defaultConfigFile = `${App.configDir}/modules/.configuration/default_options.jsonc`;
-const defaultConfigFileContents = Utils.readFile(defaultConfigFile);
-const defaultConfigOptions = parseJSONC(defaultConfigFileContents);
-
-// Clone the default config to avoid modifying the original
-let configOptions = JSON.parse(JSON.stringify(defaultConfigOptions));
-
-// Load user overrides
-const userOverrideFile = `${App.configDir}/user_options.jsonc`;
-const userOverrideContents = Utils.readFile(userOverrideFile);
-const userOverrides = parseJSONC(userOverrideContents);
-
-// Override defaults with user's options
-overrideConfigRecursive(userOverrides, configOptions);
-
-globalThis['userOptionsDefaults'] = defaultConfigOptions;
-globalThis['userOptions'] = configOptions;
-export default configOptions;
diff --git a/.config/ags/modules/.miscutils/files.js b/.config/ags/modules/.miscutils/files.js
deleted file mode 100644
index 00c17b916..000000000
--- a/.config/ags/modules/.miscutils/files.js
+++ /dev/null
@@ -1,14 +0,0 @@
-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/.config/ags/modules/.miscutils/icons.js b/.config/ags/modules/.miscutils/icons.js
deleted file mode 100644
index 66c09eda7..000000000
--- a/.config/ags/modules/.miscutils/icons.js
+++ /dev/null
@@ -1,28 +0,0 @@
-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/.config/ags/modules/.miscutils/jsonc.js b/.config/ags/modules/.miscutils/jsonc.js
deleted file mode 100644
index 689332880..000000000
--- a/.config/ags/modules/.miscutils/jsonc.js
+++ /dev/null
@@ -1,58 +0,0 @@
-export function parseJSONC(jsoncString) {
- let result = "";
- let inString = false;
- let inSingleQuote = false;
- let inMultiLineComment = false;
- let inSingleLineComment = false;
-
- for (let i = 0; i < jsoncString.length; i++) {
- let char = jsoncString[i];
- let nextChar = jsoncString[i + 1];
-
- // Handle string start/end
- if (!inSingleLineComment && !inMultiLineComment) {
- if (char === '"' && !inSingleQuote) {
- inString = !inString;
- } else if (char === "'" && !inString) {
- inSingleQuote = !inSingleQuote;
- }
- }
-
- // Handle single-line comments //
- if (!inString && !inSingleQuote && !inMultiLineComment && char === '/' && nextChar === '/') {
- inSingleLineComment = true;
- i++; // Skip next '/'
- continue;
- }
-
- // Handle multi-line comments /*
- if (!inString && !inSingleQuote && !inSingleLineComment && char === '/' && nextChar === '*') {
- inMultiLineComment = true;
- i++; // Skip next '*'
- continue;
- }
-
- // End single-line comment at newline
- if (inSingleLineComment && (char === '\n' || char === '\r')) {
- inSingleLineComment = false;
- }
-
- // End multi-line comment */
- if (inMultiLineComment && char === '*' && nextChar === '/') {
- inMultiLineComment = false;
- i++; // Skip next '/'
- continue;
- }
-
- // Only append characters if not inside a comment
- if (!inSingleLineComment && !inMultiLineComment) {
- result += char;
- }
- }
-
- // Remove trailing commas from objects and arrays
- result = result.replace(/,\s*([\]}])/g, '$1');
-
- // Parse as JSON
- return JSON.parse(result);
-}
diff --git a/.config/ags/modules/.miscutils/mathfuncs.js b/.config/ags/modules/.miscutils/mathfuncs.js
deleted file mode 100644
index 7a7c1da61..000000000
--- a/.config/ags/modules/.miscutils/mathfuncs.js
+++ /dev/null
@@ -1,16 +0,0 @@
-
-export function clamp(x, min, max) {
- return Math.min(Math.max(x, min), max);
-}
-
-export function truncateToPrecision(value, precision) {
- const factor = Math.pow(10, precision);
- const result = Math.round(value * factor) / factor;
- return result;
-}
-
-export function distance(x1, y1, x2, y2) {
- const distanceX = Math.abs(x1 - x2);
- const distanceY = Math.abs(y1 - y2);
- return Math.sqrt(distanceX * distanceX + distanceY * distanceY)
-}
\ No newline at end of file
diff --git a/.config/ags/modules/.miscutils/md2pango.js b/.config/ags/modules/.miscutils/md2pango.js
deleted file mode 100644
index e619cf972..000000000
--- a/.config/ags/modules/.miscutils/md2pango.js
+++ /dev/null
@@ -1,98 +0,0 @@
-// 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 codeBlockRegex = /^\s*```([a-zA-Z0-9]+)?\n?/;
-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 " },
- ],
- 'forceLatex': [
- { name: 'LATEX_INLINE_SQUARE', re: /\\\[(.*?)\\\]/g, sub: '\n```latex\n$1\n```' },
- { name: 'LATEX_INLINE_ROUND', re: /\\\((.*?)\\\)/g, sub: '\n```latex\n$1\n```' },
- { name: 'LATEX_INLINE_DOLLAR', re: /\$(.*?)\$/g, sub: '\n```latex\n$1\n```' }
- ]
-}
-
-const replaceCategory = (text, replaces) => {
- for (const type of replaces) {
- text = text.replace(type.re, type.sub);
- }
- return text;
-}
-
-// Main function
-
-export function replaceInlineLatexWithCodeBlocks(text) {
- return text.replace(/\\\[(.*?)\\\]|\\\((.*?)\\\)|\$\$(.*?)\$\$|(?:^|[^\w])\$(.*?[^\\])\$(?!\w)/gs, (match, square, round, double, single) => {
- const latex = square || round || double || single;
- return `\n\`\`\`latex\n${latex}\n\`\`\`\n`;
- });
-}
-
-export default (text) => {
- let lines = text.split('\n')
- let output = [];
- let inCode = false;
- // Replace
- for (const line of lines) {
- let result = line;
- if (codeBlockRegex.test(line)) inCode = !inCode;
- if (inCode) continue;
- 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(int argc, char* argv[]) {
- std::cout << GREETING;
-}
-\`\`\`
-## LaTeX
-- Inline LaTeX: \\[ \\frac{d}{dx} \\left( \\frac{x-438}{x^2+23x-7} \\right) = \\frac{-x^2 + 869}{(x^2+23x-7)^2} \\]
-- Block 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/.config/ags/modules/.miscutils/objects.js b/.config/ags/modules/.miscutils/objects.js
deleted file mode 100644
index 90ac62a22..000000000
--- a/.config/ags/modules/.miscutils/objects.js
+++ /dev/null
@@ -1,33 +0,0 @@
-export function getNestedProperty(obj, path) {
- return path.split('.').reduce((current, key) => {
- return (current && typeof current === 'object' && current.hasOwnProperty(key)) ? current[key] : undefined;
- }, obj);
-}
-
-export function updateNestedProperty(obj, path, newValue) {
- const pathArray = path.split('.');
- const lastKeyIndex = pathArray.length - 1;
-
- let current = obj;
-
- for (let i = 0; i < lastKeyIndex; i++) {
- const key = pathArray[i];
- if (!current || typeof current !== 'object') {
- return false; // Previous part of path is not an object
- }
-
- if (!current.hasOwnProperty(key)) {
- current[key] = {}; // Create the missing object
- }
- current = current[key];
- }
-
- const lastKey = pathArray[lastKeyIndex];
-
- if (!current || typeof current !== 'object') {
- return false; // Parent is not an object
- }
-
- current[lastKey] = newValue;
- return true;
-}
diff --git a/.config/ags/modules/.miscutils/system.js b/.config/ags/modules/.miscutils/system.js
deleted file mode 100644
index 377463b29..000000000
--- a/.config/ags/modules/.miscutils/system.js
+++ /dev/null
@@ -1,61 +0,0 @@
-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/.config/ags/modules/.widgethacks/advancedrevealers.js b/.config/ags/modules/.widgethacks/advancedrevealers.js
deleted file mode 100644
index 3f1279314..000000000
--- a/.config/ags/modules/.widgethacks/advancedrevealers.js
+++ /dev/null
@@ -1,86 +0,0 @@
-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/.config/ags/modules/.widgethacks/popupwindow.js b/.config/ags/modules/.widgethacks/popupwindow.js
deleted file mode 100644
index e0e101199..000000000
--- a/.config/ags/modules/.widgethacks/popupwindow.js
+++ /dev/null
@@ -1,36 +0,0 @@
-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: 'top',
- ...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/.config/ags/modules/.widgetutils/clickthrough.js b/.config/ags/modules/.widgetutils/clickthrough.js
deleted file mode 100644
index 505f1412d..000000000
--- a/.config/ags/modules/.widgetutils/clickthrough.js
+++ /dev/null
@@ -1,4 +0,0 @@
-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/.config/ags/modules/.widgetutils/cursorhover.js b/.config/ags/modules/.widgetutils/cursorhover.js
deleted file mode 100644
index 8633590ff..000000000
--- a/.config/ags/modules/.widgetutils/cursorhover.js
+++ /dev/null
@@ -1,36 +0,0 @@
-// Cursor names reference: https://docs.gtk.org/gdk4/ctor.Cursor.new_from_name.html
-const { Gdk } = imports.gi;
-
-export function setupCursorHover(button, cursorName = 'pointer') { // Hand pointing cursor on hover
- const display = Gdk.Display.get_default();
- button.connect('enter-notify-event', () => {
- const cursor = Gdk.Cursor.new_from_name(display, cursorName);
- 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
- setupCursorHover(button, 'crosshair');
-}
-
-export function setupCursorHoverGrab(button) { // Hand ready to grab on hover
- setupCursorHover(button, 'grab');
-}
-
-export function setupCursorHoverInfo(button) { // "?" mark cursor on hover
- setupCursorHover(button, 'help');
-}
-
-export function setupCursorHoverHResize(button) { // Resize left right
- setupCursorHover(button, 'ew-resize');
-}
-
-export function setupCursorHoverVResize(button) { // Resize up down
- setupCursorHover(button, 'ns-resize');
-}
diff --git a/.config/ags/modules/.widgetutils/keybind.js b/.config/ags/modules/.widgetutils/keybind.js
deleted file mode 100644
index 188ebc924..000000000
--- a/.config/ags/modules/.widgetutils/keybind.js
+++ /dev/null
@@ -1,34 +0,0 @@
-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
-}
-
-const checkSingleKeybind = (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].toLowerCase() in MODS) {
- if (!(pressedModMask & MODS[keys[i].toLowerCase()])) {
- return false;
- }
- } else if (pressedKey !== Gdk[`KEY_${keys[i]}`] && pressedKey !== Gdk[`KEY_${keys[i].toLowerCase()}`]) {
- return false;
- }
- }
- return true;
-}
-
-export const checkKeybind = (event, keybind) => {
- const keybinds = keybind.replace(' ', '').split(',');
- for (let i = 0; i < keybinds.length; i++) {
- if (checkSingleKeybind(event, keybinds[i])) {
- return true;
- }
- }
-}
diff --git a/.config/ags/modules/bar/focus/workspaces_hyprland.js b/.config/ags/modules/bar/focus/workspaces_hyprland.js
deleted file mode 100644
index b5a101aa6..000000000
--- a/.config/ags/modules/bar/focus/workspaces_hyprland.js
+++ /dev/null
@@ -1,213 +0,0 @@
-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 r-1`).catch(print),
- onScrollDown: () => Hyprland.messageAsync(`dispatch workspace r+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${userOptions.appearance.borderless ? '-borderless' : ''} 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/.config/ags/modules/bar/focus/workspaces_sway.js b/.config/ags/modules/bar/focus/workspaces_sway.js
deleted file mode 100644
index e883ece55..000000000
--- a/.config/ags/modules/bar/focus/workspaces_sway.js
+++ /dev/null
@@ -1,183 +0,0 @@
-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 Widget from "resource:///com/github/Aylur/ags/widget.js";
-import Sway from "../../../services/sway.js";
-import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
-const { execAsync, exec } = Utils;
-const { Box, DrawingArea, EventBox } = Widget;
-
-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 switchToWorkspace = (arg) => Utils.execAsync(`swaymsg workspace ${arg}`).catch(print);
-const switchToRelativeWorkspace = (self, num) =>
- execAsync([`${App.configDir}/scripts/sway/swayToRelativeWs.sh`, `${num}`]).catch(print);
-
-const WorkspaceContents = (count = 10) => {
- return DrawingArea({
- css: `transition: 90ms cubic-bezier(0.1, 1, 0, 1);`,
- attribute: {
- initialized: false,
- workspaceMask: 0,
- updateMask: (self) => {
- if (self.attribute.initialized) return; // We only need this to run once
- const workspaces = Sway.workspaces;
- let workspaceMask = 0;
- // console.log('----------------')
- for (let i = 0; i < workspaces.length; i++) {
- const ws = workspaces[i];
- // console.log(ws.name, ',', ws.num);
- if (!Number(ws.name)) return;
- const id = Number(ws.name);
- if (id <= 0) continue; // Ignore scratchpads
- if (id > count) return; // Not rendered
- if (workspaces[i].windows > 0) {
- workspaceMask |= (1 << id);
- }
- }
- self.attribute.workspaceMask = workspaceMask;
- self.attribute.initialized = true;
- },
- toggleMask: (self, occupied, name) => {
- if (occupied) self.attribute.workspaceMask |= (1 << parseInt(name));
- else self.attribute.workspaceMask &= ~(1 << parseInt(name));
- },
- },
- setup: (area) => area
- .hook(Sway.active.workspace, (area) => {
- area.setCss(`font-size: ${Sway.active.workspace.name}px;`)
- })
- .hook(Sway, (self) => self.attribute.updateMask(self), 'notify::workspaces')
- // .hook(Hyprland, (self, name) => self.attribute.toggleMask(self, true, name), 'workspace-added')
- // .hook(Hyprland, (self, name) => self.attribute.toggleMask(self, false, name), 'workspace-removed')
- .on('draw', Lang.bind(area, (area, cr) => {
- 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 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]} ${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.2; // a bit smaller than sqrt(2)*radius
- const indicatorGap = workspaceRadius - indicatorRadius;
-
- // Draw workspace numbers
- 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();
- }
-
- // Set color for text
- cr.setSourceRGBA(occupiedfg.red, occupiedfg.green, occupiedfg.blue, occupiedfg.alpha);
- }
- else
- cr.setSourceRGBA(wsfg.red, wsfg.green, wsfg.blue, wsfg.alpha);
- layout.set_text(`${i}`, -1);
- const [layoutWidth, layoutHeight] = layout.get_pixel_size();
- const x = -workspaceRadius + (workspaceDiameter * i) - (layoutWidth / 2);
- const y = (height - layoutHeight) / 2;
- cr.moveTo(x, y);
- // cr.showText(text);
- PangoCairo.show_layout(cr, layout);
- cr.stroke();
- }
-
- // Draw active ws
- // base
- cr.setSourceRGBA(activebg.red, activebg.green, activebg.blue, activebg.alpha);
- cr.arc(activeWsCenterX, activeWsCenterY, indicatorRadius, 0, 2 * Math.PI);
- cr.fill();
- // inner decor
- cr.setSourceRGBA(activefg.red, activefg.green, activefg.blue, activefg.alpha);
- cr.arc(activeWsCenterX, activeWsCenterY, indicatorRadius * 0.2, 0, 2 * Math.PI);
- cr.fill();
- }))
- ,
- })
-}
-
-export default () => EventBox({
- onScrollUp: (self) => switchToRelativeWorkspace(self, -1),
- onScrollDown: (self) => switchToRelativeWorkspace(self, +1),
- onMiddleClick: () => toggleWindowOnAllMonitors('osk'),
- onSecondaryClick: () => App.toggleWindow('overview'),
- attribute: { clicked: false },
- child: Box({
- homogeneous: true,
- className: 'bar-group-margin',
- children: [Box({
- className: `bar-group${userOptions.appearance.borderless ? '-borderless' : ''} bar-group-standalone bar-group-pad`,
- css: 'min-width: 2px;',
- children: [
- WorkspaceContents(10),
- ]
- })]
- }),
- 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);
- switchToWorkspace(wsId);
- })
- 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 * userOptions.workspaces.shown / widgetWidth);
- switchToWorkspace(wsId);
- })
- self.on('button-release-event', (self) => self.attribute.clicked = false);
- }
-});
diff --git a/.config/ags/modules/bar/main.js b/.config/ags/modules/bar/main.js
deleted file mode 100644
index 771306e86..000000000
--- a/.config/ags/modules/bar/main.js
+++ /dev/null
@@ -1,129 +0,0 @@
-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 {
- try {
- return (await import('./normal/workspaces_sway.js')).default();
- } catch {
- return null;
- }
- }
-};
-
-const FocusOptionalWorkspaces = async () => {
- try {
- return (await import('./focus/workspaces_hyprland.js')).default();
- } catch {
- try {
- return (await import('./focus/workspaces_sway.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/.config/ags/modules/bar/normal/music.js b/.config/ags/modules/bar/normal/music.js
deleted file mode 100644
index 88c9bf816..000000000
--- a/.config/ags/modules/bar/normal/music.js
+++ /dev/null
@@ -1,241 +0,0 @@
-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 } = 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;
-}
-
-function adjustVolume(direction) {
- const step = 0.1; // We use a larger step because this is player instance volume, not global
- const mpris = Mpris.getPlayer('');
- mpris.volume += (direction === 'up') ? step : -step
-}
-
-
-const BarGroup = ({ child }) => Box({
- className: 'bar-group-margin bar-sides',
- children: [
- Box({
- className: `bar-group${userOptions.appearance.borderless ? '-borderless' : ''} bar-group-standalone bar-group-pad-system`,
- children: [child],
- }),
- ]
-});
-
-const BarResource = (name, icon, command, circprogClassName = `bar-batt-circprog ${userOptions.appearance.borderless ? 'bar-batt-circprog-borderless' : ''}`, 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)
- circprog.css = `font-size: ${userOptions.appearance.borderless ? 100 : 0}px;`
- else // Set circular progress value
- circprog.css = `font-size: ${Math.max(mpris.position / mpris.length * 100, 0)}px;`
- }
- return AnimatedCircProg({
- className: `bar-music-circprog ${userOptions.appearance.borderless ? 'bar-music-circprog-borderless' : ''}`,
- 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 {
- execAsync([`${App.configDir}/scripts/sway/swayToRelativeWs.sh`, `${num}`]).catch(print);
- }
-}
-
-
-
-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.playBackStatus == 'Playing');
- label.toggleClassName('bar-music-playstate', 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 = getString('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(getString('RAM Usage'), 'memory', `LANG=C free | awk '/^Mem/ {printf("%.2f\\n", ($3/$2) * 100)}'`,
- `bar-ram-circprog ${userOptions.appearance.borderless ? 'bar-ram-circprog-borderless' : ''}`, '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(getString('Swap Usage'), 'swap_horiz', `LANG=C free | awk '/^Swap/ {if ($2 > 0) printf("%.2f\\n", ($3/$2) * 100); else print "0";}'`,
- `bar-swap-circprog ${userOptions.appearance.borderless ? 'bar-swap-circprog-borderless' : ''}`, 'bar-swap-txt', 'bar-swap-icon'),
- BarResource(getString('CPU Usage'), 'settings_motion_mode', `LANG=C top -bn1 | grep Cpu | sed 's/\\,/\\./g' | awk '{print $2}'`,
- `bar-cpu-circprog ${userOptions.appearance.borderless ? 'bar-cpu-circprog-borderless' : ''}`, 'bar-cpu-txt', 'bar-cpu-icon'),
- ]
- }),
- setup: (self) => self.hook(Mpris, label => {
- const mpris = Mpris.getPlayer('');
- self.revealChild = (!mpris || mpris.playBackStatus !== 'Playing' || userOptions.bar.alwaysShowFullResources);
- }),
- })
- ],
- })
- });
- }
- return EventBox({
- onScrollUp: () => adjustVolume('up'),
- onScrollDown: () => adjustVolume('down'),
- 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/.config/ags/modules/bar/normal/spaceleft.js b/.config/ags/modules/bar/normal/spaceleft.js
deleted file mode 100644
index d7f861271..000000000
--- a/.config/ags/modules/bar/normal/spaceleft.js
+++ /dev/null
@@ -1,96 +0,0 @@
-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';
-import { distance } from '../../.miscutils/mathfuncs.js';
-
-const OSD_DISMISS_DISTANCE = 10;
-
-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;
- });
- self.hook(Hyprland.active.workspace, 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();
- let scrollCursorX, scrollCursorY;
- return Widget.EventBox({
- onScrollUp: (self, event) => {
- let _;
- [_, scrollCursorX, scrollCursorY] = event.get_coords();
- Indicator.popup(1); // Since the brightness and speaker are both on the same window
- Brightness[monitor].screen_value += 0.05;
- },
- onScrollDown: (self, event) => {
- let _;
- [_, scrollCursorX, scrollCursorY] = event.get_coords();
- Indicator.popup(1); // Since the brightness and speaker are both on the same window
- Brightness[monitor].screen_value -= 0.05;
- },
- onPrimaryClick: () => {
- App.toggleWindow('sideleft');
- },
- setup: (self) => self.on('motion-notify-event', (self, event) => {
- const [_, cursorX, cursorY] = event.get_coords();
- if (distance(cursorX, cursorY, scrollCursorX, scrollCursorY) >= OSD_DISMISS_DISTANCE)
- Indicator.popup(-1);
- }),
- 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/.config/ags/modules/bar/normal/spaceright.js b/.config/ags/modules/bar/normal/spaceright.js
deleted file mode 100644
index dd23de33c..000000000
--- a/.config/ags/modules/bar/normal/spaceright.js
+++ /dev/null
@@ -1,106 +0,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';
-
-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";
-import { distance } from '../../.miscutils/mathfuncs.js';
-
-const OSD_DISMISS_DISTANCE = 10;
-
-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 SpaceRightInteractions = (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)
- }).on('motion-notify-event', (self, event) => {
- Indicator.popup(-1);
- }),
- child: child,
- });
- const emptyArea = SpaceRightInteractions(Widget.Box({ hexpand: true, }));
- const indicatorArea = SpaceRightInteractions(Widget.Box({
- children: [
- SeparatorDot(),
- barStatusIcons
- ],
- }));
- const actualContent = Widget.Box({
- hexpand: true,
- className: 'spacing-h-5 bar-spaceright',
- children: [
- emptyArea,
- barTray,
- indicatorArea
- ],
- });
-
- let scrollCursorX, scrollCursorY;
- return Widget.EventBox({
- onScrollUp: (self, event) => {
- if (!Audio.speaker) return;
- let _;
- [_, scrollCursorX, scrollCursorY] = event.get_coords();
- if (Audio.speaker.volume <= 0.09) Audio.speaker.volume += 0.01;
- else Audio.speaker.volume += 0.03;
- Indicator.popup(1);
- },
- onScrollDown: (self, event) => {
- if (!Audio.speaker) return;
- let _;
- [_, scrollCursorX, scrollCursorY] = event.get_coords();
- if (Audio.speaker.volume <= 0.09) Audio.speaker.volume -= 0.01;
- else Audio.speaker.volume -= 0.03;
- Indicator.popup(1);
- },
- setup: (self) => self.on('motion-notify-event', (self, event) => {
- const [_, cursorX, cursorY] = event.get_coords();
- if (distance(cursorX, cursorY, scrollCursorX, scrollCursorY) >= OSD_DISMISS_DISTANCE)
- Indicator.popup(-1);
- }),
- child: Widget.Box({
- children: [
- actualContent,
- SpaceRightInteractions(Widget.Box({ className: 'bar-corner-spacing' })),
- ]
- })
- });
-}
\ No newline at end of file
diff --git a/.config/ags/modules/bar/normal/system.js b/.config/ags/modules/bar/normal/system.js
deleted file mode 100644
index c69abe685..000000000
--- a/.config/ags/modules/bar/normal/system.js
+++ /dev/null
@@ -1,238 +0,0 @@
-// 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';
-import { setupCursorHover } from '../../.widgetutils/cursorhover.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 ${userOptions.appearance.borderless ? 'bar-batt-circprog-borderless' : ''}`,
- 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 ${userOptions.appearance.borderless ? 'bar-util-btn-borderless' : ''} icon-material txt-norm`,
- label: `${icon}`,
- setup: setupCursorHover
-})
-
-const Utilities = () => Box({
- hpack: 'center',
- className: 'spacing-h-4',
- children: [
- UtilButton({
- name: getString('Screen snip'), icon: 'screenshot_region', onClicked: () => {
- Utils.execAsync(`${App.configDir}/scripts/grimblast.sh copy area`)
- .catch(print)
- }
- }),
- UtilButton({
- name: getString('Color picker'), icon: 'colorize', onClicked: () => {
- Utils.execAsync(['hyprpicker', '-a']).catch(print)
- }
- }),
- UtilButton({
- name: getString('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${userOptions.appearance.borderless ? '-borderless' : ''} 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} • ${getString('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} • ${getString('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 r${num > 0 ? '+' : ''}${num}`).catch(print);
- } catch {
- execAsync([`${App.configDir}/scripts/sway/swayToRelativeWs.sh`, `${num}`]).catch(print);
- }
-}
-
-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/.config/ags/modules/bar/normal/tray.js b/.config/ags/modules/bar/normal/tray.js
deleted file mode 100644
index d568bc769..000000000
--- a/.config/ags/modules/bar/normal/tray.js
+++ /dev/null
@@ -1,36 +0,0 @@
-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/.config/ags/modules/bar/normal/workspaces_hyprland.js b/.config/ags/modules/bar/normal/workspaces_hyprland.js
deleted file mode 100644
index 5bc24180e..000000000
--- a/.config/ags/modules/bar/normal/workspaces_hyprland.js
+++ /dev/null
@@ -1,224 +0,0 @@
-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 r-1`).catch(print),
- onScrollDown: () => Hyprland.messageAsync(`dispatch workspace r+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${userOptions.appearance.borderless ? '-borderless' : ''} 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/.config/ags/modules/bar/normal/workspaces_sway.js b/.config/ags/modules/bar/normal/workspaces_sway.js
deleted file mode 100644
index e883ece55..000000000
--- a/.config/ags/modules/bar/normal/workspaces_sway.js
+++ /dev/null
@@ -1,183 +0,0 @@
-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 Widget from "resource:///com/github/Aylur/ags/widget.js";
-import Sway from "../../../services/sway.js";
-import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
-const { execAsync, exec } = Utils;
-const { Box, DrawingArea, EventBox } = Widget;
-
-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 switchToWorkspace = (arg) => Utils.execAsync(`swaymsg workspace ${arg}`).catch(print);
-const switchToRelativeWorkspace = (self, num) =>
- execAsync([`${App.configDir}/scripts/sway/swayToRelativeWs.sh`, `${num}`]).catch(print);
-
-const WorkspaceContents = (count = 10) => {
- return DrawingArea({
- css: `transition: 90ms cubic-bezier(0.1, 1, 0, 1);`,
- attribute: {
- initialized: false,
- workspaceMask: 0,
- updateMask: (self) => {
- if (self.attribute.initialized) return; // We only need this to run once
- const workspaces = Sway.workspaces;
- let workspaceMask = 0;
- // console.log('----------------')
- for (let i = 0; i < workspaces.length; i++) {
- const ws = workspaces[i];
- // console.log(ws.name, ',', ws.num);
- if (!Number(ws.name)) return;
- const id = Number(ws.name);
- if (id <= 0) continue; // Ignore scratchpads
- if (id > count) return; // Not rendered
- if (workspaces[i].windows > 0) {
- workspaceMask |= (1 << id);
- }
- }
- self.attribute.workspaceMask = workspaceMask;
- self.attribute.initialized = true;
- },
- toggleMask: (self, occupied, name) => {
- if (occupied) self.attribute.workspaceMask |= (1 << parseInt(name));
- else self.attribute.workspaceMask &= ~(1 << parseInt(name));
- },
- },
- setup: (area) => area
- .hook(Sway.active.workspace, (area) => {
- area.setCss(`font-size: ${Sway.active.workspace.name}px;`)
- })
- .hook(Sway, (self) => self.attribute.updateMask(self), 'notify::workspaces')
- // .hook(Hyprland, (self, name) => self.attribute.toggleMask(self, true, name), 'workspace-added')
- // .hook(Hyprland, (self, name) => self.attribute.toggleMask(self, false, name), 'workspace-removed')
- .on('draw', Lang.bind(area, (area, cr) => {
- 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 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]} ${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.2; // a bit smaller than sqrt(2)*radius
- const indicatorGap = workspaceRadius - indicatorRadius;
-
- // Draw workspace numbers
- 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();
- }
-
- // Set color for text
- cr.setSourceRGBA(occupiedfg.red, occupiedfg.green, occupiedfg.blue, occupiedfg.alpha);
- }
- else
- cr.setSourceRGBA(wsfg.red, wsfg.green, wsfg.blue, wsfg.alpha);
- layout.set_text(`${i}`, -1);
- const [layoutWidth, layoutHeight] = layout.get_pixel_size();
- const x = -workspaceRadius + (workspaceDiameter * i) - (layoutWidth / 2);
- const y = (height - layoutHeight) / 2;
- cr.moveTo(x, y);
- // cr.showText(text);
- PangoCairo.show_layout(cr, layout);
- cr.stroke();
- }
-
- // Draw active ws
- // base
- cr.setSourceRGBA(activebg.red, activebg.green, activebg.blue, activebg.alpha);
- cr.arc(activeWsCenterX, activeWsCenterY, indicatorRadius, 0, 2 * Math.PI);
- cr.fill();
- // inner decor
- cr.setSourceRGBA(activefg.red, activefg.green, activefg.blue, activefg.alpha);
- cr.arc(activeWsCenterX, activeWsCenterY, indicatorRadius * 0.2, 0, 2 * Math.PI);
- cr.fill();
- }))
- ,
- })
-}
-
-export default () => EventBox({
- onScrollUp: (self) => switchToRelativeWorkspace(self, -1),
- onScrollDown: (self) => switchToRelativeWorkspace(self, +1),
- onMiddleClick: () => toggleWindowOnAllMonitors('osk'),
- onSecondaryClick: () => App.toggleWindow('overview'),
- attribute: { clicked: false },
- child: Box({
- homogeneous: true,
- className: 'bar-group-margin',
- children: [Box({
- className: `bar-group${userOptions.appearance.borderless ? '-borderless' : ''} bar-group-standalone bar-group-pad`,
- css: 'min-width: 2px;',
- children: [
- WorkspaceContents(10),
- ]
- })]
- }),
- 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);
- switchToWorkspace(wsId);
- })
- 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 * userOptions.workspaces.shown / widgetWidth);
- switchToWorkspace(wsId);
- })
- self.on('button-release-event', (self) => self.attribute.clicked = false);
- }
-});
diff --git a/.config/ags/modules/cheatsheet/data_keybinds.js b/.config/ags/modules/cheatsheet/data_keybinds.js
deleted file mode 100644
index 5631c4983..000000000
--- a/.config/ags/modules/cheatsheet/data_keybinds.js
+++ /dev/null
@@ -1,122 +0,0 @@
-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/.config/ags/modules/cheatsheet/data_periodictable.js b/.config/ags/modules/cheatsheet/data_periodictable.js
deleted file mode 100644
index f7f47a0e0..000000000
--- a/.config/ags/modules/cheatsheet/data_periodictable.js
+++ /dev/null
@@ -1,195 +0,0 @@
-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: 'Potassium', 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/.config/ags/modules/cheatsheet/keybinds.js b/.config/ags/modules/cheatsheet/keybinds.js
deleted file mode 100644
index 2464f0a6c..000000000
--- a/.config/ags/modules/cheatsheet/keybinds.js
+++ /dev/null
@@ -1,126 +0,0 @@
-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: getString(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: getString(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/.config/ags/modules/cheatsheet/main.js b/.config/ags/modules/cheatsheet/main.js
deleted file mode 100644
index 0d591ad46..000000000
--- a/.config/ags/modules/cheatsheet/main.js
+++ /dev/null
@@ -1,146 +0,0 @@
-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: getString('Keybinds'),
- materialIcon: 'keyboard',
- contentWidget: Keybinds,
- },
- {
- name: getString('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: getString('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: 'top',
- 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/.config/ags/modules/cheatsheet/periodictable.js b/.config/ags/modules/cheatsheet/periodictable.js
deleted file mode 100644
index 92a85b9d5..000000000
--- a/.config/ags/modules/cheatsheet/periodictable.js
+++ /dev/null
@@ -1,94 +0,0 @@
-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/.config/ags/modules/crosshair/main.js b/.config/ags/modules/crosshair/main.js
deleted file mode 100644
index 9d1538aa0..000000000
--- a/.config/ags/modules/crosshair/main.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import Widget from 'resource:///com/github/Aylur/ags/widget.js';
-import { enableClickthrough } from "../.widgetutils/clickthrough.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/.config/ags/modules/desktopbackground/data_quicklaunches.js b/.config/ags/modules/desktopbackground/data_quicklaunches.js
deleted file mode 100644
index a5a9562f1..000000000
--- a/.config/ags/modules/desktopbackground/data_quicklaunches.js
+++ /dev/null
@@ -1,14 +0,0 @@
-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/.config/ags/modules/desktopbackground/main.js b/.config/ags/modules/desktopbackground/main.js
deleted file mode 100644
index 9600e151e..000000000
--- a/.config/ags/modules/desktopbackground/main.js
+++ /dev/null
@@ -1,24 +0,0 @@
-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/.config/ags/modules/desktopbackground/system.js b/.config/ags/modules/desktopbackground/system.js
deleted file mode 100644
index a65402b7c..000000000
--- a/.config/ags/modules/desktopbackground/system.js
+++ /dev/null
@@ -1,161 +0,0 @@
-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/.config/ags/modules/desktopbackground/timeandlaunches.js b/.config/ags/modules/desktopbackground/timeandlaunches.js
deleted file mode 100644
index f0e0214b4..000000000
--- a/.config/ags/modules/desktopbackground/timeandlaunches.js
+++ /dev/null
@@ -1,74 +0,0 @@
-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/.config/ags/modules/desktopbackground/wallpaper.js b/.config/ags/modules/desktopbackground/wallpaper.js
deleted file mode 100644
index 779bb2304..000000000
--- a/.config/ags/modules/desktopbackground/wallpaper.js
+++ /dev/null
@@ -1,119 +0,0 @@
-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/.config/ags/modules/dock/dock.js b/.config/ags/modules/dock/dock.js
deleted file mode 100755
index bd8297f93..000000000
--- a/.config/ags/modules/dock/dock.js
+++ /dev/null
@@ -1,315 +0,0 @@
-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);
- const ignoredAppsRegex = userOptions.dock.ignoredAppsRegex || [];
- let isIgnored = false;
-
- for (const regex of ignoredAppsRegex) {
- try {
- const pattern = new RegExp(regex);
- if (pattern.test(appClass)) {
- isIgnored = true;
- break;
- }
- } catch (e) {}
- }
-
- if (isIgnored) continue;
-
- // 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/.config/ags/modules/dock/icons.js b/.config/ags/modules/dock/icons.js
deleted file mode 100644
index 60f01a3a1..000000000
--- a/.config/ags/modules/dock/icons.js
+++ /dev/null
@@ -1,63 +0,0 @@
-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/.config/ags/modules/dock/main.js b/.config/ags/modules/dock/main.js
deleted file mode 100644
index 77d394d9f..000000000
--- a/.config/ags/modules/dock/main.js
+++ /dev/null
@@ -1,12 +0,0 @@
-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/.config/ags/modules/indicators/colorscheme.js b/.config/ags/modules/indicators/colorscheme.js
deleted file mode 100644
index f43a41fa4..000000000
--- a/.config/ags/modules/indicators/colorscheme.js
+++ /dev/null
@@ -1,236 +0,0 @@
-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: getString('Tonal Spot'), value: 'tonalspot' },
- { name: getString('Fruit Salad'), value: 'fruitsalad' },
- { name: getString('Fidelity'), value: 'fidelity' },
- { name: getString('Rainbow'), value: 'rainbow' },
- ],
- [
- { name: getString('Neutral'), value: 'neutral' },
- { name: getString('Monochrome'), value: 'monochrome' },
- { name: getString('Expressive'), value: 'expressive' },
- { name: getString('Vibrant'), value: 'vibrant' },
- ],
- [
- { name: getString('Vibrant+'), value: 'morevibrant' },
- ],
- //[
- // { name: getString('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: getString('Options'),
- hpack: 'center',
- }),
- //////////////////
- ConfigToggle({
- icon: 'dark_mode',
- name: getString('Dark Mode'),
- desc: getString('Ya should go to sleep!'),
- initValue: darkMode.value,
- onChange: (_, newValue) => {
- darkMode.value = !!newValue;
- },
- extraSetup: (self) => self.hook(darkMode, (self) => {
- self.attribute.enabled.value = darkMode.value;
- }),
- }),
- ConfigToggle({
- icon: 'border_clear',
- name: getString('Transparency'),
- desc: getString('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({
- vertical: true,
- className: 'spacing-v-5',
- children: [
- Widget.Label({
- xalign: 0,
- className: 'txt-norm titlefont txt margin-top-5',
- label: getString('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: getString('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/.config/ags/modules/indicators/indicatorvalues.js b/.config/ags/modules/indicators/indicatorvalues.js
deleted file mode 100644
index a4aef288b..000000000
--- a/.config/ags/modules/indicators/indicatorvalues.js
+++ /dev/null
@@ -1,142 +0,0 @@
-// 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';
-import { MaterialIcon } from '../.commonwidgets/materialicon.js';
-
-const OsdValue = ({
- name, icon, nameSetup = undefined, labelSetup, progressSetup, iconSetup,
- 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
- hexpand: true,
- className: `osd-bg osd-value ${extraClassName} spacing-h-5`,
- attribute: {
- 'disable': () => {
- valueNumber.label = '';
- }
- },
- children: [
- MaterialIcon(icon, 'hugeass', {vpack: 'center', setup: iconSetup}),
- Box({
- vertical: true,
- className: 'spacing-v-5',
- vpack: 'center',
- children: [
- Box({
- children: [
- valueName,
- valueNumber,
- ]
- }),
- ProgressBar({
- className: `osd-progress ${extraProgressClassName}`,
- hexpand: true,
- vertical: false,
- setup: progressSetup,
- })
- ]
- })
- ],
- ...rest,
- });
-}
-
-export default (monitor = 0) => {
- const brightnessIndicator = OsdValue({
- name: 'Brightness',
- icon: 'light_mode',
- 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 = Audio.speaker?.stream?.isMuted
- ? 0
- : 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?.stream?.isMuted
- ? 0
- : Audio.speaker?.volume;
- if (!isNaN(updateValue)) {
- if (updateValue > 1) progress.value = 1;
- else progress.value = updateValue;
- }
- }),
- iconSetup: (self) => self.hook(Audio, (progress) => {
- self.label =
- Audio.speaker?.stream?.isMuted || !Audio.speaker.volume
- ? 'volume_off'
- : 'volume_up';
- }),
- });
- 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/.config/ags/modules/indicators/main.js b/.config/ags/modules/indicators/main.js
deleted file mode 100644
index 16741937f..000000000
--- a/.config/ags/modules/indicators/main.js
+++ /dev/null
@@ -1,32 +0,0 @@
-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/.config/ags/modules/indicators/musiccontrols.js b/.config/ags/modules/indicators/musiccontrols.js
deleted file mode 100644
index 5f5183462..000000000
--- a/.config/ags/modules/indicators/musiccontrols.js
+++ /dev/null
@@ -1,418 +0,0 @@
-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';
-import { setupCursorHover } from '../.widgetutils/cursorhover.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") ? 'light' : '';
-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(() => {
- const dominantColor = `#${Utils.exec(`sh -c "magick '${coverPath}' -scale 1x1\\! -format '%[fx:int(255*r+.5)],%[fx:int(255*g+.5)],%[fx:int(255*b+.5)]' info: | sed 's/,/\\n/g' | xargs -L 1 printf '%02x' ; echo"`)}`
- // exec(`${App.configDir}/scripts/color_generation/pywal.sh -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(`cp '${App.configDir}/scripts/templates/wal/_musicwal.scss' '${GLib.get_user_state_dir()}/ags/scss/_musicwal.scss'`);
- exec(`sed -i 's/{{dominantColor}}/${dominantColor}/g' '${GLib.get_user_state_dir()}/ags/scss/_musicwal.scss'`)
- exec(`sed -i 's/{{backgroundColor}}/${darkMode.value ? "#0E1415" : "#EEF4F4"}/g' '${GLib.get_user_state_dir()}/ags/scss/_musicwal.scss'`)
- exec(`sed -i 's/{{foregroundColor}}/${darkMode.value ? "#EEF4F4" : "#0E1415"}/g' '${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',
- }),
- setup: setupCursorHover
- }),
- Button({
- className: 'osd-music-controlbtn',
- onClicked: () => player.next(),
- child: Label({
- className: 'icon-material osd-music-controlbtn-txt',
- label: 'skip_next',
- }),
- setup: setupCursorHover
- }),
- ],
- }),
- 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'),
- }),
- setup: setupCursorHover
- }),
- ],
- 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/.config/ags/modules/indicators/notificationpopups.js b/.config/ags/modules/indicators/notificationpopups.js
deleted file mode 100644
index 0c38a670e..000000000
--- a/.config/ags/modules/indicators/notificationpopups.js
+++ /dev/null
@@ -1,45 +0,0 @@
-// 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/.config/ags/modules/onscreenkeyboard/main.js b/.config/ags/modules/onscreenkeyboard/main.js
deleted file mode 100644
index 2204ebf35..000000000
--- a/.config/ags/modules/onscreenkeyboard/main.js
+++ /dev/null
@@ -1,11 +0,0 @@
-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/.config/ags/modules/onscreenkeyboard/onscreenkeyboard.js b/.config/ags/modules/onscreenkeyboard/onscreenkeyboard.js
deleted file mode 100644
index 79da3ee5d..000000000
--- a/.config/ags/modules/onscreenkeyboard/onscreenkeyboard.js
+++ /dev/null
@@ -1,267 +0,0 @@
-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 --match-mode fzf --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/.config/ags/modules/overview/actions.js b/.config/ags/modules/overview/actions.js
deleted file mode 100644
index 766cf4542..000000000
--- a/.config/ags/modules/overview/actions.js
+++ /dev/null
@@ -1,28 +0,0 @@
-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/.config/ags/modules/overview/main.js b/.config/ags/modules/overview/main.js
deleted file mode 100644
index 19118d922..000000000
--- a/.config/ags/modules/overview/main.js
+++ /dev/null
@@ -1,28 +0,0 @@
-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: 'top',
- 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/.config/ags/modules/overview/miscfunctions.js b/.config/ags/modules/overview/miscfunctions.js
deleted file mode 100644
index 196f5a7f0..000000000
--- a/.config/ags/modules/overview/miscfunctions.js
+++ /dev/null
@@ -1,146 +0,0 @@
-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";
-
-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.setValue(false).catch(print);
- }
- else if (args[0] == '>dark') { // Dark mode
- darkMode.setValue(true).catch(print);
- }
- 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] == '>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] == '>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 || pkill sway`]).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/.config/ags/modules/overview/overview_hyprland.js b/.config/ags/modules/overview/overview_hyprland.js
deleted file mode 100644
index dae007aca..000000000
--- a/.config/ags/modules/overview/overview_hyprland.js
+++ /dev/null
@@ -1,446 +0,0 @@
-// 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);
-const overviewMonitor = Variable(0);
-
-export default () => {
- 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; }
- // Prevents throwing an error when multiple monitors are plugged in but only one is enabled (#1047)
- if (monitors.length - 1 < monitor) {
- monitor = monitors.length - 1;
- }
- // Properly scale for multi monitors
- w *= monitors[overviewMonitor.value].width / monitors[monitor].width;
- h *= monitors[overviewMonitor.value].height / monitors[monitor].height;
- // Truncate if offscreen
- if (x + w > monitors[overviewMonitor.value].width) w = monitors[overviewMonitor.value].width - x;
- if (y + h > monitors[overviewMonitor.value].height) h = monitors[overviewMonitor.value].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`,
- // });
- const appIcon = Widget.Icon({
- icon: iconName,
- size: Math.min(w, h) * userOptions.overview.scale / 2.5,
- });
- 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: overviewMonitor.bind().as(monitor => `
- 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: overviewMonitor.bind().as(monitor => `
- margin: ${Math.min(monitors[monitor].width, monitors[monitor].height) * userOptions.overview.scale * userOptions.overview.wsNumMarginScale}px;
- font-size: ${monitors[monitor].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: overviewMonitor.bind().as(monitor => `
- min-width: ${1 + Math.round(monitors[monitor].width * userOptions.overview.scale)}px;
- min-height: ${1 + Math.round(monitors[monitor].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) {
- overviewMonitor.value = Hyprland.active.monitor.id;
- 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/.config/ags/modules/overview/searchbuttons.js b/.config/ags/modules/overview/searchbuttons.js
deleted file mode 100644
index 58f516076..000000000
--- a/.config/ags/modules/overview/searchbuttons.js
+++ /dev/null
@@ -1,189 +0,0 @@
-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';
-import GeminiService from '../../services/gemini.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);
- },
-});
-
-export const AiButton = ({ text }) => searchItem({
- materialIconName: 'chat_paste_go',
- name: 'Ask Gemini',
- actionName: 'Ask',
- content: `${text}`,
- onActivate: () => {
- GeminiService.send(text);
- App.closeWindow('overview');
- App.openWindow('sideleft');
- },
-});
diff --git a/.config/ags/modules/overview/searchitem.js b/.config/ags/modules/overview/searchitem.js
deleted file mode 100644
index 2a3303a48..000000000
--- a/.config/ags/modules/overview/searchitem.js
+++ /dev/null
@@ -1,65 +0,0 @@
-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/.config/ags/modules/overview/windowcontent.js b/.config/ags/modules/overview/windowcontent.js
deleted file mode 100644
index 23e839395..000000000
--- a/.config/ags/modules/overview/windowcontent.js
+++ /dev/null
@@ -1,213 +0,0 @@
-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, AiButton, NoResultButton,
-} from './searchbuttons.js';
-import { checkKeybind } from '../.widgetutils/keybind.js';
-import GeminiService from '../../services/gemini.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: getString('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/.config/ags/modules/screencorners/main.js b/.config/ags/modules/screencorners/main.js
deleted file mode 100644
index 83e09c0b4..000000000
--- a/.config/ags/modules/screencorners/main.js
+++ /dev/null
@@ -1,38 +0,0 @@
-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/.config/ags/modules/session/main.js b/.config/ags/modules/session/main.js
deleted file mode 100644
index 30c5ab6f4..000000000
--- a/.config/ags/modules/session/main.js
+++ /dev/null
@@ -1,14 +0,0 @@
-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/.config/ags/modules/session/sessionscreen.js b/.config/ags/modules/session/sessionscreen.js
deleted file mode 100644
index 007878b6e..000000000
--- a/.config/ags/modules/session/sessionscreen.js
+++ /dev/null
@@ -1,134 +0,0 @@
-// 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(getString('Lock'), 'lock', () => { closeWindowOnAllMonitors('session'); execAsync(['loginctl', 'lock-session']).catch(print) }, {}, 1);
- const logoutButton = SessionButton(getString('Logout'), 'logout', () => { closeWindowOnAllMonitors('session'); execAsync(['bash', '-c', 'pkill Hyprland || pkill sway || pkill niri || loginctl terminate-user $USER']).catch(print) }, {}, 2);
- const sleepButton = SessionButton(getString('Sleep'), 'sleep', () => { closeWindowOnAllMonitors('session'); execAsync(['bash', '-c', 'systemctl suspend || loginctl suspend']).catch(print) }, {}, 3);
- // hibernate, shutdown, reboot
- const hibernateButton = SessionButton(getString('Hibernate'), 'downloading', () => { closeWindowOnAllMonitors('session'); execAsync(['bash', '-c', 'systemctl hibernate || loginctl hibernate']).catch(print) }, {}, 4);
- const shutdownButton = SessionButton(getString('Shutdown'), 'power_settings_new', () => { closeWindowOnAllMonitors('session'); execAsync(['bash', '-c', 'systemctl poweroff || loginctl poweroff']).catch(print) }, {}, 5);
- const rebootButton = SessionButton(getString('Reboot'), 'restart_alt', () => { closeWindowOnAllMonitors('session'); execAsync(['bash', '-c', 'systemctl reboot || loginctl reboot']).catch(print) }, {}, 6);
- const cancelButton = SessionButton(getString('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: getString('Session'),
- }),
- Widget.Label({
- justify: Gtk.Justification.CENTER,
- className: 'txt-small txt',
- label: getString('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/.config/ags/modules/sideleft/apis/ai_chatmessage.js b/.config/ags/modules/sideleft/apis/ai_chatmessage.js
deleted file mode 100644
index 4e7346e43..000000000
--- a/.config/ags/modules/sideleft/apis/ai_chatmessage.js
+++ /dev/null
@@ -1,491 +0,0 @@
-const { 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, Revealer, Scrollable, Stack } = Widget;
-const { execAsync, exec } = Utils;
-import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
-import md2pango, { replaceInlineLatexWithCodeBlocks } from '../../.miscutils/md2pango.js';
-import { darkMode } from "../../.miscutils/system.js";
-import { setupCursorHover } from "../../.widgetutils/cursorhover.js";
-
-const LATEX_DIR = `${GLib.get_user_cache_dir()}/ags/media/latex`;
-const USERNAME = GLib.get_user_name();
-
-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,
- insertSpacesInsteadOfTabs: true,
- indentWidth: 4,
- tabWidth: 4,
- smartHomeEnd: true,
- smartBackspace: true,
- });
- 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${darkMode.value ? '' : '-light'}`));
- buffer.set_text(content, -1);
- return sourceView;
-}
-
-const TextBlock = (content = '') => {
- const widget = Label({
- attribute: {
- 'text': content,
- 'updateText': (text) => {
- widget.attribute.text = text;
- widget.label = md2pango(widget.attribute.text)
- },
- 'appendText': (text) => {
- widget.attribute.text += text;
- widget.label = md2pango(widget.attribute.text)
- },
- },
- hpack: 'fill',
- className: 'txt sidebar-chat-txtblock sidebar-chat-txt',
- useMarkup: true,
- xalign: 0,
- wrap: true,
- selectable: true,
- label: content,
- });
- return widget;
-}
-
-const ThinkBlock = (content = '', revealChild = true) => {
- const revealThought = Variable(revealChild);
- const mainText = Label({
- hpack: 'fill',
- className: `txt sidebar-chat-txtblock-think sidebar-chat-txt`,
- useMarkup: true,
- xalign: 0,
- wrap: true,
- selectable: true,
- label: content,
- });
- const mainTextRevealer = Revealer({
- transition: 'slide_down',
- revealChild: revealThought.value,
- child: mainText,
- setup: (self) => self.hook(revealThought, (self) => {
- self.revealChild = revealThought.value;
- })
- })
- const expandIcon = MaterialIcon(revealThought.value ? 'expand_less' : 'expand_more', 'norm', {
- setup: (self) => self.hook(revealThought, (self) => {
- self.label = revealThought.value ? 'expand_less' : 'expand_more';
- })
- });
- const widget = Box({
- attribute: {
- 'text': content,
- 'updateText': (text) => {
- widget.attribute.text = text;
- mainText.label = md2pango(widget.attribute.text);
- },
- 'appendText': (text) => {
- widget.attribute.text += text;
- mainText.label = md2pango(widget.attribute.text);
- },
- 'done': () => {
- revealThought.value = false;
- }
- },
- className: 'sidebar-chat-thinkblock',
- vertical: true,
- children: [
- Button({
- onClicked: (self) => {
- revealThought.value = !revealThought.value;
- },
- child: Box({
- className: 'spacing-h-10 padding-10',
- children: [
- Box({
- homogeneous: true,
- valign: 'center',
- className: 'sidebar-chat-thinkblock-icon',
- children: [MaterialIcon('neurology', 'large')]
- }),
- Label({
- valign: 'center',
- hexpand: true,
- xalign: 0,
- label: 'Chain of Thought',
- className: 'txt sidebar-chat-thinkblock-txt',
- }),
- Box({
- className: 'sidebar-chat-thinkblock-btn-arrow',
- homogeneous: true,
- children: [expandIcon],
- }),
- ]
- }),
- setup: setupCursorHover,
- }),
- mainTextRevealer,
- ]
- });
- return widget;
-}
-
-Utils.execAsync(['bash', '-c', `rm -rf ${LATEX_DIR}`])
- .then(() => Utils.execAsync(['bash', '-c', `mkdir -p ${LATEX_DIR}`]))
- .catch(print);
-const LatexBlock = (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: {
- 'text': content,
- 'updateText': (text) => {
- wholeThing.attribute.text = text;
- latexViewArea.attribute.render(latexViewArea, wholeThing.attribute.text).catch(print);
- },
- 'appendText': (text) => {
- wholeThing.attribute.text += text;
- latexViewArea.attribute.render(latexViewArea, wholeThing.attribute.text).catch(print);
- },
- },
- children: [Scrollable({
- vscroll: 'never',
- hscroll: 'automatic',
- child: latexViewArea
- })]
- })
- return wholeThing;
-}
-
-const CodeBlock = (content = '', lang = 'txt') => {
- if (lang == 'tex' || lang == 'latex') {
- return LatexBlock(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) => {
- // Enable useful features for multi-line code
- if (text.split('\n').length > 1) {
- sourceView.autoIndent = true;
- sourceView.highlightCurrentLine = true;
- sourceView.showLineNumbers = true;
- sourceView.showLineMarks = true;
- }
- sourceView.get_buffer().set_text(text, -1);
- },
- 'appendText': (text) => {
- codeBlock.attribute.updateText(sourceView.get_buffer().text + text);
- },
- },
- className: 'sidebar-chat-codeblock',
- vertical: true,
- children: [
- topBar,
- Box({
- className: 'sidebar-chat-codeblock-code',
- homogeneous: true,
- children: [Scrollable({
- vscroll: 'never',
- hscroll: 'automatic',
- child: sourceView,
- })],
- })
- ],
- setup: (self) => self.hook(darkMode, (self) => {
- const schemeManager = GtkSource.StyleSchemeManager.get_default();
- Utils.timeout(1000, () => { // Wait for the theme to be loaded
- sourceView.buffer.set_style_scheme(schemeManager.get_scheme(`custom${darkMode.value ? '' : '-light'}`));
- });
- }, "changed"),
- })
-
- // 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: {
- 'lastUpdateTextLength': 0,
- 'inCode': false,
- 'fullUpdate': (self, content, useCursor = false) => {
- // First text widget
- if (contentBox.attribute.lastUpdateTextLength === 0
- && contentBox.get_children().length === 0
- ) {
- contentBox.add(TextBlock())
- }
-
- const codeBlockRegex = /^\s*```([a-zA-Z0-9]+)?\n?/;
- const thinkBlockStartRegex = /^\s*/; // Start:
- const thinkBlockEndRegex = /<\/think>\s*$/; // End:
- const dividerRegex = /^\s*---/;
- const newContent = content.slice(contentBox.attribute.lastUpdateTextLength);
- // print("CONTENT:'" + content + "'")
- // print("LAST UPDATE LENGTH:" + contentBox.attribute.lastUpdateTextLength)
- // print("NEW CONTENT:" + newContent)
- if (newContent.length == 0) return;
- let lines = replaceInlineLatexWithCodeBlocks(newContent).split('\n');
- // let lines = newContent.split('\n');
-
- // Process each line except the last line (potentially incomplete)
- let lastProcessed = 0;
- for (let [index, line] of lines.entries()) {
- if (index == lines.length - 1) break;
- // Code blocks
- if (codeBlockRegex.test(line)) {
- const kids = self.get_children();
- const lastLabel = kids[kids.length - 1];
- const blockContent = lines.slice(lastProcessed, index).join('\n');
-
- if (!contentBox.attribute.inCode) {
- lastLabel.attribute.appendText(blockContent);
- if (lastLabel.label === '') lastLabel.destroy();
- contentBox.add(CodeBlock('', codeBlockRegex.exec(line)[1]));
- }
- else {
- lastLabel.attribute.appendText(blockContent);
- contentBox.add(TextBlock());
- }
-
- lastProcessed = index + 1;
- contentBox.attribute.inCode = !contentBox.attribute.inCode;
- }
- // Think block
- if (!contentBox.attribute.inCode && (thinkBlockStartRegex.test(line) || thinkBlockEndRegex.test(line))) {
- const kids = self.get_children();
- const lastLabel = kids[kids.length - 1];
- const blockContent = lines.slice(lastProcessed, index).join('\n');
-
- lastLabel.attribute.appendText(blockContent);
- if (lastLabel.label === '') lastLabel.destroy();
- if (thinkBlockStartRegex.test(line)) contentBox.add(ThinkBlock());
- else {
- lastLabel.attribute.done();
- contentBox.add(TextBlock());
- }
-
- lastProcessed = index + 1;
- }
- // Breaks
- if (!contentBox.attribute.inCode && dividerRegex.test(line)) {
- const kids = self.get_children();
- const lastLabel = kids[kids.length - 1];
- const blockContent = lines.slice(lastProcessed, index).join('\n');
- lastLabel.attribute.appendText(blockContent);
- contentBox.add(Divider());
- contentBox.add(TextBlock());
- lastProcessed = index + 1;
- }
- }
- if (lastProcessed < lines.length - 1) {
- const kids = self.get_children();
- const lastLabel = kids[kids.length - 1];
- let blockContent = lines.slice(lastProcessed, lines.length - 1).join('\n') + '\n';
- lastLabel.attribute.appendText(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.lastUpdateTextLength = content.length - lines[lines.length - 1].length;
- }
- }
- });
- 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
- if (!isDone && message.role !== 'user') return;
- messageContentBox.attribute.fullUpdate(messageContentBox, message.content + '\n', false);
- // print('----------------')
- // print(message.content)
- }, 'notify::done')
- ,
- })
- ]
- });
- return thisMessage;
-}
-
-export const SystemMessage = (content, commandName, scrolledWindow) => {
- const messageContentBox = MessageContent(content + '\n'); // Add newline so everything is added
- 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/.config/ags/modules/sideleft/apis/booru.js b/.config/ags/modules/sideleft/apis/booru.js
deleted file mode 100644
index b69d8beee..000000000
--- a/.config/ags/modules/sideleft/apis/booru.js
+++ /dev/null
@@ -1,546 +0,0 @@
-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 { SystemMessage } from './ai_chatmessage.js';
-import { AgsToggle } from '../../.commonwidgets/configwidgets_apps.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/*'`);
-
-function getDomainName(url) {
- try {
- const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/i);
- if (!match) return null;
-
- const domainParts = match[1].split('.');
- if (domainParts.length > 2) {
- // Return only the last two parts (e.g., "yande.re" from "files.yande.re")
- return domainParts.slice(-2).join('.');
- }
- return match[1]; // Return as is if no subdomain
- } catch (error) {
- print(`Invalid URL: ${url}`);
- return null;
- }
-}
-
-const TagButton = (command, entry) => Button({
- className: 'sidebar-chat-chip sidebar-chat-chip-action txt txt-small',
- // Interactions disabled for now because they aren't working
- // onClicked: () => { entry.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: getString('Powered by yande.re and konachan'),
- }),
- Button({
- className: 'txt-subtext txt-norm icon-material',
- label: 'info',
- tooltipText: getString('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: 'center',
- className: 'sidebar-chat-settings-toggles',
- children: [
- AgsToggle({
- icon: 'menstrual_health',
- name: getString('Lewds'),
- desc: getString("Shows naughty stuff when enabled"),
- option: 'sidebar.image.allowNsfw',
- extraOnChange: (self, newValue) => {
- BooruService.nsfw = newValue;
- },
- extraSetup: (self) => self.hook(BooruService, (self) => {
- self.attribute.enabled.value = BooruService.nsfw;
- }, 'notify::nsfw')
- }),
- ]
- })
- ]
- })
-});
-
-const booruWelcome = Box({
- vexpand: true,
- homogeneous: true,
- child: Box({
- className: 'spacing-v-15 margin-top-15 margin-bottom-15',
- vpack: 'center',
- vertical: true,
- children: [
- BooruInfo(),
- BooruSettings(),
- ]
- })
-});
-
-const BooruPage = (taglist, serviceName = 'Booru') => {
- const PageState = (icon, name) => Box({
- className: 'spacing-h-5 txt margin-right-5',
- 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 widgetWidth = Math.min(Math.floor(booruContent.get_allocated_width() * 0.9 / userOptions.sidebar.image.columns), data["preview_width"]);
- const widgetWidth = Math.floor(booruContent.get_allocated_width() * 0.9 / userOptions.sidebar.image.columns);
- const widgetHeight = widgetWidth / data.aspect_ratio;
- imageArea.set_size_request(widgetWidth, widgetHeight);
- const showImage = () => {
- 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: `${getString('Go to file url')} (${getDomainName(data.file_url)})`,
- icon: 'file_open',
- action: () => execAsync(['xdg-open', `https://${getDomainName(data.file_url)}/post/show/${data.id}`]).catch(print),
- }),
- ImageAction({
- name: `${getString('Go to source')} (${getDomainName(data.source)})`,
- icon: 'open_in_new',
- action: () => execAsync(['xdg-open', `${data.source}`]).catch(print),
- }),
- ImageAction({
- name: getString('Save image'),
- icon: 'save',
- action: (self) => {
- const currentTags = BooruService.queries.at(-1).realTagList.filter(tag => !tag.includes('rating:'));
- const tagDirectory = currentTags.join('+');
- const fileName = decodeURIComponent((data.file_url).substring((data.file_url).lastIndexOf('/') + 1));
- const saveCommand = `mkdir -p "$(xdg-user-dir PICTURES)/homework/${data.is_nsfw ? '🌶️/' : ''}" && curl -L -o "$(xdg-user-dir PICTURES)/homework/${data.is_nsfw ? '🌶️/' : ''}${fileName}" '${data.file_url}'`;
- print(saveCommand)
- execAsync(['bash', '-c', saveCommand])
- .then(() => self.label = 'done')
- .catch(print);
- },
- }),
- ImageAction({
- name: getString('Set as wallpaper'),
- icon: 'wallpaper',
- action: (self) => {
- const currentTags = BooruService.queries.at(-1).realTagList.filter(tag => !tag.includes('rating:'));
- let fileExtension = data.file_ext || 'jpg';
- print(data)
- const fileName = decodeURIComponent((data.file_url).substring((data.file_url).lastIndexOf('/') + 1));
- const saveCommand = `mkdir -p "$(xdg-user-dir PICTURES)/Wallpapers" && curl -L -o "$(xdg-user-dir PICTURES)/Wallpapers/${fileName}" '${data.file_url}'`;
- const setWallpaperCommand = `${App.configDir}/scripts/color_generation/switchwall.sh "$(xdg-user-dir PICTURES)/Wallpapers/${fileName}"`;
- // const
- execAsync(['bash', '-c', `${saveCommand} && ${setWallpaperCommand}`])
- .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', getString('Calling API')),
- 'download': PageState('downloading', getString('Downloading image')),
- 'done': PageState('done', getString('Finished!')),
- 'error': PageState('error', getString('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({
- className: 'margin-5',
- 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 pageTip = Revealer({
- transition: 'slide_down',
- transitionDuration: 0,
- revealChild: false,
- child: Box({
- className: 'txt-subtext margin-5',
- children: [
- Box({
- homogeneous: true,
- className: 'sidebar-booru-tip-icon',
- children: [MaterialIcon('lightbulb', 'larger')]
- }),
- Label({
- label: getString("No tag in mind? Type a page number"),
- className: 'txt-smallie',
- wrap: true,
- xalign: 0,
- })
- ]
- })
- })
- const pageContentRevealer = Revealer({
- transition: 'slide_down',
- transitionDuration: userOptions.animations.durationLarge,
- revealChild: false,
- child: Box({
- vertical: true,
- children: [
- pageImages,
- pageTip,
- ]
- }),
- });
- const thisPage = Box({
- homogeneous: true,
- className: 'sidebar-chat-message',
- attribute: {
- 'imagePath': '',
- 'isNsfw': false,
- 'showContent': () => {
- Utils.timeout(IMAGE_REVEAL_DELAY,
- () => pageContentRevealer.revealChild = true
- );
- },
- 'update': (data, force = false) => {
- // Sort by .aspect_ratio
- data = data.sort(
- (a, b) => a.aspect_ratio - b.aspect_ratio
- );
- if (data.length == 0) {
- pageTip.revealChild = true;
- downloadState.shown = 'error';
- thisPage.attribute.showContent();
- 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
- thisPage.attribute.showContent();
- downloadIndicator.attribute.hide();
- },
- },
- children: [Box({
- vertical: true,
- children: [
- pageHeading,
- Box({
- vertical: true,
- children: [pageContentRevealer],
- })
- ]
- })],
- });
- 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 = (chatEntry) => 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());
- })
- }
-});
-
-export const booruCommands = Box({
- className: 'spacing-h-5',
- setup: (self) => {
- self.pack_end(CommandButton('/clear'), false, false, 0);
- self.pack_end(CommandButton('+'), false, false, 0);
- self.pack_end(CommandButton('/mode konachan', 'Konachan'), false, false, 0);
- self.pack_end(CommandButton('/mode yandere', 'yande.re'), false, false, 0);
- }
-});
-
-const clearChat = () => { // destroy!!
- booruContent.attribute.map.forEach((value, key, map) => {
- value.destroy();
- value = null;
- });
-}
-
-export const sendMessage = (text, booruView) => {
- // 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/.config/ags/modules/sideleft/apis/chatgpt.js b/.config/ags/modules/sideleft/apis/chatgpt.js
deleted file mode 100644
index 06127f551..000000000
--- a/.config/ags/modules/sideleft/apis/chatgpt.js
+++ /dev/null
@@ -1,377 +0,0 @@
-const { 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, 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';
-
-const AGS_CONFIG_FILE = `${App.configDir}/user_options.jsonc`;
-
-export const chatGPTTabIcon = Icon({
- hpack: 'center',
- icon: `openai-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';
- // Save provider to config
- Utils.execAsync(['bash', '-c', `${App.configDir}/scripts/ags/agsconfigurator.py \
- --key ai.defaultGPTProvider \
- --value ${id} \
- --file ${AGS_CONFIG_FILE}`
- ]).catch(print);
- },
- 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,
- });
- }
- let indicatorIcon = Icon({
- icon: GPTService.providers[GPTService._currentProvider]['logo_name'],
- className: 'txt-large',
- setup: (self) => self.hook(GPTService, (self) => {
- self.icon = GPTService.providers[GPTService.providerID]['logo_name'];
- }, 'providerChanged')
- });
- const indicatorChevron = MaterialIcon('expand_more', 'norm');
- const indicatorButton = Button({
- tooltipText: getString('Select ChatGPT-compatible API provider'),
- child: Box({
- className: 'spacing-h-10 txt',
- children: [
- indicatorIcon,
- 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: `openai-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 (GPTs)`,
- }),
- Box({
- className: 'spacing-h-5',
- hpack: 'center',
- children: [
- Label({
- className: 'txt-smallie txt-subtext',
- wrap: true,
- justify: Gtk.Justification.CENTER,
- label: getString('Provider shown above'),
- }),
- Button({
- className: 'txt-subtext txt-norm icon-material',
- label: 'info',
- tooltipText: getString("Chat with models compatible with OpenAI's Chat Completions API.\nNot affiliated, endorsed, or sponsored by any of the providers."),
- 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: getString('The model\'s temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1'),
- options: [
- { value: 0.00, name: getString('Precise'), },
- { value: 0.50, name: getString('Balanced'), },
- { value: 1.00, name: getString('Creative'), },
- ],
- initIndex: 1,
- onChange: (value, name) => {
- GPTService.temperature = value;
- },
- }),
- ConfigGap({ vertical: true, size: 10 }), // Note: size can only be 5, 10, or 15
- Box({
- vertical: true,
- hpack: 'center',
- className: 'sidebar-chat-settings-toggles',
- children: [
- ConfigToggle({
- icon: 'model_training',
- name: getString('Prompt'),
- desc: getString('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.providers[GPTService.providerID]["requires_key"]
- && GPTService.key.length == 0);
- }, 'hasKey')
- ,
- child: Button({
- child: Label({
- useMarkup: true,
- wrap: true,
- className: 'txt sidebar-chat-welcome-txt',
- justify: Gtk.Justification.CENTER,
- label: getString('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 margin-top-15 margin-bottom-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')
- .hook(GPTService, (self, hasKey) => {
- self.revealChild = (
- GPTService.providers[GPTService.providerID]["requires_key"]
- && GPTService.key.length == 0);
- }, 'providerChanged')
- ,
-});
-
-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.providers[GPTService.providerID]["requires_key"]
- && GPTService.key.length == 0
- && !text.startsWith('/key')) {
- GPTService.key = text;
- chatContent.add(SystemMessage(`Key saved to \`${GPTService.keyPath}\`\nUpdate anytime with \`/key YOUR_API_KEY\`.`, 'API Key', ChatGPTView));
- text = '';
- return;
- }
- // Commands
- if (text.startsWith('/')) {
- if (text.startsWith('/clear')) clearChat();
- else if (text.startsWith('/model')) chatContent.add(SystemMessage(`${getString("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(
- `${getString("Key stored in:")}\n\`${GPTService.keyPath}\`\n${getString("To update this key, type")} \`/key YOUR_API_KEY\``,
- '/key',
- ChatGPTView));
- else {
- GPTService.key = parts[1];
- chatContent.add(SystemMessage(`${getString("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(getString("Invalid command."), 'Error', ChatGPTView))
- }
- else {
- GPTService.send(text);
- }
-}
-
-export const ChatGPTView = (chatEntry) => 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());
- })
- }
- })
- ]
-});
\ No newline at end of file
diff --git a/.config/ags/modules/sideleft/apis/gemini.js b/.config/ags/modules/sideleft/apis/gemini.js
deleted file mode 100644
index b31380662..000000000
--- a/.config/ags/modules/sideleft/apis/gemini.js
+++ /dev/null
@@ -1,296 +0,0 @@
-const { 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, 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 { AgsToggle } from '../../.commonwidgets/configwidgets_apps.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: getString('Powered by Google'),
- }),
- Button({
- className: 'txt-subtext txt-norm icon-material',
- label: 'info',
- tooltipText: getString("Not affiliated, endorsed, or sponsored by Google.\n\nPrivacy: Chat messages aren't linked to your account,\nbut 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: getString("Gemini's temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1"),
- options: [
- { value: 0.00, name: getString('Precise'), },
- { value: 0.50, name: getString('Balanced'), },
- { value: 1.00, name: getString('Creative'), },
- ],
- initIndex: 1,
- onChange: (value, name) => {
- GeminiService.temperature = value;
- },
- }),
- ConfigGap({ vertical: true, size: 10 }), // Note: size can only be 5, 10, or 15
- Box({
- vertical: true,
- hpack: 'center',
- className: 'sidebar-chat-settings-toggles',
- children: [
- AgsToggle({
- icon: 'model_training',
- name: getString('Prompt'),
- desc: getString("Tells Gemini:\n- It's a Linux sidebar assistant\n- Be brief and use bullet points"),
- option: "ai.enhancements",
- extraOnChange: (self, newValue) => {
- GeminiService.assistantPrompt = newValue;
- },
- extraOnReset: (self, newValue) => {
- GeminiService.assistantPrompt = userOptions.ai.enhancements;
- },
- }),
- AgsToggle({
- icon: 'shield',
- name: getString('Safety'),
- desc: getString("When turned off, tells the API (not the model) \nto not block harmful/explicit content"),
- option: "ai.safety",
- extraOnChange: (self, newValue) => {
- GeminiService.safe = newValue;
- },
- extraOnReset: (self, newValue) => {
- GeminiService.safe = userOptions.ai.safety;
- },
- }),
- AgsToggle({
- icon: 'history',
- name: getString('History'),
- desc: getString("Saves chat history\nMessages in previous chats won't show automatically, but they are there"),
- option: "ai.useHistory",
- extraOnChange: (self, newValue) => {
- GeminiService.useHistory = newValue;
- },
- extraOnReset: (self, newValue) => {
- GeminiService.useHistory = userOptions.ai.useHistory;
- },
- })
- ]
- })
- ]
- })
-});
-
-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 margin-top-15 margin-bottom-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 \`${GeminiService.keyPath}\`\nUpdate anytime with /key YOUR_API_KEY.`, '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(`${getString("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(
- `${getString("Key stored in:")} \n\`${GeminiService.keyPath}\`\n${getString("To update this key, type")} \`/key YOUR_API_KEY\``,
- '/key',
- GeminiView));
- else {
- GeminiService.key = parts[1];
- chatContent.add(SystemMessage(`${getString("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(getString(`Invalid command.`), 'Error', GeminiView))
- }
- else {
- GeminiService.send(text);
- }
-}
-
-export const GeminiView = (chatEntry) => 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/.config/ags/modules/sideleft/apis/waifu.js b/.config/ags/modules/sideleft/apis/waifu.js
deleted file mode 100644
index 5a1a2c019..000000000
--- a/.config/ags/modules/sideleft/apis/waifu.js
+++ /dev/null
@@ -1,418 +0,0 @@
-// 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';
-
-async function getImageViewerApp(preferredApp) {
- return 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: getString('Powered by waifu.im + other APIs'),
- }),
- Button({
- className: 'txt-subtext txt-norm icon-material',
- label: 'info',
- tooltipText: getString('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 margin-top-15 margin-bottom-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', getString('Calling API')),
- 'download': ImageState('downloading', getString('Downloading image')),
- 'done': ImageState('done', getString('Finished!')),
- 'error': ImageState('error', getString('Error')),
- 'notfound': ImageState('error', getString('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: getString('Go to source'),
- icon: 'link',
- action: () => execAsync(['xdg-open', `${thisBlock.attribute.imageData.source}`]).catch(print),
- }),
- ImageAction({
- name: getString('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: getString('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 = (chatEntry) => 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: getString('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/.config/ags/modules/sideleft/apiwidgets.js b/.config/ags/modules/sideleft/apiwidgets.js
deleted file mode 100644
index 17b7576ab..000000000
--- a/.config/ags/modules/sideleft/apiwidgets.js
+++ /dev/null
@@ -1,243 +0,0 @@
-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 } 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';
-import { updateNestedProperty } from '../.miscutils/objects.js';
-
-const EXPAND_INPUT_THRESHOLD = 30;
-const AGS_CONFIG_FILE = `${App.configDir}/user_options.jsonc`;
-
-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 ? getString('Message the model...') : getString('Enter API Key...'));
- }, 'hasKey')
- .hook(Gemini, (self) => {
- if (APIS[currentApiId].name != 'Assistant (Gemini Pro)') return;
- self.placeholderText = (Gemini.key.length > 0 ? getString('Message Gemini...') : getString('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 || event.get_keyval()[1] === Gdk.KEY_KP_Enter) {
- 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;
- }
- })
- ,
-});
-
-const APILIST = {
- 'gemini': {
- "name": 'Assistant (Gemini Pro)',
- "sendCommand": geminiSendMessage,
- "contentWidget": GeminiView(chatEntry),
- "commandBar": geminiCommands,
- "tabIcon": geminiTabIcon,
- "placeholderText": getString('Message Gemini...'),
- },
- 'gpt': {
- "name": 'Assistant (GPTs)',
- "sendCommand": chatGPTSendMessage,
- "contentWidget": ChatGPTView(chatEntry),
- "commandBar": chatGPTCommands,
- "tabIcon": chatGPTTabIcon,
- "placeholderText": getString('Message the model...'),
- },
- 'waifu': {
- "name": 'Waifus',
- "sendCommand": waifuSendMessage,
- "contentWidget": WaifuView(chatEntry),
- "commandBar": waifuCommands,
- "tabIcon": waifuTabIcon,
- "placeholderText": getString('Enter tags'),
- },
- 'booru': {
- "name": 'Booru',
- "sendCommand": booruSendMessage,
- "contentWidget": BooruView(chatEntry),
- "commandBar": booruCommands,
- "tabIcon": booruTabIcon,
- "placeholderText": getString('Enter tags and/or page number'),
- },
-}
-const APIS = userOptions.sidebar.pages.apis.order.map((apiName) => {
- const obj = { ...APILIST[apiName] };
- obj["id"] = apiName;
- return obj;
-});
-let currentApiId = APIS.findIndex(obj => obj.id === userOptions.sidebar.pages.apis.defaultPage);
-
-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
- if (APIS[currentApiId].name == APILIST['booru'].name)
- APIS[currentApiId].sendCommand(text, APILIST['booru'].contentWidget)
- else
- APIS[currentApiId].sendCommand(text)
- // Reset
- buffer.set_text("", -1);
- chatEntryWrapper.toggleClassName('sidebar-chat-wrapper-extended', false);
- chatEntry.set_valign(Gtk.Align.CENTER);
-}
-
-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),
- initIndex: currentApiId,
- onChange: (self, id) => {
- apiCommandStack.shown = APIS[id].name;
- chatPlaceholder.label = APIS[id].placeholderText;
- currentApiId = id;
- const pageName = APIS[id].id;
- const option = 'sidebar.pages.apis.defaultPage';
- updateNestedProperty(userOptions, option, pageName);
- execAsync(['bash', '-c', `${App.configDir}/scripts/ags/agsconfigurator.py \
- --key ${option} \
- --value ${pageName} \
- --file ${AGS_CONFIG_FILE}`
- ]).catch(print);
- }
-
-});
-
-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;
diff --git a/.config/ags/modules/sideleft/main.js b/.config/ags/modules/sideleft/main.js
deleted file mode 100644
index dd52ac07d..000000000
--- a/.config/ags/modules/sideleft/main.js
+++ /dev/null
@@ -1,18 +0,0 @@
-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: 'top',
- child: Box({
- children: [
- SidebarLeft(),
- clickCloseRegion({ name: 'sideleft', multimonitor: false, fillMonitor: 'horizontal' }),
- ]
- })
-});
diff --git a/.config/ags/modules/sideleft/sideleft.js b/.config/ags/modules/sideleft/sideleft.js
deleted file mode 100644
index 0b276d0af..000000000
--- a/.config/ags/modules/sideleft/sideleft.js
+++ /dev/null
@@ -1,158 +0,0 @@
-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';
-import { updateNestedProperty } from '../.miscutils/objects.js';
-
-const AGS_CONFIG_FILE = `${App.configDir}/user_options.jsonc`;
-
-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-controlbtn-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 = 'exclusive';
-// }
-// else {
-// sideleftWindow.exclusivity = 'normal';
-// }
-// },
-// },
-// vpack: 'start',
-// className: 'sidebar-controlbtn',
-// child: MaterialIcon('push_pin', 'larger'),
-// tooltipText: `Pin sidebar (${userOptions.keybinds.sidebar.pin})`,
-// onClicked: (self) => self.attribute.toggle(self),
-// setup: (self) => {
-// setupCursorHover(self);
-// self.hook(App, (self, currentName, visible) => {
-// if (currentName === 'sideleft' && visible) self.grab_focus();
-// })
-// },
-// })
-
-const expandButton = Button({
- attribute: {
- 'enabled': false,
- 'toggle': (self) => {
- self.attribute.enabled = !self.attribute.enabled;
- // We don't expand the bar, but the expand button. Funny hax but it works
- // (somehow directly expanding the sidebar directly makes it unable to unexpand)
- self.toggleClassName('sidebar-expandbtn-enabled', self.attribute.enabled);
- self.toggleClassName('sidebar-controlbtn-enabled', self.attribute.enabled);
- },
- },
- vpack: 'start',
- className: 'sidebar-controlbtn',
- child: MaterialIcon('expand_content', 'larger'),
- tooltipText: `Expand sidebar (${userOptions.keybinds.sidebar.expand})`,
- onClicked: (self) => self.attribute.toggle(self),
- setup: setupCursorHover,
-})
-
-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',
- initIndex: CONTENTS.findIndex(obj => obj.name === userOptions.sidebar.pages.defaultPage),
- onChange: (self, index) => {
- const pageName = CONTENTS[index].name;
- const option = 'sidebar.pages.defaultPage';
- updateNestedProperty(userOptions, option, pageName);
- execAsync(['bash', '-c', `${App.configDir}/scripts/ags/agsconfigurator.py \
- --key ${option} \
- --value ${pageName} \
- --file ${AGS_CONFIG_FILE}`
- ]).catch(print);
- },
- extraTabStripWidgets: [
- // pinButton,
- expandButton,
- ]
-});
-
-export default () => {
- return 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.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.expand))
- expandButton.attribute.toggle(expandButton);
- // if (checkKeybind(event, userOptions.keybinds.sidebar.pin))
- // pinButton.attribute.toggle(pinButton);
-
- 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/.config/ags/modules/sideleft/toolbox.js b/.config/ags/modules/sideleft/toolbox.js
deleted file mode 100644
index 879082fde..000000000
--- a/.config/ags/modules/sideleft/toolbox.js
+++ /dev/null
@@ -1,22 +0,0 @@
-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 Conversions from './tools/conversions.js';
-import Name from './tools/name.js';
-
-export default Scrollable({
- hscroll: "never",
- vscroll: "automatic",
- child: Box({
- vertical: true,
- className: 'spacing-v-10',
- children: [
- QuickScripts(),
- Conversions(),
- ColorPicker(),
- Box({ vexpand: true }),
- Name(),
- ]
- })
-});
diff --git a/.config/ags/modules/sideleft/tools/changeres.sh b/.config/ags/modules/sideleft/tools/changeres.sh
deleted file mode 100644
index 333f09881..000000000
--- a/.config/ags/modules/sideleft/tools/changeres.sh
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/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/.config/ags/modules/sideleft/tools/color.js b/.config/ags/modules/sideleft/tools/color.js
deleted file mode 100644
index 568a04c33..000000000
--- a/.config/ags/modules/sideleft/tools/color.js
+++ /dev/null
@@ -1,198 +0,0 @@
-// 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/.config/ags/modules/sideleft/tools/colorpicker.js b/.config/ags/modules/sideleft/tools/colorpicker.js
deleted file mode 100644
index cd37c6f03..000000000
--- a/.config/ags/modules/sideleft/tools/colorpicker.js
+++ /dev/null
@@ -1,283 +0,0 @@
-// 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: getString('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: getString('Color picker'),
- revealChild: false,
- child: Box({
- className: 'spacing-h-5',
- children: [
- hueSelector,
- saturationAndLightnessSelector,
- result,
- ]
- })
- });
-}
\ No newline at end of file
diff --git a/.config/ags/modules/sideleft/tools/conversions.js b/.config/ags/modules/sideleft/tools/conversions.js
deleted file mode 100644
index 8d26dc487..000000000
--- a/.config/ags/modules/sideleft/tools/conversions.js
+++ /dev/null
@@ -1,169 +0,0 @@
-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, Entry, EventBox, Icon, Label, Scrollable, Overlay } = Widget;
-import SidebarModule from './module.js';
-import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
-import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
-import { truncateToPrecision } from '../../.miscutils/mathfuncs.js';
-
-const VALUE_DEFAULT_PRECISION = 3;
-const conversions = [
- {
- unit1: 'px',
- unit2: 'rem',
- unit1Default: 5,
- formula1to2: '{{x}} / (parseFloat(Utils.exec(\'gsettings get org.gnome.desktop.interface font-name\').split(" ").pop().split("\'"))*4/3)',
- formula2to1: '{{x}} * (parseFloat(Utils.exec(\'gsettings get org.gnome.desktop.interface font-name\').split(" ").pop().split("\'"))*4/3)',
- forcePrecision: true,
- },
- {
- unit1: 'deg',
- unit2: 'rad',
- unit1Default: 90,
- formula1to2: '{{x}} * Math.PI / 180',
- formula2to1: '{{x}} * 180 / Math.PI',
- },
- {
- unit1: '°F',
- unit2: '°C',
- unit1Default: 68,
- formula1to2: '({{x}} - 32) * 5 / 9',
- formula2to1: '{{x}} * 9 / 5 + 32',
- },
- {
- unit1: 'Ft',
- unit2: 'Cm',
- formula1to2: '{{x}} * 30.48',
- formula2to1: '{{x}} / 30.48',
- },
- // {
- // unit1: 'Mile',
- // unit2: 'Km',
- // formula1to2: '{{x}} * 1.60934',
- // formula2to1: '{{x}} / 1.60934',
- // },
- // {
- // unit1: 'Inch',
- // unit2: 'Cm',
- // formula1to2: '{{x}} * 2.54',
- // formula2to1: '{{x}} / 2.54',
- // },
- {
- unit1: 'lbs',
- unit2: 'Kg',
- formula1to2: '{{x}} * 0.453592',
- formula2to1: '{{x}} / 0.453592',
- }
-]
-
-export default () => {
- const ValueBox = ({ unit, initValue = 0, updateCallback }) => {
- const unitName = Label({
- xalign: 0,
- className: 'txt txt-smallie txt-semibold margin-top-2 margin-left-2',
- label: `${unit}`,
- });
- const entry = Entry({
- hexpand: 'true',
- widthChars: 10,
- className: 'txt-small techfont margin-left-2',
- text: `${initValue}`,
- onChange: updateCallback,
- });
- const copyButton = Button({
- className: 'sidebar-module-csscalc-valuebox-copybtn',
- child: MaterialIcon('content_copy', 'norm'),
- onClicked: (self) => {
- Utils.execAsync(['wl-copy', entry.text]);
- self.child.label = 'done';
- Utils.timeout(1000, () => self.child.label = 'content_copy');
- },
- setup: setupCursorHover,
- });
- const wholeThing = Box({
- className: 'sidebar-module-csscalc-valuebox',
- vertical: true,
- hexpand: true,
- children: [
- unitName,
- Box({
- children: [
- entry,
- copyButton,
- ]
- })
- ],
- attribute: {
- updateValue: (value) => entry.text = `${value}`,
- getValue: () => entry.text,
- }
- });
- return wholeThing;
- }
- // Formula format is js expression, with `{{x}}` being the input value
- const BidirectionalConversion = ({
- unit1, unit2, unit1Default = 1,
- formula1to2, formula2to1,
- forcePrecision = false, precision = VALUE_DEFAULT_PRECISION,
- }) => {
- let updateLock = false;
- const convert = (value, formula) => {
- let thisValue;
- try {
- thisValue = eval(value)
- } catch (error) {
- thisValue = parseFloat(value);
- }
- // print(formula.replace('{{x}}', thisValue))
- // print(eval(formula.replace('{{x}}', thisValue)))
- const evalResult = eval(formula.replace('{{x}}', thisValue));
- const result = forcePrecision ?
- evalResult.toFixed(precision) : truncateToPrecision(evalResult, precision);
- // print(result)
- return result;
- }
- const unit1Box = ValueBox({
- unit: unit1,
- initValue: unit1Default,
- updateCallback: (self) => {
- if (updateLock) return;
- updateLock = true;
- const newValue = convert(self.text, formula1to2);
- unit2Box.attribute.updateValue(newValue || 0);
- updateLock = false;
- },
- });
- const unit2Box = ValueBox({
- unit: unit2,
- initValue: truncateToPrecision(eval(formula1to2.replace('\{{x}}', unit1Default)), precision),
- updateCallback: (self) => {
- if (updateLock) return;
- updateLock = true;
- const newValue = convert(self.text, formula2to1);
- unit1Box.attribute.updateValue(newValue || 0);
- updateLock = false;
- },
- });
- return Box({
- className: 'txt spacing-h-10',
- children: [
- unit1Box,
- MaterialIcon('swap_horiz', 'large'),
- unit2Box,
- ]
- })
- }
-
- return SidebarModule({
- icon: MaterialIcon('autorenew', 'norm'),
- name: getString('Conversions'),
- child: Box({
- vertical: true,
- className: 'spacing-v-5',
- children: conversions.map(BidirectionalConversion),
- })
- });
-}
\ No newline at end of file
diff --git a/.config/ags/modules/sideleft/tools/module.js b/.config/ags/modules/sideleft/tools/module.js
deleted file mode 100644
index 2cba49ec7..000000000
--- a/.config/ags/modules/sideleft/tools/module.js
+++ /dev/null
@@ -1,57 +0,0 @@
-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/.config/ags/modules/sideleft/tools/name.js b/.config/ags/modules/sideleft/tools/name.js
deleted file mode 100644
index a5cca936e..000000000
--- a/.config/ags/modules/sideleft/tools/name.js
+++ /dev/null
@@ -1,26 +0,0 @@
-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: getString('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/.config/ags/modules/sideleft/tools/quickscripts.js b/.config/ags/modules/sideleft/tools/quickscripts.js
deleted file mode 100644
index ccf0369e8..000000000
--- a/.config/ags/modules/sideleft/tools/quickscripts.js
+++ /dev/null
@@ -1,103 +0,0 @@
-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: getString('Change screen resolution'),
- command: `bash ${App.configDir}/modules/sideleft/tools/changeres.sh`,
- enabled: true,
- },
- {
- icon: 'nixos-symbolic',
- name: getString('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: getString('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: getString('Update packages'),
- command: `sudo apt update && sudo apt upgrade -y`,
- enabled: isDebianDistro,
- },
- {
- icon: 'fedora-symbolic',
- name: getString('Update packages'),
- command: `sudo dnf upgrade -y`,
- enabled: distroID == 'fedora',
- },
- {
- icon: 'arch-symbolic',
- name: getString('Update packages'),
- command: `sudo pacman -Syyu`,
- enabled: isArchDistro,
- },
- {
- icon: 'arch-symbolic',
- name: getString('Remove orphan packages'),
- command: `sudo pacman -R (pacman -Qdtq)`,
- enabled: isArchDistro,
- },
- {
- icon: 'flatpak-symbolic',
- name: getString('Uninstall unused flatpak packages'),
- command: `flatpak uninstall --unused`,
- enabled: hasFlatpak,
- },
-];
-
-export default () => SidebarModule({
- icon: MaterialIcon('code', 'norm'),
- name: getString('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/.config/ags/modules/sideright/calendar.js b/.config/ags/modules/sideright/calendar.js
deleted file mode 100644
index d70f98801..000000000
--- a/.config/ags/modules/sideright/calendar.js
+++ /dev/null
@@ -1,273 +0,0 @@
-import GLib from 'gi://GLib';
-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 } = Widget;
-
-import { MaterialIcon } from '../.commonwidgets/materialicon.js';
-import { setupCursorHover } from '../.widgetutils/cursorhover.js';
-
-import Todo from "../../services/todo.js";
-import { TodoWidget } from "./todolist.js";
-import { getCalendarLayout } from "./calendar_layout.js";
-
-const AGS_CONFIG_FILE = `${App.configDir}/user_options.jsonc`;
-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: getString('Mo'), today: 0 },
- { day: getString('Tu'), today: 0 },
- { day: getString('We'), today: 0 },
- { day: getString('Th'), today: 0 },
- { day: getString('Fr'), today: 0 },
- { day: getString('Sa'), today: 0 },
- { day: getString('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,
- ]
- })
- ]
- })
- });
-};
-
-export const ModuleCalendar = () => {
- const defaultShown = 'calendar';
- const navrailButton = (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);
- })
- });
- const navrail = Box({
- vpack: 'center',
- homogeneous: true,
- vertical: true,
- className: 'sidebar-navrail spacing-v-10',
- children: [
- navrailButton('calendar', 'calendar_month', getString('Calendar')),
- navrailButton('todo', 'done_outline', getString('To Do')),
- ]
- });
- const contentStack = Widget.Stack({
- hexpand: true,
- children: {
- 'calendar': CalendarWidget(),
- 'todo': TodoWidget(),
- },
- transition: 'slide_up_down',
- transitionDuration: userOptions.animations.durationLarge,
- setup: (stack) => Utils.timeout(1, () => {
- stack.shown = defaultShown;
- })
- })
-
- const CollapseButtonIcon = (collapse) => MaterialIcon(collapse ? 'expand_more' : 'expand_less', 'norm');
- const CollapseButton = (collapse) => {
- const collapseButtonIcon = CollapseButtonIcon(collapse);
- return Button({
- hpack: 'start',
- vpack: 'start',
- className: 'margin-top-5 margin-left-5 margin-bottom-5',
- onClicked: () => {
- mainStack.shown = (mainStack.shown == 'expanded') ? 'collapsed' : 'expanded';
- Utils.execAsync(['bash', '-c', `${App.configDir}/scripts/ags/agsconfigurator.py \
- --key "sidebar.calendar.expandByDefault" \
- --value ${!userOptions.sidebar.calendar.expandByDefault} \
- --file ${AGS_CONFIG_FILE}`
- ]).catch(print);
-
- },
- setup: setupCursorHover,
- child: Box({
- className: 'sidebar-calendar-btn-arrow txt',
- homogeneous: true,
- children: [collapseButtonIcon],
- }),
- tooltipText: collapse ? getString('Collapse calendar') : getString('Expand calendar'),
- })
- }
- const date = Variable('', {
- poll: [
- userOptions.time.interval,
- () => GLib.DateTime.new_now_local().format(userOptions.time.calendarDateFormat),
- ],
- })
-
- const collapsedWidget = Box({
- className: 'spacing-h-5',
- children: [
- CollapseButton(false),
- Widget.Label({
- vpack: 'center',
- className: 'txt txt-small sidebar-calendar-collapsed-pill',
- label: date.bind(),
- }),
- Widget.Label({
- vpack: 'center',
- className: 'txt txt-small sidebar-calendar-collapsed-pill',
- label: `${Todo.todo_json.length} ${getString('To do tasks')}`,
- setup: (self) => self.hook(Todo, (self) => {
- self.label = `${Todo.todo_json.length} ${getString('To do tasks')}`
- }, 'updated')
- }),
- ]
- })
-
- const mainStack = Widget.Stack({
- className: 'sidebar-group',
- homogeneous: false,
- children: {
- 'collapsed': collapsedWidget,
- 'expanded': Box({
- className: 'spacing-h-5',
- children: [
- Overlay({
- child: navrail,
- overlays: [CollapseButton(true)],
- }),
- contentStack
- ]
- }),
- },
- transition: 'slide_up_down',
- transitionDuration: userOptions.animations.durationLarge,
- shown: userOptions.sidebar.calendar.expandByDefault ? 'expanded' : 'collapsed',
- })
-
- return mainStack;
-}
-
diff --git a/.config/ags/modules/sideright/centermodules/audiocontrols.js b/.config/ags/modules/sideright/centermodules/audiocontrols.js
deleted file mode 100644
index 7cc4ec354..000000000
--- a/.config/ags/modules/sideright/centermodules/audiocontrols.js
+++ /dev/null
@@ -1,222 +0,0 @@
-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, substitute } 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 = substitute(
- stream["icon-name"] ||
- stream.stream["icon-name"] ||
- stream.stream["application-id"] ||
- stream.stream["name"]);
- })
- },
- }),
- 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;
- self.adjustment["step-increment"] = 0.1;
- })
- }),
- // 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({
- tooltipText: stream.description,
- 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: getString('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/.config/ags/modules/sideright/centermodules/bluetooth.js b/.config/ags/modules/sideright/centermodules/bluetooth.js
deleted file mode 100644
index 86648be28..000000000
--- a/.config/ags/modules/sideright/centermodules/bluetooth.js
+++ /dev/null
@@ -1,164 +0,0 @@
-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 ? getString('Connected') : (device.paired ? getString('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, () => {
- const enabledState = self.attribute.enabled;
- if (enabledState.value !== device.connected)
- enabledState.value = device.connected;
- });
- },
- })
- const deviceRemoveButton = Button({
- vpack: 'center',
- className: 'sidebar-bluetooth-device-remove',
- child: MaterialIcon('delete', 'norm'),
- tooltipText: getString('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: getString('More'),
- setup: setupCursorHover,
- })],
- })
- return Box({
- ...props,
- className: 'spacing-v-5',
- vertical: true,
- children: [
- mainContent,
- bottomBar
- ]
- });
-}
diff --git a/.config/ags/modules/sideright/centermodules/configure.js b/.config/ags/modules/sideright/centermodules/configure.js
deleted file mode 100644
index 572c77c21..000000000
--- a/.config/ags/modules/sideright/centermodules/configure.js
+++ /dev/null
@@ -1,104 +0,0 @@
-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, Overlay } = Widget;
-const { execAsync, exec } = Utils;
-import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
-import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
-import { ConfigGap, ConfigToggle } from '../../.commonwidgets/configwidgets.js';
-import { AgsSpinButton, HyprlandSpinButton, HyprlandToggle } from '../../.commonwidgets/configwidgets_apps.js';
-
-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 = Overlay({
- passThrough: true,
- child: Scrollable({
- vexpand: true,
- child: Box({
- vertical: true,
- className: 'spacing-v-10 sidebar-centermodules-scrollgradient-bottom-contentmargin',
- children: [
- ConfigSection({
- name: getString('Effects'), children: [
- ConfigToggle({
- icon: 'border_clear',
- name: getString('Transparency'),
- desc: getString('[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: getString('Blur'), desc: getString("[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: getString('X-ray'), desc: getString("[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: getString('Size'), desc: getString('[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: getString('Passes'), desc: getString('[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: getString('Animations'), desc: getString('[Hyprland] [GTK]\nEnable animations'), option: 'animations:enabled',
- extraOnChange: (self, newValue) => execAsync(['gsettings', 'set', 'org.gnome.desktop.interface', 'enable-animations', `${newValue}`]),
- extraOnReset: (self, newValue) => execAsync(['gsettings', 'set', 'org.gnome.desktop.interface', 'enable-animations', 'true']),
- }),
- Subcategory([
- AgsSpinButton({
- option: "animations.choreographyDelay",
- icon: 'clear_all',
- name: getString('Choreography delay'),
- desc: getString('In milliseconds, the delay between animations of a series'),
- step: 10, minValue: 0, maxValue: 1000,
- })
- ]),
- ]
- }),
- ConfigSection({
- name: getString('Developer'), children: [
- HyprlandToggle({ icon: 'speed', name: getString('Show FPS'), desc: getString("[Hyprland]\nShow FPS overlay on top-left corner"), option: "debug:overlay", save: false }),
- HyprlandToggle({ icon: 'sort', name: getString('Log to stdout'), desc: getString("[Hyprland]\nPrint LOG, ERR, WARN, etc. messages to the console"), option: "debug:enable_stdout_logs" }),
- HyprlandToggle({ icon: 'motion_sensor_active', name: getString('Damage tracking'), desc: getString("[Hyprland]\nEnable damage tracking\nGenerally, leave it on.\nTurn off only when a shader doesn't work"), option: "debug:damage_tracking", enableValue: 2, save: false }),
- HyprlandToggle({ icon: 'destruction', name: getString('Damage blink'), desc: getString("[Hyprland] [Epilepsy warning!]\nShow screen damage flashes"), option: "debug:damage_blink", save: false }),
- ]
- }),
- ]
- })
- }),
- overlays: [Box({
- className: 'sidebar-centermodules-scrollgradient-bottom'
- })]
- });
- return Box({
- ...props,
- className: 'spacing-v-5',
- vertical: true,
- children: [
- mainContent,
- ]
- });
-}
diff --git a/.config/ags/modules/sideright/centermodules/notificationlist.js b/.config/ags/modules/sideright/centermodules/notificationlist.js
deleted file mode 100644
index 48e94a24b..000000000
--- a/.config/ags/modules/sideright/centermodules/notificationlist.js
+++ /dev/null
@@ -1,181 +0,0 @@
-// 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) => {
- let previousLength;
- let clearing = false;
-
- 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: getString('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', getString('Silence'), (self) => {
- Notifications.dnd = !Notifications.dnd;
- self.toggleClassName('notif-listaction-btn-enabled', Notifications.dnd);
- });
- 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', getString('Clear'), () => {
- clearing = true;
- 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} ${getString("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,
- 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) => {
- previousLength = Notifications.notifications.length;
- self.hook(Notifications, (self) => {
- if (Notifications.notifications.length > 0) {
- self.shown = 'list';
- if (!clearing) previousLength = Notifications.notifications.length;
- }
- else {
- Utils.timeout(userOptions.animations.choreographyDelay * previousLength, () => {
- clearing = false;
- if (Notifications.notifications.length == 0) self.shown = 'empty';
- })
- }
- })
- },
- });
- return Box({
- ...props,
- className: 'spacing-v-5',
- vertical: true,
- children: [
- listContents,
- listTitle,
- ]
- });
-}
diff --git a/.config/ags/modules/sideright/centermodules/wifinetworks.js b/.config/ags/modules/sideright/centermodules/wifinetworks.js
deleted file mode 100644
index bac78f881..000000000
--- a/.config/ags/modules/sideright/centermodules/wifinetworks.js
+++ /dev/null
@@ -1,421 +0,0 @@
-import App from 'resource:///com/github/Aylur/ags/app.js';
-import Network from "resource:///com/github/Aylur/ags/service/network.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 { 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 = '';
-let networkAuth = null;
-let networkAuthSSID = null;
-
-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: getString("Selected"),
- }) : null,
- ]
- });
- return Button({
- onClicked: accessPoint.active ? () => { } : () => {
- connectAttempt = accessPoint.ssid;
- networkAuthSSID.label = `${getString('Connecting to')}: ${connectAttempt}`;
-
- // Check if the SSID is stored
- execAsync(['nmcli', '-g', 'NAME', 'connection', 'show'])
- .then((savedConnections) => {
- const savedSSIDs = savedConnections.split('\n');
-
- if (!savedSSIDs.includes(connectAttempt)) { // SSID not saved: show password input
- if (networkAuth) {
- networkAuth.revealChild = true;
- }
- } else { // If SSID is saved, hide password input
- if (networkAuth) {
- networkAuth.revealChild = false;
- }
- // Connect
- execAsync(['nmcli', 'device', 'wifi', 'connect', connectAttempt])
- .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 NetResource = (icon, command) => {
- const resourceLabel = Label({
- className: `txt-smaller txt-subtext`,
- });
- const widget = Button({
- child: Box({
- hpack: 'start',
- className: `spacing-h-4`,
- children: [
- MaterialIcon(icon, 'very-small'),
- resourceLabel,
- ],
- setup: (self) => self.poll(2000, () => execAsync(['bash', '-c', command])
- .then((output) => {
- resourceLabel.label = output;
- }).catch(print))
- ,
- })
- });
- return widget;
-}
-
-const CurrentNetwork = () => {
- const passwordVisible = Variable(false);
- let authLock = false;
- let timeoutId = null;
-
- const bottomSeparator = Box({
- className: 'separator-line',
- });
- const networkName = Box({
- vertical: true,
- hexpand: true,
- children: [
- Label({
- hpack: 'start',
- className: 'txt-smaller txt-subtext',
- label: getString("Current network"),
- }),
- Label({
- hpack: 'start',
- label: Network.wifi?.ssid,
- setup: (self) => self.hook(Network, (self) => {
- if (authLock) return;
- self.label = Network.wifi?.ssid;
- }),
- }),
- ]
- });
- const networkBandwidth = Box({
- vertical: true,
- hexpand: true,
- hpack: 'end',
- className: 'sidebar-wifinetworks-bandwidth',
- children: [
- NetResource('arrow_warm_up', `${App.configDir}/scripts/network_scripts/network_bandwidth.py sent`),
- NetResource('arrow_cool_down', `${App.configDir}/scripts/network_scripts/network_bandwidth.py recv`),
- ]
- });
- // const networkStatus = Box({
- // children: [Label({
- // vpack: 'center',
- // className: 'txt-subtext',
- // setup: (self) => self.hook(Network, (self) => {
- // if (authLock) return;
- // self.label = Network.wifi.state;
- // }),
- // })]
- // });
- networkAuthSSID = Label({
- className: 'margin-left-5',
- hpack: 'start',
- hexpand: true,
- label: '',
- });
- const cancelAuthButton = Button({
- className: 'txt sidebar-wifinetworks-network-button',
- label: getString('Cancel'),
- hpack: 'end',
- onClicked: () => {
- passwordVisible.value = false;
- networkAuth.revealChild = false;
- authFailed.revealChild = false;
- networkAuthSSID.label = '';
- networkName.children[1].label = Network.wifi?.ssid;
- authEntry.text = '';
- },
- setup: setupCursorHover,
- });
- const authHeader = Box({
- vertical: false,
- hpack: 'fill',
- spacing: 10,
- children: [
- networkAuthSSID,
- cancelAuthButton
- ]
- });
- const authVisible = Button({
- vpack: 'center',
- child: MaterialIcon('visibility', 'large'),
- className: 'txt sidebar-wifinetworks-auth-visible',
- onClicked: (self) => {
- passwordVisible.value = !passwordVisible.value;
- },
- setup: (self) => {
- setupCursorHover(self)
- self.hook(passwordVisible, (self) => {
- self.child.label = passwordVisible.value ? 'visibility_off' : 'visibility';
- })
- },
- });
- const authFailed = Revealer({
- revealChild: false,
- child: Label({
- className: 'txt txt-italic txt-subtext',
- label: getString('Authentication failed'),
- }),
- })
- const authEntry = Entry({
- className: 'sidebar-wifinetworks-auth-entry',
- visibility: false,
- hexpand: true,
- onAccept: (self) => {
- authLock = false;
- // Delete SSID connection before attempting to reconnect
- execAsync(['nmcli', 'connection', 'delete', connectAttempt])
- .catch(() => { }); // Ignore error if SSID not found
-
- execAsync(['nmcli', 'device', 'wifi', 'connect', connectAttempt, 'password', self.text])
- .then(() => {
- connectAttempt = ''; // Reset SSID after successful connection
- networkAuth.revealChild = false; // Hide input if successful
- authFailed.revealChild = false; // Hide failed message if successful
- self.text = ''; // Empty input for retry
- passwordVisible.value = false;
- })
- .catch(() => {
- // Connection failed, show password input again
- networkAuth.revealChild = true;
- authFailed.revealChild = true;
- });
- },
- setup: (self) => self.hook(passwordVisible, (self) => {
- self.visibility = passwordVisible.value
- }),
- placeholderText: getString('Enter network password'),
- });
- const authBox = Box({
- className: 'sidebar-wifinetworks-auth-box',
- children: [
- authEntry,
- authVisible,
- ]
- });
- const forgetButton = Button({
- label: getString('Forget'),
- hexpand: true,
- className: 'txt sidebar-wifinetworks-network-button',
- onClicked: () => {
- execAsync(['nmcli', '-t', '-f', 'ACTIVE,NAME', 'connection', 'show'])
- .then(output => {
- const activeSSID = output
- .split('\n')
- .find(line => line.startsWith('yes:'))
- ?.split(':')[1];
-
- if (activeSSID) {
- execAsync(['nmcli', 'connection', 'delete', activeSSID])
- .catch(err => Utils.execAsync(['notify-send',
- "Network",
- `Failed to forget network - Hold to copy\n${err}`,
- '-a', 'ags',
- ]).catch(print));
- }
- })
- .catch();
- },
- setup: setupCursorHover,
- });
- const propertiesButton = Button({
- label: getString('Properties'),
- className: 'txt sidebar-wifinetworks-network-button',
- hexpand: true,
- onClicked: () => {
- Utils.execAsync('nmcli -t -f uuid connection show --active').then(uuid => {
- if (uuid.trim()) {
- Utils.execAsync(`nm-connection-editor --edit ${uuid.trim()}`);
- }
- closeEverything();
- }).catch(err => Utils.execAsync(['notify-send',
- "Network",
- `Failed to get connection UUID - Hold to copy\n${err}`,
- '-a', 'ags',
- ]).catch(print));
- },
- setup: setupCursorHover,
- });
- const networkProp = Revealer({
- transition: 'slide_down',
- transitionDuration: userOptions.animations.durationLarge,
- child: Box({
- className: 'spacing-h-10',
- homogeneous: true,
- children: [
- propertiesButton,
- forgetButton,
- ],
- setup: setupCursorHover,
- }),
- setup: (self) => self.hook(Network, (self) => {
- if (Network.wifi?.ssid === '') self.revealChild = false;
- else self.revealChild = true;
- }),
- });
- networkAuth = Revealer({
- transition: 'slide_down',
- transitionDuration: userOptions.animations.durationLarge,
- child: Box({
- className: 'margin-top-10 spacing-v-5',
- vertical: true,
- children: [
- authHeader,
- authBox,
- authFailed,
- ]
- }),
- setup: (self) => self.hook(Network, (self) => {
- execAsync(['nmcli', '-g', 'NAME', 'connection', 'show'])
- .then((savedConnections) => {
- const savedSSIDs = savedConnections.split('\n');
- if (Network.wifi.state == 'failed' ||
- (Network.wifi.state == 'need_auth' && !savedSSIDs.includes(Network.wifi.ssid))) {
- authLock = true;
- connectAttempt = Network.wifi.ssid;
- self.revealChild = true;
- if (timeoutId) {
- clearTimeout(timeoutId);
- }
- timeoutId = setTimeout(() => {
- authLock = false;
- passwordVisible.value = false;
- self.revealChild = false;
- authFailed.revealChild = false;
- Network.wifi.state = 'activated';
- }, 60000); // 60 seconds timeout
- }
- }
- ).catch(print);
- }),
- });
- const actualContent = Box({
- vertical: true,
- className: 'spacing-v-10',
- children: [
- Box({
- className: 'sidebar-wifinetworks-network',
- vertical: true,
- children: [
- Box({
- className: 'spacing-h-10 margin-bottom-10',
- children: [
- MaterialIcon('language', 'hugerass'),
- networkName,
- networkBandwidth,
- // networkStatus,
- ]
- }),
- networkProp,
- 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 sidebar-centermodules-scrollgradient-bottom-contentmargin',
- 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: getString('More'),
- setup: setupCursorHover,
- })],
- })
- return Box({
- ...props,
- className: 'spacing-v-10',
- vertical: true,
- children: [
- CurrentNetwork(),
- networkList,
- bottomBar,
- ]
- });
-}
\ No newline at end of file
diff --git a/.config/ags/modules/sideright/main.js b/.config/ags/modules/sideright/main.js
deleted file mode 100644
index 883c7f0e6..000000000
--- a/.config/ags/modules/sideright/main.js
+++ /dev/null
@@ -1,18 +0,0 @@
-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: 'top',
- child: Box({
- children: [
- clickCloseRegion({ name: 'sideright', multimonitor: false, fillMonitor: 'horizontal' }),
- SidebarRight(),
- ]
- })
-});
diff --git a/.config/ags/modules/sideright/quicktoggles.js b/.config/ags/modules/sideright/quicktoggles.js
deleted file mode 100644
index 41d6ee634..000000000
--- a/.config/ags/modules/sideright/quicktoggles.js
+++ /dev/null
@@ -1,296 +0,0 @@
-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: getString('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} | ${getString("Right-click to configure")}` || getString('Unknown'));
- });
- },
- ...props,
-});
-
-export const ToggleIconBluetooth = (props = {}) => Widget.Button({
- className: 'txt-small sidebar-iconbutton',
- tooltipText: getString('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: getString('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: getString('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: getString('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 ModuleGameMode = 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: getString('Hyprland Game Mode'),
- onClicked: (button) => {
- Utils.execAsync(`hyprctl -j getoption animations:enabled`)
- .then((output) => {
- const enabled = JSON.parse(output)["int"] === 1;
- if (enabled) {
- execAsync(['bash', '-c', `hyprctl --batch "keyword animations:enabled 0; keyword decoration:shadow:enabled 0; keyword decoration:blur:enabled 0; keyword general:gaps_in 0; keyword general:gaps_out 0; keyword general:border_size 1; keyword decoration:rounding 0; keyword general:allow_tearing 1"`]).catch(print);
- execAsync(['gsettings', 'set', 'org.gnome.desktop.interface', 'enable-animations', 'false'])
- } else {
- execAsync(['bash', '-c', `hyprctl reload`]).catch(print);
- execAsync(['gsettings', 'set', 'org.gnome.desktop.interface', 'enable-animations', 'true'])
- }
- button.toggleClassName('sidebar-button-active', enabled);
- })
- },
- child: MaterialIcon('gamepad', '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: getString('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: getString('Reload Environment config'),
- onClicked: () => {
- execAsync(['bash', '-c', 'hyprctl reload || swaymsg 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: getString('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: getString('Session'),
- onClicked: () => {
- closeEverything();
- Utils.timeout(1, () => openWindowOnAllMonitors('session'));
- },
- child: MaterialIcon('power_settings_new', 'norm'),
- setup: button => {
- setupCursorHover(button);
- }
-})
diff --git a/.config/ags/modules/sideright/sideright.js b/.config/ags/modules/sideright/sideright.js
deleted file mode 100644
index 12a01297f..000000000
--- a/.config/ags/modules/sideright/sideright.js
+++ /dev/null
@@ -1,192 +0,0 @@
-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,
- ModuleGameMode,
- 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 QUICK_TOGGLES = {
- 'wifi': ToggleIconWifi(),
- 'bluetooth': ToggleIconBluetooth(),
- 'rawinput': await ModuleRawInput(),
- 'touchpad': await HyprToggleIcon('touchpad_mouse', 'No touchpad while typing', 'input:touchpad:disable_while_typing', {}),
- 'nightlight': await ModuleNightLight(),
- 'invertcolors': await ModuleInvertColors(),
- 'gamemode': await ModuleGameMode(),
- 'idleinhibitor': ModuleIdleInhibitor(),
- 'cloudflarewarp': await ModuleCloudflareWarp(),
-}
-
-const centerWidgets = [
- {
- name: getString('Notifications'),
- materialIcon: 'notifications',
- contentWidget: ModuleNotificationList,
- },
- {
- name: getString('Audio controls'),
- materialIcon: 'volume_up',
- contentWidget: ModuleAudioControls,
- },
- {
- name: getString('Bluetooth'),
- materialIcon: 'bluetooth',
- contentWidget: ModuleBluetooth,
- },
- {
- name: getString('Wifi networks'),
- materialIcon: 'wifi',
- contentWidget: ModuleWifiNetworks,
- onFocus: () => execAsync('nmcli dev wifi list').catch(print),
- },
- {
- name: getString('Quick 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 = `${getString("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: userOptions.sidebar.quickToggles.order.map(toggle => QUICK_TOGGLES[toggle])
-})
-
-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/.config/ags/modules/sideright/todolist.js b/.config/ags/modules/sideright/todolist.js
deleted file mode 100644
index 39ccc500a..000000000
--- a/.config/ags/modules/sideright/todolist.js
+++ /dev/null
@@ -1,224 +0,0 @@
-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 ? getString('Finished tasks will go here') : getString('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: getString('+ 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: getString('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: [getString('Unfinished'), getString('Done')],
- children: [
- UndoneTodoList(),
- todoItems(true),
- ]
-})
diff --git a/.config/ags/scripts/README.md b/.config/ags/scripts/README.md
deleted file mode 100644
index 6d79a5145..000000000
--- a/.config/ags/scripts/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# 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/.config/ags/scripts/ags/agsconfigurator.py b/.config/ags/scripts/ags/agsconfigurator.py
deleted file mode 100755
index 72e9b7400..000000000
--- a/.config/ags/scripts/ags/agsconfigurator.py
+++ /dev/null
@@ -1,137 +0,0 @@
-#!/usr/bin/env -S\_/bin/sh\_-c\_"source\_\$(eval\_echo\_\$ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate&&exec\_python\_-E\_"\$0"\_"\$@""
-import argparse
-import json
-import sys
-import ast
-import re
-
-def parse_value(value):
- """Parse the value into its appropriate Python type (e.g., bool, int, float, list, dict, or string)."""
- try:
- return json.loads(value)
- except json.JSONDecodeError: # Fallback to string if parsing fails
- return value
-
-def remove_trailing_commas(json_string):
- """Remove trailing commas from JSON-like structures."""
- return re.sub(r',\s*([\}\]])', r'\1', json_string)
-
-def strip_comments_except_leading(lines):
- """
- Removes all `//` and `/* ... */` comments, except for leading `//` comments at the start.
- Ensures `//` inside strings is preserved.
- Returns (preserved_comments, cleaned_json).
- """
- preserved_comments = []
- json_lines = []
- in_block_comment = False
- in_string = False
- escaped = False
-
- for line in lines:
- stripped = line.strip()
-
- # Handle block comments
- if in_block_comment:
- if "*/" in stripped:
- in_block_comment = False
- continue
- if stripped.startswith("/*"):
- in_block_comment = True
- continue
-
- # Preserve leading `//` comments at the very start
- if stripped.startswith("//") and not json_lines:
- preserved_comments.append(line)
- continue
-
- # Process line while tracking if inside a string
- new_line = []
- i = 0
- while i < len(line):
- char = line[i]
-
- if char == '"' or char == "'": # Detect string start
- if not in_string:
- in_string = char
- elif in_string == char and not escaped:
- in_string = False
- elif char == "\\" and in_string: # Handle escape sequences
- escaped = not escaped
- else:
- escaped = False
-
- # Remove inline `//` comments only if not inside a string
- if char == "/" and i + 1 < len(line) and line[i + 1] == "/" and not in_string:
- break # Stop processing the line at `//` (comment start)
-
- new_line.append(char)
- i += 1
-
- cleaned_line = "".join(new_line).rstrip()
- if cleaned_line:
- json_lines.append(cleaned_line + "\n")
-
- return preserved_comments, json_lines
-
-def update_json(file_path, key, value=None, reset=False):
- try:
- with open(file_path, 'r') as file:
- lines = file.readlines()
-
- # Separate leading comments and clean JSON
- preserved_comments, json_lines = strip_comments_except_leading(lines)
- json_string = "".join(json_lines)
- json_string = remove_trailing_commas(json_string)
-
- # Convert the cleaned string into a JSON object
- try:
- json_data = json.loads(json_string)
- except json.JSONDecodeError:
- print(f"Error decoding JSON in file: {file_path}")
- sys.exit(1)
-
- # Navigate through the key (e.g., 'search.enableFeatures.actions')
- keys = key.split('.')
- data = json_data
- for k in keys[:-1]:
- data = data.setdefault(k, {})
-
- # Update or delete the key
- if reset:
- if keys[-1] in data:
- del data[keys[-1]]
- print(f"Successfully removed {key} from {file_path}")
- else:
- print(f"Key {key} not found in {file_path}")
- else:
- data[keys[-1]] = value
- print(f"Successfully updated {key} to {value} in {file_path}")
-
- # Write back only valid JSON (with preserved leading comments)
- with open(file_path, 'w') as file:
- file.writelines(preserved_comments) # Restore leading comments
- json.dump(json_data, file, indent=4)
- file.write("\n") # Ensure a newline at the end
-
- except FileNotFoundError:
- print(f"File not found: {file_path}")
- sys.exit(1)
- except Exception as e:
- print(f"Error: {str(e)}")
- sys.exit(1)
-
-if __name__ == "__main__":
- parser = argparse.ArgumentParser(description="Update or remove a key from a JSON configuration file")
- parser.add_argument('--key', required=True, help="The key to be updated or removed (e.g., 'search.enableFeatures.actions')")
- parser.add_argument('--file', required=True, help="The path to the target JSON file")
- parser.add_argument('--value', help="The new value to assign (e.g., 'true', '42', '[1, 2, 3]')")
- parser.add_argument('--reset', action='store_true', help="If set, the key will be removed from the JSON file")
-
- args = parser.parse_args()
-
- value = None
- if args.value:
- value = parse_value(args.value)
-
- update_json(args.file, args.key, value, reset=args.reset)
diff --git a/.config/ags/scripts/ai/license_show-loaded-ollama-models.txt b/.config/ags/scripts/ai/license_show-loaded-ollama-models.txt
deleted file mode 100644
index 261eeb9e9..000000000
--- a/.config/ags/scripts/ai/license_show-loaded-ollama-models.txt
+++ /dev/null
@@ -1,201 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
diff --git a/.config/ags/scripts/ai/primary-buffer-query.sh b/.config/ags/scripts/ai/primary-buffer-query.sh
deleted file mode 100755
index a4356b4b6..000000000
--- a/.config/ags/scripts/ai/primary-buffer-query.sh
+++ /dev/null
@@ -1,41 +0,0 @@
-#!/usr/bin/env bash
-
-# Default system prompt
-SYSTEM_PROMPT="You are a helpful, quick assistant that provides brief and concise explanation \
-to given content in at most 100 characters. If the given content is not in English, translate \
-it to English. If the content is an English word, provide its meaning. If the content is a name, \
-provide some info about it. For a math expression, provide a simplification, \
-each step on a line following this style: \`2x=11 (subtract 7 from both sides)\`. \
-If you do not know the answer, simply say 'No info available'. \
-Only respond for the appropriate case and use as little text as possible.\
-The content:"
-
-first_loaded_model=$("$(dirname "$0")/show-loaded-ollama-models.sh" -j | jq -r '.[0].model' 2>/dev/null) || first_loaded_model=""
-model=${first_loaded_model:-"llama3.2"}
-
-# Parse command-line arguments
-while [[ "$#" -gt 0 ]]; do
- case $1 in
- --model) model="$2"; shift ;; # Set the model from the flag
- *) echo "Unknown parameter: $1"; exit 1 ;;
- esac
- shift
-done
-
-# Combine the system prompt with the clipboard content
-content=$(wl-paste -p | tr '\n' ' ')
-prompt="$SYSTEM_PROMPT $content"
-
-# Make the API call with the specified or default model
-response=$(curl http://localhost:11434/api/generate -d \
- "{\"model\": \"$model\",\"prompt\": \"$prompt\",\"stream\": false}" \
- | jq -r '.response')
-
-# Check if content is a single line and no longer than 30 characters
-if [[ ${#content} -le 30 && "$content" != *$'\n'* ]]; then
- notify-send --app-name="primary-buffer-query" --expire-time=10000 \
- "$content" "$response"
-else
- notify-send --app-name="primary-buffer-query" --expire-time=10000 \
- "AI Response" "$response"
-fi
diff --git a/.config/ags/scripts/ai/show-loaded-ollama-models.sh b/.config/ags/scripts/ai/show-loaded-ollama-models.sh
deleted file mode 100755
index 8dc88780d..000000000
--- a/.config/ags/scripts/ai/show-loaded-ollama-models.sh
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/bin/bash
-
-# From strikeoncmputrz/LLM_Scripts
-# License: Apache-2.0, can be found in the same folder as this script
-
-# Global Vars
-ollama_url=http://localhost
-port="11434"
-blobs=()
-model_name_paths=()
-
-
-#Parse arguments
-while [ "$#" -gt 0 ]; do
- case $1 in
- -h|--help)
- echo
- echo " Identifies Ollama models running on this operating system by parsing running processes."
- echo
- echo " Usage: $0 [options]"
- echo
- echo " Options:"
- echo " -j, --json_output Prints result as a json object. Other output disabled. (Default: false)"
- echo " -p, --port [port number] Specify Ollama Server port (Default: 11434)"
- echo " -u, --ollama_url [url] Specify Ollama Server URL (Default: http://localhost)"
- echo
- echo " Dependencies: jq"
- exit 0
- ;;
- -j|--json_output)
- json_out=1
- shift 1
- ;;
- -u|--ollama_url)
- ollama_url=$2
- shift 2
- ;;
- -p|--port)
- port=$2
- shift 2
- ;;
- *)
- echo "Unknown option: $1"
- exit 1
- ;;
- esac
-done
-
-compare_running_models_and_modelfiles() {
- json_match=()
- json_output=()
- local matching_models=()
- OLDIFS=$IFS
- for ((i=0; i<${#model_name_paths[@]}; i++)); do # Iterate over the array of modelname,blob-path
- for blob in "${blobs[@]}"; do
- IFS=',', read -ra fields <<< "${model_name_paths[i]}" # Split the string into parts
- if [ "${fields[1]}" == "$blob" ]; then # Check if current 'field' matches a blob
- matching_models+=( '{ "model": "'"${fields[0]}"'", "path": "'"${fields[1]}"'"}') # Add to list of matching models
- fi
- done
- done
-
- if [ -z "$json_out" ]; then
- echo -e "\nModel Found: \n $(echo ${matching_models[*]} | jq '.' | sed s/[{}]//g) \n"
- else
- local json_match="${matching_models[*]}"
- json_output=$(echo $json_match | jq -c -s .)
- echo "$json_output"
- fi
- IFS=$OLDIFS
-}
-
-get_running_model_paths() {
- blobs=$(ps aux | grep -- '--model' | grep -v grep | grep -Po '(?<=--model\s).*' | cut -d ' ' -f1)
- if [ -z "$blobs" ]; then
- echo -e "\n\n Warning: No running Ollama models detected!\n"
- exit 0
- fi
-}
-
-parse_modelfiles() {
- if [ -z "$json_out" ]; then
- echo -e "\nConnecting to $ollama_url:$port\n"
- if [ -z "$(curl -s $ollama_url:$port)" ]; then
- echo -e "Could not connect to Ollama. Check the ollama_url parameter and that the server is running\n"
- exit 1
- fi
- curl -s "$ollama_url:$port"
- fi
- local models=( $(curl -s "$ollama_url:$port/api/tags" | jq -r '.models[].name') )
- for model in "${models[@]}"; do
- local modelfile=$(curl -s "$ollama_url:$port/api/show" -d '{ "name": "'"$model"'", "modelfile": true }' | jq -r '.modelfile')
- model_name_paths+=($model,$(echo "$modelfile" | awk '/^FROM/{print $2}'))
- done
-}
-
-parse_modelfiles
-get_running_model_paths
-compare_running_models_and_modelfiles
diff --git a/.config/ags/scripts/color_generation/applycolor.sh b/.config/ags/scripts/color_generation/applycolor.sh
deleted file mode 100755
index f1579483e..000000000
--- a/.config/ags/scripts/color_generation/applycolor.sh
+++ /dev/null
@@ -1,202 +0,0 @@
-#!/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 template exists
- if [ ! -f "scripts/templates/fuzzel/fuzzel.theme" ]; then
- echo "Template file not found for Fuzzel. Skipping that."
- return
- fi
- # Copy template
- cp "scripts/templates/fuzzel/fuzzel.theme" "$XDG_CONFIG_HOME"/fuzzel/fuzzel.theme
- # Apply colors
- for i in "${!colorlist[@]}"; do
- sed -i "s/{{ ${colorlist[$i]} }}/${colorvalues[$i]#\#}/g" "$XDG_CONFIG_HOME"/fuzzel/fuzzel.theme
- done
-}
-
-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 template 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 template 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_ags_sourceview() {
- # Check if template file exists
- if [ ! -f "scripts/templates/ags/sourceviewtheme.xml" ]; then
- echo "Template file not found for ags sourceview. Skipping that."
- return
- fi
- # Copy template
- mkdir -p "$CACHE_DIR"/user/generated/ags
- cp "scripts/templates/ags/sourceviewtheme.xml" "$CACHE_DIR"/user/generated/ags/sourceviewtheme.xml
- cp "scripts/templates/ags/sourceviewtheme-light.xml" "$CACHE_DIR"/user/generated/ags/sourceviewtheme-light.xml
- # Apply colors
- for i in "${!colorlist[@]}"; do
- sed -i "s/{{ ${colorlist[$i]} }}/#${colorvalues[$i]#\#}/g" "$CACHE_DIR"/user/generated/ags/sourceviewtheme.xml
- sed -i "s/{{ ${colorlist[$i]} }}/#${colorvalues[$i]#\#}/g" "$CACHE_DIR"/user/generated/ags/sourceviewtheme-light.xml
- done
-
- cp "$CACHE_DIR"/user/generated/ags/sourceviewtheme.xml "$XDG_CONFIG_HOME"/ags/assets/themes/sourceviewtheme.xml
- cp "$CACHE_DIR"/user/generated/ags/sourceviewtheme-light.xml "$XDG_CONFIG_HOME"/ags/assets/themes/sourceviewtheme-light.xml
-}
-
-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() {
- # Check if template exists
- if [ ! -f "scripts/templates/gtk/gtk-colors.css" ]; then
- echo "Template file not found for gtk colors. Skipping that."
- return
- fi
- # Copy template
- mkdir -p "$CACHE_DIR"/user/generated/gtk/
- cp "scripts/templates/gtk/gtk-colors.css" "$CACHE_DIR"/user/generated/gtk/gtk-colors.css
- # Apply colors
- for i in "${!colorlist[@]}"; do
- sed -i "s/{{ ${colorlist[$i]} }}/#${colorvalues[$i]#\#}/g" "$CACHE_DIR"/user/generated/gtk/gtk-colors.css
- done
-
- # Apply to both gtk3 and gtk4
- cp "$CACHE_DIR"/user/generated/gtk/gtk-colors.css "$XDG_CONFIG_HOME"/gtk-3.0/gtk.css
- cp "$CACHE_DIR"/user/generated/gtk/gtk-colors.css "$XDG_CONFIG_HOME"/gtk-4.0/gtk.css
-
- # And set the right variant of adw gtk3
- 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() {
- agsv1 run-js "handleStyles(false);"
- agsv1 run-js 'openColorScheme.value = true; Utils.timeout(2000, () => openColorScheme.value = false);'
-}
-
-apply_qt() {
- sh "$CONFIG_DIR/scripts/kvantum/materialQT.sh" # generate kvantum theme
- python "$CONFIG_DIR/scripts/kvantum/changeAdwColors.py" # apply config colors
-}
-
-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_ags_sourceview &
-apply_hyprland &
-apply_hyprlock &
-apply_lightdark &
-apply_gtk &
-apply_qt &
-apply_fuzzel &
-apply_term &
diff --git a/.config/ags/scripts/color_generation/colorgen.sh b/.config/ags/scripts/color_generation/colorgen.sh
deleted file mode 100755
index 3b44dbbc4..000000000
--- a/.config/ags/scripts/color_generation/colorgen.sh
+++ /dev/null
@@ -1,97 +0,0 @@
-#!/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
-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")
-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
- source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate
- python color_generation/generate_colors_material.py --path "$(realpath "$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
- deactivate
- 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
- source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate
- wal -c
- wal -i "$1" -n $lightdark -q
- deactivate
- # 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
- 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
- sed -i "s/}/;\n/g" "$CACHE_DIR"/user/generated/colors_classes.scss
- if [ "$lightdark" = "light" ]; 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/.config/ags/scripts/color_generation/pywal.sh b/.config/ags/scripts/color_generation/pywal.sh
deleted file mode 100755
index ded45eee0..000000000
--- a/.config/ags/scripts/color_generation/pywal.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env bash
-# A wrapper for pywal inside the virtual env
-source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate
-#wal "$@"
-wal $*
-deactivate
diff --git a/.config/ags/scripts/color_generation/pywal_to_material.scss b/.config/ags/scripts/color_generation/pywal_to_material.scss
deleted file mode 100644
index ff049b219..000000000
--- a/.config/ags/scripts/color_generation/pywal_to_material.scss
+++ /dev/null
@@ -1,57 +0,0 @@
-$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/.config/ags/scripts/color_generation/randomwall.sh b/.config/ags/scripts/color_generation/randomwall.sh
deleted file mode 100755
index 69587360f..000000000
--- a/.config/ags/scripts/color_generation/randomwall.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/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/.config/ags/scripts/color_generation/schemes/scheme_morevibrant.py b/.config/ags/scripts/color_generation/schemes/scheme_morevibrant.py
deleted file mode 100644
index 2715021c5..000000000
--- a/.config/ags/scripts/color_generation/schemes/scheme_morevibrant.py
+++ /dev/null
@@ -1,44 +0,0 @@
-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_hct=source_color_hct,
- 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/.config/ags/scripts/color_generation/specials/_material_badapple-l.scss b/.config/ags/scripts/color_generation/specials/_material_badapple-l.scss
deleted file mode 100644
index 12c047b0f..000000000
--- a/.config/ags/scripts/color_generation/specials/_material_badapple-l.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-$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/.config/ags/scripts/color_generation/specials/_material_badapple.scss b/.config/ags/scripts/color_generation/specials/_material_badapple.scss
deleted file mode 100644
index 24dd73ff5..000000000
--- a/.config/ags/scripts/color_generation/specials/_material_badapple.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-$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/.config/ags/scripts/color_generation/switchcolor.sh b/.config/ags/scripts/color_generation/switchcolor.sh
deleted file mode 100755
index 172774b55..000000000
--- a/.config/ags/scripts/color_generation/switchcolor.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/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"
-
-# Generate colors for ags n stuff
-"$CONFIG_DIR"/scripts/color_generation/colorgen.sh "${color}" --apply
diff --git a/.config/ags/scripts/color_generation/switchwall.sh b/.config/ags/scripts/color_generation/switchwall.sh
deleted file mode 100755
index e7440889a..000000000
--- a/.config/ags/scripts/color_generation/switchwall.sh
+++ /dev/null
@@ -1,138 +0,0 @@
-#!/usr/bin/env bash
-
-XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
-XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
-CONFIG_DIR="$XDG_CONFIG_HOME/ags"
-CACHE_DIR="$XDG_CACHE_HOME/ags"
-
-CUSTOM_DIR="$XDG_CONFIG_HOME/hypr/custom"
-RESTORE_SCRIPT_DIR="$CUSTOM_DIR/scripts"
-RESTORE_SCRIPT="$RESTORE_SCRIPT_DIR/__restore_video_wallpaper.sh"
-
-VIDEO_OPTS="no-audio loop hwdec=auto scale=bilinear interpolation=no video-sync=display-resample panscan=1.0 video-scale-x=1.0 video-scale-y=1.0 video-align-x=0.5 video-align-y=0.5"
-
-mkdir -p "$RESTORE_SCRIPT_DIR"
-
-is_video() {
- local extension="${1##*.}"
- [[ "$extension" == "mp4" || "$extension" == "mkv" || "$extension" == "webm" ]] && return 0 || return 1
-}
-
-kill_existing_mpvpaper() {
- # Abort all mpvpapers' instance
- pkill -f -9 mpvpaper || true
-}
-
-create_restore_script() {
- local video_path=$1
-
- cat > "$RESTORE_SCRIPT.tmp" << EOF
-#!/bin/bash
-# Generated by switchwall.sh - Don't modify it by yourself.
-# Time: $(date)
-
-pkill -f -9 mpvpaper
-
-for monitor in \$(hyprctl monitors -j | jq -r '.[] | .name'); do
- mpvpaper -o "$VIDEO_OPTS" "\$monitor" "$video_path" &
- sleep 0.1
-done
-EOF
-
- mv "$RESTORE_SCRIPT.tmp" "$RESTORE_SCRIPT"
- chmod +x "$RESTORE_SCRIPT"
-}
-
-remove_restore() {
- cat > "$RESTORE_SCRIPT.tmp" << EOF
-#!/bin/bash
-# The content of this script will be generated by switchwall.sh - Don't modify it by yourself.
-EOF
-
- mv "$RESTORE_SCRIPT.tmp" "$RESTORE_SCRIPT"
-}
-
-switch() {
- imgpath=$1
- 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))
-
- if [ "$imgpath" == '' ]; then
- echo 'Aborted'
- exit 0
- fi
-
- kill_existing_mpvpaper
-
- if is_video "$imgpath"; then
- missing_deps=()
- if ! command -v mpvpaper &> /dev/null; then
- missing_deps+=("mpvpaper")
- fi
-
- if ! command -v ffmpeg &> /dev/null; then
- missing_deps+=("ffmpeg")
- fi
-
- if [ ${#missing_deps[@]} -gt 0 ]; then
- echo "Missing deps: ${missing_deps[*]}"
- echo "Arch: "
- echo " yay -S ${missing_deps[*]}"
- exit 0
- fi
-
- local video_path=$1
-
- monitors=$(hyprctl monitors -j | jq -r '.[] | .name')
-
- for monitor in $monitors; do
- mpvpaper -o "$VIDEO_OPTS" "$monitor" "$video_path" &
- sleep 0.1
- done
-
- # We take the first frame of video to colorgen and swww
- thumbnail="$CACHE_DIR"/user/generated/mpvpaper_thumbnail.jpg
- ffmpeg -y -i "$imgpath" -vframes 1 "$thumbnail" 2>/dev/null
-
- if [ -f "$thumbnail" ]; then
- # Apply swww wallpaper using the thumbnail
- swww img "$thumbnail" --transition-step 100 --transition-fps 120 \
- --transition-type grow --transition-angle 30 --transition-duration 1 \
- --transition-pos "$cursorposx, $cursorposy_inverted"
- "$CONFIG_DIR"/scripts/color_generation/colorgen.sh "$thumbnail" --apply --smart
-
- create_restore_script "$video_path"
- else
- echo "Cannot create image to colorgen"
- fi
- else
- # agsv1 run-js "wallpaper.set('')"
- # sleep 0.1 && agsv1 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"
-
- "$CONFIG_DIR"/scripts/color_generation/colorgen.sh "$imgpath" --apply --smart
- remove_restore
- fi
-}
-
-if [ "$1" == "--noswitch" ]; then
- if pgrep -f mpvpaper > /dev/null; then
- imgpath=$(ps -eo cmd | grep mpvpaper | grep -v grep | awk '{for(i=NF;i>0;i--) if($i!~/^-/) {print $i; break}}')
- else
- imgpath=$(swww query | awk -F 'image: ' '{print $2}')
- # imgpath=$(agsv1 run-js 'wallpaper.get(0)')
- fi
-elif [[ "$1" ]]; then
- switch "$1"
-else
- # Select and set image (hyprland)
-
- cd "$(xdg-user-dir PICTURES)/Wallpapers" || cd "$(xdg-user-dir PICTURES)" || return 1
- switch "$(yad --width 1200 --height 800 --file --add-preview --large-preview --title='Choose wallpaper')"
-fi
diff --git a/.config/ags/scripts/grimblast.sh b/.config/ags/scripts/grimblast.sh
deleted file mode 100755
index a5602f24d..000000000
--- a/.config/ags/scripts/grimblast.sh
+++ /dev/null
@@ -1,277 +0,0 @@
-#!/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/.config/ags/scripts/hyprland/hyprconfigurator.py b/.config/ags/scripts/hyprland/hyprconfigurator.py
deleted file mode 100755
index 1a9855ebf..000000000
--- a/.config/ags/scripts/hyprland/hyprconfigurator.py
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/usr/bin/env -S\_/bin/sh\_-xc\_"source\_\$(eval\_echo\_\$ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate&&exec\_python\_-E\_"\$0"\_"\$@""
-import argparse
-import re
-import os
-
-def edit_hyprland_config(file_path, key, value, reset):
- try:
- with open(file_path, 'r') as file:
- lines = file.readlines()
- except FileNotFoundError:
- print(f"Error: File '{file_path}' not found.")
- return
-
- key_pattern = re.compile(rf'^\s*{re.escape(key)}\s*=')
- new_lines = []
- found = False
-
- for line in lines:
- if key_pattern.match(line):
- found = True
- if reset:
- continue # Skip this line to remove the key
- line = f"{key} = {value}\n"
- new_lines.append(line)
-
- if not found and not reset:
- new_lines.append(f"{key} = {value}\n")
-
- with open(file_path, 'w') as file:
- file.writelines(new_lines)
-
- if reset:
- print(f"Removed '{key}' from '{file_path}'")
- else:
- print(f"Updated '{file_path}' with {key} = {value}")
-
-if __name__ == "__main__":
- parser = argparse.ArgumentParser(description="Edit a Hyprland config file.")
- parser.add_argument("--file", default="~/.config/hypr/hyprland.conf", help="Path to the Hyprland config file (default: ~/.config/hypr/hyprland.conf).")
- parser.add_argument("--key", required=True, help="Configuration key to modify or remove.")
- parser.add_argument("--value", help="New value for the configuration key (optional).", default=None)
- parser.add_argument("--reset", action="store_true", help="Remove the specified key from the config file.")
- args = parser.parse_args()
-
- file_path = os.path.expanduser(args.file)
-
- if args.reset and args.value:
- print("Error: --reset and --value cannot be used together.")
- else:
- edit_hyprland_config(file_path, args.key, args.value or "", args.reset)
-
\ No newline at end of file
diff --git a/.config/ags/scripts/network_scripts/network_bandwidth.py b/.config/ags/scripts/network_scripts/network_bandwidth.py
deleted file mode 100755
index fa4817a8e..000000000
--- a/.config/ags/scripts/network_scripts/network_bandwidth.py
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/usr/bin/env -S\_/bin/sh\_-xc\_"source\_\$(eval\_echo\_\$ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate&&exec\_python\_-E\_"\$0"\_"\$@""
-
-
-from time import sleep
-import sys
-import psutil
-
-try:
- direction = sys.argv[1]
-except IndexError:
- direction = "recv"
-
-init_bytes = final_bytes = 0
-
-match direction:
- case "recv":
- init_bytes = psutil.net_io_counters().bytes_recv
- sleep(1)
- final_bytes = psutil.net_io_counters().bytes_recv
-
- case "sent":
- init_bytes = psutil.net_io_counters().bytes_sent
- sleep(1)
- final_bytes = psutil.net_io_counters().bytes_sent
-
- case _:
- print(f"wrong direction: {direction}")
- sys.exit()
-
-i = 0
-divider = 1000
-bandwidth = int((final_bytes - init_bytes))
-units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]
-
-while bandwidth >= divider:
- i += 1
- bandwidth = bandwidth / divider
-
-print(f"{bandwidth:.1f}" + units[i] + "/s")
-
diff --git a/.config/ags/scripts/quickscripts/nixos-trim-generations.sh b/.config/ags/scripts/quickscripts/nixos-trim-generations.sh
deleted file mode 100755
index 98b59be95..000000000
--- a/.config/ags/scripts/quickscripts/nixos-trim-generations.sh
+++ /dev/null
@@ -1,243 +0,0 @@
-#!/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/.config/ags/scripts/sway/swayToRelativeWs.sh b/.config/ags/scripts/sway/swayToRelativeWs.sh
deleted file mode 100755
index 81e887b79..000000000
--- a/.config/ags/scripts/sway/swayToRelativeWs.sh
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/usr/bin/env bash
-
-# Check if sway is running
-if ! pgrep -x sway > /dev/null; then
- echo "Sway is not running"
- exit 1
-fi
-
-
-# Get the current workspace number
-current=$(swaymsg -t get_workspaces | jq '.[] | select(.focused==true) | .num')
-
-# Check if a number was passed as an argument
-if [[ "$1" =~ ^[+-]?[0-9]+$ ]]; then
- new_workspace=$((current + $1))
-else
- new_workspace=$((current + 1))
-fi
-
-# Check if the new workspace number is out of bounds
-if [[ $new_workspace -lt 1 ]]; then
- exit 0
-fi
-
-# Switch to the new workspace
-if [[ $2 == 'move' ]]; then
- swaymsg move container to workspace $new_workspace
-else
- swaymsg workspace $new_workspace
-fi
diff --git a/.config/ags/scripts/templates/ags/sourceviewtheme.xml b/.config/ags/scripts/templates/ags/sourceviewtheme.xml
deleted file mode 100644
index e2e5fe9a4..000000000
--- a/.config/ags/scripts/templates/ags/sourceviewtheme.xml
+++ /dev/null
@@ -1,121 +0,0 @@
-
-
-
-
-
-
- Leo Iannacone
- <_description>Based on SublimeText Monokai Extended - Generated with tm2gtksw2
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.config/ags/scripts/templates/fuzzel/fuzzel.theme b/.config/ags/scripts/templates/fuzzel/fuzzel.theme
deleted file mode 100644
index c71fd53a0..000000000
--- a/.config/ags/scripts/templates/fuzzel/fuzzel.theme
+++ /dev/null
@@ -1,8 +0,0 @@
-[colors]
-background={{ $background }}ff
-text={{ $onBackground }}ff
-selection={{ $surfaceVariant }}ff
-selection-text={{ $onSurfaceVariant }}ff
-border={{ $surfaceVariant }}dd
-match={{ $primary }}ff
-selection-match={{ $primary }}ff
diff --git a/.config/ags/scripts/templates/gradience/preset.json b/.config/ags/scripts/templates/gradience/preset.json
deleted file mode 100644
index aeb57489c..000000000
--- a/.config/ags/scripts/templates/gradience/preset.json
+++ /dev/null
@@ -1,144 +0,0 @@
-{
- "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/.config/ags/scripts/templates/gtk/gtk-colors.css b/.config/ags/scripts/templates/gtk/gtk-colors.css
deleted file mode 100644
index b8ed1a4c7..000000000
--- a/.config/ags/scripts/templates/gtk/gtk-colors.css
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
-* GTK Colors
-*/
-
-@define-color accent_color {{ $primary }};
-@define-color accent_fg_color {{ $onPrimary }};
-@define-color accent_bg_color {{ $primary }};
-@define-color window_bg_color {{ $background }};
-@define-color window_fg_color {{ $onBackground }};
-@define-color headerbar_bg_color {{ $surfaceDim }};
-@define-color headerbar_fg_color {{ $onSurface }};
-@define-color popover_bg_color {{ $surfaceDim }};
-@define-color popover_fg_color {{ $onSurface }};
-@define-color view_bg_color {{ $surface }};
-@define-color view_fg_color {{ $onSurface }};
-@define-color card_bg_color {{ $surface }};
-@define-color card_fg_color {{ $onSurface }};
-@define-color sidebar_bg_color @window_bg_color;
-@define-color sidebar_fg_color @window_fg_color;
-@define-color sidebar_border_color @window_bg_color;
-@define-color sidebar_backdrop_color @window_bg_color;
diff --git a/.config/ags/scripts/templates/hypr/hyprland/colors.conf b/.config/ags/scripts/templates/hypr/hyprland/colors.conf
deleted file mode 100644
index 8c0689f31..000000000
--- a/.config/ags/scripts/templates/hypr/hyprland/colors.conf
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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/.config/ags/scripts/templates/terminal/scheme-monochrome.json b/.config/ags/scripts/templates/terminal/scheme-monochrome.json
deleted file mode 100644
index 5807a26e4..000000000
--- a/.config/ags/scripts/templates/terminal/scheme-monochrome.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "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/.config/ags/scss/_bar.scss b/.config/ags/scss/_bar.scss
deleted file mode 100644
index 248f1fbc6..000000000
--- a/.config/ags/scss/_bar.scss
+++ /dev/null
@@ -1,437 +0,0 @@
-// 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;
-}
-
-.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-borderless {
- background-color: transparent;
-}
-
-.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-borderless {
- background-color: transparent;
-}
-
-.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-circprog-borderless {
- background-color: transparent;
-}
-
-.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-circprog-borderless {
- background-color: transparent;
-}
-
-.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-circprog-borderless {
- background-color: transparent;
-}
-
-.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-circprog-borderless {
- background-color: transparent;
-}
-
-.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-borderless {
- background-color: transparent;
-}
-
-.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/.config/ags/scss/_cheatsheet.scss b/.config/ags/scss/_cheatsheet.scss
deleted file mode 100644
index 19c0c9761..000000000
--- a/.config/ags/scss/_cheatsheet.scss
+++ /dev/null
@@ -1,168 +0,0 @@
-.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/.config/ags/scss/_colors.scss b/.config/ags/scss/_colors.scss
deleted file mode 100644
index 3e6942efd..000000000
--- a/.config/ags/scss/_colors.scss
+++ /dev/null
@@ -1,99 +0,0 @@
-$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: mix($surfaceContainerHigh, $onSurface, 96%);
-$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/.config/ags/scss/_common.scss b/.config/ags/scss/_common.scss
deleted file mode 100644
index e12a2031f..000000000
--- a/.config/ags/scss/_common.scss
+++ /dev/null
@@ -1,380 +0,0 @@
-* {
- 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;
-}
-
-.configtoggle-reset {
- @include small-rounding;
- color: $onLayer2;
- background-color: $layer2;
- min-width: 2.045rem;
- min-height: 2.045rem;
-}
-
-.configtoggle-reset:focus,
-.configtoggle-reset:hover {
- background-color: $layer2Hover;
-}
-
-.configtoggle-reset:active {
- background-color: $layer2Active;
-}
-
-
-.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;
- color: $onLayer2;
- background-color: $layer2;
- min-width: 2.045rem;
- min-height: 2.045rem;
- caret-color: $onLayer2;
-
- entry {
- color: $onLayer2;
- margin: 0.477rem 0.614rem;
- }
-
- button {
- @include unsharpen-rounding;
- min-width: 2.045rem;
- min-height: 2.045rem;
- -gtk-outline-radius: $rounding_small;
- }
-
- button.up {
- // Only apply rounding to (+) button
- border-top-right-radius: $rounding_small;
- border-bottom-right-radius: $rounding_small;
- }
-
- // button:focus, // Looks weird after clicking cuz it'll highlight both + and -
- button:hover {
- background-color: $layer2Hover;
- }
-
- button:active {
- background-color: $layer2Active;
- }
-}
-
-.spinbutton-reset {
- @include small-rounding;
- color: $onLayer2;
- background-color: $layer2;
- min-width: 2.045rem;
- min-height: 2.045rem;
-}
-
-.spinbutton-reset:focus,
-.spinbutton-reset:hover {
- background-color: $layer2Hover;
-}
-
-.spinbutton-reset:active {
- background-color: $layer2Active;
-}
-
-textview {
- caret-color: $onBackground;
-}
\ No newline at end of file
diff --git a/.config/ags/scss/_desktopbackground.scss b/.config/ags/scss/_desktopbackground.scss
deleted file mode 100644
index 2dda9a12d..000000000
--- a/.config/ags/scss/_desktopbackground.scss
+++ /dev/null
@@ -1,92 +0,0 @@
-.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/.config/ags/scss/_dock.scss b/.config/ags/scss/_dock.scss
deleted file mode 100644
index 2e087ffe8..000000000
--- a/.config/ags/scss/_dock.scss
+++ /dev/null
@@ -1,42 +0,0 @@
-.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/.config/ags/scss/_lib_classes.scss b/.config/ags/scss/_lib_classes.scss
deleted file mode 100644
index d375ec87d..000000000
--- a/.config/ags/scss/_lib_classes.scss
+++ /dev/null
@@ -1,515 +0,0 @@
-.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;
- caret-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 (2, 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/.config/ags/scss/_lib_mixins.scss b/.config/ags/scss/_lib_mixins.scss
deleted file mode 100644
index 3bae4553c..000000000
--- a/.config/ags/scss/_lib_mixins.scss
+++ /dev/null
@@ -1,187 +0,0 @@
-// Common colors
-$hovercolor: $surfaceContainerHigh;
-$activecolor: $surfaceContainerHighest;
-$rounding_unsharpen: 0.136rem;
-$rounding_verysmall: 0.545rem;
-$rounding_small: 0.818rem;
-$rounding_mediumsmall: 0.955rem;
-$rounding_medium: 1.159rem;
-$rounding_mediumlarge: 1.364rem;
-$rounding_large: 1.705rem;
-
-// Common rules
-@mixin unsharpen-rounding {
- border-radius: $rounding_unsharpen;
-}
-
-@mixin verysmall-rounding {
- border-radius: $rounding_verysmall;
- -gtk-outline-radius: $rounding_verysmall;
-}
-
-@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", "Adwaita Sans", "42dotSans", "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/.config/ags/scss/_music.scss b/.config/ags/scss/_music.scss
deleted file mode 100644
index 7450c5041..000000000
--- a/.config/ags/scss/_music.scss
+++ /dev/null
@@ -1,159 +0,0 @@
-@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%);
-} @else {
- $onSecondaryContainer: mix($onSecondaryContainer, white, 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/.config/ags/scss/_notifications.scss b/.config/ags/scss/_notifications.scss
deleted file mode 100644
index 152ead0d9..000000000
--- a/.config/ags/scss/_notifications.scss
+++ /dev/null
@@ -1,255 +0,0 @@
-@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/.config/ags/scss/_osd.scss b/.config/ags/scss/_osd.scss
deleted file mode 100644
index 44b808120..000000000
--- a/.config/ags/scss/_osd.scss
+++ /dev/null
@@ -1,202 +0,0 @@
-// .osd-window {
- // margin-top: 2.727rem;
-// }
-
-.osd-bg {
- min-width: 8.864rem;
-}
-
-.osd-value {
- @include elevation-border;
- @include elevation2;
- @include full-rounding;
- background-color: $layer0;
- padding: 0.341rem 1.023rem 0.341rem 0.682rem;
- min-width: 9.545rem;
-}
-
-.osd-progress {
- min-height: 0.682rem;
- min-width: 8.182rem;
- padding: 0rem;
- border-radius: 10rem;
- @include fluent_decel;
-
- trough {
- min-height: 0.682rem;
- min-width: 8.182rem;
- border-radius: 10rem;
- background-color: $layer2;
- // border: 0.068rem solid $onSecondaryContainer;
- }
-
- progress {
- @include fluent_decel;
- min-height: 0.409rem;
- min-width: 0.409rem;
- margin: 0rem 0.137rem;
- border-radius: 10rem;
- background-color: $onLayer2;
- }
-}
-
-.osd-label {
- font-size: 1.023rem;
- font-weight: 500;
-}
-
-.osd-value-txt {
- font-size: 1.023rem;
- 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/.config/ags/scss/_osk.scss b/.config/ags/scss/_osk.scss
deleted file mode 100644
index 6cefc4af2..000000000
--- a/.config/ags/scss/_osk.scss
+++ /dev/null
@@ -1,119 +0,0 @@
-$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/.config/ags/scss/_overview.scss b/.config/ags/scss/_overview.scss
deleted file mode 100644
index 1ba52c3ed..000000000
--- a/.config/ags/scss/_overview.scss
+++ /dev/null
@@ -1,139 +0,0 @@
-.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/.config/ags/scss/_session.scss b/.config/ags/scss/_session.scss
deleted file mode 100644
index 163908294..000000000
--- a/.config/ags/scss/_session.scss
+++ /dev/null
@@ -1,40 +0,0 @@
-.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/.config/ags/scss/_sidebars.scss b/.config/ags/scss/_sidebars.scss
deleted file mode 100644
index c12bd44b2..000000000
--- a/.config/ags/scss/_sidebars.scss
+++ /dev/null
@@ -1,1154 +0,0 @@
-$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-arrow {
- @include full-rounding;
- background-color: $layer2;
- min-width: 1.705rem;
- min-height: 1.705rem;
-
- &:hover,
- &:focus {
- background-color: $layer2Hover;
- }
-
- &:active {
- background-color: $layer2Active;
- }
-}
-
-.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-calendar-collapsed-pill {
- @include full-rounding;
- background-color: $layer2;
- min-width: 1.705rem;
- min-height: 1.705rem;
- padding-left: 0.341rem;
- padding-right: 0.341rem;
-
- &:hover,
- &:focus {
- background-color: $layer2Hover;
- }
-
- &:active {
- background-color: $layer2Active;
- }
-}
-
-.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;
- min-width: 1.705rem;
- min-height: 1.705rem;
- background-color: $layer2;
-
- &: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-module-csscalc-valuebox {
- @include small-rounding;
- padding: 0.477rem;
- background-color: $layer2;
- color: $onLayer2;
-}
-
-.sidebar-module-csscalc-valuebox-copybtn {
- @include verysmall-rounding;
- @include element_decel;
- min-width: 1.705rem;
- min-height: 1.705rem;
- background-color: $layer2;
- color: $onLayer2;
-
- &:hover,
- &:focus {
- background-color: $layer2Hover;
- }
-
- &:active {
- background-color: $layer2Active;
- }
-}
-
-.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-txtblock-think {
- margin-left: -0.136rem;
- padding: 0.682rem;
- padding-left: 1.159rem;
-}
-
-.sidebar-chat-thinkblock {
- @include small-rounding;
- background-color: $layer2;
- color: $onLayer3;
-}
-
-.sidebar-chat-thinkblock-icon {
- @include verysmall-rounding;
- background-color: $secondaryContainer;
- color: $onSecondaryContainer;
- min-width: 2.045rem;
- min-height: 2.045rem;
-}
-
-.sidebar-chat-thinkblock-btn-arrow {
- @include full-rounding;
- @include icon-material;
- min-width: 1.705rem;
- min-height: 1.705rem;
- background-color: $layer2;
-
- &:hover,
- &:focus {
- background-color: $layer2Hover;
- }
-
- &:active {
- background-color: $layer2Active;
- }
-}
-
-.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 {
- min-width: 16.705rem;
-}
-
-.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-controlbtn {
- @include small-rounding;
- @include element_decel;
- min-height: 2.386rem;
- min-width: 2.386rem;
- color: $onSurface;
-}
-
-.sidebar-controlbtn:hover,
-.sidebar-controlbtn:focus {
- background-color: $hovercolor;
-}
-
-.sidebar-controlbtn:active {
- background-color: $activecolor;
-}
-
-.sidebar-controlbtn-enabled {
- background-color: $secondaryContainer;
-
- label {
- color: $onSecondaryContainer;
- }
-}
-
-.sidebar-controlbtn-enabled:hover,
-.sidebar-controlbtn-enabled:focus {
- background-color: mix($secondaryContainer, $onSecondaryContainer, 90%);
-}
-
-.sidebar-controlbtn-enabled:active {
- background-color: mix($secondaryContainer, $onSecondaryContainer, 75%);
-}
-
-.sidebar-expandbtn-enabled {
- min-width: 29.113rem;
-}
-
-.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;
- margin-bottom: 0rem;
- 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;
-}
-
-.sidebar-booru-image-drawingarea {
- @include small-rounding;
- min-width: 12.273rem;
- min-height: 12.273rem;
-}
-
-.sidebar-booru-image-actions {
- @include element_decel;
- margin: 0.545rem;
-}
-
-.sidebar-booru-tip-icon {
- min-width: 2.392rem;
- min-height: 2.392rem;
-}
-
-.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-box {
- @include small-rounding;
- background-color: $layer1;
- color: $onLayer1;
-}
-
-.sidebar-wifinetworks-auth-entry {
- @include small-rounding;
- color: $onLayer1;
- padding: 0.682rem;
- caret-color: $onLayer2;
-}
-
-.sidebar-wifinetworks-auth-visible {
- @include element_decel;
- @include small-rounding;
- min-width: 2.045rem;
- min-height: 2.045rem;
- margin: 0.341rem;
-
- &:hover,
- &:focus {
- background-color: $layer3Hover;
- }
-
- &:active {
- background-color: $layer3Active;
- }
-}
-
-.sidebar-wifinetworks-bandwidth {
- min-width: 6.3rem;
- padding-left: 1rem;
-}
-
-.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 {
- @if $transparent ==False {
- background: linear-gradient(to top, $layer1 0%, transparentize($layer1, 1) 1.023rem);
- }
-}
-
-.sidebar-centermodules-scrollgradient-bottom-contentmargin {
- margin-bottom: 1.023rem;
-}
-
-.sidebar-wifinetworks-network-button {
- @include full-rounding;
- @include element_decel;
- min-width: 6.818rem;
- min-height: 2.25rem;
- color: $onLayer3;
- background-color: $layer3;
-
- &:hover,
- &:focus {
- background-color: $layer3Hover;
- }
-
- &:active {
- background-color: $layer3Active;
- }
-}
\ No newline at end of file
diff --git a/.config/ags/scss/_wal.scss b/.config/ags/scss/_wal.scss
deleted file mode 100644
index 50ec8e5ab..000000000
--- a/.config/ags/scss/_wal.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-// 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/.config/ags/scss/fallback/_material.scss b/.config/ags/scss/fallback/_material.scss
deleted file mode 100644
index 7f6294abd..000000000
--- a/.config/ags/scss/fallback/_material.scss
+++ /dev/null
@@ -1,76 +0,0 @@
-$darkmode: True;
-$transparent: False;
-$primary_paletteKeyColor: #02DCFF;
-$secondary_paletteKeyColor: #597B8F;
-$tertiary_paletteKeyColor: #557AA1;
-$neutral_paletteKeyColor: #6D797D;
-$neutral_variant_paletteKeyColor: #6A7A7F;
-$background: #091518;
-$onBackground: #D8E5E9;
-$surface: #091518;
-$surfaceDim: #091518;
-$surfaceBright: #2F3B3F;
-$surfaceContainerLowest: #051013;
-$surfaceContainerLow: #121D21;
-$surfaceContainer: #162125;
-$surfaceContainerHigh: #202C2F;
-$surfaceContainerHighest: #2B373A;
-$onSurface: #D8E5E9;
-$surfaceVariant: #3A494E;
-$onSurfaceVariant: #B9C9CE;
-$inverseSurface: #D8E5E9;
-$inverseOnSurface: #273236;
-$outline: #839398;
-$outlineVariant: #3A494E;
-$shadow: #000000;
-$scrim: #000000;
-$surfaceTint: #00D9FC;
-$primary: #00D9FC;
-$onPrimary: #003640;
-$primaryContainer: #004E5C;
-$onPrimaryContainer: #AAEDFF;
-$inversePrimary: #006879;
-$secondary: #A8CBE2;
-$onSecondary: #0D3446;
-$secondaryContainer: #2A4D60;
-$onSecondaryContainer: #C7E9FF;
-$tertiary: #A4CAF5;
-$onTertiary: #003256;
-$tertiaryContainer: #6F94BC;
-$onTertiaryContainer: #000000;
-$error: #FFB4AB;
-$onError: #690005;
-$errorContainer: #93000A;
-$onErrorContainer: #FFDAD6;
-$primaryFixed: #AAEDFF;
-$primaryFixedDim: #00D9FC;
-$onPrimaryFixed: #001F26;
-$onPrimaryFixedVariant: #004E5C;
-$secondaryFixed: #C4E7FF;
-$secondaryFixedDim: #A8CBE2;
-$onSecondaryFixed: #001E2C;
-$onSecondaryFixedVariant: #284B5D;
-$tertiaryFixed: #D0E4FF;
-$tertiaryFixedDim: #A4CAF5;
-$onTertiaryFixed: #001D34;
-$onTertiaryFixedVariant: #21496E;
-$success: #B5CCBA;
-$onSuccess: #213528;
-$successContainer: #374B3E;
-$onSuccessContainer: #D1E9D6;
-$term0: #0E1C21;
-$term1: #8383FF;
-$term2: #63DEDB;
-$term3: #75FCDD;
-$term4: #78B4C1;
-$term5: #7AAEEA;
-$term6: #81D7DE;
-$term7: #CCDBD5;
-$term8: #B1BCB5;
-$term9: #BCB9FF;
-$term10: #F4FFFE;
-$term11: #FFFFFF;
-$term12: #BEE3E8;
-$term13: #C8DAFF;
-$term14: #EAFEFF;
-$term15: #B0ECFC;
diff --git a/.config/ags/scss/main.scss b/.config/ags/scss/main.scss
deleted file mode 100644
index 57938e81a..000000000
--- a/.config/ags/scss/main.scss
+++ /dev/null
@@ -1,42 +0,0 @@
-// 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/.config/ags/services/booru.js b/.config/ags/services/booru.js
deleted file mode 100644
index b8dd98dce..000000000
--- a/.config/ags/services/booru.js
+++ /dev/null
@@ -1,156 +0,0 @@
-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.replace('rating:safe', '')}${(!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/.config/ags/services/brightness.js b/.config/ags/services/brightness.js
deleted file mode 100644
index 1800d162d..000000000
--- a/.config/ags/services/brightness.js
+++ /dev/null
@@ -1,170 +0,0 @@
-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 -b ${this._busNum} 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 -b ${this._busNum} setvcp 10 ${Math.round(percent * 100)}`;
- }
-}
-
-async function listDdcMonitorsSnBus() {
- let ddcSnBus = {};
- try {
- // Its' better not to use --brief. This way if a serial number is not
- // found we can still use the binary serial number as an alternative
- const out = await Utils.execAsync('ddcutil detect');
- const displays = out.split('\n\n');
- displays.forEach(display => {
- const reg = /[Dd]isplay/;
- if (!reg.test(display)) {
- return;
- }
- const lines = display.split('\n');
- let sn, busNum;
- let unresponsive = false;
- for (let line of lines) {
- line = line.trim()
-
- // Sometimes ddcutils will report a DP monitor twice, one of the
- // two copies of the monitor will "not support DDC/CI". Just ignore it
- // See https://www.ddcutil.com/faq/#duplicate_displayport
- if (line.includes('unresponsive')) {
- unresponsive = true;
- }
- if (line.startsWith('Serial')) {
- sn = line.split(':')[1].trim();
- // Sometimes sn can be empty. In this cases let's relay on binary sn
- } else if (line.startsWith('Binary') && !sn) {
- // Make the serial number upper case except for the leading '0x' since Hyprland
- // seems to use upper case for the rest of the string and ddcutil uses
- // lower case for all the binary sn
- sn = '0x'+line.split('(')[1].slice(2,-1).toUpperCase();
- } else if (line.startsWith('I2C bus:')) {
- busNum = line.split('/dev/i2c-')[1];
- }
- }
- if (sn && busNum && !unresponsive){
- ddcSnBus[sn] = busNum;
- }
- });
- } catch (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/.config/ags/services/darkmode.js b/.config/ags/services/darkmode.js
deleted file mode 100644
index b0f54be3c..000000000
--- a/.config/ags/services/darkmode.js
+++ /dev/null
@@ -1,39 +0,0 @@
-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/.config/ags/services/gemini.js b/.config/ags/services/gemini.js
deleted file mode 100644
index 38ae4e744..000000000
--- a/.config/ags/services/gemini.js
+++ /dev/null
@@ -1,335 +0,0 @@
-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 canceled\"" }], },
- { 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 canceled\"\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 canceled\"" }], },
- { role: "user", parts: [{ text: "raise volume by 5%" }], },
- { role: "model", parts: [{ text: "## Volume +5\n```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-2.0-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: '' }];
- _lastContentLength = 0;
- _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 }];
- if (content.length - this._lastContentLength >= userOptions.ai.charsEachUpdate) {
- this.notify('content')
- this.emit('changed')
- this._lastContentLength = content.length;
- }
- }
-
- 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', '', 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,
- },
- "tools": [
- {
- "google_search": {}
- }
- ]
- };
- 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/v1beta/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();
diff --git a/.config/ags/services/gpt.js b/.config/ags/services/gpt.js
deleted file mode 100644
index 9279af0b1..000000000
--- a/.config/ags/services/gpt.js
+++ /dev/null
@@ -1,337 +0,0 @@
-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';
-
-function guessModelLogo(model) {
- if (model.includes("llama")) return "ollama-symbolic";
- if (model.includes("gemma")) return "google-gemini-symbolic";
- if (model.includes("deepseek")) return "deepseek-symbolic";
- if (/^phi\d*:/i.test(model)) return "microsoft-symbolic";
- return "ollama-symbolic";
-}
-
-function guessModelName(model) {
- const replaced = model.replace(/-/g, ' ').replace(/:/g, ' ');
- const words = replaced.split(' ');
- words[words.length - 1] = words[words.length - 1].replace(/(\d+)b$/, (_, num) => `${num}B`)
- words[words.length - 1] = `[${words[words.length - 1]}]`; // Surround the last word with square brackets
- const result = words.join(' ');
- return result.charAt(0).toUpperCase() + result.slice(1); // Capitalize the first letter
-}
-
-const PROVIDERS = Object.assign({
- "ollama_llama_3_2": {
- "name": "Ollama - Llama 3.2",
- "logo_name": "ollama-symbolic",
- "description": getString('Ollama - Llama-3.2'),
- "base_url": 'http://localhost:11434/v1/chat/completions',
- "key_get_url": "",
- "requires_key": false,
- "key_file": "ollama_key.txt",
- "model": "llama3.2",
- },
- "openrouter": {
- "name": "OpenRouter (Llama-3-70B)",
- "logo_name": "openrouter-symbolic",
- "description": getString('A unified interface for LLMs'),
- "base_url": "https://openrouter.ai/api/v1/chat/completions",
- "key_get_url": "https://openrouter.ai/keys",
- "requires_key": true,
- "key_file": "openrouter_key.txt",
- "model": "meta-llama/llama-3-70b-instruct",
- },
-}, userOptions.ai.extraGptModels)
-
-const installedOllamaModels = JSON.parse(
- Utils.exec(`${App.configDir}/scripts/ai/show-installed-ollama-models.sh`))
- || [];
-installedOllamaModels.forEach(model => {
- const providerKey = `ollama_${model}`; // Generate a unique key for each model
- PROVIDERS[providerKey] = {
- name: `Ollama - ${guessModelName(model)}`,
- logo_name: guessModelLogo(model),
- description: `Ollama model: ${model}`,
- base_url: 'http://localhost:11434/v1/chat/completions',
- key_get_url: "",
- requires_key: false,
- key_file: "ollama_key.txt",
- model: `${model}`
- };
-});
-
-// Custom prompt
-const initMessages =
- [
- { role: "user", content: getString("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 canceled\"", },
- { role: "assistant", content: "## Grammar correction\nErrors:\n\"He rushed to where the event was supposed to be __hold____,__ he didn't know it got canceled\"\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 canceled\"", },
- { role: "user", content: "raise volume by 5%", },
- { role: "assistant", content: "## Volume +5\n```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 = '';
- _hasReasoningContent = false;
- _parsedReasoningContent = false;
- _lastContentLength = 0;
- _thinking;
- _done = false;
-
- constructor(role, content, thinking = true, done = false) {
- super();
- this._role = role;
- this._hasReasoningContent = false;
- this._parsedReasoningContent = false;
- 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 hasReasoningContent() { return this._hasReasoningContent }
- set hasReasoningContent(value) {
- this._hasReasoningContent = value;
- this.emit('changed')
- }
-
- get parsedReasoningContent() { return this._parsedReasoningContent }
- set parsedReasoningContent(value) {
- this._parsedReasoningContent = value;
- this.emit('changed')
- }
-
- get content() { return this._content }
- set content(content) {
- this._content = content;
- if (this._content.length - this._lastContentLength >= userOptions.ai.charsEachUpdate) {
- this.notify('content')
- this.emit('changed')
- this._lastContentLength = this._content.length;
- }
- }
-
- 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 (delta == null) return;
- 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 = PROVIDERS[userOptions.ai.defaultGPTProvider] ? userOptions.ai.defaultGPTProvider : Object.keys(PROVIDERS)[0];
- _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 != '') {
-
- // Ignore SSE comments (lines starting with ":")
- if (line.startsWith(':')) {
- this.readResponse(stream, aiResponse);
- return;
- }
-
- let data = line.substr(6);
- if (data == '[DONE]') return;
- try {
- const result = JSON.parse(data);
- if (result.choices[0].finish_reason === 'stop') {
- // If the stop payload has content, add it to the response
- if (result.choices[0].delta.content) {
- aiResponse.addDelta(result.choices[0].delta.content);
- }
- aiResponse.done = true;
- return;
- }
-
- // aiResponse.addDelta(result.choices[0].delta.content);
- if (!result.choices[0].delta.content && result.choices[0].delta.reasoning_content) {
- if (!aiResponse.hasReasoningContent) {
- aiResponse.hasReasoningContent = true;
- aiResponse.addDelta(`\n${result.choices[0].delta.reasoning_content}`);
- }
- else {
- aiResponse.addDelta(`${result.choices[0].delta.reasoning_content}`);
- }
- }
- else {
- if (aiResponse.hasReasoningContent) {
- aiResponse.parsedReasoningContent = true;
- aiResponse.addDelta(`\n \n`);
- }
- aiResponse.addDelta(result.choices[0].delta.content);
- }
- }
- 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,
- "stream": true,
- "keep_alive": userOptions.ai.keepAlive,
- };
- // console.log(body);
- 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();
\ No newline at end of file
diff --git a/.config/ags/services/indicator.js b/.config/ags/services/indicator.js
deleted file mode 100644
index dfe141ee1..000000000
--- a/.config/ags/services/indicator.js
+++ /dev/null
@@ -1,38 +0,0 @@
-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/.config/ags/services/messages.js b/.config/ags/services/messages.js
deleted file mode 100644
index b2d3b05f3..000000000
--- a/.config/ags/services/messages.js
+++ /dev/null
@@ -1,62 +0,0 @@
-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? For 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.execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchwall.sh '${App.configDir}/assets/images/default_wallpaper.png'`]).catch(print);
- 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,for ((i=0; i<$(hyprctl monitors -j | jq length); i++)); do ags -t "cheatsheet""$i"; done']).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);
- }
-}
diff --git a/.config/ags/services/sway.js b/.config/ags/services/sway.js
deleted file mode 100644
index 388372e9c..000000000
--- a/.config/ags/services/sway.js
+++ /dev/null
@@ -1,400 +0,0 @@
-import GLib from 'gi://GLib';
-import Gio from 'gi://Gio';
-import Service from "resource:///com/github/Aylur/ags/service.js";
-
-const SIS = GLib.getenv('SWAYSOCK');
-
-export const PAYLOAD_TYPE = {
- MESSAGE_RUN_COMMAND: 0,
- MESSAGE_GET_WORKSPACES: 1,
- MESSAGE_SUBSCRIBE: 2,
- MESSAGE_GET_OUTPUTS: 3,
- MESSAGE_GET_TREE: 4,
- MESSAGE_GET_MARKS: 5,
- MESSAGE_GET_BAR_CONFIG: 6,
- MESSAGE_GET_VERSION: 7,
- MESSAGE_GET_BINDING_NODES: 8,
- MESSAGE_GET_CONFIG: 9,
- MESSAGE_SEND_TICK: 10,
- MESSAGE_SYNC: 11,
- MESSAGE_GET_BINDING_STATE: 12,
- MESSAGE_GET_INPUTS: 100,
- MESSAGE_GET_SEATS: 101,
- EVENT_WORKSPACE: 0x80000000,
- EVENT_MODE: 0x80000002,
- EVENT_WINDOW: 0x80000003,
- EVENT_BARCONFIG_UPDATE: 0x80000004,
- EVENT_BINDING: 0x80000005,
- EVENT_SHUTDOWN: 0x80000006,
- EVENT_TICK: 0x80000007,
- EVENT_BAR_STATE_UPDATE: 0x80000014,
- EVENT_INPUT: 0x80000015,
-}
-
-const Client_Event = {
- change: undefined,
- container: undefined,
-}
-
-const Workspace_Event = {
- change: undefined,
- current: undefined,
- old: undefined,
-}
-
-const Geometry = {
- x: undefined,
- y: undefined,
- width: undefined,
- height: undefined,
-}
-
-//NOTE: not all properties are listed here
-export const Node = {
- id: undefined,
- name: undefined,
- type: undefined,
- border: undefined,
- current_border_width: undefined,
- layout: undefined,
- orientation: undefined,
- percent: undefined,
- rect: undefined,
- window_rect: undefined,
- deco_rect: undefined,
- geometry: undefined,
- urgent: undefined,
- sticky: undefined,
- marks: undefined,
- focused: undefined,
- active: undefined,
- focus: undefined,
- nodes: undefined,
- floating_nodes: undefined,
- representation: undefined,
- fullscreen_mode: undefined,
- app_id: undefined,
- pid: undefined,
- visible: undefined,
- shell: undefined,
- output: undefined,
- inhibit_idle: undefined,
- idle_inhibitors: {
- application: undefined,
- user: undefined,
- },
- window: undefined,
- window_properties: {
- title: undefined,
- class: undefined,
- instance: undefined,
- window_role: undefined,
- window_type: undefined,
- transient_for: undefined,
- }
-}
-
-export class SwayActiveClient extends Service {
- static {
- Service.register(this, {}, {
- 'id': ['int'],
- 'name': ['string'],
- 'class': ['string'],
- });
- }
-
- _id = 0;
- _name = '';
- _class = '';
-
- get id() { return this._id; }
- get name() { return this._name; }
- get class() { return this._class; }
-
- updateProperty(prop, value) {
- if (!['id', 'name', 'class'].includes(prop)) return;
- super.updateProperty(prop, value);
- this.emit('changed');
- }
-}
-
-export class SwayActiveID extends Service {
- static {
- Service.register(this, {}, {
- 'id': ['int'],
- 'name': ['string'],
- });
- }
-
- _id = 0;
- _name = '';
-
- get id() { return this._id; }
- get name() { return this._name; }
-
- update(id, name) {
- super.updateProperty('id', id);
- super.updateProperty('name', name);
- this.emit('changed');
- }
-}
-
-export class SwayActives extends Service {
- static {
- Service.register(this, {}, {
- 'client': ['jsobject'],
- 'monitor': ['jsobject'],
- 'workspace': ['jsobject'],
- });
- }
-
- _client = new SwayActiveClient;
- _monitor = new SwayActiveID;
- _workspace = new SwayActiveID;
-
- constructor() {
- super();
-
- (['client', 'workspace', 'monitor']).forEach(obj => {
- this[`_${obj}`].connect('changed', () => {
- this.notify(obj);
- this.emit('changed');
- });
- });
- }
-
- get client() { return this._client; }
- get monitor() { return this._monitor; }
- get workspace() { return this._workspace; }
-}
-
-export class Sway extends Service {
- static {
- Service.register(this, {}, {
- 'active': ['jsobject'],
- 'monitors': ['jsobject'],
- 'workspaces': ['jsobject'],
- 'clients': ['jsobject'],
- });
- }
-
- _decoder = new TextDecoder();
- _encoder = new TextEncoder();
- _socket;
-
- _active;
- _monitors;
- _workspaces;
- _clients;
-
- get active() { return this._active; }
- get monitors() { return Array.from(this._monitors.values()); }
- get workspaces() { return Array.from(this._workspaces.values()); }
- get clients() { return Array.from(this._clients.values()); }
-
- getMonitor(id) { return this._monitors.get(id); }
- getWorkspace(name) { return this._workspaces.get(name); }
- getClient(id) { return this._clients.get(id); }
-
- msg(payload) { this._send(PAYLOAD_TYPE.MESSAGE_RUN_COMMAND, payload); }
-
- constructor() {
- if (!SIS)
- console.error('Sway is not running');
- super();
-
- this._active = new SwayActives();
- this._monitors = new Map();
- this._workspaces = new Map();
- this._clients = new Map();
-
- this._socket = new Gio.SocketClient().connect(new Gio.UnixSocketAddress({
- path: `${SIS}`,
- }), null);
-
- this._watchSocket(this._socket.get_input_stream());
- this._send(PAYLOAD_TYPE.MESSAGE_GET_TREE, '');
- this._send(PAYLOAD_TYPE.MESSAGE_SUBSCRIBE, JSON.stringify(['window', 'workspace']));
-
- this._active.connect('changed', () => this.emit('changed'));
- ['monitor', 'workspace', 'client'].forEach(active =>
- this._active.connect(`notify::${active}`, () => this.notify('active')));
- }
-
- _send(payloadType, payload) {
- const pb = this._encoder.encode(payload);
- const type = new Uint32Array([payloadType]);
- const pl = new Uint32Array([pb.length]);
- const magic_string = this._encoder.encode('i3-ipc');
- const data = new Uint8Array([
- ...magic_string,
- ...(new Uint8Array(pl.buffer)),
- ...(new Uint8Array(type.buffer)),
- ...pb]);
- this._socket.get_output_stream().write(data, null);
- }
-
- _watchSocket(stream) {
- stream.read_bytes_async(14, GLib.PRIORITY_DEFAULT, null, (_, resultHeader) => {
- const data = stream.read_bytes_finish(resultHeader).get_data();
- if (!data)
- return;
- const payloadLength = new Uint32Array(data.slice(6, 10).buffer)[0];
- const payloadType = new Uint32Array(data.slice(10, 14).buffer)[0];
- stream.read_bytes_async(
- payloadLength,
- GLib.PRIORITY_DEFAULT,
- null,
- (_, resultPayload) => {
- const data = stream.read_bytes_finish(resultPayload).get_data();
- if (!data)
- return;
- this._onEvent(payloadType, JSON.parse(this._decoder.decode(data)));
- this._watchSocket(stream);
- });
- });
- }
-
- async _onEvent(event_type, event) {
- if (!event)
- return;
- try {
- switch (event_type) {
- case PAYLOAD_TYPE.EVENT_WORKSPACE:
- this._handleWorkspaceEvent(event);
- break;
- case PAYLOAD_TYPE.EVENT_WINDOW:
- this._handleWindowEvent(event);
- break;
- case PAYLOAD_TYPE.MESSAGE_GET_TREE:
- this._handleTreeMessage(event);
- break;
- default:
- break;
- }
- } catch (error) {
- logError(error);
- }
- this.emit('changed');
- }
-
- _handleWorkspaceEvent(workspaceEvent) {
- const workspace = workspaceEvent.current;
- switch (workspaceEvent.change) {
- case 'init':
- this._workspaces.set(workspace.name, workspace);
- break;
- case 'empty':
- this._workspaces.delete(workspace.name);
- break;
- case 'focus':
- this._active.workspace.update(workspace.id, workspace.name);
- this._active.monitor.update(1, workspace.output);
-
- this._workspaces.set(workspace.name, workspace);
- this._workspaces.set(workspaceEvent.old.name, workspaceEvent.old);
- break;
- case 'rename':
- if (this._active.workspace.id === workspace.id)
- this._active.workspace.updateProperty('name', workspace.name);
- this._workspaces.set(workspace.name, workspace);
- break;
- case 'reload':
- break;
- case 'move':
- case 'urgent':
- default:
- this._workspaces.set(workspace.name, workspace);
- }
- this.notify('workspaces');
- }
-
- _handleWindowEvent(clientEvent) {
- const client = clientEvent.container;
- const id = client.id;
- switch (clientEvent.change) {
- case 'new':
- case 'close':
- case 'floating':
- case 'move':
- // Refresh tree since client events don't contain the relevant information
- // to be able to modify `workspace.nodes` or `workspace.floating_nodes`.
- // There has to be a better way than this though :/
- this._send(PAYLOAD_TYPE.MESSAGE_GET_TREE, '');
- break;
- case 'focus':
- if (this._active.client.id === id)
- return;
- // eslint-disable-next-line no-case-declarations
- const current_active = this._clients.get(this._active.client.id);
- if (current_active)
- current_active.focused = false;
- this._active.client.updateProperty('id', id);
- this._active.client.updateProperty('name', client.name);
- this._active.client.updateProperty('class', client.shell === 'xwayland'
- ? client.window_properties?.class || ''
- : client.app_id,
- );
- break;
- case 'title':
- if (client.focused)
- this._active.client.updateProperty('name', client.name);
- this._clients.set(id, client);
- this.notify('clients');
- break;
- case 'fullscreen_mode':
- case 'urgent':
- case 'mark':
- default:
- this._clients.set(id, client);
- this.notify('clients');
- }
- }
-
- _handleTreeMessage(node) {
- switch (node.type) {
- case 'root':
- this._workspaces.clear();
- this._clients.clear();
- this._monitors.clear();
- node.nodes.map(n => this._handleTreeMessage(n));
- break;
- case 'output':
- this._monitors.set(node.id, node);
- if (node.active)
- this._active.monitor.update(node.id, node.name);
- node.nodes.map(n => this._handleTreeMessage(n));
- this.notify('monitors');
- break;
- case 'workspace':
- this._workspaces.set(node.name, node);
- // I think I'm missing something. There has to be a better way.
- // eslint-disable-next-line no-case-declarations
- const hasFocusedChild =
- (n) => n.nodes.some(c => c.focused || hasFocusedChild(c));
- if (node.focused || hasFocusedChild(node))
- this._active.workspace.update(node.id, node.name);
-
- node.nodes.map(n => this._handleTreeMessage(n));
- this.notify('workspaces');
- break;
- case 'con':
- case 'floating_con':
- this._clients.set(node.id, node);
- if (node.focused) {
- this._active.client.updateProperty('id', node.id);
- this._active.client.updateProperty('name', node.name);
- this._active.client.updateProperty('class', node.shell === 'xwayland'
- ? node.window_properties?.class || ''
- : node.app_id,
- );
- }
- node.nodes.map(n => this._handleTreeMessage(n));
- this.notify('clients');
- break;
- }
- }
-}
-
-export const sway = new Sway;
-export default sway;
\ No newline at end of file
diff --git a/.config/ags/services/todo.js b/.config/ags/services/todo.js
deleted file mode 100644
index d5384f3be..000000000
--- a/.config/ags/services/todo.js
+++ /dev/null
@@ -1,83 +0,0 @@
-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/.config/ags/services/waifus.js b/.config/ags/services/waifus.js
deleted file mode 100644
index 8512ed59f..000000000
--- a/.config/ags/services/waifus.js
+++ /dev/null
@@ -1,150 +0,0 @@
-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/.config/ags/services/wallpaper.js b/.config/ags/services/wallpaper.js
deleted file mode 100644
index 9fa2b83ff..000000000
--- a/.config/ags/services/wallpaper.js
+++ /dev/null
@@ -1,70 +0,0 @@
-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/.config/ags/user_options.jsonc b/.config/ags/user_options.jsonc
deleted file mode 100644
index 4c44fe01c..000000000
--- a/.config/ags/user_options.jsonc
+++ /dev/null
@@ -1,19 +0,0 @@
-// Options in this file override the defaults in:
-// ~/.config/ags/modules/.configuration/default_options.jsonc
-//
-// vscode: ctrl+click this: file://./modules/.configuration/default_options.jsonc
-// vim: `:vsp` to split window, move cursor to the path, press `gf`.
-// `Ctrl-w` twice to switch between the files
-//
-// Limitations of this file:
-// * Only line comments (//) are allowed
-// * Comments are not allowed in or below the actual content
-// (will be nuked with updates from the UI)
-//
-//
-// Example: Put this to show 8 (instead of 10) workspaces on the bar
-// "workspaces": {"shown": 8 }
-//
-{
-
-}
diff --git a/.config/ags/variables.js b/.config/ags/variables.js
deleted file mode 100644
index 347d006d0..000000000
--- a/.config/ags/variables.js
+++ /dev/null
@@ -1,84 +0,0 @@
-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;
-import { init as i18n_init, getString } from './i18n/i18n.js'
-//init i18n, Load language file
-i18n_init()
-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;
-globalThis['getString'] = getString
-// 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/.config/fcitx5/conf/classicui.conf b/.config/fcitx5/conf/classicui.conf
new file mode 100644
index 000000000..cfbcc6150
--- /dev/null
+++ b/.config/fcitx5/conf/classicui.conf
@@ -0,0 +1,31 @@
+# Vertical Candidate List
+Vertical Candidate List=False
+# Use mouse wheel to go to prev or next page
+WheelForPaging=True
+# Font
+Font="Rubik 11"
+# Menu Font
+MenuFont="Rubik 11"
+# Tray Font
+TrayFont="Rubik 11"
+# Prefer Text Icon
+PreferTextIcon=False
+# Show Layout Name In Icon
+ShowLayoutNameInIcon=True
+# Use input method language to display text
+UseInputMethodLanguageToDisplayText=True
+# Theme
+Theme=plasma
+# Dark Theme
+DarkTheme=plasma
+# Follow system light/dark color scheme
+UseDarkTheme=False
+# Follow system accent color if it is supported by theme and desktop
+UseAccentColor=True
+# Use Per Screen DPI on X11
+PerScreenDPI=False
+# Force font DPI on Wayland
+ForceWaylandDPI=0
+# Enable fractional scale under Wayland
+EnableFractionalScale=True
+
diff --git a/.config/fish/config.fish b/.config/fish/config.fish
index 2eab1b489..4a86ee4f8 100755
--- a/.config/fish/config.fish
+++ b/.config/fish/config.fish
@@ -13,11 +13,14 @@ if status is-interactive
end
starship init fish | source
-if test -f ~/.cache/ags/user/generated/terminal/sequences.txt
- cat ~/.cache/ags/user/generated/terminal/sequences.txt
+if test -f ~/.local/state/quickshell/user/generated/terminal/sequences.txt
+ cat ~/.local/state/quickshell/user/generated/terminal/sequences.txt
end
-alias pamcan=pacman
+alias pamcan pacman
+alias ls 'eza --icons'
+alias clear "printf '\033[2J\033[3J\033[1;1H'"
+
# function fish_prompt
# set_color cyan; echo (pwd)
diff --git a/.config/foot/foot.ini b/.config/foot/foot.ini
index 345d8ae62..8211dcdf4 100644
--- a/.config/foot/foot.ini
+++ b/.config/foot/foot.ini
@@ -3,7 +3,7 @@ term=xterm-256color
title=foot
-font=SpaceMono Nerd Font:size=11
+font=JetBrainsMono Nerd Font:size=11
letter-spacing=0
dpi-aware=no
diff --git a/.config/fuzzel/fuzzel.ini b/.config/fuzzel/fuzzel.ini
index c5fbe7a8b..b6b5fe99a 100644
--- a/.config/fuzzel/fuzzel.ini
+++ b/.config/fuzzel/fuzzel.ini
@@ -1,6 +1,6 @@
-include="~/.config/fuzzel/fuzzel.theme"
+include="~/.config/fuzzel/fuzzel_theme.ini"
font=Gabarito
-terminal=foot -e
+terminal=kitty -1
prompt=">> "
layer=overlay
diff --git a/.config/fuzzel/fuzzel_theme.ini b/.config/fuzzel/fuzzel_theme.ini
new file mode 100644
index 000000000..3aed8f5c2
--- /dev/null
+++ b/.config/fuzzel/fuzzel_theme.ini
@@ -0,0 +1,8 @@
+[colors]
+background=161217ff
+text=e9e0e8ff
+selection=4b454dff
+selection-text=cdc3ceff
+border=4b454ddd
+match=dfb8f6ff
+selection-match=dfb8f6ff
diff --git a/.config/hypr/custom/keybinds.conf b/.config/hypr/custom/keybinds.conf
index f34545cef..798742ee8 100644
--- a/.config/hypr/custom/keybinds.conf
+++ b/.config/hypr/custom/keybinds.conf
@@ -1,2 +1,10 @@
-# You can put your preferred keybinds here
-# https://wiki.hyprland.org/Configuring/Binds/
\ No newline at end of file
+# See https://wiki.hyprland.org/Configuring/Binds/
+#!
+##! Extra keybinds
+bind = Ctrl+Super+Alt, Slash, exec, xdg-open ~/.config/hypr/custom/keybinds.conf # Edit extra keybinds
+
+# Add stuff here
+# Use #! to add an extra column on the cheatsheet
+# Use ##! to add a section in that column
+# Add a comment after a bind to add a description, like above
+
diff --git a/.config/hypr/hypridle.conf b/.config/hypr/hypridle.conf
index db2ea6082..2ec488c75 100644
--- a/.config/hypr/hypridle.conf
+++ b/.config/hypr/hypridle.conf
@@ -7,17 +7,17 @@ general {
}
listener {
- timeout = 180 # 3mins
+ timeout = 300 # 5mins
on-timeout = loginctl lock-session
}
listener {
- timeout = 240 # 4mins
+ timeout = 600 # 10mins
on-timeout = hyprctl dispatch dpms off
on-resume = hyprctl dispatch dpms on
}
listener {
- timeout = 540 # 9mins
+ timeout = 900 # 15mins
on-timeout = $suspend_cmd
}
diff --git a/.config/hypr/hyprland.conf b/.config/hypr/hyprland.conf
index b34a3fb97..48648f475 100644
--- a/.config/hypr/hyprland.conf
+++ b/.config/hypr/hyprland.conf
@@ -1,5 +1,8 @@
# This file sources other files in `hyprland` and `custom` folders
-# You wanna add your stuff in file in `custom`
+# You wanna add your stuff in files in `custom`
+
+exec = hyprctl dispatch submap global # DO NOT REMOVE THIS OR YOU WON'T BE ABLE TO USE ANY KEYBIND
+submap = global # This is required for catchall to work
# Defaults
source=~/.config/hypr/hyprland/env.conf
diff --git a/.config/hypr/hyprland/env.conf b/.config/hypr/hyprland/env.conf
index bddf37cff..b3d843cdf 100644
--- a/.config/hypr/hyprland/env.conf
+++ b/.config/hypr/hyprland/env.conf
@@ -10,7 +10,7 @@ env = INPUT_METHOD, fcitx
# ############ Themes #############
env = QT_QPA_PLATFORM, wayland
-env = QT_QPA_PLATFORMTHEME, qt6ct
+env = QT_QPA_PLATFORMTHEME, kde
# env = QT_STYLE_OVERRIDE,kvantum
# env = WLR_NO_HARDWARE_CURSORS, 1
@@ -18,7 +18,7 @@ env = QT_QPA_PLATFORMTHEME, qt6ct
# env = WLR_DRM_NO_ATOMIC, 1
# ######## Virtual envrionment #########
-env = ILLOGICAL_IMPULSE_VIRTUAL_ENV, ~/.local/state/ags/.venv
+env = ILLOGICAL_IMPULSE_VIRTUAL_ENV, ~/.local/state/quickshell/.venv
# ############ Others #############
diff --git a/.config/hypr/hyprland/execs.conf b/.config/hypr/hyprland/execs.conf
index 1c3ab3b14..d4945605b 100644
--- a/.config/hypr/hyprland/execs.conf
+++ b/.config/hypr/hyprland/execs.conf
@@ -1,15 +1,15 @@
# Bar, wallpaper
-exec-once = swww-daemon --format xrgb
-exec-once = bash ~/.config/hypr/custom/scripts/__restore_video_wallpaper.sh
+exec-once = swww-daemon --format xrgb --no-cache
+exec-once = sleep 0.5; swww img "$(cat ~/.local/state/quickshell/user/generated/wallpaper/path.txt)" --transition-step 100 --transition-fps 120 --transition-type grow --transition-angle 30 --transition-duration 1
exec-once = /usr/lib/geoclue-2.0/demos/agent & gammastep
-exec-once = agsv1 &
+exec-once = qs &
# Input method
exec-once = fcitx5
# Core components (authentication, lock screen, notification daemon)
exec-once = gnome-keyring-daemon --start --components=secrets
-exec-once = /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 || /usr/libexec/polkit-gnome-authentication-agent-1
+exec-once = /usr/lib/polkit-kde-authentication-agent-1 || /usr/libexec/polkit-kde-authentication-agent-1 || /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 || /usr/libexec/polkit-gnome-authentication-agent-1
exec-once = hypridle
exec-once = dbus-update-activation-environment --all
exec-once = sleep 1 && dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP # Some fix idk
diff --git a/.config/hypr/hyprland/general.conf b/.config/hypr/hyprland/general.conf
index 132efc873..e8895efdc 100644
--- a/.config/hypr/hyprland/general.conf
+++ b/.config/hypr/hyprland/general.conf
@@ -1,37 +1,15 @@
# MONITOR CONFIG
-monitor=,preferred,auto,1
+monitor=,preferred,auto,1,transform, 0
# monitor=,addreserved, 0, 0, 0, 0 # Custom reserved area
# HDMI port: mirror display. To see device name, use `hyprctl monitors`
# monitor=HDMI-A-1,1920x1080@60,1920x0,1,mirror,eDP-1
-input {
- # Keyboard: Add a layout and uncomment kb_options for Win+Space switching shortcut
- kb_layout = us
- # kb_options = grp:win_space_toggle
- numlock_by_default = true
- repeat_delay = 250
- repeat_rate = 35
-
- touchpad {
- natural_scroll = yes
- disable_while_typing = true
- clickfinger_behavior = true
- scroll_factor = 0.5
- }
- special_fallthrough = true
- follow_mouse = 1
-}
-
-binds {
- # focus_window_on_workspace_c# For Auto-run stuff see execs.confhange = true
- scroll_event_delay = 0
-}
-
gestures {
workspace_swipe = true
workspace_swipe_distance = 700
- workspace_swipe_fingers = 4
+ workspace_swipe_fingers = 3
+ workspace_swipe_min_fingers = true
workspace_swipe_cancel_ratio = 0.2
workspace_swipe_min_speed_to_force = 5
workspace_swipe_direction_lock = true
@@ -44,28 +22,30 @@ general {
gaps_in = 4
gaps_out = 5
gaps_workspaces = 50
- border_size = 1
- # Fallback colors
+ border_size = 1
col.active_border = rgba(0DB7D4FF)
col.inactive_border = rgba(31313600)
-
resize_on_border = true
+
no_focus_fallback = true
- layout = dwindle
- #focus_to_other_workspaces = true # ahhhh i still haven't properly implemented this
allow_tearing = true # This just allows the `immediate` window rule to work
+
+ snap {
+ enabled = true
+ }
}
dwindle {
- preserve_split = true
- smart_split = false
- smart_resizing = false
+ preserve_split = true
+ smart_split = false
+ smart_resizing = false
+ # precise_mouse_move = true
}
decoration {
- rounding = 20
+ rounding = 18
blur {
enabled = true
@@ -73,89 +53,102 @@ decoration {
special = false
new_optimizations = true
size = 14
- passes = 4
+ passes = 3
brightness = 1
noise = 0.01
contrast = 1
popups = true
popups_ignorealpha = 0.6
+ input_methods = true
+ input_methods_ignorealpha = 0.8
}
- # Shadow
shadow {
enabled = true
ignore_window = true
- range = 20
+ range = 30
offset = 0 2
render_power = 4
- color = rgba(0000002A)
+ color = rgba(00000010)
}
-
- # Window Opacities
- # active_opacity = 1
- # inactive_opacity = 1
- # fullscreen_opacity = 1
-
- # Shader
- # screen_shader = ~/.config/hypr/shaders/nothing.frag
- # screen_shader = ~/.config/hypr/shaders/vibrance.frag
# Dim
- dim_inactive = false
- dim_strength = 0.1
- dim_special = 0
+ dim_inactive = true
+ dim_strength = 0.025
+ dim_special = 0.07
}
animations {
enabled = true
- # Animation curves
-
- bezier = linear, 0, 0, 1, 1
- bezier = md3_standard, 0.2, 0, 0, 1
- bezier = md3_decel, 0.05, 0.7, 0.1, 1
- bezier = md3_accel, 0.3, 0, 0.8, 0.15
- bezier = overshot, 0.05, 0.9, 0.1, 1.1
- bezier = crazyshot, 0.1, 1.5, 0.76, 0.92
- bezier = hyprnostretch, 0.05, 0.9, 0.1, 1.0
+ # Curves
+ bezier = expressiveFastSpatial, 0.42, 1.67, 0.21, 0.90
+ bezier = expressiveSlowSpatial, 0.39, 1.29, 0.35, 0.98
+ bezier = expressiveDefaultSpatial, 0.38, 1.21, 0.22, 1.00
+ bezier = emphasizedDecel, 0.05, 0.7, 0.1, 1
+ bezier = emphasizedAccel, 0.3, 0, 0.8, 0.15
+ bezier = standardDecel, 0, 0, 0, 1
bezier = menu_decel, 0.1, 1, 0, 1
- bezier = menu_accel, 0.38, 0.04, 1, 0.07
- bezier = easeInOutCirc, 0.85, 0, 0.15, 1
- bezier = easeOutCirc, 0, 0.55, 0.45, 1
- bezier = easeOutExpo, 0.16, 1, 0.3, 1
- bezier = softAcDecel, 0.26, 0.26, 0.15, 1
- bezier = md2, 0.4, 0, 0.2, 1 # use with .2s duration
- # Animation configs
- animation = windows, 1, 3, md3_decel, popin 60%
- animation = windowsIn, 1, 3, md3_decel, popin 60%
- animation = windowsOut, 1, 3, md3_accel, popin 60%
- animation = border, 1, 10, default
- animation = fade, 1, 3, md3_decel
- # animation = layers, 1, 2, md3_decel, slide
- animation = layersIn, 1, 3, menu_decel, slide
- animation = layersOut, 1, 1.6, menu_accel
- animation = fadeLayersIn, 1, 2, menu_decel
- animation = fadeLayersOut, 1, 0.5, menu_accel
+ bezier = menu_accel, 0.52, 0.03, 0.72, 0.08
+ # Configs
+ # windows
+ animation = windowsIn, 1, 3, emphasizedDecel, popin 80%
+ animation = windowsOut, 1, 2, emphasizedDecel, popin 90%
+ animation = windowsMove, 1, 3, emphasizedDecel, slide
+ animation = border, 1, 10, emphasizedDecel
+ # layers
+ animation = layersIn, 1, 2.7, emphasizedDecel, popin 93%
+ animation = layersOut, 1, 2, menu_accel, popin 94%
+ # fade
+ animation = fadeLayersIn, 1, 0.5, menu_decel
+ animation = fadeLayersOut, 1, 2.2, menu_accel
+ # workspaces
animation = workspaces, 1, 7, menu_decel, slide
- # animation = workspaces, 1, 2.5, softAcDecel, slide
- # animation = workspaces, 1, 7, menu_decel, slidefade 15%
- # animation = specialWorkspace, 1, 3, md3_decel, slidefadevert 15%
- animation = specialWorkspace, 1, 3, md3_decel, slidevert
+ ## specialWorkspace
+ animation = specialWorkspaceIn, 1, 2.8, emphasizedDecel, slidevert
+ animation = specialWorkspaceOut, 1, 1.2, emphasizedAccel, slidevert
+}
+
+input {
+ kb_layout = us
+ numlock_by_default = true
+ repeat_delay = 250
+ repeat_rate = 35
+
+ follow_mouse = 1
+
+ touchpad {
+ natural_scroll = yes
+ disable_while_typing = true
+ clickfinger_behavior = true
+ scroll_factor = 0.5
+ }
}
misc {
+ disable_hyprland_logo = true
+ disable_splash_rendering = true
vfr = 1
vrr = 1
+ mouse_move_enables_dpms = true
+ key_press_enables_dpms = true
animate_manual_resizes = false
animate_mouse_windowdragging = false
- enable_swallow = false
+ enable_swallow = true
swallow_regex = (foot|kitty|allacritty|Alacritty)
-
- disable_hyprland_logo = true
- force_default_wallpaper = 0
new_window_takes_over_fullscreen = 2
allow_session_lock_restore = true
-
initial_workspace_tracking = false
+ focus_on_activate = true
+}
+
+binds {
+ scroll_event_delay = 0
+ hide_special_on_workspace_change = true
+}
+
+cursor {
+ zoom_factor = 1
+ zoom_rigid = false
}
# Overview
diff --git a/.config/hypr/hyprland/keybinds.conf b/.config/hypr/hyprland/keybinds.conf
index 6b28095b3..37f4d482b 100644
--- a/.config/hypr/hyprland/keybinds.conf
+++ b/.config/hypr/hyprland/keybinds.conf
@@ -1,194 +1,195 @@
# Lines ending with `# [hidden]` won't be shown on cheatsheet
# Lines starting with #! are section headings
+#!
+##! Shell
+# These absolutely need to be on top, or they won't work consistently
+bindid = Super, Super_L, Toggle overview, global, quickshell:overviewToggleRelease # Toggle overview/launcher
+bind = Super, Super_L, exec, qs ipc call TEST_ALIVE || pkill fuzzel || fuzzel # [hidden] Launcher (fallback)
+binditn = Super, catchall, global, quickshell:overviewToggleReleaseInterrupt # [hidden]
+bind = Ctrl, Super_L, global, quickshell:overviewToggleReleaseInterrupt # [hidden]
+bind = Super, mouse:272, global, quickshell:overviewToggleReleaseInterrupt # [hidden]
+bind = Super, mouse:273, global, quickshell:overviewToggleReleaseInterrupt # [hidden]
+bind = Super, mouse:274, global, quickshell:overviewToggleReleaseInterrupt # [hidden]
+bind = Super, mouse:275, global, quickshell:overviewToggleReleaseInterrupt # [hidden]
+bind = Super, mouse:276, global, quickshell:overviewToggleReleaseInterrupt # [hidden]
+bind = Super, mouse:277, global, quickshell:overviewToggleReleaseInterrupt # [hidden]
+bind = Super, mouse_up, global, quickshell:overviewToggleReleaseInterrupt # [hidden]
+bind = Super, mouse_down,global, quickshell:overviewToggleReleaseInterrupt # [hidden]
+
+bindit = ,Super_L, global, quickshell:workspaceNumber # [hidden]
+bindd = Super, V, Clipboard history >> clipboard, global, quickshell:overviewClipboardToggle # Clipboard history >> clipboard
+bindd = Super, Period, Emoji >> clipboard, global, quickshell:overviewEmojiToggle # Emoji >> clipboard
+bindd = Super, Tab, Toggle overview, global, quickshell:overviewToggle # [hidden] Toggle overview/launcher (alt)
+bindd = Super, A, Toggle left sidebar, global, quickshell:sidebarLeftToggle # Toggle left sidebar
+bind = Super+Alt, A, global, quickshell:sidebarLeftToggleDetach # [hidden]
+bind = Super, B, global, quickshell:sidebarLeftToggle # [hidden]
+bind = Super, O, global, quickshell:sidebarLeftToggle # [hidden]
+bindd = Super, N, Toggle right sidebar, global, quickshell:sidebarRightToggle # Toggle right sidebar
+bindd = Super, Slash, Toggle cheatsheet, global, quickshell:cheatsheetToggle # Toggle cheatsheet
+bindd = Super, K, Toggle on-screen keyboard, global, quickshell:oskToggle # Toggle on-screen keyboard
+bindd = Super, M, Toggle media controls, global, quickshell:mediaControlsToggle # Toggle media controls
+bindd = Ctrl+Alt, Delete, Toggle session menu, global, quickshell:sessionToggle # Toggle session menu
+bind = Ctrl+Alt, Delete, exec, qs ipc call TEST_ALIVE || pkill wlogout || wlogout -p layer-shell # [hidden] Session menu (fallback)
+
+bindle=, XF86MonBrightnessUp, exec, qs ipc call brightness increment || agsv1 run-js 'brightness.screen_value += 0.05; indicator.popup(1);' # [hidden]
+bindle=, XF86MonBrightnessDown, exec, qs ipc call brightness decrement || agsv1 run-js 'brightness.screen_value -= 0.05; indicator.popup(1);' # [hidden]
+bindle=, XF86AudioRaiseVolume, exec, wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 2%+ # [hidden]
+bindle=, XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 2%- # [hidden]
+
+bindl = ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_SINK@ toggle # [hidden]
+bindld = Super+Shift,M, Toggle mute, exec, wpctl set-mute @DEFAULT_SINK@ toggle # [hidden]
bindl = Alt ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden]
-bindl = Super ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden]
-bindl = ,XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle # [hidden]
-bindl = ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle # [hidden]
-bindl = Super+Shift,M, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle # [hidden]
-bindle=, XF86AudioRaiseVolume, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 && wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+ # [hidden]
-bindle=, XF86AudioLowerVolume, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 && wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- # [hidden]
+bindl = ,XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden]
+bindld = Super+Alt,M, Toggle mic, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden]
+bindd = Ctrl+Super, T, Change wallpaper, exec, ~/.config/quickshell/scripts/switchwall.sh # Change wallpaper
+bind = Ctrl+Super, R, exec, killall ags agsv1 gjs ydotool qs quickshell; qs & # Restart widgets
-# Uncomment these if you can't get AGS to work
-#bindle=, XF86MonBrightnessUp, exec, brightnessctl set '12.75+'
-#bindle=, XF86MonBrightnessDown, exec, brightnessctl set '12.75-'
-
-#!
-##! Essentials for beginners
-
-bind = Super, T, exec, foot # Launch foot (terminal)
-bind = Super, Return, exec, foot # [hidden] # In case you're from i3 or its Wayland clone
-bind = , Super, exec, true # Open app launcher
-bind = Ctrl+Super, T, exec, ~/.config/ags/scripts/color_generation/switchwall.sh # Change wallpaper
-##! Actions
+##! Utilities
# Screenshot, Record, OCR, Color picker, Clipboard history
-bind = Super, V, exec, pkill fuzzel || cliphist list | fuzzel --match-mode fzf --dmenu | cliphist decode | wl-copy # Clipboard history >> clipboard
-bind = Super, Period, exec, pkill fuzzel || ~/.local/bin/fuzzel-emoji # Pick emoji >> clipboard
-bind = Ctrl+Shift+Alt, Delete, exec, pkill wlogout || wlogout -p layer-shell # [hidden]
-bind = Super+Shift, S, exec, ~/.config/ags/scripts/grimblast.sh --freeze copy area # Screen snip
-bind = Super+Shift+Alt, S, exec, grim -g "$(slurp)" - | swappy -f - # Screen snip >> edit
+bindd = Super, V, Copy clipboard history entry, exec, qs ipc call TEST_ALIVE || pkill fuzzel || cliphist list | fuzzel --match-mode fzf --dmenu | cliphist decode | wl-copy # [hidden] Clipboard history >> clipboard (fallback)
+bindd = Super, Period, Copy an emoji, exec, qs ipc call TEST_ALIVE || pkill fuzzel || ~/.config/hypr/hyprland/scripts/fuzzel-emoji.sh copy # [hidden] Emoji >> clipboard (fallback)
+bindd = Super+Shift, S, Screen snip, exec, pidof slurp || hyprshot --freeze --clipboard-only --mode region --silent # Screen snip >> clipboard
+bindd = Super+Shift+Alt, S, Screen snip and annotate, exec, pidof slurp || grim -g "$(slurp)" - | swappy -f - # Screen snip and annotate
# OCR
-bind = Super+Shift,T,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract -l eng "tmp.png" - | wl-copy && rm "tmp.png" # Screen snip to text >> clipboard
-bind = Ctrl+Super+Shift,S,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract "tmp.png" - | wl-copy && rm "tmp.png" # [hidden]
+bindd = Super+Shift, T, Character recognition,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract "tmp.png" - | wl-copy && rm "tmp.png" # [hidden]
# Color picker
-bind = Super+Shift, C, exec, hyprpicker -a # Pick color (Hex) >> clipboard
+bindd = Super+Shift, C, Color picker, exec, hyprpicker -a # Pick color (Hex) >> clipboard
# Fullscreen screenshot
-bindl=,Print,exec,grim - | wl-copy # Screenshot >> clipboard
-bindl= Ctrl,Print, exec, mkdir -p ~/Pictures/Screenshots && ~/.config/ags/scripts/grimblast.sh copysave screen ~/Pictures/Screenshots/Screenshot_"$(date '+%Y-%m-%d_%H.%M.%S')".png # Screenshot >> clipboard & file
-# AI
-bind = Super+Shift+Alt, mouse:273, exec, ~/.config/ags/scripts/ai/primary-buffer-query.sh # Provide AI response for selected text
-
+bindld = ,Print, Screenshot >> clipboard ,exec,grim - | wl-copy # Screenshot >> clipboard
+bindld = Ctrl,Print, Screenshot >> clipboard & save, exec, mkdir -p $(xdg-user-dir PICTURES)/Screenshots && grim $(xdg-user-dir PICTURES)/Screenshots/Screenshot_"$(date '+%Y-%m-%d_%H.%M.%S')".png # Screenshot >> clipboard & file
# Recording stuff
-bind = Super+Alt, R, exec, ~/.config/ags/scripts/record-script.sh # Record region (no sound)
-bind = Ctrl+Alt, R, exec, ~/.config/ags/scripts/record-script.sh --fullscreen # [hidden] Record screen (no sound)
-bind = Super+Shift+Alt, R, exec, ~/.config/ags/scripts/record-script.sh --fullscreen-sound # Record screen (with sound)
-##! Session
-bind = Ctrl+Super, L, exec, agsv1 run-js 'lock.lock()' # [hidden]
-bind = Super, L, exec, loginctl lock-session # Lock
-bind = Super+Shift, L, exec, loginctl lock-session # [hidden]
-bindl = Super+Shift, L, exec, sleep 0.1 && systemctl suspend || loginctl suspend # Suspend system
-bind = Ctrl+Shift+Alt+Super, Delete, exec, systemctl poweroff || loginctl poweroff # [hidden] Power off
+bindd = Super+Alt, R, Record region (no sound), exec, ~/.config/hypr/hyprland/scripts/record.sh # Record region (no sound)
+bindd = Ctrl+Alt, R, Record screen (no sound), exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen # [hidden] Record screen (no sound)
+bindd = Super+Shift+Alt, R, Record screen (with sound), exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen-sound # Record screen (with sound)
+# AI
+bindd = Super+Shift+Alt, mouse:273, Generate AI summary for selected text, exec, ~/.config/ags/scripts/ai/primary-buffer-query.sh # AI summary for selected text
#!
-##! Window management
+##! Window
# Focusing
-#/# bind = Super, ←/↑/→/↓,, # Move focus in direction
+bindm = Super, mouse:272, movewindow # Move
+bindm = Super, mouse:274, movewindow # [hidden]
+bindm = Super, mouse:273, resizewindow # Resize
+#/# bind = Super, ←/↑/→/↓,, # Focus in direction
bind = Super, Left, movefocus, l # [hidden]
bind = Super, Right, movefocus, r # [hidden]
bind = Super, Up, movefocus, u # [hidden]
bind = Super, Down, movefocus, d # [hidden]
bind = Super, BracketLeft, movefocus, l # [hidden]
bind = Super, BracketRight, movefocus, r # [hidden]
-bindm = Super, mouse:272, movewindow
-bindm = Super, mouse:273, resizewindow
-bind = Super, Q, killactive,
-bind = Super+Shift+Alt, Q, exec, hyprctl kill # Pick and kill a window
-##! Window arrangement
-#/# bind = Super+Shift, ←/↑/→/↓,, # Window: move in direction
+#/# bind = Super+Shift, ←/↑/→/↓,, # Move in direction
bind = Super+Shift, Left, movewindow, l # [hidden]
bind = Super+Shift, Right, movewindow, r # [hidden]
bind = Super+Shift, Up, movewindow, u # [hidden]
bind = Super+Shift, Down, movewindow, d # [hidden]
+bind = Alt, F4, killactive, # [hidden] Close (Windows)
+bind = Super, Q, killactive, # Close
+bind = Super+Shift+Alt, Q, exec, hyprctl kill # Forcefully zap a window
+
+
# Window split ratio
-#/# binde = Super, +/-,, # Window: split ratio +/- 0.1
-binde = Super, Minus, splitratio, -0.1 # [hidden]
-binde = Super, Equal, splitratio, +0.1 # [hidden]
+#/# binde = Super, ;/',, # Adjust split ratio
binde = Super, Semicolon, splitratio, -0.1 # [hidden]
binde = Super, Apostrophe, splitratio, +0.1 # [hidden]
# Positioning mode
-bind = Super+Alt, Space, togglefloating,
-bind = Super+Alt, F, fullscreenstate, 0 3 # Toggle fake fullscreen
-bind = Super, F, fullscreen, 0
-bind = Super, D, fullscreen, 1
+bind = Super+Alt, Space, togglefloating, # Float/Tile
+bind = Super, D, fullscreen, 1 # Maximize
+bind = Super, F, fullscreen, 0 # Fullscreen
+bind = Super+Alt, F, fullscreenstate, 0 3 # Fullscreen spoof
+bind = Super, P, pin # Pin
-#!
-##! Workspace navigation
-# Switching
-#/# bind = Super, Hash,, # Focus workspace # (1, 2, 3, 4, ...)
-bind = Super, 1, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh workspace 1 # [hidden]
-bind = Super, 2, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh workspace 2 # [hidden]
-bind = Super, 3, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh workspace 3 # [hidden]
-bind = Super, 4, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh workspace 4 # [hidden]
-bind = Super, 5, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh workspace 5 # [hidden]
-bind = Super, 6, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh workspace 6 # [hidden]
-bind = Super, 7, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh workspace 7 # [hidden]
-bind = Super, 8, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh workspace 8 # [hidden]
-bind = Super, 9, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh workspace 9 # [hidden]
-bind = Super, 0, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh workspace 10 # [hidden]
+#/# bind = Super+Alt, Hash,, # Send to workspace # (1, 2, 3,...)
+bind = Super+Alt, 1, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh movetoworkspacesilent 1 # [hidden]
+bind = Super+Alt, 2, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh movetoworkspacesilent 2 # [hidden]
+bind = Super+Alt, 3, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh movetoworkspacesilent 3 # [hidden]
+bind = Super+Alt, 4, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh movetoworkspacesilent 4 # [hidden]
+bind = Super+Alt, 5, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh movetoworkspacesilent 5 # [hidden]
+bind = Super+Alt, 6, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh movetoworkspacesilent 6 # [hidden]
+bind = Super+Alt, 7, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh movetoworkspacesilent 7 # [hidden]
+bind = Super+Alt, 8, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh movetoworkspacesilent 8 # [hidden]
+bind = Super+Alt, 9, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh movetoworkspacesilent 9 # [hidden]
+bind = Super+Alt, 0, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh movetoworkspacesilent 10 # [hidden]
-#/# bind = Super, Scroll ↑/↓,, # Workspace: focus left/right
-bind = Super, mouse_up, workspace, +1 # [hidden]
-bind = Super, mouse_down, workspace, -1 # [hidden]
-bind = Ctrl+Super, mouse_up, workspace, r+1 # [hidden]
-bind = Ctrl+Super, mouse_down, workspace, r-1 # [hidden]
-#/# bind = Ctrl+Super, ←/→,, # Workspace: focus left/right
-bind = Ctrl+Super, Right, workspace, r+1 # [hidden]
-bind = Ctrl+Super, Left, workspace, r-1 # [hidden]
-#/# bind = Ctrl+Super+Alt, ←/→,, # Workspace: focus non-empty left/right
-bind = Ctrl+Super+Alt, Right, workspace, m+1 # [hidden]
-bind = Ctrl+Super+Alt, Left, workspace, m-1 # [hidden]
-#/# bind = Super, Page_↑/↓,, # Workspace: focus left/right
-bind = Super, Page_Down, workspace, +1 # [hidden]
-bind = Super, Page_Up, workspace, -1 # [hidden]
-bind = Ctrl+Super, Page_Down, workspace, r+1 # [hidden]
-bind = Ctrl+Super, Page_Up, workspace, r-1 # [hidden]
-## Special
-bind = Super, S, togglespecialworkspace,
-bind = Super, mouse:275, togglespecialworkspace,
-
-##! Workspace management
-# Move window to workspace Super + Alt + [0-9]
-#/# bind = Super+Alt, Hash,, # Window: move to workspace # (1, 2, 3, 4, ...)
-bind = Super+Alt, 1, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh movetoworkspacesilent 1 # [hidden]
-bind = Super+Alt, 2, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh movetoworkspacesilent 2 # [hidden]
-bind = Super+Alt, 3, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh movetoworkspacesilent 3 # [hidden]
-bind = Super+Alt, 4, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh movetoworkspacesilent 4 # [hidden]
-bind = Super+Alt, 5, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh movetoworkspacesilent 5 # [hidden]
-bind = Super+Alt, 6, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh movetoworkspacesilent 6 # [hidden]
-bind = Super+Alt, 7, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh movetoworkspacesilent 7 # [hidden]
-bind = Super+Alt, 8, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh movetoworkspacesilent 8 # [hidden]
-bind = Super+Alt, 9, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh movetoworkspacesilent 9 # [hidden]
-bind = Super+Alt, 0, exec, ~/.config/ags/scripts/hyprland/workspace_action.sh movetoworkspacesilent 10 # [hidden]
-
-bind = Ctrl+Super+Shift, Up, movetoworkspacesilent, special # [hidden]
-
-bind = Ctrl+Super+Shift, Right, movetoworkspace, r+1 # [hidden]
-bind = Ctrl+Super+Shift, Left, movetoworkspace, r-1 # [hidden]
-bind = Ctrl+Super, BracketLeft, workspace, -1 # [hidden]
-bind = Ctrl+Super, BracketRight, workspace, +1 # [hidden]
-bind = Ctrl+Super, Up, workspace, r-5 # [hidden]
-bind = Ctrl+Super, Down, workspace, r+5 # [hidden]
-#/# bind = Super+Shift, Scroll ↑/↓,, # Window: move to workspace left/right
+# #/# bind = Super+Shift, Scroll ↑/↓,, # Send to workspace left/right
bind = Super+Shift, mouse_down, movetoworkspace, r-1 # [hidden]
bind = Super+Shift, mouse_up, movetoworkspace, r+1 # [hidden]
bind = Super+Alt, mouse_down, movetoworkspace, -1 # [hidden]
bind = Super+Alt, mouse_up, movetoworkspace, +1 # [hidden]
-#/# bind = Super+Shift, Page_↑/↓,, # Window: move to workspace left/right
+
+#/# bind = Super+Shift, Page_↑/↓,, # Send to workspace left/right
bind = Super+Alt, Page_Down, movetoworkspace, +1 # [hidden]
bind = Super+Alt, Page_Up, movetoworkspace, -1 # [hidden]
bind = Super+Shift, Page_Down, movetoworkspace, r+1 # [hidden]
bind = Super+Shift, Page_Up, movetoworkspace, r-1 # [hidden]
-bind = Super+Alt, S, movetoworkspacesilent, special
-bind = Super, P, pin
+bind = Ctrl+Super+Shift, Right, movetoworkspace, r+1 # [hidden]
+bind = Ctrl+Super+Shift, Left, movetoworkspace, r-1 # [hidden]
+
+bind = Super+Alt, S, movetoworkspacesilent, special # Send to scratchpad
bind = Ctrl+Super, S, togglespecialworkspace, # [hidden]
bind = Alt, Tab, cyclenext # [hidden] sus keybind
bind = Alt, Tab, bringactivetotop, # [hidden] bring it to the top
-#!
-##! Widgets
-bindr = Ctrl+Super, R, exec, killall ags agsv1 ydotool; agsv1 & # Restart widgets
-bindr = Ctrl+Super+Alt, R, exec, hyprctl reload; killall agsv1 ydotool; agsv1 & # [hidden]
-bind = Ctrl+Alt, Slash, exec, agsv1 run-js 'cycleMode();' # Cycle bar mode (normal, focus)
-bindir = Super, Super_L, exec, agsv1 -t 'overview' # Toggle overview/launcher
-bind = Super, Tab, exec, agsv1 -t 'overview' # [hidden]
-bind = Super, Slash, exec, for ((i=0; i<$(hyprctl monitors -j | jq length); i++)); do agsv1 -t "cheatsheet""$i"; done # Show cheatsheet
-bind = Super, B, exec, agsv1 -t 'sideleft' # Toggle left sidebar
-bind = Super, A, exec, agsv1 -t 'sideleft' # [hidden]
-bind = Super, O, exec, agsv1 -t 'sideleft' # [hidden]
-bind = Super, N, exec, agsv1 -t 'sideright' # Toggle right sidebar
-bind = Super, M, exec, agsv1 run-js 'openMusicControls.value = (!mpris.getPlayer() ? false : !openMusicControls.value);' # Toggle music controls
-bind = Super, Comma, exec, agsv1 run-js 'openColorScheme.value = true; Utils.timeout(2000, () => openColorScheme.value = false);' # View color scheme and options
-bind = Super, K, exec, for ((i=0; i<$(hyprctl monitors -j | jq length); i++)); do agsv1 -t "osk""$i"; done # Toggle on-screen keyboard
-bind = Ctrl+Alt, Delete, exec, for ((i=0; i<$(hyprctl monitors -j | jq length); i++)); do agsv1 -t "session""$i"; done # Toggle power menu
-bind = Ctrl+Super, G, exec, for ((i=0; i<$(hyprctl monitors -j | jq length); i++)); do agsv1 -t "crosshair""$i"; done # Toggle crosshair
-bindle=, XF86MonBrightnessUp, exec, agsv1 run-js 'brightness.screen_value += 0.05; indicator.popup(1);' # [hidden]
-bindle=, XF86MonBrightnessDown, exec, agsv1 run-js 'brightness.screen_value -= 0.05; indicator.popup(1);' # [hidden]
-bindl = , XF86AudioMute, exec, agsv1 run-js 'indicator.popup(1);' # [hidden]
-bindl = Super+Shift,M, exec, agsv1 run-js 'indicator.popup(1);' # [hidden]
+##! Workspace
+# Switching
+#/# bind = Super, Hash,, # Focus workspace # (1, 2, 3,...)
+bind = Super, 1, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh workspace 1 # [hidden]
+bind = Super, 2, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh workspace 2 # [hidden]
+bind = Super, 3, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh workspace 3 # [hidden]
+bind = Super, 4, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh workspace 4 # [hidden]
+bind = Super, 5, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh workspace 5 # [hidden]
+bind = Super, 6, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh workspace 6 # [hidden]
+bind = Super, 7, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh workspace 7 # [hidden]
+bind = Super, 8, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh workspace 8 # [hidden]
+bind = Super, 9, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh workspace 9 # [hidden]
+bind = Super, 0, exec, ~/.config/hypr/hyprland/scripts/workspace_action.sh workspace 10 # [hidden]
+#/# bind = Ctrl+Super, ←/→,, # Focus left/right
+bind = Ctrl+Super, Right, workspace, r+1 # [hidden]
+bind = Ctrl+Super, Left, workspace, r-1 # [hidden]
+#/# bind = Ctrl+Super+Alt, ←/→,, # [hidden] Focus busy left/right
+bind = Ctrl+Super+Alt, Right, workspace, m+1 # [hidden]
+bind = Ctrl+Super+Alt, Left, workspace, m-1 # [hidden]
+#/# bind = Super, Page_↑/↓,, # Focus left/right
+bind = Super, Page_Down, workspace, +1 # [hidden]
+bind = Super, Page_Up, workspace, -1 # [hidden]
+bind = Ctrl+Super, Page_Down, workspace, r+1 # [hidden]
+bind = Ctrl+Super, Page_Up, workspace, r-1 # [hidden]
+#/# bind = Super, Scroll ↑/↓,, # Focus left/right
+bind = Super, mouse_up, workspace, +1 # [hidden]
+bind = Super, mouse_down, workspace, -1 # [hidden]
+bind = Ctrl+Super, mouse_up, workspace, r+1 # [hidden]
+bind = Ctrl+Super, mouse_down, workspace, r-1 # [hidden]
+## Special
+bind = Super, S, togglespecialworkspace, # Toggle scratchpad
+bind = Super, mouse:275, togglespecialworkspace, # [hidden]
+bind = Ctrl+Super, BracketLeft, workspace, -1 # [hidden]
+bind = Ctrl+Super, BracketRight, workspace, +1 # [hidden]
+bind = Ctrl+Super, Up, workspace, r-5 # [hidden]
+bind = Ctrl+Super, Down, workspace, r+5 # [hidden]
+
+#!
# Testing
-# bind = SuperAlt, f12, exec, notify-send "Hyprland version: $(hyprctl version | head -2 | tail -1 | cut -f2 -d ' ')" "owo" -a 'Hyprland keybind'
-# bind = Super+Alt, f12, exec, notify-send "Millis since epoch" "$(date +%s%N | cut -b1-13)" -a 'Hyprland keybind'
-bind = Super+Alt, f12, exec, notify-send 'Test notification' "Here's a really long message to test truncation and wrapping\nYou can middle click or flick this notification to dismiss it!" -a 'Shell' -A "Test1=I got it!" -A "Test2=Another action" -t 5000 # [hidden]
+bind = Super+Alt, f11, exec, bash -c 'RANDOM_IMAGE=$(find ~/Pictures -type f | grep -v -i "nipple" | grep -v -i "pussy" | shuf -n 1); ACTION=$(notify-send "Test notification with body image" "This notification should contain your user account image and Discord icon . Oh and here is a random image in your Pictures folder: " -a "Hyprland keybind" -p -h "string:image-path:/var/lib/AccountsService/icons/$USER" -t 6000 -i "discord" -A "openImage=Open profile image" -A "action2=Open the random image" -A "action3=Useless button"); [[ $ACTION == *openImage ]] && xdg-open "/var/lib/AccountsService/icons/$USER"; [[ $ACTION == *action2 ]] && xdg-open \"$RANDOM_IMAGE\"' # [hidden]
+bind = Super+Alt, f12, exec, bash -c 'RANDOM_IMAGE=$(find ~/Pictures -type f | grep -v -i "nipple" | grep -v -i "pussy" | shuf -n 1); ACTION=$(notify-send "Test notification" "This notification should contain a random image in your Pictures folder and Discord icon .\nFlick right to dismiss! " -a "Discord (fake)" -p -h "string:image-path:$RANDOM_IMAGE" -t 6000 -i "discord" -A "openImage=Open profile image" -A "action2=Useless button" -A "action3=Cry more"); [[ $ACTION == *openImage ]] && xdg-open "/var/lib/AccountsService/icons/$USER"' # [hidden]
bind = Super+Alt, Equal, exec, notify-send "Urgent notification" "Ah hell no" -u critical -a 'Hyprland keybind' # [hidden]
+##! Session
+bindd = Super, L, Lock, exec, loginctl lock-session # Lock
+bind = Super+Shift, L, exec, loginctl lock-session # [hidden]
+bindld = Super+Shift, L, Suspend system, exec, sleep 0.1 && systemctl suspend || loginctl suspend # Sleep
+bindd = Ctrl+Shift+Alt+Super, Delete, Shutdown, exec, systemctl poweroff || loginctl poweroff # [hidden] Power off
+
+##! Screen
+# Zoom
+binde = Super, Minus, exec, ~/.config/hypr/hyprland/scripts/zoom.sh decrease 0.1 # Zoom out
+binde = Super, Equal, exec, ~/.config/hypr/hyprland/scripts/zoom.sh increase 0.1 # Zoom in
+
##! Media
bindl= Super+Shift, N, exec, playerctl next || playerctl position `bc <<< "100 * $(playerctl metadata mpris:length) / 1000000 / 100"` # Next track
bindl= ,XF86AudioNext, exec, playerctl next || playerctl position `bc <<< "100 * $(playerctl metadata mpris:length) / 1000000 / 100"` # [hidden]
bindl= ,XF86AudioPrev, exec, playerctl previous # [hidden]
-bindel = Super+Shift, Comma, exec, ~/.config/ags/scripts/music/adjust-volume.sh -0.03 # Raise music volume
-bindel = Super+Shift, Period, exec, ~/.config/ags/scripts/music/adjust-volume.sh 0.03 # Lower music volume
bind = Super+Shift+Alt, mouse:275, exec, playerctl previous # [hidden]
bind = Super+Shift+Alt, mouse:276, exec, playerctl next || playerctl position `bc <<< "100 * $(playerctl metadata mpris:length) / 1000000 / 100"` # [hidden]
bindl= Super+Shift, B, exec, playerctl previous # Previous track
@@ -196,26 +197,19 @@ bindl= Super+Shift, P, exec, playerctl play-pause # Play/pause media
bindl= ,XF86AudioPlay, exec, playerctl play-pause # [hidden]
bindl= ,XF86AudioPause, exec, playerctl play-pause # [hidden]
-#!
##! Apps
-bind = Super, T, exec, # Launch foot (terminal)
-bind = Super, Z, exec, Zed # Launch Zed (editor)
-bind = Super, C, exec, code # Launch VSCode (editor)
-bind = Super, E, exec, nautilus --new-window # Launch Nautilus (file manager)
-bind = Super+Alt, E, exec, thunar # [hidden]
-bind = Super, W, exec, google-chrome-stable || firefox # [hidden] Let's not give people (more) reason to shit on my rice
-bind = Ctrl+Super, W, exec, firefox # Launch Firefox (browser)
-bind = Super, X, exec, gnome-text-editor --new-window # Launch GNOME Text Editor
-bind = Super+Shift, W, exec, wps # Launch WPS Office
-bind = Super, I, exec, XDG_CURRENT_DESKTOP="gnome" gnome-control-center # Launch GNOME Settings
-bind = Ctrl+Super, V, exec, pavucontrol # Launch pavucontrol (volume mixer)
-bind = Ctrl+Super+Shift, V, exec, easyeffects # Launch EasyEffects (equalizer & other audio effects)
-bind = Ctrl+Shift, Escape, exec, gnome-system-monitor # Launch GNOME System monitor
-bind = Ctrl+Super, Slash, exec, pkill anyrun || anyrun # Toggle fallback launcher: anyrun
-bind = Super+Alt, Slash, exec, pkill fuzzel || fuzzel # Toggle fallback launcher: fuzzel
+bind = Super, Return, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # Terminal
+bind = Super, T, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # [hidden] Kitty (terminal) (alt)
+bind = Ctrl+Alt, T, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # [hidden] Kitty (for Ubuntu people)
+bind = Super, E, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "dolphin" "nautilus" "nemo" "thunar" # File manager
+bind = Super, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "zen-browser" "firefox" "brave" "chromium" "google-chrome-stable" "microsoft-edge-stable" "opera" # Browser
+bind = Super, C, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "code" "codium" "zed" "kate" "gnome-text-editor" "emacs" "command -v nvim && kitty -1 nvim" # Code editor
+bind = Super+Shift, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "wps" "onlyoffice-desktopeditors" # Office software
+bind = Super, X, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kate" "gnome-text-editor" "emacs" # Text editor
+bind = Ctrl+Super, V, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "pavucontrol-qt" "pavucontrol" # Volume mixer
+bind = Super, I, exec, XDG_CURRENT_DESKTOP=gnome ~/.config/hypr/hyprland/scripts/launch_first_available.sh "systemsettings" "gnome-control-center" "better-control" # Settings app
+bind = Ctrl+Shift, Escape, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "gnome-system-monitor" "plasma-systemmonitzor --page-name Processes" "command -v btop && kitty -1 fish -c btop" # System monitor
# Cursed stuff
## Make window not amogus large
bind = Ctrl+Super, Backslash, resizeactive, exact 640 480 # [hidden]
-
-
diff --git a/.config/hypr/hyprland/rules.conf b/.config/hypr/hyprland/rules.conf
index 0f0a2e333..c73303086 100644
--- a/.config/hypr/hyprland/rules.conf
+++ b/.config/hypr/hyprland/rules.conf
@@ -19,6 +19,15 @@ windowrulev2 = center, class:^(org.pulseaudio.pavucontrol)$
windowrulev2 = float, class:^(nm-connection-editor)$
windowrulev2 = size 45%, class:^(nm-connection-editor)$
windowrulev2 = center, class:^(nm-connection-editor)$
+windowrulev2 = float, class:.*plasmawindowed.*
+windowrulev2 = float, class:kcm_.*
+windowrulev2 = float, class:.*bluedevilwizard
+
+# No appearance
+# kde-material-you-colors spawns a window when changing dark/light theme. This is to make sure it doesn't interfere at all.
+windowrulev2 = float, class:^(plasma-changeicons)$
+windowrulev2 = noinitialfocus, class:^(plasma-changeicons)$
+windowrulev2 = move 999999 999999, class:^(plasma-changeicons)$
# Tiling
windowrulev2 = tile, class:^dev\.warp\.Warp$
@@ -102,3 +111,32 @@ layerrule = blur, indicator.*
layerrule = ignorealpha 0.6, indicator.*
layerrule = blur, osk[0-9]*
layerrule = ignorealpha 0.6, osk[0-9]*
+
+# Quickshell
+layerrule = blurpopups, quickshell:.*
+layerrule = blur, quickshell:.*
+layerrule = ignorealpha 0.79, quickshell:.*
+layerrule = animation slide, quickshell:bar
+layerrule = animation fade, quickshell:screenCorners
+layerrule = animation slide right, quickshell:sidebarRight
+layerrule = animation slide left, quickshell:sidebarLeft
+layerrule = animation slide bottom, quickshell:osk
+layerrule = animation slide bottom, quickshell:dock
+layerrule = blur, quickshell:session
+layerrule = noanim, quickshell:session
+layerrule = ignorealpha 0, quickshell:session
+layerrule = animation fade, quickshell:notificationPopup
+layerrule = blur, quickshell:backgroundWidgets
+layerrule = ignorealpha 0.05, quickshell:backgroundWidgets
+
+
+# Launchers need to be FAST
+layerrule = noanim, quickshell:overview
+layerrule = noanim, gtk4-layer-shell
+## outfoxxed's stuff
+layerrule = blur, shell:bar
+layerrule = ignorezero, shell:bar
+layerrule = blur, shell:notifications
+layerrule = ignorealpha 0.1, shell:notifications
+
+
diff --git a/.local/bin/fuzzel-emoji b/.config/hypr/hyprland/scripts/fuzzel-emoji.sh
similarity index 99%
rename from .local/bin/fuzzel-emoji
rename to .config/hypr/hyprland/scripts/fuzzel-emoji.sh
index a7d9291a1..7a1b9e1b4 100755
--- a/.local/bin/fuzzel-emoji
+++ b/.config/hypr/hyprland/scripts/fuzzel-emoji.sh
@@ -1,10 +1,26 @@
#!/bin/bash
-if [ $? -eq 0 ]
-then
- sed '1,/^### DATA ###$/d' $0 | fuzzel --match-mode fzf --dmenu | cut -d ' ' -f 1 | tr -d '\n' | wl-copy
-else
- sed '1,/^### DATA ###$/d' $0 | fuzzel --match-mode fzf --dmenu | cut -d ' ' -f 1 | tr -d '\n' | wl-copy
-fi
+set -euo pipefail
+
+MODE="${1:-type}"
+
+emoji="$(sed '1,/^### DATA ###$/d' "$0" | fuzzel --match-mode fzf --dmenu | cut -d ' ' -f 1 | tr -d '\n')"
+
+case "$MODE" in
+ type)
+ wtype "${emoji}" || wl-copy "${emoji}"
+ ;;
+ copy)
+ wl-copy "${emoji}"
+ ;;
+ both)
+ wtype "${emoji}" || true
+ wl-copy "${emoji}"
+ ;;
+ *)
+ echo "Usage: $0 [type|copy|both]"
+ exit 1
+ ;;
+esac
exit
### DATA ###
😀 grinning face face smile happy joy :D grin
@@ -1862,7 +1878,7 @@ exit
→ right arrow
↓ down arrow
←↑→↓ all directions up down left right arrows
-AH↗️HA↘️HA↗️HA↘️HA↗️HA↘️HA↗️HA↘️ pekora arrows hahaha rabbit
+AH↗️HA↘️HA↗️HA↘️ pekora arrows hahaha rabbit
• dot circle separator
「」 japanese quote square bracket
¯\_(ツ)_/¯ shrug idk i dont know
diff --git a/.config/hypr/hyprland/scripts/launch_first_available.sh b/.config/hypr/hyprland/scripts/launch_first_available.sh
new file mode 100755
index 000000000..499947a9d
--- /dev/null
+++ b/.config/hypr/hyprland/scripts/launch_first_available.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+for cmd in "$@"; do
+ eval "command -v ${cmd%% *}" >/dev/null 2>&1 || continue
+ eval "$cmd" &
+ exit
+done
+exit 1
diff --git a/.config/ags/scripts/record-script.sh b/.config/hypr/hyprland/scripts/record.sh
similarity index 91%
rename from .config/ags/scripts/record-script.sh
rename to .config/hypr/hyprland/scripts/record.sh
index b838dab62..320d8a28c 100755
--- a/.config/ags/scripts/record-script.sh
+++ b/.config/hypr/hyprland/scripts/record.sh
@@ -18,15 +18,15 @@ mkdir -p "${xdgvideo:-$HOME/Videos}"
cd "${xdgvideo:-$HOME/Videos}" || exit
if pgrep wf-recorder > /dev/null; then
- notify-send "Recording Stopped" "Stopped" -a 'record-script.sh' &
+ notify-send "Recording Stopped" "Stopped" -a 'Recorder' &
pkill wf-recorder &
else
if ! region="$(slurp 2>&1)"; then
- notify-send "Recording Cancelled" "Selection was Cancelled" -a 'record-script.sh'
+ notify-send "Recording Cancelled" "Selection was Cancelled" -a 'Recorder'
exit 1
fi
- notify-send "Starting recording" 'recording_'"$(getdate)"'.mp4' -a 'record-script.sh'
+ notify-send "Starting recording" 'recording_'"$(getdate)"'.mp4' -a 'Recorder'
if [[ "$1" == "--sound" ]]; then
wf-recorder --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --geometry "$region" --audio="$(getaudiooutput)" & disown
elif [[ "$1" == "--fullscreen-sound" ]]; then
diff --git a/.config/ags/scripts/hyprland/workspace_action.sh b/.config/hypr/hyprland/scripts/workspace_action.sh
similarity index 100%
rename from .config/ags/scripts/hyprland/workspace_action.sh
rename to .config/hypr/hyprland/scripts/workspace_action.sh
diff --git a/.config/hypr/hyprland/scripts/zoom.sh b/.config/hypr/hyprland/scripts/zoom.sh
new file mode 100755
index 000000000..4713684e4
--- /dev/null
+++ b/.config/hypr/hyprland/scripts/zoom.sh
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+
+# Controls Hyprland's cursor zoom_factor, clamped between 1.0 and 3.0
+
+# Get current zoom level
+get_zoom() {
+ hyprctl getoption -j cursor:zoom_factor | jq '.float'
+}
+
+# Clamp a value between 1.0 and 3.0
+clamp() {
+ local val="$1"
+ awk "BEGIN {
+ v = $val;
+ if (v < 1.0) v = 1.0;
+ if (v > 3.0) v = 3.0;
+ print v;
+ }"
+}
+
+# Set zoom level
+set_zoom() {
+ local value="$1"
+ clamped=$(clamp "$value")
+ hyprctl keyword cursor:zoom_factor "$clamped"
+}
+
+case "$1" in
+ reset)
+ set_zoom 1.0
+ ;;
+ increase)
+ if [[ -z "$2" ]]; then
+ echo "Usage: $0 increase STEP"
+ exit 1
+ fi
+ current=$(get_zoom)
+ new=$(awk "BEGIN { print $current + $2 }")
+ set_zoom "$new"
+ ;;
+ decrease)
+ if [[ -z "$2" ]]; then
+ echo "Usage: $0 decrease STEP"
+ exit 1
+ fi
+ current=$(get_zoom)
+ new=$(awk "BEGIN { print $current - $2 }")
+ set_zoom "$new"
+ ;;
+ *)
+ echo "Usage: $0 {reset|increase STEP|decrease STEP}"
+ exit 1
+ ;;
+esac
diff --git a/.config/kde-material-you-colors/config.conf b/.config/kde-material-you-colors/config.conf
new file mode 100644
index 000000000..1d63c8405
--- /dev/null
+++ b/.config/kde-material-you-colors/config.conf
@@ -0,0 +1,76 @@
+[CUSTOM]
+# INSTRUCTIONS
+# Run kde-material-you-colors with no arguments from terminal
+# to debug your configuration changing in real time.
+
+# Monitor to get wallpaper from
+# For me main is 0 but second one is 6, play with this to find yours
+# Default is 0
+monitor = 0
+
+# File containing absolute path of an image (Takes precedence over automatic wallpaper detection)
+# Commented by default
+file = /home/end/.local/state/quickshell/user/wallpaper.txt
+
+# List of 7 space separated colors (hex or rgb) to be used for text in pywal/konsole/KSyntaxHighlighting instead of wallpaper ones
+# Accepted values are hex e.g #ff0000 and rgb e.g 255,0,0 colors (rgb is converted to hex)
+# Commented by default
+# Example using catppuccin color scheme:
+custom_colors_list = #ED8796 #A6DA95 #EED49F #8AADF4 #F5BDE6 #8BD5CA #f5a97f
+
+# Enable Light mode
+# Accepted values are True or False
+# Commented by default to follow System Color Setting (Material You Light/Dark only)
+# NOTE:
+# Will fallback to dark mode if not defined here or enabled in Settings
+#light = False
+
+# Alternative color mode (default is 0), some images return more than one color, this will use either the matched or last color
+# Default is 0
+ncolor = 0
+
+# Light scheme icons theme
+#iconslight = OneUI-light
+iconslight = breeze-plus
+
+# Dark scheme icons theme
+#iconsdark = OneUI-dark
+iconsdark = breeze-plus-dark
+
+# Use pywal to theme other programs using Material You colors
+pywal=False
+
+# The amount of perceptible color for backgrounds in dark mode
+# A number between 0 and 4.0 (limited for accessibility purposes)
+# Defaults to 1 if not set
+#light_blend_multiplier = 1.0
+
+# The amount of perceptible color for backgrounds in dark mode
+# A number between 0 and 4.0 (limited for accessibility purposes)
+# Defaults to 1 if not set
+#dark_blend_multiplier = 1.0
+
+# A script/command that will be executed on start or wallpaper/dark/light/settings change
+# example below using https://github.com/vlevit/notify-send.sh to send a desktop notification:
+#on_change_hook = notify-send.sh "kde-material-you-colors" "This is a test" -t 2000
+
+# Scheme Variant
+# Changes between Material You scheme variants (0-8)
+# 0 = Content
+# 1 = Expressive
+# 2 = Fidelity
+# 3 = Monochrome
+# 4 = Neutral
+# 5 = TonalSpot
+# 6 = Vibrant
+# 7 = Rainbow
+# 8 = FruitSalad
+# Default is 5
+scheme_variant = 5
+
+# Colorfulness
+chroma_multiplier = 1
+
+# Brightness
+# An integer between 0.5 and 1.5
+tone_multiplier = 1
diff --git a/.config/kdeglobals b/.config/kdeglobals
new file mode 100644
index 000000000..c46bd632f
--- /dev/null
+++ b/.config/kdeglobals
@@ -0,0 +1,180 @@
+[ColorEffects:Disabled]
+ChangeSelectionColor=
+Color=#211f24
+ColorAmount=0.5
+ColorEffect=3
+ContrastAmount=0
+ContrastEffect=0
+Enable=
+IntensityAmount=0
+IntensityEffect=0
+
+[ColorEffects:Inactive]
+ChangeSelectionColor=true
+Color=#0c0a10
+ColorAmount=0.025
+ColorEffect=0
+ContrastAmount=0.1
+ContrastEffect=0
+Enable=true
+IntensityAmount=0
+IntensityEffect=0
+
+[Colors:Button]
+BackgroundAlternate=#47434c
+BackgroundNormal=#2b292f
+DecorationFocus=#cdb9fb
+DecorationHover=#cdb9fb
+ForegroundActive=#e6e0e9
+ForegroundInactive=#948f99
+ForegroundLink=#8fc9fc
+ForegroundNegative=#ffb3b4
+ForegroundNeutral=#fcb38a
+ForegroundNormal=#e6e0e9
+ForegroundPositive=#00e479
+ForegroundVisited=#ebb2ff
+
+[Colors:Complementary]
+BackgroundAlternate=#121016
+BackgroundNormal=#211f24
+DecorationFocus=#cdb9fb
+DecorationHover=#cdb9fb
+ForegroundActive=#e6e0e9
+ForegroundInactive=#948f99
+ForegroundLink=#8fc9fc
+ForegroundNegative=#ffb3b4
+ForegroundNeutral=#fcb38a
+ForegroundNormal=#cac4cf
+ForegroundPositive=#00e479
+ForegroundVisited=#ebb2ff
+
+[Colors:Header]
+BackgroundAlternate=#211f24
+BackgroundNormal=#211f24
+DecorationFocus=#cdb9fb
+DecorationHover=#cdb9fb
+ForegroundActive=#e6e0e9
+ForegroundInactive=#948f99
+ForegroundLink=#8fc9fc
+ForegroundNegative=#ffb3b4
+ForegroundNeutral=#fcb38a
+ForegroundNormal=#cac4cf
+ForegroundPositive=#00e479
+ForegroundVisited=#ebb2ff
+
+[Colors:Header][Inactive]
+BackgroundAlternate=#211f24
+BackgroundNormal=#211f24
+DecorationFocus=#cdb9fb
+DecorationHover=#cdb9fb
+ForegroundActive=#e6e0e9
+ForegroundInactive=#948f99
+ForegroundLink=#8fc9fc
+ForegroundNegative=#ffb3b4
+ForegroundNeutral=#fcb38a
+ForegroundNormal=#cac4cf
+ForegroundPositive=#00e479
+ForegroundVisited=#ebb2ff
+
+[Colors:Selection]
+BackgroundAlternate=#cdb9fb
+BackgroundNormal=#cdb9fb
+DecorationFocus=#cdb9fb
+DecorationHover=#c9bfd8
+ForegroundActive=#36265d
+ForegroundInactive=#36265d
+ForegroundLink=#004b73
+ForegroundNegative=#920023
+ForegroundNeutral=#753400
+ForegroundNormal=#36265d
+ForegroundPositive=#005228
+ForegroundVisited=#74009f
+
+[Colors:Tooltip]
+BackgroundAlternate=#47434c
+BackgroundNormal=#211f24
+DecorationFocus=#cdb9fb
+DecorationHover=#cdb9fb
+ForegroundActive=#e6e0e9
+ForegroundInactive=#948f99
+ForegroundLink=#8fc9fc
+ForegroundNegative=#ffb3b4
+ForegroundNeutral=#fcb38a
+ForegroundNormal=#e6e0e9
+ForegroundPositive=#00e479
+ForegroundVisited=#ebb2ff
+
+[Colors:View]
+BackgroundAlternate=#211f24
+BackgroundNormal=#121016
+DecorationFocus=#cdb9fb
+DecorationHover=#65558f
+ForegroundActive=#e6e0e9
+ForegroundInactive=#948f99
+ForegroundLink=#8fc9fc
+ForegroundNegative=#ffb3b4
+ForegroundNeutral=#fcb38a
+ForegroundNormal=#e6e0e9
+ForegroundPositive=#00e479
+ForegroundVisited=#ebb2ff
+
+[Colors:Window]
+BackgroundAlternate=#47434c
+BackgroundNormal=#211f24
+DecorationFocus=#cdb9fb
+DecorationHover=#cdb9fb
+ForegroundActive=#8fc9fc
+ForegroundInactive=#948f99
+ForegroundLink=#8fc9fc
+ForegroundNegative=#ffb3b4
+ForegroundNeutral=#fcb38a
+ForegroundNormal=#cac4cf
+ForegroundPositive=#00e479
+ForegroundVisited=#ebb2ff
+
+[General]
+ColorScheme=MaterialYouDark
+ColorSchemeHash=3c0cecefbea43cdb8fe3da156e4a106f7384a526
+LastUsedCustomAccentColor=184,117,220
+XftHintStyle=hintslight
+XftSubPixel=none
+fixed=JetBrainsMono Nerd Font,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
+font=Rubik,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
+menuFont=Rubik,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
+smallestReadableFont=Rubik,9,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
+toolBarFont=Rubik,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
+
+[Icons]
+Theme=breeze-dark
+
+[KFileDialog Settings]
+Allow Expansion=false
+Automatically select filename extension=true
+Breadcrumb Navigation=true
+Decoration position=2
+LocationCombo Completionmode=5
+PathCombo Completionmode=5
+Show Bookmarks=false
+Show Full Path=false
+Show Inline Previews=true
+Show Preview=false
+Show Speedbar=true
+Show hidden files=false
+Sort by=Name
+Sort directories first=true
+Sort hidden files last=false
+Sort reversed=false
+Speedbar Width=168
+View Style=DetailTree
+
+[Sounds]
+Theme=freedesktop
+
+[WM]
+activeBackground=54,52,58
+activeBlend=252,252,252
+activeFont=Rubik,10,-1,5,400,0,0,0,0,0,0,0,0,0,0,1
+activeForeground=230,224,233
+inactiveBackground=76,70,90
+inactiveBlend=161,169,177
+inactiveForeground=232,222,248
diff --git a/.config/kitty/kitty.conf b/.config/kitty/kitty.conf
index be6c0e5bc..008bbbe98 100644
--- a/.config/kitty/kitty.conf
+++ b/.config/kitty/kitty.conf
@@ -1,9 +1,10 @@
# Font
-font_family SpaceMono Nerd Font
+font_family JetBrains Mono Nerd Font
font_size 11.0
-# Cursor shape
+# Cursor
cursor_shape beam
+cursor_trail 1
# Padding (why weird value? consistency with foot)
window_margin_width 21.75
@@ -14,17 +15,22 @@ confirm_os_window_close 0
# Use fish shell
shell fish
-# Copy for normies
-map ctrl+c copy_or_interrupt
+# Copy
+map ctrl+c copy_or_interrupt
+
+# Search
+map ctrl+f launch --location=hsplit --allow-remote-control kitty +kitten search.py @active-kitty-window-id
+map kitty_mod+f launch --location=hsplit --allow-remote-control kitty +kitten search.py @active-kitty-window-id
+
+# Scroll & Zoom
+map page_up scroll_page_up
+map page_down scroll_page_down
-# Zoom
map ctrl+plus change_font_size all +1
map ctrl+equal change_font_size all +1
map ctrl+kp_add change_font_size all +1
-
map ctrl+minus change_font_size all -1
map ctrl+underscore change_font_size all -1
map ctrl+kp_subtract change_font_size all -1
-
map ctrl+0 change_font_size all 0
map ctrl+kp_0 change_font_size all 0
diff --git a/.config/kitty/scroll_mark.py b/.config/kitty/scroll_mark.py
new file mode 100644
index 000000000..0193b20b3
--- /dev/null
+++ b/.config/kitty/scroll_mark.py
@@ -0,0 +1,18 @@
+from kittens.tui.handler import result_handler
+from kitty.boss import Boss
+
+
+def main(args: list[str]) -> None:
+ pass
+
+
+@result_handler(no_ui=True)
+def handle_result(
+ args: list[str], answer: str, target_window_id: int, boss: Boss
+) -> None:
+ w = boss.window_id_map.get(target_window_id)
+ if w is not None:
+ if len(args) > 1 and args[1] != "prev":
+ w.scroll_to_mark(prev=False)
+ else:
+ w.scroll_to_mark()
diff --git a/.config/kitty/search.py b/.config/kitty/search.py
new file mode 100644
index 000000000..1fd554d1f
--- /dev/null
+++ b/.config/kitty/search.py
@@ -0,0 +1,341 @@
+# Kitty search from https://github.com/trygveaa/kitty-kitten-search
+# License: GPLv3
+
+import json
+import re
+import subprocess
+from gettext import gettext as _
+from pathlib import Path
+from subprocess import PIPE, run
+
+from kittens.tui.handler import Handler
+from kittens.tui.line_edit import LineEdit
+from kittens.tui.loop import Loop
+from kittens.tui.operations import (
+ clear_screen,
+ cursor,
+ set_line_wrapping,
+ set_window_title,
+ styled,
+)
+from kitty.config import cached_values_for
+from kitty.key_encoding import EventType
+from kitty.typing_compat import KeyEventType, ScreenSize
+
+NON_SPACE_PATTERN = re.compile(r"\S+")
+SPACE_PATTERN = re.compile(r"\s+")
+SPACE_PATTERN_END = re.compile(r"\s+$")
+SPACE_PATTERN_START = re.compile(r"^\s+")
+
+NON_ALPHANUM_PATTERN = re.compile(r"[^\w\d]+")
+NON_ALPHANUM_PATTERN_END = re.compile(r"[^\w\d]+$")
+NON_ALPHANUM_PATTERN_START = re.compile(r"^[^\w\d]+")
+ALPHANUM_PATTERN = re.compile(r"[\w\d]+")
+
+
+def call_remote_control(args: list[str]) -> None:
+ subprocess.run(["kitty", "@", *args], capture_output=True)
+
+
+def reindex(
+ text: str, pattern: re.Pattern[str], right: bool = False
+) -> tuple[int, int]:
+ if not right:
+ m = pattern.search(text)
+ else:
+ matches = [x for x in pattern.finditer(text) if x]
+ if not matches:
+ raise ValueError
+ m = matches[-1]
+
+ if not m:
+ raise ValueError
+
+ return m.span()
+
+
+SCROLLMARK_FILE = Path(__file__).parent.absolute() / "scroll_mark.py"
+
+
+class Search(Handler):
+ def __init__(
+ self, cached_values: dict[str, str], window_ids: list[int], error: str = ""
+ ) -> None:
+ self.cached_values = cached_values
+ self.window_ids = window_ids
+ self.error = error
+ self.line_edit = LineEdit()
+ last_search = cached_values.get("last_search", "")
+ self.line_edit.add_text(last_search)
+ self.text_marked = bool(last_search)
+ self.mode = cached_values.get("mode", "text")
+ self.update_prompt()
+ self.mark()
+
+ def update_prompt(self) -> None:
+ self.prompt = "~> " if self.mode == "regex" else "=> "
+
+ def init_terminal_state(self) -> None:
+ self.write(set_line_wrapping(False))
+ self.write(set_window_title(_("Search")))
+
+ def initialize(self) -> None:
+ self.init_terminal_state()
+ self.draw_screen()
+
+ def draw_screen(self) -> None:
+ self.write(clear_screen())
+ if self.window_ids:
+ input_text = self.line_edit.current_input
+ if self.text_marked:
+ self.line_edit.current_input = styled(input_text, reverse=True)
+ self.line_edit.write(self.write, self.prompt)
+ self.line_edit.current_input = input_text
+ if self.error:
+ with cursor(self.write):
+ self.print("")
+ for l in self.error.split("\n"):
+ self.print(l)
+
+ def refresh(self) -> None:
+ self.draw_screen()
+ self.mark()
+
+ def switch_mode(self) -> None:
+ if self.mode == "regex":
+ self.mode = "text"
+ else:
+ self.mode = "regex"
+ self.cached_values["mode"] = self.mode
+ self.update_prompt()
+
+ def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
+ if self.text_marked:
+ self.text_marked = False
+ self.line_edit.clear()
+ self.line_edit.on_text(text, in_bracketed_paste)
+ self.refresh()
+
+ def on_key(self, key_event: KeyEventType) -> None:
+ if (
+ self.text_marked
+ and key_event.type == EventType.PRESS
+ and key_event.key
+ not in [
+ "TAB",
+ "LEFT_CONTROL",
+ "RIGHT_CONTROL",
+ "LEFT_ALT",
+ "RIGHT_ALT",
+ "LEFT_SHIFT",
+ "RIGHT_SHIFT",
+ "LEFT_SUPER",
+ "RIGHT_SUPER",
+ ]
+ ):
+ self.text_marked = False
+ self.refresh()
+
+ if self.line_edit.on_key(key_event):
+ self.refresh()
+ return
+
+ if key_event.matches("ctrl+u"):
+ self.line_edit.clear()
+ self.refresh()
+ elif key_event.matches("ctrl+a"):
+ self.line_edit.home()
+ self.refresh()
+ elif key_event.matches("ctrl+e"):
+ self.line_edit.end()
+ self.refresh()
+ elif key_event.matches("ctrl+backspace") or key_event.matches("ctrl+w"):
+ before, _ = self.line_edit.split_at_cursor()
+
+ try:
+ start, _ = reindex(before, SPACE_PATTERN_END, right=True)
+ except ValueError:
+ start = -1
+
+ try:
+ space = before[:start].rindex(" ")
+ except ValueError:
+ space = 0
+ self.line_edit.backspace(len(before) - space)
+ self.refresh()
+ elif key_event.matches("ctrl+left") or key_event.matches("ctrl+b"):
+ before, _ = self.line_edit.split_at_cursor()
+ try:
+ start, _ = reindex(before, SPACE_PATTERN_END, right=True)
+ except ValueError:
+ start = -1
+
+ try:
+ space = before[:start].rindex(" ")
+ except ValueError:
+ space = 0
+ self.line_edit.left(len(before) - space)
+ self.refresh()
+ elif key_event.matches("ctrl+right") or key_event.matches("ctrl+f"):
+ _, after = self.line_edit.split_at_cursor()
+ try:
+ _, end = reindex(after, SPACE_PATTERN_START)
+ except ValueError:
+ end = 0
+
+ try:
+ space = after[end:].index(" ") + 1
+ except ValueError:
+ space = len(after)
+ self.line_edit.right(space)
+ self.refresh()
+ elif key_event.matches("alt+backspace") or key_event.matches("alt+w"):
+ before, _ = self.line_edit.split_at_cursor()
+
+ try:
+ start, _ = reindex(before, NON_ALPHANUM_PATTERN_END, right=True)
+ except ValueError:
+ start = -1
+ else:
+ self.line_edit.backspace(len(before) - start)
+ self.refresh()
+ return
+
+ try:
+ start, _ = reindex(before, NON_ALPHANUM_PATTERN, right=True)
+ except ValueError:
+ self.line_edit.backspace(len(before))
+ self.refresh()
+ return
+
+ self.line_edit.backspace(len(before) - (start + 1))
+ self.refresh()
+ elif key_event.matches("alt+left") or key_event.matches("alt+b"):
+ before, _ = self.line_edit.split_at_cursor()
+
+ try:
+ start, _ = reindex(before, NON_ALPHANUM_PATTERN_END, right=True)
+ except ValueError:
+ start = -1
+ else:
+ self.line_edit.left(len(before) - start)
+ self.refresh()
+ return
+
+ try:
+ start, _ = reindex(before, NON_ALPHANUM_PATTERN, right=True)
+ except ValueError:
+ self.line_edit.left(len(before))
+ self.refresh()
+ return
+
+ self.line_edit.left(len(before) - (start + 1))
+ self.refresh()
+ elif key_event.matches("alt+right") or key_event.matches("alt+f"):
+ _, after = self.line_edit.split_at_cursor()
+
+ try:
+ _, end = reindex(after, NON_ALPHANUM_PATTERN_START)
+ except ValueError:
+ end = 0
+ else:
+ self.line_edit.right(end)
+ self.refresh()
+ return
+
+ try:
+ _, end = reindex(after, NON_ALPHANUM_PATTERN)
+ except ValueError:
+ self.line_edit.right(len(after))
+ self.refresh()
+ return
+
+ self.line_edit.right(end - 1)
+ self.refresh()
+ elif key_event.matches("tab"):
+ self.switch_mode()
+ self.refresh()
+ elif key_event.matches("up") or key_event.matches("f3"):
+ for match_arg in self.match_args():
+ call_remote_control(["kitten", match_arg, str(SCROLLMARK_FILE)])
+ elif key_event.matches("down") or key_event.matches("shift+f3"):
+ for match_arg in self.match_args():
+ call_remote_control(["kitten", match_arg, str(SCROLLMARK_FILE), "next"])
+ elif key_event.matches("enter"):
+ self.quit(0)
+ elif key_event.matches("esc"):
+ self.quit(1)
+
+ def on_interrupt(self) -> None:
+ self.quit(1)
+
+ def on_eot(self) -> None:
+ self.quit(1)
+
+ def on_resize(self, screen_size: ScreenSize) -> None:
+ self.refresh()
+
+ def match_args(self) -> list[str]:
+ return [f"--match=id:{window_id}" for window_id in self.window_ids]
+
+ def mark(self) -> None:
+ if not self.window_ids:
+ return
+ text = self.line_edit.current_input
+ if text:
+ match_case = "i" if text.islower() else ""
+ match_type = match_case + self.mode
+ for match_arg in self.match_args():
+ try:
+ call_remote_control(
+ ["create-marker", match_arg, match_type, "1", text]
+ )
+ except SystemExit:
+ self.remove_mark()
+ else:
+ self.remove_mark()
+
+ def remove_mark(self) -> None:
+ for match_arg in self.match_args():
+ call_remote_control(["remove-marker", match_arg])
+
+ def quit(self, return_code: int) -> None:
+ self.cached_values["last_search"] = self.line_edit.current_input
+ self.remove_mark()
+ if return_code:
+ for match_arg in self.match_args():
+ call_remote_control(["scroll-window", match_arg, "end"])
+ self.quit_loop(return_code)
+
+
+def main(args: list[str]) -> None:
+ call_remote_control(
+ ["resize-window", "--self", "--axis=vertical", "--increment", "-100"]
+ )
+
+ error = ""
+ if len(args) < 2 or not args[1].isdigit():
+ error = "Error: Window id must be provided as the first argument."
+
+ window_id = int(args[1])
+ window_ids = [window_id]
+ if len(args) > 2 and args[2] == "--all-windows":
+ ls_output = run(["kitty", "@", "ls"], stdout=PIPE)
+ ls_json = json.loads(ls_output.stdout.decode())
+ current_tab = None
+ for os_window in ls_json:
+ for tab in os_window["tabs"]:
+ for kitty_window in tab["windows"]:
+ if kitty_window["id"] == window_id:
+ current_tab = tab
+ if current_tab:
+ window_ids = [
+ w["id"] for w in current_tab["windows"] if not w["is_focused"]
+ ]
+ else:
+ error = "Error: Could not find the window id provided."
+
+ loop = Loop()
+ with cached_values_for("search") as cached_values:
+ handler = Search(cached_values, window_ids, error)
+ loop.loop(handler)
diff --git a/.config/matugen/config.toml b/.config/matugen/config.toml
new file mode 100644
index 000000000..66c17a37e
--- /dev/null
+++ b/.config/matugen/config.toml
@@ -0,0 +1,48 @@
+[config]
+version_check = false
+
+[templates.m3colors]
+input_path = '~/.config/matugen/templates/colors.json'
+output_path = '~/.local/state/quickshell/user/generated/colors.json'
+
+[templates.hyprland]
+input_path = '~/.config/matugen/templates/hyprland/colors.conf'
+output_path = '~/.config/hypr/hyprland/colors.conf'
+
+[templates.hyprlock]
+input_path = '~/.config/matugen/templates/hyprland/hyprlock.conf'
+output_path = '~/.config/hypr/hyprlock.conf'
+
+[templates.fuzzel]
+input_path = '~/.config/matugen/templates/fuzzel/fuzzel_theme.ini'
+output_path = '~/.config/fuzzel/fuzzel_theme.ini'
+
+[templates.gtk3]
+input_path = '~/.config/matugen/templates/gtk/gtk-colors.css'
+output_path = '~/.config/gtk-3.0/gtk.css'
+
+[templates.gtk4]
+input_path = '~/.config/matugen/templates/gtk/gtk-colors.css'
+output_path = '~/.config/gtk-4.0/gtk.css'
+
+[templates.ags_source_view]
+input_path = '~/.config/matugen/templates/ags/sourceviewtheme.xml'
+output_path = '~/.config/ags/assets/themes/sourceviewtheme.xml'
+
+[templates.ags_source_view_light]
+input_path = '~/.config/matugen/templates/ags/sourceviewtheme-light.xml'
+output_path = '~/.config/ags/assets/themes/sourceviewtheme-light.xml'
+
+[templates.ags_scss]
+input_path = '~/.config/matugen/templates/ags/_material.scss'
+output_path = '~/.local/state/ags/scss/_material.scss'
+post_hook = 'pidof gjs && agsv1 run-js "handleStyles(false)"'
+
+[templates.kde_colors]
+input_path = '~/.config/matugen/templates/kde/color.txt'
+output_path = '~/.local/state/quickshell/user/generated/color.txt'
+post_hook = '~/.config/matugen/templates/kde/kde-material-you-colors-wrapper.sh'
+
+[templates.wallpaper]
+input_path = '~/.config/matugen/templates/wallpaper.txt'
+output_path = '~/.local/state/quickshell/user/generated/wallpaper/path.txt'
diff --git a/.config/matugen/scripts/least_busy_region.py b/.config/matugen/scripts/least_busy_region.py
new file mode 100755
index 000000000..a1f2f47b4
--- /dev/null
+++ b/.config/matugen/scripts/least_busy_region.py
@@ -0,0 +1,338 @@
+#!/usr/bin/env python3
+# Disclaimer: This script was ai-generated and went through minimal revision.
+
+import os
+os.environ["OPENCV_LOG_LEVEL"] = "SILENT"
+import cv2
+import numpy as np
+import argparse
+import json
+
+def center_crop(img, target_w, target_h):
+ h, w = img.shape[:2]
+ if w == target_w and h == target_h:
+ return img
+ x1 = max(0, (w - target_w) // 2)
+ y1 = max(0, (h - target_h) // 2)
+ x2 = x1 + target_w
+ y2 = y1 + target_h
+ return img[y1:y2, x1:x2]
+
+def find_least_busy_region(image_path, region_width=300, region_height=200, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", padding=50):
+ img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
+ if img is None:
+ raise FileNotFoundError(f"Image not found: {image_path}")
+ orig_h, orig_w = img.shape
+ scale = 1.0
+ if screen_width is not None and screen_height is not None:
+ scale_w = screen_width / orig_w
+ scale_h = screen_height / orig_h
+ if screen_mode == "fill":
+ scale = max(scale_w, scale_h)
+ else:
+ scale = min(scale_w, scale_h)
+ new_w = int(orig_w * scale)
+ new_h = int(orig_h * scale)
+ if verbose:
+ print(f"Scaling image from {orig_w}x{orig_h} to {new_w}x{new_h} (scale: {scale:.3f}, mode: {screen_mode})")
+ img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
+ img = center_crop(img, screen_width, screen_height)
+ if verbose:
+ print(f"Cropped image to {screen_width}x{screen_height}")
+ else:
+ if verbose:
+ print(f"Using original image size: {orig_w}x{orig_h}")
+ arr = img.astype(np.float64)
+ h, w = arr.shape
+ # Use OpenCV's integral for fast computation
+ integral = cv2.integral(arr, sdepth=cv2.CV_64F)[1:,1:]
+ integral_sq = cv2.integral(arr**2, sdepth=cv2.CV_64F)[1:,1:]
+ def region_sum(ii, x1, y1, x2, y2):
+ total = ii[y2, x2]
+ if x1 > 0:
+ total -= ii[y2, x1-1]
+ if y1 > 0:
+ total -= ii[y1-1, x2]
+ if x1 > 0 and y1 > 0:
+ total += ii[y1-1, x1-1]
+ return total
+ min_var = None
+ min_coords = (0, 0)
+ area = region_width * region_height
+ x_start = padding
+ y_start = padding
+ x_end = w - region_width - padding + 1
+ y_end = h - region_height - padding + 1
+ for y in range(y_start, max(y_end, y_start+1), stride):
+ for x in range(x_start, max(x_end, x_start+1), stride):
+ x1, y1 = x, y
+ x2, y2 = x + region_width - 1, y + region_height - 1
+ s = region_sum(integral, x1, y1, x2, y2)
+ s2 = region_sum(integral_sq, x1, y1, x2, y2)
+ mean = s / area
+ var = (s2 / area) - (mean ** 2)
+ if (min_var is None) or (var < min_var):
+ min_var = var
+ min_coords = (x, y)
+ return min_coords, min_var
+
+def find_largest_region(image_path, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", threshold=100.0, aspect_ratio=1.0, padding=50):
+ img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
+ if img is None:
+ raise FileNotFoundError(f"Image not found: {image_path}")
+ orig_h, orig_w = img.shape
+ scale = 1.0
+ if screen_width is not None and screen_height is not None:
+ scale_w = screen_width / orig_w
+ scale_h = screen_height / orig_h
+ if screen_mode == "fill":
+ scale = max(scale_w, scale_h)
+ else:
+ scale = min(scale_w, scale_h)
+ new_w = int(orig_w * scale)
+ new_h = int(orig_h * scale)
+ if verbose:
+ print(f"Scaling image from {orig_w}x{orig_h} to {new_w}x{new_h} (scale: {scale:.3f}, mode: {screen_mode})")
+ img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
+ img = center_crop(img, screen_width, screen_height)
+ if verbose:
+ print(f"Cropped image to {screen_width}x{screen_height}")
+ else:
+ if verbose:
+ print(f"Using original image size: {orig_w}x{orig_h}")
+ arr = img.astype(np.float64)
+ h, w = arr.shape
+ # Use OpenCV's integral for fast computation
+ integral = cv2.integral(arr, sdepth=cv2.CV_64F)[1:,1:]
+ integral_sq = cv2.integral(arr**2, sdepth=cv2.CV_64F)[1:,1:]
+ def region_sum(ii, x1, y1, x2, y2):
+ total = ii[y2, x2]
+ if x1 > 0:
+ total -= ii[y2, x1-1]
+ if y1 > 0:
+ total -= ii[y1-1, x2]
+ if x1 > 0 and y1 > 0:
+ total += ii[y1-1, x1-1]
+ return total
+ min_size = 10
+ max_size = min(h, int(w / aspect_ratio)) if aspect_ratio >= 1.0 else min(int(h * aspect_ratio), w)
+ best = None
+ best_size = min_size
+ while min_size <= max_size:
+ mid = (min_size + max_size) // 2
+ if aspect_ratio >= 1.0:
+ region_h = mid
+ region_w = int(mid * aspect_ratio)
+ else:
+ region_w = mid
+ region_h = int(mid / aspect_ratio)
+ if region_w > w or region_h > h:
+ max_size = mid - 1
+ continue
+ found = False
+ x_start = padding
+ y_start = padding
+ x_end = w - region_w - padding + 1
+ y_end = h - region_h - padding + 1
+ for y in range(y_start, max(y_end, y_start+1), stride):
+ for x in range(x_start, max(x_end, x_start+1), stride):
+ x1, y1 = x, y
+ x2, y2 = x + region_w - 1, y + region_h - 1
+ s = region_sum(integral, x1, y1, x2, y2)
+ s2 = region_sum(integral_sq, x1, y1, x2, y2)
+ area = region_w * region_h
+ mean = s / area
+ var = (s2 / area) - (mean ** 2)
+ if var <= threshold:
+ found = True
+ best = (x, y, region_w, region_h, var)
+ break
+ if found:
+ break
+ if found:
+ best_size = mid
+ min_size = mid + 1
+ else:
+ max_size = mid - 1
+ if best:
+ x, y, region_w, region_h, var = best
+ center_x = x + region_w // 2
+ center_y = y + region_h // 2
+ return (center_x, center_y), (region_w, region_h), var
+ else:
+ return None, (0, 0), None
+
+def draw_region(image_path, coords, region_width=300, region_height=200, output_path='output.png', screen_width=None, screen_height=None, screen_mode="fill"):
+ img = cv2.imread(image_path)
+ if img is None:
+ raise FileNotFoundError(f"Image not found: {image_path}")
+ orig_h, orig_w = img.shape[:2]
+ if screen_width is not None and screen_height is not None:
+ scale_w = screen_width / orig_w
+ scale_h = screen_height / orig_h
+ if screen_mode == "fill":
+ scale = max(scale_w, scale_h)
+ else:
+ scale = min(scale_w, scale_h)
+ new_w = int(orig_w * scale)
+ new_h = int(orig_h * scale)
+ img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
+ img = center_crop(img, screen_width, screen_height)
+ x, y = coords
+ cv2.rectangle(img, (x, y), (x+region_width-1, y+region_height-1), (0,0,255), 3)
+ cv2.imwrite(output_path, img)
+ print(f"Saved output image with rectangle at {output_path}")
+
+def draw_largest_region(image_path, center, size, output_path='output.png', screen_width=None, screen_height=None, screen_mode="fill"):
+ img = cv2.imread(image_path)
+ if img is None:
+ raise FileNotFoundError(f"Image not found: {image_path}")
+ orig_h, orig_w = img.shape[:2]
+ if screen_width is not None and screen_height is not None:
+ scale_w = screen_width / orig_w
+ scale_h = screen_height / orig_h
+ if screen_mode == "fill":
+ scale = max(scale_w, scale_h)
+ else:
+ scale = min(scale_w, scale_h)
+ new_w = int(orig_w * scale)
+ new_h = int(orig_h * scale)
+ img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
+ img = center_crop(img, screen_width, screen_height)
+ cx, cy = center
+ region_w, region_h = size
+ x1 = cx - region_w // 2
+ y1 = cy - region_h // 2
+ x2 = cx + region_w // 2 - 1
+ y2 = cy + region_h // 2 - 1
+ cv2.rectangle(img, (x1, y1), (x2, y2), (255,0,0), 3)
+ cv2.imwrite(output_path, img)
+ print(f"Saved output image with largest region at {output_path}")
+
+def get_dominant_color(image_path, x, y, w, h, screen_width=None, screen_height=None, screen_mode="fill"):
+ img = cv2.imread(image_path)
+ if img is None:
+ raise FileNotFoundError(f"Image not found: {image_path}")
+ orig_h, orig_w = img.shape[:2]
+ if screen_width is not None and screen_height is not None:
+ scale_w = screen_width / orig_w
+ scale_h = screen_height / orig_h
+ if screen_mode == "fill":
+ scale = max(scale_w, scale_h)
+ else:
+ scale = min(scale_w, scale_h)
+ new_w = int(orig_w * scale)
+ new_h = int(orig_h * scale)
+ img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
+ img = center_crop(img, screen_width, screen_height)
+ # Ensure region is within bounds
+ x = max(0, x)
+ y = max(0, y)
+ w = max(1, min(w, img.shape[1] - x))
+ h = max(1, min(h, img.shape[0] - y))
+ region = img[y:y+h, x:x+w]
+ if region.size == 0 or region.shape[0] == 0 or region.shape[1] == 0:
+ return [0, 0, 0]
+ region = region.reshape((-1, 3))
+ # Filter out black pixels (optional, improves accuracy for some images)
+ non_black = region[np.any(region > 10, axis=1)]
+ if non_black.shape[0] == 0:
+ non_black = region
+ region = np.float32(non_black)
+ if region.shape[0] < 3:
+ return [int(x) for x in np.mean(region, axis=0)]
+ # K-means to find dominant color
+ criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
+ K = min(3, region.shape[0])
+ _, labels, centers = cv2.kmeans(region, K, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
+ counts = np.bincount(labels.flatten())
+ dominant = centers[np.argmax(counts)]
+ return [int(x) for x in dominant]
+
+def main():
+ parser = argparse.ArgumentParser(description="Find least busy region in an image and output a JSON. Made for determining a suitable position for a wallpaper widget.")
+ parser.add_argument("image_path", help="Path to the input image")
+ parser.add_argument("--width", type=int, default=300, help="Region width")
+ parser.add_argument("--height", type=int, default=200, help="Region height")
+ parser.add_argument("-v", "--visual-output", action="store_true", help="Output image with rectangle")
+ parser.add_argument("--screen-width", type=int, default=1920, help="Screen width for wallpaper scaling")
+ parser.add_argument("--screen-height", type=int, default=1080, help="Screen height for wallpaper scaling")
+ parser.add_argument("--stride", type=int, default=4, help="Step size for sliding window (higher is faster, less precise)")
+ parser.add_argument("--screen-mode", choices=["fill", "fit"], default="fill", help="Wallpaper scaling mode: 'fill' (default) or 'fit'")
+ parser.add_argument("--verbose", action="store_true", help="Print verbose output")
+ parser.add_argument("-l", "--largest-region", action="store_true", help="Find the largest region under the variance threshold and output its center")
+ parser.add_argument("-t", "--variance-threshold", type=float, default=1000.0, help="Variance threshold for largest region mode")
+ parser.add_argument("--aspect-ratio", type=float, default=1.78, help="Aspect ratio (width/height) for largest region mode")
+ parser.add_argument("--padding", type=int, default=50, help="Minimum distance from region to image edge (default: 50)")
+ args = parser.parse_args()
+
+ if args.largest_region:
+ center, size, var = find_largest_region(
+ args.image_path,
+ screen_width=args.screen_width,
+ screen_height=args.screen_height,
+ verbose=args.verbose,
+ stride=args.stride,
+ screen_mode=args.screen_mode,
+ threshold=args.variance_threshold,
+ aspect_ratio=args.aspect_ratio,
+ padding=args.padding
+ )
+ if center:
+ if args.visual_output:
+ draw_largest_region(args.image_path, center, size, screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode)
+ # Extract dominant color
+ cx, cy = center
+ region_w, region_h = size
+ x1 = cx - region_w // 2
+ y1 = cy - region_h // 2
+ dominant_color = get_dominant_color(
+ args.image_path, x1, y1, region_w, region_h,
+ screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode
+ )
+ dominant_color_hex = '#{:02x}{:02x}{:02x}'.format(*dominant_color)
+ print(json.dumps({
+ "center_x": center[0],
+ "center_y": center[1],
+ "width": size[0],
+ "height": size[1],
+ "variance": var,
+ "dominant_color": dominant_color_hex
+ }))
+ else:
+ print(json.dumps({"error": "No region found under the threshold."}))
+ return
+
+ coords, variance = find_least_busy_region(
+ args.image_path,
+ region_width=args.width,
+ region_height=args.height,
+ screen_width=args.screen_width,
+ screen_height=args.screen_height,
+ verbose=args.verbose,
+ stride=args.stride,
+ screen_mode=args.screen_mode,
+ padding=args.padding
+ )
+ if args.visual_output:
+ draw_region(args.image_path, coords, region_width=args.width, region_height=args.height, screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode)
+ # Output JSON with center point
+ center_x = coords[0] + args.width // 2
+ center_y = coords[1] + args.height // 2
+ dominant_color = get_dominant_color(
+ args.image_path, coords[0], coords[1], args.width, args.height,
+ screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode
+ )
+ dominant_color_hex = '#{:02x}{:02x}{:02x}'.format(*dominant_color)
+ print(json.dumps({
+ "center_x": center_x,
+ "center_y": center_y,
+ "width": args.width,
+ "height": args.height,
+ "variance": variance,
+ "dominant_color": dominant_color_hex
+ }))
+
+if __name__ == "__main__":
+ main()
+
diff --git a/.config/matugen/templates/ags/_material.scss b/.config/matugen/templates/ags/_material.scss
new file mode 100644
index 000000000..81acbfec0
--- /dev/null
+++ b/.config/matugen/templates/ags/_material.scss
@@ -0,0 +1,70 @@
+$darkmode: False;
+$transparent: False;
+$background: {{colors.background.default.hex}};
+$onBackground: {{colors.on_background.default.hex}};
+$surface: {{colors.surface.default.hex}};
+$surfaceDim: {{colors.surface_dim.default.hex}};
+$surfaceBright: {{colors.surface_bright.default.hex}};
+$surfaceContainerLowest: {{colors.surface_container_lowest.default.hex}};
+$surfaceContainerLow: {{colors.surface_container_low.default.hex}};
+$surfaceContainer: {{colors.surface_container.default.hex}};
+$surfaceContainerHigh: {{colors.surface_container_high.default.hex}};
+$surfaceContainerHighest: {{colors.surface_container_highest.default.hex}};
+$onSurface: {{colors.on_surface.default.hex}};
+$surfaceVariant: {{colors.surface_variant.default.hex}};
+$onSurfaceVariant: {{colors.on_surface_variant.default.hex}};
+$inverseSurface: {{colors.inverse_surface.default.hex}};
+$inverseOnSurface: {{colors.inverse_on_surface.default.hex}};
+$outline: {{colors.outline.default.hex}};
+$outlineVariant: {{colors.outline_variant.default.hex}};
+$shadow: {{colors.shadow.default.hex}};
+$scrim: {{colors.scrim.default.hex}};
+$primary: {{colors.primary.default.hex}};
+$onPrimary: {{colors.on_primary.default.hex}};
+$primaryContainer: {{colors.primary_container.default.hex}};
+$onPrimaryContainer: {{colors.on_primary_container.default.hex}};
+$inversePrimary: {{colors.inverse_primary.default.hex}};
+$secondary: {{colors.secondary.default.hex}};
+$onSecondary: {{colors.on_secondary.default.hex}};
+$secondaryContainer: {{colors.secondary_container.default.hex}};
+$onSecondaryContainer: {{colors.on_secondary_container.default.hex}};
+$tertiary: {{colors.tertiary.default.hex}};
+$onTertiary: {{colors.on_tertiary.default.hex}};
+$tertiaryContainer: {{colors.tertiary_container.default.hex}};
+$onTertiaryContainer: {{colors.on_tertiary_container.default.hex}};
+$error: {{colors.error.default.hex}};
+$onError: {{colors.on_error.default.hex}};
+$errorContainer: {{colors.error_container.default.hex}};
+$onErrorContainer: {{colors.on_error_container.default.hex}};
+$primaryFixed: {{colors.primary_fixed.default.hex}};
+$primaryFixedDim: {{colors.primary_fixed_dim.default.hex}};
+$onPrimaryFixed: {{colors.on_primary_fixed.default.hex}};
+$onPrimaryFixedVariant: {{colors.on_primary_fixed_variant.default.hex}};
+$secondaryFixed: {{colors.secondary_fixed.default.hex}};
+$secondaryFixedDim: {{colors.secondary_fixed_dim.default.hex}};
+$onSecondaryFixed: {{colors.on_secondary_fixed.default.hex}};
+$onSecondaryFixedVariant: {{colors.on_secondary_fixed_variant.default.hex}};
+$tertiaryFixed: {{colors.tertiary_fixed.default.hex}};
+$tertiaryFixedDim: {{colors.tertiary_fixed_dim.default.hex}};
+$onTertiaryFixed: {{colors.on_tertiary_fixed.default.hex}};
+$onTertiaryFixedVariant: {{colors.on_tertiary_fixed_variant.default.hex}};
+$success: #B5CCBA;
+$onSuccess: #213528;
+$successContainer: #374B3E;
+$onSuccessContainer: #D1E9D6;
+$term0: #0D1C20;
+$term1: #8383FF;
+$term2: #63DFD4;
+$term3: #75FCDD;
+$term4: #76B4BD;
+$term5: #7AAEEA;
+$term6: #81D8D7;
+$term7: #CCDBD5;
+$term8: #B1BCB5;
+$term9: #BCB9FF;
+$term10: #F6FFFD;
+$term11: #FFFFFF;
+$term12: #BEE3E5;
+$term13: #C8DAFF;
+$term14: #E5FFFE;
+$term15: #ADEDF6;
diff --git a/.config/ags/scripts/templates/ags/sourceviewtheme-light.xml b/.config/matugen/templates/ags/sourceviewtheme-light.xml
similarity index 95%
rename from .config/ags/scripts/templates/ags/sourceviewtheme-light.xml
rename to .config/matugen/templates/ags/sourceviewtheme-light.xml
index eca04bf3b..d501c3186 100644
--- a/.config/ags/scripts/templates/ags/sourceviewtheme-light.xml
+++ b/.config/matugen/templates/ags/sourceviewtheme-light.xml
@@ -3,13 +3,13 @@
end_4
<_description>Catppuccin port but very random
-
+
-
+
diff --git a/.config/ags/assets/themes/sourceviewtheme.xml b/.config/matugen/templates/ags/sourceviewtheme.xml
similarity index 95%
rename from .config/ags/assets/themes/sourceviewtheme.xml
rename to .config/matugen/templates/ags/sourceviewtheme.xml
index ef76a7414..719809918 100644
--- a/.config/ags/assets/themes/sourceviewtheme.xml
+++ b/.config/matugen/templates/ags/sourceviewtheme.xml
@@ -27,13 +27,13 @@
Leo Iannacone
<_description>Based on SublimeText Monokai Extended - Generated with tm2gtksw2
-
+
-
+
diff --git a/.config/matugen/templates/colors.json b/.config/matugen/templates/colors.json
new file mode 100644
index 000000000..25f80e836
--- /dev/null
+++ b/.config/matugen/templates/colors.json
@@ -0,0 +1,51 @@
+{
+ "background": "{{colors.background.default.hex}}",
+ "error": "{{colors.error.default.hex}}",
+ "error_container": "{{colors.error_container.default.hex}}",
+ "inverse_on_surface": "{{colors.inverse_on_surface.default.hex}}",
+ "inverse_primary": "{{colors.inverse_primary.default.hex}}",
+ "inverse_surface": "{{colors.inverse_surface.default.hex}}",
+ "on_background": "{{colors.on_background.default.hex}}",
+ "on_error": "{{colors.on_error.default.hex}}",
+ "on_error_container": "{{colors.on_error_container.default.hex}}",
+ "on_primary": "{{colors.on_primary.default.hex}}",
+ "on_primary_container": "{{colors.on_primary_container.default.hex}}",
+ "on_primary_fixed": "{{colors.on_primary_fixed.default.hex}}",
+ "on_primary_fixed_variant": "{{colors.on_primary_fixed_variant.default.hex}}",
+ "on_secondary": "{{colors.on_secondary.default.hex}}",
+ "on_secondary_container": "{{colors.on_secondary_container.default.hex}}",
+ "on_secondary_fixed": "{{colors.on_secondary_fixed.default.hex}}",
+ "on_secondary_fixed_variant": "{{colors.on_secondary_fixed_variant.default.hex}}",
+ "on_surface": "{{colors.on_surface.default.hex}}",
+ "on_surface_variant": "{{colors.on_surface_variant.default.hex}}",
+ "on_tertiary": "{{colors.on_tertiary.default.hex}}",
+ "on_tertiary_container": "{{colors.on_tertiary_container.default.hex}}",
+ "on_tertiary_fixed": "{{colors.on_tertiary_fixed.default.hex}}",
+ "on_tertiary_fixed_variant": "{{colors.on_tertiary_fixed_variant.default.hex}}",
+ "outline": "{{colors.outline.default.hex}}",
+ "outline_variant": "{{colors.outline_variant.default.hex}}",
+ "primary": "{{colors.primary.default.hex}}",
+ "primary_container": "{{colors.primary_container.default.hex}}",
+ "primary_fixed": "{{colors.primary_fixed.default.hex}}",
+ "primary_fixed_dim": "{{colors.primary_fixed_dim.default.hex}}",
+ "scrim": "{{colors.scrim.default.hex}}",
+ "secondary": "{{colors.secondary.default.hex}}",
+ "secondary_container": "{{colors.secondary_container.default.hex}}",
+ "secondary_fixed": "{{colors.secondary_fixed.default.hex}}",
+ "secondary_fixed_dim": "{{colors.secondary_fixed_dim.default.hex}}",
+ "shadow": "{{colors.shadow.default.hex}}",
+ "surface": "{{colors.surface.default.hex}}",
+ "surface_bright": "{{colors.surface_bright.default.hex}}",
+ "surface_container": "{{colors.surface_container.default.hex}}",
+ "surface_container_high": "{{colors.surface_container_high.default.hex}}",
+ "surface_container_highest": "{{colors.surface_container_highest.default.hex}}",
+ "surface_container_low": "{{colors.surface_container_low.default.hex}}",
+ "surface_container_lowest": "{{colors.surface_container_lowest.default.hex}}",
+ "surface_dim": "{{colors.surface_dim.default.hex}}",
+ "surface_tint": "{{colors.surface_tint.default.hex}}",
+ "surface_variant": "{{colors.surface_variant.default.hex}}",
+ "tertiary": "{{colors.tertiary.default.hex}}",
+ "tertiary_container": "{{colors.tertiary_container.default.hex}}",
+ "tertiary_fixed": "{{colors.tertiary_fixed.default.hex}}",
+ "tertiary_fixed_dim": "{{colors.tertiary_fixed_dim.default.hex}}"
+}
diff --git a/.config/matugen/templates/fuzzel/fuzzel_theme.ini b/.config/matugen/templates/fuzzel/fuzzel_theme.ini
new file mode 100644
index 000000000..ec7e1a686
--- /dev/null
+++ b/.config/matugen/templates/fuzzel/fuzzel_theme.ini
@@ -0,0 +1,8 @@
+[colors]
+background={{colors.background.default.hex_stripped}}ff
+text={{colors.on_background.default.hex_stripped}}ff
+selection={{colors.surface_variant.default.hex_stripped}}ff
+selection-text={{colors.on_surface_variant.default.hex_stripped}}ff
+border={{colors.surface_variant.default.hex_stripped}}dd
+match={{colors.primary.default.hex_stripped}}ff
+selection-match={{colors.primary.default.hex_stripped}}ff
diff --git a/.config/matugen/templates/gtk/gtk-colors.css b/.config/matugen/templates/gtk/gtk-colors.css
new file mode 100644
index 000000000..c0054f864
--- /dev/null
+++ b/.config/matugen/templates/gtk/gtk-colors.css
@@ -0,0 +1,22 @@
+/*
+* GTK Colors
+* Generated with Matugen
+*/
+
+@define-color accent_color {{colors.primary.default.hex}};
+@define-color accent_fg_color {{colors.on_primary.default.hex}};
+@define-color accent_bg_color {{colors.primary.default.hex}};
+@define-color window_bg_color {{colors.background.default.hex}};
+@define-color window_fg_color {{colors.on_background.default.hex}};
+@define-color headerbar_bg_color {{colors.surface_dim.default.hex}};
+@define-color headerbar_fg_color {{colors.on_surface.default.hex}};
+@define-color popover_bg_color {{colors.surface_dim.default.hex}};
+@define-color popover_fg_color {{colors.on_surface.default.hex}};
+@define-color view_bg_color {{colors.surface.default.hex}};
+@define-color view_fg_color {{colors.on_surface.default.hex}};
+@define-color card_bg_color {{colors.surface.default.hex}};
+@define-color card_fg_color {{colors.on_surface.default.hex}};
+@define-color sidebar_bg_color @window_bg_color;
+@define-color sidebar_fg_color @window_fg_color;
+@define-color sidebar_border_color @window_bg_color;
+@define-color sidebar_backdrop_color @window_bg_color;
diff --git a/.config/matugen/templates/hyprland/colors.conf b/.config/matugen/templates/hyprland/colors.conf
new file mode 100644
index 000000000..67fdaea71
--- /dev/null
+++ b/.config/matugen/templates/hyprland/colors.conf
@@ -0,0 +1,32 @@
+general {
+ col.active_border = rgba({{colors.on_surface.default.hex_stripped}}39)
+ col.inactive_border = rgba({{colors.outline.default.hex_stripped}}30)
+}
+
+misc {
+ background_color = rgba({{colors.surface.dark.hex_stripped}}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({{colors.background.default.hex_stripped}}FF)
+ col.text = rgba({{colors.on_background.default.hex_stripped}}FF)
+
+
+ # example buttons (R -> L)
+ # hyprbars-button = color, size, on-click
+ hyprbars-button = rgb({{colors.on_background.default.hex_stripped}}), 13, , hyprctl dispatch killactive
+ hyprbars-button = rgb({{colors.on_background.default.hex_stripped}}), 13, , hyprctl dispatch fullscreen 1
+ hyprbars-button = rgb({{colors.on_background.default.hex_stripped}}), 13, , hyprctl dispatch movetoworkspacesilent special
+ }
+}
+
+windowrulev2 = bordercolor rgba({{colors.primary.default.hex_stripped}}AA) rgba({{colors.primary.default.hex_stripped}}77),pinned:1
diff --git a/.config/ags/scripts/templates/hypr/hyprlock.conf b/.config/matugen/templates/hyprland/hyprlock.conf
similarity index 82%
rename from .config/ags/scripts/templates/hypr/hyprlock.conf
rename to .config/matugen/templates/hyprland/hyprlock.conf
index 41342ef35..eab167199 100644
--- a/.config/ags/scripts/templates/hypr/hyprlock.conf
+++ b/.config/matugen/templates/hyprland/hyprlock.conf
@@ -1,18 +1,18 @@
-$text_color = rgba({{ $primaryFixed }}FF)
-$entry_background_color = rgba({{ $onPrimaryFixed }}11)
-$entry_border_color = rgba({{ $outline }}55)
-$entry_color = rgba({{ $primaryFixed }}FF)
+$text_color = rgba({{colors.primary_fixed.default.hex_stripped}}FF)
+$entry_background_color = rgba({{colors.on_primary_fixed.default.hex_stripped}}11)
+$entry_border_color = rgba({{colors.outline.default.hex_stripped}}55)
+$entry_color = rgba({{colors.primary_fixed.default.hex_stripped}}FF)
$font_family = Rubik Light
$font_family_clock = Rubik Light
$font_material_symbols = Material Symbols Rounded
background {
color = rgba(181818FF)
- # path = {{ SWWW_WALL }}
- # path = screenshot
- # blur_size = 15
- # blur_passes = 4
+ path = {{image}}
+ blur_size = 15
+ blur_passes = 4
+ brightness = 0.33
}
input-field {
monitor =
@@ -41,6 +41,7 @@ label { # Caps Lock Warning
valign = center
}
+
label { # Clock
monitor =
text = $TIME
@@ -68,8 +69,6 @@ label { # User
monitor =
text = $USER
color = $text_color
- shadow_passes = 1
- shadow_boost = 0.35
outline_thickness = 2
dots_size = 0.2 # Scale of input-field height, 0.2 - 0.8
dots_spacing = 0.2 # Scale of dots' absolute size, 0.0 - 1.0
diff --git a/.config/matugen/templates/kde/color.txt b/.config/matugen/templates/kde/color.txt
new file mode 100644
index 000000000..35142904f
--- /dev/null
+++ b/.config/matugen/templates/kde/color.txt
@@ -0,0 +1 @@
+{{colors.source_color.default.hex}}
\ No newline at end of file
diff --git a/.config/matugen/templates/kde/kde-material-you-colors-wrapper.sh b/.config/matugen/templates/kde/kde-material-you-colors-wrapper.sh
new file mode 100755
index 000000000..02856d603
--- /dev/null
+++ b/.config/matugen/templates/kde/kde-material-you-colors-wrapper.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}"
+
+color=$(tr -d '\n' < "$XDG_STATE_HOME/quickshell/user/generated/color.txt")
+
+current_mode=$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null | tr -d "'")
+if [[ "$current_mode" == "prefer-dark" ]]; then
+ mode_flag="-d"
+else
+ mode_flag="-l"
+fi
+
+kde-material-you-colors "$mode_flag" --color "$color"
diff --git a/.config/matugen/templates/wallpaper.txt b/.config/matugen/templates/wallpaper.txt
new file mode 100644
index 000000000..33b0c5bfe
--- /dev/null
+++ b/.config/matugen/templates/wallpaper.txt
@@ -0,0 +1 @@
+{{image}}
diff --git a/.config/quickshell/GlobalStates.qml b/.config/quickshell/GlobalStates.qml
new file mode 100644
index 000000000..1b879421b
--- /dev/null
+++ b/.config/quickshell/GlobalStates.qml
@@ -0,0 +1,44 @@
+import "root:/modules/common/"
+import QtQuick
+import Quickshell
+import Quickshell.Hyprland
+import Quickshell.Io
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+Singleton {
+ id: root
+ property bool sidebarLeftOpen: false
+ property bool sidebarRightOpen: false
+ property bool overviewOpen: false
+ property bool workspaceShowNumbers: false
+ property bool superReleaseMightTrigger: true
+
+ // When user is not reluctant while pressing super, they probably don't need to see workspace numbers
+ onSuperReleaseMightTriggerChanged: {
+ workspaceShowNumbersTimer.stop()
+ }
+
+ Timer {
+ id: workspaceShowNumbersTimer
+ interval: ConfigOptions.bar.workspaces.showNumberDelay
+ // interval: 0
+ repeat: false
+ onTriggered: {
+ workspaceShowNumbers = true
+ }
+ }
+
+ GlobalShortcut {
+ name: "workspaceNumber"
+ description: qsTr("Hold to show workspace numbers, release to show icons")
+
+ onPressed: {
+ workspaceShowNumbersTimer.start()
+ }
+ onReleased: {
+ workspaceShowNumbersTimer.stop()
+ workspaceShowNumbers = false
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/ReloadPopup.qml b/.config/quickshell/ReloadPopup.qml
new file mode 100644
index 000000000..03abed423
--- /dev/null
+++ b/.config/quickshell/ReloadPopup.qml
@@ -0,0 +1,156 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Qt5Compat.GraphicalEffects
+
+Scope {
+ id: root
+ property bool failed;
+ property string errorString;
+
+ // Connect to the Quickshell global to listen for the reload signals.
+ Connections {
+ target: Quickshell
+
+ function onReloadCompleted() {
+ root.failed = false;
+ popupLoader.loading = true;
+ }
+
+ function onReloadFailed(error: string) {
+ // Close any existing popup before making a new one.
+ popupLoader.active = false;
+
+ root.failed = true;
+ root.errorString = error;
+ popupLoader.loading = true;
+ }
+ }
+
+ // Keep the popup in a loader because it isn't needed most of the time
+ LazyLoader {
+ id: popupLoader
+
+ PanelWindow {
+ id: popup
+
+ anchors.top: true
+ margins.top: 0
+
+ implicitWidth: rect.width + shadow.radius * 2
+ implicitHeight: rect.height + shadow.radius * 2
+
+ // color blending is a bit odd as detailed in the type reference.
+ color: "transparent"
+
+ Rectangle {
+ id: rect
+ anchors.centerIn: parent
+ color: failed ? "#ffe99195" : "#ffD1E8D5"
+
+ implicitHeight: layout.implicitHeight + 30
+ implicitWidth: layout.implicitWidth + 30
+ radius: 12
+
+ // Fills the whole area of the rectangle, making any clicks go to it,
+ // which dismiss the popup.
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ onClicked: {
+ popupLoader.active = false
+ }
+
+ // makes the mouse area track mouse hovering, so the hide animation
+ // can be paused when hovering.
+ hoverEnabled: true
+ }
+
+ ColumnLayout {
+ id: layout
+ spacing: 10
+ anchors {
+ top: parent.top
+ topMargin: 10
+ horizontalCenter: parent.horizontalCenter
+ }
+
+ Text {
+ renderType: Text.NativeRendering
+ font.family: "Rubik"
+ font.pointSize: 14
+ text: root.failed ? "Quickshell: Reload failed" : "Quickshell reloaded"
+ color: failed ? "#ff93000A" : "#ff0C1F13"
+ }
+
+ Text {
+ renderType: Text.NativeRendering
+ font.family: "JetBrains Mono NF"
+ font.pointSize: 11
+ text: root.errorString
+ color: failed ? "#ff93000A" : "#ff0C1F13"
+ // When visible is false, it also takes up no space.
+ visible: root.errorString != ""
+ }
+ }
+
+ // A progress bar on the bottom of the screen, showing how long until the
+ // popup is removed.
+ Rectangle {
+ z: 2
+ id: bar
+ color: failed ? "#ff93000A" : "#ff0C1F13"
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.margins: 10
+ height: 5
+ radius: 9999
+
+ PropertyAnimation {
+ id: anim
+ target: bar
+ property: "width"
+ from: rect.width - bar.anchors.margins * 2
+ to: 0
+ duration: failed ? 10000 : 1000
+ onFinished: popupLoader.active = false
+
+ // Pause the animation when the mouse is hovering over the popup,
+ // so it stays onscreen while reading. This updates reactively
+ // when the mouse moves on and off the popup.
+ paused: mouseArea.containsMouse
+ }
+ }
+ // Its bg
+ Rectangle {
+ z: 1
+ id: bar_bg
+ color: failed ? "#30af1b25" : "#4027643e"
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.margins: 10
+ height: 5
+ radius: 9999
+ width: rect.width - bar.anchors.margins * 2
+ }
+
+ // We could set `running: true` inside the animation, but the width of the
+ // rectangle might not be calculated yet, due to the layout.
+ // In the `Component.onCompleted` event handler, all of the component's
+ // properties and children have been initialized.
+ Component.onCompleted: anim.start()
+ }
+
+ DropShadow {
+ id: shadow
+ anchors.fill: rect
+ horizontalOffset: 0
+ verticalOffset: 2
+ radius: 6
+ samples: radius * 2 + 1 // Ideally should be 2 * radius + 1, see qt docs
+ color: "#44000000"
+ source: rect
+ }
+ }
+ }
+}
diff --git a/.config/ags/assets/icons/ai-openai-symbolic.svg b/.config/quickshell/assets/icons/ai-openai-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/ai-openai-symbolic.svg
rename to .config/quickshell/assets/icons/ai-openai-symbolic.svg
diff --git a/.config/ags/assets/icons/arch-symbolic.svg b/.config/quickshell/assets/icons/arch-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/arch-symbolic.svg
rename to .config/quickshell/assets/icons/arch-symbolic.svg
diff --git a/.config/ags/assets/icons/cachyos-symbolic.svg b/.config/quickshell/assets/icons/cachyos-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/cachyos-symbolic.svg
rename to .config/quickshell/assets/icons/cachyos-symbolic.svg
diff --git a/.config/ags/assets/icons/cloudflare-dns-symbolic.svg b/.config/quickshell/assets/icons/cloudflare-dns-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/cloudflare-dns-symbolic.svg
rename to .config/quickshell/assets/icons/cloudflare-dns-symbolic.svg
diff --git a/.config/ags/assets/icons/crosshair-symbolic.svg b/.config/quickshell/assets/icons/crosshair-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/crosshair-symbolic.svg
rename to .config/quickshell/assets/icons/crosshair-symbolic.svg
diff --git a/.config/ags/assets/icons/debian-symbolic.svg b/.config/quickshell/assets/icons/debian-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/debian-symbolic.svg
rename to .config/quickshell/assets/icons/debian-symbolic.svg
diff --git a/.config/ags/assets/icons/deepseek-symbolic.svg b/.config/quickshell/assets/icons/deepseek-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/deepseek-symbolic.svg
rename to .config/quickshell/assets/icons/deepseek-symbolic.svg
diff --git a/.config/ags/assets/icons/desktop-symbolic.svg b/.config/quickshell/assets/icons/desktop-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/desktop-symbolic.svg
rename to .config/quickshell/assets/icons/desktop-symbolic.svg
diff --git a/.config/ags/assets/icons/endeavouros-symbolic.svg b/.config/quickshell/assets/icons/endeavouros-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/endeavouros-symbolic.svg
rename to .config/quickshell/assets/icons/endeavouros-symbolic.svg
diff --git a/.config/ags/assets/icons/fedora-symbolic.svg b/.config/quickshell/assets/icons/fedora-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/fedora-symbolic.svg
rename to .config/quickshell/assets/icons/fedora-symbolic.svg
diff --git a/.config/ags/assets/icons/flatpak-symbolic.svg b/.config/quickshell/assets/icons/flatpak-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/flatpak-symbolic.svg
rename to .config/quickshell/assets/icons/flatpak-symbolic.svg
diff --git a/.config/ags/assets/icons/github-symbolic.svg b/.config/quickshell/assets/icons/github-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/github-symbolic.svg
rename to .config/quickshell/assets/icons/github-symbolic.svg
diff --git a/.config/quickshell/assets/icons/google-gemini-symbolic.svg b/.config/quickshell/assets/icons/google-gemini-symbolic.svg
new file mode 120000
index 000000000..7aa8c18b6
--- /dev/null
+++ b/.config/quickshell/assets/icons/google-gemini-symbolic.svg
@@ -0,0 +1 @@
+spark-symbolic.svg
\ No newline at end of file
diff --git a/.config/ags/assets/icons/linux-symbolic.svg b/.config/quickshell/assets/icons/linux-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/linux-symbolic.svg
rename to .config/quickshell/assets/icons/linux-symbolic.svg
diff --git a/.config/ags/assets/icons/microsoft-symbolic.svg b/.config/quickshell/assets/icons/microsoft-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/microsoft-symbolic.svg
rename to .config/quickshell/assets/icons/microsoft-symbolic.svg
diff --git a/.config/ags/assets/icons/nixos-symbolic.svg b/.config/quickshell/assets/icons/nixos-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/nixos-symbolic.svg
rename to .config/quickshell/assets/icons/nixos-symbolic.svg
diff --git a/.config/ags/assets/icons/ollama-symbolic.svg b/.config/quickshell/assets/icons/ollama-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/ollama-symbolic.svg
rename to .config/quickshell/assets/icons/ollama-symbolic.svg
diff --git a/.config/ags/assets/icons/openai-symbolic.svg b/.config/quickshell/assets/icons/openai-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/openai-symbolic.svg
rename to .config/quickshell/assets/icons/openai-symbolic.svg
diff --git a/.config/ags/assets/icons/openrouter-symbolic.svg b/.config/quickshell/assets/icons/openrouter-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/openrouter-symbolic.svg
rename to .config/quickshell/assets/icons/openrouter-symbolic.svg
diff --git a/.config/ags/assets/icons/google-gemini-symbolic.svg b/.config/quickshell/assets/icons/spark-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/google-gemini-symbolic.svg
rename to .config/quickshell/assets/icons/spark-symbolic.svg
diff --git a/.config/ags/assets/icons/ubuntu-symbolic.svg b/.config/quickshell/assets/icons/ubuntu-symbolic.svg
similarity index 100%
rename from .config/ags/assets/icons/ubuntu-symbolic.svg
rename to .config/quickshell/assets/icons/ubuntu-symbolic.svg
diff --git a/.config/ags/assets/images/default_wallpaper.png b/.config/quickshell/assets/images/default_wallpaper.png
similarity index 100%
rename from .config/ags/assets/images/default_wallpaper.png
rename to .config/quickshell/assets/images/default_wallpaper.png
diff --git a/.config/quickshell/modules/backgroundWidgets/BackgroundWidgets.qml b/.config/quickshell/modules/backgroundWidgets/BackgroundWidgets.qml
new file mode 100644
index 000000000..9df0a9d35
--- /dev/null
+++ b/.config/quickshell/modules/backgroundWidgets/BackgroundWidgets.qml
@@ -0,0 +1,135 @@
+import "root:/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Wayland
+import Quickshell.Hyprland
+import Quickshell.Services.UPower
+
+Scope {
+ id: root
+ property string filePath: `${Directories.state}/user/generated/wallpaper/least_busy_region.json`
+ property real centerX: -500
+ property real centerY: -500
+ property color dominantColor: Appearance.colors.colPrimary
+ property bool dominantColorIsDark: dominantColor.hslLightness < 0.5
+ property color colBackground: ColorUtils.transparentize(ColorUtils.mix(Appearance.colors.colPrimary, Appearance.colors.colSecondaryContainer), 1)
+ property color colText: ColorUtils.colorWithLightness(Appearance.colors.colPrimary, (root.dominantColorIsDark ? 0.8 : 0.12))
+
+ function updateWidgetPosition(fileContent) {
+ // console.log("[BackgroundWidgets] Updating widget position with content:", fileContent)
+ const parsedContent = JSON.parse(fileContent)
+ root.centerX = parsedContent.center_x
+ root.centerY = parsedContent.center_y
+ root.dominantColor = parsedContent.dominant_color || Appearance.colors.colPrimary
+ }
+
+ Timer {
+ id: delayedFileRead
+ interval: ConfigOptions.hacks.arbitraryRaceConditionDelay
+ running: false
+ onTriggered: {
+ root.updateWidgetPosition(leastBusyRegionFileView.text())
+ }
+ }
+
+ FileView {
+ id: leastBusyRegionFileView
+ path: Qt.resolvedUrl(root.filePath)
+ watchChanges: true
+ onFileChanged: {
+ this.reload()
+ delayedFileRead.start()
+ }
+ onLoadedChanged: {
+ const fileContent = leastBusyRegionFileView.text()
+ root.updateWidgetPosition(fileContent)
+ }
+ }
+
+ Variants { // For each monitor
+ model: Quickshell.screens
+
+ Loader {
+ required property var modelData
+ readonly property HyprlandMonitor monitor: Hyprland.monitorFor(modelData)
+ active: !ToplevelManager.activeToplevel?.activated
+ sourceComponent: PanelWindow { // Window
+ id: windowRoot
+ screen: modelData
+ property var textHorizontalAlignment: root.centerX / monitor.scale < windowRoot.width / 3 ? Text.AlignLeft :
+ (root.centerX / monitor.scale > windowRoot.width * 2 / 3 ? Text.AlignRight : Text.AlignHCenter)
+
+ WlrLayershell.layer: WlrLayer.Bottom
+ WlrLayershell.namespace: "quickshell:backgroundWidgets"
+
+ anchors {
+ top: true
+ bottom:true
+ left: true
+ right: true
+ }
+ color: "transparent"
+ HyprlandWindow.visibleMask: Region {
+ item: widgetBackground
+ }
+
+ Rectangle {
+ id: widgetBackground
+ property real verticalPadding: 20
+ property real horizontalPadding: 30
+ radius: 40
+ color: root.colBackground
+ implicitHeight: columnLayout.implicitHeight + verticalPadding * 2
+ implicitWidth: columnLayout.implicitWidth + horizontalPadding * 2
+ anchors {
+ left: parent.left
+ top: parent.top
+ leftMargin: (root.centerX / monitor.scale - implicitWidth / 2)
+ topMargin: (root.centerY / monitor.scale - implicitHeight / 2)
+ Behavior on leftMargin {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+ Behavior on topMargin {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+ }
+
+ ColumnLayout {
+ id: columnLayout
+ anchors.centerIn: parent
+ spacing: -5
+
+ StyledText {
+ Layout.fillWidth: true
+ horizontalAlignment: windowRoot.textHorizontalAlignment
+ font.pixelSize: 95
+ color: root.colText
+ style: Text.Raised
+ styleColor: Appearance.colors.colShadow
+ text: DateTime.time
+ }
+ StyledText {
+ Layout.fillWidth: true
+ horizontalAlignment: windowRoot.textHorizontalAlignment
+ font.pixelSize: 25
+ color: root.colText
+ style: Text.Raised
+ styleColor: Appearance.colors.colShadow
+ text: DateTime.date
+ }
+ }
+ }
+
+ }
+ }
+
+ }
+
+}
diff --git a/.config/quickshell/modules/bar/ActiveWindow.qml b/.config/quickshell/modules/bar/ActiveWindow.qml
new file mode 100644
index 000000000..95e25c6f2
--- /dev/null
+++ b/.config/quickshell/modules/bar/ActiveWindow.qml
@@ -0,0 +1,42 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Layouts
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Item {
+ id: root
+ required property var bar
+ readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen)
+ readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
+
+ implicitWidth: colLayout.implicitWidth
+
+ ColumnLayout {
+ id: colLayout
+
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.right: parent.right
+ spacing: -4
+
+ StyledText {
+ Layout.fillWidth: true
+ font.pixelSize: Appearance.font.pixelSize.smaller
+ color: Appearance.colors.colSubtext
+ elide: Text.ElideRight
+ text: root.activeWindow?.activated ? root.activeWindow?.appId : qsTr("Desktop")
+ }
+
+ StyledText {
+ Layout.fillWidth: true
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: Appearance.colors.colOnLayer0
+ elide: Text.ElideRight
+ text: root.activeWindow?.activated ? root.activeWindow?.title : `${qsTr("Workspace")} ${monitor.activeWorkspace?.id}`
+ }
+
+ }
+
+}
diff --git a/.config/quickshell/modules/bar/Bar.qml b/.config/quickshell/modules/bar/Bar.qml
new file mode 100644
index 000000000..9fb9b32ed
--- /dev/null
+++ b/.config/quickshell/modules/bar/Bar.qml
@@ -0,0 +1,469 @@
+import "root:/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Qt5Compat.GraphicalEffects
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Hyprland
+import Quickshell.Services.UPower
+
+Scope {
+ id: bar
+
+ readonly property int barHeight: Appearance.sizes.barHeight
+ readonly property int osdHideMouseMoveThreshold: 20
+ property bool showBarBackground: ConfigOptions.bar.showBackground
+
+ component VerticalBarSeparator: Rectangle {
+ Layout.topMargin: barHeight / 3
+ Layout.bottomMargin: barHeight / 3
+ Layout.fillHeight: true
+ implicitWidth: 1
+ color: Appearance.colors.colOutlineVariant
+ }
+
+ Variants { // For each monitor
+ model: {
+ const screens = Quickshell.screens;
+ const list = ConfigOptions.bar.screenList;
+ if (!list || list.length === 0)
+ return screens;
+ return screens.filter(screen => list.includes(screen.name));
+ }
+
+ PanelWindow { // Bar window
+ id: barRoot
+ screen: modelData
+
+ property ShellScreen modelData
+ property var brightnessMonitor: Brightness.getMonitorForScreen(modelData)
+ property real useShortenedForm: (Appearance.sizes.barHellaShortenScreenWidthThreshold >= screen.width) ? 2 :
+ (Appearance.sizes.barShortenScreenWidthThreshold >= screen.width) ? 1 : 0
+ readonly property int centerSideModuleWidth:
+ (useShortenedForm == 2) ? Appearance.sizes.barCenterSideModuleWidthHellaShortened :
+ (useShortenedForm == 1) ? Appearance.sizes.barCenterSideModuleWidthShortened :
+ Appearance.sizes.barCenterSideModuleWidth
+
+ WlrLayershell.namespace: "quickshell:bar"
+ implicitHeight: barHeight + Appearance.rounding.screenRounding
+ exclusiveZone: showBarBackground ? barHeight : (barHeight - 4)
+ mask: Region {
+ item: barContent
+ }
+ color: "transparent"
+
+ anchors {
+ top: !ConfigOptions.bar.bottom
+ bottom: ConfigOptions.bar.bottom
+ left: true
+ right: true
+ }
+
+ Rectangle { // Bar background
+ id: barContent
+ anchors {
+ right: parent.right
+ left: parent.left
+ top: !ConfigOptions.bar.bottom ? parent.top : undefined
+ bottom: ConfigOptions.bar.bottom ? parent.bottom : undefined
+ }
+ color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
+ height: barHeight
+
+ MouseArea { // Left side | scroll to change brightness
+ id: barLeftSideMouseArea
+ anchors.left: parent.left
+ implicitHeight: barHeight
+ width: (barRoot.width - middleSection.width) / 2
+ property bool hovered: false
+ property real lastScrollX: 0
+ property real lastScrollY: 0
+ property bool trackingScroll: false
+ acceptedButtons: Qt.LeftButton
+ hoverEnabled: true
+ propagateComposedEvents: true
+ onEntered: (event) => {
+ barLeftSideMouseArea.hovered = true
+ }
+ onExited: (event) => {
+ barLeftSideMouseArea.hovered = false
+ barLeftSideMouseArea.trackingScroll = false
+ }
+ onPressed: (event) => {
+ if (event.button === Qt.LeftButton) {
+ Hyprland.dispatch('global quickshell:sidebarLeftOpen')
+ }
+ }
+ // Scroll to change brightness
+ WheelHandler {
+ onWheel: (event) => {
+ if (event.angleDelta.y < 0)
+ barRoot.brightnessMonitor.setBrightness(barRoot.brightnessMonitor.brightness - 0.05);
+ else if (event.angleDelta.y > 0)
+ barRoot.brightnessMonitor.setBrightness(barRoot.brightnessMonitor.brightness + 0.05);
+ // Store the mouse position and start tracking
+ barLeftSideMouseArea.lastScrollX = event.x;
+ barLeftSideMouseArea.lastScrollY = event.y;
+ barLeftSideMouseArea.trackingScroll = true;
+ }
+ acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+ }
+ onPositionChanged: (mouse) => {
+ if (barLeftSideMouseArea.trackingScroll) {
+ const dx = mouse.x - barLeftSideMouseArea.lastScrollX;
+ const dy = mouse.y - barLeftSideMouseArea.lastScrollY;
+ if (Math.sqrt(dx*dx + dy*dy) > osdHideMouseMoveThreshold) {
+ Hyprland.dispatch('global quickshell:osdBrightnessHide')
+ barLeftSideMouseArea.trackingScroll = false;
+ }
+ }
+ }
+ Item { // Left section
+ anchors.fill: parent
+ implicitHeight: leftSectionRowLayout.implicitHeight
+ implicitWidth: leftSectionRowLayout.implicitWidth
+
+ ScrollHint {
+ reveal: barLeftSideMouseArea.hovered
+ icon: "light_mode"
+ tooltipText: qsTr("Scroll to change brightness")
+ side: "left"
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+
+ }
+
+ RowLayout { // Content
+ id: leftSectionRowLayout
+ anchors.fill: parent
+ spacing: 10
+
+ RippleButton { // Left sidebar button
+ Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
+ Layout.leftMargin: Appearance.rounding.screenRounding
+ Layout.fillWidth: false
+ property real buttonPadding: 5
+ implicitWidth: distroIcon.width + buttonPadding * 2
+ implicitHeight: distroIcon.height + buttonPadding * 2
+
+ buttonRadius: Appearance.rounding.full
+ colBackground: barLeftSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
+ colBackgroundHover: Appearance.colors.colLayer1Hover
+ colRipple: Appearance.colors.colLayer1Active
+ colBackgroundToggled: Appearance.colors.colSecondaryContainer
+ colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
+ colRippleToggled: Appearance.colors.colSecondaryContainerActive
+ toggled: GlobalStates.sidebarLeftOpen
+ property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0
+
+ onPressed: {
+ Hyprland.dispatch('global quickshell:sidebarLeftToggle')
+ }
+
+ CustomIcon {
+ id: distroIcon
+ anchors.centerIn: parent
+ width: 19.5
+ height: 19.5
+ source: ConfigOptions.bar.topLeftIcon == 'distro' ?
+ SystemInfo.distroIcon : "spark-symbolic"
+ }
+
+ ColorOverlay {
+ anchors.fill: distroIcon
+ source: distroIcon
+ color: Appearance.colors.colOnLayer0
+ }
+ }
+
+ ActiveWindow {
+ visible: barRoot.useShortenedForm === 0
+ Layout.rightMargin: Appearance.rounding.screenRounding
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ bar: barRoot
+ }
+ }
+ }
+ }
+
+ RowLayout { // Middle section
+ id: middleSection
+ anchors.centerIn: parent
+ spacing: ConfigOptions?.bar.borderless ? 4 : 8
+
+ BarGroup {
+ id: leftCenterGroup
+ Layout.preferredWidth: barRoot.centerSideModuleWidth
+ Layout.fillHeight: true
+
+ Resources {
+ alwaysShowAllResources: barRoot.useShortenedForm === 2
+ Layout.fillWidth: barRoot.useShortenedForm === 2
+ }
+
+ Media {
+ visible: barRoot.useShortenedForm < 2
+ Layout.fillWidth: true
+ }
+
+ }
+
+ VerticalBarSeparator {visible: ConfigOptions?.bar.borderless}
+
+ BarGroup {
+ id: middleCenterGroup
+ padding: workspacesWidget.widgetPadding
+ Layout.fillHeight: true
+
+ Workspaces {
+ id: workspacesWidget
+ bar: barRoot
+ Layout.fillHeight: true
+ MouseArea { // Right-click to toggle overview
+ anchors.fill: parent
+ acceptedButtons: Qt.RightButton
+
+ onPressed: (event) => {
+ if (event.button === Qt.RightButton) {
+ Hyprland.dispatch('global quickshell:overviewToggle')
+ }
+ }
+ }
+ }
+ }
+
+ VerticalBarSeparator {visible: ConfigOptions?.bar.borderless}
+
+ BarGroup {
+ id: rightCenterGroup
+ Layout.preferredWidth: barRoot.centerSideModuleWidth
+ Layout.fillHeight: true
+
+ ClockWidget {
+ showDate: (ConfigOptions.bar.verbose && barRoot.useShortenedForm < 2)
+ Layout.alignment: Qt.AlignVCenter
+ Layout.fillWidth: true
+ }
+
+ UtilButtons {
+ visible: (ConfigOptions.bar.verbose && barRoot.useShortenedForm === 0)
+ Layout.alignment: Qt.AlignVCenter
+ }
+
+ BatteryIndicator {
+ visible: (barRoot.useShortenedForm < 2 && UPower.displayDevice.isLaptopBattery)
+ Layout.alignment: Qt.AlignVCenter
+ }
+ }
+
+ }
+
+ MouseArea { // Right side | scroll to change volume
+ id: barRightSideMouseArea
+
+ anchors.right: parent.right
+ implicitHeight: barHeight
+ width: (barRoot.width - middleSection.width) / 2
+
+ property bool hovered: false
+ property real lastScrollX: 0
+ property real lastScrollY: 0
+ property bool trackingScroll: false
+
+ acceptedButtons: Qt.LeftButton
+ hoverEnabled: true
+ propagateComposedEvents: true
+ onEntered: (event) => {
+ barRightSideMouseArea.hovered = true
+ }
+ onExited: (event) => {
+ barRightSideMouseArea.hovered = false
+ barRightSideMouseArea.trackingScroll = false
+ }
+ onPressed: (event) => {
+ if (event.button === Qt.LeftButton) {
+ Hyprland.dispatch('global quickshell:sidebarRightOpen')
+ }
+ else if (event.button === Qt.RightButton) {
+ MprisController.activePlayer.next()
+ }
+ }
+ // Scroll to change volume
+ WheelHandler {
+ onWheel: (event) => {
+ const currentVolume = Audio.value;
+ const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2;
+ if (event.angleDelta.y < 0)
+ Audio.sink.audio.volume -= step;
+ else if (event.angleDelta.y > 0)
+ Audio.sink.audio.volume = Math.min(1, Audio.sink.audio.volume + step);
+ // Store the mouse position and start tracking
+ barRightSideMouseArea.lastScrollX = event.x;
+ barRightSideMouseArea.lastScrollY = event.y;
+ barRightSideMouseArea.trackingScroll = true;
+ }
+ acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+ }
+ onPositionChanged: (mouse) => {
+ if (barRightSideMouseArea.trackingScroll) {
+ const dx = mouse.x - barRightSideMouseArea.lastScrollX;
+ const dy = mouse.y - barRightSideMouseArea.lastScrollY;
+ if (Math.sqrt(dx*dx + dy*dy) > osdHideMouseMoveThreshold) {
+ Hyprland.dispatch('global quickshell:osdVolumeHide')
+ barRightSideMouseArea.trackingScroll = false;
+ }
+ }
+ }
+
+ Item {
+ anchors.fill: parent
+ implicitHeight: rightSectionRowLayout.implicitHeight
+ implicitWidth: rightSectionRowLayout.implicitWidth
+
+ ScrollHint {
+ reveal: barRightSideMouseArea.hovered
+ icon: "volume_up"
+ tooltipText: qsTr("Scroll to change volume")
+ side: "right"
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ RowLayout {
+ id: rightSectionRowLayout
+ anchors.fill: parent
+ spacing: 5
+ layoutDirection: Qt.RightToLeft
+
+ RippleButton { // Right sidebar button
+ id: rightSidebarButton
+ Layout.margins: 4
+ Layout.rightMargin: Appearance.rounding.screenRounding
+ Layout.fillHeight: true
+ implicitWidth: indicatorsRowLayout.implicitWidth + 10*2
+ buttonRadius: Appearance.rounding.full
+ colBackground: barRightSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
+ colBackgroundHover: Appearance.colors.colLayer1Hover
+ colRipple: Appearance.colors.colLayer1Active
+ colBackgroundToggled: Appearance.colors.colSecondaryContainer
+ colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
+ colRippleToggled: Appearance.colors.colSecondaryContainerActive
+ toggled: GlobalStates.sidebarRightOpen
+ property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0
+
+ Behavior on colText {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+
+ onPressed: {
+ Hyprland.dispatch('global quickshell:sidebarRightToggle')
+ }
+
+ RowLayout {
+ id: indicatorsRowLayout
+ anchors.centerIn: parent
+ property real realSpacing: 15
+ spacing: 0
+
+ Revealer {
+ reveal: Audio.sink?.audio?.muted ?? false
+ Layout.fillHeight: true
+ Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0
+ Behavior on Layout.rightMargin {
+ NumberAnimation {
+ duration: Appearance.animation.elementMoveFast.duration
+ easing.type: Appearance.animation.elementMoveFast.type
+ easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
+ }
+ }
+ MaterialSymbol {
+ text: "volume_off"
+ iconSize: Appearance.font.pixelSize.larger
+ color: rightSidebarButton.colText
+ }
+ }
+ Revealer {
+ reveal: Audio.source?.audio?.muted ?? false
+ Layout.fillHeight: true
+ Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0
+ Behavior on Layout.rightMargin {
+ NumberAnimation {
+ duration: Appearance.animation.elementMoveFast.duration
+ easing.type: Appearance.animation.elementMoveFast.type
+ easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
+ }
+ }
+ MaterialSymbol {
+ text: "mic_off"
+ iconSize: Appearance.font.pixelSize.larger
+ color: rightSidebarButton.colText
+ }
+ }
+ MaterialSymbol {
+ Layout.rightMargin: indicatorsRowLayout.realSpacing
+ text: Network.materialSymbol
+ iconSize: Appearance.font.pixelSize.larger
+ color: rightSidebarButton.colText
+ }
+ MaterialSymbol {
+ text: Bluetooth.bluetoothConnected ? "bluetooth_connected" : Bluetooth.bluetoothEnabled ? "bluetooth" : "bluetooth_disabled"
+ iconSize: Appearance.font.pixelSize.larger
+ color: rightSidebarButton.colText
+ }
+ }
+ }
+
+ SysTray {
+ bar: barRoot
+ visible: barRoot.useShortenedForm === 0
+ Layout.fillWidth: false
+ Layout.fillHeight: true
+ }
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+ }
+ }
+ }
+ }
+
+ // Round decorators
+ Item {
+ anchors {
+ left: parent.left
+ right: parent.right
+ // top: barContent.bottom
+ top: ConfigOptions.bar.bottom ? undefined : barContent.bottom
+ bottom: ConfigOptions.bar.bottom ? barContent.top : undefined
+ }
+ height: Appearance.rounding.screenRounding
+
+ RoundCorner {
+ anchors.top: parent.top
+ anchors.left: parent.left
+ size: Appearance.rounding.screenRounding
+ corner: ConfigOptions.bar.bottom ? cornerEnum.bottomLeft : cornerEnum.topLeft
+ color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
+ }
+ RoundCorner {
+ anchors.top: parent.top
+ anchors.right: parent.right
+ size: Appearance.rounding.screenRounding
+ corner: ConfigOptions.bar.bottom ? cornerEnum.bottomRight : cornerEnum.topRight
+ color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
+ }
+ }
+
+ }
+
+ }
+
+}
diff --git a/.config/quickshell/modules/bar/BarGroup.qml b/.config/quickshell/modules/bar/BarGroup.qml
new file mode 100644
index 000000000..a71b67e46
--- /dev/null
+++ b/.config/quickshell/modules/bar/BarGroup.qml
@@ -0,0 +1,37 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import QtQuick
+import QtQuick.Layouts
+
+Item {
+ id: root
+ property real padding: 5
+ implicitHeight: 40
+ implicitWidth: rowLayout.implicitWidth + padding * 2
+ default property alias items: rowLayout.children
+
+ Rectangle {
+ id: background
+ anchors {
+ fill: parent
+ topMargin: 4
+ bottomMargin: 4
+ }
+ color: ConfigOptions?.bar.borderless ? "transparent" : Appearance.colors.colLayer1
+ radius: Appearance.rounding.small
+ }
+
+ RowLayout {
+ id: rowLayout
+ anchors {
+ verticalCenter: parent.verticalCenter
+ left: parent.left
+ right: parent.right
+ leftMargin: root.padding
+ rightMargin: root.padding
+ }
+ spacing: 4
+
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/bar/BatteryIndicator.qml b/.config/quickshell/modules/bar/BatteryIndicator.qml
new file mode 100644
index 000000000..61a981575
--- /dev/null
+++ b/.config/quickshell/modules/bar/BatteryIndicator.qml
@@ -0,0 +1,97 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Services.UPower
+
+Item {
+ id: root
+ property bool borderless: ConfigOptions.bar.borderless
+ readonly property var chargeState: Battery.chargeState
+ readonly property bool isCharging: Battery.isCharging
+ readonly property bool isPluggedIn: Battery.isPluggedIn
+ readonly property real percentage: Battery.percentage
+ readonly property bool isLow: percentage <= ConfigOptions.battery.low / 100
+ readonly property color batteryLowBackground: Appearance.m3colors.darkmode ? Appearance.m3colors.m3error : Appearance.m3colors.m3errorContainer
+ readonly property color batteryLowOnBackground: Appearance.m3colors.darkmode ? Appearance.m3colors.m3errorContainer : Appearance.m3colors.m3error
+
+ implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2
+ implicitHeight: 32
+
+ RowLayout {
+ id: rowLayout
+
+ spacing: 4
+ anchors.centerIn: parent
+
+ Rectangle {
+ implicitWidth: (isCharging ? (boltIconLoader?.item?.width ?? 0) : 0)
+
+ Behavior on implicitWidth {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignVCenter
+ color: Appearance.colors.colOnLayer1
+ text: `${Math.round(percentage * 100)}`
+ }
+
+ CircularProgress {
+ Layout.alignment: Qt.AlignVCenter
+ lineWidth: 2
+ value: percentage
+ size: 26
+ secondaryColor: (isLow && !isCharging) ? batteryLowBackground : Appearance.colors.colSecondaryContainer
+ primaryColor: (isLow && !isCharging) ? batteryLowOnBackground : Appearance.m3colors.m3onSecondaryContainer
+ fill: (isLow && !isCharging)
+
+ MaterialSymbol {
+ anchors.centerIn: parent
+ fill: 1
+ text: "battery_full"
+ iconSize: Appearance.font.pixelSize.normal
+ color: (isLow && !isCharging) ? batteryLowOnBackground : Appearance.m3colors.m3onSecondaryContainer
+ }
+
+ }
+
+ }
+
+ Loader {
+ id: boltIconLoader
+ active: true
+ anchors.left: rowLayout.left
+ anchors.verticalCenter: rowLayout.verticalCenter
+
+ Connections {
+ target: root
+ function onIsChargingChanged() {
+ if (isCharging) boltIconLoader.active = true
+ }
+ }
+
+ sourceComponent: MaterialSymbol {
+ id: boltIcon
+
+ text: "bolt"
+ iconSize: Appearance.font.pixelSize.large
+ color: Appearance.m3colors.m3onSecondaryContainer
+ visible: opacity > 0 // Only show when charging
+ opacity: isCharging ? 1 : 0 // Keep opacity for visibility
+ onVisibleChanged: {
+ if (!visible) boltIconLoader.active = false
+ }
+
+ Behavior on opacity {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+
+ }
+ }
+
+}
diff --git a/.config/quickshell/modules/bar/CircleUtilButton.qml b/.config/quickshell/modules/bar/CircleUtilButton.qml
new file mode 100644
index 000000000..3027c130c
--- /dev/null
+++ b/.config/quickshell/modules/bar/CircleUtilButton.qml
@@ -0,0 +1,20 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets/"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+
+RippleButton {
+ id: button
+
+ required default property Item content
+ property bool extraActiveCondition: false
+
+ implicitHeight: Math.max(content.implicitHeight, 26, content.implicitHeight)
+ implicitWidth: implicitHeight
+ contentItem: content
+
+}
diff --git a/.config/quickshell/modules/bar/ClockWidget.qml b/.config/quickshell/modules/bar/ClockWidget.qml
new file mode 100644
index 000000000..9e3403838
--- /dev/null
+++ b/.config/quickshell/modules/bar/ClockWidget.qml
@@ -0,0 +1,41 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import QtQuick
+import QtQuick.Layouts
+
+Item {
+ id: root
+ property bool borderless: ConfigOptions.bar.borderless
+ property bool showDate: ConfigOptions.bar.verbose
+ implicitWidth: rowLayout.implicitWidth
+ implicitHeight: 32
+
+ RowLayout {
+ id: rowLayout
+ anchors.centerIn: parent
+ spacing: 4
+
+ StyledText {
+ font.pixelSize: Appearance.font.pixelSize.large
+ color: Appearance.colors.colOnLayer1
+ text: DateTime.time
+ }
+
+ StyledText {
+ visible: root.showDate
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: Appearance.colors.colOnLayer1
+ text: "•"
+ }
+
+ StyledText {
+ visible: root.showDate
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: Appearance.colors.colOnLayer1
+ text: DateTime.date
+ }
+
+ }
+
+}
diff --git a/.config/quickshell/modules/bar/Media.qml b/.config/quickshell/modules/bar/Media.qml
new file mode 100644
index 000000000..3bd8a78a4
--- /dev/null
+++ b/.config/quickshell/modules/bar/Media.qml
@@ -0,0 +1,83 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Services.Mpris
+import Quickshell.Hyprland
+
+Item {
+ id: root
+ property bool borderless: ConfigOptions.bar.borderless
+ readonly property MprisPlayer activePlayer: MprisController.activePlayer
+ readonly property string cleanedTitle: StringUtils.cleanMusicTitle(activePlayer?.trackTitle) || qsTr("No media")
+
+ Layout.fillHeight: true
+ implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2
+ implicitHeight: 40
+
+ Timer {
+ running: activePlayer?.playbackState == MprisPlaybackState.Playing
+ interval: 1000
+ repeat: true
+ onTriggered: activePlayer.positionChanged()
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.MiddleButton | Qt.BackButton | Qt.ForwardButton | Qt.RightButton | Qt.LeftButton
+ onPressed: (event) => {
+ if (event.button === Qt.MiddleButton) {
+ activePlayer.togglePlaying();
+ } else if (event.button === Qt.BackButton) {
+ activePlayer.previous();
+ } else if (event.button === Qt.ForwardButton || event.button === Qt.RightButton) {
+ activePlayer.next();
+ } else if (event.button === Qt.LeftButton) {
+ Hyprland.dispatch("global quickshell:mediaControlsToggle")
+ }
+ }
+ }
+
+ RowLayout { // Real content
+ id: rowLayout
+
+ spacing: 4
+ anchors.fill: parent
+
+ CircularProgress {
+ Layout.alignment: Qt.AlignVCenter
+ Layout.leftMargin: rowLayout.spacing
+ lineWidth: 2
+ value: activePlayer?.position / activePlayer?.length
+ size: 26
+ secondaryColor: Appearance.colors.colSecondaryContainer
+ primaryColor: Appearance.m3colors.m3onSecondaryContainer
+
+ MaterialSymbol {
+ anchors.centerIn: parent
+ fill: 1
+ text: activePlayer?.isPlaying ? "pause" : "music_note"
+ iconSize: Appearance.font.pixelSize.normal
+ color: Appearance.m3colors.m3onSecondaryContainer
+ }
+
+ }
+
+ StyledText {
+ width: rowLayout.width - (CircularProgress.size + rowLayout.spacing * 2)
+ Layout.alignment: Qt.AlignVCenter
+ Layout.fillWidth: true // Ensures the text takes up available space
+ Layout.rightMargin: rowLayout.spacing
+ horizontalAlignment: Text.AlignHCenter
+ elide: Text.ElideRight // Truncates the text on the right
+ color: Appearance.colors.colOnLayer1
+ text: `${cleanedTitle}${activePlayer?.trackArtist ? ' • ' + activePlayer.trackArtist : ''}`
+ }
+
+ }
+
+}
diff --git a/.config/quickshell/modules/bar/Resource.qml b/.config/quickshell/modules/bar/Resource.qml
new file mode 100644
index 000000000..fed7a153c
--- /dev/null
+++ b/.config/quickshell/modules/bar/Resource.qml
@@ -0,0 +1,59 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+
+Item {
+ required property string iconName
+ required property double percentage
+ property bool shown: true
+ clip: true
+ visible: width > 0 && height > 0
+ implicitWidth: resourceRowLayout.x < 0 ? 0 : childrenRect.width
+ implicitHeight: childrenRect.height
+
+ RowLayout {
+ spacing: 4
+ id: resourceRowLayout
+ x: shown ? 0 : -resourceRowLayout.width
+
+ CircularProgress {
+ Layout.alignment: Qt.AlignVCenter
+ lineWidth: 2
+ value: percentage
+ size: 26
+ secondaryColor: Appearance.colors.colSecondaryContainer
+ primaryColor: Appearance.m3colors.m3onSecondaryContainer
+
+ MaterialSymbol {
+ anchors.centerIn: parent
+ fill: 1
+ text: iconName
+ iconSize: Appearance.font.pixelSize.normal
+ color: Appearance.m3colors.m3onSecondaryContainer
+ }
+
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignVCenter
+ color: Appearance.colors.colOnLayer1
+ text: `${Math.round(percentage * 100)}`
+ }
+
+ Behavior on x {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+
+ }
+
+ Behavior on implicitWidth {
+ NumberAnimation {
+ duration: Appearance.animation.elementMove.duration
+ easing.type: Appearance.animation.elementMove.type
+ easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/bar/Resources.qml b/.config/quickshell/modules/bar/Resources.qml
new file mode 100644
index 000000000..9f9b969ca
--- /dev/null
+++ b/.config/quickshell/modules/bar/Resources.qml
@@ -0,0 +1,50 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Services.Mpris
+
+Item {
+ id: root
+ property bool borderless: ConfigOptions.bar.borderless
+ property bool alwaysShowAllResources: false
+ implicitWidth: rowLayout.implicitWidth + rowLayout.anchors.leftMargin + rowLayout.anchors.rightMargin
+ implicitHeight: 32
+
+ RowLayout {
+ id: rowLayout
+
+ spacing: 0
+ anchors.fill: parent
+ anchors.leftMargin: 4
+ anchors.rightMargin: 4
+
+ Resource {
+ iconName: "memory"
+ percentage: ResourceUsage.memoryUsedPercentage
+ }
+
+ Resource {
+ iconName: "swap_horiz"
+ percentage: ResourceUsage.swapUsedPercentage
+ shown: (ConfigOptions.bar.resources.alwaysShowSwap && percentage > 0) ||
+ (MprisController.activePlayer?.trackTitle == null) ||
+ root.alwaysShowAllResources
+ Layout.leftMargin: shown ? 4 : 0
+ }
+
+ Resource {
+ iconName: "settings_slow_motion"
+ percentage: ResourceUsage.cpuUsage
+ shown: ConfigOptions.bar.resources.alwaysShowCpu ||
+ !(MprisController.activePlayer?.trackTitle?.length > 0) ||
+ root.alwaysShowAllResources
+ Layout.leftMargin: shown ? 4 : 0
+ }
+
+ }
+
+}
diff --git a/.config/quickshell/modules/bar/ScrollHint.qml b/.config/quickshell/modules/bar/ScrollHint.qml
new file mode 100644
index 000000000..f5f47c748
--- /dev/null
+++ b/.config/quickshell/modules/bar/ScrollHint.qml
@@ -0,0 +1,58 @@
+import "root:/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Revealer { // Scroll hint
+ id: root
+ property string icon
+ property string side: "left"
+ property string tooltipText: ""
+
+ MouseArea {
+ anchors.right: root.side === "left" ? parent.right : undefined
+ anchors.left: root.side === "right" ? parent.left : undefined
+ implicitWidth: contentColumnLayout.implicitWidth
+ implicitHeight: contentColumnLayout.implicitHeight
+ property bool hovered: false
+
+ hoverEnabled: true
+ onEntered: hovered = true
+ onExited: hovered = false
+ acceptedButtons: Qt.NoButton
+
+ // StyledToolTip {
+ // extraVisibleCondition: tooltipText.length > 0
+ // content: tooltipText
+ // }
+
+ ColumnLayout {
+ id: contentColumnLayout
+ anchors.centerIn: parent
+ spacing: -5
+ MaterialSymbol {
+ Layout.leftMargin: 5
+ Layout.rightMargin: 5
+ text: "keyboard_arrow_up"
+ iconSize: 14
+ color: Appearance.colors.colSubtext
+ }
+ MaterialSymbol {
+ Layout.leftMargin: 5
+ Layout.rightMargin: 5
+ text: root.icon
+ iconSize: 14
+ color: Appearance.colors.colSubtext
+ }
+ MaterialSymbol {
+ Layout.leftMargin: 5
+ Layout.rightMargin: 5
+ text: "keyboard_arrow_down"
+ iconSize: 14
+ color: Appearance.colors.colSubtext
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/bar/SysTray.qml b/.config/quickshell/modules/bar/SysTray.qml
new file mode 100644
index 000000000..4f76ddfa8
--- /dev/null
+++ b/.config/quickshell/modules/bar/SysTray.qml
@@ -0,0 +1,50 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Layouts
+import Quickshell.Hyprland
+import Quickshell.Services.SystemTray
+import Quickshell.Wayland
+import Quickshell.Widgets
+
+// TODO: More fancy animation
+Item {
+ id: root
+
+ required property var bar
+
+ height: parent.height
+ implicitWidth: rowLayout.implicitWidth
+ Layout.leftMargin: Appearance.rounding.screenRounding
+
+ RowLayout {
+ id: rowLayout
+
+ anchors.fill: parent
+ spacing: 15
+
+ Repeater {
+ model: SystemTray.items
+
+ SysTrayItem {
+ required property SystemTrayItem modelData
+
+ bar: root.bar
+ item: modelData
+ }
+
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignVCenter
+ font.pixelSize: Appearance.font.pixelSize.larger
+ color: Appearance.colors.colSubtext
+ text: "•"
+ visible: {
+ SystemTray.items.values.length > 0
+ }
+ }
+
+ }
+
+}
diff --git a/.config/quickshell/modules/bar/SysTrayItem.qml b/.config/quickshell/modules/bar/SysTrayItem.qml
new file mode 100644
index 000000000..1886e6a02
--- /dev/null
+++ b/.config/quickshell/modules/bar/SysTrayItem.qml
@@ -0,0 +1,66 @@
+import "root:/modules/common/"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Services.SystemTray
+import Quickshell.Widgets
+import Qt5Compat.GraphicalEffects
+
+MouseArea {
+ id: root
+
+ required property var bar
+ required property SystemTrayItem item
+ property bool targetMenuOpen: false
+ property int trayItemWidth: 16
+
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ Layout.fillHeight: true
+ implicitWidth: trayItemWidth
+ onClicked: (event) => {
+ switch (event.button) {
+ case Qt.LeftButton:
+ item.activate();
+ break;
+ case Qt.RightButton:
+ if (item.hasMenu) menu.open();
+ break;
+ }
+ event.accepted = true;
+ }
+
+ QsMenuAnchor {
+ id: menu
+
+ menu: root.item.menu
+ anchor.window: bar
+ anchor.rect.x: root.x + bar.width
+ anchor.rect.y: root.y
+ anchor.rect.height: root.height
+ anchor.edges: Edges.Bottom
+ }
+
+ IconImage {
+ id: trayIcon
+ visible: false // There's already color overlay
+ source: root.item.icon
+ anchors.centerIn: parent
+ width: parent.width
+ height: parent.height
+ }
+
+ Desaturate {
+ id: desaturatedIcon
+ visible: false // There's already color overlay
+ anchors.fill: trayIcon
+ source: trayIcon
+ desaturation: 1 // 1.0 means fully grayscale
+ }
+ ColorOverlay {
+ anchors.fill: desaturatedIcon
+ source: desaturatedIcon
+ color: ColorUtils.transparentize(Appearance.colors.colOnLayer0, 0.6)
+ }
+
+}
diff --git a/.config/quickshell/modules/bar/UtilButtons.qml b/.config/quickshell/modules/bar/UtilButtons.qml
new file mode 100644
index 000000000..9d9832a0a
--- /dev/null
+++ b/.config/quickshell/modules/bar/UtilButtons.qml
@@ -0,0 +1,85 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+import Quickshell.Services.Pipewire
+
+Item {
+ id: root
+ property bool borderless: ConfigOptions.bar.borderless
+ implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2
+
+ RowLayout {
+ id: rowLayout
+
+ spacing: 4
+ anchors.centerIn: parent
+
+ Loader {
+ active: ConfigOptions.bar.utilButtons.showScreenSnip
+ visible: ConfigOptions.bar.utilButtons.showScreenSnip
+ sourceComponent: CircleUtilButton {
+ Layout.alignment: Qt.AlignVCenter
+ onClicked: Hyprland.dispatch("exec hyprshot --freeze --clipboard-only --mode region --silent")
+ MaterialSymbol {
+ horizontalAlignment: Qt.AlignHCenter
+ fill: 1
+ text: "screenshot_region"
+ iconSize: Appearance.font.pixelSize.large
+ color: Appearance.colors.colOnLayer2
+ }
+ }
+ }
+
+ Loader {
+ active: ConfigOptions.bar.utilButtons.showColorPicker
+ visible: ConfigOptions.bar.utilButtons.showColorPicker
+ sourceComponent: CircleUtilButton {
+ Layout.alignment: Qt.AlignVCenter
+ onClicked: Hyprland.dispatch("exec hyprpicker -a")
+ MaterialSymbol {
+ horizontalAlignment: Qt.AlignHCenter
+ fill: 1
+ text: "colorize"
+ iconSize: Appearance.font.pixelSize.large
+ color: Appearance.colors.colOnLayer2
+ }
+ }
+ }
+
+ Loader {
+ active: ConfigOptions.bar.utilButtons.showKeyboardToggle
+ visible: ConfigOptions.bar.utilButtons.showKeyboardToggle
+ sourceComponent: CircleUtilButton {
+ Layout.alignment: Qt.AlignVCenter
+ onClicked: Hyprland.dispatch("global quickshell:oskToggle")
+ MaterialSymbol {
+ horizontalAlignment: Qt.AlignHCenter
+ fill: 0
+ text: "keyboard"
+ iconSize: Appearance.font.pixelSize.large
+ color: Appearance.colors.colOnLayer2
+ }
+ }
+ }
+
+ Loader {
+ active: ConfigOptions.bar.utilButtons.showMicToggle
+ visible: ConfigOptions.bar.utilButtons.showMicToggle
+ sourceComponent: CircleUtilButton {
+ Layout.alignment: Qt.AlignVCenter
+ onClicked: Hyprland.dispatch("exec wpctl set-mute @DEFAULT_SOURCE@ toggle")
+ MaterialSymbol {
+ horizontalAlignment: Qt.AlignHCenter
+ fill: 0
+ text: Pipewire.defaultAudioSource?.audio?.muted ? "mic_off" : "mic"
+ iconSize: Appearance.font.pixelSize.large
+ color: Appearance.colors.colOnLayer2
+ }
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/modules/bar/Workspaces.qml b/.config/quickshell/modules/bar/Workspaces.qml
new file mode 100644
index 000000000..93de99c0f
--- /dev/null
+++ b/.config/quickshell/modules/bar/Workspaces.qml
@@ -0,0 +1,270 @@
+import "root:/"
+import "root:/services/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Hyprland
+import Quickshell.Io
+import Quickshell.Widgets
+import Qt5Compat.GraphicalEffects
+
+Item {
+ required property var bar
+ property bool borderless: ConfigOptions.bar.borderless
+ readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen)
+ readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
+
+ readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / ConfigOptions.bar.workspaces.shown)
+ property list workspaceOccupied: []
+ property int widgetPadding: 4
+ property int workspaceButtonWidth: 26
+ property real workspaceIconSize: workspaceButtonWidth * 0.69
+ property real workspaceIconSizeShrinked: workspaceButtonWidth * 0.55
+ property real workspaceIconOpacityShrinked: 1
+ property real workspaceIconMarginShrinked: -4
+ property int workspaceIndexInGroup: (monitor.activeWorkspace?.id - 1) % ConfigOptions.bar.workspaces.shown
+
+ // Function to update workspaceOccupied
+ function updateWorkspaceOccupied() {
+ workspaceOccupied = Array.from({ length: ConfigOptions.bar.workspaces.shown }, (_, i) => {
+ return Hyprland.workspaces.values.some(ws => ws.id === workspaceGroup * ConfigOptions.bar.workspaces.shown + i + 1);
+ })
+ }
+
+ // Initialize workspaceOccupied when the component is created
+ Component.onCompleted: updateWorkspaceOccupied()
+
+ // Listen for changes in Hyprland.workspaces.values
+ Connections {
+ target: Hyprland.workspaces
+ function onValuesChanged() {
+ updateWorkspaceOccupied();
+ }
+ }
+
+ implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2
+ implicitHeight: 40
+
+ // Scroll to switch workspaces
+ WheelHandler {
+ onWheel: (event) => {
+ if (event.angleDelta.y < 0)
+ Hyprland.dispatch(`workspace r+1`);
+ else if (event.angleDelta.y > 0)
+ Hyprland.dispatch(`workspace r-1`);
+ }
+ acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.BackButton
+ onPressed: (event) => {
+ if (event.button === Qt.BackButton) {
+ Hyprland.dispatch(`togglespecialworkspace`);
+ }
+ }
+ }
+
+ // Workspaces - background
+ RowLayout {
+ id: rowLayout
+ z: 1
+
+ spacing: 0
+ anchors.fill: parent
+ implicitHeight: 40
+
+ Repeater {
+ model: ConfigOptions.bar.workspaces.shown
+
+ Rectangle {
+ z: 1
+ implicitWidth: workspaceButtonWidth
+ implicitHeight: workspaceButtonWidth
+ radius: Appearance.rounding.full
+ property var leftOccupied: (workspaceOccupied[index-1] && !(!activeWindow?.activated && monitor.activeWorkspace?.id === index))
+ property var rightOccupied: (workspaceOccupied[index+1] && !(!activeWindow?.activated && monitor.activeWorkspace?.id === index+2))
+ property var radiusLeft: leftOccupied ? 0 : Appearance.rounding.full
+ property var radiusRight: rightOccupied ? 0 : Appearance.rounding.full
+
+ topLeftRadius: radiusLeft
+ bottomLeftRadius: radiusLeft
+ topRightRadius: radiusRight
+ bottomRightRadius: radiusRight
+
+ color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4)
+ opacity: (workspaceOccupied[index] && !(!activeWindow?.activated && monitor.activeWorkspace?.id === index+1)) ? 1 : 0
+
+ Behavior on opacity {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+ Behavior on radiusLeft {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+
+ Behavior on radiusRight {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+
+ }
+
+ }
+
+ }
+
+ // Active workspace
+ Rectangle {
+ z: 2
+ // Make active ws indicator, which has a brighter color, smaller to look like it is of the same size as ws occupied highlight
+ property real activeWorkspaceMargin: 2
+ implicitHeight: workspaceButtonWidth - activeWorkspaceMargin * 2
+ radius: Appearance.rounding.full
+ color: Appearance.colors.colPrimary
+ anchors.verticalCenter: parent.verticalCenter
+
+ property real idx1: workspaceIndexInGroup
+ property real idx2: workspaceIndexInGroup
+ x: Math.min(idx1, idx2) * workspaceButtonWidth + activeWorkspaceMargin
+ implicitWidth: Math.abs(idx1 - idx2) * workspaceButtonWidth + workspaceButtonWidth - activeWorkspaceMargin * 2
+
+ Behavior on activeWorkspaceMargin {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Behavior on idx1 { // Leading anim
+ NumberAnimation {
+ duration: 100
+ easing.type: Easing.OutSine
+ }
+ }
+ Behavior on idx2 { // Following anim
+ NumberAnimation {
+ duration: 300
+ easing.type: Easing.OutSine
+ }
+ }
+ }
+
+ // Workspaces - numbers
+ RowLayout {
+ id: rowLayoutNumbers
+ z: 3
+
+ spacing: 0
+ anchors.fill: parent
+ implicitHeight: 40
+
+ Repeater {
+ model: ConfigOptions.bar.workspaces.shown
+
+ Button {
+ id: button
+ property int workspaceValue: workspaceGroup * ConfigOptions.bar.workspaces.shown + index + 1
+ Layout.fillHeight: true
+ onPressed: Hyprland.dispatch(`workspace ${workspaceValue}`)
+ width: workspaceButtonWidth
+
+ background: Item {
+ id: workspaceButtonBackground
+ implicitWidth: workspaceButtonWidth
+ implicitHeight: workspaceButtonWidth
+ property var biggestWindow: {
+ const windowsInThisWorkspace = HyprlandData.windowList.filter(w => w.workspace.id == button.workspaceValue)
+ return windowsInThisWorkspace.reduce((maxWin, win) => {
+ const maxArea = (maxWin?.size?.[0] ?? 0) * (maxWin?.size?.[1] ?? 0)
+ const winArea = (win?.size?.[0] ?? 0) * (win?.size?.[1] ?? 0)
+ return winArea > maxArea ? win : maxWin
+ }, null)
+ }
+ property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing")
+
+ StyledText { // Workspace number text
+ opacity: GlobalStates.workspaceShowNumbers
+ || ((ConfigOptions?.bar.workspaces.alwaysShowNumbers && (!ConfigOptions?.bar.workspaces.showAppIcons || !workspaceButtonBackground.biggestWindow || GlobalStates.workspaceShowNumbers))
+ || (GlobalStates.workspaceShowNumbers && !ConfigOptions?.bar.workspaces.showAppIcons)
+ ) ? 1 : 0
+ z: 3
+
+ anchors.centerIn: parent
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ font.pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2)
+ text: `${button.workspaceValue}`
+ elide: Text.ElideRight
+ color: (monitor.activeWorkspace?.id == button.workspaceValue) ?
+ Appearance.m3colors.m3onPrimary :
+ (workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
+ Appearance.colors.colOnLayer1Inactive)
+
+ Behavior on opacity {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ }
+ Rectangle { // Dot instead of ws number
+ opacity: (ConfigOptions?.bar.workspaces.alwaysShowNumbers
+ || GlobalStates.workspaceShowNumbers
+ || (ConfigOptions?.bar.workspaces.showAppIcons && workspaceButtonBackground.biggestWindow)
+ ) ? 0 : 1
+ visible: opacity > 0
+ anchors.centerIn: parent
+ width: workspaceButtonWidth * 0.18
+ height: width
+ radius: width / 2
+ color: (monitor.activeWorkspace?.id == button.workspaceValue) ?
+ Appearance.m3colors.m3onPrimary :
+ (workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
+ Appearance.colors.colOnLayer1Inactive)
+
+ Behavior on opacity {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ }
+ Item { // Main app icon
+ anchors.centerIn: parent
+ width: workspaceButtonWidth
+ height: workspaceButtonWidth
+ opacity: !ConfigOptions?.bar.workspaces.showAppIcons ? 0 :
+ (workspaceButtonBackground.biggestWindow && !GlobalStates.workspaceShowNumbers && ConfigOptions?.bar.workspaces.showAppIcons) ?
+ 1 : workspaceButtonBackground.biggestWindow ? workspaceIconOpacityShrinked : 0
+ visible: opacity > 0
+ IconImage {
+ id: mainAppIcon
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ anchors.bottomMargin: (!GlobalStates.workspaceShowNumbers && ConfigOptions?.bar.workspaces.showAppIcons) ?
+ (workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked
+ anchors.rightMargin: (!GlobalStates.workspaceShowNumbers && ConfigOptions?.bar.workspaces.showAppIcons) ?
+ (workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked
+
+ source: workspaceButtonBackground.mainAppIconSource
+ implicitSize: (!GlobalStates.workspaceShowNumbers && ConfigOptions?.bar.workspaces.showAppIcons) ? workspaceIconSize : workspaceIconSizeShrinked
+
+ Behavior on opacity {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Behavior on anchors.bottomMargin {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Behavior on anchors.rightMargin {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Behavior on implicitSize {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ }
+ }
+ }
+
+
+ }
+
+ }
+
+ }
+
+}
diff --git a/.config/quickshell/modules/cheatsheet/Cheatsheet.qml b/.config/quickshell/modules/cheatsheet/Cheatsheet.qml
new file mode 100644
index 000000000..79711f88f
--- /dev/null
+++ b/.config/quickshell/modules/cheatsheet/Cheatsheet.qml
@@ -0,0 +1,165 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Scope { // Scope
+ id: root
+
+ Loader {
+ id: cheatsheetLoader
+ active: false
+
+ sourceComponent: PanelWindow { // Window
+ id: cheatsheetRoot
+ visible: cheatsheetLoader.active
+
+ anchors {
+ top: true
+ bottom: true
+ left: true
+ right: true
+ }
+
+ function hide() {
+ cheatsheetLoader.active = false
+ }
+ exclusiveZone: 0
+ implicitWidth: cheatsheetBackground.width + Appearance.sizes.elevationMargin * 2
+ implicitHeight: cheatsheetBackground.height + Appearance.sizes.elevationMargin * 2
+ WlrLayershell.namespace: "quickshell:cheatsheet"
+ // Hyprland 0.49: Focus is always exclusive and setting this breaks mouse focus grab
+ // WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
+ color: "transparent"
+
+ mask: Region {
+ item: cheatsheetBackground
+ }
+
+ HyprlandFocusGrab { // Click outside to close
+ id: grab
+ windows: [ cheatsheetRoot ]
+ active: cheatsheetRoot.visible
+ onCleared: () => {
+ if (!active) cheatsheetRoot.hide()
+ }
+ }
+
+
+ // Background
+ StyledRectangularShadow {
+ target: cheatsheetBackground
+ }
+ Rectangle {
+ id: cheatsheetBackground
+ anchors.centerIn: parent
+ color: Appearance.colors.colLayer0
+ radius: Appearance.rounding.windowRounding
+ property real padding: 30
+ implicitWidth: cheatsheetColumnLayout.implicitWidth + padding * 2
+ implicitHeight: cheatsheetColumnLayout.implicitHeight + padding * 2
+
+ Keys.onPressed: (event) => { // Esc to close
+ if (event.key === Qt.Key_Escape) {
+ cheatsheetRoot.hide()
+ }
+ }
+
+ RippleButton { // Close button
+ id: closeButton
+ focus: cheatsheetRoot.visible
+ implicitWidth: 40
+ implicitHeight: 40
+ buttonRadius: Appearance.rounding.full
+ anchors {
+ top: parent.top
+ right: parent.right
+ topMargin: 20
+ rightMargin: 20
+ }
+
+ onClicked: {
+ cheatsheetRoot.hide()
+ }
+
+ contentItem: MaterialSymbol {
+ anchors.centerIn: parent
+ horizontalAlignment: Text.AlignHCenter
+ font.pixelSize: Appearance.font.pixelSize.title
+ text: "close"
+ }
+ }
+
+ ColumnLayout { // Real content
+ id: cheatsheetColumnLayout
+ anchors.centerIn: parent
+ spacing: 20
+
+ StyledText {
+ id: cheatsheetTitle
+ Layout.alignment: Qt.AlignHCenter
+ font.family: Appearance.font.family.title
+ font.pixelSize: Appearance.font.pixelSize.title
+ text: qsTr("Cheat sheet")
+ }
+ CheatsheetKeybinds {}
+ }
+ }
+
+ }
+ }
+
+ IpcHandler {
+ target: "cheatsheet"
+
+ function toggle(): void {
+ cheatsheetLoader.active = !cheatsheetLoader.active
+ }
+
+ function close(): void {
+ cheatsheetLoader.active = false
+ }
+
+ function open(): void {
+ cheatsheetLoader.active = true
+ }
+ }
+
+ GlobalShortcut {
+ name: "cheatsheetToggle"
+ description: qsTr("Toggles cheatsheet on press")
+
+ onPressed: {
+ cheatsheetLoader.active = !cheatsheetLoader.active;
+ }
+ }
+
+ GlobalShortcut {
+ name: "cheatsheetOpen"
+ description: qsTr("Opens cheatsheet on press")
+
+ onPressed: {
+ cheatsheetLoader.active = true;
+ }
+ }
+
+ GlobalShortcut {
+ name: "cheatsheetClose"
+ description: qsTr("Closes cheatsheet on press")
+
+ onPressed: {
+ cheatsheetLoader.active = false;
+ }
+ }
+
+}
diff --git a/.config/quickshell/modules/cheatsheet/CheatsheetKeybinds.qml b/.config/quickshell/modules/cheatsheet/CheatsheetKeybinds.qml
new file mode 100644
index 000000000..03b031153
--- /dev/null
+++ b/.config/quickshell/modules/cheatsheet/CheatsheetKeybinds.qml
@@ -0,0 +1,149 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/file_utils.js" as FileUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Widgets
+import Quickshell.Hyprland
+
+Item {
+ id: root
+ readonly property var keybinds: HyprlandKeybinds.keybinds
+ property real spacing: 20
+ property real titleSpacing: 7
+ implicitWidth: rowLayout.implicitWidth
+ implicitHeight: rowLayout.implicitHeight
+
+ property var keyBlacklist: ["Super_L"]
+ property var keySubstitutions: ({
+ "Super": "",
+ "mouse_up": "Scroll ↓", // ikr, weird
+ "mouse_down": "Scroll ↑", // trust me bro
+ "mouse:272": "LMB",
+ "mouse:273": "RMB",
+ "mouse:275": "MouseBack",
+ "Slash": "/",
+ "Hash": "#",
+ "Return": "Enter",
+ // "Shift": "",
+ })
+
+ RowLayout { // Keybind columns
+ id: rowLayout
+ spacing: root.spacing
+ Repeater {
+ model: keybinds.children
+
+ delegate: ColumnLayout { // Keybind sections
+ spacing: root.spacing
+ required property var modelData
+ Layout.alignment: Qt.AlignTop
+ Repeater {
+ model: modelData.children
+
+ delegate: Item { // Section with real keybinds
+ required property var modelData
+ implicitWidth: sectionColumnLayout.implicitWidth
+ implicitHeight: sectionColumnLayout.implicitHeight
+ ColumnLayout {
+ id: sectionColumnLayout
+ anchors.centerIn: parent
+ spacing: root.titleSpacing
+ StyledText {
+ id: sectionTitle
+ font.family: Appearance.font.family.title
+ font.pixelSize: Appearance.font.pixelSize.huge
+ color: Appearance.colors.colOnLayer0
+ text: modelData.name
+ }
+
+ GridLayout {
+ id: keybindGrid
+ columns: 2
+ Repeater {
+ model: {
+ var result = [];
+ for (var i = 0; i < modelData.keybinds.length; i++) {
+ const keybind = modelData.keybinds[i];
+ result.push({
+ "type": "keys",
+ "mods": keybind.mods,
+ "key": keybind.key,
+ });
+ result.push({
+ "type": "comment",
+ "comment": keybind.comment,
+ });
+ }
+ return result;
+ }
+ delegate: Item {
+ required property var modelData
+ implicitWidth: keybindLoader.implicitWidth
+ implicitHeight: keybindLoader.implicitHeight
+
+ Loader {
+ id: keybindLoader
+ sourceComponent: (modelData.type === "keys") ? keysComponent : commentComponent
+ }
+
+ Component {
+ id: keysComponent
+ RowLayout {
+ spacing: 4
+ Repeater {
+ model: modelData.mods
+ delegate: KeyboardKey {
+ required property var modelData
+ key: keySubstitutions[modelData] || modelData
+ }
+ }
+ StyledText {
+ id: keybindPlus
+ visible: !keyBlacklist.includes(modelData.key) && modelData.mods.length > 0
+ Layout.alignment: Qt.AlignVCenter
+ text: "+"
+ }
+ KeyboardKey {
+ id: keybindKey
+ visible: !keyBlacklist.includes(modelData.key)
+ key: keySubstitutions[modelData.key] || modelData.key
+ color: Appearance.colors.colOnLayer0
+ }
+ }
+ }
+
+ Component {
+ id: commentComponent
+ Item {
+ id: commentItem
+ implicitWidth: commentText.implicitWidth + 8 * 2
+ implicitHeight: commentText.implicitHeight
+
+ StyledText {
+ id: commentText
+ anchors.centerIn: parent
+ font.pixelSize: Appearance.font.pixelSize.smaller
+ text: modelData.comment
+ }
+ }
+ }
+ }
+
+ }
+ }
+ }
+ }
+
+ }
+ }
+
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/common/Appearance.qml b/.config/quickshell/modules/common/Appearance.qml
new file mode 100644
index 000000000..7622b29a4
--- /dev/null
+++ b/.config/quickshell/modules/common/Appearance.qml
@@ -0,0 +1,304 @@
+import QtQuick
+import Quickshell
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+Singleton {
+ id: root
+ property QtObject m3colors
+ property QtObject animation
+ property QtObject animationCurves
+ property QtObject colors
+ property QtObject rounding
+ property QtObject font
+ property QtObject sizes
+ property string syntaxHighlightingTheme
+
+ // Extremely conservative transparency values for consistency and readability
+ property real transparency: ConfigOptions?.appearance.transparency ? (m3colors.darkmode ? 0.1 : 0) : 0
+ property real contentTransparency: ConfigOptions?.appearance.transparency ? (m3colors.darkmode ? 0.55 : 0) : 0
+
+ m3colors: QtObject {
+ property bool darkmode: false
+ property bool transparent: false
+ property color m3primary_paletteKeyColor: "#91689E"
+ property color m3secondary_paletteKeyColor: "#837186"
+ property color m3tertiary_paletteKeyColor: "#9D6A67"
+ property color m3neutral_paletteKeyColor: "#7C757B"
+ property color m3neutral_variant_paletteKeyColor: "#7D747D"
+ property color m3background: "#161217"
+ property color m3onBackground: "#EAE0E7"
+ property color m3surface: "#161217"
+ property color m3surfaceDim: "#161217"
+ property color m3surfaceBright: "#3D373D"
+ property color m3surfaceContainerLowest: "#110D12"
+ property color m3surfaceContainerLow: "#1F1A1F"
+ property color m3surfaceContainer: "#231E23"
+ property color m3surfaceContainerHigh: "#2D282E"
+ property color m3surfaceContainerHighest: "#383339"
+ property color m3onSurface: "#EAE0E7"
+ property color m3surfaceVariant: "#4C444D"
+ property color m3onSurfaceVariant: "#CFC3CD"
+ property color m3inverseSurface: "#EAE0E7"
+ property color m3inverseOnSurface: "#342F34"
+ property color m3outline: "#988E97"
+ property color m3outlineVariant: "#4C444D"
+ property color m3shadow: "#000000"
+ property color m3scrim: "#000000"
+ property color m3surfaceTint: "#E5B6F2"
+ property color m3primary: "#E5B6F2"
+ property color m3onPrimary: "#452152"
+ property color m3primaryContainer: "#5D386A"
+ property color m3onPrimaryContainer: "#F9D8FF"
+ property color m3inversePrimary: "#775084"
+ property color m3secondary: "#D5C0D7"
+ property color m3onSecondary: "#392C3D"
+ property color m3secondaryContainer: "#534457"
+ property color m3onSecondaryContainer: "#F2DCF3"
+ property color m3tertiary: "#F5B7B3"
+ property color m3onTertiary: "#4C2523"
+ property color m3tertiaryContainer: "#BA837F"
+ property color m3onTertiaryContainer: "#000000"
+ property color m3error: "#FFB4AB"
+ property color m3onError: "#690005"
+ property color m3errorContainer: "#93000A"
+ property color m3onErrorContainer: "#FFDAD6"
+ property color m3primaryFixed: "#F9D8FF"
+ property color m3primaryFixedDim: "#E5B6F2"
+ property color m3onPrimaryFixed: "#2E0A3C"
+ property color m3onPrimaryFixedVariant: "#5D386A"
+ property color m3secondaryFixed: "#F2DCF3"
+ property color m3secondaryFixedDim: "#D5C0D7"
+ property color m3onSecondaryFixed: "#241727"
+ property color m3onSecondaryFixedVariant: "#514254"
+ property color m3tertiaryFixed: "#FFDAD7"
+ property color m3tertiaryFixedDim: "#F5B7B3"
+ property color m3onTertiaryFixed: "#331110"
+ property color m3onTertiaryFixedVariant: "#663B39"
+ property color m3success: "#B5CCBA"
+ property color m3onSuccess: "#213528"
+ property color m3successContainer: "#374B3E"
+ property color m3onSuccessContainer: "#D1E9D6"
+ property color term0: "#EDE4E4"
+ property color term1: "#B52755"
+ property color term2: "#A97363"
+ property color term3: "#AF535D"
+ property color term4: "#A67F7C"
+ property color term5: "#B2416B"
+ property color term6: "#8D76AD"
+ property color term7: "#272022"
+ property color term8: "#0E0D0D"
+ property color term9: "#B52755"
+ property color term10: "#A97363"
+ property color term11: "#AF535D"
+ property color term12: "#A67F7C"
+ property color term13: "#B2416B"
+ property color term14: "#8D76AD"
+ property color term15: "#221A1A"
+ }
+
+ colors: QtObject {
+ property color colSubtext: m3colors.m3outline
+ property color colLayer0: ColorUtils.transparentize(m3colors.m3background, root.transparency)
+ property color colOnLayer0: m3colors.m3onBackground
+ property color colLayer0Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.9, root.contentTransparency))
+ property color colLayer0Active: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.8, root.contentTransparency))
+ property color colLayer1: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3surfaceContainerLow, m3colors.m3background, 0.8), root.contentTransparency);
+ property color colOnLayer1: m3colors.m3onSurfaceVariant;
+ property color colOnLayer1Inactive: ColorUtils.mix(colOnLayer1, colLayer1, 0.45);
+ property color colLayer2: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3surfaceContainer, m3colors.m3surfaceContainerHigh, 0.7), root.contentTransparency)
+ property color colOnLayer2: m3colors.m3onSurface;
+ property color colOnLayer2Disabled: ColorUtils.mix(colOnLayer2, m3colors.m3background, 0.4);
+ property color colLayer3: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3surfaceContainerHigh, m3colors.m3onSurface, 0.96), root.contentTransparency)
+ property color colOnLayer3: m3colors.m3onSurface;
+ property color colLayer1Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.92), root.contentTransparency)
+ property color colLayer1Active: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.85), root.contentTransparency);
+ property color colLayer2Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer2, colOnLayer2, 0.90), root.contentTransparency)
+ property color colLayer2Active: ColorUtils.transparentize(ColorUtils.mix(colLayer2, colOnLayer2, 0.80), root.contentTransparency);
+ property color colLayer2Disabled: ColorUtils.transparentize(ColorUtils.mix(colLayer2, m3colors.m3background, 0.8), root.contentTransparency);
+ property color colLayer3Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer3, colOnLayer3, 0.90), root.contentTransparency)
+ property color colLayer3Active: ColorUtils.transparentize(ColorUtils.mix(colLayer3, colOnLayer3, 0.80), root.contentTransparency);
+ property color colPrimary: m3colors.m3primary
+ property color colOnPrimary: m3colors.m3onPrimary
+ property color colPrimaryHover: ColorUtils.mix(colors.colPrimary, colLayer1Hover, 0.87)
+ property color colPrimaryActive: ColorUtils.mix(colors.colPrimary, colLayer1Active, 0.7)
+ property color colPrimaryContainer: m3colors.m3primaryContainer
+ property color colPrimaryContainerHover: ColorUtils.mix(colors.colPrimaryContainer, colLayer1Hover, 0.7)
+ property color colPrimaryContainerActive: ColorUtils.mix(colors.colPrimaryContainer, colLayer1Active, 0.6)
+ property color colSecondary: m3colors.m3secondary
+ property color colSecondaryHover: ColorUtils.mix(m3colors.m3secondary, colLayer1Hover, 0.85)
+ property color colSecondaryActive: ColorUtils.mix(m3colors.m3secondary, colLayer1Active, 0.4)
+ property color colSecondaryContainer: ColorUtils.transparentize(m3colors.m3secondaryContainer, root.contentTransparency)
+ property color colSecondaryContainerHover: ColorUtils.mix(m3colors.m3secondaryContainer, colLayer1Hover, 0.6)
+ property color colSecondaryContainerActive: ColorUtils.mix(m3colors.m3secondaryContainer, colLayer1Active, 0.54)
+ property color colOnSecondaryContainer: m3colors.m3onSecondaryContainer
+ property color colSurfaceContainerLow: ColorUtils.transparentize(m3colors.m3surfaceContainerLow, root.contentTransparency)
+ property color colSurfaceContainer: ColorUtils.transparentize(m3colors.m3surfaceContainer, root.contentTransparency)
+ property color colSurfaceContainerHigh: ColorUtils.transparentize(m3colors.m3surfaceContainerHigh, root.contentTransparency)
+ property color colSurfaceContainerHighest: ColorUtils.transparentize(m3colors.m3surfaceContainerHighest, root.contentTransparency)
+ property color colSurfaceContainerHighestHover: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.95)
+ property color colSurfaceContainerHighestActive: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.85)
+ property color colTooltip: m3colors.darkmode ? ColorUtils.mix(m3colors.m3background, "#3C4043", 0.5) : "#3C4043" // m3colors.m3inverseSurface in the specs, but the m3 website actually uses #3C4043
+ property color colOnTooltip: "#F8F9FA" // m3colors.m3inverseOnSurface in the specs, but the m3 website actually uses this color
+ property color colScrim: ColorUtils.transparentize(m3colors.m3scrim, 0.5)
+ property color colShadow: ColorUtils.transparentize(m3colors.m3shadow, 0.7)
+ property color colOutlineVariant: m3colors.m3outlineVariant
+ }
+
+ rounding: QtObject {
+ property int unsharpen: 2
+ property int verysmall: 8
+ property int small: 12
+ property int normal: 17
+ property int large: 23
+ property int verylarge: 30
+ property int full: 9999
+ property int screenRounding: large
+ property int windowRounding: 18
+ }
+
+ font: QtObject {
+ property QtObject family: QtObject {
+ property string main: "Rubik"
+ property string title: "Gabarito"
+ property string iconMaterial: "Material Symbols Rounded"
+ property string iconNerd: "SpaceMono NF"
+ property string monospace: "JetBrains Mono NF"
+ property string reading: "Readex Pro"
+ }
+ property QtObject pixelSize: QtObject {
+ property int smallest: 10
+ property int smaller: 13
+ property int small: 15
+ property int normal: 16
+ property int large: 17
+ property int larger: 19
+ property int huge: 22
+ property int hugeass: 23
+ property int title: 28
+ }
+ }
+
+ animationCurves: QtObject {
+ readonly property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.90, 1, 1] // Default, 350ms
+ readonly property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1.00, 1, 1] // Default, 500ms
+ readonly property list expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1] // Default, 650ms
+ readonly property list expressiveEffects: [0.34, 0.80, 0.34, 1.00, 1, 1] // Default, 200ms
+ readonly property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
+ readonly property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
+ readonly property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
+ readonly property list standard: [0.2, 0, 0, 1, 1, 1]
+ readonly property list standardAccel: [0.3, 0, 1, 1, 1, 1]
+ readonly property list standardDecel: [0, 0, 0, 1, 1, 1]
+ }
+
+ animation: QtObject {
+ property QtObject elementMove: QtObject {
+ property int duration: 500
+ property int type: Easing.BezierSpline
+ property list bezierCurve: animationCurves.expressiveDefaultSpatial
+ property int velocity: 650
+ property Component numberAnimation: Component {
+ NumberAnimation {
+ duration: root.animation.elementMove.duration
+ easing.type: root.animation.elementMove.type
+ easing.bezierCurve: root.animation.elementMove.bezierCurve
+ }
+ }
+ property Component colorAnimation: Component {
+ ColorAnimation {
+ duration: root.animation.elementMove.duration
+ easing.type: root.animation.elementMove.type
+ easing.bezierCurve: root.animation.elementMove.bezierCurve
+ }
+ }
+ }
+ property QtObject elementMoveEnter: QtObject {
+ property int duration: 400
+ property int type: Easing.BezierSpline
+ property list bezierCurve: animationCurves.emphasizedDecel
+ property int velocity: 650
+ property Component numberAnimation: Component {
+ NumberAnimation {
+ duration: root.animation.elementMoveEnter.duration
+ easing.type: root.animation.elementMoveEnter.type
+ easing.bezierCurve: root.animation.elementMoveEnter.bezierCurve
+ }
+ }
+ }
+ property QtObject elementMoveExit: QtObject {
+ property int duration: 200
+ property int type: Easing.BezierSpline
+ property list bezierCurve: animationCurves.emphasizedAccel
+ property int velocity: 650
+ property Component numberAnimation: Component {
+ NumberAnimation {
+ duration: root.animation.elementMoveExit.duration
+ easing.type: root.animation.elementMoveExit.type
+ easing.bezierCurve: root.animation.elementMoveExit.bezierCurve
+ }
+ }
+ }
+ property QtObject elementMoveFast: QtObject {
+ property int duration: 200
+ property int type: Easing.BezierSpline
+ property list bezierCurve: animationCurves.expressiveEffects
+ property int velocity: 850
+ property Component colorAnimation: Component { ColorAnimation {
+ duration: root.animation.elementMoveFast.duration
+ easing.type: root.animation.elementMoveFast.type
+ easing.bezierCurve: root.animation.elementMoveFast.bezierCurve
+ }}
+ property Component numberAnimation: Component { NumberAnimation {
+ duration: root.animation.elementMoveFast.duration
+ easing.type: root.animation.elementMoveFast.type
+ easing.bezierCurve: root.animation.elementMoveFast.bezierCurve
+ }}
+ }
+
+ property QtObject clickBounce: QtObject {
+ property int duration: 200
+ property int type: Easing.BezierSpline
+ property list bezierCurve: animationCurves.expressiveFastSpatial
+ property int velocity: 850
+ property Component numberAnimation: Component { NumberAnimation {
+ duration: root.animation.clickBounce.duration
+ easing.type: root.animation.clickBounce.type
+ easing.bezierCurve: root.animation.clickBounce.bezierCurve
+ }}
+ }
+ property QtObject scroll: QtObject {
+ property int duration: 400
+ property int type: Easing.BezierSpline
+ property list bezierCurve: animationCurves.standardDecel
+ }
+ property QtObject menuDecel: QtObject {
+ property int duration: 350
+ property int type: Easing.OutExpo
+ }
+ }
+
+ sizes: QtObject {
+ property real barHeight: 40
+ property real barCenterSideModuleWidth: ConfigOptions?.bar.verbose ? 360 : 140
+ property real barCenterSideModuleWidthShortened: 280
+ property real barCenterSideModuleWidthHellaShortened: 190
+ property real barShortenScreenWidthThreshold: 1200 // Shorten if screen width is at most this value
+ property real barHellaShortenScreenWidthThreshold: 1000 // Shorten even more...
+ property real sidebarWidth: 460
+ property real sidebarWidthExtended: 750
+ property real osdWidth: 200
+ property real mediaControlsWidth: 440
+ property real mediaControlsHeight: 160
+ property real notificationPopupWidth: 410
+ property real searchWidthCollapsed: 260
+ property real searchWidth: 450
+ property real hyprlandGapsOut: 5
+ property real elevationMargin: 10
+ property real fabShadowRadius: 5
+ property real fabHoveredShadowRadius: 7
+ }
+
+ syntaxHighlightingTheme: Appearance.m3colors.darkmode ? "Monokai" : "ayu Light"
+}
diff --git a/.config/quickshell/modules/common/ConfigOptions.qml b/.config/quickshell/modules/common/ConfigOptions.qml
new file mode 100644
index 000000000..1a776a203
--- /dev/null
+++ b/.config/quickshell/modules/common/ConfigOptions.qml
@@ -0,0 +1,144 @@
+import QtQuick
+import Quickshell
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+Singleton {
+ property QtObject ai: QtObject {
+ property string systemPrompt: qsTr("Use casual tone. No user knowledge is to be assumed except basic Linux literacy. Be brief and concise: When explaining concepts, use bullet points (prefer minus sign (-) over asterisk (*)) and highlight keywords in bold to pinpoint the main concepts instead of long paragraphs. You are also encouraged to split your response with h2 headers, each header title beginning with an emoji, like `## 🐧 Linux`. When making changes to the user's config, you must get the config to know what values there are before setting.")
+ }
+
+ property QtObject appearance: QtObject {
+ property int fakeScreenRounding: 2 // 0: None | 1: Always | 2: When not fullscreen
+ property bool transparency: false
+ }
+
+ property QtObject audio: QtObject { // Values in %
+ property QtObject protection: QtObject { // Prevent sudden bangs
+ property bool enable: true
+ property real maxAllowedIncrease: 10
+ property real maxAllowed: 90 // Realistically should already provide some protection when it's 99...
+ }
+ }
+
+ property QtObject apps: QtObject {
+ property string bluetooth: "kcmshell6 kcm_bluetooth"
+ property string imageViewer: "loupe"
+ property string network: "plasmawindowed org.kde.plasma.networkmanagement"
+ property string networkEthernet: "kcmshell6 kcm_networkmanagement"
+ property string settings: "systemsettings"
+ property string taskManager: "plasma-systemmonitor --page-name Processes"
+ property string terminal: "kitty -1" // This is only for shell actions
+ }
+
+ property QtObject battery: QtObject {
+ property int low: 20
+ property int critical: 5
+ property int suspend: 2
+ }
+
+ property QtObject bar: QtObject {
+ property bool bottom: false // Instead of top
+ property bool borderless: false // true for no grouping of items
+ property string topLeftIcon: "spark" // Options: distro, spark
+ property bool showBackground: true
+ property bool verbose: true
+ property QtObject resources: QtObject {
+ property bool alwaysShowSwap: true
+ property bool alwaysShowCpu: false
+ }
+ property list screenList: [] // List of names, like "eDP-1", find out with 'hyprctl monitors' command
+ property QtObject utilButtons: QtObject {
+ property bool showScreenSnip: true
+ property bool showColorPicker: false
+ property bool showMicToggle: false
+ property bool showKeyboardToggle: true
+ }
+ property QtObject workspaces: QtObject {
+ property int shown: 10
+ property bool showAppIcons: true
+ property bool alwaysShowNumbers: false
+ property int showNumberDelay: 300 // milliseconds
+ }
+ }
+
+ property QtObject dock: QtObject {
+ property real height: 60
+ property real hoverRegionHeight: 3
+ property bool pinnedOnStartup: false
+ property bool hoverToReveal: false // When false, only reveals on empty workspace
+ property list pinnedApps: [ // IDs of pinned entries
+ "org.kde.dolphin",
+ "kitty",
+ ]
+ }
+
+ property QtObject language: QtObject {
+ property QtObject translator: QtObject {
+ property string engine: "auto" // Run `trans -list-engines` for available engines. auto should use google
+ property string targetLanguage: "auto" // Run `trans -list-all` for available languages
+ property string sourceLanguage: "auto"
+ }
+ }
+
+ property QtObject networking: QtObject {
+ property string userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
+ }
+
+ property QtObject osd: QtObject {
+ property int timeout: 1000
+ }
+
+ property QtObject osk: QtObject {
+ property string layout: "qwerty_full"
+ property bool pinnedOnStartup: false
+ }
+
+ property QtObject overview: QtObject {
+ property real scale: 0.18 // Relative to screen size
+ property real numOfRows: 2
+ property real numOfCols: 5
+ property bool showXwaylandIndicator: true
+ }
+
+ property QtObject resources: QtObject {
+ property int updateInterval: 3000
+ }
+
+ property QtObject search: QtObject {
+ property int nonAppResultDelay: 30 // This prevents lagging when typing
+ property string engineBaseUrl: "https://www.google.com/search?q="
+ property list excludedSites: [ "quora.com" ]
+ property bool sloppy: false // Uses levenshtein distance based scoring instead of fuzzy sort. Very weird.
+ property QtObject prefix: QtObject {
+ property string action: "/"
+ property string clipboard: ";"
+ property string emojis: ":"
+ }
+ }
+
+ property QtObject sidebar: QtObject {
+ property QtObject translator: QtObject {
+ property int delay: 300 // Delay before sending request. Reduces (potential) rate limits and lag.
+ }
+ property QtObject booru: QtObject {
+ property bool allowNsfw: false
+ property string defaultProvider: "yandere"
+ property int limit: 20
+ property QtObject zerochan: QtObject {
+ property string username: "[unset]"
+ }
+ }
+ }
+
+ property QtObject time: QtObject {
+ // https://doc.qt.io/qt-6/qtime.html#toString
+ property string format: "hh:mm"
+ property string dateFormat: "dddd, dd/MM"
+ }
+
+ property QtObject hacks: QtObject {
+ property int arbitraryRaceConditionDelay: 20 // milliseconds
+ }
+
+}
diff --git a/.config/quickshell/modules/common/Directories.qml b/.config/quickshell/modules/common/Directories.qml
new file mode 100644
index 000000000..d2c570234
--- /dev/null
+++ b/.config/quickshell/modules/common/Directories.qml
@@ -0,0 +1,43 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common/functions/file_utils.js" as FileUtils
+import Qt.labs.platform
+import QtQuick
+import Quickshell
+import Quickshell.Hyprland
+
+Singleton {
+ // XDG Dirs, with "file://"
+ readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0]
+ readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0]
+ readonly property string cache: StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]
+ readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
+ readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0]
+
+ // Other dirs used by the shell, without "file://"
+ property string favicons: FileUtils.trimFileProtocol(`${Directories.cache}/media/favicons`)
+ property string coverArt: FileUtils.trimFileProtocol(`${Directories.cache}/media/coverart`)
+ property string booruPreviews: FileUtils.trimFileProtocol(`${Directories.cache}/media/boorus`)
+ property string booruDownloads: FileUtils.trimFileProtocol(Directories.pictures + "/homework")
+ property string booruDownloadsNsfw: FileUtils.trimFileProtocol(Directories.pictures + "/homework/🌶️")
+ property string latexOutput: FileUtils.trimFileProtocol(`${Directories.cache}/media/latex`)
+ property string shellConfig: FileUtils.trimFileProtocol(`${Directories.config}/illogical-impulse`)
+ property string shellConfigName: "config.json"
+ property string shellConfigPath: `${Directories.shellConfig}/${Directories.shellConfigName}`
+ property string todoPath: FileUtils.trimFileProtocol(`${Directories.state}/user/todo.json`)
+ property string notificationsPath: FileUtils.trimFileProtocol(`${Directories.cache}/notifications/notifications.json`)
+ property string generatedMaterialThemePath: FileUtils.trimFileProtocol(`${Directories.state}/user/generated/colors.json`)
+ property string cliphistDecode: FileUtils.trimFileProtocol(`/tmp/quickshell/media/cliphist`)
+ property string wallpaperSwitchScriptPath: FileUtils.trimFileProtocol(`${Directories.config}/quickshell/scripts/switchwall.sh`)
+ // Cleanup on init
+ Component.onCompleted: {
+ Hyprland.dispatch(`exec mkdir -p '${shellConfig}'`)
+ Hyprland.dispatch(`exec mkdir -p '${favicons}'`)
+ Hyprland.dispatch(`exec rm -rf '${coverArt}'; mkdir -p '${coverArt}'`)
+ Hyprland.dispatch(`exec rm -rf '${booruPreviews}'; mkdir -p '${booruPreviews}'`)
+ Hyprland.dispatch(`exec mkdir -p '${booruDownloads}' && mkdir -p '${booruDownloadsNsfw}'`)
+ Hyprland.dispatch(`exec rm -rf '${latexOutput}'; mkdir -p '${latexOutput}'`)
+ Hyprland.dispatch(`exec rm -rf '${cliphistDecode}'; mkdir -p '${cliphistDecode}'`)
+ }
+}
diff --git a/.config/quickshell/modules/common/PersistentStates.qml b/.config/quickshell/modules/common/PersistentStates.qml
new file mode 100644
index 000000000..6887b4795
--- /dev/null
+++ b/.config/quickshell/modules/common/PersistentStates.qml
@@ -0,0 +1,23 @@
+import QtQuick
+import Quickshell
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+Singleton {
+ property QtObject ai: QtObject {
+ property string model
+ property real temperature: 0.5
+ }
+
+ property QtObject sidebar: QtObject {
+ property QtObject bottomGroup: QtObject {
+ property bool collapsed: false
+ }
+ }
+
+ property QtObject booru: QtObject {
+ property bool allowNsfw: false
+ property string provider: "yandere"
+ }
+
+}
diff --git a/.config/quickshell/modules/common/functions/color_utils.js b/.config/quickshell/modules/common/functions/color_utils.js
new file mode 100644
index 000000000..eb0fc0c2f
--- /dev/null
+++ b/.config/quickshell/modules/common/functions/color_utils.js
@@ -0,0 +1,109 @@
+// This module provides high level utility functions for color manipulation.
+
+/**
+ * Returns a color with the hue of color2 and the saturation, value, and alpha of color1.
+ *
+ * @param {string} color1 - The base color (any Qt.color-compatible string).
+ * @param {string} color2 - The color to take hue from.
+ * @returns {Qt.rgba} The resulting color.
+ */
+function colorWithHueOf(color1, color2) {
+ var c1 = Qt.color(color1);
+ var c2 = Qt.color(color2);
+
+ // Qt.color hsvHue/hsvSaturation/hsvValue/alpha return 0-1
+ var hue = c2.hsvHue;
+ var sat = c1.hsvSaturation;
+ var val = c1.hsvValue;
+ var alpha = c1.a;
+
+ return Qt.hsva(hue, sat, val, alpha);
+}
+
+/**
+ * Returns a color with the saturation of color2 and the hue/value/alpha of color1.
+ *
+ * @param {string} color1 - The base color (any Qt.color-compatible string).
+ * @param {string} color2 - The color to take saturation from.
+ * @returns {Qt.rgba} The resulting color.
+ */
+function colorWithSaturationOf(color1, color2) {
+ var c1 = Qt.color(color1);
+ var c2 = Qt.color(color2);
+
+ var hue = c1.hsvHue;
+ var sat = c2.hsvSaturation;
+ var val = c1.hsvValue;
+ var alpha = c1.a;
+
+ return Qt.hsva(hue, sat, val, alpha);
+}
+
+/**
+ * Returns a color with the given lightness and the hue, saturation, and alpha of the input color (using HSL).
+ *
+ * @param {string} color - The base color (any Qt.color-compatible string).
+ * @param {number} lightness - The lightness value to use (0-1).
+ * @returns {Qt.rgba} The resulting color.
+ */
+function colorWithLightness(color, lightness) {
+ var c = Qt.color(color);
+ return Qt.hsla(c.hslHue, c.hslSaturation, lightness, c.a);
+}
+
+/**
+ * Returns a color with the lightness of color2 and the hue, saturation, and alpha of color1 (using HSL).
+ *
+ * @param {string} color1 - The base color (any Qt.color-compatible string).
+ * @param {string} color2 - The color to take lightness from.
+ * @returns {Qt.rgba} The resulting color.
+ */
+function colorWithLightnessOf(color1, color2) {
+ var c2 = Qt.color(color2);
+ return colorWithLightness(color1, c2.hslLightness);
+}
+
+/**
+ * Adapts color1 to the accent (hue and saturation) of color2 using HSL, keeping lightness and alpha from color1.
+ *
+ * @param {string} color1 - The base color (any Qt.color-compatible string).
+ * @param {string} color2 - The accent color.
+ * @returns {Qt.rgba} The resulting color.
+ */
+function adaptToAccent(color1, color2) {
+ var c1 = Qt.color(color1);
+ var c2 = Qt.color(color2);
+
+ var hue = c2.hslHue;
+ var sat = c2.hslSaturation;
+ var light = c1.hslLightness;
+ var alpha = c1.a;
+
+ return Qt.hsla(hue, sat, light, alpha);
+}
+
+/**
+ * Mixes two colors by a given percentage.
+ *
+ * @param {string} color1 - The first color (any Qt.color-compatible string).
+ * @param {string} color2 - The second color.
+ * @param {number} percentage - The mix ratio (0-1). 1 = all color1, 0 = all color2.
+ * @returns {Qt.rgba} The resulting mixed color.
+ */
+function mix(color1, color2, percentage = 0.5) {
+ var c1 = Qt.color(color1);
+ var c2 = Qt.color(color2);
+ return Qt.rgba(percentage * c1.r + (1 - percentage) * c2.r, percentage * c1.g + (1 - percentage) * c2.g, percentage * c1.b + (1 - percentage) * c2.b, percentage * c1.a + (1 - percentage) * c2.a);
+}
+
+/**
+ * Transparentizes a color by a given percentage.
+ *
+ * @param {string} color - The color (any Qt.color-compatible string).
+ * @param {number} percentage - The amount to transparentize (0-1).
+ * @returns {Qt.rgba} The resulting color.
+ */
+function transparentize(color, percentage = 1) {
+ var c = Qt.color(color);
+ return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage));
+}
diff --git a/.config/quickshell/modules/common/functions/file_utils.js b/.config/quickshell/modules/common/functions/file_utils.js
new file mode 100644
index 000000000..758950ded
--- /dev/null
+++ b/.config/quickshell/modules/common/functions/file_utils.js
@@ -0,0 +1,9 @@
+/**
+ * Trims the File protocol off the input string
+ * @param {string} str
+ * @returns {string}
+ */
+function trimFileProtocol(str) {
+ return str.startsWith("file://") ? str.slice(7) : str;
+}
+
diff --git a/.config/quickshell/modules/common/functions/fuzzysort.js b/.config/quickshell/modules/common/functions/fuzzysort.js
new file mode 100644
index 000000000..1c1d9b9db
--- /dev/null
+++ b/.config/quickshell/modules/common/functions/fuzzysort.js
@@ -0,0 +1,682 @@
+.pragma library
+
+// https://github.com/farzher/fuzzysort
+// License: MIT | Copyright (c) 2018 Stephen Kamenar
+// A copy of the license is available in the `licenses` folder of this repository
+
+var single = (search, target) => {
+ if(!search || !target) return NULL
+
+ var preparedSearch = getPreparedSearch(search)
+ if(!isPrepared(target)) target = getPrepared(target)
+
+ var searchBitflags = preparedSearch.bitflags
+ if((searchBitflags & target._bitflags) !== searchBitflags) return NULL
+
+ return algorithm(preparedSearch, target)
+}
+
+var go = (search, targets, options) => {
+ if(!search) return options?.all ? all(targets, options) : noResults
+
+ var preparedSearch = getPreparedSearch(search)
+ var searchBitflags = preparedSearch.bitflags
+ var containsSpace = preparedSearch.containsSpace
+
+ var threshold = denormalizeScore( options?.threshold || 0 )
+ var limit = options?.limit || INFINITY
+
+ var resultsLen = 0; var limitedCount = 0
+ var targetsLen = targets.length
+
+ function push_result(result) {
+ if(resultsLen < limit) { q.add(result); ++resultsLen }
+ else {
+ ++limitedCount
+ if(result._score > q.peek()._score) q.replaceTop(result)
+ }
+ }
+
+ // This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys]
+
+ // options.key
+ if(options?.key) {
+ var key = options.key
+ for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
+ var target = getValue(obj, key)
+ if(!target) continue
+ if(!isPrepared(target)) target = getPrepared(target)
+
+ if((searchBitflags & target._bitflags) !== searchBitflags) continue
+ var result = algorithm(preparedSearch, target)
+ if(result === NULL) continue
+ if(result._score < threshold) continue
+
+ result.obj = obj
+ push_result(result)
+ }
+
+ // options.keys
+ } else if(options?.keys) {
+ var keys = options.keys
+ var keysLen = keys.length
+
+ outer: for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
+
+ { // early out based on bitflags
+ var keysBitflags = 0
+ for (var keyI = 0; keyI < keysLen; ++keyI) {
+ var key = keys[keyI]
+ var target = getValue(obj, key)
+ if(!target) { tmpTargets[keyI] = noTarget; continue }
+ if(!isPrepared(target)) target = getPrepared(target)
+ tmpTargets[keyI] = target
+
+ keysBitflags |= target._bitflags
+ }
+
+ if((searchBitflags & keysBitflags) !== searchBitflags) continue
+ }
+
+ if(containsSpace) for(let i=0; i -1000) {
+ if(keysSpacesBestScores[i] > NEGATIVE_INFINITY) {
+ var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/
+ if(tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp
+ }
+ }
+ if(allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i]
+ }
+ }
+
+ if(containsSpace) {
+ for(let i=0; i -1000) {
+ if(score > NEGATIVE_INFINITY) {
+ var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/
+ if(tmp > score) score = tmp
+ }
+ }
+ if(result._score > score) score = result._score
+ }
+ }
+
+ objResults.obj = obj
+ objResults._score = score
+ if(options?.scoreFn) {
+ score = options.scoreFn(objResults)
+ if(!score) continue
+ score = denormalizeScore(score)
+ objResults._score = score
+ }
+
+ if(score < threshold) continue
+ push_result(objResults)
+ }
+
+ // no keys
+ } else {
+ for(var i = 0; i < targetsLen; ++i) { var target = targets[i]
+ if(!target) continue
+ if(!isPrepared(target)) target = getPrepared(target)
+
+ if((searchBitflags & target._bitflags) !== searchBitflags) continue
+ var result = algorithm(preparedSearch, target)
+ if(result === NULL) continue
+ if(result._score < threshold) continue
+
+ push_result(result)
+ }
+ }
+
+ if(resultsLen === 0) return noResults
+ var results = new Array(resultsLen)
+ for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
+ results.total = resultsLen + limitedCount
+ return results
+}
+
+
+// this is written as 1 function instead of 2 for minification. perf seems fine ...
+// except when minified. the perf is very slow
+var highlight = (result, open='', close=' ') => {
+ var callback = typeof open === 'function' ? open : undefined
+
+ var target = result.target
+ var targetLen = target.length
+ var indexes = result.indexes
+ var highlighted = ''
+ var matchI = 0
+ var indexesI = 0
+ var opened = false
+ var parts = []
+
+ for(var i = 0; i < targetLen; ++i) { var char = target[i]
+ if(indexes[indexesI] === i) {
+ ++indexesI
+ if(!opened) { opened = true
+ if(callback) {
+ parts.push(highlighted); highlighted = ''
+ } else {
+ highlighted += open
+ }
+ }
+
+ if(indexesI === indexes.length) {
+ if(callback) {
+ highlighted += char
+ parts.push(callback(highlighted, matchI++)); highlighted = ''
+ parts.push(target.substr(i+1))
+ } else {
+ highlighted += char + close + target.substr(i+1)
+ }
+ break
+ }
+ } else {
+ if(opened) { opened = false
+ if(callback) {
+ parts.push(callback(highlighted, matchI++)); highlighted = ''
+ } else {
+ highlighted += close
+ }
+ }
+ }
+ highlighted += char
+ }
+
+ return callback ? parts : highlighted
+}
+
+
+var prepare = (target) => {
+ if(typeof target === 'number') target = ''+target
+ else if(typeof target !== 'string') target = ''
+ var info = prepareLowerInfo(target)
+ return new_result(target, {_targetLower:info._lower, _targetLowerCodes:info.lowerCodes, _bitflags:info.bitflags})
+}
+
+var cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() }
+
+
+// Below this point is only internal code
+// Below this point is only internal code
+// Below this point is only internal code
+// Below this point is only internal code
+
+
+class Result {
+ get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a,b)=>a-b) }
+ set ['indexes'](indexes) { return this._indexes = indexes }
+ ['highlight'](open, close) { return highlight(this, open, close) }
+ get ['score']() { return normalizeScore(this._score) }
+ set ['score'](score) { this._score = denormalizeScore(score) }
+}
+
+class KeysResult extends Array {
+ get ['score']() { return normalizeScore(this._score) }
+ set ['score'](score) { this._score = denormalizeScore(score) }
+}
+
+var new_result = (target, options) => {
+ const result = new Result()
+ result['target'] = target
+ result['obj'] = options.obj ?? NULL
+ result._score = options._score ?? NEGATIVE_INFINITY
+ result._indexes = options._indexes ?? []
+ result._targetLower = options._targetLower ?? ''
+ result._targetLowerCodes = options._targetLowerCodes ?? NULL
+ result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL
+ result._bitflags = options._bitflags ?? 0
+ return result
+}
+
+
+var normalizeScore = score => {
+ if(score === NEGATIVE_INFINITY) return 0
+ if(score > 1) return score
+ return Math.E ** ( ((-score + 1)**.04307 - 1) * -2)
+}
+var denormalizeScore = normalizedScore => {
+ if(normalizedScore === 0) return NEGATIVE_INFINITY
+ if(normalizedScore > 1) return normalizedScore
+ return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307)
+}
+
+
+var prepareSearch = (search) => {
+ if(typeof search === 'number') search = ''+search
+ else if(typeof search !== 'string') search = ''
+ search = search.trim()
+ var info = prepareLowerInfo(search)
+
+ var spaceSearches = []
+ if(info.containsSpace) {
+ var searches = search.split(/\s+/)
+ searches = [...new Set(searches)] // distinct
+ for(var i=0; i {
+ if(target.length > 999) return prepare(target) // don't cache huge targets
+ var targetPrepared = preparedCache.get(target)
+ if(targetPrepared !== undefined) return targetPrepared
+ targetPrepared = prepare(target)
+ preparedCache.set(target, targetPrepared)
+ return targetPrepared
+}
+var getPreparedSearch = (search) => {
+ if(search.length > 999) return prepareSearch(search) // don't cache huge searches
+ var searchPrepared = preparedSearchCache.get(search)
+ if(searchPrepared !== undefined) return searchPrepared
+ searchPrepared = prepareSearch(search)
+ preparedSearchCache.set(search, searchPrepared)
+ return searchPrepared
+}
+
+
+var all = (targets, options) => {
+ var results = []; results.total = targets.length // this total can be wrong if some targets are skipped
+
+ var limit = options?.limit || INFINITY
+
+ if(options?.key) {
+ for(var i=0;i= limit) return results
+ }
+ } else if(options?.keys) {
+ for(var i=0;i= 0; --keyI) {
+ var target = getValue(obj, options.keys[keyI])
+ if(!target) { objResults[keyI] = noTarget; continue }
+ if(!isPrepared(target)) target = getPrepared(target)
+ target._score = NEGATIVE_INFINITY
+ target._indexes.len = 0
+ objResults[keyI] = target
+ }
+ objResults.obj = obj
+ objResults._score = NEGATIVE_INFINITY
+ results.push(objResults); if(results.length >= limit) return results
+ }
+ } else {
+ for(var i=0;i= limit) return results
+ }
+ }
+
+ return results
+}
+
+
+var algorithm = (preparedSearch, prepared, allowSpaces=false, allowPartialMatch=false) => {
+ if(allowSpaces===false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch)
+
+ var searchLower = preparedSearch._lower
+ var searchLowerCodes = preparedSearch.lowerCodes
+ var searchLowerCode = searchLowerCodes[0]
+ var targetLowerCodes = prepared._targetLowerCodes
+ var searchLen = searchLowerCodes.length
+ var targetLen = targetLowerCodes.length
+ var searchI = 0 // where we at
+ var targetI = 0 // where you at
+ var matchesSimpleLen = 0
+
+ // very basic fuzzy match; to remove non-matching targets ASAP!
+ // walk through target. find sequential matches.
+ // if all chars aren't found then exit
+ for(;;) {
+ var isMatch = searchLowerCode === targetLowerCodes[targetI]
+ if(isMatch) {
+ matchesSimple[matchesSimpleLen++] = targetI
+ ++searchI; if(searchI === searchLen) break
+ searchLowerCode = searchLowerCodes[searchI]
+ }
+ ++targetI; if(targetI >= targetLen) return NULL // Failed to find searchI
+ }
+
+ var searchI = 0
+ var successStrict = false
+ var matchesStrictLen = 0
+
+ var nextBeginningIndexes = prepared._nextBeginningIndexes
+ if(nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target)
+ targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]
+
+ // Our target string successfully matched all characters in sequence!
+ // Let's try a more advanced and strict test to improve the score
+ // only count it as a match if it's consecutive or a beginning character!
+ var backtrackCount = 0
+ if(targetI !== targetLen) for(;;) {
+ if(targetI >= targetLen) {
+ // We failed to find a good spot for this search char, go back to the previous search char and force it forward
+ if(searchI <= 0) break // We failed to push chars forward for a better match
+
+ ++backtrackCount; if(backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match
+
+ --searchI
+ var lastMatch = matchesStrict[--matchesStrictLen]
+ targetI = nextBeginningIndexes[lastMatch]
+
+ } else {
+ var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]
+ if(isMatch) {
+ matchesStrict[matchesStrictLen++] = targetI
+ ++searchI; if(searchI === searchLen) { successStrict = true; break }
+ ++targetI
+ } else {
+ targetI = nextBeginningIndexes[targetI]
+ }
+ }
+ }
+
+ // check if it's a substring match
+ var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow
+ var isSubstring = !!~substringIndex
+ var isSubstringBeginning = !isSubstring ? false : substringIndex===0 || prepared._nextBeginningIndexes[substringIndex-1] === substringIndex
+
+ // if it's a substring match but not at a beginning index, let's try to find a substring starting at a beginning index for a better score
+ if(isSubstring && !isSubstringBeginning) {
+ for(var i=0; i {
+ var score = 0
+
+ var extraMatchGroupCount = 0
+ for(var i = 1; i < searchLen; ++i) {
+ if(matches[i] - matches[i-1] !== 1) {score -= matches[i]; ++extraMatchGroupCount}
+ }
+ var unmatchedDistance = matches[searchLen-1] - matches[0] - (searchLen-1)
+
+ score -= (12+unmatchedDistance) * extraMatchGroupCount // penality for more groups
+
+ if(matches[0] !== 0) score -= matches[0]*matches[0]*.2 // penality for not starting near the beginning
+
+ if(!successStrict) {
+ score *= 1000
+ } else {
+ // successStrict on a target with too many beginning indexes loses points for being a bad target
+ var uniqueBeginningIndexes = 1
+ for(var i = nextBeginningIndexes[0]; i < targetLen; i=nextBeginningIndexes[i]) ++uniqueBeginningIndexes
+
+ if(uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes-24)*10 // quite arbitrary numbers here ...
+ }
+
+ score -= (targetLen - searchLen)/2 // penality for longer targets
+
+ if(isSubstring) score /= 1+searchLen*searchLen*1 // bonus for being a full substring
+ if(isSubstringBeginning) score /= 1+searchLen*searchLen*1 // bonus for substring starting on a beginningIndex
+
+ score -= (targetLen - searchLen)/2 // penality for longer targets
+
+ return score
+ }
+
+ if(!successStrict) {
+ if(isSubstring) for(var i=0; i {
+ var seen_indexes = new Set()
+ var score = 0
+ var result = NULL
+
+ var first_seen_index_last_search = 0
+ var searches = preparedSearch.spaceSearches
+ var searchesLen = searches.length
+ var changeslen = 0
+
+ // Return _nextBeginningIndexes back to its normal state
+ var resetNextBeginningIndexes = () => {
+ for(let i=changeslen-1; i>=0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i*2 + 0]] = nextBeginningIndexesChanges[i*2 + 1]
+ }
+
+ var hasAtLeast1Match = false
+ for(var i=0; i=0; i--) {
+ if(toReplace !== target._nextBeginningIndexes[i]) break
+ target._nextBeginningIndexes[i] = newBeginningIndex
+ nextBeginningIndexesChanges[changeslen*2 + 0] = i
+ nextBeginningIndexesChanges[changeslen*2 + 1] = toReplace
+ changeslen++
+ }
+ }
+ }
+
+ score += result._score / searchesLen
+ allowPartialMatchScores[i] = result._score / searchesLen
+
+ // dock points based on order otherwise "c man" returns Manifest.cpp instead of CheatManager.h
+ if(result._indexes[0] < first_seen_index_last_search) {
+ score -= (first_seen_index_last_search - result._indexes[0]) * 2
+ }
+ first_seen_index_last_search = result._indexes[0]
+
+ for(var j=0; j score) {
+ if(allowPartialMatch) {
+ for(var i=0; i str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '')
+
+var prepareLowerInfo = (str) => {
+ str = remove_accents(str)
+ var strLen = str.length
+ var lower = str.toLowerCase()
+ var lowerCodes = [] // new Array(strLen) sparse array is too slow
+ var bitflags = 0
+ var containsSpace = false // space isn't stored in bitflags because of how searching with a space works
+
+ for(var i = 0; i < strLen; ++i) {
+ var lowerCode = lowerCodes[i] = lower.charCodeAt(i)
+
+ if(lowerCode === 32) {
+ containsSpace = true
+ continue // it's important that we don't set any bitflags for space
+ }
+
+ var bit = lowerCode>=97&&lowerCode<=122 ? lowerCode-97 // alphabet
+ : lowerCode>=48&&lowerCode<=57 ? 26 // numbers
+ // 3 bits available
+ : lowerCode<=127 ? 30 // other ascii
+ : 31 // other utf8
+ bitflags |= 1< {
+ var targetLen = target.length
+ var beginningIndexes = []; var beginningIndexesLen = 0
+ var wasUpper = false
+ var wasAlphanum = false
+ for(var i = 0; i < targetLen; ++i) {
+ var targetCode = target.charCodeAt(i)
+ var isUpper = targetCode>=65&&targetCode<=90
+ var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57
+ var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum
+ wasUpper = isUpper
+ wasAlphanum = isAlphanum
+ if(isBeginning) beginningIndexes[beginningIndexesLen++] = i
+ }
+ return beginningIndexes
+}
+var prepareNextBeginningIndexes = (target) => {
+ target = remove_accents(target)
+ var targetLen = target.length
+ var beginningIndexes = prepareBeginningIndexes(target)
+ var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow
+ var lastIsBeginning = beginningIndexes[0]
+ var lastIsBeginningI = 0
+ for(var i = 0; i < targetLen; ++i) {
+ if(lastIsBeginning > i) {
+ nextBeginningIndexes[i] = lastIsBeginning
+ } else {
+ lastIsBeginning = beginningIndexes[++lastIsBeginningI]
+ nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning
+ }
+ }
+ return nextBeginningIndexes
+}
+
+var preparedCache = new Map()
+var preparedSearchCache = new Map()
+
+// the theory behind these being globals is to reduce garbage collection by not making new arrays
+var matchesSimple = []; var matchesStrict = []
+var nextBeginningIndexesChanges = [] // allows straw berry to match strawberry well, by modifying the end of a substring to be considered a beginning index for the rest of the search
+var keysSpacesBestScores = []; var allowPartialMatchScores = []
+var tmpTargets = []; var tmpResults = []
+
+// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop]
+// prop = 'key1.key2' 10ms
+// prop = ['key1', 'key2'] 27ms
+// prop = obj => obj.tags.join() ??ms
+var getValue = (obj, prop) => {
+ var tmp = obj[prop]; if(tmp !== undefined) return tmp
+ if(typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower
+ var segs = prop
+ if(!Array.isArray(prop)) segs = prop.split('.')
+ var len = segs.length
+ var i = -1
+ while (obj && (++i < len)) obj = obj[segs[i]]
+ return obj
+}
+
+var isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' }
+var INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY
+var noResults = []; noResults.total = 0
+var NULL = null
+
+var noTarget = prepare('')
+
+// Hacked version of https://github.com/lemire/FastPriorityQueue.js
+var fastpriorityqueue=r=>{var e=[],o=0,a={},v=r=>{for(var a=0,v=e[a],c=1;c>1]=e[a],c=1+(a<<1)}for(var f=a-1>>1;a>0&&v._score>1)e[a]=e[f];e[a]=v};return a.add=(r=>{var a=o;e[o++]=r;for(var v=a-1>>1;a>0&&r._score>1)e[a]=e[v];e[a]=r}),a.poll=(r=>{if(0!==o){var a=e[0];return e[0]=e[--o],v(),a}}),a.peek=(r=>{if(0!==o)return e[0]}),a.replaceTop=(r=>{e[0]=r,v()}),a}
+var q = fastpriorityqueue() // reuse this
diff --git a/.config/quickshell/modules/common/functions/levendist.js b/.config/quickshell/modules/common/functions/levendist.js
new file mode 100644
index 000000000..90180d216
--- /dev/null
+++ b/.config/quickshell/modules/common/functions/levendist.js
@@ -0,0 +1,141 @@
+// Original code from https://github.com/koeqaife/hyprland-material-you
+// Original code license: GPLv3
+// Translated to Js from Cython with an LLM and reviewed
+
+function min3(a, b, c) {
+ return a < b && a < c ? a : b < c ? b : c;
+}
+
+function max3(a, b, c) {
+ return a > b && a > c ? a : b > c ? b : c;
+}
+
+function min2(a, b) {
+ return a < b ? a : b;
+}
+
+function max2(a, b) {
+ return a > b ? a : b;
+}
+
+function levenshteinDistance(s1, s2) {
+ let len1 = s1.length;
+ let len2 = s2.length;
+
+ if (len1 === 0) return len2;
+ if (len2 === 0) return len1;
+
+ if (len2 > len1) {
+ [s1, s2] = [s2, s1];
+ [len1, len2] = [len2, len1];
+ }
+
+ let prev = new Array(len2 + 1);
+ let curr = new Array(len2 + 1);
+
+ for (let j = 0; j <= len2; j++) {
+ prev[j] = j;
+ }
+
+ for (let i = 1; i <= len1; i++) {
+ curr[0] = i;
+ for (let j = 1; j <= len2; j++) {
+ let cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
+ curr[j] = min3(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
+ }
+ [prev, curr] = [curr, prev];
+ }
+
+ return prev[len2];
+}
+
+function partialRatio(shortS, longS) {
+ let lenS = shortS.length;
+ let lenL = longS.length;
+ let best = 0.0;
+
+ if (lenS === 0) return 1.0;
+
+ for (let i = 0; i <= lenL - lenS; i++) {
+ let sub = longS.slice(i, i + lenS);
+ let dist = levenshteinDistance(shortS, sub);
+ let score = 1.0 - (dist / lenS);
+ if (score > best) best = score;
+ }
+
+ return best;
+}
+
+function computeScore(s1, s2) {
+ if (s1 === s2) return 1.0;
+
+ let dist = levenshteinDistance(s1, s2);
+ let maxLen = max2(s1.length, s2.length);
+ if (maxLen === 0) return 1.0;
+
+ let full = 1.0 - (dist / maxLen);
+ let part = s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1);
+
+ let score = 0.85 * full + 0.15 * part;
+
+ if (s1 && s2 && s1[0] !== s2[0]) {
+ score -= 0.05;
+ }
+
+ let lenDiff = Math.abs(s1.length - s2.length);
+ if (lenDiff >= 3) {
+ score -= 0.05 * lenDiff / maxLen;
+ }
+
+ let commonPrefixLen = 0;
+ let minLen = min2(s1.length, s2.length);
+ for (let i = 0; i < minLen; i++) {
+ if (s1[i] === s2[i]) {
+ commonPrefixLen++;
+ } else {
+ break;
+ }
+ }
+ score += 0.02 * commonPrefixLen;
+
+ if (s1.includes(s2) || s2.includes(s1)) {
+ score += 0.06;
+ }
+
+ return Math.max(0.0, Math.min(1.0, score));
+}
+
+function computeTextMatchScore(s1, s2) {
+ if (s1 === s2) return 1.0;
+
+ let dist = levenshteinDistance(s1, s2);
+ let maxLen = max2(s1.length, s2.length);
+ if (maxLen === 0) return 1.0;
+
+ let full = 1.0 - (dist / maxLen);
+ let part = s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1);
+
+ let score = 0.4 * full + 0.6 * part;
+
+ let lenDiff = Math.abs(s1.length - s2.length);
+ if (lenDiff >= 10) {
+ score -= 0.02 * lenDiff / maxLen;
+ }
+
+ let commonPrefixLen = 0;
+ let minLen = min2(s1.length, s2.length);
+ for (let i = 0; i < minLen; i++) {
+ if (s1[i] === s2[i]) {
+ commonPrefixLen++;
+ } else {
+ break;
+ }
+ }
+ score += 0.01 * commonPrefixLen;
+
+ if (s1.includes(s2) || s2.includes(s1)) {
+ score += 0.2;
+ }
+
+ return Math.max(0.0, Math.min(1.0, score));
+}
diff --git a/.config/quickshell/modules/common/functions/object_utils.js b/.config/quickshell/modules/common/functions/object_utils.js
new file mode 100644
index 000000000..96c632fd3
--- /dev/null
+++ b/.config/quickshell/modules/common/functions/object_utils.js
@@ -0,0 +1,45 @@
+function toPlainObject(qtObj) {
+ if (qtObj === null || typeof qtObj !== "object") return qtObj;
+
+ // Handle arrays
+ if (Array.isArray(qtObj)) {
+ return qtObj.map(toPlainObject);
+ }
+
+ const result = ({});
+ for (let key in qtObj) {
+ if (
+ typeof qtObj[key] !== "function" &&
+ !key.startsWith("objectName") &&
+ !key.startsWith("children") &&
+ !key.startsWith("object") &&
+ !key.startsWith("parent") &&
+ !key.startsWith("metaObject") &&
+ !key.startsWith("destroyed") &&
+ !key.startsWith("reloadableId")
+ ) {
+ result[key] = toPlainObject(qtObj[key]);
+ }
+ }
+ return result;
+}
+
+function applyToQtObject(qtObj, jsonObj) {
+ if (!qtObj || typeof jsonObj !== "object" || jsonObj === null) return;
+
+ for (let key in jsonObj) {
+ if (!qtObj.hasOwnProperty(key)) continue;
+
+ // Check if the property is a QtObject (not a value)
+ const value = qtObj[key];
+ const jsonValue = jsonObj[key];
+
+ // If it's an object and not an array, recurse
+ if (value && typeof value === "object" && !Array.isArray(value)) {
+ applyToQtObject(value, jsonValue);
+ } else {
+ // Otherwise, assign the value
+ qtObj[key] = jsonValue;
+ }
+ }
+}
diff --git a/.config/quickshell/modules/common/functions/string_utils.js b/.config/quickshell/modules/common/functions/string_utils.js
new file mode 100644
index 000000000..c22671ebf
--- /dev/null
+++ b/.config/quickshell/modules/common/functions/string_utils.js
@@ -0,0 +1,188 @@
+/**
+ * Formats a string according to the args that are passed in
+ * @param { string } str
+ * @param {...any} args
+ * @returns
+ */
+function format(str, ...args) {
+ return str.replace(/{(\d+)}/g, (match, index) =>
+ typeof args[index] !== 'undefined' ? args[index] : match
+ );
+}
+
+/**
+ * Returns the domain of the passed in url or null
+ * @param { string } url
+ * @returns { string| null }
+ */
+function getDomain(url) {
+ const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/);
+ return match ? match[1] : null;
+}
+
+/**
+ * Returns the base url of the passed in url or null
+ * @param { string } url
+ * @returns { string | null }
+ */
+function getBaseUrl(url) {
+ const match = url.match(/^(https?:\/\/[^\/]+)(\/.*)?$/);
+ return match ? match[1] : null;
+}
+
+/**
+ * Escapes single quotes in shell commands
+ * @param { string } str
+ * @returns { string }
+ */
+function shellSingleQuoteEscape(str) {
+ // escape single quotes
+ return String(str)
+ // .replace(/\\/g, '\\\\')
+ .replace(/'/g, "'\\''");
+}
+
+/**
+ * Splits markdown blocks into three different types: text, think, and code.
+ * @param { string } markdown
+ */
+function splitMarkdownBlocks(markdown) {
+ const regex = /```(\w+)?\n([\s\S]*?)```|([\s\S]*?)<\/think>/g;
+ /**
+ * @type {{type: "text" | "think" | "code"; content: string; lang: string | undefined; completed: boolean | undefined}[]}
+ */
+ let result = [];
+ let lastIndex = 0;
+ let match;
+ while ((match = regex.exec(markdown)) !== null) {
+ if (match.index > lastIndex) {
+ const text = markdown.slice(lastIndex, match.index);
+ if (text.trim()) {
+ result.push({ type: "text", content: text });
+ }
+ }
+ if (match[0].startsWith('```')) {
+ if (match[2] && match[2].trim()) {
+ result.push({ type: "code", lang: match[1] || "", content: match[2], completed: true });
+ }
+ } else if (match[0].startsWith('')) {
+ if (match[3] && match[3].trim()) {
+ result.push({ type: "think", content: match[3], completed: true });
+ }
+ }
+ lastIndex = regex.lastIndex;
+ }
+ // Handle any remaining text after the last match
+ if (lastIndex < markdown.length) {
+ const text = markdown.slice(lastIndex);
+ // Check for unfinished block
+ const thinkStart = text.indexOf('');
+ const codeStart = text.indexOf('```');
+ if (
+ thinkStart !== -1 &&
+ (codeStart === -1 || thinkStart < codeStart)
+ ) {
+ const beforeThink = text.slice(0, thinkStart);
+ if (beforeThink.trim()) {
+ result.push({ type: "text", content: beforeThink });
+ }
+ const thinkContent = text.slice(thinkStart + 7);
+ if (thinkContent.trim()) {
+ result.push({ type: "think", content: thinkContent, completed: false });
+ }
+ } else if (codeStart !== -1) {
+ const beforeCode = text.slice(0, codeStart);
+ if (beforeCode.trim()) {
+ result.push({ type: "text", content: beforeCode });
+ }
+ // Try to detect language after ```
+ const codeLangMatch = text.slice(codeStart + 3).match(/^(\w+)?\n/);
+ let lang = "";
+ let codeContentStart = codeStart + 3;
+ if (codeLangMatch) {
+ lang = codeLangMatch[1] || "";
+ codeContentStart += codeLangMatch[0].length;
+ } else if (text[codeStart + 3] === '\n') {
+ codeContentStart += 1;
+ }
+ const codeContent = text.slice(codeContentStart);
+ if (codeContent.trim()) {
+ result.push({ type: "code", lang, content: codeContent, completed: false });
+ }
+ } else if (text.trim()) {
+ result.push({ type: "text", content: text });
+ }
+ }
+ // console.log(JSON.stringify(result, null, 2));
+ return result;
+}
+
+/**
+ * Returns the original string with backslashes escaped
+ * @param { string } str
+ * @returns { string }
+ */
+function escapeBackslashes(str) {
+ return str.replace(/\\/g, '\\\\');
+}
+
+/**
+ * Wraps words to supplied maximum length
+ * @param { string | null } str
+ * @param { number } maxLen
+ * @returns { string }
+ */
+function wordWrap(str, maxLen) {
+ if (!str) return "";
+ let words = str.split(" ");
+ let lines = [];
+ let current = "";
+ for (let i = 0; i < words.length; ++i) {
+ if ((current + (current.length > 0 ? " " : "") + words[i]).length > maxLen) {
+ if (current.length > 0) lines.push(current);
+ current = words[i];
+ } else {
+ current += (current.length > 0 ? " " : "") + words[i];
+ }
+ }
+ if (current.length > 0) lines.push(current);
+ return lines.join("\n");
+}
+
+function cleanMusicTitle(title) {
+ if (!title) return "";
+ // Brackets
+ title = title.replace(/^ *\([^)]*\) */g, " "); // Round brackets
+ title = title.replace(/^ *\[[^\]]*\] */g, " "); // Square brackets
+ title = title.replace(/^ *\{[^\}]*\} */g, " "); // Curly brackets
+ // Japenis brackets
+ title = title.replace(/^ *【[^】]*】/, "") // Touhou
+ title = title.replace(/^ *《[^》]*》/, "") // ??
+ title = title.replace(/^ *「[^」]*」/, "") // OP/ED
+ title = title.replace(/^ *『[^』]*』/, "") // OP/ED
+
+ return title;
+}
+
+function friendlyTimeForSeconds(seconds) {
+ if (isNaN(seconds) || seconds < 0) return "0:00";
+ seconds = Math.floor(seconds);
+ const h = Math.floor(seconds / 3600);
+ const m = Math.floor((seconds % 3600) / 60);
+ const s = seconds % 60;
+ if (h > 0) {
+ return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
+ } else {
+ return `${m}:${s.toString().padStart(2, '0')}`;
+ }
+}
+
+function escapeHtml(str) {
+ if (typeof str !== 'string') return str;
+ return str
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
diff --git a/.config/quickshell/modules/common/widgets/ButtonGroup.qml b/.config/quickshell/modules/common/widgets/ButtonGroup.qml
new file mode 100644
index 000000000..5356535f4
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/ButtonGroup.qml
@@ -0,0 +1,47 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+/**
+ * A container that supports GroupButton children for bounciness.
+ * See https://m3.material.io/components/button-groups/overview
+ */
+Rectangle {
+ id: root
+ default property alias data: rowLayout.data
+ property real spacing: 5
+ property real padding: 0
+ property int clickIndex: rowLayout.clickIndex
+
+ property real contentWidth: {
+ let total = 0;
+ for (let i = 0; i < rowLayout.children.length; ++i) {
+ const child = rowLayout.children[i];
+ total += child.baseWidth ?? child.implicitWidth ?? child.width;
+ }
+ return total + rowLayout.spacing * (rowLayout.children.length - 1);
+ }
+
+ topLeftRadius: rowLayout.children.length > 0 ? (rowLayout.children[0].radius + padding) :
+ Appearance?.rounding?.small
+ bottomLeftRadius: topLeftRadius
+ topRightRadius: rowLayout.children.length > 0 ? (rowLayout.children[rowLayout.children.length - 1].radius + padding) :
+ Appearance?.rounding?.small
+ bottomRightRadius: topRightRadius
+
+ color: "transparent"
+ width: root.contentWidth + padding * 2
+ implicitHeight: rowLayout.implicitHeight + padding * 2
+ implicitWidth: root.contentWidth + padding * 2
+
+ children: [RowLayout {
+ id: rowLayout
+ anchors.fill: parent
+ anchors.margins: root.padding
+ spacing: root.spacing
+ property int clickIndex: -1
+ }]
+}
diff --git a/.config/quickshell/modules/common/widgets/CircularProgress.qml b/.config/quickshell/modules/common/widgets/CircularProgress.qml
new file mode 100644
index 000000000..c3731e64e
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/CircularProgress.qml
@@ -0,0 +1,95 @@
+// From https://github.com/rafzby/circular-progressbar with modifications
+// License: LGPL-3.0 - A copy can be found in `licenses` folder of repo
+
+import QtQuick
+import "root:/modules/common"
+
+/**
+ * Material 3 circular progress. See https://m3.material.io/components/progress-indicators/specs
+ */
+Item {
+ id: root
+
+ property int size: 30
+ property int lineWidth: 2
+ property real value: 0
+ property color primaryColor: Appearance.m3colors.m3onSecondaryContainer
+ property color secondaryColor: Appearance.colors.colSecondaryContainer
+ property real gapAngle: Math.PI / 9
+ property bool fill: false
+ property int fillOverflow: 2
+ property int animationDuration: 1000
+ property var easingType: Easing.OutCubic
+
+ width: size
+ height: size
+
+ signal animationFinished();
+
+ onValueChanged: {
+ canvas.degree = value * 360;
+ }
+ onPrimaryColorChanged: {
+ canvas.requestPaint();
+ }
+ onSecondaryColorChanged: {
+ canvas.requestPaint();
+ }
+
+ Canvas {
+ id: canvas
+
+ property real degree: 0
+
+ anchors.fill: parent
+ antialiasing: true
+
+ onDegreeChanged: {
+ requestPaint();
+ }
+
+ onPaint: {
+ var ctx = getContext("2d");
+ var x = root.width / 2;
+ var y = root.height / 2;
+ var radius = root.size / 2 - root.lineWidth;
+ var startAngle = (Math.PI / 180) * 270;
+ var fullAngle = (Math.PI / 180) * (270 + 360);
+ var progressAngle = (Math.PI / 180) * (270 + degree);
+ var epsilon = 0.01; // Small angle in radians
+
+ ctx.reset();
+ if (root.fill) {
+ ctx.fillStyle = root.secondaryColor;
+ ctx.beginPath();
+ ctx.arc(x, y, radius + fillOverflow, startAngle, fullAngle);
+ ctx.fill();
+ }
+ ctx.lineCap = 'round';
+ ctx.lineWidth = root.lineWidth;
+
+ // Secondary
+ ctx.beginPath();
+ ctx.arc(x, y, radius, progressAngle + gapAngle, fullAngle - gapAngle);
+ ctx.strokeStyle = root.secondaryColor;
+ ctx.stroke();
+
+ // Primary (value indication)
+ var endAngle = progressAngle + (value > 0 ? 0 : epsilon);
+ ctx.beginPath();
+ ctx.arc(x, y, radius, startAngle, endAngle);
+ ctx.strokeStyle = root.primaryColor;
+ ctx.stroke();
+ }
+
+ Behavior on degree {
+ NumberAnimation {
+ duration: root.animationDuration
+ easing.type: root.easingType
+ }
+
+ }
+
+ }
+
+}
diff --git a/.config/quickshell/modules/common/widgets/CliphistImage.qml b/.config/quickshell/modules/common/widgets/CliphistImage.qml
new file mode 100644
index 000000000..9de344507
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/CliphistImage.qml
@@ -0,0 +1,101 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/file_utils.js" as FileUtils
+import Qt5Compat.GraphicalEffects
+import Qt.labs.platform
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell.Widgets
+import Quickshell.Hyprland
+
+Rectangle {
+ id: root
+ property string entry
+ property real maxWidth
+ property real maxHeight
+
+ property string imageDecodePath: Directories.cliphistDecode
+ property string imageDecodeFileName: `${entryNumber}`
+ property string imageDecodeFilePath: `${imageDecodePath}/${imageDecodeFileName}`
+ property string source
+
+ property int entryNumber: {
+ if (!root.entry) return 0
+ const match = root.entry.match(/^(\d+)\t/)
+ return match ? parseInt(match[1]) : 0
+ }
+ property int imageWidth: {
+ if (!root.entry) return 0
+ const match = root.entry.match(/(\d+)x(\d+)/)
+ return match ? parseInt(match[1]) : 0
+ }
+ property int imageHeight: {
+ if (!root.entry) return 0
+ const match = root.entry.match(/(\d+)x(\d+)/)
+ return match ? parseInt(match[2]) : 0
+ }
+ property real scale: {
+ return Math.min(
+ root.maxWidth / imageWidth,
+ root.maxHeight / imageHeight,
+ 1
+ )
+ }
+
+ color: Appearance.colors.colLayer1
+ radius: Appearance.rounding.small
+ implicitHeight: imageHeight * scale
+ implicitWidth: imageWidth * scale
+
+ Component.onCompleted: {
+ decodeImageProcess.running = true
+ }
+
+ Process {
+ id: decodeImageProcess
+ command: ["bash", "-c",
+ `[ -f ${imageDecodeFilePath} ] || echo '${StringUtils.shellSingleQuoteEscape(root.entry)}' | cliphist decode > '${imageDecodeFilePath}'`
+ ]
+ onExited: (exitCode, exitStatus) => {
+ if (exitCode === 0) {
+ root.source = imageDecodeFilePath
+ } else {
+ console.error("[CliphistImage] Failed to decode image for entry:", root.entry)
+ root.source = ""
+ }
+ }
+ }
+
+ Component.onDestruction: {
+ Hyprland.dispatch(`exec bash -c "[ -f '${imageDecodeFilePath}' ] && rm -f '${imageDecodeFilePath}'"`)
+ }
+
+ Image {
+ id: image
+ anchors.fill: parent
+
+ source: Qt.resolvedUrl(root.source)
+ fillMode: Image.PreserveAspectFit
+ antialiasing: true
+ asynchronous: true
+
+ width: root.imageWidth * root.scale
+ height: root.imageHeight * root.scale
+ sourceSize.width: width
+ sourceSize.height: height
+
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: image.width
+ height: image.height
+ radius: root.radius
+ }
+ }
+ }
+}
+
diff --git a/.config/quickshell/modules/common/widgets/CustomIcon.qml b/.config/quickshell/modules/common/widgets/CustomIcon.qml
new file mode 100644
index 000000000..8905b0715
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/CustomIcon.qml
@@ -0,0 +1,24 @@
+import QtQuick
+import Quickshell
+import Quickshell.Widgets
+
+Item {
+ id: root
+
+ property string source: ""
+ property string iconFolder: "root:/assets/icons" // The folder to check first
+ width: 30
+ height: 30
+
+ IconImage {
+ id: iconImage
+ anchors.fill: parent
+ source: {
+ if (iconFolder && iconFolder + "/" + root.source) {
+ return iconFolder + "/" + root.source
+ }
+ return root.source
+ }
+ implicitSize: root.height
+ }
+}
diff --git a/.config/quickshell/modules/common/widgets/DialogButton.qml b/.config/quickshell/modules/common/widgets/DialogButton.qml
new file mode 100644
index 000000000..86150c502
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/DialogButton.qml
@@ -0,0 +1,38 @@
+import "root:/modules/common"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+
+/**
+ * Material 3 dialog button. See https://m3.material.io/components/dialogs/overview
+ */
+RippleButton {
+ id: button
+
+ property string buttonText
+ implicitHeight: 30
+ implicitWidth: buttonTextWidget.implicitWidth + 15 * 2
+ buttonRadius: Appearance?.rounding.full ?? 9999
+
+ property color colEnabled: Appearance?.colors.colPrimary ?? "#65558F"
+ property color colDisabled: Appearance?.m3colors.m3outline ?? "#8D8C96"
+
+ contentItem: StyledText {
+ id: buttonTextWidget
+ anchors.fill: parent
+ anchors.leftMargin: 15
+ anchors.rightMargin: 15
+ text: buttonText
+ horizontalAlignment: Text.AlignHCenter
+ font.pixelSize: Appearance?.font.pixelSize.small ?? 12
+ color: button.enabled ? button.colEnabled : button.colDisabled
+
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+
+}
diff --git a/.config/quickshell/modules/common/widgets/DragManager.qml b/.config/quickshell/modules/common/widgets/DragManager.qml
new file mode 100644
index 000000000..087729edb
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/DragManager.qml
@@ -0,0 +1,74 @@
+import "root:/modules/common"
+import "root:/services"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+/**
+ * A convenience MouseArea for handling drag events.
+ */
+MouseArea {
+ id: root
+ hoverEnabled: true
+ acceptedButtons: Qt.LeftButton
+
+ property bool interactive: true
+ property bool automaticallyReset: true
+ readonly property real dragDiffX: _dragDiffX
+ readonly property real dragDiffY: _dragDiffY
+
+ signal dragPressed(diffX: real, diffY: real)
+ signal dragReleased(diffX: real, diffY: real)
+
+ property real startX: 0
+ property real startY: 0
+ property bool dragging: false
+ property real _dragDiffX: 0
+ property real _dragDiffY: 0
+
+ function resetDrag() {
+ _dragDiffX = 0
+ _dragDiffY = 0
+ }
+
+ onPressed: (mouse) => {
+ if (!root.interactive) {
+ if (mouse.button === Qt.LeftButton) {
+ mouse.accepted = false;
+ }
+ return;
+ }
+ if (mouse.button === Qt.LeftButton) {
+ startX = mouse.x
+ startY = mouse.y
+ }
+ }
+ onReleased: (mouse) => {
+ if (!root.interactive) {
+ return;
+ }
+ dragging = false
+ root.dragReleased(_dragDiffX, _dragDiffY);
+ if (root.automaticallyReset) {
+ root.resetDrag();
+ }
+ }
+ onPositionChanged: (mouse) => {
+ if (!root.interactive) {
+ return;
+ }
+ if (mouse.buttons & Qt.LeftButton) {
+ root._dragDiffX = mouse.x - startX
+ root._dragDiffY = mouse.y - startY
+ const dist = Math.sqrt(root._dragDiffX * root._dragDiffX + root._dragDiffY * root._dragDiffY);
+ root.dragPressed(_dragDiffX, _dragDiffY);
+ root.dragging = true;
+ }
+ }
+ onCanceled: (mouse) => {
+ if (!root.interactive) {
+ return;
+ }
+ released(mouse);
+ }
+}
diff --git a/.config/quickshell/modules/common/widgets/Favicon.qml b/.config/quickshell/modules/common/widgets/Favicon.qml
new file mode 100644
index 000000000..74fc6d749
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/Favicon.qml
@@ -0,0 +1,53 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/file_utils.js" as FileUtils
+import Qt5Compat.GraphicalEffects
+import Qt.labs.platform
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell.Widgets
+import Quickshell.Hyprland
+
+IconImage {
+ id: root
+ property string url
+ property string displayText
+
+ property real size: 32
+ property string downloadUserAgent: ConfigOptions?.networking.userAgent ?? ""
+ property string faviconDownloadPath: Directories.favicons
+ property string domainName: url.includes("vertexaisearch") ? displayText : StringUtils.getDomain(url)
+ property string faviconUrl: `https://www.google.com/s2/favicons?domain=${domainName}&sz=32`
+ property string fileName: `${domainName}.ico`
+ property string faviconFilePath: `${faviconDownloadPath}/${fileName}`
+ property string urlToLoad
+
+ Process {
+ id: faviconDownloadProcess
+ running: false
+ command: ["bash", "-c", `[ -f ${faviconFilePath} ] || curl -s '${root.faviconUrl}' -o '${faviconFilePath}' -L -H 'User-Agent: ${downloadUserAgent}'`]
+ onExited: (exitCode, exitStatus) => {
+ root.urlToLoad = root.faviconFilePath
+ }
+ }
+
+ Component.onCompleted: {
+ faviconDownloadProcess.running = true
+ }
+
+ source: Qt.resolvedUrl(root.urlToLoad)
+ implicitSize: root.size
+
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: root.implicitSize
+ height: root.implicitSize
+ radius: Appearance.rounding.full
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/common/widgets/FlowButtonGroup.qml b/.config/quickshell/modules/common/widgets/FlowButtonGroup.qml
new file mode 100644
index 000000000..ec9526ef2
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/FlowButtonGroup.qml
@@ -0,0 +1,8 @@
+import QtQuick
+
+/**
+ * This is just to make sure `RippleButton`s can be used in a Flow layout.
+ */
+Flow {
+ property int clickIndex: -1
+}
diff --git a/.config/quickshell/modules/common/widgets/GroupButton.qml b/.config/quickshell/modules/common/widgets/GroupButton.qml
new file mode 100644
index 000000000..5d4d9d199
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/GroupButton.qml
@@ -0,0 +1,115 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell.Widgets
+
+/**
+ * Material 3 button with expressive bounciness.
+ * See https://m3.material.io/components/button-groups/overview
+ */
+Button {
+ id: root
+ property bool toggled
+ property string buttonText
+ property real buttonRadius: Appearance?.rounding?.small ?? 4
+ property real buttonRadiusPressed: buttonRadius
+ property var downAction // When left clicking (down)
+ property var releaseAction // When left clicking (release)
+ property var altAction // When right clicking
+ property var middleClickAction // When middle clicking
+ property bool bounce: true
+ property real baseWidth: contentItem.implicitWidth + padding * 2
+ property real baseHeight: contentItem.implicitHeight + padding * 2
+ property real clickedWidth: baseWidth + 20
+ property real clickedHeight: baseHeight
+ property var parentGroup: root.parent
+ property int clickIndex: parentGroup?.clickIndex ?? -1
+
+ Layout.fillWidth: (clickIndex - 1 <= parentGroup.children.indexOf(root) && parentGroup.children.indexOf(root) <= clickIndex + 1)
+ Layout.fillHeight: (clickIndex - 1 <= parentGroup.children.indexOf(root) && parentGroup.children.indexOf(root) <= clickIndex + 1)
+ implicitWidth: (root.down && bounce) ? clickedWidth : baseWidth
+ implicitHeight: (root.down && bounce) ? clickedHeight : baseHeight
+
+ Behavior on implicitWidth {
+ animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
+ }
+
+ Behavior on implicitHeight {
+ animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
+ }
+
+ Behavior on radius {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+ property color colBackground: ColorUtils.transparentize(Appearance?.colors.colLayer1Hover, 1) || "transparent"
+ property color colBackgroundHover: Appearance?.colors.colLayer1Hover ?? "#E5DFED"
+ property color colBackgroundActive: Appearance?.colors.colLayer1Active ?? "#D6CEE2"
+ property color colBackgroundToggled: Appearance?.colors.colPrimary ?? "#65558F"
+ property color colBackgroundToggledHover: Appearance?.colors.colPrimaryHover ?? "#77699C"
+ property color colBackgroundToggledActive: Appearance?.colors.colPrimaryActive ?? "#D6CEE2"
+
+ property real radius: root.down ? root.buttonRadiusPressed : root.buttonRadius
+ property color color: root.enabled ? (root.toggled ?
+ (root.down ? colBackgroundToggledActive :
+ root.hovered ? colBackgroundToggledHover :
+ colBackgroundToggled) :
+ (root.down ? colBackgroundActive :
+ root.hovered ? colBackgroundHover :
+ colBackground)) : colBackground
+
+ onDownChanged: {
+ if (root.down) {
+ if (root.parent.clickIndex !== undefined) {
+ root.parent.clickIndex = parent.children.indexOf(root)
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+ onPressed: (event) => {
+ if(event.button === Qt.RightButton) {
+ if (root.altAction) root.altAction();
+ return;
+ }
+ if(event.button === Qt.MiddleButton) {
+ if (root.middleClickAction) root.middleClickAction();
+ return;
+ }
+ root.down = true
+ if (root.downAction) root.downAction();
+ }
+ onReleased: (event) => {
+ root.down = false
+ if (event.button != Qt.LeftButton) return;
+ if (root.releaseAction) root.releaseAction();
+ root.click() // Because the MouseArea already consumed the event
+ }
+ onCanceled: (event) => {
+ root.down = false
+ }
+ }
+
+ background: Rectangle {
+ id: buttonBackground
+ radius: root.radius
+ implicitHeight: 50
+
+ color: root.color
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+
+ contentItem: StyledText {
+ text: root.buttonText
+ }
+}
diff --git a/.config/quickshell/modules/common/widgets/KeyboardKey.qml b/.config/quickshell/modules/common/widgets/KeyboardKey.qml
new file mode 100644
index 000000000..0cc80429e
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/KeyboardKey.qml
@@ -0,0 +1,46 @@
+import "root:/modules/common"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+
+Rectangle {
+ id: root
+ property string key
+
+ property real horizontalPadding: 7
+ property real verticalPadding: 2
+ property real borderWidth: 1
+ property real extraBottomBorderWidth: 2
+ property color borderColor: Appearance.colors.colOnLayer0
+ property real borderRadius: 5
+ property color keyColor: Appearance.colors.colSurfaceContainerLow
+ implicitWidth: keyFace.implicitWidth + borderWidth * 2
+ implicitHeight: keyFace.implicitHeight + borderWidth * 2 + extraBottomBorderWidth
+ radius: borderRadius
+ color: borderColor
+
+ Rectangle {
+ id: keyFace
+ anchors {
+ fill: parent
+ topMargin: borderWidth
+ leftMargin: borderWidth
+ rightMargin: borderWidth
+ bottomMargin: extraBottomBorderWidth + borderWidth
+ }
+ implicitWidth: keyText.implicitWidth + horizontalPadding * 2
+ implicitHeight: keyText.implicitHeight + verticalPadding * 2
+ color: keyColor
+ radius: borderRadius - borderWidth
+
+ StyledText {
+ id: keyText
+ anchors.centerIn: parent
+ font.family: Appearance.font.family.monospace
+ font.pixelSize: Appearance.font.pixelSize.smaller
+ text: key
+ }
+ }
+}
diff --git a/.config/quickshell/modules/common/widgets/MaterialSymbol.qml b/.config/quickshell/modules/common/widgets/MaterialSymbol.qml
new file mode 100644
index 000000000..aac0b0315
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/MaterialSymbol.qml
@@ -0,0 +1,33 @@
+import "root:/modules/common/"
+import QtQuick
+import QtQuick.Layouts
+
+Text {
+ id: root
+ property real iconSize: Appearance?.font.pixelSize.small ?? 16
+ property real fill: 0
+ property real truncatedFill: Math.round(fill * 100) / 100 // Reduce memory consumption spikes from constant font remapping
+ renderType: Text.NativeRendering
+ font {
+ hintingPreference: Font.PreferFullHinting
+ family: Appearance?.font.family.iconMaterial ?? "Material Symbols Rounded"
+ pixelSize: iconSize
+ }
+ verticalAlignment: Text.AlignVCenter
+ color: Appearance.m3colors.m3onBackground
+
+ // Behavior on fill {
+ // NumberAnimation {
+ // duration: Appearance?.animation.elementMoveFast.duration ?? 200
+ // easing.type: Appearance?.animation.elementMoveFast.type ?? Easing.BezierSpline
+ // easing.bezierCurve: Appearance?.animation.elementMoveFast.bezierCurve ?? [0.34, 0.80, 0.34, 1.00, 1, 1]
+ // }
+ // }
+
+ font.variableAxes: {
+ "FILL": truncatedFill,
+ // "wght": font.weight,
+ // "GRAD": 0,
+ "opsz": iconSize,
+ }
+}
diff --git a/.config/quickshell/modules/common/widgets/MenuButton.qml b/.config/quickshell/modules/common/widgets/MenuButton.qml
new file mode 100644
index 000000000..0c4c4f411
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/MenuButton.qml
@@ -0,0 +1,31 @@
+import "root:/modules/common"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+
+RippleButton {
+ id: root
+
+ buttonRadius: 0
+ implicitHeight: 36
+ implicitWidth: buttonTextWidget.implicitWidth + 14 * 2
+
+ contentItem: StyledText {
+ id: buttonTextWidget
+ anchors.fill: parent
+ anchors.leftMargin: 14
+ anchors.rightMargin: 14
+ text: root.buttonText
+ horizontalAlignment: Text.AlignLeft
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: root.enabled ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3outline
+
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+
+}
diff --git a/.config/quickshell/modules/common/widgets/NavRailButton.qml b/.config/quickshell/modules/common/widgets/NavRailButton.qml
new file mode 100644
index 000000000..0a241553f
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/NavRailButton.qml
@@ -0,0 +1,60 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Io
+
+Button {
+ id: button
+
+ property bool toggled
+ property string buttonIcon
+ property string buttonText
+
+ Layout.alignment: Qt.AlignHCenter
+ implicitHeight: columnLayout.implicitHeight
+ implicitWidth: columnLayout.implicitWidth
+
+ background: null
+ PointingHandInteraction {}
+
+ // Real stuff
+ ColumnLayout {
+ id: columnLayout
+ spacing: 5
+ Rectangle {
+ width: 62
+ implicitHeight: navRailButtonIcon.height + 2 * 2
+ Layout.alignment: Qt.AlignHCenter
+ radius: Appearance.rounding.full
+ color: toggled ?
+ (button.down ? Appearance.colors.colSecondaryContainerActive : button.hovered ? Appearance.colors.colSecondaryContainerHover : Appearance.colors.colSecondaryContainer) :
+ (button.down ? Appearance.colors.colLayer1Active : button.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1))
+
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ MaterialSymbol {
+ id: navRailButtonIcon
+ anchors.centerIn: parent
+ iconSize: Appearance.font.pixelSize.hugeass
+ fill: toggled ? 1 : 0
+ text: buttonIcon
+ color: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer1
+
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+ }
+
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ text: buttonText
+ color: Appearance.colors.colOnLayer1
+ }
+ }
+
+}
diff --git a/.config/quickshell/modules/common/widgets/NotificationActionButton.qml b/.config/quickshell/modules/common/widgets/NotificationActionButton.qml
new file mode 100644
index 000000000..e85735a71
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/NotificationActionButton.qml
@@ -0,0 +1,27 @@
+import "root:/modules/common"
+import "root:/services"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Services.Notifications
+
+RippleButton {
+ id: button
+ property string buttonText
+ property string urgency
+
+ implicitHeight: 30
+ leftPadding: 15
+ rightPadding: 15
+ buttonRadius: Appearance.rounding.small
+ colBackground: (urgency == NotificationUrgency.Critical) ? Appearance.colors.colSecondaryContainer : Appearance.colors.colSurfaceContainerHighest
+ colBackgroundHover: (urgency == NotificationUrgency.Critical) ? Appearance.colors.colSecondaryContainerHover : Appearance.colors.colSurfaceContainerHighestHover
+ colRipple: (urgency == NotificationUrgency.Critical) ? Appearance.colors.colSecondaryContainerActive : Appearance.colors.colSurfaceContainerHighestActive
+
+ contentItem: StyledText {
+ horizontalAlignment: Text.AlignHCenter
+ text: buttonText
+ color: (urgency == NotificationUrgency.Critical) ? Appearance.m3colors.m3onSurfaceVariant : Appearance.m3colors.m3onSurface
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/common/widgets/NotificationAppIcon.qml b/.config/quickshell/modules/common/widgets/NotificationAppIcon.qml
new file mode 100644
index 000000000..81505aff6
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/NotificationAppIcon.qml
@@ -0,0 +1,105 @@
+import "root:/modules/common"
+import "./notification_utils.js" as NotificationUtils
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Services.Notifications
+
+Rectangle { // App icon
+ id: root
+ property var appIcon: ""
+ property var summary: ""
+ property var urgency: NotificationUrgency.Normal
+ property var image: ""
+ property real scale: 1
+ property real size: 45 * scale
+ property real materialIconScale: 0.57
+ property real appIconScale: 0.7
+ property real smallAppIconScale: 0.49
+ property real materialIconSize: size * materialIconScale
+ property real appIconSize: size * appIconScale
+ property real smallAppIconSize: size * smallAppIconScale
+
+ implicitWidth: size
+ implicitHeight: size
+ radius: Appearance.rounding.full
+ color: Appearance.colors.colSecondaryContainer
+ Loader {
+ id: materialSymbolLoader
+ active: root.appIcon == ""
+ anchors.fill: parent
+ sourceComponent: MaterialSymbol {
+ text: {
+ const defaultIcon = NotificationUtils.findSuitableMaterialSymbol("")
+ const guessedIcon = NotificationUtils.findSuitableMaterialSymbol(root.summary)
+ return (root.urgency == NotificationUrgency.Critical && guessedIcon === defaultIcon) ?
+ "release_alert" : guessedIcon
+ }
+ anchors.fill: parent
+ color: (root.urgency == NotificationUrgency.Critical) ?
+ ColorUtils.mix(Appearance.m3colors.m3onSecondary, Appearance.m3colors.m3onSecondaryContainer, 0.1) :
+ Appearance.m3colors.m3onSecondaryContainer
+ iconSize: root.materialIconSize
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+ Loader {
+ id: appIconLoader
+ active: root.image == "" && root.appIcon != ""
+ anchors.centerIn: parent
+ sourceComponent: IconImage {
+ id: appIconImage
+ implicitSize: root.appIconSize
+ asynchronous: true
+ source: Quickshell.iconPath(root.appIcon, "image-missing")
+ }
+ }
+ Loader {
+ id: notifImageLoader
+ active: root.image != ""
+ anchors.fill: parent
+ sourceComponent: Item {
+ anchors.fill: parent
+ Image {
+ id: notifImage
+ anchors.fill: parent
+ readonly property int size: parent.width
+
+ source: root.image
+ fillMode: Image.PreserveAspectCrop
+ cache: false
+ antialiasing: true
+ asynchronous: true
+
+ width: size
+ height: size
+ sourceSize.width: size
+ sourceSize.height: size
+
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: notifImage.size
+ height: notifImage.size
+ radius: Appearance.rounding.full
+ }
+ }
+ }
+ Loader {
+ id: notifImageAppIconLoader
+ active: root.appIcon != ""
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ sourceComponent: IconImage {
+ implicitSize: root.smallAppIconSize
+ asynchronous: true
+ source: Quickshell.iconPath(root.appIcon, "image-missing")
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/common/widgets/NotificationGroup.qml b/.config/quickshell/modules/common/widgets/NotificationGroup.qml
new file mode 100644
index 000000000..e79f400ae
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/NotificationGroup.qml
@@ -0,0 +1,244 @@
+import "root:/modules/common"
+import "root:/services"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import "./notification_utils.js" as NotificationUtils
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Widgets
+import Quickshell.Hyprland
+import Quickshell.Services.Notifications
+
+/**
+ * A group of notifications from the same app.
+ * Similar to Android's notifications
+ */
+Item { // Notification group area
+ id: root
+ property var notificationGroup
+ property var notifications: notificationGroup?.notifications ?? []
+ property int notificationCount: notifications.length
+ property bool multipleNotifications: notificationCount > 1
+ property bool expanded: false
+ property bool popup: false
+ property real padding: 10
+ implicitHeight: background.implicitHeight
+
+ property real dragConfirmThreshold: 70 // Drag further to discard notification
+ property real dismissOvershoot: 20 // Account for gaps and bouncy animations
+ property var qmlParent: root.parent.parent // There's something between this and the parent ListView
+ property var parentDragIndex: qmlParent.dragIndex
+ property var parentDragDistance: qmlParent.dragDistance
+ property var dragIndexDiff: Math.abs(parentDragIndex - index)
+ property real xOffset: dragIndexDiff == 0 ? Math.max(0, parentDragDistance) :
+ parentDragDistance > dragConfirmThreshold ? 0 :
+ dragIndexDiff == 1 ? Math.max(0, parentDragDistance * 0.3) :
+ dragIndexDiff == 2 ? Math.max(0, parentDragDistance * 0.1) : 0
+
+ function destroyWithAnimation() {
+ root.qmlParent.resetDrag()
+ background.anchors.leftMargin = background.anchors.leftMargin; // Break binding
+ destroyAnimation.running = true;
+ }
+
+ SequentialAnimation { // Drag finish animation
+ id: destroyAnimation
+ running: false
+
+ NumberAnimation {
+ target: background.anchors
+ property: "leftMargin"
+ to: root.width + root.dismissOvershoot
+ duration: Appearance.animation.elementMove.duration
+ easing.type: Appearance.animation.elementMove.type
+ easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
+ }
+ onFinished: () => {
+ root.notifications.forEach((notif) => {
+ Qt.callLater(() => {
+ Notifications.discardNotification(notif.id);
+ });
+ });
+ }
+ }
+
+ function toggleExpanded() {
+ if (expanded) implicitHeightAnim.enabled = true;
+ else implicitHeightAnim.enabled = false;
+ root.expanded = !root.expanded;
+ }
+
+ DragManager { // Drag manager
+ id: dragManager
+ anchors.fill: parent
+ interactive: !expanded
+ automaticallyReset: false
+ acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+
+ onClicked: (mouse) => {
+ if (mouse.button === Qt.RightButton)
+ root.toggleExpanded();
+ else if (mouse.button === Qt.MiddleButton)
+ root.destroyWithAnimation();
+ }
+
+ onDraggingChanged: () => {
+ if (dragging) {
+ root.qmlParent.dragIndex = root.index ?? root.parent.children.indexOf(root);
+ }
+ }
+
+ onDragDiffXChanged: () => {
+ root.qmlParent.dragDistance = dragDiffX;
+ }
+
+ onDragReleased: (diffX, diffY) => {
+ if (diffX > root.dragConfirmThreshold)
+ root.destroyWithAnimation();
+ else
+ dragManager.resetDrag();
+ }
+ }
+
+ StyledRectangularShadow {
+ target: background
+ visible: popup
+ }
+ Rectangle { // Background of the notification
+ id: background
+ anchors.left: parent.left
+ width: parent.width
+ color: Appearance.colors.colSurfaceContainer
+ radius: Appearance.rounding.normal
+ anchors.leftMargin: root.xOffset
+
+ Behavior on anchors.leftMargin {
+ enabled: !dragManager.dragging
+ NumberAnimation {
+ duration: Appearance.animation.elementMove.duration
+ easing.type: Appearance.animation.elementMove.type
+ easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
+ }
+ }
+
+ clip: true
+ implicitHeight: expanded ?
+ row.implicitHeight + padding * 2 :
+ Math.min(80, row.implicitHeight + padding * 2)
+
+ Behavior on implicitHeight {
+ id: implicitHeightAnim
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+ RowLayout { // Left column for icon, right column for content
+ id: row
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: root.padding
+ spacing: 10
+
+ NotificationAppIcon { // Icons
+ Layout.alignment: Qt.AlignTop
+ Layout.fillWidth: false
+ image: root?.multipleNotifications ? "" : notificationGroup?.notifications[0]?.image ?? ""
+ appIcon: notificationGroup?.appIcon
+ summary: notificationGroup?.notifications[root.notificationCount - 1]?.summary
+ }
+
+ ColumnLayout { // Content
+ Layout.fillWidth: true
+ spacing: expanded ? (root.multipleNotifications ?
+ (notificationGroup?.notifications[root.notificationCount - 1].image != "") ? 35 :
+ 5 : 0) : 0
+ // spacing: 00
+ Behavior on spacing {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+ Item { // App name (or summary when there's only 1 notif) and time
+ id: topRow
+ // spacing: 0
+ Layout.fillWidth: true
+ property real fontSize: Appearance.font.pixelSize.smaller
+ property bool showAppName: root.multipleNotifications
+ implicitHeight: Math.max(topTextRow.implicitHeight, expandButton.implicitHeight)
+
+ RowLayout {
+ id: topTextRow
+ anchors.left: parent.left
+ anchors.right: expandButton.left
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 5
+ StyledText {
+ id: appName
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ text: (topRow.showAppName ?
+ notificationGroup?.appName :
+ notificationGroup?.notifications[0]?.summary) || ""
+ font.pixelSize: topRow.showAppName ?
+ topRow.fontSize :
+ Appearance.font.pixelSize.small
+ color: topRow.showAppName ?
+ Appearance.colors.colSubtext :
+ Appearance.colors.colOnLayer2
+ }
+ StyledText {
+ id: timeText
+ // Layout.fillWidth: true
+ Layout.rightMargin: 10
+ horizontalAlignment: Text.AlignLeft
+ text: NotificationUtils.getFriendlyNotifTimeString(notificationGroup?.time)
+ font.pixelSize: topRow.fontSize
+ color: Appearance.colors.colSubtext
+ }
+ }
+ NotificationGroupExpandButton {
+ id: expandButton
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ count: root.notificationCount
+ expanded: root.expanded
+ fontSize: topRow.fontSize
+ onClicked: { root.toggleExpanded() }
+ }
+ }
+
+ StyledListView { // Notification body (expanded)
+ id: notificationsColumn
+ implicitHeight: contentHeight
+ Layout.fillWidth: true
+ spacing: expanded ? 5 : 3
+ // clip: true
+ interactive: false
+ Behavior on spacing {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ model: ScriptModel {
+ values: root.expanded ? root.notifications.slice().reverse() :
+ root.notifications.slice().reverse().slice(0, 2)
+ }
+ delegate: NotificationItem {
+ required property int index
+ required property var modelData
+ notificationObject: modelData
+ expanded: root.expanded
+ onlyNotification: (root.notificationCount === 1)
+ opacity: (!root.expanded && index == 1 && root.notificationCount > 2) ? 0.5 : 1
+ visible: root.expanded || (index < 2)
+ anchors.left: parent?.left
+ anchors.right: parent?.right
+ }
+ }
+
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/modules/common/widgets/NotificationGroupExpandButton.qml b/.config/quickshell/modules/common/widgets/NotificationGroupExpandButton.qml
new file mode 100644
index 000000000..bd1a8e96d
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/NotificationGroupExpandButton.qml
@@ -0,0 +1,52 @@
+import "root:/modules/common"
+import "root:/services"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Services.Notifications
+
+RippleButton { // Expand button
+ id: root
+ required property int count
+ required property bool expanded
+ property real fontSize: Appearance?.font.pixelSize.small ?? 12
+ property real iconSize: Appearance?.font.pixelSize.normal ?? 16
+ implicitHeight: fontSize + 4 * 2
+ implicitWidth: Math.max(contentItem.implicitWidth + 5 * 2, 30)
+ Layout.alignment: Qt.AlignVCenter
+ Layout.fillHeight: false
+
+ buttonRadius: Appearance.rounding.full
+ colBackground: ColorUtils.mix(Appearance?.colors.colLayer2, Appearance?.colors.colLayer2Hover, 0.5)
+ colBackgroundHover: Appearance?.colors.colLayer2Hover ?? "#E5DFED"
+ colRipple: Appearance?.colors.colLayer2Active ?? "#D6CEE2"
+
+ contentItem: Item {
+ anchors.centerIn: parent
+ implicitWidth: contentRow.implicitWidth
+ RowLayout {
+ id: contentRow
+ anchors.centerIn: parent
+ spacing: 3
+ StyledText {
+ Layout.leftMargin: 4
+ visible: root.count > 1
+ text: root.count
+ font.pixelSize: root.fontSize
+ }
+ MaterialSymbol {
+ text: "keyboard_arrow_down"
+ iconSize: root.iconSize
+ color: Appearance.colors.colOnLayer2
+ rotation: expanded ? 180 : 0
+ Behavior on rotation {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ }
+ }
+ }
+
+}
diff --git a/.config/quickshell/modules/common/widgets/NotificationItem.qml b/.config/quickshell/modules/common/widgets/NotificationItem.qml
new file mode 100644
index 000000000..bf8a76871
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/NotificationItem.qml
@@ -0,0 +1,327 @@
+import "root:/modules/common"
+import "root:/services"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import "./notification_utils.js" as NotificationUtils
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Widgets
+import Quickshell.Hyprland
+import Quickshell.Services.Notifications
+
+Item { // Notification item area
+ id: root
+ property var notificationObject
+ property bool expanded: false
+ property bool onlyNotification: false
+ property real fontSize: Appearance.font.pixelSize.small
+ property real padding: onlyNotification ? 0 : 8
+
+ property real dragConfirmThreshold: 70 // Drag further to discard notification
+ property real dismissOvershoot: notificationIcon.implicitWidth + 20 // Account for gaps and bouncy animations
+ property var qmlParent: root?.parent?.parent // There's something between this and the parent ListView
+ property var parentDragIndex: qmlParent?.dragIndex ?? -1
+ property var parentDragDistance: qmlParent?.dragDistance ?? 0
+ property var dragIndexDiff: Math.abs(parentDragIndex - index)
+ property real xOffset: dragIndexDiff == 0 ? Math.max(0, parentDragDistance) :
+ parentDragDistance > dragConfirmThreshold ? 0 :
+ dragIndexDiff == 1 ? Math.max(0, parentDragDistance * 0.3) :
+ dragIndexDiff == 2 ? Math.max(0, parentDragDistance * 0.1) : 0
+
+ implicitHeight: background.implicitHeight
+
+ function processNotificationBody(body, appName) {
+ let processedBody = body
+
+ // Clean Chromium-based browsers notifications - remove first line
+ if (appName) {
+ const lowerApp = appName.toLowerCase()
+ const chromiumBrowsers = [
+ "brave", "chrome", "chromium", "vivaldi", "opera", "microsoft edge"
+ ]
+
+ if (chromiumBrowsers.some(name => lowerApp.includes(name))) {
+ const lines = body.split('\n\n')
+
+ if (lines.length > 1 && lines[0].startsWith(' {
+ Notifications.discardNotification(notificationObject.id);
+ }
+ }
+
+ DragManager { // Drag manager
+ id: dragManager
+ anchors.fill: root
+ anchors.leftMargin: root.expanded ? -notificationIcon.implicitWidth : 0
+ interactive: expanded
+ automaticallyReset: false
+ acceptedButtons: Qt.LeftButton | Qt.MiddleButton
+
+ onClicked: (mouse) => {
+ if (mouse.button === Qt.MiddleButton) {
+ root.destroyWithAnimation();
+ }
+ }
+
+ onPressAndHold: (mouse) => {
+ if (mouse.button === Qt.LeftButton) {
+ Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(notificationObject.body)}'`)
+ notificationSummaryText.text = String.format(qsTr("{0} (copied)"), notificationObject.summary)
+ }
+ }
+ onDraggingChanged: () => {
+ if (dragging) {
+ root.qmlParent.dragIndex = root.index ?? root.parent.children.indexOf(root);
+ }
+ }
+
+ onDragDiffXChanged: () => {
+ root.qmlParent.dragDistance = dragDiffX;
+ }
+
+ onDragReleased: (diffX, diffY) => {
+ if (diffX > root.dragConfirmThreshold)
+ root.destroyWithAnimation();
+ else
+ dragManager.resetDrag();
+ }
+ }
+
+ NotificationAppIcon { // App icon
+ id: notificationIcon
+ opacity: (!onlyNotification && notificationObject.image != "" && expanded) ? 1 : 0
+ visible: opacity > 0
+
+ Behavior on opacity {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+ image: notificationObject.image
+ anchors.right: background.left
+ anchors.top: background.top
+ anchors.rightMargin: 10
+ }
+
+ Rectangle { // Background of notification item
+ id: background
+ width: parent.width
+ anchors.left: parent.left
+ radius: Appearance.rounding.small
+ anchors.leftMargin: root.xOffset
+
+ Behavior on anchors.leftMargin {
+ enabled: !dragManager.dragging
+ NumberAnimation {
+ duration: Appearance.animation.elementMove.duration
+ easing.type: Appearance.animation.elementMove.type
+ easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
+ }
+ }
+
+ color: (expanded && !onlyNotification) ?
+ (notificationObject.urgency == NotificationUrgency.Critical) ?
+ ColorUtils.mix(Appearance.colors.colSecondaryContainer, Appearance.colors.colLayer2, 0.35) :
+ (Appearance.colors.colSurfaceContainerHigh) :
+ ColorUtils.transparentize(Appearance.colors.colSurfaceContainerHighest)
+
+ implicitHeight: expanded ? (contentColumn.implicitHeight + padding * 2) : summaryRow.implicitHeight
+ Behavior on implicitHeight {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+
+ ColumnLayout { // Content column
+ id: contentColumn
+ anchors.fill: parent
+ anchors.margins: expanded ? root.padding : 0
+ spacing: 3
+
+ Behavior on anchors.margins {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+ RowLayout { // Summary row
+ id: summaryRow
+ visible: !root.onlyNotification || !root.expanded
+ Layout.fillWidth: true
+ implicitHeight: summaryText.implicitHeight
+ // Layout.fillWidth: true
+ StyledText {
+ id: summaryText
+ visible: !root.onlyNotification
+ font.pixelSize: root.fontSize
+ color: Appearance.colors.colOnLayer2
+ elide: Text.ElideRight
+ text: root.notificationObject.summary || ""
+ }
+ StyledText {
+ opacity: !root.expanded ? 1 : 0
+ visible: opacity > 0
+ Behavior on opacity {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Layout.fillWidth: true
+ font.pixelSize: root.fontSize
+ color: Appearance.colors.colSubtext
+ elide: Text.ElideRight
+ textFormat: Text.StyledText
+ text: {
+ return processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, " ")
+ }
+ }
+ }
+
+ ColumnLayout { // Expanded content
+ Layout.fillWidth: true
+ opacity: root.expanded ? 1 : 0
+ visible: opacity > 0
+
+ StyledText { // Notification body (expanded)
+ id: notificationBodyText
+ Behavior on opacity {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Layout.fillWidth: true
+ font.pixelSize: root.fontSize
+ color: Appearance.colors.colSubtext
+ wrapMode: Text.Wrap
+ elide: Text.ElideRight
+ textFormat: Text.RichText
+ text: {
+ return `` +
+ `${processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, " ")}`
+ }
+
+ onLinkActivated: (link) => {
+ Qt.openUrlExternally(link)
+ Hyprland.dispatch("global quickshell:sidebarRightClose")
+ }
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.NoButton // Only for hover
+ hoverEnabled: true
+ cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
+ }
+ }
+
+ Flickable { // Notification actions
+ id: actionsFlickable
+ Layout.fillWidth: true
+ implicitHeight: actionRowLayout.implicitHeight
+ contentWidth: actionRowLayout.implicitWidth
+ clip: !onlyNotification
+
+ Behavior on opacity {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Behavior on height {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Behavior on implicitHeight {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+ RowLayout {
+ id: actionRowLayout
+ Layout.alignment: Qt.AlignBottom
+
+ NotificationActionButton {
+ Layout.fillWidth: true
+ buttonText: qsTr("Close")
+ urgency: notificationObject.urgency
+ implicitWidth: (notificationObject.actions.length == 0) ? ((actionsFlickable.width - actionRowLayout.spacing) / 2) :
+ (contentItem.implicitWidth + leftPadding + rightPadding)
+
+ onClicked: {
+ root.destroyWithAnimation()
+ }
+
+ contentItem: MaterialSymbol {
+ iconSize: Appearance.font.pixelSize.large
+ horizontalAlignment: Text.AlignHCenter
+ color: (notificationObject.urgency == NotificationUrgency.Critical) ?
+ Appearance.m3colors.m3onSurfaceVariant : Appearance.m3colors.m3onSurface
+ text: "close"
+ }
+ }
+
+ Repeater {
+ id: actionRepeater
+ model: notificationObject.actions
+ NotificationActionButton {
+ Layout.fillWidth: true
+ buttonText: modelData.text
+ urgency: notificationObject.urgency
+ onClicked: {
+ Notifications.attemptInvokeAction(notificationObject.id, modelData.identifier);
+ }
+ }
+ }
+
+ NotificationActionButton {
+ Layout.fillWidth: true
+ urgency: notificationObject.urgency
+ implicitWidth: (notificationObject.actions.length == 0) ? ((actionsFlickable.width - actionRowLayout.spacing) / 2) :
+ (contentItem.implicitWidth + leftPadding + rightPadding)
+
+ onClicked: {
+ Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(notificationObject.body)}'`)
+ copyIcon.text = "inventory"
+ copyIconTimer.restart()
+ }
+
+ Timer {
+ id: copyIconTimer
+ interval: 1500
+ repeat: false
+ onTriggered: {
+ copyIcon.text = "content_copy"
+ }
+ }
+
+ contentItem: MaterialSymbol {
+ id: copyIcon
+ iconSize: Appearance.font.pixelSize.large
+ horizontalAlignment: Text.AlignHCenter
+ color: (notificationObject.urgency == NotificationUrgency.Critical) ?
+ Appearance.m3colors.m3onSurfaceVariant : Appearance.m3colors.m3onSurface
+ text: "content_copy"
+ }
+ }
+
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/modules/common/widgets/NotificationListView.qml b/.config/quickshell/modules/common/widgets/NotificationListView.qml
new file mode 100644
index 000000000..087e4a403
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/NotificationListView.qml
@@ -0,0 +1,31 @@
+import "root:/"
+import "root:/modules/common/"
+import "root:/modules/common/widgets"
+import "root:/services"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+StyledListView { // Scrollable window
+ id: root
+ property bool popup: false
+
+ spacing: 3
+
+ model: ScriptModel {
+ values: root.popup ? Notifications.popupAppNameList : Notifications.appNameList
+ }
+ delegate: NotificationGroup {
+ required property int index
+ required property var modelData
+ popup: root.popup
+ anchors.left: parent?.left
+ anchors.right: parent?.right
+ notificationGroup: popup ?
+ Notifications.popupGroupsByAppName[modelData] :
+ Notifications.groupsByAppName[modelData]
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/common/widgets/PointingHandInteraction.qml b/.config/quickshell/modules/common/widgets/PointingHandInteraction.qml
new file mode 100644
index 000000000..cf8b065f7
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/PointingHandInteraction.qml
@@ -0,0 +1,7 @@
+import QtQuick
+
+MouseArea {
+ anchors.fill: parent
+ onPressed: (mouse) => mouse.accepted = false
+ cursorShape: Qt.PointingHandCursor
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/common/widgets/PrimaryTabBar.qml b/.config/quickshell/modules/common/widgets/PrimaryTabBar.qml
new file mode 100644
index 000000000..cd048e8b6
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/PrimaryTabBar.qml
@@ -0,0 +1,97 @@
+import "root:/modules/common"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+
+ColumnLayout {
+ id: root
+ spacing: 0
+ required property var tabButtonList // Something like [{"icon": "notifications", "name": qsTr("Notifications")}, {"icon": "volume_up", "name": qsTr("Volume mixer")}]
+ required property var externalTrackedTab
+ property bool enableIndicatorAnimation: false
+ property color colIndicator: Appearance?.colors.colPrimary ?? "#65558F"
+ property color colBorder: Appearance?.m3colors.m3outlineVariant ?? "#C6C6D0"
+ signal currentIndexChanged(int index)
+
+ property bool centerTabBar: parent.width > 500
+ Layout.fillWidth: !centerTabBar
+ Layout.alignment: Qt.AlignHCenter
+ implicitWidth: Math.max(tabBar.implicitWidth, 600)
+
+ TabBar {
+ id: tabBar
+ Layout.fillWidth: true
+ currentIndex: root.externalTrackedTab
+ onCurrentIndexChanged: {
+ root.onCurrentIndexChanged(currentIndex)
+ }
+
+ background: Item {
+ WheelHandler {
+ onWheel: (event) => {
+ if (event.angleDelta.y < 0)
+ tabBar.currentIndex = Math.min(tabBar.currentIndex + 1, root.tabButtonList.length - 1)
+ else if (event.angleDelta.y > 0)
+ tabBar.currentIndex = Math.max(tabBar.currentIndex - 1, 0)
+ }
+ acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+ }
+ }
+
+ Repeater {
+ model: root.tabButtonList
+ delegate: PrimaryTabButton {
+ selected: (index == root.externalTrackedTab)
+ buttonText: modelData.name
+ buttonIcon: modelData.icon
+ minimumWidth: 160
+ }
+ }
+ }
+
+ Item { // Tab indicator
+ id: tabIndicator
+ Layout.fillWidth: true
+ height: 3
+ Connections {
+ target: root
+ function onExternalTrackedTabChanged() {
+ root.enableIndicatorAnimation = true
+ }
+ }
+
+ Rectangle {
+ id: indicator
+ property int tabCount: root.tabButtonList.length
+ property real fullTabSize: root.width / tabCount;
+ property real targetWidth: tabBar.contentItem.children[0].children[tabBar.currentIndex].tabContentWidth
+
+ implicitWidth: targetWidth
+ anchors {
+ top: parent.top
+ bottom: parent.bottom
+ }
+
+ x: tabBar.currentIndex * fullTabSize + (fullTabSize - targetWidth) / 2
+
+ color: root.colIndicator
+ radius: Appearance?.rounding.full ?? 9999
+
+ Behavior on x {
+ animation: Appearance?.animation.elementMove.numberAnimation.createObject(this)
+ }
+
+ Behavior on implicitWidth {
+ animation: Appearance?.animation.elementMove.numberAnimation.createObject(this)
+ }
+ }
+ }
+
+ Rectangle { // Tabbar bottom border
+ id: tabBarBottomBorder
+ Layout.fillWidth: true
+ implicitHeight: 1
+ color: root.colBorder
+ }
+}
diff --git a/.config/quickshell/modules/common/widgets/PrimaryTabButton.qml b/.config/quickshell/modules/common/widgets/PrimaryTabButton.qml
new file mode 100644
index 000000000..a47f108b7
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/PrimaryTabButton.qml
@@ -0,0 +1,173 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell.Widgets
+
+TabButton {
+ id: button
+ property string buttonText
+ property string buttonIcon
+ property real minimumWidth: 110
+ property bool selected: false
+ property int tabContentWidth: contentItem.children[0].implicitWidth
+ property int rippleDuration: 1200
+ height: buttonBackground.height
+ implicitWidth: Math.max(tabContentWidth, buttonBackground.implicitWidth, minimumWidth)
+
+ property color colBackground: ColorUtils.transparentize(Appearance?.colors.colLayer1Hover, 1) || "transparent"
+ property color colBackgroundHover: Appearance?.colors.colLayer1Hover ?? "#E5DFED"
+ property color colRipple: Appearance?.colors.colLayer1Active ?? "#D6CEE2"
+ property color colActive: Appearance?.colors.colPrimary ?? "#65558F"
+ property color colInactive: Appearance?.colors.colOnLayer1 ?? "#45464F"
+
+ component RippleAnim: NumberAnimation {
+ duration: rippleDuration
+ easing.type: Appearance?.animation.elementMoveEnter.type
+ easing.bezierCurve: Appearance?.animationCurves.standardDecel
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onPressed: (event) => {
+ const {x,y} = event
+ const stateY = buttonBackground.y;
+ rippleAnim.x = x;
+ rippleAnim.y = y - stateY;
+
+ const dist = (ox,oy) => ox*ox + oy*oy
+ const stateEndY = stateY + buttonBackground.height
+ rippleAnim.radius = Math.sqrt(Math.max(dist(0, stateY), dist(0, stateEndY), dist(width, stateY), dist(width, stateEndY)))
+
+ rippleFadeAnim.complete();
+ rippleAnim.restart();
+ }
+ onReleased: (event) => {
+ button.click() // Because the MouseArea already consumed the event
+ rippleFadeAnim.restart();
+ }
+ }
+
+ RippleAnim {
+ id: rippleFadeAnim
+ target: ripple
+ property: "opacity"
+ to: 0
+ }
+
+ SequentialAnimation {
+ id: rippleAnim
+
+ property real x
+ property real y
+ property real radius
+
+ PropertyAction {
+ target: ripple
+ property: "x"
+ value: rippleAnim.x
+ }
+ PropertyAction {
+ target: ripple
+ property: "y"
+ value: rippleAnim.y
+ }
+ PropertyAction {
+ target: ripple
+ property: "opacity"
+ value: 1
+ }
+ ParallelAnimation {
+ RippleAnim {
+ target: ripple
+ properties: "implicitWidth,implicitHeight"
+ from: 0
+ to: rippleAnim.radius * 2
+ }
+ }
+ }
+
+ background: Rectangle {
+ id: buttonBackground
+ radius: Appearance?.rounding.small
+ implicitHeight: 50
+ color: (button.hovered ? button.colBackgroundHover : button.colBackground)
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: buttonBackground.width
+ height: buttonBackground.height
+ radius: buttonBackground.radius
+ }
+ }
+
+ Behavior on color {
+ animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+
+ Item {
+ id: ripple
+ width: ripple.implicitWidth
+ height: ripple.implicitHeight
+ opacity: 0
+
+ property real implicitWidth: 0
+ property real implicitHeight: 0
+ visible: width > 0 && height > 0
+
+ Behavior on opacity {
+ animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+
+ RadialGradient {
+ anchors.fill: parent
+ gradient: Gradient {
+ GradientStop { position: 0.0; color: button.colRipple }
+ GradientStop { position: 0.3; color: button.colRipple }
+ GradientStop { position: 0.5 ; color: Qt.rgba(button.colRipple.r, button.colRipple.g, button.colRipple.b, 0) }
+ }
+ }
+
+ transform: Translate {
+ x: -ripple.width / 2
+ y: -ripple.height / 2
+ }
+ }
+ }
+
+ contentItem: Item {
+ anchors.centerIn: buttonBackground
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: 0
+ MaterialSymbol {
+ visible: buttonIcon?.length > 0
+ Layout.alignment: Qt.AlignHCenter
+ horizontalAlignment: Text.AlignHCenter
+ text: buttonIcon
+ iconSize: Appearance?.font.pixelSize.hugeass ?? 25
+ fill: selected ? 1 : 0
+ color: selected ? button.colActive : button.colInactive
+ Behavior on color {
+ animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+ StyledText {
+ id: buttonTextWidget
+ Layout.alignment: Qt.AlignHCenter
+ horizontalAlignment: Text.AlignHCenter
+ font.pixelSize: Appearance?.font.pixelSize.small
+ color: selected ? button.colActive : button.colInactive
+ text: buttonText
+ Behavior on color {
+ animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/common/widgets/Revealer.qml b/.config/quickshell/modules/common/widgets/Revealer.qml
new file mode 100644
index 000000000..2fb5dc3a8
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/Revealer.qml
@@ -0,0 +1,26 @@
+import "root:/modules/common"
+import QtQuick
+import Quickshell
+
+/**
+ * Recreation of GTK revealer. Expects one single child.
+ */
+Item {
+ id: root
+ property bool reveal
+ property bool vertical: false
+ clip: true
+
+ implicitWidth: (reveal || vertical) ? childrenRect.width : 0
+ implicitHeight: (reveal || !vertical) ? childrenRect.height : 0
+ visible: reveal && width > 0 && height > 0
+
+ Behavior on implicitWidth {
+ enabled: !vertical
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ Behavior on implicitHeight {
+ enabled: vertical
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+}
diff --git a/.config/quickshell/modules/common/widgets/RippleButton.qml b/.config/quickshell/modules/common/widgets/RippleButton.qml
new file mode 100644
index 000000000..9931cd02a
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/RippleButton.qml
@@ -0,0 +1,186 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell.Widgets
+
+/**
+ * A button with ripple effect similar to in Material Design.
+ */
+Button {
+ id: root
+ property bool toggled
+ property string buttonText
+ property real buttonRadius: Appearance?.rounding?.small ?? 4
+ property real buttonRadiusPressed: buttonRadius
+ property real buttonEffectiveRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius
+ property int rippleDuration: 1200
+ property bool rippleEnabled: true
+ property var downAction // When left clicking (down)
+ property var releaseAction // When left clicking (release)
+ property var altAction // When right clicking
+ property var middleClickAction // When middle clicking
+
+ property color colBackground: ColorUtils.transparentize(Appearance?.colors.colLayer1Hover, 1) || "transparent"
+ property color colBackgroundHover: Appearance?.colors.colLayer1Hover ?? "#E5DFED"
+ property color colBackgroundToggled: Appearance?.colors.colPrimary ?? "#65558F"
+ property color colBackgroundToggledHover: Appearance?.colors.colPrimaryHover ?? "#77699C"
+ property color colRipple: Appearance?.colors.colLayer1Active ?? "#D6CEE2"
+ property color colRippleToggled: Appearance?.colors.colPrimaryActive ?? "#D6CEE2"
+
+ property color buttonColor: root.enabled ? (root.toggled ?
+ (root.hovered ? colBackgroundToggledHover :
+ colBackgroundToggled) :
+ (root.hovered ? colBackgroundHover :
+ colBackground)) : colBackground
+ property color rippleColor: root.toggled ? colRippleToggled : colRipple
+
+ function startRipple(x, y) {
+ const stateY = buttonBackground.y;
+ rippleAnim.x = x;
+ rippleAnim.y = y - stateY;
+
+ const dist = (ox,oy) => ox*ox + oy*oy
+ const stateEndY = stateY + buttonBackground.height
+ rippleAnim.radius = Math.sqrt(Math.max(dist(0, stateY), dist(0, stateEndY), dist(width, stateY), dist(width, stateEndY)))
+
+ rippleFadeAnim.complete();
+ rippleAnim.restart();
+ }
+
+ component RippleAnim: NumberAnimation {
+ duration: rippleDuration
+ easing.type: Appearance?.animation.elementMoveEnter.type
+ easing.bezierCurve: Appearance?.animationCurves.standardDecel
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+ onPressed: (event) => {
+ if(event.button === Qt.RightButton) {
+ if (root.altAction) root.altAction();
+ return;
+ }
+ if(event.button === Qt.MiddleButton) {
+ if (root.middleClickAction) root.middleClickAction();
+ return;
+ }
+ root.down = true
+ if (root.downAction) root.downAction();
+ if (!root.rippleEnabled) return;
+ const {x,y} = event
+ startRipple(x, y)
+ }
+ onReleased: (event) => {
+ root.down = false
+ if (event.button != Qt.LeftButton) return;
+ if (root.releaseAction) root.releaseAction();
+ root.click() // Because the MouseArea already consumed the event
+ if (!root.rippleEnabled) return;
+ rippleFadeAnim.restart();
+ }
+ onCanceled: (event) => {
+ root.down = false
+ if (!root.rippleEnabled) return;
+ rippleFadeAnim.restart();
+ }
+ }
+
+ RippleAnim {
+ id: rippleFadeAnim
+ target: ripple
+ property: "opacity"
+ to: 0
+ }
+
+ SequentialAnimation {
+ id: rippleAnim
+
+ property real x
+ property real y
+ property real radius
+
+ PropertyAction {
+ target: ripple
+ property: "x"
+ value: rippleAnim.x
+ }
+ PropertyAction {
+ target: ripple
+ property: "y"
+ value: rippleAnim.y
+ }
+ PropertyAction {
+ target: ripple
+ property: "opacity"
+ value: 1
+ }
+ ParallelAnimation {
+ RippleAnim {
+ target: ripple
+ properties: "implicitWidth,implicitHeight"
+ from: 0
+ to: rippleAnim.radius * 2
+ }
+ }
+ }
+
+ background: Rectangle {
+ id: buttonBackground
+ radius: root.buttonEffectiveRadius
+ implicitHeight: 50
+
+ color: root.buttonColor
+ Behavior on color {
+ animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: buttonBackground.width
+ height: buttonBackground.height
+ radius: root.buttonEffectiveRadius
+ }
+ }
+
+ Item {
+ id: ripple
+ width: ripple.implicitWidth
+ height: ripple.implicitHeight
+ opacity: 0
+ visible: width > 0 && height > 0
+
+ property real implicitWidth: 0
+ property real implicitHeight: 0
+
+ Behavior on opacity {
+ animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+
+ RadialGradient {
+ anchors.fill: parent
+ gradient: Gradient {
+ GradientStop { position: 0.0; color: root.rippleColor }
+ GradientStop { position: 0.3; color: root.rippleColor }
+ GradientStop { position: 0.5; color: Qt.rgba(root.rippleColor.r, root.rippleColor.g, root.rippleColor.b, 0) }
+ }
+ }
+
+ transform: Translate {
+ x: -ripple.width / 2
+ y: -ripple.height / 2
+ }
+ }
+ }
+
+ contentItem: StyledText {
+ text: root.buttonText
+ }
+}
diff --git a/.config/quickshell/modules/common/widgets/RoundCorner.qml b/.config/quickshell/modules/common/widgets/RoundCorner.qml
new file mode 100644
index 000000000..c9a2827a6
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/RoundCorner.qml
@@ -0,0 +1,64 @@
+import QtQuick 2.9
+
+Item {
+ id: root
+
+ property int size: 25
+ property color color: "#000000"
+
+ onColorChanged: {
+ canvas.requestPaint();
+ }
+
+ property QtObject cornerEnum: QtObject {
+ property int topLeft: 0
+ property int topRight: 1
+ property int bottomLeft: 2
+ property int bottomRight: 3
+ }
+
+ property int corner: cornerEnum.topLeft // Default to TopLeft
+
+ width: size
+ height: size
+
+ Canvas {
+ id: canvas
+
+ anchors.fill: parent
+ antialiasing: true
+
+ onPaint: {
+ var ctx = getContext("2d");
+ var r = root.size;
+
+ ctx.beginPath();
+ switch (root.corner) {
+ case cornerEnum.topLeft:
+ ctx.arc(r, r, r, Math.PI, 3 * Math.PI / 2);
+ ctx.lineTo(0, 0);
+ break;
+ case cornerEnum.topRight:
+ ctx.arc(0, r, r, 3 * Math.PI / 2, 2 * Math.PI);
+ ctx.lineTo(r, 0);
+ break;
+ case cornerEnum.bottomLeft:
+ ctx.arc(r, 0, r, Math.PI / 2, Math.PI);
+ ctx.lineTo(0, r);
+ break;
+ case cornerEnum.bottomRight:
+ ctx.arc(0, 0, r, 0, Math.PI / 2);
+ ctx.lineTo(r, r);
+ break;
+ }
+ ctx.closePath();
+ ctx.fillStyle = root.color;
+ ctx.fill();
+ }
+ }
+
+ Behavior on size {
+ animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+}
diff --git a/.config/quickshell/modules/common/widgets/SecondaryTabButton.qml b/.config/quickshell/modules/common/widgets/SecondaryTabButton.qml
new file mode 100644
index 000000000..1d3b6381f
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/SecondaryTabButton.qml
@@ -0,0 +1,163 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell.Widgets
+
+TabButton {
+ id: root
+ property string buttonText
+ property string buttonIcon
+ property bool selected: false
+ property int rippleDuration: 1200
+ height: buttonBackground.height
+ property int tabContentWidth: buttonBackground.width - buttonBackground.radius*2
+
+ property color colBackground: ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
+ property color colBackgroundHover: Appearance.colors.colLayer1Hover
+ property color colRipple: Appearance.colors.colLayer1Active
+
+ PointingHandInteraction {}
+
+ component RippleAnim: NumberAnimation {
+ duration: rippleDuration
+ easing.type: Appearance.animation.elementMoveEnter.type
+ easing.bezierCurve: Appearance.animationCurves.standardDecel
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onPressed: (event) => {
+ const {x,y} = event
+ const stateY = buttonBackground.y;
+ rippleAnim.x = x;
+ rippleAnim.y = y - stateY;
+
+ const dist = (ox,oy) => ox*ox + oy*oy
+ const stateEndY = stateY + buttonBackground.height
+ rippleAnim.radius = Math.sqrt(Math.max(dist(0, stateY), dist(0, stateEndY), dist(width, stateY), dist(width, stateEndY)))
+
+ rippleFadeAnim.complete();
+ rippleAnim.restart();
+ }
+ onReleased: (event) => {
+ root.click() // Because the MouseArea already consumed the event
+ rippleFadeAnim.restart();
+ }
+ }
+
+ RippleAnim {
+ id: rippleFadeAnim
+ target: ripple
+ property: "opacity"
+ to: 0
+ }
+
+ SequentialAnimation {
+ id: rippleAnim
+
+ property real x
+ property real y
+ property real radius
+
+ PropertyAction {
+ target: ripple
+ property: "x"
+ value: rippleAnim.x
+ }
+ PropertyAction {
+ target: ripple
+ property: "y"
+ value: rippleAnim.y
+ }
+ PropertyAction {
+ target: ripple
+ property: "opacity"
+ value: 1
+ }
+ ParallelAnimation {
+ RippleAnim {
+ target: ripple
+ properties: "implicitWidth,implicitHeight"
+ from: 0
+ to: rippleAnim.radius * 2
+ }
+ }
+ }
+
+ background: Rectangle {
+ id: buttonBackground
+ radius: Appearance?.rounding.small ?? 7
+ implicitHeight: 37
+ color: (root.hovered ? root.colBackgroundHover : root.colBackground)
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: buttonBackground.width
+ height: buttonBackground.height
+ radius: buttonBackground.radius
+ }
+ }
+
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+
+ Rectangle {
+ id: ripple
+
+ radius: Appearance.rounding.full
+ color: root.colRipple
+ opacity: 0
+
+ transform: Translate {
+ x: -ripple.width / 2
+ y: -ripple.height / 2
+ }
+ }
+ }
+
+ contentItem: Item {
+ anchors.centerIn: buttonBackground
+ RowLayout {
+ anchors.centerIn: parent
+ spacing: 0
+
+ Loader {
+ id: iconLoader
+ active: buttonIcon?.length > 0
+ sourceComponent: buttonIcon?.length > 0 ? materialSymbolComponent : null
+ Layout.rightMargin: 5
+ }
+
+ Component {
+ id: materialSymbolComponent
+ MaterialSymbol {
+ verticalAlignment: Text.AlignVCenter
+ text: buttonIcon
+ iconSize: Appearance.font.pixelSize.huge
+ fill: selected ? 1 : 0
+ color: selected ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer1
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+ }
+ StyledText {
+ id: buttonTextWidget
+ verticalAlignment: Text.AlignVCenter
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: selected ? Appearance.colors.colPrimary : Appearance.colors.colOnLayer1
+ text: buttonText
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/common/widgets/SelectionDialog.qml b/.config/quickshell/modules/common/widgets/SelectionDialog.qml
new file mode 100644
index 000000000..9cf0940ed
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/SelectionDialog.qml
@@ -0,0 +1,129 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+
+Item {
+ id: root
+ property real dialogPadding: 15
+ property real dialogMargin: 30
+ property string titleText: "Selection Dialog"
+ property alias items: choiceModel.values
+ property int selectedId: choiceListView.currentIndex
+ property var defaultChoice
+
+ signal canceled();
+ signal selected(var result);
+
+ Rectangle { // Scrim
+ id: scrimOverlay
+ anchors.fill: parent
+ radius: Appearance.rounding.small
+ color: Appearance.colors.colScrim
+ MouseArea {
+ hoverEnabled: true
+ anchors.fill: parent
+ preventStealing: true
+ propagateComposedEvents: false
+ }
+ }
+
+ Rectangle { // The dialog
+ id: dialog
+ color: Appearance.colors.colSurfaceContainerHigh
+ radius: Appearance.rounding.normal
+ anchors.fill: parent
+ anchors.margins: dialogMargin
+ implicitHeight: dialogColumnLayout.implicitHeight
+
+ ColumnLayout {
+ id: dialogColumnLayout
+ anchors.fill: parent
+ spacing: 16
+
+ StyledText {
+ id: dialogTitle
+ Layout.topMargin: dialogPadding
+ Layout.leftMargin: dialogPadding
+ Layout.rightMargin: dialogPadding
+ Layout.alignment: Qt.AlignLeft
+ color: Appearance.m3colors.m3onSurface
+ font.pixelSize: Appearance.font.pixelSize.larger
+ text: root.titleText
+ }
+
+ Rectangle {
+ color: Appearance.m3colors.m3outline
+ implicitHeight: 1
+ Layout.fillWidth: true
+ Layout.leftMargin: dialogPadding
+ Layout.rightMargin: dialogPadding
+ }
+
+ ListView {
+ id: choiceListView
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+ currentIndex: root.defaultChoice !== undefined ? root.items.indexOf(root.defaultChoice) : -1
+
+ model: ScriptModel {
+ id: choiceModel
+ }
+
+ delegate: StyledRadioButton {
+ id: radioButton
+ required property var modelData
+ required property int index
+ anchors {
+ left: parent?.left
+ right: parent?.right
+ leftMargin: root.dialogPadding
+ rightMargin: root.dialogPadding
+ }
+
+ description: modelData.toString()
+ checked: index === choiceListView.currentIndex
+
+ onCheckedChanged: {
+ if (checked) {
+ choiceListView.currentIndex = index;
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ color: Appearance.m3colors.m3outline
+ implicitHeight: 1
+ Layout.fillWidth: true
+ Layout.leftMargin: dialogPadding
+ Layout.rightMargin: dialogPadding
+ }
+
+ RowLayout {
+ id: dialogButtonsRowLayout
+ Layout.bottomMargin: dialogPadding
+ Layout.leftMargin: dialogPadding
+ Layout.rightMargin: dialogPadding
+ Layout.alignment: Qt.AlignRight
+
+ DialogButton {
+ buttonText: qsTr("Cancel")
+ onClicked: root.canceled()
+ }
+ DialogButton {
+ buttonText: qsTr("OK")
+ onClicked: root.selected(
+ root.selectedId === -1 ? null :
+ root.items[root.selectedId]
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/modules/common/widgets/StyledLabel.qml b/.config/quickshell/modules/common/widgets/StyledLabel.qml
new file mode 100644
index 000000000..f5201baea
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/StyledLabel.qml
@@ -0,0 +1,16 @@
+import "root:/modules/common"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Label {
+ renderType: Text.NativeRendering
+ verticalAlignment: Text.AlignVCenter
+ font {
+ hintingPreference: Font.PreferFullHinting
+ family: Appearance?.font.family.main ?? "sans-serif"
+ pixelSize: Appearance?.font.pixelSize.small ?? 15
+ }
+ color: Appearance?.m3colors.m3onBackground ?? "black"
+ linkColor: Appearance?.m3colors.m3primary
+}
diff --git a/.config/quickshell/modules/common/widgets/StyledListView.qml b/.config/quickshell/modules/common/widgets/StyledListView.qml
new file mode 100644
index 000000000..76d9782b4
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/StyledListView.qml
@@ -0,0 +1,110 @@
+import "root:/"
+import "root:/modules/common/"
+import "root:/modules/common/widgets"
+import "root:/services"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+/**
+ * A ListView with animations.
+ */
+ListView {
+ id: root
+ spacing: 5
+ property real removeOvershoot: 20 // Account for gaps and bouncy animations
+ property int dragIndex: -1
+ property real dragDistance: 0
+ property bool popin: true
+
+ function resetDrag() {
+ root.dragIndex = -1
+ root.dragDistance = 0
+ }
+
+ add: Transition {
+ animations: [
+ Appearance?.animation.elementMove.numberAnimation.createObject(this, {
+ properties: popin ? "opacity,scale" : "opacity",
+ from: 0,
+ to: 1,
+ }),
+ ]
+ }
+
+ addDisplaced: Transition {
+ animations: [
+ Appearance?.animation.elementMove.numberAnimation.createObject(this, {
+ property: "y",
+ }),
+ Appearance?.animation.elementMove.numberAnimation.createObject(this, {
+ properties: popin ? "opacity,scale" : "opacity",
+ to: 1,
+ }),
+ ]
+ }
+
+ // displaced: Transition {
+ // animations: [
+ // Appearance?.animation.elementMove.numberAnimation.createObject(this, {
+ // property: "y",
+ // }),
+ // Appearance?.animation.elementMove.numberAnimation.createObject(this, {
+ // properties: "opacity,scale",
+ // to: 1,
+ // }),
+ // ]
+ // }
+
+ // move: Transition {
+ // animations: [
+ // Appearance?.animation.elementMove.numberAnimation.createObject(this, {
+ // property: "y",
+ // }),
+ // Appearance?.animation.elementMove.numberAnimation.createObject(this, {
+ // properties: "opacity,scale",
+ // to: 1,
+ // }),
+ // ]
+ // }
+ // moveDisplaced: Transition {
+ // animations: [
+ // Appearance?.animation.elementMove.numberAnimation.createObject(this, {
+ // property: "y",
+ // }),
+ // Appearance?.animation.elementMove.numberAnimation.createObject(this, {
+ // properties: "opacity,scale",
+ // to: 1,
+ // }),
+ // ]
+ // }
+
+ remove: Transition {
+ animations: [
+ Appearance?.animation.elementMove.numberAnimation.createObject(this, {
+ property: "x",
+ to: root.width + root.removeOvershoot,
+ }),
+ Appearance?.animation.elementMove.numberAnimation.createObject(this, {
+ property: "opacity",
+ to: 0,
+ })
+ ]
+ }
+
+ // This is movement when something is removed, not removing animation!
+ removeDisplaced: Transition {
+ animations: [
+ Appearance?.animation.elementMove.numberAnimation.createObject(this, {
+ property: "y",
+ }),
+ Appearance?.animation.elementMove.numberAnimation.createObject(this, {
+ properties: "opacity,scale",
+ to: 1,
+ }),
+ ]
+ }
+}
diff --git a/.config/quickshell/modules/common/widgets/StyledProgressBar.qml b/.config/quickshell/modules/common/widgets/StyledProgressBar.qml
new file mode 100644
index 000000000..31bce5915
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/StyledProgressBar.qml
@@ -0,0 +1,105 @@
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Widgets
+import Qt5Compat.GraphicalEffects
+
+/**
+ * Material 3 progress bar. See https://m3.material.io/components/progress-indicators/overview
+ */
+ProgressBar {
+ id: root
+ property real valueBarWidth: 120
+ property real valueBarHeight: 4
+ property real valueBarGap: 4
+ property color highlightColor: Appearance?.colors.colPrimary ?? "#685496"
+ property color trackColor: Appearance?.m3colors.m3secondaryContainer ?? "#F1D3F9"
+ property bool sperm: false // If true, the progress bar will have a wavy fill effect
+ property real spermAmplitudeMultiplier: sperm ? 0.5 : 0
+ property real spermFrequency: 6
+ property real spermFps: 60
+
+ Behavior on spermAmplitudeMultiplier {
+ animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+ Behavior on value {
+ animation: Appearance?.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+
+ background: Rectangle {
+ anchors.fill: parent
+ color: "transparent"
+ radius: Appearance?.rounding.full ?? 9999
+ implicitHeight: valueBarHeight
+ implicitWidth: valueBarWidth
+ }
+
+ contentItem: Item {
+ implicitWidth: parent.width
+ implicitHeight: parent.height
+
+ Canvas {
+ id: wavyFill
+ anchors {
+ left: parent.left
+ right: parent.right
+ verticalCenter: parent.verticalCenter
+ }
+ height: parent.height * 6
+ onPaint: {
+ var ctx = getContext("2d");
+ ctx.clearRect(0, 0, width, height);
+
+ var progress = root.visualPosition;
+ var fillWidth = progress * width;
+ var amplitude = parent.height * root.spermAmplitudeMultiplier;
+ var frequency = root.spermFrequency;
+ var phase = Date.now() / 400.0;
+ var centerY = height / 2;
+
+ ctx.strokeStyle = root.highlightColor;
+ ctx.lineWidth = parent.height;
+ ctx.lineCap = "round";
+ ctx.beginPath();
+ for (var x = ctx.lineWidth / 2; x <= fillWidth; x += 1) {
+ var waveY = centerY + amplitude * Math.sin(frequency * 2 * Math.PI * x / width + phase);
+ if (x === 0)
+ ctx.moveTo(x, waveY);
+ else
+ ctx.lineTo(x, waveY);
+ }
+ ctx.stroke();
+ }
+ Connections {
+ target: root
+ function onValueChanged() { wavyFill.requestPaint(); }
+ function onHighlightColorChanged() { wavyFill.requestPaint(); }
+ }
+ Timer {
+ interval: 1000 / root.spermFps
+ running: root.sperm
+ repeat: root.sperm
+ onTriggered: wavyFill.requestPaint()
+ }
+ }
+ Rectangle { // Right remaining part fill
+ anchors.right: parent.right
+ width: (1 - root.visualPosition) * parent.width - valueBarGap
+ height: parent.height
+ radius: Appearance?.rounding.full ?? 9999
+ color: root.trackColor
+ }
+ Rectangle { // Stop point
+ anchors.right: parent.right
+ width: valueBarGap
+ height: valueBarGap
+ radius: Appearance?.rounding.full ?? 9999
+ color: root.highlightColor
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/common/widgets/StyledRadioButton.qml b/.config/quickshell/modules/common/widgets/StyledRadioButton.qml
new file mode 100644
index 000000000..3ef1ee8ad
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/StyledRadioButton.qml
@@ -0,0 +1,86 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Widgets
+import Quickshell.Services.Pipewire
+
+RadioButton {
+ id: root
+ implicitHeight: 40
+ property string description
+ property color activeColor: Appearance?.colors.colPrimary ?? "#685496"
+ property color inactiveColor: Appearance?.m3colors.m3onSurfaceVariant ?? "#45464F"
+
+ PointingHandInteraction {}
+
+ indicator: Item{}
+
+ contentItem: RowLayout {
+ Layout.fillWidth: true
+ spacing: 12
+ Rectangle {
+ id: radio
+ Layout.fillWidth: false
+ Layout.alignment: Qt.AlignVCenter
+ width: 20
+ height: 20
+ radius: Appearance?.rounding.full
+ border.color: checked ? root.activeColor : root.inactiveColor
+ border.width: 2
+ color: "transparent"
+
+ // Checked indicator
+ Rectangle {
+ anchors.centerIn: parent
+ width: checked ? 10 : 4
+ height: checked ? 10 : 4
+ radius: Appearance?.rounding.full
+ color: Appearance?.colors.colPrimary
+ opacity: checked ? 1 : 0
+
+ Behavior on opacity {
+ animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Behavior on width {
+ animation: Appearance?.animation.elementMove.numberAnimation.createObject(this)
+ }
+ Behavior on height {
+ animation: Appearance?.animation.elementMove.numberAnimation.createObject(this)
+ }
+
+ }
+
+ // Hover
+ Rectangle {
+ anchors.centerIn: parent
+ width: root.hovered ? 40 : 20
+ height: root.hovered ? 40 : 20
+ radius: Appearance?.rounding.full
+ color: Appearance?.m3colors.m3onSurface
+ opacity: root.hovered ? 0.1 : 0
+
+ Behavior on opacity {
+ animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Behavior on width {
+ animation: Appearance?.animation.elementMove.numberAnimation.createObject(this)
+ }
+ Behavior on height {
+ animation: Appearance?.animation.elementMove.numberAnimation.createObject(this)
+ }
+ }
+ }
+
+ StyledText {
+ text: root.description
+ Layout.alignment: Qt.AlignVCenter
+ Layout.fillWidth: true
+ wrapMode: Text.Wrap
+ color: Appearance?.m3colors.m3onSurface
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/common/widgets/StyledRectangularShadow.qml b/.config/quickshell/modules/common/widgets/StyledRectangularShadow.qml
new file mode 100644
index 000000000..6e1f2e16e
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/StyledRectangularShadow.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Effects
+import "root:/modules/common"
+
+RectangularShadow {
+ required property var target
+ anchors.fill: target
+ radius: target.radius
+ blur: 1.2 * Appearance.sizes.elevationMargin
+ spread: 1
+ color: Appearance.colors.colShadow
+ cached: true
+}
diff --git a/.config/quickshell/modules/common/widgets/StyledSlider.qml b/.config/quickshell/modules/common/widgets/StyledSlider.qml
new file mode 100644
index 000000000..ca0980030
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/StyledSlider.qml
@@ -0,0 +1,113 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Widgets
+
+// Material 3 slider. See https://m3.material.io/components/sliders/overview
+Slider {
+ id: root
+ property real scale: 0.85
+ property real backgroundDotSize: 4 * scale
+ property real backgroundDotMargins: 4 * scale
+ // property real handleMargins: 0 * scale
+ property real handleMargins: (root.pressed ? 0 : 2) * scale
+ property real handleWidth: (root.pressed ? 3 : 5) * scale
+ property real handleHeight: 44 * scale
+ property real handleLimit: root.backgroundDotMargins
+ property real trackHeight: 30 * scale
+ property color highlightColor: Appearance.colors.colPrimary
+ property color trackColor: Appearance.colors.colSecondaryContainer
+ property color handleColor: Appearance.m3colors.m3onSecondaryContainer
+ property real trackRadius: Appearance.rounding.verysmall * scale
+ property real unsharpenRadius: Appearance.rounding.unsharpen
+
+ property real limitedHandleRangeWidth: (root.availableWidth - handleWidth - root.handleLimit * 2)
+ property string tooltipContent: `${Math.round(value * 100)}%`
+ Layout.fillWidth: true
+ from: 0
+ to: 1
+
+ Behavior on value { // This makes the adjusted value (like volume) shift smoothly
+ SmoothedAnimation {
+ velocity: Appearance.animation.elementMoveFast.velocity
+ }
+ }
+
+ Behavior on handleMargins {
+ NumberAnimation {
+ duration: Appearance.animation.elementMoveFast.duration
+ easing.type: Appearance.animation.elementMoveFast.type
+ easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onPressed: (mouse) => mouse.accepted = false
+ cursorShape: root.pressed ? Qt.ClosedHandCursor : Qt.PointingHandCursor
+ }
+
+ background: Item {
+ anchors.verticalCenter: parent.verticalCenter
+ implicitHeight: trackHeight
+
+ // Fill left
+ Rectangle {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ width: root.handleLimit * 2 + root.visualPosition * root.limitedHandleRangeWidth - (root.handleMargins + root.handleWidth / 2)
+ height: trackHeight
+ color: root.highlightColor
+ topLeftRadius: root.trackRadius
+ bottomLeftRadius: root.trackRadius
+ topRightRadius: root.unsharpenRadius
+ bottomRightRadius: root.unsharpenRadius
+ }
+
+ // Fill right
+ Rectangle {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: parent.right
+ width: root.handleLimit * 2 + (1 - root.visualPosition) * root.limitedHandleRangeWidth - (root.handleMargins + root.handleWidth / 2)
+ height: trackHeight
+ color: root.trackColor
+ topLeftRadius: root.unsharpenRadius
+ bottomLeftRadius: root.unsharpenRadius
+ topRightRadius: root.trackRadius
+ bottomRightRadius: root.trackRadius
+ }
+
+ // Dot at the end
+ Rectangle {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: parent.right
+ anchors.rightMargin: root.backgroundDotMargins
+ width: root.backgroundDotSize
+ height: root.backgroundDotSize
+ radius: Appearance.rounding.full
+ color: root.handleColor
+ }
+ }
+
+ handle: Rectangle {
+ id: handle
+ x: root.leftPadding + root.handleLimit + root.visualPosition * root.limitedHandleRangeWidth
+ y: root.topPadding + root.availableHeight / 2 - height / 2
+ implicitWidth: root.handleWidth
+ implicitHeight: root.handleHeight
+ radius: Appearance.rounding.full
+ color: root.handleColor
+
+ Behavior on implicitWidth {
+ animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+ StyledToolTip {
+ extraVisibleCondition: root.pressed
+ content: root.tooltipContent
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/common/widgets/StyledSwitch.qml b/.config/quickshell/modules/common/widgets/StyledSwitch.qml
new file mode 100644
index 000000000..217a2f7e4
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/StyledSwitch.qml
@@ -0,0 +1,60 @@
+import "root:/modules/common/"
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import Qt5Compat.GraphicalEffects
+
+/**
+ * Material 3 switch. See https://m3.material.io/components/switch/overview
+ */
+Switch {
+ id: root
+ property real scale: 1
+ implicitHeight: 32 * root.scale
+ implicitWidth: 52 * root.scale
+ property color activeColor: Appearance?.colors.colPrimary ?? "#685496"
+ property color inactiveColor: Appearance?.colors.colSurfaceContainerHighest ?? "#45464F"
+
+ PointingHandInteraction {}
+
+ // Custom track styling
+ background: Rectangle {
+ width: parent.width
+ height: parent.height
+ radius: Appearance?.rounding.full ?? 9999
+ color: root.checked ? root.activeColor : root.inactiveColor
+ border.width: 2 * root.scale
+ border.color: root.checked ? root.activeColor : Appearance.m3colors.m3outline
+
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ Behavior on border.color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+
+ // Custom thumb styling
+ indicator: Rectangle {
+ width: root.pressed ? (28 * root.scale) : root.checked ? (24 * root.scale) : (16 * root.scale)
+ height: root.pressed ? (28 * root.scale) : root.checked ? (24 * root.scale) : (16 * root.scale)
+ radius: Appearance.rounding.full
+ color: root.checked ? Appearance.m3colors.m3onPrimary : Appearance.m3colors.m3outline
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.leftMargin: root.checked ? (root.pressed ? (22 * root.scale) : 24 * root.scale) : (root.pressed ? (2 * root.scale) : 8 * root.scale)
+
+ Behavior on anchors.leftMargin {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+ Behavior on width {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+ Behavior on height {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+}
diff --git a/.config/quickshell/modules/common/widgets/StyledText.qml b/.config/quickshell/modules/common/widgets/StyledText.qml
new file mode 100644
index 000000000..7750456e0
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/StyledText.qml
@@ -0,0 +1,15 @@
+import "root:/modules/common"
+import QtQuick
+import QtQuick.Layouts
+
+Text {
+ renderType: Text.NativeRendering
+ verticalAlignment: Text.AlignVCenter
+ font {
+ hintingPreference: Font.PreferFullHinting
+ family: Appearance?.font.family.main ?? "sans-serif"
+ pixelSize: Appearance?.font.pixelSize.small ?? 15
+ }
+ color: Appearance?.m3colors.m3onBackground ?? "black"
+ linkColor: Appearance?.m3colors.m3primary
+}
diff --git a/.config/quickshell/modules/common/widgets/StyledTextArea.qml b/.config/quickshell/modules/common/widgets/StyledTextArea.qml
new file mode 100644
index 000000000..67d417576
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/StyledTextArea.qml
@@ -0,0 +1,15 @@
+import "root:/modules/common"
+import QtQuick
+import QtQuick.Controls
+
+TextArea {
+ renderType: Text.NativeRendering
+ selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
+ selectionColor: Appearance.colors.colSecondaryContainer
+ placeholderTextColor: Appearance.m3colors.m3outline
+ font {
+ family: Appearance?.font.family.main ?? "sans-serif"
+ pixelSize: Appearance?.font.pixelSize.small ?? 15
+ hintingPreference: Font.PreferFullHinting
+ }
+}
diff --git a/.config/quickshell/modules/common/widgets/StyledToolTip.qml b/.config/quickshell/modules/common/widgets/StyledToolTip.qml
new file mode 100644
index 000000000..1b4bd033a
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/StyledToolTip.qml
@@ -0,0 +1,60 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+ToolTip {
+ id: root
+ property string content
+ property bool extraVisibleCondition: true
+ property bool alternativeVisibleCondition: false
+ property bool internalVisibleCondition: {
+ const ans = (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition
+ return ans
+ }
+ verticalPadding: 5
+ horizontalPadding: 10
+ opacity: internalVisibleCondition ? 1 : 0
+ visible: opacity > 0
+
+ Behavior on opacity {
+ animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+ background: null
+
+ contentItem: Item {
+ id: contentItemBackground
+ implicitWidth: tooltipTextObject.width + 2 * root.horizontalPadding
+ implicitHeight: tooltipTextObject.height + 2 * root.verticalPadding
+
+ Rectangle {
+ id: backgroundRectangle
+ anchors.bottom: contentItemBackground.bottom
+ anchors.horizontalCenter: contentItemBackground.horizontalCenter
+ color: Appearance?.colors.colTooltip ?? "#3C4043"
+ radius: Appearance?.rounding.verysmall ?? 7
+ width: internalVisibleCondition ? (tooltipTextObject.width + 2 * padding) : 0
+ height: internalVisibleCondition ? (tooltipTextObject.height + 2 * padding) : 0
+ clip: true
+
+ Behavior on width {
+ animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Behavior on height {
+ animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+ StyledText {
+ id: tooltipTextObject
+ anchors.centerIn: parent
+ text: content
+ font.pixelSize: Appearance?.font.pixelSize.smaller ?? 14
+ font.hintingPreference: Font.PreferNoHinting // Prevent shaky text
+ color: Appearance?.colors.colOnTooltip ?? "#FFFFFF"
+ wrapMode: Text.Wrap
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/common/widgets/VerticalButtonGroup.qml b/.config/quickshell/modules/common/widgets/VerticalButtonGroup.qml
new file mode 100644
index 000000000..7d8fc29fe
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/VerticalButtonGroup.qml
@@ -0,0 +1,47 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+/**
+ * A container that supports GroupButton children for bounciness.
+ * See https://m3.material.io/components/button-groups/overview
+ */
+Rectangle {
+ id: root
+ default property alias content: columnLayout.data
+ property real spacing: 5
+ property real padding: 0
+ property int clickIndex: columnLayout.clickIndex
+
+ property real contentHeight: {
+ let total = 0;
+ for (let i = 0; i < columnLayout.children.length; ++i) {
+ const child = columnLayout.children[i];
+ total += child.baseHeight ?? child.implicitHeight ?? child.height;
+ }
+ return total + columnLayout.spacing * (columnLayout.children.length - 1);
+ }
+
+ topLeftRadius: columnLayout.children.length > 0 ? (columnLayout.children[0].radius + padding) :
+ Appearance?.rounding?.small
+ topRightRadius: topLeftRadius
+ bottomLeftRadius: columnLayout.children.length > 0 ? (columnLayout.children[columnLayout.children.length - 1].radius + padding) :
+ Appearance?.rounding?.small
+ bottomRightRadius: bottomLeftRadius
+
+ color: "transparent"
+ height: root.contentHeight + padding * 2
+ implicitWidth: columnLayout.implicitWidth + padding * 2
+ implicitHeight: root.contentHeight + padding * 2
+
+ children: [ColumnLayout {
+ id: columnLayout
+ anchors.fill: parent
+ anchors.margins: root.padding
+ spacing: root.spacing
+ property int clickIndex: -1
+ }]
+}
diff --git a/.config/quickshell/modules/common/widgets/WaveVisualizer.qml b/.config/quickshell/modules/common/widgets/WaveVisualizer.qml
new file mode 100644
index 000000000..571e71838
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/WaveVisualizer.qml
@@ -0,0 +1,78 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Effects
+import QtQuick.Layouts
+import QtQuick.Controls
+import Quickshell
+import Quickshell.Io
+
+Canvas { // Visualizer
+ id: root
+ property list points
+ property list smoothPoints
+ property real maxVisualizerValue: 1000
+ property int smoothing: 2
+ property bool live: true
+ property color color: Appearance.m3colors.m3primary
+
+ onPointsChanged: () => {
+ root.requestPaint()
+ }
+
+ anchors.fill: parent
+ onPaint: {
+ var ctx = getContext("2d");
+ ctx.clearRect(0, 0, width, height);
+
+ var points = root.points;
+ var maxVal = root.maxVisualizerValue || 1;
+ var h = height;
+ var w = width;
+ var n = points.length;
+ if (n < 2) return;
+
+ // Smoothing: simple moving average (optional)
+ var smoothWindow = root.smoothing; // adjust for more/less smoothing
+ root.smoothPoints = [];
+ for (var i = 0; i < n; ++i) {
+ var sum = 0, count = 0;
+ for (var j = -smoothWindow; j <= smoothWindow; ++j) {
+ var idx = Math.max(0, Math.min(n - 1, i + j));
+ sum += points[idx];
+ count++;
+ }
+ root.smoothPoints.push(sum / count);
+ }
+ if (!root.live) root.smoothPoints.fill(0); // If not playing, show no points
+
+ ctx.beginPath();
+ ctx.moveTo(0, h);
+ for (var i = 0; i < n; ++i) {
+ var x = i * w / (n - 1);
+ var y = h - (root.smoothPoints[i] / maxVal) * h;
+ ctx.lineTo(x, y);
+ }
+ ctx.lineTo(w, h);
+ ctx.closePath();
+
+ ctx.fillStyle = Qt.rgba(
+ root.color.r,
+ root.color.g,
+ root.color.b,
+ 0.15
+ );
+ ctx.fill();
+ }
+
+ layer.enabled: true
+ layer.effect: MultiEffect { // Blur a bit to obscure away the points
+ source: root
+ saturation: 0.2
+ blurEnabled: true
+ blurMax: 7
+ blur: 1
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/common/widgets/notification_utils.js b/.config/quickshell/modules/common/widgets/notification_utils.js
new file mode 100644
index 000000000..9b151055c
--- /dev/null
+++ b/.config/quickshell/modules/common/widgets/notification_utils.js
@@ -0,0 +1,77 @@
+
+/**
+ * @param { string } summary
+ * @returns { string }
+ */
+function findSuitableMaterialSymbol(summary = "") {
+ const defaultType = 'chat';
+ if(summary.length === 0) return defaultType;
+
+ const keywordsToTypes = {
+ 'reboot': 'restart_alt',
+ 'recording': 'screen_record',
+ 'battery': 'power',
+ 'power': 'power',
+ 'screenshot': 'screenshot_monitor',
+ 'welcome': 'waving_hand',
+ 'time': 'scheduleb',
+ 'installed': 'download',
+ 'configuration reloaded': 'reset_wrench',
+ 'config': 'reset_wrench',
+ 'update': 'update',
+ 'ai response': 'neurology',
+ 'control': 'settings',
+ 'upscale': 'compare',
+ 'install': 'deployed_code_update',
+ 'startswith:file': 'folder_copy', // Declarative startsWith check
+ };
+
+ const lowerSummary = summary.toLowerCase();
+
+ for (const [keyword, type] of Object.entries(keywordsToTypes)) {
+ if (keyword.startsWith('startswith:')) {
+ const startsWithKeyword = keyword.replace('startswith:', '');
+ if (lowerSummary.startsWith(startsWithKeyword)) {
+ return type;
+ }
+ } else if (lowerSummary.includes(keyword)) {
+ return type;
+ }
+ }
+
+ return defaultType;
+}
+
+/**
+ * @param { number | string | Date } timestamp
+ * @returns { string }
+ */
+const getFriendlyNotifTimeString = (timestamp) => {
+ if (!timestamp) return '';
+ const messageTime = new Date(timestamp);
+ const now = new Date();
+ const diffMs = now.getTime() - messageTime.getTime();
+
+ // Less than 1 minute
+ if (diffMs < 60000)
+ return 'Now';
+
+ // Same day - show relative time
+ if (messageTime.toDateString() === now.toDateString()) {
+ const diffMinutes = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMs / 3600000);
+
+ if (diffHours > 0) {
+ return `${diffHours}h`;
+ } else {
+ return `${diffMinutes}m`;
+ }
+ }
+
+ // Yesterday
+ if (messageTime.toDateString() === new Date(now.getTime() - 86400000).toDateString())
+ return 'Yesterday';
+
+ // Older dates
+ return Qt.formatDateTime(messageTime, "MMMM dd");
+};
\ No newline at end of file
diff --git a/.config/quickshell/modules/dock/Dock.qml b/.config/quickshell/modules/dock/Dock.qml
new file mode 100644
index 000000000..524fbc11f
--- /dev/null
+++ b/.config/quickshell/modules/dock/Dock.qml
@@ -0,0 +1,148 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Scope { // Scope
+ id: root
+ property bool pinned: ConfigOptions?.dock.pinnedOnStartup ?? false
+
+ Variants { // For each monitor
+ model: Quickshell.screens
+
+ Loader {
+ id: dockLoader
+ required property var modelData
+ active: ConfigOptions?.dock.hoverToReveal || (!ToplevelManager.activeToplevel?.activated)
+
+ sourceComponent: PanelWindow { // Window
+ id: dockRoot
+ screen: dockLoader.modelData
+
+ property bool reveal: root.pinned
+ || (ConfigOptions?.dock.hoverToReveal && dockMouseArea.containsMouse)
+ || dockApps.requestDockShow
+ || (!ToplevelManager.activeToplevel?.activated)
+
+ anchors {
+ bottom: true
+ left: true
+ right: true
+ }
+
+ exclusiveZone: root.pinned ? implicitHeight
+ - (Appearance.sizes.hyprlandGapsOut)
+ - (Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut) : 0
+
+ implicitWidth: dockBackground.implicitWidth
+ WlrLayershell.namespace: "quickshell:dock"
+ color: "transparent"
+
+ implicitHeight: (ConfigOptions?.dock.height ?? 70) + Appearance.sizes.elevationMargin + Appearance.sizes.hyprlandGapsOut
+
+ mask: Region {
+ item: dockMouseArea
+ }
+
+ MouseArea {
+ id: dockMouseArea
+ anchors.top: parent.top
+ height: parent.height
+ anchors.topMargin: dockRoot.reveal ? 0 :
+ ConfigOptions?.dock.hoverToReveal ? (dockRoot.implicitHeight - ConfigOptions.dock.hoverRegionHeight) :
+ (dockRoot.implicitHeight + 1)
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ hoverEnabled: true
+
+ Behavior on anchors.topMargin {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+ Item {
+ id: dockHoverRegion
+ anchors.fill: parent
+
+ Item { // Wrapper for the dock background
+ id: dockBackground
+ anchors {
+ top: parent.top
+ bottom: parent.bottom
+ horizontalCenter: parent.horizontalCenter
+ }
+
+ implicitWidth: dockRow.implicitWidth + 5 * 2
+ height: parent.height - Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut
+
+ StyledRectangularShadow {
+ target: dockVisualBackground
+ }
+ Rectangle { // The real rectangle that is visible
+ id: dockVisualBackground
+ property real margin: Appearance.sizes.elevationMargin
+ anchors.fill: parent
+ anchors.topMargin: Appearance.sizes.elevationMargin
+ anchors.bottomMargin: Appearance.sizes.hyprlandGapsOut
+ color: Appearance.colors.colLayer0
+ radius: Appearance.rounding.large
+ }
+
+ RowLayout {
+ id: dockRow
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: 3
+ property real padding: 5
+
+ VerticalButtonGroup {
+ Layout.topMargin: Appearance.sizes.hyprlandGapsOut // why does this work
+ GroupButton { // Pin button
+ baseWidth: 35
+ baseHeight: 35
+ clickedWidth: baseWidth
+ clickedHeight: baseHeight + 20
+ buttonRadius: Appearance.rounding.normal
+ toggled: root.pinned
+ onClicked: root.pinned = !root.pinned
+ contentItem: MaterialSymbol {
+ text: "keep"
+ horizontalAlignment: Text.AlignHCenter
+ iconSize: Appearance.font.pixelSize.larger
+ color: root.pinned ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer0
+ }
+ }
+ }
+ DockSeparator {}
+ DockApps { id: dockApps; }
+ DockSeparator {}
+ DockButton {
+ Layout.fillHeight: true
+ onClicked: Hyprland.dispatch("global quickshell:overviewToggle")
+ contentItem: MaterialSymbol {
+ anchors.fill: parent
+ horizontalAlignment: Text.AlignHCenter
+ font.pixelSize: parent.width / 2
+ text: "apps"
+ color: Appearance.colors.colOnLayer0
+ }
+ }
+ }
+ }
+ }
+
+ }
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/modules/dock/DockAppButton.qml b/.config/quickshell/modules/dock/DockAppButton.qml
new file mode 100644
index 000000000..f4623e625
--- /dev/null
+++ b/.config/quickshell/modules/dock/DockAppButton.qml
@@ -0,0 +1,115 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+DockButton {
+ id: root
+ property var appToplevel
+ property var appListRoot
+ property int lastFocused: -1
+ property real iconSize: 35
+ property real countDotWidth: 10
+ property real countDotHeight: 4
+ property bool appIsActive: appToplevel.toplevels.find(t => (t.activated == true)) !== undefined
+
+ property bool isSeparator: appToplevel.appId === "SEPARATOR"
+ property var desktopEntry: DesktopEntries.byId(appToplevel.appId)
+ enabled: !isSeparator
+ implicitWidth: isSeparator ? 1 : implicitHeight - topInset - bottomInset
+
+ Loader {
+ active: isSeparator
+ anchors {
+ fill: parent
+ topMargin: dockVisualBackground.margin + dockRow.padding + Appearance.rounding.normal
+ bottomMargin: dockVisualBackground.margin + dockRow.padding + Appearance.rounding.normal
+ }
+ sourceComponent: DockSeparator {}
+ }
+
+ Loader {
+ anchors.fill: parent
+ active: appToplevel.toplevels.length > 0
+ sourceComponent: MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ acceptedButtons: Qt.NoButton
+ onEntered: {
+ appListRoot.lastHoveredButton = root
+ appListRoot.buttonHovered = true
+ lastFocused = appToplevel.toplevels.length - 1
+ }
+ onExited: {
+ if (appListRoot.lastHoveredButton === root) {
+ appListRoot.buttonHovered = false
+ }
+ }
+ }
+ }
+
+ onClicked: {
+ if (appToplevel.toplevels.length === 0) {
+ root.desktopEntry?.execute();
+ return;
+ }
+ lastFocused = (lastFocused + 1) % appToplevel.toplevels.length
+ appToplevel.toplevels[lastFocused].activate()
+ }
+
+ middleClickAction: () => {
+ root.desktopEntry?.execute();
+ }
+
+ contentItem: Loader {
+ active: !isSeparator
+ sourceComponent: Item {
+ anchors.centerIn: parent
+
+ Loader {
+ id: iconImageLoader
+ anchors {
+ left: parent.left
+ right: parent.right
+ verticalCenter: parent.verticalCenter
+ }
+ active: !root.isSeparator
+ sourceComponent: IconImage {
+ source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel.appId), "image-missing")
+ implicitSize: root.iconSize
+ }
+ }
+
+ RowLayout {
+ spacing: 3
+ anchors {
+ top: iconImageLoader.bottom
+ topMargin: 2
+ horizontalCenter: parent.horizontalCenter
+ }
+ Repeater {
+ model: Math.min(appToplevel.toplevels.length, 3)
+ delegate: Rectangle {
+ required property int index
+ radius: Appearance.rounding.full
+ implicitWidth: (appToplevel.toplevels.length <= 3) ?
+ root.countDotWidth : root.countDotHeight // Circles when too many
+ implicitHeight: root.countDotHeight
+ color: appIsActive ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colOnLayer0, 0.4)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/modules/dock/DockApps.qml b/.config/quickshell/modules/dock/DockApps.qml
new file mode 100644
index 000000000..ffda024c7
--- /dev/null
+++ b/.config/quickshell/modules/dock/DockApps.qml
@@ -0,0 +1,262 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Item {
+ id: root
+ property real maxWindowPreviewHeight: 200
+ property real maxWindowPreviewWidth: 300
+ property real windowControlsHeight: 30
+
+ property Item lastHoveredButton
+ property bool buttonHovered: false
+ property bool requestDockShow: previewPopup.show
+
+ Layout.fillHeight: true
+ Layout.topMargin: Appearance.sizes.hyprlandGapsOut // why does this work
+ implicitWidth: listView.implicitWidth
+
+ StyledListView {
+ id: listView
+ spacing: 2
+ orientation: ListView.Horizontal
+ anchors {
+ top: parent.top
+ bottom: parent.bottom
+ }
+ implicitWidth: contentWidth
+
+ Behavior on implicitWidth {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+ model: ScriptModel {
+ objectProp: "appId"
+ values: {
+ var map = new Map();
+
+ // Pinned apps
+ const pinnedApps = ConfigOptions?.dock.pinnedApps ?? [];
+ for (const appId of pinnedApps) {
+ if (!map.has(appId.toLowerCase())) map.set(appId.toLowerCase(), ({
+ pinned: true,
+ toplevels: []
+ }));
+ }
+
+ // Separator
+ if (pinnedApps.length > 0) {
+ map.set("SEPARATOR", { pinned: false, toplevels: [] });
+ }
+
+ // Open windows
+ for (const toplevel of ToplevelManager.toplevels.values) {
+ if (!map.has(toplevel.appId.toLowerCase())) map.set(toplevel.appId.toLowerCase(), ({
+ pinned: false,
+ toplevels: []
+ }));
+ map.get(toplevel.appId.toLowerCase()).toplevels.push(toplevel);
+ }
+
+ var values = [];
+
+ for (const [key, value] of map) {
+ values.push({ appId: key, toplevels: value.toplevels, pinned: value.pinned });
+ }
+
+ return values;
+ }
+ }
+ delegate: DockAppButton {
+ required property var modelData
+ appToplevel: modelData
+ appListRoot: root
+ }
+ }
+
+ PopupWindow {
+ id: previewPopup
+ property var appTopLevel: root.lastHoveredButton?.appToplevel
+ property bool allPreviewsReady: false
+ Connections {
+ target: root
+ function onLastHoveredButtonChanged() {
+ previewPopup.allPreviewsReady = false; // Reset readiness when the hovered button changes
+ }
+ }
+ function updatePreviewReadiness() {
+ for(var i = 0; i < previewRowLayout.children.length; i++) {
+ const view = previewRowLayout.children[i];
+ if (view.hasContent === false) {
+ allPreviewsReady = false;
+ return;
+ }
+ }
+ allPreviewsReady = true;
+ }
+ property bool shouldShow: {
+ const hoverConditions = (popupMouseArea.containsMouse || root.buttonHovered)
+ return hoverConditions && allPreviewsReady;
+ }
+ property bool show: false
+
+ onShouldShowChanged: {
+ if (shouldShow) {
+ // show = true;
+ updateTimer.restart();
+ } else {
+ updateTimer.restart();
+ }
+ }
+ Timer {
+ id: updateTimer
+ interval: 100
+ onTriggered: {
+ previewPopup.show = previewPopup.shouldShow
+ }
+ }
+ anchor {
+ window: root.QsWindow.window
+ adjustment: PopupAdjustment.None
+ gravity: Edges.Top | Edges.Right
+ edges: Edges.Top | Edges.Left
+
+ }
+ visible: popupBackground.visible
+ color: "transparent"
+ implicitWidth: root.QsWindow.window?.width ?? 1
+ implicitHeight: popupMouseArea.implicitHeight + root.windowControlsHeight + Appearance.sizes.elevationMargin * 2
+
+ MouseArea {
+ id: popupMouseArea
+ anchors.bottom: parent.bottom
+ implicitWidth: popupBackground.implicitWidth + Appearance.sizes.elevationMargin * 2
+ implicitHeight: root.maxWindowPreviewHeight + root.windowControlsHeight + Appearance.sizes.elevationMargin * 2
+ hoverEnabled: true
+ x: {
+ const itemCenter = root.QsWindow?.mapFromItem(root.lastHoveredButton, root.lastHoveredButton?.width / 2, 0);
+ return itemCenter.x - width / 2
+ }
+ StyledRectangularShadow {
+ target: popupBackground
+ opacity: previewPopup.show ? 1 : 0
+ visible: opacity > 0
+ Behavior on opacity {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ }
+ Rectangle {
+ id: popupBackground
+ property real padding: 5
+ opacity: previewPopup.show ? 1 : 0
+ visible: opacity > 0
+ Behavior on opacity {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ clip: true
+ color: Appearance.colors.colSurfaceContainer
+ radius: Appearance.rounding.normal
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: Appearance.sizes.elevationMargin
+ anchors.horizontalCenter: parent.horizontalCenter
+ implicitHeight: previewRowLayout.implicitHeight + padding * 2
+ implicitWidth: previewRowLayout.implicitWidth + padding * 2
+ Behavior on implicitWidth {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Behavior on implicitHeight {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+ RowLayout {
+ id: previewRowLayout
+ anchors.centerIn: parent
+ Repeater {
+ model: ScriptModel {
+ values: previewPopup.appTopLevel?.toplevels ?? []
+ }
+ RippleButton {
+ id: windowButton
+ required property var modelData
+ padding: 0
+ middleClickAction: () => {
+ windowButton.modelData?.close();
+ }
+ onClicked: {
+ windowButton.modelData?.activate();
+ }
+ contentItem: ColumnLayout {
+ implicitWidth: screencopyView.implicitWidth
+ implicitHeight: screencopyView.implicitHeight
+
+ ButtonGroup {
+ contentWidth: parent.width - anchors.margins * 2
+ WrapperRectangle {
+ Layout.fillWidth: true
+ color: ColorUtils.transparentize(Appearance.colors.colSurfaceContainer)
+ radius: Appearance.rounding.small
+ margin: 5
+ StyledText {
+ Layout.fillWidth: true
+ font.pixelSize: Appearance.font.pixelSize.small
+ text: windowButton.modelData?.title
+ elide: Text.ElideRight
+ color: Appearance.m3colors.m3onSurface
+ }
+ }
+ GroupButton {
+ id: closeButton
+ colBackground: ColorUtils.transparentize(Appearance.colors.colSurfaceContainer)
+ baseWidth: windowControlsHeight
+ baseHeight: windowControlsHeight
+ buttonRadius: Appearance.rounding.full
+ contentItem: MaterialSymbol {
+ anchors.centerIn: parent
+ horizontalAlignment: Text.AlignHCenter
+ text: "close"
+ iconSize: Appearance.font.pixelSize.normal
+ color: Appearance.m3colors.m3onSurface
+ }
+ onClicked: {
+ windowButton.modelData?.close();
+ }
+ }
+ }
+ ScreencopyView {
+ id: screencopyView
+ captureSource: previewPopup ? windowButton.modelData : null
+ live: true
+ paintCursor: true
+ constraintSize: Qt.size(root.maxWindowPreviewWidth, root.maxWindowPreviewHeight)
+ onHasContentChanged: {
+ previewPopup.updatePreviewReadiness();
+ }
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: screencopyView.width
+ height: screencopyView.height
+ radius: Appearance.rounding.small
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/modules/dock/DockButton.qml b/.config/quickshell/modules/dock/DockButton.qml
new file mode 100644
index 000000000..577cbcdc7
--- /dev/null
+++ b/.config/quickshell/modules/dock/DockButton.qml
@@ -0,0 +1,16 @@
+import "root:/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+RippleButton {
+ Layout.fillHeight: true
+ Layout.topMargin: Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut
+ implicitWidth: implicitHeight - topInset - bottomInset
+ buttonRadius: Appearance.rounding.normal
+
+ topInset: Appearance.sizes.hyprlandGapsOut + dockRow.padding
+ bottomInset: Appearance.sizes.hyprlandGapsOut + dockRow.padding
+}
diff --git a/.config/quickshell/modules/dock/DockSeparator.qml b/.config/quickshell/modules/dock/DockSeparator.qml
new file mode 100644
index 000000000..abb45d1da
--- /dev/null
+++ b/.config/quickshell/modules/dock/DockSeparator.qml
@@ -0,0 +1,13 @@
+import "root:/"
+import "root:/modules/common"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Rectangle {
+ Layout.topMargin: Appearance.sizes.elevationMargin + dockRow.padding + Appearance.rounding.normal
+ Layout.bottomMargin: Appearance.sizes.hyprlandGapsOut + dockRow.padding + Appearance.rounding.normal
+ Layout.fillHeight: true
+ implicitWidth: 1
+ color: Appearance.colors.colOutlineVariant
+}
diff --git a/.config/quickshell/modules/mediaControls/MediaControls.qml b/.config/quickshell/modules/mediaControls/MediaControls.qml
new file mode 100644
index 000000000..4350658f6
--- /dev/null
+++ b/.config/quickshell/modules/mediaControls/MediaControls.qml
@@ -0,0 +1,189 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/file_utils.js" as FileUtils
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import Quickshell
+import Quickshell.Io
+import Quickshell.Services.Mpris
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Scope {
+ id: root
+ property bool visible: false
+ readonly property MprisPlayer activePlayer: MprisController.activePlayer
+ readonly property var realPlayers: Mpris.players.values.filter(player => isRealPlayer(player))
+ readonly property var meaningfulPlayers: filterDuplicatePlayers(realPlayers)
+ readonly property real osdWidth: Appearance.sizes.osdWidth
+ readonly property real widgetWidth: Appearance.sizes.mediaControlsWidth
+ readonly property real widgetHeight: Appearance.sizes.mediaControlsHeight
+ property real contentPadding: 13
+ property real popupRounding: Appearance.rounding.screenRounding - Appearance.sizes.elevationMargin + 1
+ property real artRounding: Appearance.rounding.verysmall
+ property list visualizerPoints: []
+
+ property bool hasPlasmaIntegration: false
+ function isRealPlayer(player) {
+ // return true
+ return (
+ // Remove unecessary native buses from browsers if there's plasma integration
+ !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.firefox')) &&
+ !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.chromium')) &&
+ // playerctld just copies other buses and we don't need duplicates
+ !player.dbusName?.startsWith('org.mpris.MediaPlayer2.playerctld') &&
+ // Non-instance mpd bus
+ !(player.dbusName?.endsWith('.mpd') && !player.dbusName.endsWith('MediaPlayer2.mpd'))
+ );
+ }
+ function filterDuplicatePlayers(players) {
+ let filtered = [];
+ let used = new Set();
+
+ for (let i = 0; i < players.length; ++i) {
+ if (used.has(i)) continue;
+ let p1 = players[i];
+ let group = [i];
+
+ // Find duplicates by trackTitle prefix
+ for (let j = i + 1; j < players.length; ++j) {
+ let p2 = players[j];
+ if (p1.trackTitle && p2.trackTitle &&
+ (p1.trackTitle.includes(p2.trackTitle) || p2.trackTitle.includes(p1.trackTitle))) {
+ group.push(j);
+ }
+ }
+
+ // Pick the one with non-empty trackArtUrl, or fallback to the first
+ let chosenIdx = group.find(idx => players[idx].trackArtUrl && players[idx].trackArtUrl.length > 0);
+ if (chosenIdx === undefined) chosenIdx = group[0];
+
+ filtered.push(players[chosenIdx]);
+ group.forEach(idx => used.add(idx));
+ }
+ return filtered;
+ }
+
+ Process {
+ id: cavaProc
+ running: mediaControlsLoader.active
+ onRunningChanged: {
+ if (!cavaProc.running) {
+ root.visualizerPoints = [];
+ }
+ }
+ command: ["cava", "-p", `${FileUtils.trimFileProtocol(Directories.config)}/quickshell/scripts/cava/raw_output_config.txt`]
+ stdout: SplitParser {
+ onRead: data => {
+ // Parse `;`-separated values into the visualizerPoints array
+ let points = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p));
+ root.visualizerPoints = points;
+ }
+ }
+ }
+
+ Loader {
+ id: mediaControlsLoader
+ active: false
+
+ sourceComponent: PanelWindow {
+ id: mediaControlsRoot
+ visible: true
+
+ exclusiveZone: 0
+ implicitWidth: (
+ (mediaControlsRoot.screen.width / 2) // Middle of screen
+ - (osdWidth / 2) // Dodge OSD
+ - (widgetWidth / 2) // Account for widget width
+ ) * 2
+ implicitHeight: playerColumnLayout.implicitHeight
+ color: "transparent"
+ WlrLayershell.namespace: "quickshell:mediaControls"
+
+ anchors {
+ top: !ConfigOptions.bar.bottom
+ bottom: ConfigOptions.bar.bottom
+ left: true
+ }
+ mask: Region {
+ item: playerColumnLayout
+ }
+
+ ColumnLayout {
+ id: playerColumnLayout
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ x: (mediaControlsRoot.screen.width / 2) // Middle of screen
+ - (osdWidth / 2) // Dodge OSD
+ - (widgetWidth) // Account for widget width
+ + (Appearance.sizes.elevationMargin) // It's fine for shadows to overlap
+ spacing: -Appearance.sizes.elevationMargin // Shadow overlap okay
+
+ Repeater {
+ model: ScriptModel {
+ values: root.meaningfulPlayers
+ }
+ delegate: PlayerControl {
+ required property MprisPlayer modelData
+ player: modelData
+ visualizerPoints: root.visualizerPoints
+ }
+ }
+ }
+ }
+ }
+
+ IpcHandler {
+ target: "mediaControls"
+
+ function toggle(): void {
+ mediaControlsLoader.active = !mediaControlsLoader.active;
+ if(mediaControlsLoader.active) Notifications.timeoutAll();
+ }
+
+ function close(): void {
+ mediaControlsLoader.active = false;
+ }
+
+ function open(): void {
+ mediaControlsLoader.active = true;
+ Notifications.timeoutAll();
+ }
+ }
+
+ GlobalShortcut {
+ name: "mediaControlsToggle"
+ description: qsTr("Toggles media controls on press")
+
+ onPressed: {
+ if (!mediaControlsLoader.active && Mpris.players.values.filter(player => isRealPlayer(player)).length === 0) {
+ return;
+ }
+ mediaControlsLoader.active = !mediaControlsLoader.active;
+ if(mediaControlsLoader.active) Notifications.timeoutAll();
+ }
+ }
+ GlobalShortcut {
+ name: "mediaControlsOpen"
+ description: qsTr("Opens media controls on press")
+
+ onPressed: {
+ mediaControlsLoader.active = true;
+ Notifications.timeoutAll();
+ }
+ }
+ GlobalShortcut {
+ name: "mediaControlsClose"
+ description: qsTr("Closes media controls on press")
+
+ onPressed: {
+ mediaControlsLoader.active = false;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/mediaControls/PlayerControl.qml b/.config/quickshell/modules/mediaControls/PlayerControl.qml
new file mode 100644
index 000000000..cce0788db
--- /dev/null
+++ b/.config/quickshell/modules/mediaControls/PlayerControl.qml
@@ -0,0 +1,303 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import "root:/modules/common/functions/file_utils.js" as FileUtils
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Effects
+import QtQuick.Layouts
+import QtQuick.Controls
+import Quickshell
+import Quickshell.Io
+import Quickshell.Services.Mpris
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Item { // Player instance
+ id: playerController
+ required property MprisPlayer player
+ property var artUrl: player?.trackArtUrl
+ property string artDownloadLocation: Directories.coverArt
+ property string artFileName: Qt.md5(artUrl) + ".jpg"
+ property string artFilePath: `${artDownloadLocation}/${artFileName}`
+ property color artDominantColor: colorQuantizer?.colors[0] || Appearance.m3colors.m3secondaryContainer
+ property bool downloaded: false
+ property list visualizerPoints: []
+ property real maxVisualizerValue: 1000 // Max value in the data points
+ property int visualizerSmoothing: 2 // Number of points to average for smoothing
+
+ implicitWidth: widgetWidth
+ implicitHeight: widgetHeight
+
+ component TrackChangeButton: RippleButton {
+ implicitWidth: 24
+ implicitHeight: 24
+
+ property var iconName
+ colBackground: ColorUtils.transparentize(blendedColors.colSecondaryContainer, 1)
+ colBackgroundHover: blendedColors.colSecondaryContainerHover
+ colRipple: blendedColors.colSecondaryContainerActive
+
+ contentItem: MaterialSymbol {
+ iconSize: Appearance.font.pixelSize.huge
+ fill: 1
+ horizontalAlignment: Text.AlignHCenter
+ color: blendedColors.colOnSecondaryContainer
+ text: iconName
+
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+ }
+
+ Timer { // Force update for prevision
+ running: playerController.player?.playbackState == MprisPlaybackState.Playing
+ interval: 1000
+ repeat: true
+ onTriggered: {
+ playerController.player.positionChanged()
+ }
+ }
+
+ onArtUrlChanged: {
+ if (playerController.artUrl.length == 0) {
+ playerController.artDominantColor = Appearance.m3colors.m3secondaryContainer
+ return;
+ }
+ // console.log("PlayerControl: Art URL changed to", playerController.artUrl)
+ // console.log("Download cmd:", coverArtDownloader.command.join(" "))
+ playerController.downloaded = false
+ coverArtDownloader.running = true
+ }
+
+ Process { // Cover art downloader
+ id: coverArtDownloader
+ property string targetFile: playerController.artUrl
+ command: [ "bash", "-c", `[ -f ${artFilePath} ] || curl -sSL '${targetFile}' -o '${artFilePath}'` ]
+ onExited: (exitCode, exitStatus) => {
+ playerController.downloaded = true
+ }
+ }
+
+ ColorQuantizer {
+ id: colorQuantizer
+ source: playerController.downloaded ? Qt.resolvedUrl(artFilePath) : ""
+ depth: 0 // 2^0 = 1 color
+ rescaleSize: 1 // Rescale to 1x1 pixel for faster processing
+ }
+
+ property bool backgroundIsDark: artDominantColor.hslLightness < 0.5
+ property QtObject blendedColors: QtObject {
+ property color colLayer0: ColorUtils.mix(Appearance.colors.colLayer0, artDominantColor, (backgroundIsDark && Appearance.m3colors.darkmode) ? 0.6 : 0.5)
+ property color colLayer1: ColorUtils.mix(Appearance.colors.colLayer1, artDominantColor, 0.5)
+ property color colOnLayer0: ColorUtils.mix(Appearance.colors.colOnLayer0, artDominantColor, 0.5)
+ property color colOnLayer1: ColorUtils.mix(Appearance.colors.colOnLayer1, artDominantColor, 0.5)
+ property color colSubtext: ColorUtils.mix(Appearance.colors.colOnLayer1, artDominantColor, 0.5)
+ property color colPrimary: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.colors.colPrimary, artDominantColor), artDominantColor, 0.5)
+ property color colPrimaryHover: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.colors.colPrimaryHover, artDominantColor), artDominantColor, 0.3)
+ property color colPrimaryActive: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.colors.colPrimaryActive, artDominantColor), artDominantColor, 0.3)
+ property color colSecondaryContainer: ColorUtils.mix(Appearance.m3colors.m3secondaryContainer, artDominantColor, 0.15)
+ property color colSecondaryContainerHover: ColorUtils.mix(Appearance.colors.colSecondaryContainerHover, artDominantColor, 0.3)
+ property color colSecondaryContainerActive: ColorUtils.mix(Appearance.colors.colSecondaryContainerActive, artDominantColor, 0.5)
+ property color colOnPrimary: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.m3colors.m3onPrimary, artDominantColor), artDominantColor, 0.5)
+ property color colOnSecondaryContainer: ColorUtils.mix(Appearance.m3colors.m3onSecondaryContainer, artDominantColor, 0.5)
+
+ }
+
+ StyledRectangularShadow {
+ target: background
+ }
+ Rectangle { // Background
+ id: background
+ anchors.fill: parent
+ anchors.margins: Appearance.sizes.elevationMargin
+ color: blendedColors.colLayer0
+ radius: root.popupRounding
+
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: background.width
+ height: background.height
+ radius: background.radius
+ }
+ }
+
+ Image {
+ id: blurredArt
+ anchors.fill: parent
+ source: playerController.downloaded ? Qt.resolvedUrl(artFilePath) : ""
+ sourceSize.width: background.width
+ sourceSize.height: background.height
+ fillMode: Image.PreserveAspectCrop
+ cache: false
+ antialiasing: true
+ asynchronous: true
+
+ layer.enabled: true
+ layer.effect: MultiEffect {
+ source: blurredArt
+ saturation: 0.2
+ blurEnabled: true
+ blurMax: 100
+ blur: 1
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: ColorUtils.transparentize(blendedColors.colLayer0, 0.25)
+ radius: root.popupRounding
+ }
+ }
+
+ WaveVisualizer {
+ id: visualizerCanvas
+ anchors.fill: parent
+ live: playerController.player?.isPlaying
+ points: playerController.visualizerPoints
+ maxVisualizerValue: playerController.maxVisualizerValue
+ smoothing: playerController.visualizerSmoothing
+ color: blendedColors.colPrimary
+ }
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: root.contentPadding
+ spacing: 15
+
+ Rectangle { // Art background
+ id: artBackground
+ Layout.fillHeight: true
+ implicitWidth: height
+ radius: root.artRounding
+ color: ColorUtils.transparentize(blendedColors.colLayer1, 0.5)
+
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: artBackground.width
+ height: artBackground.height
+ radius: artBackground.radius
+ }
+ }
+
+ Image { // Art image
+ id: mediaArt
+ property int size: parent.height
+ anchors.fill: parent
+
+ source: playerController.downloaded ? Qt.resolvedUrl(artFilePath) : ""
+ fillMode: Image.PreserveAspectCrop
+ cache: false
+ antialiasing: true
+ asynchronous: true
+
+ width: size
+ height: size
+ sourceSize.width: size
+ sourceSize.height: size
+ }
+ }
+
+ ColumnLayout { // Info & controls
+ Layout.fillHeight: true
+ spacing: 2
+
+ StyledText {
+ id: trackTitle
+ Layout.fillWidth: true
+ font.pixelSize: Appearance.font.pixelSize.large
+ color: blendedColors.colOnLayer0
+ elide: Text.ElideRight
+ text: StringUtils.cleanMusicTitle(playerController.player?.trackTitle) || "Untitled"
+ }
+ StyledText {
+ id: trackArtist
+ Layout.fillWidth: true
+ font.pixelSize: Appearance.font.pixelSize.smaller
+ color: blendedColors.colSubtext
+ elide: Text.ElideRight
+ text: playerController.player?.trackArtist
+ }
+ Item { Layout.fillHeight: true }
+ Item {
+ Layout.fillWidth: true
+ implicitHeight: trackTime.implicitHeight + sliderRow.implicitHeight
+
+ StyledText {
+ id: trackTime
+ anchors.bottom: sliderRow.top
+ anchors.bottomMargin: 5
+ anchors.left: parent.left
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: blendedColors.colSubtext
+ elide: Text.ElideRight
+ text: `${StringUtils.friendlyTimeForSeconds(playerController.player?.position)} / ${StringUtils.friendlyTimeForSeconds(playerController.player?.length)}`
+ }
+ RowLayout {
+ id: sliderRow
+ anchors {
+ bottom: parent.bottom
+ left: parent.left
+ right: parent.right
+ }
+ TrackChangeButton {
+ iconName: "skip_previous"
+ onClicked: playerController.player?.previous()
+ }
+ Item {
+ id: progressBarContainer
+ Layout.fillWidth: true
+ implicitHeight: progressBar.implicitHeight
+
+ StyledProgressBar {
+ id: progressBar
+ anchors.fill: parent
+ highlightColor: blendedColors.colPrimary
+ trackColor: blendedColors.colSecondaryContainer
+ value: playerController.player?.position / playerController.player?.length
+ sperm: playerController.player?.isPlaying
+ }
+ }
+ TrackChangeButton {
+ iconName: "skip_next"
+ onClicked: playerController.player?.next()
+ }
+ }
+
+ RippleButton {
+ id: playPauseButton
+ anchors.right: parent.right
+ anchors.bottom: sliderRow.top
+ anchors.bottomMargin: 5
+ property real size: 44
+ implicitWidth: size
+ implicitHeight: size
+ onClicked: playerController.player.togglePlaying();
+
+ buttonRadius: playerController.player?.isPlaying ? Appearance?.rounding.normal : size / 2
+ colBackground: playerController.player?.isPlaying ? blendedColors.colPrimary : blendedColors.colSecondaryContainer
+ colBackgroundHover: playerController.player?.isPlaying ? blendedColors.colPrimaryHover : blendedColors.colSecondaryContainerHover
+ colRipple: playerController.player?.isPlaying ? blendedColors.colPrimaryActive : blendedColors.colSecondaryContainerActive
+
+ contentItem: MaterialSymbol {
+ iconSize: Appearance.font.pixelSize.huge
+ fill: 1
+ horizontalAlignment: Text.AlignHCenter
+ color: playerController.player?.isPlaying ? blendedColors.colOnPrimary : blendedColors.colOnSecondaryContainer
+ text: playerController.player?.isPlaying ? "pause" : "play_arrow"
+
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/notificationPopup/NotificationPopup.qml b/.config/quickshell/modules/notificationPopup/NotificationPopup.qml
new file mode 100644
index 000000000..fb046343d
--- /dev/null
+++ b/.config/quickshell/modules/notificationPopup/NotificationPopup.qml
@@ -0,0 +1,47 @@
+import "root:/"
+import "root:/modules/common/"
+import "root:/modules/common/widgets"
+import "root:/services"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Scope {
+ id: notificationPopup
+
+ PanelWindow {
+ id: root
+ visible: (Notifications.popupList.length > 0)
+ screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? null
+
+ WlrLayershell.namespace: "quickshell:notificationPopup"
+ WlrLayershell.layer: WlrLayer.Overlay
+ exclusiveZone: 0
+
+ anchors {
+ top: true
+ right: true
+ bottom: true
+ }
+
+ mask: Region {
+ item: listview.contentItem
+ }
+
+ color: "transparent"
+ implicitWidth: Appearance.sizes.notificationPopupWidth
+
+ NotificationListView {
+ id: listview
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.topMargin: 5
+ implicitWidth: parent.width - Appearance.sizes.elevationMargin * 2
+ popup: true
+ }
+ }
+}
diff --git a/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayBrightness.qml b/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayBrightness.qml
new file mode 100644
index 000000000..765386bc8
--- /dev/null
+++ b/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayBrightness.qml
@@ -0,0 +1,152 @@
+import "root:/services/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+import Quickshell.Wayland
+
+Scope {
+ id: root
+ property bool showOsdValues: false
+ property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name)
+ property var brightnessMonitor: Brightness.getMonitorForScreen(focusedScreen)
+
+ function triggerOsd() {
+ showOsdValues = true
+ osdTimeout.restart()
+ }
+
+ Timer {
+ id: osdTimeout
+ interval: ConfigOptions.osd.timeout
+ repeat: false
+ running: false
+ onTriggered: {
+ showOsdValues = false
+ }
+ }
+
+ Connections {
+ target: Audio.sink?.audio ?? null
+ function onVolumeChanged() {
+ if (!Audio.ready) return
+ root.showOsdValues = false
+ }
+ }
+
+ Connections {
+ target: Brightness
+ function onBrightnessChanged() {
+ if (!root.brightnessMonitor.ready) return
+ root.triggerOsd()
+ }
+ }
+
+ Loader {
+ id: osdLoader
+ active: showOsdValues
+
+ sourceComponent: PanelWindow {
+ id: osdRoot
+
+ Connections {
+ target: root
+ function onFocusedScreenChanged() {
+ osdRoot.screen = root.focusedScreen
+ }
+ }
+
+ exclusionMode: ExclusionMode.Normal
+ WlrLayershell.namespace: "quickshell:onScreenDisplay"
+ WlrLayershell.layer: WlrLayer.Overlay
+ color: "transparent"
+
+ anchors {
+ top: !ConfigOptions.bar.bottom
+ bottom: ConfigOptions.bar.bottom
+ }
+ mask: Region {
+ item: osdValuesWrapper
+ }
+
+ implicitWidth: columnLayout.implicitWidth
+ implicitHeight: columnLayout.implicitHeight
+ visible: osdLoader.active
+
+ ColumnLayout {
+ id: columnLayout
+ anchors.horizontalCenter: parent.horizontalCenter
+ Item {
+ id: osdValuesWrapper
+ // Extra space for shadow
+ implicitHeight: osdValues.implicitHeight + Appearance.sizes.elevationMargin * 2
+ implicitWidth: osdValues.implicitWidth
+ clip: true
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ onEntered: root.showOsdValues = false
+ }
+
+ Behavior on implicitHeight {
+ NumberAnimation {
+ duration: Appearance.animation.menuDecel.duration
+ easing.type: Appearance.animation.menuDecel.type
+ }
+ }
+
+ OsdValueIndicator {
+ id: osdValues
+ anchors.fill: parent
+ anchors.margins: Appearance.sizes.elevationMargin
+ value: root.brightnessMonitor?.brightness ?? 50
+ icon: "light_mode"
+ rotateIcon: true
+ scaleIcon: true
+ name: qsTr("Brightness")
+ }
+ }
+ }
+
+ }
+ }
+
+ IpcHandler {
+ target: "osdBrightness"
+
+ function trigger() {
+ root.triggerOsd()
+ }
+
+ function hide() {
+ showOsdValues = false
+ }
+
+ function toggle() {
+ showOsdValues = !showOsdValues
+ }
+ }
+
+ GlobalShortcut {
+ name: "osdBrightnessTrigger"
+ description: qsTr("Triggers brightness OSD on press")
+
+ onPressed: {
+ root.triggerOsd()
+ }
+ }
+ GlobalShortcut {
+ name: "osdBrightnessHide"
+ description: qsTr("Hides brightness OSD on press")
+
+ onPressed: {
+ root.showOsdValues = false
+ }
+ }
+
+}
diff --git a/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayVolume.qml b/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayVolume.qml
new file mode 100644
index 000000000..5d23b4405
--- /dev/null
+++ b/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayVolume.qml
@@ -0,0 +1,203 @@
+import "root:/services/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Scope {
+ id: root
+ property bool showOsdValues: false
+ property string protectionMessage: ""
+ property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name)
+
+ function triggerOsd() {
+ showOsdValues = true
+ osdTimeout.restart()
+ }
+
+ Timer {
+ id: osdTimeout
+ interval: ConfigOptions.osd.timeout
+ repeat: false
+ running: false
+ onTriggered: {
+ root.showOsdValues = false
+ root.protectionMessage = ""
+ }
+ }
+
+ Connections {
+ target: Brightness
+ function onBrightnessChanged() {
+ showOsdValues = false
+ }
+ }
+
+ Connections { // Listen to volume changes
+ target: Audio.sink?.audio ?? null
+ function onVolumeChanged() {
+ if (!Audio.ready) return
+ root.triggerOsd()
+ }
+ function onMutedChanged() {
+ if (!Audio.ready) return
+ root.triggerOsd()
+ }
+ }
+
+ Connections { // Listen to protection triggers
+ target: Audio
+ function onSinkProtectionTriggered(reason) {
+ root.protectionMessage = reason;
+ root.triggerOsd()
+ }
+ }
+
+ Loader {
+ id: osdLoader
+ active: showOsdValues
+
+ sourceComponent: PanelWindow {
+ id: osdRoot
+
+ Connections {
+ target: root
+ function onFocusedScreenChanged() {
+ osdRoot.screen = root.focusedScreen
+ }
+ }
+
+ exclusionMode: ExclusionMode.Normal
+ WlrLayershell.namespace: "quickshell:onScreenDisplay"
+ WlrLayershell.layer: WlrLayer.Overlay
+ color: "transparent"
+
+ anchors {
+ top: !ConfigOptions.bar.bottom
+ bottom: ConfigOptions.bar.bottom
+ }
+ mask: Region {
+ item: osdValuesWrapper
+ }
+
+ implicitWidth: columnLayout.implicitWidth
+ implicitHeight: columnLayout.implicitHeight
+ visible: osdLoader.active
+
+ ColumnLayout {
+ id: columnLayout
+ anchors.horizontalCenter: parent.horizontalCenter
+ Item {
+ id: osdValuesWrapper
+ // Extra space for shadow
+ implicitHeight: contentColumnLayout.implicitHeight + Appearance.sizes.elevationMargin * 2
+ implicitWidth: contentColumnLayout.implicitWidth
+ clip: true
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ onEntered: root.showOsdValues = false
+ }
+
+ ColumnLayout {
+ id: contentColumnLayout
+ anchors {
+ top: parent.top
+ left: parent.left
+ right: parent.right
+ leftMargin: Appearance.sizes.elevationMargin
+ rightMargin: Appearance.sizes.elevationMargin
+ }
+ spacing: 0
+
+ OsdValueIndicator {
+ id: osdValues
+ Layout.fillWidth: true
+ value: Audio.sink?.audio.volume ?? 0
+ icon: Audio.sink?.audio.muted ? "volume_off" : "volume_up"
+ name: qsTr("Volume")
+ }
+
+ Item {
+ id: protectionMessageWrapper
+ implicitHeight: protectionMessageBackground.implicitHeight
+ implicitWidth: protectionMessageBackground.implicitWidth
+ Layout.alignment: Qt.AlignHCenter
+ opacity: root.protectionMessage !== "" ? 1 : 0
+
+ StyledRectangularShadow {
+ target: protectionMessageBackground
+ }
+ Rectangle {
+ id: protectionMessageBackground
+ anchors.centerIn: parent
+ color: Appearance.m3colors.m3error
+ property real padding: 10
+ implicitHeight: protectionMessageRowLayout.implicitHeight + padding * 2
+ implicitWidth: protectionMessageRowLayout.implicitWidth + padding * 2
+ radius: Appearance.rounding.normal
+
+ RowLayout {
+ id: protectionMessageRowLayout
+ anchors.centerIn: parent
+ MaterialSymbol {
+ id: protectionMessageIcon
+ text: "dangerous"
+ iconSize: Appearance.font.pixelSize.hugeass
+ color: Appearance.m3colors.m3onError
+ }
+ StyledText {
+ id: protectionMessageTextWidget
+ horizontalAlignment: Text.AlignHCenter
+ color: Appearance.m3colors.m3onError
+ wrapMode: Text.Wrap
+ text: root.protectionMessage
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ IpcHandler {
+ target: "osdVolume"
+
+ function trigger() {
+ root.triggerOsd()
+ }
+
+ function hide() {
+ showOsdValues = false
+ }
+
+ function toggle() {
+ showOsdValues = !showOsdValues
+ }
+ }
+ GlobalShortcut {
+ name: "osdVolumeTrigger"
+ description: qsTr("Triggers volume OSD on press")
+
+ onPressed: {
+ root.triggerOsd()
+ }
+ }
+ GlobalShortcut {
+ name: "osdVolumeHide"
+ description: qsTr("Hides volume OSD on press")
+
+ onPressed: {
+ root.showOsdValues = false
+ }
+ }
+
+}
diff --git a/.config/quickshell/modules/onScreenDisplay/OsdValueIndicator.qml b/.config/quickshell/modules/onScreenDisplay/OsdValueIndicator.qml
new file mode 100644
index 000000000..dfbf4a6b8
--- /dev/null
+++ b/.config/quickshell/modules/onScreenDisplay/OsdValueIndicator.qml
@@ -0,0 +1,101 @@
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Widgets
+// import Qt5Compat.GraphicalEffects
+
+Item {
+ id: root
+ required property real value
+ required property string icon
+ required property string name
+ property bool rotateIcon: false
+ property bool scaleIcon: false
+
+ property real valueIndicatorVerticalPadding: 9
+ property real valueIndicatorLeftPadding: 10
+ property real valueIndicatorRightPadding: 20 // An icon is circle ish, a column isn't, hence the extra padding
+
+ Layout.margins: Appearance.sizes.elevationMargin
+ implicitWidth: Appearance.sizes.osdWidth
+ implicitHeight: valueIndicator.implicitHeight
+
+ StyledRectangularShadow {
+ target: valueIndicator
+ }
+ WrapperRectangle {
+ id: valueIndicator
+ anchors.fill: parent
+ radius: Appearance.rounding.full
+ color: Appearance.colors.colLayer0
+ implicitWidth: valueRow.implicitWidth
+
+ RowLayout { // Icon on the left, stuff on the right
+ id: valueRow
+ Layout.margins: 10
+ anchors.fill: parent
+ spacing: 10
+
+ Item {
+ implicitWidth: 30
+ implicitHeight: 30
+ Layout.alignment: Qt.AlignVCenter
+ Layout.leftMargin: valueIndicatorLeftPadding
+ Layout.topMargin: valueIndicatorVerticalPadding
+ Layout.bottomMargin: valueIndicatorVerticalPadding
+ MaterialSymbol { // Icon
+ anchors.centerIn: parent
+ color: Appearance.colors.colOnLayer0
+ renderType: Text.QtRendering
+
+ text: root.icon
+ iconSize: 20 + 10 * (root.scaleIcon ? value : 1)
+ rotation: 180 * (root.rotateIcon ? value : 0)
+
+ Behavior on iconSize {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ Behavior on rotation {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+
+ }
+ }
+ ColumnLayout { // Stuff
+ Layout.alignment: Qt.AlignVCenter
+ Layout.rightMargin: valueIndicatorRightPadding
+ spacing: 5
+
+ RowLayout { // Name fill left, value on the right end
+ Layout.leftMargin: valueProgressBar.height / 2 // Align text with progressbar radius curve's left end
+ Layout.rightMargin: valueProgressBar.height / 2 // Align text with progressbar radius curve's left end
+
+ StyledText {
+ color: Appearance.colors.colOnLayer0
+ font.pixelSize: Appearance.font.pixelSize.small
+ Layout.fillWidth: true
+ text: root.name
+ }
+
+ StyledText {
+ color: Appearance.colors.colOnLayer0
+ font.pixelSize: Appearance.font.pixelSize.small
+ Layout.fillWidth: false
+ text: Math.round(root.value * 100)
+ }
+ }
+
+ StyledProgressBar {
+ id: valueProgressBar
+ Layout.fillWidth: true
+ value: root.value
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/onScreenKeyboard/OnScreenKeyboard.qml b/.config/quickshell/modules/onScreenKeyboard/OnScreenKeyboard.qml
new file mode 100644
index 000000000..e78f45b2e
--- /dev/null
+++ b/.config/quickshell/modules/onScreenKeyboard/OnScreenKeyboard.qml
@@ -0,0 +1,169 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Scope { // Scope
+ id: root
+ property bool pinned: ConfigOptions?.osk.pinnedOnStartup ?? false
+
+ component OskControlButton: GroupButton { // Pin button
+ baseWidth: 40
+ baseHeight: 40
+ clickedWidth: baseWidth
+ clickedHeight: baseHeight + 20
+ buttonRadius: Appearance.rounding.normal
+ }
+
+ Loader {
+ id: oskLoader
+ active: false
+ onActiveChanged: {
+ if (!oskLoader.active) {
+ Ydotool.releaseAllKeys();
+ }
+ }
+
+ sourceComponent: PanelWindow { // Window
+ id: oskRoot
+ visible: oskLoader.active
+
+ anchors {
+ bottom: true
+ left: true
+ right: true
+ }
+
+ function hide() {
+ oskLoader.active = false
+ }
+ exclusiveZone: root.pinned ? implicitHeight - Appearance.sizes.hyprlandGapsOut : 0
+ implicitWidth: oskBackground.width + Appearance.sizes.elevationMargin * 2
+ implicitHeight: oskBackground.height + Appearance.sizes.elevationMargin * 2
+ WlrLayershell.namespace: "quickshell:osk"
+ WlrLayershell.layer: WlrLayer.Overlay
+ // Hyprland 0.49: Focus is always exclusive and setting this breaks mouse focus grab
+ // WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
+ color: "transparent"
+
+ mask: Region {
+ item: oskBackground
+ }
+
+
+ // Background
+ StyledRectangularShadow {
+ target: oskBackground
+ }
+ Rectangle {
+ id: oskBackground
+ anchors.centerIn: parent
+ color: Appearance.colors.colLayer0
+ radius: Appearance.rounding.windowRounding
+ property real padding: 10
+ implicitWidth: oskRowLayout.implicitWidth + padding * 2
+ implicitHeight: oskRowLayout.implicitHeight + padding * 2
+
+ Keys.onPressed: (event) => { // Esc to close
+ if (event.key === Qt.Key_Escape) {
+ oskRoot.hide()
+ }
+ }
+
+ RowLayout {
+ id: oskRowLayout
+ anchors.centerIn: parent
+ spacing: 5
+ VerticalButtonGroup {
+ OskControlButton { // Pin button
+ toggled: root.pinned
+ onClicked: root.pinned = !root.pinned
+ contentItem: MaterialSymbol {
+ text: "keep"
+ horizontalAlignment: Text.AlignHCenter
+ iconSize: Appearance.font.pixelSize.larger
+ color: root.pinned ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer0
+ }
+ }
+ OskControlButton {
+ onClicked: () => {
+ oskRoot.hide()
+ }
+ contentItem: MaterialSymbol {
+ horizontalAlignment: Text.AlignHCenter
+ text: "keyboard_hide"
+ iconSize: Appearance.font.pixelSize.larger
+ }
+ }
+ }
+ Rectangle {
+ Layout.topMargin: 20
+ Layout.bottomMargin: 20
+ Layout.fillHeight: true
+ implicitWidth: 1
+ color: Appearance.colors.colOutlineVariant
+ }
+ OskContent {
+ id: oskContent
+ Layout.fillWidth: true
+ }
+ }
+ }
+
+ }
+ }
+
+ IpcHandler {
+ target: "osk"
+
+ function toggle(): void {
+ oskLoader.active = !oskLoader.active
+ }
+
+ function close(): void {
+ oskLoader.active = false
+ }
+
+ function open(): void {
+ oskLoader.active = true
+ }
+ }
+
+ GlobalShortcut {
+ name: "oskToggle"
+ description: qsTr("Toggles on screen keyboard on press")
+
+ onPressed: {
+ oskLoader.active = !oskLoader.active;
+ }
+ }
+
+ GlobalShortcut {
+ name: "oskOpen"
+ description: qsTr("Opens on screen keyboard on press")
+
+ onPressed: {
+ oskLoader.active = true;
+ }
+ }
+
+ GlobalShortcut {
+ name: "oskClose"
+ description: qsTr("Closes on screen keyboard on press")
+
+ onPressed: {
+ oskLoader.active = false;
+ }
+ }
+
+}
diff --git a/.config/quickshell/modules/onScreenKeyboard/OskContent.qml b/.config/quickshell/modules/onScreenKeyboard/OskContent.qml
new file mode 100644
index 000000000..06e954adc
--- /dev/null
+++ b/.config/quickshell/modules/onScreenKeyboard/OskContent.qml
@@ -0,0 +1,47 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "layouts.js" as Layouts
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Widgets
+import Quickshell.Hyprland
+
+Item {
+ id: root
+ property var activeLayoutName: ConfigOptions?.osk.layout ?? Layouts.defaultLayout
+ property var layouts: Layouts.byName
+ property var currentLayout: layouts[activeLayoutName]
+
+ implicitWidth: keyRows.implicitWidth
+ implicitHeight: keyRows.implicitHeight
+
+ ColumnLayout {
+ id: keyRows
+ anchors.fill: parent
+ spacing: 5
+
+ Repeater {
+ model: root.currentLayout.keys
+
+ delegate: RowLayout {
+ id: keyRow
+ required property var modelData
+ spacing: 5
+
+ Repeater {
+ model: modelData
+ // A normal key looks like this: {label: "a", labelShift: "A", shape: "normal", keycode: 30, type: "normal"}
+ delegate: OskKey {
+ required property var modelData
+ keyData: modelData
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/modules/onScreenKeyboard/OskKey.qml b/.config/quickshell/modules/onScreenKeyboard/OskKey.qml
new file mode 100644
index 000000000..1f28a9e57
--- /dev/null
+++ b/.config/quickshell/modules/onScreenKeyboard/OskKey.qml
@@ -0,0 +1,125 @@
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+
+RippleButton {
+ id: root
+ property var keyData
+ property string key: keyData.label
+ property string type: keyData.keytype
+ property var keycode: keyData.keycode
+ property string shape: keyData.shape
+ property bool isShift: Ydotool.shiftKeys.includes(keycode)
+ property bool isBackspace: (key.toLowerCase() == "backspace")
+ property bool isEnter: (key.toLowerCase() == "enter" || key.toLowerCase() == "return")
+ property real baseWidth: 45
+ property real baseHeight: 45
+ property var widthMultiplier: ({
+ "normal": 1,
+ "fn": 1,
+ "tab": 1.6,
+ "caps": 1.9,
+ "shift": 2.5,
+ "control": 1.3
+ })
+ property var heightMultiplier: ({
+ "normal": 1,
+ "fn": 0.7,
+ "tab": 1,
+ "caps": 1,
+ "shift": 1,
+ "control": 1
+ })
+ toggled: isShift ? Ydotool.shiftMode : false
+
+ enabled: shape != "empty"
+ colBackground: shape == "empty" ? ColorUtils.transparentize(Appearance.colors.colLayer1) : Appearance.colors.colLayer1
+ buttonRadius: Appearance.rounding.small
+ implicitWidth: baseWidth * widthMultiplier[shape] || baseWidth
+ implicitHeight: baseHeight * heightMultiplier[shape] || baseHeight
+ Layout.fillWidth: shape == "space" || shape == "expand"
+
+ Connections {
+ target: Ydotool
+ enabled: isShift
+ function onShiftModeChanged() {
+ if (Ydotool.shiftMode == 0) {
+ capsLockTimer.hasStarted = false;
+ }
+ }
+ }
+
+ Timer {
+ id: capsLockTimer
+ property bool hasStarted: false
+ property bool canCaps: false
+ interval: 300
+ function startWaiting() {
+ hasStarted = true;
+ canCaps = true;
+ start();
+ }
+ onTriggered: {
+ canCaps = false;
+ }
+ }
+
+ downAction: () => {
+ Ydotool.press(root.keycode);
+ if (isShift && Ydotool.shiftMode == 0) Ydotool.shiftMode = 1;
+ }
+ releaseAction: () => {
+ if (root.type == "normal") {
+ Ydotool.release(root.keycode);
+ if (Ydotool.shiftMode == 1) {
+ Ydotool.releaseShiftKeys()
+ }
+ } else if (isShift) {
+ if (Ydotool.shiftMode == 1) {
+ if (!capsLockTimer.hasStarted) {
+ capsLockTimer.startWaiting();
+ } else {
+ if (capsLockTimer.canCaps) {
+ Ydotool.shiftMode = 2; // Caps lock mode
+ } else {
+ Ydotool.releaseShiftKeys()
+ }
+ }
+ } else if (Ydotool.shiftMode == 2) {
+ Ydotool.releaseShiftKeys();
+ }
+ } else if (root.type == "modkey") {
+ root.toggled = !root.toggled;
+ if (!root.toggled) {
+ if (isShift) {
+ Ydotool.releaseShiftKeys();
+ } else {
+ Ydotool.release(root.keycode);
+ }
+ }
+ }
+
+ }
+
+ contentItem: StyledText {
+ id: keyText
+ anchors.fill: parent
+ font.family: (isBackspace || isEnter) ? Appearance.font.family.iconMaterial : Appearance.font.family.main
+ font.pixelSize: root.shape == "fn" ? Appearance.font.pixelSize.small :
+ (isBackspace || isEnter) ? Appearance.font.pixelSize.huge :
+ Appearance.font.pixelSize.large
+ horizontalAlignment: Text.AlignHCenter
+ color: root.toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
+ text: root.isBackspace ? "backspace" : root.isEnter ? "subdirectory_arrow_left" :
+ Ydotool.shiftMode == 2 ? (root.keyData.labelCaps || root.keyData.labelShift || root.keyData.label) :
+ Ydotool.shiftMode == 1 ? (root.keyData.labelShift || root.keyData.label) :
+ root.keyData.label
+ }
+}
diff --git a/.config/ags/modules/onscreenkeyboard/data_keyboardlayouts.js b/.config/quickshell/modules/onScreenKeyboard/layouts.js
similarity index 98%
rename from .config/ags/modules/onscreenkeyboard/data_keyboardlayouts.js
rename to .config/quickshell/modules/onScreenKeyboard/layouts.js
index 1a67b7a1f..6b8b98f06 100644
--- a/.config/ags/modules/onscreenkeyboard/data_keyboardlayouts.js
+++ b/.config/quickshell/modules/onScreenKeyboard/layouts.js
@@ -1,9 +1,9 @@
// 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: {
+const defaultLayout = "qwerty_full";
+const byName = {
+ "qwerty_full": {
name: "QWERTY - Full",
name_short: "US",
comment: "Like physical keyboard",
@@ -88,7 +88,7 @@ export const oskLayouts = {
{ keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
],
[
- { keytype: "modkey", label: "Shift", labelShift: "Shift ⇧", labelCaps: "Locked ⇩", shape: "shift", keycode: 42 },
+ { keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", 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 },
@@ -99,7 +99,7 @@ export const oskLayouts = {
{ 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: "Shift", labelShift: "Shift", labelCaps: "Caps", shape: "expand", keycode: 54 } // optional
],
[
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 29 },
@@ -113,7 +113,7 @@ export const oskLayouts = {
]
]
},
- qwertz_full: {
+ "qwertz_full": {
name: "QWERTZ - Full",
name_short: "DE",
comment: "Keyboard layout commonly used in German-speaking countries",
diff --git a/.config/quickshell/modules/overview/Overview.qml b/.config/quickshell/modules/overview/Overview.qml
new file mode 100644
index 000000000..a7817e6e9
--- /dev/null
+++ b/.config/quickshell/modules/overview/Overview.qml
@@ -0,0 +1,240 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Scope {
+ id: overviewScope
+ property bool dontAutoCancelSearch: false
+ Variants {
+ id: overviewVariants
+ model: Quickshell.screens
+ PanelWindow {
+ id: root
+ required property var modelData
+ property string searchingText: ""
+ readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
+ property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor.id)
+ screen: modelData
+ visible: GlobalStates.overviewOpen
+
+ WlrLayershell.namespace: "quickshell:overview"
+ WlrLayershell.layer: WlrLayer.Overlay
+ // WlrLayershell.keyboardFocus: GlobalStates.overviewOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
+ color: "transparent"
+
+ mask: Region {
+ item: GlobalStates.overviewOpen ? columnLayout : null
+ }
+ HyprlandWindow.visibleMask: Region {
+ item: GlobalStates.overviewOpen ? columnLayout : null
+ }
+
+
+ anchors {
+ top: true
+ left: true
+ right: true
+ bottom: true
+ }
+
+ HyprlandFocusGrab {
+ id: grab
+ windows: [ root ]
+ property bool canBeActive: root.monitorIsFocused
+ active: false
+ onCleared: () => {
+ if (!active) GlobalStates.overviewOpen = false
+ }
+ }
+
+ Connections {
+ target: GlobalStates
+ function onOverviewOpenChanged() {
+ if (!GlobalStates.overviewOpen) {
+ searchWidget.disableExpandAnimation()
+ overviewScope.dontAutoCancelSearch = false;
+ } else {
+ if (!overviewScope.dontAutoCancelSearch) {
+ searchWidget.cancelSearch()
+ }
+ delayedGrabTimer.start()
+ }
+ }
+ }
+
+ Timer {
+ id: delayedGrabTimer
+ interval: ConfigOptions.hacks.arbitraryRaceConditionDelay
+ repeat: false
+ onTriggered: {
+ if (!grab.canBeActive) return
+ grab.active = GlobalStates.overviewOpen
+ }
+ }
+
+ implicitWidth: columnLayout.implicitWidth
+ implicitHeight: columnLayout.implicitHeight
+
+ function setSearchingText(text) {
+ searchWidget.setSearchingText(text);
+ }
+
+ ColumnLayout {
+ id: columnLayout
+ visible: GlobalStates.overviewOpen
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ top: !ConfigOptions.bar.bottom ? parent.top : undefined
+ bottom: ConfigOptions.bar.bottom ? parent.bottom : undefined
+ }
+
+ Keys.onPressed: (event) => {
+ if (event.key === Qt.Key_Escape) {
+ GlobalStates.overviewOpen = false;
+ } else if (event.key === Qt.Key_Left) {
+ if (!root.searchingText) Hyprland.dispatch("workspace r-1");
+ } else if (event.key === Qt.Key_Right) {
+ if (!root.searchingText) Hyprland.dispatch("workspace r+1");
+ }
+ }
+
+ Item {
+ height: 1 // Prevent Wayland protocol error
+ width: 1 // Prevent Wayland protocol error
+ }
+
+ SearchWidget {
+ id: searchWidget
+ Layout.alignment: Qt.AlignHCenter
+ onSearchingTextChanged: (text) => {
+ root.searchingText = searchingText
+ }
+ }
+
+ Loader {
+ id: overviewLoader
+ active: GlobalStates.overviewOpen
+ sourceComponent: OverviewWidget {
+ panelWindow: root
+ visible: (root.searchingText == "")
+ }
+ }
+ }
+
+ }
+ }
+
+ IpcHandler {
+ target: "overview"
+
+ function toggle() {
+ GlobalStates.overviewOpen = !GlobalStates.overviewOpen
+ }
+ function close() {
+ GlobalStates.overviewOpen = false
+ }
+ function open() {
+ GlobalStates.overviewOpen = true
+ }
+ function toggleReleaseInterrupt() {
+ GlobalStates.superReleaseMightTrigger = false
+ }
+ }
+
+ GlobalShortcut {
+ name: "overviewToggle"
+ description: qsTr("Toggles overview on press")
+
+ onPressed: {
+ GlobalStates.overviewOpen = !GlobalStates.overviewOpen
+ }
+ }
+ GlobalShortcut {
+ name: "overviewClose"
+ description: qsTr("Closes overview")
+
+ onPressed: {
+ GlobalStates.overviewOpen = false
+ }
+ }
+ GlobalShortcut {
+ name: "overviewToggleRelease"
+ description: qsTr("Toggles overview on release")
+
+ onPressed: {
+ GlobalStates.superReleaseMightTrigger = true
+ }
+
+ onReleased: {
+ if (!GlobalStates.superReleaseMightTrigger) {
+ GlobalStates.superReleaseMightTrigger = true
+ return
+ }
+ GlobalStates.overviewOpen = !GlobalStates.overviewOpen
+ }
+ }
+ GlobalShortcut {
+ name: "overviewToggleReleaseInterrupt"
+ description: qsTr("Interrupts possibility of overview being toggled on release. ") +
+ qsTr("This is necessary because GlobalShortcut.onReleased in quickshell triggers whether or not you press something else while holding the key. ") +
+ qsTr("To make sure this works consistently, use binditn = MODKEYS, catchall in an automatically triggered submap that includes everything.")
+
+ onPressed: {
+ GlobalStates.superReleaseMightTrigger = false
+ }
+ }
+ GlobalShortcut {
+ name: "overviewClipboardToggle"
+ description: qsTr("Toggle clipboard query on overview widget")
+
+ onPressed: {
+ if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) {
+ GlobalStates.overviewOpen = false;
+ return;
+ }
+ for (let i = 0; i < overviewVariants.instances.length; i++) {
+ let panelWindow = overviewVariants.instances[i];
+ if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
+ overviewScope.dontAutoCancelSearch = true;
+ panelWindow.setSearchingText(
+ ConfigOptions.search.prefix.clipboard
+ );
+ GlobalStates.overviewOpen = true;
+ return
+ }
+ }
+ }
+ }
+
+ GlobalShortcut {
+ name: "overviewEmojiToggle"
+ description: qsTr("Toggle emoji query on overview widget")
+
+ onPressed: {
+ if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) {
+ GlobalStates.overviewOpen = false;
+ return;
+ }
+ for (let i = 0; i < overviewVariants.instances.length; i++) {
+ let panelWindow = overviewVariants.instances[i];
+ if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
+ overviewScope.dontAutoCancelSearch = true;
+ panelWindow.setSearchingText(
+ ConfigOptions.search.prefix.emojis
+ );
+ GlobalStates.overviewOpen = true;
+ return
+ }
+ }
+ }
+ }
+
+}
diff --git a/.config/quickshell/modules/overview/OverviewWidget.qml b/.config/quickshell/modules/overview/OverviewWidget.qml
new file mode 100644
index 000000000..e0999d6e3
--- /dev/null
+++ b/.config/quickshell/modules/overview/OverviewWidget.qml
@@ -0,0 +1,269 @@
+import "root:/"
+import "root:/services/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Item {
+ id: root
+ required property var panelWindow
+ readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
+ readonly property var toplevels: ToplevelManager.toplevels
+ readonly property int workspacesShown: ConfigOptions.overview.numOfRows * ConfigOptions.overview.numOfCols
+ readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / workspacesShown)
+ property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor.id)
+ property var windows: HyprlandData.windowList
+ property var windowByAddress: HyprlandData.windowByAddress
+ property var windowAddresses: HyprlandData.addresses
+ property var monitorData: HyprlandData.monitors.find(m => m.id === root.monitor.id)
+ property real scale: ConfigOptions.overview.scale
+ property color activeBorderColor: Appearance.colors.colSecondary
+
+ property real workspaceImplicitWidth: (monitorData?.transform % 2 === 1) ?
+ ((monitor.height - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale / monitor.scale) :
+ ((monitor.width - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale / monitor.scale)
+ property real workspaceImplicitHeight: (monitorData?.transform % 2 === 1) ?
+ ((monitor.width - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale / monitor.scale) :
+ ((monitor.height - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale / monitor.scale)
+
+ property real workspaceNumberMargin: 80
+ property real workspaceNumberSize: Math.min(workspaceImplicitHeight, workspaceImplicitWidth) * monitor.scale
+ property int workspaceZ: 0
+ property int windowZ: 1
+ property int windowDraggingZ: 99999
+ property real workspaceSpacing: 5
+
+ property int draggingFromWorkspace: -1
+ property int draggingTargetWorkspace: -1
+
+ implicitWidth: overviewBackground.implicitWidth + Appearance.sizes.elevationMargin * 2
+ implicitHeight: overviewBackground.implicitHeight + Appearance.sizes.elevationMargin * 2
+
+ property Component windowComponent: OverviewWindow {}
+ property list windowWidgets: []
+
+ StyledRectangularShadow {
+ target: overviewBackground
+ }
+ Rectangle { // Background
+ id: overviewBackground
+ property real padding: 10
+ anchors.fill: parent
+ anchors.margins: Appearance.sizes.elevationMargin
+
+ implicitWidth: workspaceColumnLayout.implicitWidth + padding * 2
+ implicitHeight: workspaceColumnLayout.implicitHeight + padding * 2
+ radius: Appearance.rounding.screenRounding * root.scale + padding
+ color: Appearance.colors.colLayer0
+
+ ColumnLayout { // Workspaces
+ id: workspaceColumnLayout
+
+ z: root.workspaceZ
+ anchors.centerIn: parent
+ spacing: workspaceSpacing
+ Repeater {
+ model: ConfigOptions.overview.numOfRows
+ delegate: RowLayout {
+ id: row
+ property int rowIndex: index
+ spacing: workspaceSpacing
+
+ Repeater { // Workspace repeater
+ model: ConfigOptions.overview.numOfCols
+ Rectangle { // Workspace
+ id: workspace
+ property int colIndex: index
+ property int workspaceValue: root.workspaceGroup * workspacesShown + rowIndex * ConfigOptions.overview.numOfCols + colIndex + 1
+ property color defaultWorkspaceColor: Appearance.colors.colLayer1 // TODO: reconsider this color for a cleaner look
+ property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1)
+ property color hoveredBorderColor: Appearance.colors.colLayer2Hover
+ property bool hoveredWhileDragging: false
+
+ implicitWidth: root.workspaceImplicitWidth
+ implicitHeight: root.workspaceImplicitHeight
+ color: hoveredWhileDragging ? hoveredWorkspaceColor : defaultWorkspaceColor
+ radius: Appearance.rounding.screenRounding * root.scale
+ border.width: 2
+ border.color: hoveredWhileDragging ? hoveredBorderColor : "transparent"
+
+ StyledText {
+ anchors.centerIn: parent
+ text: workspaceValue
+ font.pixelSize: root.workspaceNumberSize * root.scale
+ font.weight: Font.DemiBold
+ color: ColorUtils.transparentize(Appearance.colors.colOnLayer1, 0.8)
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ MouseArea {
+ id: workspaceArea
+ anchors.fill: parent
+ acceptedButtons: Qt.LeftButton
+ onClicked: {
+ if (root.draggingTargetWorkspace === -1) {
+ // Hyprland.dispatch(`exec qs ipc call overview close`)
+ GlobalStates.overviewOpen = false
+ Hyprland.dispatch(`workspace ${workspaceValue}`)
+ }
+ }
+ }
+
+ DropArea {
+ anchors.fill: parent
+ onEntered: {
+ root.draggingTargetWorkspace = workspaceValue
+ if (root.draggingFromWorkspace == root.draggingTargetWorkspace) return;
+ hoveredWhileDragging = true
+ }
+ onExited: {
+ hoveredWhileDragging = false
+ if (root.draggingTargetWorkspace == workspaceValue) root.draggingTargetWorkspace = -1
+ }
+ }
+
+ }
+ }
+ }
+ }
+ }
+
+ Item { // Windows & focused workspace indicator
+ id: windowSpace
+ anchors.centerIn: parent
+ implicitWidth: workspaceColumnLayout.implicitWidth
+ implicitHeight: workspaceColumnLayout.implicitHeight
+
+ Repeater { // Window repeater
+ model: ScriptModel {
+ values: {
+ // console.log(JSON.stringify(ToplevelManager.toplevels.values.map(t => t), null, 2))
+ return ToplevelManager.toplevels.values.filter((toplevel) => {
+ const address = `0x${toplevel.HyprlandToplevel.address}`
+ // console.log(`Checking window with address: ${address}`)
+ var win = windowByAddress[address]
+ return (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown)
+ })
+ }
+ }
+ delegate: OverviewWindow {
+ id: window
+ required property var modelData
+ property var address: `0x${modelData.HyprlandToplevel.address}`
+ windowData: windowByAddress[address]
+ toplevel: modelData
+ monitorData: root.monitorData
+ scale: root.scale
+ availableWorkspaceWidth: root.workspaceImplicitWidth
+ availableWorkspaceHeight: root.workspaceImplicitHeight
+
+ property int monitorId: windowData?.monitor
+ property var monitor: HyprlandData.monitors[monitorId]
+
+ property bool atInitPosition: (initX == x && initY == y)
+ restrictToWorkspace: Drag.active || atInitPosition
+
+ property int workspaceColIndex: (windowData?.workspace.id - 1) % ConfigOptions.overview.numOfCols
+ property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / ConfigOptions.overview.numOfCols)
+ xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex - (monitor?.x * root.scale)
+ yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex - (monitor?.y * root.scale)
+
+ Timer {
+ id: updateWindowPosition
+ interval: ConfigOptions.hacks.arbitraryRaceConditionDelay
+ repeat: false
+ running: false
+ onTriggered: {
+ window.x = Math.round(Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0) + xOffset)
+ window.y = Math.round(Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0) + yOffset)
+ // console.log(`[OverviewWindow] Updated position for window ${windowData?.address} to (${window.x}, ${window.y})`)
+ }
+ }
+
+ z: atInitPosition ? root.windowZ : root.windowDraggingZ
+ Drag.hotSpot.x: targetWindowWidth / 2
+ Drag.hotSpot.y: targetWindowHeight / 2
+ MouseArea {
+ id: dragArea
+ anchors.fill: parent
+ hoverEnabled: true
+ onEntered: hovered = true // For hover color change
+ onExited: hovered = false // For hover color change
+ acceptedButtons: Qt.LeftButton | Qt.MiddleButton
+ drag.target: parent
+ onPressed: {
+ root.draggingFromWorkspace = windowData?.workspace.id
+ window.pressed = true
+ window.Drag.active = true
+ window.Drag.source = window
+ // console.log(`[OverviewWindow] Dragging window ${windowData?.address} from position (${window.x}, ${window.y})`)
+ }
+ onReleased: {
+ const targetWorkspace = root.draggingTargetWorkspace
+ window.pressed = false
+ window.Drag.active = false
+ root.draggingFromWorkspace = -1
+ if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) {
+ Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${window.windowData?.address}`)
+ updateWindowPosition.restart()
+ }
+ else {
+ window.x = window.initX
+ window.y = window.initY
+ }
+ }
+ onClicked: (event) => {
+ if (!windowData) return;
+
+ if (event.button === Qt.LeftButton) {
+ GlobalStates.overviewOpen = false
+ Hyprland.dispatch(`focuswindow address:${windowData.address}`)
+ event.accepted = true
+ } else if (event.button === Qt.MiddleButton) {
+ Hyprland.dispatch(`closewindow address:${windowData.address}`)
+ event.accepted = true
+ }
+ }
+
+ StyledToolTip {
+ extraVisibleCondition: false
+ alternativeVisibleCondition: dragArea.containsMouse && !window.Drag.active
+ content: `${windowData.title}\n[${windowData.class}] ${windowData.xwayland ? "[XWayland] " : ""}\n`
+ }
+ }
+ }
+ }
+
+ Rectangle { // Focused workspace indicator
+ id: focusedWorkspaceIndicator
+ property int activeWorkspaceInGroup: monitor.activeWorkspace?.id - (root.workspaceGroup * root.workspacesShown)
+ property int activeWorkspaceRowIndex: Math.floor((activeWorkspaceInGroup - 1) / ConfigOptions.overview.numOfCols)
+ property int activeWorkspaceColIndex: (activeWorkspaceInGroup - 1) % ConfigOptions.overview.numOfCols
+ x: (root.workspaceImplicitWidth + workspaceSpacing) * activeWorkspaceColIndex
+ y: (root.workspaceImplicitHeight + workspaceSpacing) * activeWorkspaceRowIndex
+ z: root.windowZ
+ width: root.workspaceImplicitWidth
+ height: root.workspaceImplicitHeight
+ color: "transparent"
+ radius: Appearance.rounding.screenRounding * root.scale
+ border.width: 2
+ border.color: root.activeBorderColor
+ Behavior on x {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ Behavior on y {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/modules/overview/OverviewWindow.qml b/.config/quickshell/modules/overview/OverviewWindow.qml
new file mode 100644
index 000000000..3b376988b
--- /dev/null
+++ b/.config/quickshell/modules/overview/OverviewWindow.qml
@@ -0,0 +1,111 @@
+import "root:/"
+import "root:/services/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Io
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Item { // Window
+ id: root
+ property var toplevel
+ property var windowData
+ property var monitorData
+ property var scale
+ property var availableWorkspaceWidth
+ property var availableWorkspaceHeight
+ property bool restrictToWorkspace: true
+ property real initX: Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0) + xOffset
+ property real initY: Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0) + yOffset
+ property real xOffset: 0
+ property real yOffset: 0
+
+ property var targetWindowWidth: windowData?.size[0] * scale
+ property var targetWindowHeight: windowData?.size[1] * scale
+ property bool hovered: false
+ property bool pressed: false
+
+ property var iconToWindowRatio: 0.35
+ property var xwaylandIndicatorToIconRatio: 0.35
+ property var iconToWindowRatioCompact: 0.6
+ property var iconPath: Quickshell.iconPath(AppSearch.guessIcon(windowData?.class), "image-missing")
+ property bool compactMode: Appearance.font.pixelSize.smaller * 4 > targetWindowHeight || Appearance.font.pixelSize.smaller * 4 > targetWindowWidth
+
+ property bool indicateXWayland: (ConfigOptions.overview.showXwaylandIndicator && windowData?.xwayland) ?? false
+
+ x: initX
+ y: initY
+ width: Math.round(Math.min(windowData?.size[0] * root.scale, (restrictToWorkspace ? windowData?.size[0] : availableWorkspaceWidth - x + xOffset)))
+ height: Math.round(Math.min(windowData?.size[1] * root.scale, (restrictToWorkspace ? windowData?.size[1] : availableWorkspaceHeight - y + yOffset)))
+
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: root.width
+ height: root.height
+ radius: Appearance.rounding.windowRounding * root.scale
+ }
+ }
+
+ Behavior on x {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ Behavior on y {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ Behavior on width {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ Behavior on height {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+
+ ScreencopyView {
+ id: windowPreview
+ anchors.fill: parent
+ captureSource: GlobalStates.overviewOpen ? root.toplevel : null
+ live: true
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Appearance.rounding.windowRounding * root.scale
+ color: pressed ? ColorUtils.transparentize(Appearance.colors.colLayer2Active, 0.5) :
+ hovered ? ColorUtils.transparentize(Appearance.colors.colLayer2Hover, 0.7) :
+ ColorUtils.transparentize(Appearance.colors.colLayer2)
+ border.color : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.7)
+ border.width : 1
+ }
+
+ ColumnLayout {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.right: parent.right
+ spacing: Appearance.font.pixelSize.smaller * 0.5
+
+ Image {
+ id: windowIcon
+ property var iconSize: Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio)
+ // mipmap: true
+ Layout.alignment: Qt.AlignHCenter
+ source: root.iconPath
+ width: iconSize
+ height: iconSize
+ sourceSize: Qt.size(iconSize, iconSize)
+
+ Behavior on width {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ Behavior on height {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/overview/SearchItem.qml b/.config/quickshell/modules/overview/SearchItem.qml
new file mode 100644
index 000000000..d23cb4c09
--- /dev/null
+++ b/.config/quickshell/modules/overview/SearchItem.qml
@@ -0,0 +1,228 @@
+// pragma NativeMethodBehavior: AcceptThisObject
+import "root:/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Widgets
+import Quickshell.Hyprland
+
+RippleButton {
+ id: root
+ property var entry
+ property string query
+ property bool entryShown: entry?.shown ?? true
+ property string itemType: entry?.type
+ property string itemName: entry?.name
+ property string itemIcon: entry?.icon ?? ""
+ property var itemExecute: entry?.execute
+ property string fontType: entry?.fontType ?? "main"
+ property string itemClickActionName: entry?.clickActionName
+ property string bigText: entry?.bigText ?? ""
+ property string materialSymbol: entry?.materialSymbol ?? ""
+ property string cliphistRawString: entry?.cliphistRawString ?? ""
+
+ property string highlightPrefix: ``
+ property string highlightSuffix: ` `
+ function highlightContent(content, query) {
+ if (!query || query.length === 0 || content == query || fontType === "monospace")
+ return StringUtils.escapeHtml(content);
+
+ let contentLower = content.toLowerCase();
+ let queryLower = query.toLowerCase();
+
+ let result = "";
+ let lastIndex = 0;
+ let qIndex = 0;
+
+ for (let i = 0; i < content.length && qIndex < query.length; i++) {
+ if (contentLower[i] === queryLower[qIndex]) {
+ // Add non-highlighted part (escaped)
+ if (i > lastIndex)
+ result += StringUtils.escapeHtml(content.slice(lastIndex, i));
+ // Add highlighted character (escaped)
+ result += root.highlightPrefix + StringUtils.escapeHtml(content[i]) + root.highlightSuffix;
+ lastIndex = i + 1;
+ qIndex++;
+ }
+ }
+ // Add the rest of the string (escaped)
+ if (lastIndex < content.length)
+ result += StringUtils.escapeHtml(content.slice(lastIndex));
+
+ return result;
+ }
+ property string displayContent: highlightContent(root.itemName, root.query)
+
+ property list urls: {
+ if (!root.itemName) return [];
+ // Regular expression to match URLs
+ const urlRegex = /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi;
+ const matches = root.itemName?.match(urlRegex)
+ ?.filter(url => !url.includes("…")) // Elided = invalid
+ return matches ? matches : [];
+ }
+
+ visible: root.entryShown
+ property int horizontalMargin: 10
+ property int buttonHorizontalPadding: 10
+ property int buttonVerticalPadding: 5
+ property bool keyboardDown: false
+
+ implicitHeight: rowLayout.implicitHeight + root.buttonVerticalPadding * 2
+ implicitWidth: rowLayout.implicitWidth + root.buttonHorizontalPadding * 2
+ buttonRadius: Appearance.rounding.normal
+ colBackground: (root.down || root.keyboardDown) ? Appearance.colors.colLayer1Active :
+ ((root.hovered || root.focus) ? Appearance.colors.colLayer1Hover :
+ ColorUtils.transparentize(Appearance.colors.colSurfaceContainerHigh, 1))
+ colBackgroundHover: Appearance.colors.colLayer1Hover
+ colRipple: Appearance.colors.colLayer1Active
+
+ background {
+ anchors.fill: root
+ anchors.leftMargin: root.horizontalMargin
+ anchors.rightMargin: root.horizontalMargin
+ }
+
+ PointingHandInteraction {}
+ onClicked: {
+ root.itemExecute()
+ Hyprland.dispatch("global quickshell:overviewClose")
+ }
+ Keys.onPressed: (event) => {
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ root.keyboardDown = true
+ root.clicked()
+ event.accepted = true;
+ }
+ }
+ Keys.onReleased: (event) => {
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ root.keyboardDown = false
+ event.accepted = true;
+ }
+ }
+
+ RowLayout {
+ id: rowLayout
+ spacing: iconLoader.sourceComponent === null ? 0 : 10
+ anchors.fill: parent
+ anchors.leftMargin: root.horizontalMargin + root.buttonHorizontalPadding
+ anchors.rightMargin: root.horizontalMargin + root.buttonHorizontalPadding
+
+ // Icon
+ Loader {
+ id: iconLoader
+ active: true
+ sourceComponent: root.materialSymbol !== "" ? materialSymbolComponent :
+ root.bigText ? bigTextComponent :
+ root.itemIcon !== "" ? iconImageComponent :
+ null
+ }
+
+ Component {
+ id: iconImageComponent
+ IconImage {
+ source: Quickshell.iconPath(root.itemIcon, "image-missing")
+ width: 35
+ height: 35
+ }
+ }
+
+ Component {
+ id: materialSymbolComponent
+ MaterialSymbol {
+ text: root.materialSymbol
+ iconSize: 30
+ color: Appearance.m3colors.m3onSurface
+ }
+ }
+
+ Component {
+ id: bigTextComponent
+ StyledText {
+ text: root.bigText
+ font.pixelSize: Appearance.font.pixelSize.larger
+ color: Appearance.m3colors.m3onSurface
+ }
+ }
+
+ // Main text
+ ColumnLayout {
+ id: contentColumn
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignVCenter
+ spacing: 0
+ StyledText {
+ font.pixelSize: Appearance.font.pixelSize.smaller
+ color: Appearance.colors.colSubtext
+ visible: root.itemType && root.itemType != qsTr("App")
+ text: root.itemType
+ }
+ RowLayout {
+ Loader { // Checkmark for copied clipboard entry
+ visible: itemName == Quickshell.clipboardText && root.cliphistRawString
+ active: itemName == Quickshell.clipboardText && root.cliphistRawString
+ sourceComponent: Rectangle {
+ implicitWidth: activeText.implicitHeight
+ implicitHeight: activeText.implicitHeight
+ radius: Appearance.rounding.full
+ color: Appearance.colors.colPrimary
+ MaterialSymbol {
+ id: activeText
+ anchors.centerIn: parent
+ text: "check"
+ font.pixelSize: Appearance.font.pixelSize.normal
+ color: Appearance.m3colors.m3onPrimary
+ }
+ }
+ }
+ Repeater { // Favicons for links
+ model: root.query == root.itemName ? [] : root.urls
+ Favicon {
+ required property var modelData
+ size: parent.height
+ url: modelData
+ }
+ }
+ StyledText { // Item name/content
+ Layout.fillWidth: true
+ id: nameText
+ textFormat: Text.StyledText // RichText also works, but StyledText ensures elide work
+ font.pixelSize: Appearance.font.pixelSize.small
+ font.family: Appearance.font.family[root.fontType]
+ color: Appearance.m3colors.m3onSurface
+ horizontalAlignment: Text.AlignLeft
+ elide: Text.ElideRight
+ text: `${root.displayContent}`
+ }
+ }
+ Loader { // Clipboard image preview
+ active: root.cliphistRawString && /^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(root.cliphistRawString)
+ sourceComponent: CliphistImage {
+ Layout.fillWidth: true
+ entry: root.cliphistRawString
+ maxWidth: contentColumn.width
+ maxHeight: 140
+ }
+ }
+ }
+
+ // Action text
+ StyledText {
+ Layout.fillWidth: false
+ visible: (root.hovered || root.focus)
+ id: clickAction
+ font.pixelSize: Appearance.font.pixelSize.normal
+ color: Appearance.colors.colSubtext
+ horizontalAlignment: Text.AlignRight
+ text: root.itemClickActionName
+ }
+ }
+}
diff --git a/.config/quickshell/modules/overview/SearchWidget.qml b/.config/quickshell/modules/overview/SearchWidget.qml
new file mode 100644
index 000000000..9e6866240
--- /dev/null
+++ b/.config/quickshell/modules/overview/SearchWidget.qml
@@ -0,0 +1,425 @@
+import "root:/"
+import "root:/services/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import Qt5Compat.GraphicalEffects
+import Qt.labs.platform
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Effects
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+
+Item { // Wrapper
+ id: root
+ readonly property string xdgConfigHome: Directories.config
+ property string searchingText: ""
+ property bool showResults: searchingText != ""
+ property real searchBarHeight: searchBar.height + Appearance.sizes.elevationMargin * 2
+ implicitWidth: searchWidgetContent.implicitWidth + Appearance.sizes.elevationMargin * 2
+ implicitHeight: searchWidgetContent.implicitHeight + Appearance.sizes.elevationMargin * 2
+
+ property string mathResult: ""
+
+ function disableExpandAnimation() {
+ searchWidthBehavior.enabled = false;
+ }
+
+ function cancelSearch() {
+ searchInput.selectAll()
+ root.searchingText = ""
+ searchWidthBehavior.enabled = true;
+ }
+
+ function setSearchingText(text) {
+ searchInput.text = text;
+ root.searchingText = text;
+ }
+
+ property var searchActions: [
+ {
+ action: "img",
+ execute: () => {
+ executor.executeCommand(Directories.wallpaperSwitchScriptPath)
+ }
+ },
+ {
+ action: "dark",
+ execute: () => {
+ executor.executeCommand(`${Directories.wallpaperSwitchScriptPath} --mode dark --noswitch`)
+ }
+ },
+ {
+ action: "light",
+ execute: () => {
+ executor.executeCommand(`${Directories.wallpaperSwitchScriptPath} --mode light --noswitch`)
+ }
+ },
+ {
+ action: "accentcolor",
+ execute: (args) => {
+ executor.executeCommand(
+ `${Directories.wallpaperSwitchScriptPath} --noswitch --color ${args != '' ? ("'"+args+"'") : ""}`
+ )
+ }
+ },
+ {
+ action: "todo",
+ execute: (args) => {
+ Todo.addTask(args)
+ }
+ },
+ ]
+
+ function focusFirstItemIfNeeded() {
+ if (searchInput.focus) appResults.currentIndex = 0; // Focus the first item
+ }
+
+ Timer {
+ id: nonAppResultsTimer
+ interval: ConfigOptions.search.nonAppResultDelay
+ onTriggered: {
+ mathProcess.calculateExpression(root.searchingText);
+ }
+ }
+
+ Process {
+ id: mathProcess
+ property list baseCommand: ["qalc", "-t"]
+ function calculateExpression(expression) {
+ // mathProcess.running = false
+ mathProcess.command = baseCommand.concat(expression)
+ mathProcess.running = true
+ }
+ stdout: SplitParser {
+ onRead: data => {
+ root.mathResult = data
+ root.focusFirstItemIfNeeded()
+ }
+ }
+ }
+
+ Process {
+ id: executor
+ property list baseCommand: ["bash", "-c"]
+ function executeCommand(command) {
+ executor.command = baseCommand.concat(
+ `${command} || ${ConfigOptions.apps.terminal} fish -C 'echo "${qsTr("Searching for package with that command")}..." && pacman -F ${command}'`
+ )
+ executor.startDetached()
+ }
+ }
+
+ Keys.onPressed: (event) => {
+ // Prevent Esc and Backspace from registering
+ if (event.key === Qt.Key_Escape) return;
+
+ // Handle Backspace: focus and delete character if not focused
+ if (event.key === Qt.Key_Backspace) {
+ if (!searchInput.activeFocus) {
+ searchInput.forceActiveFocus();
+ if (event.modifiers & Qt.ControlModifier) {
+ // Delete word before cursor
+ let text = searchInput.text;
+ let pos = searchInput.cursorPosition;
+ if (pos > 0) {
+ // Find the start of the previous word
+ let left = text.slice(0, pos);
+ let match = left.match(/(\s*\S+)\s*$/);
+ let deleteLen = match ? match[0].length : 1;
+ searchInput.text = text.slice(0, pos - deleteLen) + text.slice(pos);
+ searchInput.cursorPosition = pos - deleteLen;
+ }
+ } else {
+ // Delete character before cursor if any
+ if (searchInput.cursorPosition > 0) {
+ searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition - 1) +
+ searchInput.text.slice(searchInput.cursorPosition);
+ searchInput.cursorPosition -= 1;
+ }
+ }
+ // Always move cursor to end after programmatic edit
+ searchInput.cursorPosition = searchInput.text.length;
+ event.accepted = true;
+ }
+ // If already focused, let TextField handle it
+ return;
+ }
+
+ // Only handle visible printable characters (ignore control chars, arrows, etc.)
+ if (
+ event.text &&
+ event.text.length === 1 &&
+ event.key !== Qt.Key_Enter &&
+ event.key !== Qt.Key_Return &&
+ event.text.charCodeAt(0) >= 0x20 // ignore control chars like Backspace, Tab, etc.
+ ) {
+ if (!searchInput.activeFocus) {
+ searchInput.forceActiveFocus();
+ // Insert the character at the cursor position
+ searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition) +
+ event.text +
+ searchInput.text.slice(searchInput.cursorPosition);
+ searchInput.cursorPosition += 1;
+ event.accepted = true;
+ }
+ }
+ }
+
+ StyledRectangularShadow {
+ target: searchWidgetContent
+ }
+ Rectangle { // Background
+ id: searchWidgetContent
+ anchors.centerIn: parent
+ implicitWidth: columnLayout.implicitWidth
+ implicitHeight: columnLayout.implicitHeight
+ radius: Appearance.rounding.large
+ color: Appearance.colors.colLayer0
+
+ ColumnLayout {
+ id: columnLayout
+ anchors.centerIn: parent
+ spacing: 0
+
+ // clip: true
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: searchWidgetContent.width
+ height: searchWidgetContent.width
+ radius: searchWidgetContent.radius
+ }
+ }
+
+ RowLayout {
+ id: searchBar
+ spacing: 5
+ MaterialSymbol {
+ id: searchIcon
+ Layout.leftMargin: 15
+ iconSize: Appearance.font.pixelSize.huge
+ color: Appearance.m3colors.m3onSurface
+ text: root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard) ? 'content_paste_search' : 'search'
+ }
+ TextField { // Search box
+ id: searchInput
+
+ focus: GlobalStates.overviewOpen
+ Layout.rightMargin: 15
+ padding: 15
+ renderType: Text.NativeRendering
+ font {
+ family: Appearance?.font.family.main ?? "sans-serif"
+ pixelSize: Appearance?.font.pixelSize.small ?? 15
+ hintingPreference: Font.PreferFullHinting
+ }
+ color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
+ selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
+ selectionColor: Appearance.colors.colSecondaryContainer
+ placeholderText: qsTr("Search, calculate or run")
+ placeholderTextColor: Appearance.m3colors.m3outline
+ implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth
+
+ Behavior on implicitWidth {
+ id: searchWidthBehavior
+ enabled: false
+ NumberAnimation {
+ duration: 300
+ easing.type: Appearance.animation.elementMove.type
+ easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
+ }
+ }
+
+ onTextChanged: root.searchingText = text
+
+ onAccepted: {
+ if (appResults.count > 0) {
+ // Get the first visible delegate and trigger its click
+ let firstItem = appResults.itemAtIndex(0);
+ if (firstItem && firstItem.clicked) {
+ firstItem.clicked();
+ }
+ }
+ }
+
+ background: null
+
+ cursorDelegate: Rectangle {
+ width: 1
+ color: searchInput.activeFocus ? Appearance.colors.colPrimary : "transparent"
+ radius: 1
+ }
+ }
+ }
+
+ Rectangle { // Separator
+ visible: root.showResults
+ Layout.fillWidth: true
+ height: 1
+ color: Appearance.colors.colOutlineVariant
+ }
+
+ ListView { // App results
+ id: appResults
+ visible: root.showResults
+ Layout.fillWidth: true
+ implicitHeight: Math.min(600, appResults.contentHeight + topMargin + bottomMargin)
+ clip: true
+ topMargin: 10
+ bottomMargin: 10
+ spacing: 2
+ KeyNavigation.up: searchBar
+ highlightMoveDuration : 100
+
+ onFocusChanged: {
+ if(focus) appResults.currentIndex = 1;
+ }
+
+ Connections {
+ target: root
+ function onSearchingTextChanged() {
+ if (appResults.count > 0)
+ appResults.currentIndex = 0;
+ }
+ }
+
+ model: ScriptModel {
+ id: model
+ values: { // Search results are handled here
+ ////////////////// Skip? //////////////////
+ if(root.searchingText == "") return [];
+
+ ///////////// Special cases ///////////////
+ if (root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard)) { // Clipboard
+ const searchString = root.searchingText.slice(ConfigOptions.search.prefix.clipboard.length);
+ return Cliphist.fuzzyQuery(searchString).map(entry => {
+ return {
+ cliphistRawString: entry,
+ name: entry.replace(/^\s*\S+\s+/, ""),
+ clickActionName: "",
+ type: `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`,
+ execute: () => {
+ Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(entry)}' | cliphist decode | wl-copy`);
+ }
+ };
+ }).filter(Boolean);
+ }
+ if (root.searchingText.startsWith(ConfigOptions.search.prefix.emojis)) { // Clipboard
+ const searchString = root.searchingText.slice(ConfigOptions.search.prefix.emojis.length);
+ return Emojis.fuzzyQuery(searchString).map(entry => {
+ return {
+ cliphistRawString: entry,
+ bigText: entry.match(/^\s*(\S+)/)?.[1] || "",
+ name: entry.replace(/^\s*\S+\s+/, ""),
+ clickActionName: "",
+ type: "Emoji",
+ execute: () => {
+ Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(entry.match(/^\s*(\S+)/)?.[1])}'`);
+ }
+ };
+ }).filter(Boolean);
+ }
+
+
+ ////////////////// Init ///////////////////
+ nonAppResultsTimer.restart();
+ const mathResultObject = {
+ name: root.mathResult,
+ clickActionName: qsTr("Copy"),
+ type: qsTr("Math result"),
+ fontType: "monospace",
+ materialSymbol: 'calculate',
+ execute: () => {
+ Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(root.mathResult)}'`)
+ }
+ }
+ const commandResultObject = {
+ name: searchingText.replace("file://", ""),
+ clickActionName: qsTr("Run"),
+ type: qsTr("Run command"),
+ fontType: "monospace",
+ materialSymbol: 'terminal',
+ execute: () => {
+ executor.executeCommand(searchingText.startsWith('sudo') ? `${ConfigOptions.apps.terminal} fish -C '${root.searchingText.replace("file://", "")}'` : root.searchingText.replace("file://", ""));
+ }
+ }
+ const launcherActionObjects = root.searchActions
+ .map(action => {
+ const actionString = `${ConfigOptions.search.prefix.action}${action.action}`;
+ if (actionString.startsWith(root.searchingText) || root.searchingText.startsWith(actionString)) {
+ return {
+ name: root.searchingText.startsWith(actionString) ? root.searchingText : actionString,
+ clickActionName: qsTr("Run"),
+ type: qsTr("Action"),
+ materialSymbol: 'settings_suggest',
+ execute: () => {
+ action.execute(root.searchingText.split(" ").slice(1).join(" "))
+ },
+ };
+ }
+ return null;
+ })
+ .filter(Boolean);
+
+ let result = [];
+
+ //////////////// Apps //////////////////
+ result = result.concat(
+ AppSearch.fuzzyQuery(root.searchingText)
+ .map((entry) => {
+ entry.clickActionName = qsTr("Launch");
+ entry.type = qsTr("App");
+ return entry;
+ })
+ );
+
+ ////////// Launcher actions ////////////
+ result = result.concat(launcherActionObjects);
+
+ /////////// Math result & command //////////
+ const startsWithNumber = /^\d/.test(root.searchingText);
+ if (startsWithNumber) {
+ result.push(mathResultObject);
+ result.push(commandResultObject);
+ } else {
+ result.push(commandResultObject);
+ result.push(mathResultObject);
+ }
+
+ ///////////////// Web search ////////////////
+ result.push({
+ name: root.searchingText,
+ clickActionName: qsTr("Search"),
+ type: qsTr("Search the web"),
+ materialSymbol: 'travel_explore',
+ execute: () => {
+ let url = ConfigOptions.search.engineBaseUrl + root.searchingText
+ for (let site of ConfigOptions.search.excludedSites) {
+ url += ` -site:${site}`;
+ }
+ Qt.openUrlExternally(url);
+ }
+ });
+
+ return result;
+ }
+ }
+
+ delegate: SearchItem { // The selectable item for each search result
+ required property var modelData
+ anchors.left: parent?.left
+ anchors.right: parent?.right
+ entry: modelData
+ query: root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard) ?
+ root.searchingText.slice(ConfigOptions.search.prefix.clipboard.length) :
+ root.searchingText;
+ }
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/screenCorners/ScreenCorners.qml b/.config/quickshell/modules/screenCorners/ScreenCorners.qml
new file mode 100644
index 000000000..3988d73d8
--- /dev/null
+++ b/.config/quickshell/modules/screenCorners/ScreenCorners.qml
@@ -0,0 +1,87 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Scope {
+ id: screenCorners
+ readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
+
+ Variants {
+ model: Quickshell.screens
+
+ PanelWindow {
+ visible: (ConfigOptions.appearance.fakeScreenRounding === 1
+ || (ConfigOptions.appearance.fakeScreenRounding === 2
+ && !activeWindow?.fullscreen))
+
+ property var modelData
+
+ screen: modelData
+ exclusionMode: ExclusionMode.Ignore
+ mask: Region {
+ item: null
+ }
+ HyprlandWindow.visibleMask: Region {
+ Region {
+ item: topLeftCorner
+ }
+ Region {
+ item: topRightCorner
+ }
+ Region {
+ item: bottomLeftCorner
+ }
+ Region {
+ item: bottomRightCorner
+ }
+ }
+ WlrLayershell.namespace: "quickshell:screenCorners"
+ WlrLayershell.layer: WlrLayer.Overlay
+ color: "transparent"
+
+ anchors {
+ top: true
+ left: true
+ right: true
+ bottom: true
+ }
+
+ RoundCorner {
+ id: topLeftCorner
+ anchors.top: parent.top
+ anchors.left: parent.left
+ size: Appearance.rounding.screenRounding
+ corner: cornerEnum.topLeft
+ }
+ RoundCorner {
+ id: topRightCorner
+ anchors.top: parent.top
+ anchors.right: parent.right
+ size: Appearance.rounding.screenRounding
+ corner: cornerEnum.topRight
+ }
+ RoundCorner {
+ id: bottomLeftCorner
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ size: Appearance.rounding.screenRounding
+ corner: cornerEnum.bottomLeft
+ }
+ RoundCorner {
+ id: bottomRightCorner
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ size: Appearance.rounding.screenRounding
+ corner: cornerEnum.bottomRight
+ }
+
+ }
+
+ }
+
+}
diff --git a/.config/quickshell/modules/session/Session.qml b/.config/quickshell/modules/session/Session.qml
new file mode 100644
index 000000000..94e6123ff
--- /dev/null
+++ b/.config/quickshell/modules/session/Session.qml
@@ -0,0 +1,228 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Scope {
+ id: root
+ property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name)
+
+ Loader {
+ id: sessionLoader
+ active: false
+
+ sourceComponent: PanelWindow { // Session menu
+ id: sessionRoot
+ visible: sessionLoader.active
+ property string subtitle
+
+ function hide() {
+ sessionLoader.active = false
+ }
+
+
+ exclusionMode: ExclusionMode.Ignore
+ WlrLayershell.namespace: "quickshell:session"
+ WlrLayershell.layer: WlrLayer.Overlay
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
+ color: ColorUtils.transparentize(Appearance.m3colors.m3background, 0.3)
+
+ anchors {
+ top: true
+ left: true
+ right: true
+ }
+
+ implicitWidth: root.focusedScreen?.width ?? 0
+ implicitHeight: root.focusedScreen?.height ?? 0
+
+ MouseArea {
+ id: sessionMouseArea
+ anchors.fill: parent
+ onClicked: {
+ sessionRoot.hide()
+ }
+ }
+
+ ColumnLayout { // Content column
+ anchors.centerIn: parent
+ spacing: 15
+
+ Keys.onPressed: (event) => {
+ if (event.key === Qt.Key_Escape) {
+ sessionRoot.hide();
+ }
+ }
+
+ ColumnLayout {
+ Layout.alignment: Qt.AlignHCenter
+ spacing: 0
+ StyledText { // Title
+ Layout.alignment: Qt.AlignHCenter
+ horizontalAlignment: Text.AlignHCenter
+ font.family: Appearance.font.family.title
+ font.pixelSize: Appearance.font.pixelSize.title
+ font.weight: Font.DemiBold
+ text: qsTr("Session")
+ }
+
+ StyledText { // Small instruction
+ Layout.alignment: Qt.AlignHCenter
+ horizontalAlignment: Text.AlignHCenter
+ font.family: Appearance.font.family.title
+ font.pixelSize: Appearance.font.pixelSize.normal
+ text: qsTr("Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel")
+ }
+ }
+
+ GridLayout {
+ columns: 4
+ columnSpacing: 15
+ rowSpacing: 15
+
+ SessionActionButton {
+ id: sessionLock
+ focus: sessionRoot.visible
+ buttonIcon: "lock"
+ buttonText: qsTr("Lock")
+ onClicked: { Hyprland.dispatch("exec loginctl lock-session"); sessionRoot.hide() }
+ onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText }
+ KeyNavigation.right: sessionSleep
+ KeyNavigation.down: sessionHibernate
+ }
+ SessionActionButton {
+ id: sessionSleep
+ buttonIcon: "dark_mode"
+ buttonText: qsTr("Sleep")
+ onClicked: { Hyprland.dispatch("exec systemctl suspend || loginctl suspend"); sessionRoot.hide() }
+ onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText }
+ KeyNavigation.left: sessionLock
+ KeyNavigation.right: sessionLogout
+ KeyNavigation.down: sessionShutdown
+ }
+ SessionActionButton {
+ id: sessionLogout
+ buttonIcon: "logout"
+ buttonText: qsTr("Logout")
+ onClicked: { Hyprland.dispatch("exec pkill Hyprland"); sessionRoot.hide() }
+ onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText }
+ KeyNavigation.left: sessionSleep
+ KeyNavigation.right: sessionTaskManager
+ KeyNavigation.down: sessionReboot
+ }
+ SessionActionButton {
+ id: sessionTaskManager
+ buttonIcon: "browse_activity"
+ buttonText: qsTr("Task Manager")
+ onClicked: { Hyprland.dispatch(`exec ${ConfigOptions.apps.taskManager}`); sessionRoot.hide() }
+ onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText }
+ KeyNavigation.left: sessionLogout
+ KeyNavigation.down: sessionFirmwareReboot
+ }
+
+ SessionActionButton {
+ id: sessionHibernate
+ buttonIcon: "downloading"
+ buttonText: qsTr("Hibernate")
+ onClicked: { Hyprland.dispatch("exec systemctl hibernate || loginctl hibernate"); sessionRoot.hide() }
+ onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText }
+ KeyNavigation.up: sessionLock
+ KeyNavigation.right: sessionShutdown
+ }
+ SessionActionButton {
+ id: sessionShutdown
+ buttonIcon: "power_settings_new"
+ buttonText: qsTr("Shutdown")
+ onClicked: { Hyprland.dispatch("exec systemctl poweroff || loginctl poweroff"); sessionRoot.hide() }
+ onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText }
+ KeyNavigation.left: sessionHibernate
+ KeyNavigation.right: sessionReboot
+ KeyNavigation.up: sessionSleep
+ }
+ SessionActionButton {
+ id: sessionReboot
+ buttonIcon: "restart_alt"
+ buttonText: qsTr("Reboot")
+ onClicked: { Hyprland.dispatch("exec reboot || loginctl reboot"); sessionRoot.hide() }
+ onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText }
+ KeyNavigation.left: sessionShutdown
+ KeyNavigation.right: sessionFirmwareReboot
+ KeyNavigation.up: sessionLogout
+ }
+ SessionActionButton {
+ id: sessionFirmwareReboot
+ buttonIcon: "settings_applications"
+ buttonText: qsTr("Reboot to firmware settings")
+ onClicked: { Hyprland.dispatch("exec systemctl reboot --firmware-setup || loginctl reboot --firmware-setup"); sessionRoot.hide() }
+ onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText }
+ KeyNavigation.up: sessionTaskManager
+ KeyNavigation.left: sessionReboot
+ }
+ }
+
+ Rectangle {
+ Layout.alignment: Qt.AlignHCenter
+ radius: Appearance.rounding.normal
+ implicitHeight: sessionSubtitle.implicitHeight + 10 * 2
+ implicitWidth: sessionSubtitle.implicitWidth + 15 * 2
+ color: Appearance.colors.colTooltip
+ clip: true
+
+ Behavior on implicitWidth {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+
+ StyledText {
+ id: sessionSubtitle
+ anchors.centerIn: parent
+ color: Appearance.colors.colOnTooltip
+ text: sessionRoot.subtitle
+ }
+ }
+ }
+
+ }
+ }
+
+ IpcHandler {
+ target: "session"
+
+ function toggle(): void {
+ sessionLoader.active = !sessionLoader.active;
+ }
+
+ function close(): void {
+ sessionLoader.active = false;
+ }
+
+ function open(): void {
+ sessionLoader.active = true;
+ }
+ }
+
+ GlobalShortcut {
+ name: "sessionToggle"
+ description: qsTr("Toggles session screen on press")
+
+ onPressed: {
+ sessionLoader.active = !sessionLoader.active;
+ }
+ }
+
+ GlobalShortcut {
+ name: "sessionOpen"
+ description: qsTr("Opens session screen on press")
+
+ onPressed: {
+ sessionLoader.active = true;
+ }
+ }
+
+}
diff --git a/.config/quickshell/modules/session/SessionActionButton.qml b/.config/quickshell/modules/session/SessionActionButton.qml
new file mode 100644
index 000000000..becda60c1
--- /dev/null
+++ b/.config/quickshell/modules/session/SessionActionButton.qml
@@ -0,0 +1,62 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets/"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+
+RippleButton {
+ id: button
+
+ property string buttonIcon
+ property string buttonText
+ property bool keyboardDown: false
+ property real size: 120
+
+ buttonRadius: (button.focus || button.down) ? size / 2 : Appearance.rounding.verylarge
+ colBackground: button.keyboardDown ? Appearance.colors.colSecondaryContainerActive :
+ button.focus ? Appearance.colors.colPrimary :
+ Appearance.colors.colSecondaryContainer
+ colBackgroundHover: Appearance.colors.colPrimary
+ colRipple: Appearance.colors.colPrimaryActive
+ property color colText: (button.down || button.keyboardDown || button.focus || button.hovered) ?
+ Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer0
+
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+ background.implicitHeight: size
+ background.implicitWidth: size
+
+ Behavior on buttonRadius {
+ animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
+ }
+
+ Keys.onPressed: (event) => {
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ keyboardDown = true
+ button.clicked()
+ event.accepted = true;
+ }
+ }
+ Keys.onReleased: (event) => {
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ keyboardDown = false
+ event.accepted = true;
+ }
+ }
+
+ contentItem: MaterialSymbol {
+ id: icon
+ anchors.fill: parent
+ color: button.colText
+ horizontalAlignment: Text.AlignHCenter
+ iconSize: 45
+ text: buttonIcon
+ }
+
+ StyledToolTip {
+ content: buttonText
+ }
+
+}
diff --git a/.config/quickshell/modules/sidebarLeft/AiChat.qml b/.config/quickshell/modules/sidebarLeft/AiChat.qml
new file mode 100644
index 000000000..1ce752c7b
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/AiChat.qml
@@ -0,0 +1,555 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "./aiChat/"
+import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/file_utils.js" as FileUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Qt5Compat.GraphicalEffects
+import Quickshell.Io
+import Quickshell
+import Quickshell.Hyprland
+
+Item {
+ id: root
+ property var inputField: messageInputField
+ property string commandPrefix: "/"
+
+ property var suggestionQuery: ""
+ property var suggestionList: []
+
+ onFocusChanged: (focus) => {
+ if (focus) {
+ root.inputField.forceActiveFocus()
+ }
+ }
+
+ Keys.onPressed: (event) => {
+ messageInputField.forceActiveFocus()
+ if (event.modifiers === Qt.NoModifier) {
+ if (event.key === Qt.Key_PageUp) {
+ messageListView.contentY = Math.max(0, messageListView.contentY - messageListView.height / 2)
+ event.accepted = true
+ } else if (event.key === Qt.Key_PageDown) {
+ messageListView.contentY = Math.min(messageListView.contentHeight - messageListView.height / 2, messageListView.contentY + messageListView.height / 2)
+ event.accepted = true
+ }
+ }
+ }
+
+ property var allCommands: [
+ {
+ name: "model",
+ description: qsTr("Choose model"),
+ execute: (args) => {
+ Ai.setModel(args[0]);
+ }
+ },
+ {
+ name: "clear",
+ description: qsTr("Clear chat history"),
+ execute: () => {
+ Ai.clearMessages();
+ }
+ },
+ {
+ name: "key",
+ description: qsTr("Set API key"),
+ execute: (args) => {
+ if (args[0] == "get") {
+ Ai.printApiKey()
+ } else {
+ Ai.setApiKey(args[0]);
+ }
+ }
+ },
+ {
+ name: "temp",
+ description: qsTr("Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5."),
+ execute: (args) => {
+ // console.log(args)
+ if (args.length == 0 || args[0] == "get") {
+ Ai.printTemperature()
+ } else {
+ const temp = parseFloat(args[0]);
+ Ai.setTemperature(temp);
+ }
+ }
+ },
+ {
+ name: "test",
+ description: qsTr("Markdown test"),
+ execute: () => {
+ Ai.addMessage(`
+
+A longer think block to test revealing animation
+OwO wem ipsum dowo sit amet, consekituwet awipiscing ewit, sed do eiuwsmod tempow inwididunt ut wabowe et dowo mawa. Ut enim ad minim weniam, quis nostwud exeucitation uwuwamcow bowowis nisi ut awiquip ex ea commowo consequat. Duuis aute iwuwe dowo in wepwependewit in wowuptate velit esse ciwwum dowo eu fugiat nuwa pawiatuw. Excepteuw sint occaecat cupidatat non pwowoident, sunt in cuwpa qui officia desewunt mowit anim id est wabowum. Meouw! >w<
+Mowe uwu wem ipsum!
+
+## ✏️ Markdown test
+### Formatting
+
+- *Italic*, \`Monospace\`, **Bold**, [Link](https://example.com)
+- Arch lincox icon
+
+### Table
+
+Quickshell vs AGS/Astal
+
+| | Quickshell | AGS/Astal |
+|--------------------------|------------------|-------------------|
+| UI Toolkit | Qt | Gtk3/Gtk4 |
+| Language | QML | Js/Ts/Lua |
+| Reactivity | Implied | Needs declaration |
+| Widget placement | Mildly difficult | More intuitive |
+| Bluetooth & Wifi support | ❌ | ✅ |
+| No-delay keybinds | ✅ | ❌ |
+| Development | New APIs | New syntax |
+
+### Code block
+
+Just a hello world...
+
+\`\`\`cpp
+#include
+// This is intentionally very long to test scrolling
+const std::string GREETING = \"UwU\";
+int main(int argc, char* argv[]) {
+ std::cout << GREETING;
+}
+\`\`\`
+
+### LaTeX
+
+
+Inline w/ dollar signs: $\\frac{1}{2} = \\frac{2}{4}$
+
+Inline w/ double dollar signs: $$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$
+
+Inline w/ backslash and square brackets \\[\\int_0^\\infty \\frac{1}{x^2} dx = \\infty\\]
+
+Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
+`,
+ Ai.interfaceRole);
+ }
+ },
+ ]
+
+ function handleInput(inputText) {
+ if (inputText.startsWith(root.commandPrefix)) {
+ // Handle special commands
+ const command = inputText.split(" ")[0].substring(1);
+ const args = inputText.split(" ").slice(1);
+ const commandObj = root.allCommands.find(cmd => cmd.name === `${command}`);
+ if (commandObj) {
+ commandObj.execute(args);
+ } else {
+ Ai.addMessage(qsTr("Unknown command: ") + command, Ai.interfaceRole);
+ }
+ }
+ else {
+ Ai.sendUserMessage(inputText);
+ }
+ }
+
+ ColumnLayout {
+ id: columnLayout
+ anchors.fill: parent
+
+ Item { // Messages
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ StyledListView { // Message list
+ id: messageListView
+ anchors.fill: parent
+ spacing: 10
+ popin: false
+
+ property int lastResponseLength: 0
+
+ clip: true
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: swipeView.width
+ height: swipeView.height
+ radius: Appearance.rounding.small
+ }
+ }
+
+ add: null // Prevent function calls from being janky
+
+ Behavior on contentY {
+ NumberAnimation {
+ id: scrollAnim
+ duration: Appearance.animation.scroll.duration
+ easing.type: Appearance.animation.scroll.type
+ easing.bezierCurve: Appearance.animation.scroll.bezierCurve
+ }
+ }
+
+ model: ScriptModel {
+ values: Ai.messageIDs.filter(id => {
+ const message = Ai.messageByID[id];
+ return message?.visibleToUser ?? true;
+ })
+ }
+ delegate: AiMessage {
+ required property var modelData
+ required property int index
+ messageIndex: index
+ messageData: {
+ Ai.messageByID[modelData]
+ }
+ messageInputField: root.inputField
+ }
+ }
+
+ Item { // Placeholder when list is empty
+ opacity: Ai.messageIDs.length === 0 ? 1 : 0
+ visible: opacity > 0
+ anchors.fill: parent
+
+ Behavior on opacity {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: 5
+
+ MaterialSymbol {
+ Layout.alignment: Qt.AlignHCenter
+ iconSize: 60
+ color: Appearance.m3colors.m3outline
+ text: "neurology"
+ }
+ StyledText {
+ id: widgetNameText
+ Layout.alignment: Qt.AlignHCenter
+ font.pixelSize: Appearance.font.pixelSize.larger
+ font.family: Appearance.font.family.title
+ color: Appearance.m3colors.m3outline
+ horizontalAlignment: Text.AlignHCenter
+ text: qsTr("Large language models")
+ }
+ StyledText {
+ id: widgetDescriptionText
+ Layout.fillWidth: true
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: Appearance.m3colors.m3outline
+ horizontalAlignment: Text.AlignLeft
+ wrapMode: Text.Wrap
+ text: qsTr("Ctrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window")
+ }
+ }
+ }
+ }
+
+ Item { // Suggestion description
+ visible: descriptionText.text.length > 0
+ Layout.fillWidth: true
+ implicitHeight: descriptionBackground.implicitHeight
+
+ Rectangle {
+ id: descriptionBackground
+ color: Appearance.colors.colTooltip
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ implicitHeight: descriptionText.implicitHeight + 5 * 2
+ radius: Appearance.rounding.verysmall
+
+ StyledText {
+ id: descriptionText
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: 10
+ anchors.rightMargin: 10
+ anchors.verticalCenter: parent.verticalCenter
+ font.pixelSize: Appearance.font.pixelSize.smaller
+ color: Appearance.colors.colOnTooltip
+ wrapMode: Text.Wrap
+ text: root.suggestionList[suggestions.selectedIndex]?.description ?? ""
+ }
+ }
+ }
+
+ FlowButtonGroup { // Suggestions
+ id: suggestions
+ visible: root.suggestionList.length > 0 && messageInputField.text.length > 0
+ property int selectedIndex: 0
+ Layout.fillWidth: true
+ spacing: 5
+
+ Repeater {
+ id: suggestionRepeater
+ model: {
+ suggestions.selectedIndex = 0
+ return root.suggestionList.slice(0, 10)
+ }
+ delegate: ApiCommandButton {
+ id: commandButton
+ colBackground: suggestions.selectedIndex === index ? Appearance.colors.colLayer2Hover : Appearance.colors.colLayer2
+ bounce: false
+ contentItem: StyledText {
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: Appearance.m3colors.m3onSurface
+ horizontalAlignment: Text.AlignHCenter
+ text: modelData.displayName ?? modelData.name
+ }
+
+ onHoveredChanged: {
+ if (commandButton.hovered) {
+ suggestions.selectedIndex = index;
+ }
+ }
+ onClicked: {
+ suggestions.acceptSuggestion(modelData.name)
+ }
+ }
+ }
+
+ function acceptSuggestion(word) {
+ const words = messageInputField.text.trim().split(/\s+/);
+ if (words.length > 0) {
+ words[words.length - 1] = word;
+ } else {
+ words.push(word);
+ }
+ const updatedText = words.join(" ") + " ";
+ messageInputField.text = updatedText;
+ messageInputField.cursorPosition = messageInputField.text.length;
+ messageInputField.forceActiveFocus();
+ }
+
+ function acceptSelectedWord() {
+ if (suggestions.selectedIndex >= 0 && suggestions.selectedIndex < suggestionRepeater.count) {
+ const word = root.suggestionList[suggestions.selectedIndex].name;
+ suggestions.acceptSuggestion(word);
+ }
+ }
+ }
+
+ Rectangle { // Input area
+ id: inputWrapper
+ property real columnSpacing: 5
+ Layout.fillWidth: true
+ radius: Appearance.rounding.small
+ color: Appearance.colors.colLayer1
+ implicitWidth: messageInputField.implicitWidth
+ implicitHeight: Math.max(inputFieldRowLayout.implicitHeight + inputFieldRowLayout.anchors.topMargin
+ + commandButtonsRow.implicitHeight + commandButtonsRow.anchors.bottomMargin + columnSpacing, 45)
+ clip: true
+ border.color: Appearance.colors.colOutlineVariant
+ border.width: 1
+
+ Behavior on implicitHeight {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+
+ RowLayout { // Input field and send button
+ id: inputFieldRowLayout
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: 5
+ spacing: 0
+
+ StyledTextArea { // The actual TextArea
+ id: messageInputField
+ wrapMode: TextArea.Wrap
+ Layout.fillWidth: true
+ padding: 10
+ color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
+ placeholderText: StringUtils.format(qsTr('Message the model... "{0}" for commands'), root.commandPrefix)
+
+ background: null
+
+ onTextChanged: { // Handle suggestions
+ if(messageInputField.text.length === 0) {
+ root.suggestionQuery = ""
+ root.suggestionList = []
+ return
+ } else if(messageInputField.text.startsWith(`${root.commandPrefix}model`)) {
+ root.suggestionQuery = messageInputField.text.split(" ")[1] ?? ""
+ const modelResults = Fuzzy.go(root.suggestionQuery, Ai.modelList.map(model => {
+ return {
+ name: Fuzzy.prepare(model),
+ obj: model,
+ }
+ }), {
+ all: true,
+ key: "name"
+ })
+ root.suggestionList = modelResults.map(model => {
+ return {
+ name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "model ") : ""}${model.target}`,
+ displayName: `${Ai.models[model.target].name}`,
+ description: `${Ai.models[model.target].description}`,
+ }
+ })
+ } else if(messageInputField.text.startsWith(root.commandPrefix)) {
+ root.suggestionQuery = messageInputField.text
+ root.suggestionList = root.allCommands.filter(cmd => cmd.name.startsWith(messageInputField.text.substring(1))).map(cmd => {
+ return {
+ name: `${root.commandPrefix}${cmd.name}`,
+ description: `${cmd.description}`,
+ }
+ })
+ }
+ }
+
+ function accept() {
+ root.handleInput(text)
+ text = ""
+ }
+
+ Keys.onPressed: (event) => {
+ if (event.key === Qt.Key_Tab) {
+ suggestions.acceptSelectedWord();
+ event.accepted = true;
+ } else if (event.key === Qt.Key_Up && suggestions.visible) {
+ suggestions.selectedIndex = Math.max(0, suggestions.selectedIndex - 1);
+ event.accepted = true;
+ } else if (event.key === Qt.Key_Down && suggestions.visible) {
+ suggestions.selectedIndex = Math.min(root.suggestionList.length - 1, suggestions.selectedIndex + 1);
+ event.accepted = true;
+ } else if ((event.key === Qt.Key_Enter || event.key === Qt.Key_Return)) {
+ if (event.modifiers & Qt.ShiftModifier) {
+ // Insert newline
+ messageInputField.insert(messageInputField.cursorPosition, "\n")
+ event.accepted = true
+ } else { // Accept text
+ const inputText = messageInputField.text
+ messageInputField.clear()
+ root.handleInput(inputText)
+ event.accepted = true
+ }
+ }
+ }
+ }
+
+ RippleButton { // Send button
+ id: sendButton
+ Layout.alignment: Qt.AlignTop
+ Layout.rightMargin: 5
+ implicitWidth: 40
+ implicitHeight: 40
+ buttonRadius: Appearance.rounding.small
+ enabled: messageInputField.text.length > 0
+ toggled: enabled
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: sendButton.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
+ onClicked: {
+ const inputText = messageInputField.text
+ root.handleInput(inputText)
+ messageInputField.clear()
+ }
+ }
+
+ contentItem: MaterialSymbol {
+ anchors.centerIn: parent
+ horizontalAlignment: Text.AlignHCenter
+ iconSize: Appearance.font.pixelSize.larger
+ // fill: sendButton.enabled ? 1 : 0
+ color: sendButton.enabled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2Disabled
+ text: "send"
+ }
+ }
+ }
+
+ RowLayout { // Controls
+ id: commandButtonsRow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: 5
+ anchors.leftMargin: 5
+ anchors.rightMargin: 5
+ spacing: 5
+
+ property var commandsShown: [
+ {
+ name: "model",
+ sendDirectly: false,
+ },
+ {
+ name: "clear",
+ sendDirectly: true,
+ },
+ ]
+
+ Item {
+ implicitHeight: providerRowLayout.implicitHeight + 5 * 2
+ implicitWidth: providerRowLayout.implicitWidth + 10 * 2
+
+ RowLayout {
+ id: providerRowLayout
+ anchors.centerIn: parent
+
+ MaterialSymbol {
+ text: "api"
+ iconSize: Appearance.font.pixelSize.large
+ }
+ StyledText {
+ id: providerName
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: Appearance.m3colors.m3onSurface
+ elide: Text.ElideRight
+ text: Ai.getModel().name
+ }
+ }
+ StyledToolTip {
+ id: toolTip
+ extraVisibleCondition: false
+ alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered
+ content: StringUtils.format(qsTr("Current model: {0}\nSet it with {1}model MODEL"),
+ Ai.getModel().name, root.commandPrefix)
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ }
+ }
+
+ Item { Layout.fillWidth: true }
+
+ ButtonGroup {
+ padding: 0
+
+ Repeater { // Command buttons
+ model: commandButtonsRow.commandsShown
+ delegate: ApiCommandButton {
+ property string commandRepresentation: `${root.commandPrefix}${modelData.name}`
+ buttonText: commandRepresentation
+ onClicked: {
+ if(modelData.sendDirectly) {
+ root.handleInput(commandRepresentation)
+ } else {
+ messageInputField.text = commandRepresentation + " "
+ messageInputField.cursorPosition = messageInputField.text.length
+ messageInputField.forceActiveFocus()
+ }
+ if (modelData.name === "clear") {
+ messageInputField.text = ""
+ }
+ }
+ }
+ }
+ }
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarLeft/Anime.qml b/.config/quickshell/modules/sidebarLeft/Anime.qml
new file mode 100644
index 000000000..1300d5482
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/Anime.qml
@@ -0,0 +1,637 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/file_utils.js" as FileUtils
+import "./anime/"
+import Qt.labs.platform
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Qt5Compat.GraphicalEffects
+import Quickshell.Io
+import Quickshell
+import Quickshell.Hyprland
+
+Item {
+ id: root
+ property var inputField: tagInputField
+ readonly property var responses: Booru.responses
+ property string previewDownloadPath: Directories.booruPreviews
+ property string downloadPath: Directories.booruDownloads
+ property string nsfwPath: Directories.booruDownloadsNsfw
+ property string commandPrefix: "/"
+ property real scrollOnNewResponse: 100
+ property int tagSuggestionDelay: 210
+ property var suggestionQuery: ""
+ property var suggestionList: []
+
+ Connections {
+ target: Booru
+ function onTagSuggestion(query, suggestions) {
+ root.suggestionQuery = query;
+ root.suggestionList = suggestions;
+ }
+ }
+
+ property var allCommands: [
+ {
+ name: "mode",
+ description: qsTr("Set the current API provider"),
+ execute: (args) => {
+ Booru.setProvider(args[0]);
+ }
+ },
+ {
+ name: "clear",
+ description: qsTr("Clear the current list of images"),
+ execute: () => {
+ Booru.clearResponses();
+ }
+ },
+ {
+ name: "next",
+ description: qsTr("Get the next page of results"),
+ execute: () => {
+ if (root.responses.length > 0) {
+ const lastResponse = root.responses[root.responses.length - 1];
+ root.handleInput(`${lastResponse.tags.join(" ")} ${parseInt(lastResponse.page) + 1}`);
+ }
+ }
+ },
+ {
+ name: "safe",
+ description: qsTr("Disable NSFW content"),
+ execute: () => {
+ PersistentStateManager.setState("booru.allowNsfw", false);
+ }
+ },
+ {
+ name: "lewd",
+ description: qsTr("Allow NSFW content"),
+ execute: () => {
+ PersistentStateManager.setState("booru.allowNsfw", true);
+ }
+ },
+ ]
+
+ function handleInput(inputText) {
+ if (inputText.startsWith(root.commandPrefix)) {
+ // Handle special commands
+ const command = inputText.split(" ")[0].substring(1);
+ const args = inputText.split(" ").slice(1);
+ const commandObj = root.allCommands.find(cmd => cmd.name === `${command}`);
+ if (commandObj) {
+ commandObj.execute(args);
+ } else {
+ Booru.addSystemMessage(qsTr("Unknown command: ") + command);
+ }
+ }
+ else if (inputText.trim() == "+") {
+ if (root.responses.length > 0) {
+ const lastResponse = root.responses[root.responses.length - 1]
+ root.handleInput(lastResponse.tags.join(" ") + ` ${parseInt(lastResponse.page) + 1}`);
+ }
+ }
+ else {
+ // Create tag list
+ const tagList = inputText.split(/\s+/).filter(tag => tag.length > 0);
+ let pageIndex = 1;
+ for (let i = 0; i < tagList.length; ++i) { // Detect page number
+ if (/^\d+$/.test(tagList[i])) {
+ pageIndex = parseInt(tagList[i], 10);
+ tagList.splice(i, 1);
+ break;
+ }
+ }
+ Booru.makeRequest(tagList, PersistentStates.booru.allowNsfw, ConfigOptions.sidebar.booru.limit, pageIndex);
+ }
+ }
+
+ onFocusChanged: (focus) => {
+ if (focus) {
+ tagInputField.forceActiveFocus()
+ }
+ }
+
+ Keys.onPressed: (event) => {
+ tagInputField.forceActiveFocus()
+ if (event.modifiers === Qt.NoModifier) {
+ if (event.key === Qt.Key_PageUp) {
+ booruResponseListView.contentY = Math.max(0, booruResponseListView.contentY - booruResponseListView.height / 2)
+ event.accepted = true
+ } else if (event.key === Qt.Key_PageDown) {
+ booruResponseListView.contentY = Math.min(booruResponseListView.contentHeight - booruResponseListView.height / 2, booruResponseListView.contentY + booruResponseListView.height / 2)
+ event.accepted = true
+ }
+ }
+ }
+
+
+ ColumnLayout {
+ id: columnLayout
+ anchors.fill: parent
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ StyledListView { // Booru responses
+ id: booruResponseListView
+ anchors.fill: parent
+ spacing: 10
+
+ property int lastResponseLength: 0
+
+ clip: true
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: swipeView.width
+ height: swipeView.height
+ radius: Appearance.rounding.small
+ }
+ }
+
+ Behavior on contentY {
+ NumberAnimation {
+ id: scrollAnim
+ duration: Appearance.animation.scroll.duration
+ easing.type: Appearance.animation.scroll.type
+ easing.bezierCurve: Appearance.animation.scroll.bezierCurve
+ }
+ }
+
+ model: ScriptModel {
+ values: {
+ if(root.responses.length > booruResponseListView.lastResponseLength) {
+ if (booruResponseListView.lastResponseLength > 0 && root.responses[booruResponseListView.lastResponseLength].provider != "system")
+ booruResponseListView.contentY = booruResponseListView.contentY + root.scrollOnNewResponse
+ booruResponseListView.lastResponseLength = root.responses.length
+ }
+ return root.responses
+ }
+ }
+ delegate: BooruResponse {
+ responseData: modelData
+ tagInputField: root.inputField
+ previewDownloadPath: root.previewDownloadPath
+ downloadPath: root.downloadPath
+ nsfwPath: root.nsfwPath
+ }
+ }
+
+ Item { // Placeholder when list is empty
+ opacity: root.responses.length === 0 ? 1 : 0
+ visible: opacity > 0
+ anchors.fill: parent
+
+ Behavior on opacity {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: 5
+
+ MaterialSymbol {
+ Layout.alignment: Qt.AlignHCenter
+ iconSize: 60
+ color: Appearance.m3colors.m3outline
+ text: "bookmark_heart"
+ }
+ StyledText {
+ id: widgetNameText
+ Layout.alignment: Qt.AlignHCenter
+ font.pixelSize: Appearance.font.pixelSize.larger
+ font.family: Appearance.font.family.title
+ color: Appearance.m3colors.m3outline
+ horizontalAlignment: Text.AlignHCenter
+ text: qsTr("Anime boorus")
+ }
+ }
+ }
+
+ Item { // Queries awaiting response
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.margins: 10
+ implicitHeight: pendingBackground.implicitHeight
+ opacity: Booru.runningRequests > 0 ? 1 : 0
+ visible: opacity > 0
+
+ Behavior on opacity {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+
+ Rectangle {
+ id: pendingBackground
+ color: Appearance.m3colors.m3inverseSurface
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ implicitHeight: pendingText.implicitHeight + 12 * 2
+ radius: Appearance.rounding.verysmall
+
+ StyledText {
+ id: pendingText
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: 12
+ anchors.rightMargin: 12
+ anchors.verticalCenter: parent.verticalCenter
+ font.pixelSize: Appearance.font.pixelSize.smaller
+ color: Appearance.m3colors.m3inverseOnSurface
+ wrapMode: Text.Wrap
+ text: StringUtils.format(qsTr("{0} queries pending"), Booru.runningRequests)
+ }
+ }
+ }
+ }
+
+ Item { // Tag suggestion description
+ visible: tagDescriptionText.text.length > 0
+ Layout.fillWidth: true
+ implicitHeight: tagDescriptionBackground.implicitHeight
+
+ Rectangle {
+ id: tagDescriptionBackground
+ color: Appearance.colors.colTooltip
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ implicitHeight: tagDescriptionText.implicitHeight + 5 * 2
+ radius: Appearance.rounding.verysmall
+
+ StyledText {
+ id: tagDescriptionText
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: 10
+ anchors.rightMargin: 10
+ anchors.verticalCenter: parent.verticalCenter
+ font.pixelSize: Appearance.font.pixelSize.smaller
+ color: Appearance.colors.colOnTooltip
+ wrapMode: Text.Wrap
+ text: root.suggestionList[tagSuggestions.selectedIndex]?.description ?? ""
+ }
+ }
+ }
+
+ FlowButtonGroup { // Tag suggestions
+ id: tagSuggestions
+ visible: root.suggestionList.length > 0 && tagInputField.text.length > 0
+ property int selectedIndex: 0
+ Layout.fillWidth: true
+ spacing: 5
+
+ Repeater {
+ id: tagSuggestionRepeater
+ model: {
+ tagSuggestions.selectedIndex = 0
+ return root.suggestionList.slice(0, 10)
+ }
+ delegate: ApiCommandButton {
+ id: tagButton
+ colBackground: tagSuggestions.selectedIndex === index ? Appearance.colors.colLayer2Hover : Appearance.colors.colLayer2
+ bounce: false
+ contentItem: RowLayout {
+ anchors.centerIn: parent
+ spacing: 5
+ StyledText {
+ Layout.fillWidth: false
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: Appearance.m3colors.m3onSurface
+ horizontalAlignment: Text.AlignRight
+ text: modelData.displayName ?? modelData.name
+ }
+ StyledText {
+ Layout.fillWidth: false
+ visible: modelData.count !== undefined
+ font.pixelSize: Appearance.font.pixelSize.smaller
+ color: Appearance.m3colors.m3outline
+ horizontalAlignment: Text.AlignLeft
+ text: modelData.count ?? ""
+ }
+ }
+
+ onHoveredChanged: {
+ if (tagButton.hovered) {
+ tagSuggestions.selectedIndex = index;
+ }
+ }
+ onClicked: {
+ tagSuggestions.acceptTag(modelData.name)
+ }
+ }
+ }
+
+ function acceptTag(tag) {
+ const words = tagInputField.text.trim().split(/\s+/);
+ if (words.length > 0) {
+ words[words.length - 1] = tag;
+ } else {
+ words.push(tag);
+ }
+ const updatedText = words.join(" ") + " ";
+ tagInputField.text = updatedText;
+ tagInputField.cursorPosition = tagInputField.text.length;
+ tagInputField.forceActiveFocus();
+ }
+
+ function acceptSelectedTag() {
+ if (tagSuggestions.selectedIndex >= 0 && tagSuggestions.selectedIndex < tagSuggestionRepeater.count) {
+ const tag = root.suggestionList[tagSuggestions.selectedIndex].name;
+ tagSuggestions.acceptTag(tag);
+ }
+ }
+ }
+
+ Rectangle { // Tag input area
+ id: tagInputContainer
+ property real columnSpacing: 5
+ Layout.fillWidth: true
+ radius: Appearance.rounding.small
+ color: Appearance.colors.colLayer1
+ implicitWidth: tagInputField.implicitWidth
+ implicitHeight: Math.max(inputFieldRowLayout.implicitHeight + inputFieldRowLayout.anchors.topMargin
+ + commandButtonsRow.implicitHeight + commandButtonsRow.anchors.bottomMargin + columnSpacing, 45)
+ clip: true
+ border.color: Appearance.colors.colOutlineVariant
+ border.width: 1
+
+ Behavior on implicitHeight {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+
+ RowLayout { // Input field and send button
+ id: inputFieldRowLayout
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: 5
+ spacing: 0
+
+ StyledTextArea { // The actual TextArea
+ id: tagInputField
+ wrapMode: TextArea.Wrap
+ Layout.fillWidth: true
+ padding: 10
+ color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
+ renderType: Text.NativeRendering
+ placeholderText: StringUtils.format(qsTr('Enter tags, or "{0}" for commands'), root.commandPrefix)
+
+ background: null
+
+ property Timer searchTimer: Timer { // Timer for tag suggestions
+ interval: root.tagSuggestionDelay
+ repeat: false
+ onTriggered: {
+ const inputText = tagInputField.text
+ const words = inputText.trim().split(/\s+/);
+ if (words.length > 0) {
+ Booru.triggerTagSearch(words[words.length - 1]);
+ }
+ }
+ }
+
+ onTextChanged: { // Handle tag suggestions
+ if(tagInputField.text.length === 0) {
+ root.suggestionQuery = ""
+ root.suggestionList = []
+ searchTimer.stop();
+ return
+ }
+ if(tagInputField.text.startsWith(`${root.commandPrefix}mode`)) {
+ root.suggestionQuery = tagInputField.text.split(" ")[1] ?? ""
+ const providerResults = Fuzzy.go(root.suggestionQuery, Booru.providerList.map(provider => {
+ return {
+ name: Fuzzy.prepare(provider),
+ obj: provider,
+ }
+ }), {
+ all: true,
+ key: "name"
+ })
+ root.suggestionList = providerResults.map(provider => {
+ return {
+ name: `${tagInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "mode ") : ""}${provider.target}`,
+ displayName: `${Booru.providers[provider.target].name}`,
+ description: `${Booru.providers[provider.target].description}`,
+ }
+ })
+ searchTimer.stop();
+ return
+ }
+ if(tagInputField.text.startsWith(root.commandPrefix)) {
+ root.suggestionQuery = tagInputField.text
+ root.suggestionList = root.allCommands.filter(cmd => cmd.name.startsWith(tagInputField.text.substring(1))).map(cmd => {
+ return {
+ name: `${root.commandPrefix}${cmd.name}`,
+ description: `${cmd.description}`,
+ }
+ })
+ searchTimer.stop();
+ return
+ }
+ searchTimer.restart();
+ }
+
+ function accept() {
+ root.handleInput(text)
+ text = ""
+ }
+
+ Keys.onPressed: (event) => {
+ if (event.key === Qt.Key_Tab) {
+ tagSuggestions.acceptSelectedTag();
+ event.accepted = true;
+ } else if (event.key === Qt.Key_Up) {
+ tagSuggestions.selectedIndex = Math.max(0, tagSuggestions.selectedIndex - 1);
+ event.accepted = true;
+ } else if (event.key === Qt.Key_Down) {
+ tagSuggestions.selectedIndex = Math.min(root.suggestionList.length - 1, tagSuggestions.selectedIndex + 1);
+ event.accepted = true;
+ } else if ((event.key === Qt.Key_Enter || event.key === Qt.Key_Return)) {
+ if (event.modifiers & Qt.ShiftModifier) {
+ // Insert newline
+ tagInputField.insert(tagInputField.cursorPosition, "\n")
+ event.accepted = true
+ } else { // Accept text
+ const inputText = tagInputField.text
+ root.handleInput(inputText)
+ tagInputField.clear()
+ event.accepted = true
+ }
+ }
+ }
+ }
+
+ RippleButton { // Send button
+ id: sendButton
+ Layout.alignment: Qt.AlignTop
+ Layout.rightMargin: 5
+ implicitWidth: 40
+ implicitHeight: 40
+ buttonRadius: Appearance.rounding.small
+ enabled: tagInputField.text.length > 0
+ toggled: enabled
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: sendButton.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
+ onClicked: {
+ const inputText = tagInputField.text
+ root.handleInput(inputText)
+ tagInputField.clear()
+ }
+ }
+
+ contentItem: MaterialSymbol {
+ anchors.centerIn: parent
+ horizontalAlignment: Text.AlignHCenter
+ iconSize: Appearance.font.pixelSize.larger
+ // fill: sendButton.enabled ? 1 : 0
+ color: sendButton.enabled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2Disabled
+ text: "send"
+ }
+ }
+ }
+
+ RowLayout { // Controls
+ id: commandButtonsRow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: 5
+ anchors.leftMargin: 5
+ anchors.rightMargin: 5
+ spacing: 5
+
+ property var commandsShown: [
+ {
+ name: "mode",
+ sendDirectly: false,
+ },
+ {
+ name: "clear",
+ sendDirectly: true,
+ },
+ ]
+
+ Item {
+ implicitHeight: providerRowLayout.implicitHeight + 5 * 2
+ implicitWidth: providerRowLayout.implicitWidth + 10 * 2
+
+ RowLayout {
+ id: providerRowLayout
+ anchors.centerIn: parent
+
+ MaterialSymbol {
+ text: "api"
+ iconSize: Appearance.font.pixelSize.large
+ }
+ StyledText {
+ id: providerName
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: Appearance.m3colors.m3onSurface
+ text: Booru.providers[Booru.currentProvider].name
+ }
+ }
+ StyledToolTip {
+ id: toolTip
+ extraVisibleCondition: false
+ alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered
+ // content: qsTr("The current API used. Endpoint: ") + Booru.providers[Booru.currentProvider].url + qsTr("\nSet with /mode PROVIDER")
+ content: StringUtils.format(qsTr("Current API endpoint: {0}\nSet it with {1}mode PROVIDER"),
+ Booru.providers[Booru.currentProvider].url, root.commandPrefix)
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ }
+ }
+
+ StyledText {
+ font.pixelSize: Appearance.font.pixelSize.large
+ color: Appearance.colors.colOnLayer1
+ text: "•"
+ }
+
+ Item { // NSFW toggle
+ visible: width > 0
+ implicitWidth: switchesRow.implicitWidth
+ Layout.fillHeight: true
+
+ RowLayout {
+ id: switchesRow
+ spacing: 5
+ anchors.centerIn: parent
+
+ MouseArea {
+ hoverEnabled: true
+ PointingHandInteraction {}
+ onClicked: {
+ nsfwSwitch.checked = !nsfwSwitch.checked
+ }
+ }
+
+ StyledText {
+ Layout.fillHeight: true
+ Layout.leftMargin: 10
+ Layout.alignment: Qt.AlignVCenter
+ font.pixelSize: Appearance.font.pixelSize.smaller
+ color: nsfwSwitch.enabled ? Appearance.colors.colOnLayer1 : Appearance.m3colors.m3outline
+ text: qsTr("Allow NSFW")
+ }
+ StyledSwitch {
+ id: nsfwSwitch
+ enabled: Booru.currentProvider !== "zerochan"
+ scale: 0.6
+ Layout.alignment: Qt.AlignVCenter
+ checked: (PersistentStates.booru.allowNsfw && Booru.currentProvider !== "zerochan")
+ onCheckedChanged: {
+ if (!nsfwSwitch.enabled) return;
+ PersistentStateManager.setState("booru.allowNsfw", checked)
+ }
+ }
+ }
+ }
+
+ Item { Layout.fillWidth: true }
+
+ ButtonGroup {
+ padding: 0
+ Repeater { // Command buttons
+ id: commandRepeater
+ model: commandButtonsRow.commandsShown
+ delegate: ApiCommandButton {
+ id: tagButton
+ property string commandRepresentation: `${root.commandPrefix}${modelData.name}`
+ buttonText: commandRepresentation
+ colBackground: Appearance.colors.colLayer2
+
+ onClicked: {
+ if(modelData.sendDirectly) {
+ root.handleInput(commandRepresentation)
+ } else {
+ tagInputField.text = commandRepresentation + " "
+ tagInputField.cursorPosition = tagInputField.text.length
+ tagInputField.forceActiveFocus()
+ }
+ if (modelData.name === "clear") {
+ tagInputField.text = ""
+ }
+ }
+ }
+ }
+ }
+ }
+
+ }
+ }
+}
diff --git a/.config/quickshell/modules/sidebarLeft/ApiCommandButton.qml b/.config/quickshell/modules/sidebarLeft/ApiCommandButton.qml
new file mode 100644
index 000000000..b9fab2961
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/ApiCommandButton.qml
@@ -0,0 +1,29 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+
+GroupButton {
+ id: button
+ property string buttonText
+
+ horizontalPadding: 8
+ verticalPadding: 6
+
+ baseWidth: contentItem.implicitWidth + horizontalPadding * 2
+ clickedWidth: baseWidth + 20
+ baseHeight: contentItem.implicitHeight + verticalPadding * 2
+
+ colBackground: Appearance.colors.colLayer2
+ colBackgroundHover: Appearance.colors.colLayer2Hover
+ colBackgroundActive: Appearance.colors.colLayer2Active
+
+ contentItem: StyledText {
+ horizontalAlignment: Text.AlignHCenter
+ text: buttonText
+ color: Appearance.m3colors.m3onSurface
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml b/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml
new file mode 100644
index 000000000..ce0b22802
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml
@@ -0,0 +1,205 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Effects
+import Qt5Compat.GraphicalEffects
+import Quickshell.Io
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Scope { // Scope
+ id: root
+ property int sidebarPadding: 15
+ property bool detach: false
+ property Component contentComponent: SidebarLeftContent {}
+ property Item sidebarContent
+
+ Component.onCompleted: {
+ root.sidebarContent = contentComponent.createObject(null, {
+ "scopeRoot": root,
+ });
+ sidebarLoader.item.contentParent.children = [root.sidebarContent];
+ }
+
+ onDetachChanged: {
+ if (root.detach) {
+ sidebarContent.parent = null; // Detach content from sidebar
+ sidebarLoader.active = false; // Unload sidebar
+ detachedSidebarLoader.active = true; // Load detached window
+ detachedSidebarLoader.item.contentParent.children = [sidebarContent];
+ } else {
+ sidebarContent.parent = null; // Detach content from window
+ detachedSidebarLoader.active = false; // Unload detached window
+ sidebarLoader.active = true; // Load sidebar
+ sidebarLoader.item.contentParent.children = [sidebarContent];
+ }
+ }
+
+ Loader {
+ id: sidebarLoader
+ active: true
+
+ sourceComponent: PanelWindow { // Window
+ id: sidebarRoot
+ visible: GlobalStates.sidebarLeftOpen
+
+ property bool extend: false
+ property real sidebarWidth: sidebarRoot.extend ? Appearance.sizes.sidebarWidthExtended : Appearance.sizes.sidebarWidth
+ property var contentParent: sidebarLeftBackground
+
+ function hide() {
+ GlobalStates.sidebarLeftOpen = false
+ }
+
+ exclusiveZone: 0
+ implicitWidth: Appearance.sizes.sidebarWidthExtended + Appearance.sizes.elevationMargin
+ WlrLayershell.namespace: "quickshell:sidebarLeft"
+ // Hyprland 0.49: OnDemand is Exclusive, Exclusive just breaks click-outside-to-close
+ // WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
+ color: "transparent"
+
+ anchors {
+ top: true
+ left: true
+ bottom: true
+ }
+
+ mask: Region {
+ item: sidebarLeftBackground
+ }
+
+ HyprlandFocusGrab { // Click outside to close
+ id: grab
+ windows: [ sidebarRoot ]
+ active: sidebarRoot.visible
+ onActiveChanged: { // Focus the selected tab
+ if (active) sidebarLeftBackground.children[0].focusActiveItem()
+ }
+ onCleared: () => {
+ if (!active) sidebarRoot.hide()
+ }
+ }
+
+ // Content
+ StyledRectangularShadow {
+ target: sidebarLeftBackground
+ radius: sidebarLeftBackground.radius
+ }
+ Rectangle {
+ id: sidebarLeftBackground
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.topMargin: Appearance.sizes.hyprlandGapsOut
+ anchors.leftMargin: Appearance.sizes.hyprlandGapsOut
+ width: sidebarRoot.sidebarWidth - Appearance.sizes.hyprlandGapsOut - Appearance.sizes.elevationMargin
+ height: parent.height - Appearance.sizes.hyprlandGapsOut * 2
+ color: Appearance.colors.colLayer0
+ radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
+
+ Behavior on width {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+
+ Keys.onPressed: (event) => {
+ if (event.key === Qt.Key_Escape) {
+ sidebarRoot.hide();
+ }
+ if (event.modifiers === Qt.ControlModifier) {
+ if (event.key === Qt.Key_O) {
+ sidebarRoot.extend = !sidebarRoot.extend;
+ }
+ else if (event.key === Qt.Key_P) {
+ root.detach = !root.detach;
+ }
+ event.accepted = true;
+ }
+ }
+ }
+ }
+ }
+
+ Loader {
+ id: detachedSidebarLoader
+ active: false
+
+ sourceComponent: FloatingWindow {
+ id: detachedSidebarRoot
+ visible: GlobalStates.sidebarLeftOpen
+ property var contentParent: detachedSidebarBackground
+
+ Rectangle {
+ id: detachedSidebarBackground
+ anchors.fill: parent
+ color: Appearance.colors.colLayer0
+
+ Keys.onPressed: (event) => {
+ if (event.modifiers === Qt.ControlModifier) {
+ if (event.key === Qt.Key_P) {
+ root.detach = !root.detach;
+ }
+ event.accepted = true;
+ }
+ }
+ }
+ }
+ }
+
+ IpcHandler {
+ target: "sidebarLeft"
+
+ function toggle(): void {
+ GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen
+ }
+
+ function close(): void {
+ GlobalStates.sidebarLeftOpen = false
+ }
+
+ function open(): void {
+ GlobalStates.sidebarLeftOpen = true
+ }
+ }
+
+ GlobalShortcut {
+ name: "sidebarLeftToggle"
+ description: qsTr("Toggles left sidebar on press")
+
+ onPressed: {
+ GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
+ }
+ }
+
+ GlobalShortcut {
+ name: "sidebarLeftOpen"
+ description: qsTr("Opens left sidebar on press")
+
+ onPressed: {
+ GlobalStates.sidebarLeftOpen = true;
+ }
+ }
+
+ GlobalShortcut {
+ name: "sidebarLeftClose"
+ description: qsTr("Closes left sidebar on press")
+
+ onPressed: {
+ GlobalStates.sidebarLeftOpen = false;
+ }
+ }
+
+ GlobalShortcut {
+ name: "sidebarLeftToggleDetach"
+ description: qsTr("Detach left sidebar into a window/Attach it back")
+
+ onPressed: {
+ root.detach = !root.detach;
+ }
+ }
+
+}
diff --git a/.config/quickshell/modules/sidebarLeft/SidebarLeftContent.qml b/.config/quickshell/modules/sidebarLeft/SidebarLeftContent.qml
new file mode 100644
index 000000000..b0358e7b4
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/SidebarLeftContent.qml
@@ -0,0 +1,96 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Effects
+import Qt5Compat.GraphicalEffects
+import Quickshell.Io
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Item {
+ id: root
+ required property var scopeRoot
+ anchors.fill: parent
+ property var tabButtonList: [
+ {"icon": "neurology", "name": qsTr("Intelligence")},
+ {"icon": "translate", "name": qsTr("Translator")},
+ {"icon": "bookmark_heart", "name": qsTr("Anime")},
+ ]
+ property int selectedTab: 0
+
+ function focusActiveItem() {
+ swipeView.currentItem.forceActiveFocus()
+ }
+
+ Keys.onPressed: (event) => {
+ if (event.modifiers === Qt.ControlModifier) {
+ if (event.key === Qt.Key_PageDown) {
+ root.selectedTab = Math.min(root.selectedTab + 1, root.tabButtonList.length - 1)
+ event.accepted = true;
+ }
+ else if (event.key === Qt.Key_PageUp) {
+ root.selectedTab = Math.max(root.selectedTab - 1, 0)
+ event.accepted = true;
+ }
+ else if (event.key === Qt.Key_Tab) {
+ root.selectedTab = (root.selectedTab + 1) % root.tabButtonList.length;
+ event.accepted = true;
+ }
+ else if (event.key === Qt.Key_Backtab) {
+ root.selectedTab = (root.selectedTab - 1 + root.tabButtonList.length) % root.tabButtonList.length;
+ event.accepted = true;
+ }
+ }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: sidebarPadding
+
+ spacing: sidebarPadding
+
+ PrimaryTabBar { // Tab strip
+ id: tabBar
+ tabButtonList: root.tabButtonList
+ externalTrackedTab: root.selectedTab
+ function onCurrentIndexChanged(currentIndex) {
+ root.selectedTab = currentIndex
+ }
+ }
+
+ SwipeView { // Content pages
+ id: swipeView
+ Layout.topMargin: 5
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ spacing: 10
+
+ currentIndex: tabBar.externalTrackedTab
+ onCurrentIndexChanged: {
+ tabBar.enableIndicatorAnimation = true
+ root.selectedTab = currentIndex
+ }
+
+ clip: true
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: swipeView.width
+ height: swipeView.height
+ radius: Appearance.rounding.small
+ }
+ }
+
+ AiChat {}
+ Translator {}
+ Anime {}
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarLeft/Translator.qml b/.config/quickshell/modules/sidebarLeft/Translator.qml
new file mode 100644
index 000000000..37e5a37b1
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/Translator.qml
@@ -0,0 +1,248 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "./translator/"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+
+/**
+ * Translator widget with the `trans` commandline tool.
+ */
+Item {
+ id: root
+ // Widgets
+ property var inputField: inputCanvas.inputTextArea
+ // Widget variables
+ property bool translationFor: false // Indicates if the translation is for an autocorrected text
+ property string translatedText: ""
+ property list languages: []
+ // Options
+ property string targetLanguage: ConfigOptions.language.translator.targetLanguage
+ property string sourceLanguage: ConfigOptions.language.translator.sourceLanguage
+ property string hostLanguage: targetLanguage
+
+ property bool showLanguageSelector: false
+ property bool languageSelectorTarget: false // true for target language, false for source language
+
+ function showLanguageSelectorDialog(isTargetLang: bool) {
+ root.languageSelectorTarget = isTargetLang;
+ root.showLanguageSelector = true
+ }
+
+ onFocusChanged: (focus) => {
+ if (focus) {
+ root.inputField.forceActiveFocus()
+ }
+ }
+
+ Timer {
+ id: translateTimer
+ interval: ConfigOptions.sidebar.translator.delay
+ repeat: false
+ onTriggered: () => {
+ if (root.inputField.text.trim().length > 0) {
+ // console.log("Translating with command:", translateProc.command);
+ translateProc.running = false;
+ translateProc.buffer = ""; // Clear the buffer
+ translateProc.running = true; // Restart the process
+ } else {
+ root.translatedText = "";
+ }
+ }
+ }
+
+ Process {
+ id: translateProc
+ command: ["bash", "-c", `trans -no-theme -no-bidi`
+ + ` -source '${StringUtils.shellSingleQuoteEscape(root.sourceLanguage)}'`
+ + ` -target '${StringUtils.shellSingleQuoteEscape(root.targetLanguage)}'`
+ + ` -no-ansi '${StringUtils.shellSingleQuoteEscape(root.inputField.text.trim())}'`]
+ property string buffer: ""
+ stdout: SplitParser {
+ onRead: data => {
+ translateProc.buffer += data + "\n";
+ }
+ }
+ onExited: (exitCode, exitStatus) => {
+ // 1. Split into sections by double newlines
+ const sections = translateProc.buffer.trim().split(/\n\s*\n/);
+ // console.log("BUFFER:", translateProc.buffer);
+ // console.log("SECTIONS:", sections);
+
+ // 2. Extract relevant data
+ root.translatedText = sections.length > 1 ? sections[1].trim() : "";
+ }
+ }
+
+ Process {
+ id: getLanguagesProc
+ command: ["trans", "-list-languages", "-no-bidi"]
+ property list bufferList: ["auto"]
+ running: true
+ stdout: SplitParser {
+ onRead: data => {
+ getLanguagesProc.bufferList.push(data.trim());
+ }
+ }
+ onExited: (exitCode, exitStatus) => {
+ // Ensure "auto" is always the first language
+ let langs = getLanguagesProc.bufferList
+ .filter(lang => lang.trim().length > 0 && lang !== "auto")
+ .sort((a, b) => a.localeCompare(b));
+ langs.unshift("auto");
+ root.languages = langs;
+ getLanguagesProc.bufferList = []; // Clear the buffer
+ }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ Flickable {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ contentHeight: contentColumn.implicitHeight
+
+ ColumnLayout {
+ id: contentColumn
+ anchors.fill: parent
+
+ LanguageSelectorButton { // Target language button
+ id: targetLanguageButton
+ displayText: root.targetLanguage
+ onClicked: {
+ root.showLanguageSelectorDialog(true);
+ }
+ }
+
+ TextCanvas { // Content translation
+ id: outputCanvas
+ isInput: false
+ placeholderText: qsTr("Translation goes here...")
+ property bool hasTranslation: (root.translatedText.trim().length > 0)
+ text: hasTranslation ? root.translatedText : ""
+ GroupButton {
+ id: copyButton
+ baseWidth: height
+ buttonRadius: Appearance.rounding.small
+ enabled: outputCanvas.displayedText.trim().length > 0
+ contentItem: MaterialSymbol {
+ anchors.centerIn: parent
+ horizontalAlignment: Text.AlignHCenter
+ iconSize: Appearance.font.pixelSize.larger
+ text: "content_copy"
+ color: copyButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext
+ }
+ onClicked: {
+ Quickshell.clipboardText = outputCanvas.displayedText
+ }
+ }
+ GroupButton {
+ id: searchButton
+ baseWidth: height
+ buttonRadius: Appearance.rounding.small
+ enabled: outputCanvas.displayedText.trim().length > 0
+ contentItem: MaterialSymbol {
+ anchors.centerIn: parent
+ horizontalAlignment: Text.AlignHCenter
+ iconSize: Appearance.font.pixelSize.larger
+ text: "travel_explore"
+ color: searchButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext
+ }
+ onClicked: {
+ let url = ConfigOptions.search.engineBaseUrl + outputCanvas.displayedText;
+ for (let site of ConfigOptions.search.excludedSites) {
+ url += ` -site:${site}`;
+ }
+ Qt.openUrlExternally(url);
+ }
+ }
+ }
+
+ }
+ }
+
+ LanguageSelectorButton { // Source language button
+ id: sourceLanguageButton
+ displayText: root.sourceLanguage
+ onClicked: {
+ root.showLanguageSelectorDialog(false);
+ }
+ }
+
+ TextCanvas { // Content input
+ id: inputCanvas
+ isInput: true
+ placeholderText: qsTr("Enter text to translate...")
+ onInputTextChanged: {
+ translateTimer.restart();
+ }
+ GroupButton {
+ id: pasteButton
+ baseWidth: height
+ buttonRadius: Appearance.rounding.small
+ contentItem: MaterialSymbol {
+ anchors.centerIn: parent
+ horizontalAlignment: Text.AlignHCenter
+ iconSize: Appearance.font.pixelSize.larger
+ text: "content_paste"
+ color: deleteButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext
+ }
+ onClicked: {
+ root.inputField.text = Quickshell.clipboardText
+ }
+ }
+ GroupButton {
+ id: deleteButton
+ baseWidth: height
+ buttonRadius: Appearance.rounding.small
+ enabled: inputCanvas.inputTextArea.text.length > 0
+ contentItem: MaterialSymbol {
+ anchors.centerIn: parent
+ horizontalAlignment: Text.AlignHCenter
+ iconSize: Appearance.font.pixelSize.larger
+ text: "close"
+ color: deleteButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext
+ }
+ onClicked: {
+ root.inputField.text = ""
+ }
+ }
+ }
+ }
+
+ Loader {
+ anchors.fill: parent
+ active: root.showLanguageSelector
+ visible: root.showLanguageSelector
+ z: 9999
+ sourceComponent: SelectionDialog {
+ id: languageSelectorDialog
+ titleText: qsTr("Select Language")
+ items: root.languages
+ defaultChoice: root.languageSelectorTarget ? root.targetLanguage : root.sourceLanguage
+ onCanceled: () => {
+ root.showLanguageSelector = false;
+ }
+ onSelected: (result) => {
+ root.showLanguageSelector = false;
+ if (!result || result.length === 0) return; // No selection made
+
+ if (root.languageSelectorTarget) {
+ root.targetLanguage = result;
+ ConfigLoader.setConfigValueAndSave("language.translator.targetLanguage", result); // Save to config
+ } else {
+ root.sourceLanguage = result;
+ ConfigLoader.setConfigValueAndSave("language.translator.sourceLanguage", result); // Save to config
+ }
+
+ translateTimer.restart(); // Restart translation after language change
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/AiMessage.qml b/.config/quickshell/modules/sidebarLeft/aiChat/AiMessage.qml
new file mode 100644
index 000000000..8d7952110
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/aiChat/AiMessage.qml
@@ -0,0 +1,298 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "../"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+import Qt5Compat.GraphicalEffects
+import org.kde.syntaxhighlighting
+
+Rectangle {
+ id: root
+ property int messageIndex
+ property var messageData
+ property var messageInputField
+
+ property real messagePadding: 7
+ property real contentSpacing: 3
+
+ property bool enableMouseSelection: false
+ property bool renderMarkdown: true
+ property bool editing: false
+
+ property list messageBlocks: StringUtils.splitMarkdownBlocks(root.messageData?.content)
+
+ anchors.left: parent?.left
+ anchors.right: parent?.right
+ implicitHeight: columnLayout.implicitHeight + root.messagePadding * 2
+
+ radius: Appearance.rounding.normal
+ color: Appearance.colors.colLayer1
+
+ function saveMessage() {
+ if (!root.editing) return;
+ // Get all Loader children (each represents a segment)
+ const segments = messageContentColumnLayout.children
+ .map(child => child.segment)
+ .filter(segment => (segment));
+
+ // Reconstruct markdown
+ const newContent = segments.map(segment => {
+ if (segment.type === "code") {
+ const lang = segment.lang ? segment.lang : "";
+ // Remove trailing newlines
+ const code = segment.content.replace(/\n+$/, "");
+ return "```" + lang + "\n" + code + "\n```";
+ } else {
+ return segment.content;
+ }
+ }).join("");
+
+ root.editing = false
+ root.messageData.content = newContent;
+ }
+
+ Keys.onPressed: (event) => {
+ if ( // Prevent de-select
+ event.key === Qt.Key_Control ||
+ event.key == Qt.Key_Shift ||
+ event.key == Qt.Key_Alt ||
+ event.key == Qt.Key_Meta
+ ) {
+ event.accepted = true
+ }
+ // Ctrl + S to save
+ if ((event.key === Qt.Key_S) && event.modifiers == Qt.ControlModifier) {
+ root.saveMessage();
+ event.accepted = true;
+ }
+ }
+
+ ColumnLayout { // Main layout of the whole thing
+ id: columnLayout
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: messagePadding
+ spacing: root.contentSpacing
+
+ RowLayout { // Header
+ spacing: 15
+ Layout.fillWidth: true
+
+ Rectangle { // Name
+ id: nameWrapper
+ color: Appearance.colors.colSecondaryContainer
+ // color: "transparent"
+ radius: Appearance.rounding.small
+ implicitHeight: Math.max(nameRowLayout.implicitHeight + 5 * 2, 30)
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignVCenter
+
+ RowLayout {
+ id: nameRowLayout
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: 10
+ anchors.rightMargin: 10
+ spacing: 7
+
+ Item {
+ Layout.alignment: Qt.AlignVCenter
+ Layout.fillHeight: true
+ implicitWidth: messageData?.role == 'assistant' ? modelIcon.width : roleIcon.implicitWidth
+ implicitHeight: messageData?.role == 'assistant' ? modelIcon.height : roleIcon.implicitHeight
+
+ CustomIcon {
+ id: modelIcon
+ anchors.centerIn: parent
+ visible: messageData?.role == 'assistant' && Ai.models[messageData?.model].icon
+ width: Appearance.font.pixelSize.large
+ height: Appearance.font.pixelSize.large
+ source: messageData?.role == 'assistant' ? Ai.models[messageData?.model].icon :
+ messageData?.role == 'user' ? 'linux-symbolic' : 'desktop-symbolic'
+ }
+ ColorOverlay {
+ visible: modelIcon.visible
+ anchors.fill: modelIcon
+ source: modelIcon
+ color: Appearance.m3colors.m3onSecondaryContainer
+ }
+
+ MaterialSymbol {
+ id: roleIcon
+ anchors.centerIn: parent
+ visible: !modelIcon.visible
+ iconSize: Appearance.font.pixelSize.larger
+ color: Appearance.m3colors.m3onSecondaryContainer
+ text: messageData?.role == 'user' ? 'person' :
+ messageData?.role == 'interface' ? 'settings' :
+ messageData?.role == 'assistant' ? 'neurology' :
+ 'computer'
+ }
+ }
+
+ StyledText {
+ id: providerName
+ Layout.alignment: Qt.AlignVCenter
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ font.pixelSize: Appearance.font.pixelSize.normal
+ color: Appearance.m3colors.m3onSecondaryContainer
+ text: messageData?.role == 'assistant' ? Ai.models[messageData?.model].name :
+ (messageData?.role == 'user' && SystemInfo.username) ? SystemInfo.username :
+ qsTr("Interface")
+ }
+ }
+ }
+
+ Button { // Not visible to model
+ id: modelVisibilityIndicator
+ visible: messageData?.role == 'interface'
+ implicitWidth: 16
+ implicitHeight: 30
+ Layout.alignment: Qt.AlignVCenter
+
+ background: Item
+
+ MaterialSymbol {
+ id: notVisibleToModelText
+ anchors.centerIn: parent
+ iconSize: Appearance.font.pixelSize.small
+ color: Appearance.colors.colSubtext
+ text: "visibility_off"
+ }
+ StyledToolTip {
+ content: qsTr("Not visible to model")
+ }
+ }
+
+ ButtonGroup {
+ spacing: 5
+
+ AiMessageControlButton {
+ id: copyButton
+ buttonIcon: activated ? "inventory" : "content_copy"
+
+ onClicked: {
+ Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(root.messageData?.content)}'`)
+ copyButton.activated = true
+ copyIconTimer.restart()
+ }
+
+ Timer {
+ id: copyIconTimer
+ interval: 1500
+ repeat: false
+ onTriggered: {
+ copyButton.activated = false
+ }
+ }
+
+ StyledToolTip {
+ content: qsTr("Copy")
+ }
+ }
+ AiMessageControlButton {
+ id: editButton
+ activated: root.editing
+ enabled: root.messageData?.done ?? false
+ buttonIcon: "edit"
+ onClicked: {
+ root.editing = !root.editing
+ if (!root.editing) { // Save changes
+ root.saveMessage()
+ }
+ }
+ StyledToolTip {
+ content: root.editing ? qsTr("Save") : qsTr("Edit")
+ }
+ }
+ AiMessageControlButton {
+ id: toggleMarkdownButton
+ activated: !root.renderMarkdown
+ buttonIcon: "code"
+ onClicked: {
+ root.renderMarkdown = !root.renderMarkdown
+ }
+ StyledToolTip {
+ content: qsTr("View Markdown source")
+ }
+ }
+ AiMessageControlButton {
+ id: deleteButton
+ buttonIcon: "close"
+ onClicked: {
+ Ai.removeMessage(root.messageIndex)
+ }
+ StyledToolTip {
+ content: qsTr("Delete")
+ }
+ }
+ }
+ }
+
+ ColumnLayout { // Message content
+ id: messageContentColumnLayout
+
+ spacing: 0
+ Repeater {
+ model: ScriptModel {
+ values: root.messageBlocks.map((block, index) => index)
+ }
+ delegate: Loader {
+ required property int index
+ property var thisBlock: root.messageBlocks[index]
+ Layout.fillWidth: true
+ // property var segment: thisBlock
+ property var segmentContent: thisBlock.content
+ property var segmentLang: thisBlock.lang
+ property var messageData: root.messageData
+ property var editing: root.editing
+ property var renderMarkdown: root.renderMarkdown
+ property var enableMouseSelection: root.enableMouseSelection
+ property bool thinking: root.messageData?.thinking ?? true
+ property bool done: root.messageData?.done ?? false
+ property bool completed: thisBlock.completed ?? false
+
+ source: thisBlock.type === "code" ? "MessageCodeBlock.qml" :
+ thisBlock.type === "think" ? "MessageThinkBlock.qml" :
+ "MessageTextBlock.qml"
+
+ }
+ }
+ }
+
+ Flow { // Annotations
+ id: annotationFlowLayout
+ visible: root.messageData?.annotationSources?.length > 0
+ spacing: 5
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignLeft
+
+ Repeater {
+ model: ScriptModel {
+ values: root.messageData?.annotationSources || []
+ }
+ delegate: AnnotationSourceButton {
+ id: annotationButton
+ displayText: modelData.text
+ url: modelData.url
+ }
+ }
+
+ }
+
+ }
+}
+
diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/AiMessageControlButton.qml b/.config/quickshell/modules/sidebarLeft/aiChat/AiMessageControlButton.qml
new file mode 100644
index 000000000..2d7cb82df
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/aiChat/AiMessageControlButton.qml
@@ -0,0 +1,30 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+
+GroupButton {
+ id: button
+ property string buttonIcon
+ property bool activated: false
+ toggled: activated
+
+ baseWidth: height
+
+ contentItem: MaterialSymbol {
+ horizontalAlignment: Text.AlignHCenter
+ iconSize: Appearance.font.pixelSize.larger
+ text: buttonIcon
+ color: button.activated ? Appearance.m3colors.m3onPrimary :
+ button.enabled ? Appearance.m3colors.m3onSurface :
+ Appearance.colors.colOnLayer1Inactive
+
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+}
diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/AnnotationSourceButton.qml b/.config/quickshell/modules/sidebarLeft/aiChat/AnnotationSourceButton.qml
new file mode 100644
index 000000000..a253a291e
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/aiChat/AnnotationSourceButton.qml
@@ -0,0 +1,57 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import Qt5Compat.GraphicalEffects
+import Qt.labs.platform
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell.Widgets
+import Quickshell.Hyprland
+
+RippleButton {
+ id: root
+ property string displayText
+ property string url
+
+ property real faviconSize: 20
+ implicitHeight: 30
+ leftPadding: (implicitHeight - faviconSize) / 2
+ rightPadding: 10
+ buttonRadius: Appearance.rounding.full
+ colBackground: Appearance.colors.colSurfaceContainerHighest
+ colBackgroundHover: Appearance.colors.colSurfaceContainerHighestHover
+ colRipple: Appearance.colors.colSurfaceContainerHighestActive
+
+ PointingHandInteraction {}
+ onClicked: {
+ if (url) {
+ Qt.openUrlExternally(url)
+ Hyprland.dispatch("global quickshell:sidebarLeftClose")
+ }
+ }
+
+ contentItem: Item {
+ anchors.centerIn: parent
+ implicitWidth: rowLayout.implicitWidth
+ implicitHeight: rowLayout.implicitHeight
+ RowLayout {
+ id: rowLayout
+ anchors.fill: parent
+ spacing: 5
+ Favicon {
+ url: root.url
+ size: root.faviconSize
+ displayText: root.displayText
+ }
+ StyledText {
+ id: text
+ horizontalAlignment: Text.AlignHCenter
+ text: displayText
+ color: Appearance.m3colors.m3onSurface
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/MessageCodeBlock.qml b/.config/quickshell/modules/sidebarLeft/aiChat/MessageCodeBlock.qml
new file mode 100644
index 000000000..ea7bb0ee3
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/aiChat/MessageCodeBlock.qml
@@ -0,0 +1,256 @@
+pragma ComponentBehavior: Bound
+
+import "root:/"
+import "root:/services"
+import "root:/modules/common/"
+import "root:/modules/common/widgets"
+import "../"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/file_utils.js" as FileUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Hyprland
+import Qt5Compat.GraphicalEffects
+import org.kde.syntaxhighlighting
+
+ColumnLayout {
+ // These are needed on the parent loader
+ property bool editing: parent?.editing ?? false
+ property bool renderMarkdown: parent?.renderMarkdown ?? true
+ property bool enableMouseSelection: parent?.enableMouseSelection ?? false
+ property var segmentContent: parent?.segmentContent ?? ({})
+ property var segmentLang: parent?.segmentLang ?? "txt"
+ property var messageData: parent?.messageData ?? {}
+
+ property real codeBlockBackgroundRounding: Appearance.rounding.small
+ property real codeBlockHeaderPadding: 3
+ property real codeBlockComponentSpacing: 2
+
+ spacing: codeBlockComponentSpacing
+ anchors.left: parent.left
+ anchors.right: parent.right
+
+ Rectangle { // Code background
+ Layout.fillWidth: true
+ topLeftRadius: codeBlockBackgroundRounding
+ topRightRadius: codeBlockBackgroundRounding
+ bottomLeftRadius: Appearance.rounding.unsharpen
+ bottomRightRadius: Appearance.rounding.unsharpen
+ color: Appearance.colors.colSurfaceContainerHighest
+ implicitHeight: codeBlockTitleBarRowLayout.implicitHeight + codeBlockHeaderPadding * 2
+
+ RowLayout { // Language and buttons
+ id: codeBlockTitleBarRowLayout
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: codeBlockHeaderPadding
+ anchors.rightMargin: codeBlockHeaderPadding
+ spacing: 5
+
+ StyledText {
+ id: codeBlockLanguage
+ Layout.alignment: Qt.AlignLeft
+ Layout.fillWidth: false
+ Layout.topMargin: 7
+ Layout.bottomMargin: 7
+ Layout.leftMargin: 10
+ font.pixelSize: Appearance.font.pixelSize.small
+ font.weight: Font.DemiBold
+ color: Appearance.colors.colOnLayer2
+ text: segmentLang ? Repository.definitionForName(segmentLang).name : "plain"
+ }
+
+ Item { Layout.fillWidth: true }
+
+ ButtonGroup {
+ AiMessageControlButton {
+ id: copyCodeButton
+ buttonIcon: activated ? "inventory" : "content_copy"
+
+ onClicked: {
+ Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(segmentContent)}'`)
+ copyCodeButton.activated = true
+ copyIconTimer.restart()
+ }
+
+ Timer {
+ id: copyIconTimer
+ interval: 1500
+ repeat: false
+ onTriggered: {
+ copyCodeButton.activated = false
+ }
+ }
+ StyledToolTip {
+ content: qsTr("Copy code")
+ }
+ }
+ AiMessageControlButton {
+ id: saveCodeButton
+ buttonIcon: activated ? "check" : "save"
+
+ onClicked: {
+ const downloadPath = FileUtils.trimFileProtocol(Directories.downloads)
+ Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(segmentContent)}' > '${downloadPath}/code.${segmentLang || "txt"}'`)
+ Hyprland.dispatch(`exec notify-send 'Code saved to file' '${downloadPath}/code.${segmentLang || "txt"}' -a Shell`)
+ saveCodeButton.activated = true
+ saveIconTimer.restart()
+ }
+
+ Timer {
+ id: saveIconTimer
+ interval: 1500
+ repeat: false
+ onTriggered: {
+ saveCodeButton.activated = false
+ }
+ }
+ StyledToolTip {
+ content: qsTr("Save to Downloads")
+ }
+ }
+ }
+ }
+ }
+
+ RowLayout { // Line numbers and code
+ spacing: codeBlockComponentSpacing
+
+ Rectangle { // Line numbers
+ implicitWidth: 40
+ Layout.fillHeight: true
+ Layout.fillWidth: false
+ topLeftRadius: Appearance.rounding.unsharpen
+ bottomLeftRadius: codeBlockBackgroundRounding
+ topRightRadius: Appearance.rounding.unsharpen
+ bottomRightRadius: Appearance.rounding.unsharpen
+ color: Appearance.colors.colLayer2
+
+ ColumnLayout {
+ id: lineNumberColumnLayout
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.rightMargin: 5
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 0
+
+ Repeater {
+ model: codeTextArea.text.split("\n").length
+ Text {
+ required property int index
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignRight
+ font.family: Appearance.font.family.monospace
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: Appearance.colors.colSubtext
+ horizontalAlignment: Text.AlignRight
+ text: index + 1
+ }
+ }
+ }
+ }
+
+ Rectangle { // Code background
+ Layout.fillWidth: true
+ topLeftRadius: Appearance.rounding.unsharpen
+ bottomLeftRadius: Appearance.rounding.unsharpen
+ topRightRadius: Appearance.rounding.unsharpen
+ bottomRightRadius: codeBlockBackgroundRounding
+ color: Appearance.colors.colLayer2
+ implicitHeight: codeTextArea.implicitHeight
+
+ ScrollView {
+ id: codeScrollView
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ implicitWidth: parent.width
+ implicitHeight: codeTextArea.implicitHeight + 1
+ contentWidth: codeTextArea.width - 1
+ // contentHeight: codeTextArea.contentHeight
+ clip: true
+ ScrollBar.vertical.policy: ScrollBar.AlwaysOff
+
+ ScrollBar.horizontal: ScrollBar {
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ padding: 5
+ policy: ScrollBar.AsNeeded
+ opacity: visualSize == 1 ? 0 : 1
+ visible: opacity > 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Appearance.animation.elementMoveFast.duration
+ easing.type: Appearance.animation.elementMoveFast.type
+ easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
+ }
+ }
+
+ contentItem: Rectangle {
+ implicitHeight: 6
+ radius: Appearance.rounding.small
+ color: Appearance.colors.colLayer2Active
+ }
+ }
+
+ TextArea { // Code
+ id: codeTextArea
+ Layout.fillWidth: true
+ readOnly: !editing
+ selectByMouse: enableMouseSelection || editing
+ renderType: Text.NativeRendering
+ font.family: Appearance.font.family.monospace
+ font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
+ font.pixelSize: Appearance.font.pixelSize.small
+ selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
+ selectionColor: Appearance.colors.colSecondaryContainer
+ // wrapMode: TextEdit.Wrap
+ color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
+
+ text: segmentContent
+ onTextChanged: {
+ segmentContent = text
+ }
+
+ Keys.onPressed: (event) => {
+ if (event.key === Qt.Key_Tab) {
+ // Insert 4 spaces at cursor
+ const cursor = codeTextArea.cursorPosition;
+ codeTextArea.insert(cursor, " ");
+ codeTextArea.cursorPosition = cursor + 4;
+ event.accepted = true;
+ } else if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
+ codeTextArea.copy();
+ event.accepted = true;
+ }
+ }
+
+ SyntaxHighlighter {
+ id: highlighter
+ textEdit: codeTextArea
+ repository: Repository
+ definition: Repository.definitionForName(segmentLang || "plaintext")
+ theme: Appearance.syntaxHighlightingTheme
+ }
+ }
+ }
+
+ // MouseArea to block scrolling
+ // MouseArea {
+ // id: codeBlockMouseArea
+ // anchors.fill: parent
+ // acceptedButtons: editing ? Qt.NoButton : Qt.LeftButton
+ // cursorShape: (enableMouseSelection || editing) ? Qt.IBeamCursor : Qt.ArrowCursor
+ // onWheel: (event) => {
+ // event.accepted = false
+ // }
+ // }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/MessageTextBlock.qml b/.config/quickshell/modules/sidebarLeft/aiChat/MessageTextBlock.qml
new file mode 100644
index 000000000..faa6c5590
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/aiChat/MessageTextBlock.qml
@@ -0,0 +1,147 @@
+pragma ComponentBehavior: Bound
+
+import "root:/"
+import "root:/services"
+import "root:/modules/common/"
+import "root:/modules/common/widgets"
+import "../"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Hyprland
+import Qt5Compat.GraphicalEffects
+
+ColumnLayout {
+ id: root
+ // These are needed on the parent loader
+ property bool editing: parent?.editing ?? false
+ property bool renderMarkdown: parent?.renderMarkdown ?? true
+ property bool enableMouseSelection: parent?.enableMouseSelection ?? false
+ property string segmentContent: parent?.segmentContent ?? ({})
+ property var messageData: parent?.messageData ?? {}
+ property bool done: parent?.done ?? true
+ property list renderedLatexHashes: []
+
+ property string renderedSegmentContent: ""
+
+ Layout.fillWidth: true
+
+ Timer {
+ id: renderTimer
+ interval: 1000
+ repeat: false
+ onTriggered: {
+ renderLatex()
+ for (const hash of renderedLatexHashes) {
+ handleRenderedLatex(hash, true);
+ }
+ }
+ }
+
+ function renderLatex() {
+ // Regex for $...$, $$...$$, \[...\]
+ // Note: This is a simple approach and may need refinement for edge cases
+ let regex = /(\$\$([\s\S]+?)\$\$)|(\$([^\$]+?)\$)|(\\\[((?:.|\n)+?)\\\])|(\\\(([\s\S]+?)\\\))/g;
+ let match;
+ while ((match = regex.exec(segmentContent)) !== null) {
+ let expression = match[1] || match[2] || match[3] || match[4] || match[5] || match[6] || match[7] || match[8];
+ if (expression) {
+ Qt.callLater(() => {
+ const [renderHash, isNew] = LatexRenderer.requestRender(expression.trim());
+ if (!renderedLatexHashes.includes(renderHash)) {
+ renderedLatexHashes.push(renderHash);
+ }
+ });
+ }
+ }
+ }
+
+ function handleRenderedLatex(hash, force = false) {
+ if (renderedLatexHashes.includes(hash) || force) {
+ const imagePath = LatexRenderer.renderedImagePaths[hash];
+ const markdownImage = ``;
+
+ const expression = LatexRenderer.processedExpressions[hash];
+ renderedSegmentContent = renderedSegmentContent.replace(expression, markdownImage);
+ }
+ }
+
+ onDoneChanged: {
+ renderTimer.restart();
+ }
+ onEditingChanged: {
+ if (!editing) {
+ renderLatex()
+ } else {
+ // console.log("Editing mode enabled", segmentContent)
+ textArea.text = segmentContent
+ }
+ }
+
+ onSegmentContentChanged: {
+ // console.log("Segment content changed: " + segmentContent);
+ renderedSegmentContent = segmentContent;
+ if (!root.editing && segmentContent) {
+ root.renderLatex();
+ }
+ }
+
+ onRenderedSegmentContentChanged: {
+ // console.log("Rendered segment content changed: " + renderedSegmentContent);
+ if (renderedSegmentContent) {
+ textArea.text = renderedSegmentContent;
+ }
+ }
+
+ // When something finishes rendering
+ // 1. Check if the hash is in the list
+ // 2. If it is, replace the expression with the image path
+ Connections {
+ target: LatexRenderer
+ function onRenderFinished(hash, imagePath) {
+ const expression = LatexRenderer.processedExpressions[hash];
+ // console.log("Render finished: " + hash + " " + expression);
+ handleRenderedLatex(hash);
+ }
+ }
+
+ TextArea {
+ id: textArea
+
+ Layout.fillWidth: true
+ readOnly: !editing
+ selectByMouse: enableMouseSelection || editing
+ renderType: Text.NativeRendering
+ font.family: Appearance.font.family.reading
+ font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
+ font.pixelSize: Appearance.font.pixelSize.small
+ selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
+ selectionColor: Appearance.colors.colSecondaryContainer
+ wrapMode: TextEdit.Wrap
+ color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
+ textFormat: renderMarkdown ? TextEdit.MarkdownText : TextEdit.PlainText
+ text: qsTr("Waiting for response...")
+
+ onTextChanged: {
+ if (!root.editing) return
+ segmentContent = text
+ }
+
+ onLinkActivated: (link) => {
+ Qt.openUrlExternally(link)
+ Hyprland.dispatch("global quickshell:sidebarLeftClose")
+ }
+
+ MouseArea { // Pointing hand for links
+ anchors.fill: parent
+ acceptedButtons: Qt.NoButton // Only for hover
+ hoverEnabled: true
+ cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor :
+ (enableMouseSelection || editing) ? Qt.IBeamCursor : Qt.ArrowCursor
+ }
+ }
+}
diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/MessageThinkBlock.qml b/.config/quickshell/modules/sidebarLeft/aiChat/MessageThinkBlock.qml
new file mode 100644
index 000000000..1ae941b78
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/aiChat/MessageThinkBlock.qml
@@ -0,0 +1,180 @@
+pragma ComponentBehavior: Bound
+
+import "root:/"
+import "root:/services"
+import "root:/modules/common/"
+import "root:/modules/common/widgets"
+import "../"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Hyprland
+import Qt5Compat.GraphicalEffects
+
+Item {
+ id: root
+ // These are needed on the parent loader
+ property bool editing: parent?.editing ?? false
+ property bool renderMarkdown: parent?.renderMarkdown ?? true
+ property bool enableMouseSelection: parent?.enableMouseSelection ?? false
+ property string segmentContent: parent?.segmentContent ?? ({})
+ property var messageData: parent?.messageData ?? {}
+ property bool done: parent?.done ?? true
+ property bool completed: parent?.completed ?? false
+
+ property real thinkBlockBackgroundRounding: Appearance.rounding.small
+ property real thinkBlockHeaderPaddingVertical: 3
+ property real thinkBlockHeaderPaddingHorizontal: 10
+ property real thinkBlockComponentSpacing: 2
+
+ property var collapseAnimation: messageTextBlock.implicitHeight > 40 ? Appearance.animation.elementMoveEnter : Appearance.animation.elementMoveFast
+ property bool collapsed: true /* should be root.completed but its kinda buggy rn so nope */
+
+ Layout.fillWidth: true
+ implicitHeight: collapsed ? header.implicitHeight : columnLayout.implicitHeight
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: root.width
+ height: root.height
+ radius: thinkBlockBackgroundRounding
+ }
+ }
+
+ Behavior on implicitHeight {
+ enabled: root.completed ?? false
+ NumberAnimation {
+ duration: collapseAnimation.duration
+ easing.type: collapseAnimation.type
+ easing.bezierCurve: collapseAnimation.bezierCurve
+ }
+ }
+
+ ColumnLayout {
+ id: columnLayout
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ spacing: 0
+
+ Rectangle { // Header background
+ id: header
+ color: Appearance.colors.colSurfaceContainerHighest
+ Layout.fillWidth: true
+ implicitHeight: thinkBlockTitleBarRowLayout.implicitHeight + thinkBlockHeaderPaddingVertical * 2
+
+ MouseArea { // Click to reveal
+ id: headerMouseArea
+ enabled: root.completed
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ hoverEnabled: true
+ onClicked: {
+ root.collapsed = !root.collapsed
+ }
+ }
+
+ RowLayout { // Header content
+ id: thinkBlockTitleBarRowLayout
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: thinkBlockHeaderPaddingHorizontal
+ anchors.rightMargin: thinkBlockHeaderPaddingHorizontal
+ spacing: 10
+
+ MaterialSymbol {
+ Layout.fillWidth: false
+ Layout.topMargin: 7
+ Layout.bottomMargin: 7
+ Layout.leftMargin: 3
+ text: "linked_services"
+ }
+ StyledText {
+ id: thinkBlockLanguage
+ Layout.fillWidth: false
+ Layout.alignment: Qt.AlignLeft
+ text: root.completed ? qsTr("Chain of Thought") : (qsTr("Thinking") + ".".repeat(Math.random() * 4))
+ }
+ Item { Layout.fillWidth: true }
+ RippleButton { // Expand button
+ id: expandButton
+ visible: root.completed
+ implicitWidth: 22
+ implicitHeight: 22
+ colBackground: headerMouseArea.containsMouse ? Appearance.colors.colLayer2Hover
+ : ColorUtils.transparentize(Appearance.colors.colLayer2, 1)
+ colBackgroundHover: Appearance.colors.colLayer2Hover
+ colRipple: Appearance.colors.colLayer2Active
+
+ onClicked: { root.collapsed = !root.collapsed }
+
+ contentItem: MaterialSymbol {
+ anchors.centerIn: parent
+ text: "keyboard_arrow_down"
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ iconSize: Appearance.font.pixelSize.normal
+ color: Appearance.colors.colOnLayer2
+ rotation: root.collapsed ? 0 : 180
+ Behavior on rotation {
+ NumberAnimation {
+ duration: Appearance.animation.elementMoveFast.duration
+ easing.type: Appearance.animation.elementMoveFast.type
+ easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
+ }
+ }
+ }
+
+ }
+
+ }
+
+ }
+
+ Item {
+ id: content
+ Layout.fillWidth: true
+ implicitHeight: collapsed ? 0 : contentBackground.implicitHeight + thinkBlockComponentSpacing
+ clip: true
+
+ Behavior on implicitHeight {
+ enabled: root.completed ?? false
+ NumberAnimation {
+ duration: collapseAnimation.duration
+ easing.type: collapseAnimation.easing
+ easing.bezierCurve: collapseAnimation.bezierCurve
+ }
+ }
+
+ Rectangle {
+ id: contentBackground
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ implicitHeight: messageTextBlock.implicitHeight
+ color: Appearance.colors.colLayer2
+
+ // Load data for the message at the correct scope
+ property bool editing: root.editing
+ property bool renderMarkdown: root.renderMarkdown
+ property bool enableMouseSelection: root.enableMouseSelection
+ property string segmentContent: root.segmentContent
+ property var messageData: root.messageData
+ property bool done: root.done
+
+ MessageTextBlock {
+ id: messageTextBlock
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarLeft/anime/BooruImage.qml b/.config/quickshell/modules/sidebarLeft/anime/BooruImage.qml
new file mode 100644
index 000000000..4e115bc76
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/anime/BooruImage.qml
@@ -0,0 +1,191 @@
+import "root:/"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQml
+import Qt.labs.platform
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Effects
+import Qt5Compat.GraphicalEffects
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+
+Button {
+ id: root
+ property var imageData
+ property var rowHeight
+ property bool manualDownload: false
+ property string previewDownloadPath
+ property string downloadPath
+ property string nsfwPath
+ property string fileName: decodeURIComponent((imageData.file_url).substring((imageData.file_url).lastIndexOf('/') + 1))
+ property string filePath: `${root.previewDownloadPath}/${root.fileName}`
+ property int maxTagStringLineLength: 50
+ property real imageRadius: Appearance.rounding.small
+
+ property bool showActions: false
+ Process {
+ id: downloadProcess
+ running: false
+ command: ["bash", "-c", `[ -f ${root.filePath} ] || curl -sSL '${root.imageData.preview_url ?? root.imageData.sample_url}' -o '${root.filePath}'`]
+ onExited: (exitCode, exitStatus) => {
+ imageObject.source = `${previewDownloadPath}/${root.fileName}`
+ }
+ }
+
+ Component.onCompleted: {
+ if (root.manualDownload) {
+ downloadProcess.running = true
+ }
+ }
+
+ StyledToolTip {
+ content: `${StringUtils.wordWrap(root.imageData.tags, root.maxTagStringLineLength)}`
+ }
+
+ padding: 0
+ implicitWidth: root.rowHeight * modelData.aspect_ratio
+ implicitHeight: root.rowHeight
+
+ background: Rectangle {
+ implicitWidth: root.rowHeight * modelData.aspect_ratio
+ implicitHeight: root.rowHeight
+ radius: imageRadius
+ color: Appearance.colors.colLayer2
+ }
+
+ contentItem: Item {
+ anchors.fill: parent
+
+ Image {
+ id: imageObject
+ anchors.fill: parent
+ width: root.rowHeight * modelData.aspect_ratio
+ height: root.rowHeight
+ visible: opacity > 0
+ opacity: status === Image.Ready ? 1 : 0
+ fillMode: Image.PreserveAspectFit
+ source: modelData.preview_url
+ sourceSize.width: root.rowHeight * modelData.aspect_ratio
+ sourceSize.height: root.rowHeight
+
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: root.rowHeight * modelData.aspect_ratio
+ height: root.rowHeight
+ radius: imageRadius
+ }
+ }
+
+ Behavior on opacity {
+ animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
+ }
+ }
+
+ RippleButton {
+ id: menuButton
+ anchors.top: parent.top
+ anchors.right: parent.right
+ property real buttonSize: 30
+ anchors.margins: Math.max(root.imageRadius - buttonSize / 2, 8)
+ implicitHeight: buttonSize
+ implicitWidth: buttonSize
+
+ buttonRadius: Appearance.rounding.full
+ colBackground: ColorUtils.transparentize(Appearance.m3colors.m3surface, 0.3)
+ colBackgroundHover: ColorUtils.transparentize(ColorUtils.mix(Appearance.m3colors.m3surface, Appearance.m3colors.m3onSurface, 0.8), 0.2)
+ colRipple: ColorUtils.transparentize(ColorUtils.mix(Appearance.m3colors.m3surface, Appearance.m3colors.m3onSurface, 0.6), 0.1)
+
+ contentItem: MaterialSymbol {
+ horizontalAlignment: Text.AlignHCenter
+ iconSize: Appearance.font.pixelSize.large
+ color: Appearance.m3colors.m3onSurface
+ text: "more_vert"
+ }
+
+ onClicked: {
+ root.showActions = !root.showActions
+ }
+ }
+
+ Loader {
+ id: contextMenuLoader
+ active: root.showActions
+ anchors.top: menuButton.bottom
+ anchors.right: parent.right
+ anchors.margins: 8
+
+ sourceComponent: Item {
+ width: contextMenu.width
+ height: contextMenu.height
+
+ StyledRectangularShadow {
+ target: contextMenu
+ }
+ Rectangle {
+ id: contextMenu
+ anchors.centerIn: parent
+ opacity: root.showActions ? 1 : 0
+ visible: opacity > 0
+ radius: Appearance.rounding.small
+ color: Appearance.colors.colSurfaceContainer
+ implicitHeight: contextMenuColumnLayout.implicitHeight + radius * 2
+ implicitWidth: contextMenuColumnLayout.implicitWidth
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Appearance.animation.elementMoveFast.duration
+ easing.type: Appearance.animation.elementMoveFast.type
+ easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
+ }
+ }
+
+ ColumnLayout {
+ id: contextMenuColumnLayout
+ anchors.centerIn: parent
+ spacing: 0
+
+ MenuButton {
+ id: openFileLinkButton
+ Layout.fillWidth: true
+ buttonText: qsTr("Open file link")
+ onClicked: {
+ root.showActions = false
+ Hyprland.dispatch("keyword cursor:no_warps true")
+ Qt.openUrlExternally(root.imageData.file_url)
+ Hyprland.dispatch("keyword cursor:no_warps false")
+ }
+ }
+ MenuButton {
+ id: sourceButton
+ visible: root.imageData.source && root.imageData.source.length > 0
+ Layout.fillWidth: true
+ buttonText: StringUtils.format(qsTr("Go to source ({0})"), StringUtils.getDomain(root.imageData.source))
+ enabled: root.imageData.source && root.imageData.source.length > 0
+ onClicked: {
+ root.showActions = false
+ Hyprland.dispatch("keyword cursor:no_warps true")
+ Qt.openUrlExternally(root.imageData.source)
+ Hyprland.dispatch("keyword cursor:no_warps false")
+ }
+ }
+ MenuButton {
+ id: downloadButton
+ Layout.fillWidth: true
+ buttonText: qsTr("Download")
+ onClicked: {
+ root.showActions = false
+ Hyprland.dispatch(`exec curl '${root.imageData.file_url}' -o '${root.imageData.is_nsfw ? root.nsfwPath : root.downloadPath}/${root.fileName}' && notify-send '${qsTr("Download complete")}' '${root.downloadPath}/${root.fileName}' -a 'Shell'`)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarLeft/anime/BooruResponse.qml b/.config/quickshell/modules/sidebarLeft/anime/BooruResponse.qml
new file mode 100644
index 000000000..7a207582f
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/anime/BooruResponse.qml
@@ -0,0 +1,301 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "../"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Io
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+import Qt5Compat.GraphicalEffects
+
+Rectangle {
+ id: root
+ property var responseData
+ property var tagInputField
+
+ property string previewDownloadPath
+ property string downloadPath
+ property string nsfwPath
+
+ property real availableWidth: parent.width
+ property real rowTooShortThreshold: 190
+ property real imageSpacing: 5
+ property real responsePadding: 5
+
+ anchors.left: parent?.left
+ anchors.right: parent?.right
+ implicitHeight: columnLayout.implicitHeight + root.responsePadding * 2
+
+ Component.onCompleted: {
+ // Break property bind to prevent aggressive updates
+ availableWidth = parent.width
+ }
+
+ Connections {
+ target: parent
+ function onWidthChanged() {
+ updateWidthTimer.restart()
+ }
+ }
+
+ Timer {
+ id: updateWidthTimer
+ interval: 100
+ onTriggered: {
+ availableWidth = parent.width
+ }
+ }
+
+ radius: Appearance.rounding.normal
+ color: Appearance.colors.colLayer1
+
+ ColumnLayout {
+ id: columnLayout
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: responsePadding
+ spacing: root.imageSpacing
+
+ RowLayout { // Header
+ Rectangle { // Provider name
+ id: providerNameWrapper
+ color: Appearance.colors.colSecondaryContainer
+ radius: Appearance.rounding.small
+ implicitWidth: providerName.implicitWidth + 10 * 2
+ implicitHeight: Math.max(providerName.implicitHeight + 5 * 2, 30)
+ Layout.alignment: Qt.AlignVCenter
+
+ StyledText {
+ id: providerName
+ anchors.centerIn: parent
+ font.pixelSize: Appearance.font.pixelSize.large
+ color: Appearance.m3colors.m3onSecondaryContainer
+ text: Booru.providers[root.responseData.provider].name
+ }
+ }
+ Item { Layout.fillWidth: true }
+ Item { // Page number
+ visible: root.responseData.page != "" && root.responseData.page > 0
+ implicitWidth: Math.max(pageNumber.implicitWidth + 10 * 2, 30)
+ implicitHeight: pageNumber.implicitHeight + 5 * 2
+ Layout.alignment: Qt.AlignVCenter
+
+ StyledText {
+ id: pageNumber
+ anchors.centerIn: parent
+ font.pixelSize: Appearance.font.pixelSize.smaller
+ color: Appearance.colors.colOnLayer2
+ // text: `Page ${root.responseData.page}`
+ text: StringUtils.format(qsTr("Page {0}"), root.responseData.page)
+ }
+ }
+ }
+
+ Flickable { // Tag strip
+ id: tagsFlickable
+ visible: root.responseData.tags.length > 0
+ Layout.alignment: Qt.AlignLeft
+ Layout.fillWidth: {
+ return true
+ }
+ implicitHeight: tagRowLayout.implicitHeight
+ // height: tagRowLayout.implicitHeight
+ contentWidth: tagRowLayout.implicitWidth
+
+ clip: true
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: tagsFlickable.width
+ height: tagsFlickable.height
+ radius: Appearance.rounding.small
+ }
+ }
+
+ Behavior on height {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+ Behavior on implicitHeight {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+
+ RowLayout {
+ id: tagRowLayout
+ Layout.alignment: Qt.AlignBottom
+
+ Repeater {
+ id: tagRepeater
+ model: root.responseData.tags
+
+ ApiCommandButton {
+ Layout.fillWidth: false
+ buttonText: modelData
+ onClicked: {
+ if(root.tagInputField.text.length !== 0) root.tagInputField.text += " "
+ root.tagInputField.text += modelData
+ }
+ }
+ }
+
+ }
+ }
+
+ StyledText { // Message
+ id: messageText
+ Layout.fillWidth: true
+ visible: root.responseData.message.length > 0
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: Appearance.colors.colOnLayer1
+ text: root.responseData.message
+ wrapMode: Text.WordWrap
+ Layout.margins: responsePadding
+ textFormat: Text.MarkdownText
+ onLinkActivated: (link) => {
+ Qt.openUrlExternally(link)
+ Hyprland.dispatch("global quickshell:sidebarLeftClose")
+ }
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.NoButton // Only for hover
+ hoverEnabled: true
+ cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
+ }
+ }
+
+ Repeater {
+ model: ScriptModel {
+ values: {
+ // Greedily add images to a row as long as rowHeight >= rowTooShortThreshold
+ let i = 0;
+ let rows = [];
+ const responseList = root.responseData.images;
+ const minRowHeight = rowTooShortThreshold;
+ const availableImageWidth = availableWidth - root.imageSpacing - (responsePadding * 2);
+
+ while (i < responseList.length) {
+ let row = {
+ height: 0,
+ images: [],
+ };
+ let j = i;
+ let combinedAspect = 0;
+ let rowHeight = 0;
+
+ // Try to add as many images as possible without going below minRowHeight
+ while (j < responseList.length) {
+ combinedAspect += responseList[j].aspect_ratio;
+ // Subtract imageSpacing for each gap between images in the row
+ let imagesInRow = j - i + 1;
+ let totalSpacing = root.imageSpacing * (imagesInRow - 1);
+ let rowAvailableWidth = availableImageWidth - totalSpacing;
+ rowHeight = rowAvailableWidth / combinedAspect;
+ if (rowHeight < minRowHeight) {
+ combinedAspect -= responseList[j].aspect_ratio;
+ imagesInRow -= 1;
+ totalSpacing = root.imageSpacing * (imagesInRow - 1);
+ rowAvailableWidth = availableImageWidth - totalSpacing;
+ rowHeight = rowAvailableWidth / combinedAspect;
+ break;
+ }
+ j++;
+ }
+
+ // If we couldn't add any image (shouldn't happen), add at least one
+ if (j === i) {
+ row.images.push(responseList[i]);
+ row.height = availableImageWidth / responseList[i].aspect_ratio;
+ rows.push(row);
+ i++;
+ } else {
+ for (let k = i; k < j; k++) {
+ row.images.push(responseList[k]);
+ }
+ // Recalculate spacing for the final row
+ let imagesInRow = j - i;
+ let totalSpacing = root.imageSpacing * (imagesInRow - 1);
+ let rowAvailableWidth = availableImageWidth - totalSpacing;
+ row.height = rowAvailableWidth / combinedAspect;
+ rows.push(row);
+ i = j;
+ }
+ }
+ return rows;
+ }
+ }
+ delegate: RowLayout {
+ id: imageRow
+ required property var modelData
+ property var rowHeight: modelData.height
+ spacing: root.imageSpacing
+
+ Repeater {
+ model: modelData.images
+ delegate: BooruImage {
+ required property var modelData
+ imageData: modelData
+ rowHeight: imageRow.rowHeight
+ imageRadius: imageRow.modelData.images.length == 1 ? 50 : Appearance.rounding.normal
+ // Download manually to reduce redundant requests or make sure downloading works
+ manualDownload: ["danbooru", "waifu.im", "t.alcy.cc"].includes(root.responseData.provider)
+ previewDownloadPath: root.previewDownloadPath
+ downloadPath: root.downloadPath
+ nsfwPath: root.nsfwPath
+ }
+ }
+ }
+ }
+
+ RippleButton { // Next page button
+ id: button
+ property string buttonText
+ visible: root.responseData.page != "" && root.responseData.page > 0
+
+ Layout.alignment: Qt.AlignRight
+ implicitHeight: 30
+ leftPadding: 10
+ rightPadding: 5
+
+ onClicked: {
+ tagInputField.text = `${responseData.tags.join(" ")} ${parseInt(root.responseData.page) + 1}`
+ tagInputField.accept()
+ }
+
+ buttonRadius: Appearance.rounding.small
+ colBackground: Appearance.colors.colSurfaceContainerHighest
+ colBackgroundHover: Appearance.colors.colSurfaceContainerHighestHover
+ colRipple: Appearance.colors.colSurfaceContainerHighestActive
+
+ contentItem: Item {
+ anchors.fill: parent
+ implicitHeight: nextPageRow.implicitHeight
+ implicitWidth: nextPageRow.implicitWidth
+
+ RowLayout {
+ id: nextPageRow
+ anchors.centerIn: parent
+ spacing: 0
+ StyledText {
+ Layout.alignment: Qt.AlignVCenter
+ verticalAlignment: Text.AlignVCenter
+ text: "Next page"
+ color: Appearance.m3colors.m3onSurface
+ }
+ MaterialSymbol {
+ Layout.alignment: Qt.AlignVCenter
+ iconSize: Appearance.font.pixelSize.larger
+ color: Appearance.m3colors.m3onSurface
+ text: "chevron_right"
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarLeft/translator/LanguageSelectorButton.qml b/.config/quickshell/modules/sidebarLeft/translator/LanguageSelectorButton.qml
new file mode 100644
index 000000000..37df25f83
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/translator/LanguageSelectorButton.qml
@@ -0,0 +1,45 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+
+RippleButton {
+ id: root
+ property string displayText: ""
+ colBackground: Appearance.colors.colLayer2
+
+ implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
+ implicitHeight: contentItem.implicitHeight + verticalPadding * 2
+
+ contentItem: Item {
+ anchors.centerIn: parent
+ implicitWidth: languageRow.implicitWidth
+ implicitHeight: languageText.implicitHeight
+ RowLayout {
+ id: languageRow
+ anchors.centerIn: parent
+ spacing: 0
+ StyledText {
+ id: languageText
+ Layout.alignment: Qt.AlignVCenter
+ Layout.leftMargin: 5
+ text: root.displayText
+ color: Appearance.colors.colOnLayer2
+ font.pixelSize: Appearance.font.pixelSize.small
+ }
+ MaterialSymbol {
+ Layout.alignment: Qt.AlignVCenter
+ iconSize: Appearance.font.pixelSize.hugeass
+ text: "arrow_drop_down"
+ color: Appearance.colors.colOnLayer2
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/modules/sidebarLeft/translator/TextCanvas.qml b/.config/quickshell/modules/sidebarLeft/translator/TextCanvas.qml
new file mode 100644
index 000000000..dad25020f
--- /dev/null
+++ b/.config/quickshell/modules/sidebarLeft/translator/TextCanvas.qml
@@ -0,0 +1,92 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+
+Rectangle {
+ id: root
+ property bool isInput: true // true for input, false for output
+ property string placeholderText
+ property string text: ""
+ property var inputTextArea: isInput ? inputLoader.item : undefined
+ readonly property string displayedText: isInput ? inputLoader.item.text :
+ root.text.length > 0 ? outputLoader.item.text : ""
+ default property alias actionButtons: actions.data
+ Layout.fillWidth: true
+ implicitHeight: Math.max(150, inputColumn.implicitHeight)
+ color: isInput ? Appearance.colors.colLayer1 : Appearance.colors.colSurfaceContainer
+ radius: Appearance.rounding.normal
+ border.color: isInput ? Appearance.colors.colOutlineVariant : "transparent"
+ border.width: isInput ? 1 : 0
+
+ signal inputTextChanged(); // Signal emitted when text changes
+
+ ColumnLayout {
+ id: inputColumn
+ anchors.fill: parent
+ spacing: 0
+
+ Loader {
+ id: inputLoader
+ active: root.isInput
+ visible: root.isInput
+ Layout.fillWidth: true
+ sourceComponent: StyledTextArea { // Input area
+ id: inputTextArea
+ placeholderText: root.placeholderText
+ wrapMode: TextEdit.Wrap
+ textFormat: TextEdit.PlainText
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: Appearance.colors.colOnLayer1
+ padding: 15
+ background: null
+ onTextChanged: root.inputTextChanged()
+ }
+ }
+
+ Loader {
+ id: outputLoader
+ active: !root.isInput
+ visible: !root.isInput
+ Layout.fillWidth: true
+ sourceComponent: StyledText { // Output area
+ id: outputTextArea
+ padding: 15
+ wrapMode: Text.Wrap
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: root.text.length > 0 ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext
+ text: root.text.length > 0 ? root.text : root.placeholderText
+ }
+ }
+
+ Item { Layout.fillHeight: true }
+
+ RowLayout { // Status row
+ Layout.fillWidth: true
+ Layout.margins: 10
+ spacing: 10
+
+ Loader {
+ active: root.isInput
+ visible: root.isInput
+ Layout.leftMargin: 10
+ sourceComponent: Text {
+ text: qsTr("%1 characters").arg(inputLoader.item.text.length)
+ color: Appearance.colors.colOnLayer1
+ font.pixelSize: Appearance.font.pixelSize.smaller
+ }
+ }
+ Item { Layout.fillWidth: true }
+ ButtonGroup {
+ id: actions
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarRight/BottomWidgetGroup.qml b/.config/quickshell/modules/sidebarRight/BottomWidgetGroup.qml
new file mode 100644
index 000000000..a29dddeda
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/BottomWidgetGroup.qml
@@ -0,0 +1,236 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import "./calendar"
+import "./todo"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+
+Rectangle {
+ id: root
+ radius: Appearance.rounding.normal
+ color: Appearance.colors.colLayer1
+ clip: true
+ implicitHeight: collapsed ? collapsedBottomWidgetGroupRow.implicitHeight : bottomWidgetGroupRow.implicitHeight
+ property int selectedTab: 0
+ property bool collapsed: PersistentStates.sidebar.bottomGroup.collapsed
+ property var tabs: [
+ {"type": "calendar", "name": "Calendar", "icon": "calendar_month", "widget": calendarWidget},
+ {"type": "todo", "name": "To Do", "icon": "done_outline", "widget": todoWidget}
+ ]
+
+ Behavior on implicitHeight {
+ NumberAnimation {
+ duration: Appearance.animation.elementMove.duration
+ easing.type: Appearance.animation.elementMove.type
+ easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
+ }
+ }
+
+ function setCollapsed(state) {
+ PersistentStateManager.setState("sidebar.bottomGroup.collapsed", state)
+ if (collapsed) {
+ bottomWidgetGroupRow.opacity = 0
+ }
+ else {
+ collapsedBottomWidgetGroupRow.opacity = 0
+ }
+ collapseCleanFadeTimer.start()
+ }
+
+ Timer {
+ id: collapseCleanFadeTimer
+ interval: Appearance.animation.elementMove.duration / 2
+ repeat: false
+ onTriggered: {
+ if(collapsed) collapsedBottomWidgetGroupRow.opacity = 1
+ else bottomWidgetGroupRow.opacity = 1
+ }
+ }
+
+ Keys.onPressed: (event) => {
+ if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp)
+ && event.modifiers === Qt.ControlModifier) {
+ if (event.key === Qt.Key_PageDown) {
+ root.selectedTab = Math.min(root.selectedTab + 1, root.tabs.length - 1)
+ } else if (event.key === Qt.Key_PageUp) {
+ root.selectedTab = Math.max(root.selectedTab - 1, 0)
+ }
+ event.accepted = true;
+ }
+ }
+
+ // The thing when collapsed
+ RowLayout {
+ id: collapsedBottomWidgetGroupRow
+ opacity: collapsed ? 1 : 0
+ visible: opacity > 0
+ Behavior on opacity {
+ NumberAnimation {
+ id: collapsedBottomWidgetGroupRowFade
+ duration: Appearance.animation.elementMove.duration / 2
+ easing.type: Appearance.animation.elementMove.type
+ easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
+ }
+ }
+
+ spacing: 15
+
+ CalendarHeaderButton {
+ Layout.margins: 10
+ Layout.rightMargin: 0
+ forceCircle: true
+ onClicked: {
+ root.setCollapsed(false)
+ }
+ contentItem: MaterialSymbol {
+ text: "keyboard_arrow_up"
+ iconSize: Appearance.font.pixelSize.larger
+ horizontalAlignment: Text.AlignHCenter
+ color: Appearance.colors.colOnLayer1
+ }
+ }
+
+ StyledText {
+ property int remainingTasks: Todo.list.filter(task => !task.done).length;
+ Layout.margins: 10
+ Layout.leftMargin: 0
+ text: `${DateTime.collapsedCalendarFormat} • ${remainingTasks} task${remainingTasks > 1 ? "s" : ""}`
+ font.pixelSize: Appearance.font.pixelSize.large
+ color: Appearance.colors.colOnLayer1
+ }
+ }
+
+ // The thing when expanded
+ RowLayout {
+ id: bottomWidgetGroupRow
+
+ opacity: collapsed ? 0 : 1
+ visible: opacity > 0
+ Behavior on opacity {
+ NumberAnimation {
+ id: bottomWidgetGroupRowFade
+ duration: Appearance.animation.elementMove.duration / 2
+ easing.type: Appearance.animation.elementMove.type
+ easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
+ }
+ }
+
+ anchors.fill: parent
+ height: tabStack.height
+ spacing: 10
+
+ // Navigation rail
+ Item {
+ Layout.fillHeight: true
+ Layout.fillWidth: false
+ Layout.leftMargin: 10
+ Layout.topMargin: 10
+ width: tabBar.width
+ // Navigation rail buttons
+ ColumnLayout {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.leftMargin: 5
+ id: tabBar
+ spacing: 15
+ Repeater {
+ model: root.tabs
+ NavRailButton {
+ toggled: root.selectedTab == index
+ buttonText: modelData.name
+ buttonIcon: modelData.icon
+ onClicked: {
+ root.selectedTab = index
+ }
+ }
+ }
+ }
+ // Collapse button
+ CalendarHeaderButton {
+ anchors.left: parent.left
+ anchors.top: parent.top
+ forceCircle: true
+ onClicked: {
+ root.setCollapsed(true)
+ }
+ contentItem: MaterialSymbol {
+ text: "keyboard_arrow_down"
+ iconSize: Appearance.font.pixelSize.larger
+ horizontalAlignment: Text.AlignHCenter
+ color: Appearance.colors.colOnLayer1
+ }
+ }
+ }
+
+ // Content area
+ StackLayout {
+ id: tabStack
+ Layout.fillWidth: true
+ height: tabStack.children[0]?.tabLoader?.implicitHeight // TODO: make this less stupid
+ Layout.alignment: Qt.AlignVCenter
+ property int realIndex: 0
+ property int animationDuration: Appearance.animation.elementMoveFast.duration * 1.5
+
+ // Switch the tab on halfway of the anim duration
+ Connections {
+ target: root
+ function onSelectedTabChanged() {
+ delayedStackSwitch.start()
+ tabStack.realIndex = root.selectedTab
+ }
+ }
+ Timer {
+ id: delayedStackSwitch
+ interval: tabStack.animationDuration / 2
+ repeat: false
+ onTriggered: {
+ tabStack.currentIndex = root.selectedTab
+ }
+ }
+
+ Repeater {
+ model: tabs
+ Item { // TODO: make behavior on y also act for the item that's switched to
+ id: tabItem
+ property int tabIndex: index
+ property string tabType: modelData.type
+ property int animDistance: 5
+ property var tabLoader: tabLoader
+ // Opacity: show up only when being animated to
+ opacity: (tabStack.currentIndex === tabItem.tabIndex && tabStack.realIndex === tabItem.tabIndex) ? 1 : 0
+ // Y: starts animating when user selects a different tab
+ y: (tabStack.realIndex === tabItem.tabIndex) ? 0 : (tabStack.realIndex < tabItem.tabIndex) ? animDistance : -animDistance
+ Behavior on opacity { NumberAnimation { duration: tabStack.animationDuration / 2; easing.type: Easing.OutCubic } }
+ Behavior on y { NumberAnimation { duration: tabStack.animationDuration; easing.type: Easing.OutExpo } }
+ Loader {
+ id: tabLoader
+ anchors.fill: parent
+ sourceComponent: modelData.widget
+ focus: root.selectedTab === tabItem.tabIndex
+ }
+ }
+ }
+ }
+ }
+
+ // Calendar component
+ Component {
+ id: calendarWidget
+
+ CalendarWidget {
+ anchors.centerIn: parent
+ }
+ }
+
+ // To Do component
+ Component {
+ id: todoWidget
+ TodoWidget {
+ anchors.fill: parent
+ anchors.margins: 5
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarRight/CenterWidgetGroup.qml b/.config/quickshell/modules/sidebarRight/CenterWidgetGroup.qml
new file mode 100644
index 000000000..1b426da43
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/CenterWidgetGroup.qml
@@ -0,0 +1,82 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import "./calendar"
+import "./notifications"
+import "./todo"
+import "./volumeMixer"
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+
+Rectangle {
+ id: root
+ radius: Appearance.rounding.normal
+ color: Appearance.colors.colLayer1
+
+ property int selectedTab: 0
+ property var tabButtonList: [{"icon": "notifications", "name": qsTr("Notifications")}, {"icon": "volume_up", "name": qsTr("Volume mixer")}]
+
+ Keys.onPressed: (event) => {
+ if (event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) {
+ if (event.key === Qt.Key_PageDown) {
+ root.selectedTab = Math.min(root.selectedTab + 1, root.tabButtonList.length - 1)
+ } else if (event.key === Qt.Key_PageUp) {
+ root.selectedTab = Math.max(root.selectedTab - 1, 0)
+ }
+ event.accepted = true;
+ }
+ if (event.modifiers === Qt.ControlModifier) {
+ if (event.key === Qt.Key_Tab) {
+ root.selectedTab = (root.selectedTab + 1) % root.tabButtonList.length
+ } else if (event.key === Qt.Key_Backtab) {
+ root.selectedTab = (root.selectedTab - 1 + root.tabButtonList.length) % root.tabButtonList.length
+ }
+ event.accepted = true;
+ }
+ }
+
+ ColumnLayout {
+ anchors.margins: 5
+ anchors.fill: parent
+ spacing: 0
+
+ PrimaryTabBar {
+ id: tabBar
+ tabButtonList: root.tabButtonList
+ externalTrackedTab: root.selectedTab
+
+ function onCurrentIndexChanged(currentIndex) {
+ root.selectedTab = currentIndex
+ }
+ }
+
+ SwipeView {
+ id: swipeView
+ Layout.topMargin: 5
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ spacing: 10
+ currentIndex: root.selectedTab
+ onCurrentIndexChanged: {
+ tabBar.enableIndicatorAnimation = true
+ root.selectedTab = currentIndex
+ }
+
+ clip: true
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: swipeView.width
+ height: swipeView.height
+ radius: Appearance.rounding.small
+ }
+ }
+
+ NotificationList {}
+ VolumeMixer {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarRight/SidebarRight.qml b/.config/quickshell/modules/sidebarRight/SidebarRight.qml
new file mode 100644
index 000000000..ed9788f0e
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/SidebarRight.qml
@@ -0,0 +1,246 @@
+import "root:/"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "./quickToggles/"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Effects
+import Qt5Compat.GraphicalEffects
+import Quickshell.Io
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+Scope {
+ property int sidebarWidth: Appearance.sizes.sidebarWidth
+ property int sidebarPadding: 15
+
+ PanelWindow {
+ id: sidebarRoot
+ visible: GlobalStates.sidebarRightOpen
+
+ function hide() {
+ GlobalStates.sidebarRightOpen = false
+ }
+
+ exclusiveZone: 0
+ implicitWidth: sidebarWidth
+ WlrLayershell.namespace: "quickshell:sidebarRight"
+ // Hyprland 0.49: Focus is always exclusive and setting this breaks mouse focus grab
+ // WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
+ color: "transparent"
+
+ anchors {
+ top: true
+ right: true
+ bottom: true
+ }
+
+ HyprlandFocusGrab {
+ id: grab
+ windows: [ sidebarRoot ]
+ active: GlobalStates.sidebarRightOpen
+ onCleared: () => {
+ if (!active) sidebarRoot.hide()
+ }
+ }
+
+ Loader {
+ id: sidebarContentLoader
+ active: GlobalStates.sidebarRightOpen
+ anchors {
+ top: parent.top
+ bottom: parent.bottom
+ right: parent.right
+ left: parent.left
+ topMargin: Appearance.sizes.hyprlandGapsOut
+ rightMargin: Appearance.sizes.hyprlandGapsOut
+ bottomMargin: Appearance.sizes.hyprlandGapsOut
+ leftMargin: Appearance.sizes.elevationMargin
+ }
+ width: sidebarWidth - Appearance.sizes.hyprlandGapsOut - Appearance.sizes.elevationMargin
+ height: parent.height - Appearance.sizes.hyprlandGapsOut * 2
+
+ focus: GlobalStates.sidebarRightOpen
+ Keys.onPressed: (event) => {
+ if (event.key === Qt.Key_Escape) {
+ sidebarRoot.hide();
+ }
+ }
+
+ sourceComponent: Item {
+ implicitHeight: sidebarRightBackground.implicitHeight
+ implicitWidth: sidebarRightBackground.implicitWidth
+
+ StyledRectangularShadow {
+ target: sidebarRightBackground
+ }
+ Rectangle {
+ id: sidebarRightBackground
+
+ anchors.fill: parent
+ implicitHeight: parent.height - Appearance.sizes.hyprlandGapsOut * 2
+ implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2
+ color: Appearance.colors.colLayer0
+ radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
+
+ ColumnLayout {
+ spacing: sidebarPadding
+ anchors.fill: parent
+ anchors.margins: sidebarPadding
+
+ RowLayout {
+ Layout.fillHeight: false
+ spacing: 10
+ Layout.margins: 10
+ Layout.topMargin: 5
+ Layout.bottomMargin: 0
+
+ Item {
+ implicitWidth: distroIcon.width
+ implicitHeight: distroIcon.height
+ CustomIcon {
+ id: distroIcon
+ width: 25
+ height: 25
+ source: SystemInfo.distroIcon
+ }
+ ColorOverlay {
+ anchors.fill: distroIcon
+ source: distroIcon
+ color: Appearance.colors.colOnLayer0
+ }
+ }
+
+ StyledText {
+ font.pixelSize: Appearance.font.pixelSize.normal
+ color: Appearance.colors.colOnLayer0
+ text: StringUtils.format(qsTr("Uptime: {0}"), DateTime.uptime)
+ textFormat: Text.MarkdownText
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ ButtonGroup {
+ QuickToggleButton {
+ toggled: false
+ buttonIcon: "restart_alt"
+ onClicked: {
+ Hyprland.dispatch("reload")
+ Quickshell.reload(true)
+ }
+ StyledToolTip {
+ content: qsTr("Reload Hyprland & Quickshell")
+ }
+ }
+ QuickToggleButton {
+ toggled: false
+ buttonIcon: "settings"
+ onClicked: {
+ Hyprland.dispatch(`exec ${ConfigOptions.apps.settings}`)
+ Hyprland.dispatch(`global quickshell:sidebarRightClose`)
+ }
+ StyledToolTip {
+ content: qsTr("Plasma Settings")
+ }
+ }
+ QuickToggleButton {
+ toggled: false
+ buttonIcon: "power_settings_new"
+ onClicked: {
+ Hyprland.dispatch("global quickshell:sessionOpen")
+ }
+ StyledToolTip {
+ content: qsTr("Session")
+ }
+ }
+ }
+ }
+
+ ButtonGroup {
+ Layout.alignment: Qt.AlignHCenter
+ spacing: 5
+ padding: 5
+ color: Appearance.colors.colLayer1
+
+ NetworkToggle {}
+ BluetoothToggle {}
+ NightLight {}
+ GameMode {}
+ IdleInhibitor {}
+ }
+
+ // Center widget group
+ CenterWidgetGroup {
+ focus: sidebarRoot.visible
+ Layout.alignment: Qt.AlignHCenter
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ }
+
+ BottomWidgetGroup {
+ Layout.alignment: Qt.AlignHCenter
+ Layout.fillHeight: false
+ Layout.fillWidth: true
+ Layout.preferredHeight: implicitHeight
+ }
+ }
+ }
+ }
+ }
+
+
+ }
+
+ IpcHandler {
+ target: "sidebarRight"
+
+ function toggle(): void {
+ GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
+ if(GlobalStates.sidebarRightOpen) Notifications.timeoutAll();
+ }
+
+ function close(): void {
+ GlobalStates.sidebarRightOpen = false;
+ }
+
+ function open(): void {
+ GlobalStates.sidebarRightOpen = true;
+ Notifications.timeoutAll();
+ }
+ }
+
+ GlobalShortcut {
+ name: "sidebarRightToggle"
+ description: qsTr("Toggles right sidebar on press")
+
+ onPressed: {
+ GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
+ if(GlobalStates.sidebarRightOpen) Notifications.timeoutAll();
+ }
+ }
+ GlobalShortcut {
+ name: "sidebarRightOpen"
+ description: qsTr("Opens right sidebar on press")
+
+ onPressed: {
+ GlobalStates.sidebarRightOpen = true;
+ Notifications.timeoutAll();
+ }
+ }
+ GlobalShortcut {
+ name: "sidebarRightClose"
+ description: qsTr("Closes right sidebar on press")
+
+ onPressed: {
+ GlobalStates.sidebarRightOpen = false;
+ }
+ }
+
+}
diff --git a/.config/quickshell/modules/sidebarRight/calendar/CalendarDayButton.qml b/.config/quickshell/modules/sidebarRight/calendar/CalendarDayButton.qml
new file mode 100644
index 000000000..7d1af447d
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/calendar/CalendarDayButton.qml
@@ -0,0 +1,36 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+RippleButton {
+ id: button
+ property string day
+ property int isToday
+ property bool bold
+
+ Layout.fillWidth: false
+ Layout.fillHeight: false
+ implicitWidth: 38;
+ implicitHeight: 38;
+
+ toggled: (isToday == 1)
+ buttonRadius: Appearance.rounding.small
+
+ contentItem: StyledText {
+ anchors.fill: parent
+ text: day
+ horizontalAlignment: Text.AlignHCenter
+ font.weight: bold ? Font.DemiBold : Font.Normal
+ color: (isToday == 1) ? Appearance.m3colors.m3onPrimary :
+ (isToday == 0) ? Appearance.colors.colOnLayer1 :
+ Appearance.colors.colOutlineVariant
+
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+}
+
diff --git a/.config/quickshell/modules/sidebarRight/calendar/CalendarHeaderButton.qml b/.config/quickshell/modules/sidebarRight/calendar/CalendarHeaderButton.qml
new file mode 100644
index 000000000..92ef3c854
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/calendar/CalendarHeaderButton.qml
@@ -0,0 +1,38 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+RippleButton {
+ id: button
+ property string buttonText: ""
+ property string tooltipText: ""
+ property bool forceCircle: false
+
+ implicitHeight: 30
+ implicitWidth: forceCircle ? implicitHeight : (contentItem.implicitWidth + 10 * 2)
+ Behavior on implicitWidth {
+ SmoothedAnimation {
+ velocity: Appearance.animation.elementMove.velocity
+ }
+ }
+
+ background.anchors.fill: button
+ buttonRadius: Appearance.rounding.full
+ colBackground: Appearance.colors.colLayer2
+ colBackgroundHover: Appearance.colors.colLayer2Hover
+ colRipple: Appearance.colors.colLayer2Active
+
+ contentItem: StyledText {
+ text: buttonText
+ horizontalAlignment: Text.AlignHCenter
+ font.pixelSize: Appearance.font.pixelSize.larger
+ color: Appearance.colors.colOnLayer1
+ }
+
+ StyledToolTip {
+ content: tooltipText
+ extraVisibleCondition: tooltipText.length > 0
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarRight/calendar/CalendarWidget.qml b/.config/quickshell/modules/sidebarRight/calendar/CalendarWidget.qml
new file mode 100644
index 000000000..1f11d87a2
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/calendar/CalendarWidget.qml
@@ -0,0 +1,122 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "./calendar_layout.js" as CalendarLayout
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Item {
+ // Layout.topMargin: 10
+ anchors.topMargin: 10
+ property int monthShift: 0
+ property var viewingDate: CalendarLayout.getDateInXMonthsTime(monthShift)
+ property var calendarLayout: CalendarLayout.getCalendarLayout(viewingDate, monthShift === 0)
+ width: calendarColumn.width
+ implicitHeight: calendarColumn.height + 10 * 2
+
+ Keys.onPressed: (event) => {
+ if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp)
+ && event.modifiers === Qt.NoModifier) {
+ if (event.key === Qt.Key_PageDown) {
+ monthShift++;
+ } else if (event.key === Qt.Key_PageUp) {
+ monthShift--;
+ }
+ event.accepted = true;
+ }
+ }
+ MouseArea {
+ anchors.fill: parent
+ onWheel: (event) => {
+ if (event.angleDelta.y > 0) {
+ monthShift--;
+ } else if (event.angleDelta.y < 0) {
+ monthShift++;
+ }
+ }
+ }
+
+ ColumnLayout {
+ id: calendarColumn
+ anchors.centerIn: parent
+ spacing: 5
+
+ // Calendar header
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 5
+ CalendarHeaderButton {
+ clip: true
+ buttonText: `${monthShift != 0 ? "• " : ""}${viewingDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")}`
+ tooltipText: (monthShift === 0) ? "" : qsTr("Jump to current month")
+ onClicked: {
+ monthShift = 0;
+ }
+ }
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: false
+ }
+ CalendarHeaderButton {
+ forceCircle: true
+ onClicked: {
+ monthShift--;
+ }
+ contentItem: MaterialSymbol {
+ text: "chevron_left"
+ iconSize: Appearance.font.pixelSize.larger
+ horizontalAlignment: Text.AlignHCenter
+ color: Appearance.colors.colOnLayer1
+ }
+ }
+ CalendarHeaderButton {
+ forceCircle: true
+ onClicked: {
+ monthShift++;
+ }
+ contentItem: MaterialSymbol {
+ text: "chevron_right"
+ iconSize: Appearance.font.pixelSize.larger
+ horizontalAlignment: Text.AlignHCenter
+ color: Appearance.colors.colOnLayer1
+ }
+ }
+ }
+
+ // Week days row
+ RowLayout {
+ id: weekDaysRow
+ Layout.alignment: Qt.AlignHCenter
+ Layout.fillHeight: false
+ spacing: 5
+ Repeater {
+ model: CalendarLayout.weekDays
+ delegate: CalendarDayButton {
+ day: modelData.day
+ isToday: modelData.today
+ bold: true
+ enabled: false
+ }
+ }
+ }
+
+ // Real week rows
+ Repeater {
+ id: calendarRows
+ // model: calendarLayout
+ model: 6
+ delegate: RowLayout {
+ Layout.alignment: Qt.AlignHCenter
+ Layout.fillHeight: false
+ spacing: 5
+ Repeater {
+ model: Array(7).fill(modelData)
+ delegate: CalendarDayButton {
+ day: calendarLayout[modelData][index].day
+ isToday: calendarLayout[modelData][index].today
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/ags/modules/sideright/calendar_layout.js b/.config/quickshell/modules/sidebarRight/calendar/calendar_layout.js
similarity index 70%
rename from .config/ags/modules/sideright/calendar_layout.js
rename to .config/quickshell/modules/sidebarRight/calendar/calendar_layout.js
index 35b481d6d..7f750b411 100644
--- a/.config/ags/modules/sideright/calendar_layout.js
+++ b/.config/quickshell/modules/sidebarRight/calendar/calendar_layout.js
@@ -1,3 +1,13 @@
+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 },
+]
+
function checkLeapYear(year) {
return (
year % 400 == 0 ||
@@ -30,7 +40,27 @@ function getPrevMonthDays(month, year) {
return 31;
}
-export function getCalendarLayout(dateObject, highlight) {
+function getDateInXMonthsTime(x) {
+ var currentDate = new Date(); // Get the current date
+ if (x == 0) return currentDate; // If x is 0, return 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;
+}
+
+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();
diff --git a/.config/quickshell/modules/sidebarRight/notifications/NotificationList.qml b/.config/quickshell/modules/sidebarRight/notifications/NotificationList.qml
new file mode 100644
index 000000000..491b7683f
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/notifications/NotificationList.qml
@@ -0,0 +1,119 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Widgets
+
+Item {
+ id: root
+
+ NotificationListView { // Scrollable window
+ id: listview
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.bottom: statusRow.top
+ anchors.bottomMargin: 5
+
+ clip: true
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: listview.width
+ height: listview.height
+ radius: Appearance.rounding.normal
+ }
+ }
+
+ popup: false
+ }
+
+ // Placeholder when list is empty
+ Item {
+ anchors.fill: listview
+
+ visible: opacity > 0
+ opacity: (Notifications.list.length === 0) ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Appearance.animation.menuDecel.duration
+ easing.type: Appearance.animation.menuDecel.type
+ }
+ }
+
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: 5
+
+ MaterialSymbol {
+ Layout.alignment: Qt.AlignHCenter
+ iconSize: 55
+ color: Appearance.m3colors.m3outline
+ text: "notifications_active"
+ }
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ font.pixelSize: Appearance.font.pixelSize.normal
+ color: Appearance.m3colors.m3outline
+ horizontalAlignment: Text.AlignHCenter
+ text: qsTr("No notifications")
+ }
+ }
+ }
+
+ Item {
+ id: statusRow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+
+ Layout.fillWidth: true
+ implicitHeight: Math.max(
+ controls.implicitHeight,
+ statusText.implicitHeight
+ )
+
+ StyledText {
+ id: statusText
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: 10
+ horizontalAlignment: Text.AlignHCenter
+ text: `${Notifications.list.length} notifications`
+
+ opacity: Notifications.list.length > 0 ? 1 : 0
+ visible: opacity > 0
+ Behavior on opacity {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+ }
+
+ ButtonGroup {
+ id: controls
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.rightMargin: 5
+
+ NotificationStatusButton {
+ buttonIcon: "notifications_paused"
+ buttonText: qsTr("Silent")
+ toggled: Notifications.silent
+ onClicked: () => {
+ Notifications.silent = !Notifications.silent;
+ }
+ }
+ NotificationStatusButton {
+ buttonIcon: "clear_all"
+ buttonText: qsTr("Clear")
+ onClicked: () => {
+ Notifications.discardAllNotifications()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarRight/notifications/NotificationStatusButton.qml b/.config/quickshell/modules/sidebarRight/notifications/NotificationStatusButton.qml
new file mode 100644
index 000000000..bcfeaecfd
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/notifications/NotificationStatusButton.qml
@@ -0,0 +1,44 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+GroupButton {
+ id: button
+ property string buttonText: ""
+ property string buttonIcon: ""
+
+ baseWidth: content.implicitWidth + 10 * 2
+ baseHeight: 30
+
+ buttonRadius: baseHeight / 2
+ buttonRadiusPressed: Appearance.rounding.small
+ colBackground: Appearance.colors.colLayer2
+ colBackgroundHover: Appearance.colors.colLayer2Hover
+ colBackgroundActive: Appearance.colors.colLayer2Active
+ property color colText: toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
+
+ contentItem: Item {
+ id: content
+ anchors.fill: parent
+ implicitWidth: contentRowLayout.implicitWidth
+ implicitHeight: contentRowLayout.implicitHeight
+ RowLayout {
+ id: contentRowLayout
+ anchors.centerIn: parent
+ spacing: 5
+ MaterialSymbol {
+ text: buttonIcon
+ iconSize: Appearance.font.pixelSize.large
+ color: button.colText
+ }
+ StyledText {
+ text: buttonText
+ font.pixelSize: Appearance.font.pixelSize.small
+ color: button.colText
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarRight/quickToggles/BluetoothToggle.qml b/.config/quickshell/modules/sidebarRight/quickToggles/BluetoothToggle.qml
new file mode 100644
index 000000000..083ecc036
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/quickToggles/BluetoothToggle.qml
@@ -0,0 +1,36 @@
+import "../"
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+
+QuickToggleButton {
+ toggled: Bluetooth.bluetoothEnabled
+ buttonIcon: Bluetooth.bluetoothConnected ? "bluetooth_connected" : Bluetooth.bluetoothEnabled ? "bluetooth" : "bluetooth_disabled"
+ onClicked: {
+ toggleBluetooth.running = true
+ }
+ altAction: () => {
+ Hyprland.dispatch(`exec ${ConfigOptions.apps.bluetooth}`)
+ Hyprland.dispatch("global quickshell:sidebarRightClose")
+ }
+ Process {
+ id: toggleBluetooth
+ command: ["bash", "-c", `bluetoothctl power ${Bluetooth.bluetoothEnabled ? "off" : "on"}`]
+ onRunningChanged: {
+ if(!running) {
+ Bluetooth.update()
+ }
+ }
+ }
+ StyledToolTip {
+ content: StringUtils.format(qsTr("{0} | Right-click to configure"),
+ (Bluetooth.bluetoothEnabled && Bluetooth.bluetoothDeviceName.length > 0) ?
+ Bluetooth.bluetoothDeviceName : qsTr("Bluetooth"))
+
+ }
+}
diff --git a/.config/quickshell/modules/sidebarRight/quickToggles/GameMode.qml b/.config/quickshell/modules/sidebarRight/quickToggles/GameMode.qml
new file mode 100644
index 000000000..1cd56acc7
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/quickToggles/GameMode.qml
@@ -0,0 +1,26 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "../"
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+
+QuickToggleButton {
+ property bool enabled: false
+ buttonIcon: "gamepad"
+ toggled: enabled
+
+ onClicked: {
+ enabled = !enabled
+ if (enabled) {
+ // gameModeOn.running = true
+ Hyprland.dispatch(`exec hyprctl --batch "keyword animations:enabled 0; keyword decoration:shadow:enabled 0; keyword decoration:blur:enabled 0; keyword general:gaps_in 0; keyword general:gaps_out 0; keyword general:border_size 1; keyword decoration:rounding 0; keyword general:allow_tearing 1"`)
+ } else {
+ Hyprland.dispatch("exec hyprctl reload")
+ }
+ }
+
+ StyledToolTip {
+ content: qsTr("Game mode")
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarRight/quickToggles/IdleInhibitor.qml b/.config/quickshell/modules/sidebarRight/quickToggles/IdleInhibitor.qml
new file mode 100644
index 000000000..b48d3467f
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/quickToggles/IdleInhibitor.qml
@@ -0,0 +1,32 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "../"
+import Quickshell.Io
+import Quickshell
+import Quickshell.Hyprland
+
+QuickToggleButton {
+ id: root
+ toggled: false
+ buttonIcon: "coffee"
+ onClicked: {
+ if (toggled) {
+ root.toggled = false
+ Hyprland.dispatch("exec pkill wayland-idle") // pkill doesn't accept too long names
+ } else {
+ root.toggled = true
+ Hyprland.dispatch('exec ${XDG_CONFIG_HOME:-$HOME/.config}/quickshell/scripts/wayland-idle-inhibitor.py')
+ }
+ }
+ Process {
+ id: fetchActiveState
+ running: true
+ command: ["bash", "-c", "pidof wayland-idle-inhibitor.py"]
+ onExited: (exitCode, exitStatus) => {
+ root.toggled = exitCode === 0
+ }
+ }
+ StyledToolTip {
+ content: qsTr("Keep system awake")
+ }
+}
diff --git a/.config/quickshell/modules/sidebarRight/quickToggles/NetworkToggle.qml b/.config/quickshell/modules/sidebarRight/quickToggles/NetworkToggle.qml
new file mode 100644
index 000000000..5271e3769
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/quickToggles/NetworkToggle.qml
@@ -0,0 +1,33 @@
+import "root:/services"
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "../"
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+
+QuickToggleButton {
+ toggled: Network.networkName.length > 0 && Network.networkName != "lo"
+ buttonIcon: Network.materialSymbol
+ onClicked: {
+ toggleNetwork.running = true
+ }
+ altAction: () => {
+ Hyprland.dispatch(`exec ${Network.ethernet ? ConfigOptions.apps.networkEthernet : ConfigOptions.apps.network}`)
+ Hyprland.dispatch("global quickshell:sidebarRightClose")
+ }
+ Process {
+ id: toggleNetwork
+ command: ["bash", "-c", "nmcli radio wifi | grep -q enabled && nmcli radio wifi off || nmcli radio wifi on"]
+ onRunningChanged: {
+ if(!running) {
+ Network.update()
+ }
+ }
+ }
+ StyledToolTip {
+ content: StringUtils.format(qsTr("{0} | Right-click to configure"), Network.networkName)
+ }
+}
diff --git a/.config/quickshell/modules/sidebarRight/quickToggles/NightLight.qml b/.config/quickshell/modules/sidebarRight/quickToggles/NightLight.qml
new file mode 100644
index 000000000..72df3e1ef
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/quickToggles/NightLight.qml
@@ -0,0 +1,42 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "../"
+import Quickshell.Io
+import Quickshell
+
+QuickToggleButton {
+ id: nightLightButton
+ property bool enabled: false
+ toggled: enabled
+ buttonIcon: "nightlight"
+ onClicked: {
+ nightLightButton.enabled = !nightLightButton.enabled
+ if (enabled) {
+ nightLightOn.startDetached()
+ }
+ else {
+ nightLightOff.startDetached()
+ }
+ }
+ Process {
+ id: nightLightOn
+ command: ["gammastep"]
+ }
+ Process {
+ id: nightLightOff
+ command: ["pkill", "gammastep"]
+ }
+ Process {
+ id: updateNightLightState
+ running: true
+ command: ["pidof", "gammastep"]
+ stdout: SplitParser {
+ onRead: (data) => { // if not empty then set toggled to true
+ nightLightButton.enabled = data.length > 0
+ }
+ }
+ }
+ StyledToolTip {
+ content: qsTr("Night Light")
+ }
+}
diff --git a/.config/quickshell/modules/sidebarRight/quickToggles/QuickToggleButton.qml b/.config/quickshell/modules/sidebarRight/quickToggles/QuickToggleButton.qml
new file mode 100644
index 000000000..c80f91ba6
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/quickToggles/QuickToggleButton.qml
@@ -0,0 +1,33 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Io
+
+GroupButton {
+ id: button
+ property string buttonIcon
+ baseWidth: altAction ? 60 : 40
+ baseHeight: 40
+ clickedWidth: baseWidth + 20
+ toggled: false
+ buttonRadius: (altAction && toggled) ? Appearance?.rounding.normal : Math.min(baseHeight, baseWidth) / 2
+ buttonRadiusPressed: Appearance?.rounding?.small
+
+ contentItem: MaterialSymbol {
+ anchors.centerIn: parent
+ iconSize: Appearance.font.pixelSize.larger
+ fill: toggled ? 1 : 0
+ color: toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ text: buttonIcon
+
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+
+}
diff --git a/.config/quickshell/modules/sidebarRight/todo/TaskList.qml b/.config/quickshell/modules/sidebarRight/todo/TaskList.qml
new file mode 100644
index 000000000..b727ab1cd
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/todo/TaskList.qml
@@ -0,0 +1,180 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+
+Item {
+ id: root
+ required property var taskList;
+ property string emptyPlaceholderIcon
+ property string emptyPlaceholderText
+ property int todoListItemSpacing: 5
+ property int todoListItemPadding: 8
+ property int listBottomPadding: 80
+
+ Flickable {
+ id: flickable
+ anchors.fill: parent
+ contentHeight: columnLayout.height
+
+ clip: true
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: flickable.width
+ height: flickable.height
+ radius: Appearance.rounding.small
+ }
+ }
+
+ ColumnLayout {
+ id: columnLayout
+ width: parent.width
+ spacing: 0
+ Repeater {
+ model: ScriptModel {
+ values: taskList
+ }
+ delegate: Item {
+ id: todoItem
+ property bool pendingDoneToggle: false
+ property bool pendingDelete: false
+ property bool enableHeightAnimation: false
+
+ Layout.fillWidth: true
+ implicitHeight: todoItemRectangle.implicitHeight + todoListItemSpacing
+ height: implicitHeight
+ clip: true
+
+ Behavior on implicitHeight {
+ enabled: enableHeightAnimation
+ NumberAnimation {
+ duration: Appearance.animation.elementMoveFast.duration
+ easing.type: Appearance.animation.elementMoveFast.type
+ easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
+ }
+ }
+
+ function startAction() {
+ enableHeightAnimation = true
+ todoItem.implicitHeight = 0
+ actionTimer.start()
+ }
+
+ Timer {
+ id: actionTimer
+ interval: Appearance.animation.elementMoveFast.duration
+ repeat: false
+ onTriggered: {
+ if (todoItem.pendingDelete) {
+ Todo.deleteItem(modelData.originalIndex)
+ } else if (todoItem.pendingDoneToggle) {
+ if (!modelData.done) Todo.markDone(modelData.originalIndex)
+ else Todo.markUnfinished(modelData.originalIndex)
+ }
+ }
+ }
+
+ Rectangle {
+ id: todoItemRectangle
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ implicitHeight: todoContentRowLayout.implicitHeight
+ color: Appearance.colors.colLayer2
+ radius: Appearance.rounding.small
+ ColumnLayout {
+ id: todoContentRowLayout
+ anchors.left: parent.left
+ anchors.right: parent.right
+
+ StyledText {
+ Layout.fillWidth: true // Needed for wrapping
+ Layout.leftMargin: 10
+ Layout.rightMargin: 10
+ Layout.topMargin: todoListItemPadding
+ id: todoContentText
+ text: modelData.content
+ wrapMode: Text.Wrap
+ }
+ RowLayout {
+ Layout.leftMargin: 10
+ Layout.rightMargin: 10
+ Layout.bottomMargin: todoListItemPadding
+ Item {
+ Layout.fillWidth: true
+ }
+ TodoItemActionButton {
+ Layout.fillWidth: false
+ onClicked: {
+ todoItem.pendingDoneToggle = true
+ todoItem.startAction()
+ }
+ contentItem: MaterialSymbol {
+ anchors.centerIn: parent
+ horizontalAlignment: Text.AlignHCenter
+ text: modelData.done ? "remove_done" : "check"
+ iconSize: Appearance.font.pixelSize.larger
+ color: Appearance.colors.colOnLayer1
+ }
+ }
+ TodoItemActionButton {
+ Layout.fillWidth: false
+ onClicked: {
+ todoItem.pendingDelete = true
+ todoItem.startAction()
+ }
+ contentItem: MaterialSymbol {
+ anchors.centerIn: parent
+ horizontalAlignment: Text.AlignHCenter
+ text: "delete_forever"
+ iconSize: Appearance.font.pixelSize.larger
+ color: Appearance.colors.colOnLayer1
+ }
+ }
+ }
+ }
+ }
+ }
+
+ }
+ // Bottom padding
+ Item {
+ implicitHeight: listBottomPadding
+ }
+ }
+ }
+
+ Item { // Placeholder when list is empty
+ visible: opacity > 0
+ opacity: taskList.length === 0 ? 1 : 0
+ anchors.fill: parent
+
+ Behavior on opacity {
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: 5
+
+ MaterialSymbol {
+ Layout.alignment: Qt.AlignHCenter
+ iconSize: 55
+ color: Appearance.m3colors.m3outline
+ text: emptyPlaceholderIcon
+ }
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ font.pixelSize: Appearance.font.pixelSize.normal
+ color: Appearance.m3colors.m3outline
+ horizontalAlignment: Text.AlignHCenter
+ text: emptyPlaceholderText
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarRight/todo/TodoItemActionButton.qml b/.config/quickshell/modules/sidebarRight/todo/TodoItemActionButton.qml
new file mode 100644
index 000000000..e013a18bc
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/todo/TodoItemActionButton.qml
@@ -0,0 +1,35 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+RippleButton {
+ id: button
+ property string buttonText: ""
+ property string tooltipText: ""
+
+ implicitHeight: 30
+ implicitWidth: implicitHeight
+
+ Behavior on implicitWidth {
+ SmoothedAnimation {
+ velocity: Appearance.animation.elementMove.velocity
+ }
+ }
+
+ buttonRadius: Appearance.rounding.small
+
+ contentItem: StyledText {
+ text: buttonText
+ horizontalAlignment: Text.AlignHCenter
+ font.pixelSize: Appearance.font.pixelSize.larger
+ color: Appearance.colors.colOnLayer1
+ }
+
+ StyledToolTip {
+ content: tooltipText
+ extraVisibleCondition: tooltipText.length > 0
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarRight/todo/TodoWidget.qml b/.config/quickshell/modules/sidebarRight/todo/TodoWidget.qml
new file mode 100644
index 000000000..9f5f4fd08
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/todo/TodoWidget.qml
@@ -0,0 +1,308 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import "root:/modules/common/functions/color_utils.js" as ColorUtils
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Effects
+import QtQuick.Layouts
+
+Item {
+ id: root
+ property int currentTab: 0
+ property var tabButtonList: [{"icon": "checklist", "name": qsTr("Unfinished")}, {"name": qsTr("Done"), "icon": "check_circle"}]
+ property bool showAddDialog: false
+ property int dialogMargins: 20
+ property int fabSize: 48
+ property int fabMargins: 14
+
+ Keys.onPressed: (event) => {
+ if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) && event.modifiers === Qt.NoModifier) {
+ if (event.key === Qt.Key_PageDown) {
+ currentTab = Math.min(currentTab + 1, root.tabButtonList.length - 1)
+ } else if (event.key === Qt.Key_PageUp) {
+ currentTab = Math.max(currentTab - 1, 0)
+ }
+ event.accepted = true;
+ }
+ // Open add dialog on "N" (any modifiers)
+ else if (event.key === Qt.Key_N) {
+ root.showAddDialog = true
+ event.accepted = true;
+ }
+ // Close dialog on Esc if open
+ else if (event.key === Qt.Key_Escape && root.showAddDialog) {
+ root.showAddDialog = false
+ event.accepted = true;
+ }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ TabBar {
+ id: tabBar
+ Layout.fillWidth: true
+ currentIndex: currentTab
+ onCurrentIndexChanged: currentTab = currentIndex
+
+ background: Item {
+ WheelHandler {
+ onWheel: (event) => {
+ if (event.angleDelta.y < 0)
+ tabBar.currentIndex = Math.min(tabBar.currentIndex + 1, root.tabButtonList.length - 1)
+ else if (event.angleDelta.y > 0)
+ tabBar.currentIndex = Math.max(tabBar.currentIndex - 1, 0)
+ }
+ acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+ }
+ }
+
+ Repeater {
+ model: root.tabButtonList
+ delegate: SecondaryTabButton {
+ selected: (index == currentTab)
+ buttonText: modelData.name
+ buttonIcon: modelData.icon
+ }
+ }
+ }
+
+ Item { // Tab indicator
+ id: tabIndicator
+ Layout.fillWidth: true
+ height: 3
+ property bool enableIndicatorAnimation: false
+ Connections {
+ target: root
+ function onCurrentTabChanged() {
+ tabIndicator.enableIndicatorAnimation = true
+ }
+ }
+
+ Rectangle {
+ id: indicator
+ property int tabCount: root.tabButtonList.length
+ property real fullTabSize: root.width / tabCount;
+ property real targetWidth: tabBar.contentItem.children[0].children[tabBar.currentIndex].tabContentWidth
+
+ implicitWidth: targetWidth
+ anchors {
+ top: parent.top
+ bottom: parent.bottom
+ }
+
+ x: tabBar.currentIndex * fullTabSize + (fullTabSize - targetWidth) / 2
+
+ color: Appearance.colors.colPrimary
+ radius: Appearance.rounding.full
+
+ Behavior on x {
+ enabled: tabIndicator.enableIndicatorAnimation
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+
+ Behavior on implicitWidth {
+ enabled: tabIndicator.enableIndicatorAnimation
+ animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
+ }
+ }
+ }
+
+ Rectangle { // Tabbar bottom border
+ id: tabBarBottomBorder
+ Layout.fillWidth: true
+ height: 1
+ color: Appearance.colors.colOutlineVariant
+ }
+
+ SwipeView {
+ id: swipeView
+ Layout.topMargin: 10
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ spacing: 10
+ clip: true
+ currentIndex: currentTab
+ onCurrentIndexChanged: {
+ tabIndicator.enableIndicatorAnimation = true
+ currentTab = currentIndex
+ }
+
+ // To Do tab
+ TaskList {
+ listBottomPadding: root.fabSize + root.fabMargins * 2
+ emptyPlaceholderIcon: "check_circle"
+ emptyPlaceholderText: qsTr("Nothing here!")
+ taskList: Todo.list
+ .map(function(item, i) { return Object.assign({}, item, {originalIndex: i}); })
+ .filter(function(item) { return !item.done; })
+ }
+ TaskList {
+ listBottomPadding: root.fabSize + root.fabMargins * 2
+ emptyPlaceholderIcon: "checklist"
+ emptyPlaceholderText: qsTr("Finished tasks will go here")
+ taskList: Todo.list
+ .map(function(item, i) { return Object.assign({}, item, {originalIndex: i}); })
+ .filter(function(item) { return item.done; })
+ }
+
+ }
+ }
+
+ // + FAB
+ StyledRectangularShadow {
+ target: fabButton
+ radius: Appearance.rounding.normal
+ }
+ Button {
+ id: fabButton
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.rightMargin: root.fabMargins
+ anchors.bottomMargin: root.fabMargins
+ width: root.fabSize
+ height: root.fabSize
+ PointingHandInteraction {}
+
+ onClicked: root.showAddDialog = true
+
+ background: Rectangle {
+ id: fabBackground
+ anchors.fill: parent
+ radius: Appearance.rounding.normal
+ color: (fabButton.down) ? Appearance.colors.colPrimaryContainerActive : (fabButton.hovered ? Appearance.colors.colPrimaryContainerHover : Appearance.colors.colPrimaryContainer)
+
+ Behavior on color {
+ animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
+ }
+ }
+
+ contentItem: MaterialSymbol {
+ text: "add"
+ horizontalAlignment: Text.AlignHCenter
+ iconSize: Appearance.font.pixelSize.huge
+ color: Appearance.m3colors.m3onPrimaryContainer
+ }
+ }
+
+ Item {
+ anchors.fill: parent
+ z: 9999
+
+ visible: opacity > 0
+ opacity: root.showAddDialog ? 1 : 0
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Appearance.animation.elementMoveFast.duration
+ easing.type: Appearance.animation.elementMoveFast.type
+ easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
+ }
+ }
+
+ onVisibleChanged: {
+ if (!visible) {
+ todoInput.text = ""
+ fabButton.focus = true
+ }
+ }
+
+ Rectangle { // Scrim
+ anchors.fill: parent
+ radius: Appearance.rounding.small
+ color: Appearance.colors.colScrim
+ MouseArea {
+ hoverEnabled: true
+ anchors.fill: parent
+ preventStealing: true
+ propagateComposedEvents: false
+ }
+ }
+
+ Rectangle { // The dialog
+ id: dialog
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: root.dialogMargins
+ implicitHeight: dialogColumnLayout.implicitHeight
+
+ color: Appearance.colors.colSurfaceContainerHigh
+ radius: Appearance.rounding.normal
+
+ function addTask() {
+ if (todoInput.text.length > 0) {
+ Todo.addTask(todoInput.text)
+ todoInput.text = ""
+ root.showAddDialog = false
+ root.currentTab = 0 // Show unfinished tasks
+ }
+ }
+
+ ColumnLayout {
+ id: dialogColumnLayout
+ anchors.fill: parent
+ spacing: 16
+
+ StyledText {
+ Layout.topMargin: 16
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.alignment: Qt.AlignLeft
+ color: Appearance.m3colors.m3onSurface
+ font.pixelSize: Appearance.font.pixelSize.larger
+ text: qsTr("Add task")
+ }
+
+ TextField {
+ id: todoInput
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ padding: 10
+ color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
+ renderType: Text.NativeRendering
+ selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
+ selectionColor: Appearance.colors.colSecondaryContainer
+ placeholderText: qsTr("Task description")
+ placeholderTextColor: Appearance.m3colors.m3outline
+ focus: root.showAddDialog
+ onAccepted: dialog.addTask()
+
+ background: Rectangle {
+ anchors.fill: parent
+ radius: Appearance.rounding.verysmall
+ border.width: 2
+ border.color: todoInput.activeFocus ? Appearance.colors.colPrimary : Appearance.m3colors.m3outline
+ color: "transparent"
+ }
+
+ cursorDelegate: Rectangle {
+ width: 1
+ color: todoInput.activeFocus ? Appearance.colors.colPrimary : "transparent"
+ radius: 1
+ }
+ }
+
+ RowLayout {
+ Layout.bottomMargin: 16
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.alignment: Qt.AlignRight
+ spacing: 5
+
+ DialogButton {
+ buttonText: qsTr("Cancel")
+ onClicked: root.showAddDialog = false
+ }
+ DialogButton {
+ buttonText: qsTr("Add")
+ enabled: todoInput.text.length > 0
+ onClicked: dialog.addTask()
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/modules/sidebarRight/volumeMixer/AudioDeviceSelectorButton.qml b/.config/quickshell/modules/sidebarRight/volumeMixer/AudioDeviceSelectorButton.qml
new file mode 100644
index 000000000..cc956cc03
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/volumeMixer/AudioDeviceSelectorButton.qml
@@ -0,0 +1,55 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Widgets
+import Quickshell.Services.Pipewire
+
+GroupButton {
+ id: button
+ required property bool input
+
+ buttonRadius: Appearance.rounding.small
+ colBackground: Appearance.colors.colLayer2
+ colBackgroundHover: Appearance.colors.colLayer2Hover
+ colBackgroundActive: Appearance.colors.colLayer2Active
+ clickedWidth: baseWidth + 30
+
+ contentItem: RowLayout {
+ anchors.fill: parent
+ anchors.margins: 5
+ spacing: 5
+
+ MaterialSymbol {
+ Layout.alignment: Qt.AlignVCenter
+ Layout.fillWidth: false
+ Layout.leftMargin: 5
+ color: Appearance.colors.colOnLayer2
+ iconSize: Appearance.font.pixelSize.hugeass
+ text: input ? "mic_external_on" : "media_output"
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.rightMargin: 5
+ spacing: 0
+ StyledText {
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ font.pixelSize: Appearance.font.pixelSize.normal
+ text: input ? qsTr("Input") : qsTr("Output")
+ color: Appearance.colors.colOnLayer2
+ }
+ StyledText {
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ font.pixelSize: Appearance.font.pixelSize.smaller
+ text: (input ? Pipewire.defaultAudioSource?.description : Pipewire.defaultAudioSink?.description) ?? qsTr("Unknown")
+ color: Appearance.m3colors.m3outline
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixer.qml b/.config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixer.qml
new file mode 100644
index 000000000..2e1570f30
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixer.qml
@@ -0,0 +1,288 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Services.Pipewire
+
+
+Item {
+ id: root
+ property bool showDeviceSelector: false
+ property bool deviceSelectorInput
+ property int dialogMargins: 16
+ property PwNode selectedDevice
+
+ function showDeviceSelectorDialog(input: bool) {
+ root.selectedDevice = null
+ root.showDeviceSelector = true
+ root.deviceSelectorInput = input
+ }
+
+ Keys.onPressed: (event) => {
+ // Close dialog on pressing Esc if open
+ if (event.key === Qt.Key_Escape && root.showDeviceSelector) {
+ root.showDeviceSelector = false
+ event.accepted = true;
+ }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Flickable {
+ id: flickable
+ anchors.fill: parent
+ contentHeight: volumeMixerColumnLayout.height
+
+ clip: true
+ layer.enabled: true
+ layer.effect: OpacityMask {
+ maskSource: Rectangle {
+ width: flickable.width
+ height: flickable.height
+ radius: Appearance.rounding.normal
+ }
+ }
+
+ ColumnLayout {
+ id: volumeMixerColumnLayout
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: 10
+ spacing: 10
+
+ // Get a list of nodes that output to the default sink
+ PwNodeLinkTracker {
+ id: linkTracker
+ node: Pipewire.defaultAudioSink
+ }
+
+ Repeater {
+ model: linkTracker.linkGroups
+
+ VolumeMixerEntry {
+ Layout.fillWidth: true
+ // Get links to the default sinnk
+ required property PwLinkGroup modelData
+ // Consider sources that output to the default sink
+ node: modelData.source
+ }
+ }
+ }
+ }
+
+ // Placeholder when list is empty
+ Item {
+ anchors.fill: flickable
+
+ visible: opacity > 0
+ opacity: (linkTracker.linkGroups.length === 0) ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Appearance.animation.menuDecel.duration
+ easing.type: Appearance.animation.menuDecel.type
+ }
+ }
+
+ ColumnLayout {
+ anchors.centerIn: parent
+ spacing: 5
+
+ MaterialSymbol {
+ Layout.alignment: Qt.AlignHCenter
+ iconSize: 55
+ color: Appearance.m3colors.m3outline
+ text: "brand_awareness"
+ }
+ StyledText {
+ Layout.alignment: Qt.AlignHCenter
+ font.pixelSize: Appearance.font.pixelSize.normal
+ color: Appearance.m3colors.m3outline
+ horizontalAlignment: Text.AlignHCenter
+ text: qsTr("No audio source")
+ }
+ }
+ }
+ }
+ // Device selector
+ ButtonGroup {
+ id: deviceSelectorRowLayout
+ Layout.fillWidth: true
+ Layout.fillHeight: false
+ AudioDeviceSelectorButton {
+ Layout.fillWidth: true
+ input: false
+ onClicked: root.showDeviceSelectorDialog(input)
+ }
+ AudioDeviceSelectorButton {
+ Layout.fillWidth: true
+ input: true
+ onClicked: root.showDeviceSelectorDialog(input)
+ }
+ }
+ }
+
+ // Device selector dialog
+ Item {
+ anchors.fill: parent
+ z: 9999
+
+ visible: opacity > 0
+ opacity: root.showDeviceSelector ? 1 : 0
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Appearance.animation.elementMoveFast.duration
+ easing.type: Appearance.animation.elementMoveFast.type
+ easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
+ }
+ }
+
+ Rectangle { // Scrim
+ id: scrimOverlay
+ anchors.fill: parent
+ radius: Appearance.rounding.small
+ color: Appearance.colors.colScrim
+ MouseArea {
+ hoverEnabled: true
+ anchors.fill: parent
+ preventStealing: true
+ propagateComposedEvents: false
+ }
+ }
+
+ Rectangle { // The dialog
+ id: dialog
+ color: Appearance.colors.colSurfaceContainerHigh
+ radius: Appearance.rounding.normal
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: 30
+ implicitHeight: dialogColumnLayout.implicitHeight
+
+ ColumnLayout {
+ id: dialogColumnLayout
+ anchors.fill: parent
+ spacing: 16
+
+ StyledText {
+ id: dialogTitle
+ Layout.topMargin: dialogMargins
+ Layout.leftMargin: dialogMargins
+ Layout.rightMargin: dialogMargins
+ Layout.alignment: Qt.AlignLeft
+ color: Appearance.m3colors.m3onSurface
+ font.pixelSize: Appearance.font.pixelSize.larger
+ text: `Select ${root.deviceSelectorInput ? "input" : "output"} device`
+ }
+
+ Rectangle {
+ color: Appearance.m3colors.m3outline
+ implicitHeight: 1
+ Layout.fillWidth: true
+ Layout.leftMargin: dialogMargins
+ Layout.rightMargin: dialogMargins
+ }
+
+ Flickable {
+ id: dialogFlickable
+ Layout.fillWidth: true
+ clip: true
+ implicitHeight: Math.min(scrimOverlay.height - dialogMargins * 8 - dialogTitle.height - dialogButtonsRowLayout.height, devicesColumnLayout.implicitHeight)
+
+ contentHeight: devicesColumnLayout.implicitHeight
+
+ ColumnLayout {
+ id: devicesColumnLayout
+ anchors.fill: parent
+ Layout.fillWidth: true
+ spacing: 0
+
+ Repeater {
+ model: ScriptModel {
+ values: Pipewire.nodes.values.filter(node => {
+ return !node.isStream && node.isSink !== root.deviceSelectorInput && node.audio
+ })
+ }
+
+ // This could and should be refractored, but all data becomes null when passed wtf
+ delegate: StyledRadioButton {
+ id: radioButton
+ required property var modelData
+ Layout.leftMargin: root.dialogMargins
+ Layout.rightMargin: root.dialogMargins
+ Layout.fillWidth: true
+
+ description: modelData.description
+ checked: modelData.id === Pipewire.defaultAudioSink?.id
+
+ Connections {
+ target: root
+ function onShowDeviceSelectorChanged() {
+ if(!root.showDeviceSelector) return;
+ radioButton.checked = (modelData.id === Pipewire.defaultAudioSink?.id)
+ }
+ }
+
+ onCheckedChanged: {
+ if (checked) {
+ root.selectedDevice = modelData
+ }
+ }
+ }
+ }
+ Item {
+ implicitHeight: dialogMargins
+ }
+ }
+ }
+
+ Rectangle {
+ color: Appearance.m3colors.m3outline
+ implicitHeight: 1
+ Layout.fillWidth: true
+ Layout.leftMargin: dialogMargins
+ Layout.rightMargin: dialogMargins
+ }
+
+ RowLayout {
+ id: dialogButtonsRowLayout
+ Layout.bottomMargin: dialogMargins
+ Layout.leftMargin: dialogMargins
+ Layout.rightMargin: dialogMargins
+ Layout.alignment: Qt.AlignRight
+
+ DialogButton {
+ buttonText: qsTr("Cancel")
+ onClicked: {
+ root.showDeviceSelector = false
+ }
+ }
+ DialogButton {
+ buttonText: qsTr("OK")
+ onClicked: {
+ root.showDeviceSelector = false
+ if (root.selectedDevice) {
+ if (root.deviceSelectorInput) {
+ Pipewire.preferredDefaultAudioSource = root.selectedDevice
+ } else {
+ Pipewire.preferredDefaultAudioSink = root.selectedDevice
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/.config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml b/.config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml
new file mode 100644
index 000000000..c4600c7b8
--- /dev/null
+++ b/.config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml
@@ -0,0 +1,65 @@
+import "root:/modules/common"
+import "root:/modules/common/widgets"
+import "root:/services"
+import Qt5Compat.GraphicalEffects
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Widgets
+import Quickshell.Services.Pipewire
+
+Item {
+ id: root
+ required property PwNode node;
+ PwObjectTracker { objects: [ node ] }
+
+ implicitHeight: rowLayout.implicitHeight
+
+ RowLayout {
+ id: rowLayout
+ anchors.fill: parent
+ spacing: 10
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 0
+
+ RowLayout {
+ StyledText {
+ Layout.fillWidth: true
+ font.pixelSize: Appearance.font.pixelSize.normal
+ elide: Text.ElideRight
+ text: {
+ // application.name -> description -> name
+ const app = root.node.properties["application.name"] ?? (root.node.description != "" ? root.node.description : root.node.name);
+ const media = root.node.properties["media.name"];
+ return media != undefined ? `${app} • ${media}` : app;
+ }
+ }
+ }
+
+ RowLayout {
+ Image {
+ property real size: slider.trackHeight * 1.3
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+ visible: source != ""
+ sourceSize.width: size
+ sourceSize.height: size
+ source: {
+ let icon;
+ icon = AppSearch.guessIcon(root.node.properties["application.icon-name"]);
+ if (AppSearch.iconExists(icon)) return Quickshell.iconPath(icon, "image-missing");
+ icon = AppSearch.guessIcon(root.node.properties["node.name"]);
+ return Quickshell.iconPath(icon, "image-missing");
+ }
+ }
+ StyledSlider {
+ id: slider
+ value: root.node.audio.volume
+ onValueChanged: root.node.audio.volume = value
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.config/ags/scripts/ai/show-installed-ollama-models.sh b/.config/quickshell/scripts/ai/show-installed-ollama-models.sh
similarity index 100%
rename from .config/ags/scripts/ai/show-installed-ollama-models.sh
rename to .config/quickshell/scripts/ai/show-installed-ollama-models.sh
diff --git a/.config/quickshell/scripts/applycolor.sh b/.config/quickshell/scripts/applycolor.sh
new file mode 100755
index 000000000..58fad4ce3
--- /dev/null
+++ b/.config/quickshell/scripts/applycolor.sh
@@ -0,0 +1,66 @@
+#!/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/quickshell"
+CACHE_DIR="$XDG_CACHE_HOME/quickshell"
+STATE_DIR="$XDG_STATE_HOME/quickshell"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+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 "$STATE_DIR"/user/generated ]; then
+ mkdir -p "$STATE_DIR"/user/generated
+fi
+cd "$CONFIG_DIR" || exit
+
+colornames=''
+colorstrings=''
+colorlist=()
+colorvalues=()
+
+colornames=$(cat $STATE_DIR/user/generated/material_colors.scss | cut -d: -f1)
+colorstrings=$(cat $STATE_DIR/user/generated/material_colors.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_term() {
+ # Check if terminal escape sequence template exists
+ if [ ! -f "$SCRIPT_DIR"/terminal/sequences.txt ]; then
+ echo "Template file not found for Terminal. Skipping that."
+ return
+ fi
+ # Copy template
+ mkdir -p "$STATE_DIR"/user/generated/terminal
+ cp "$SCRIPT_DIR"/terminal/sequences.txt "$STATE_DIR"/user/generated/terminal/sequences.txt
+ # Apply colors
+ for i in "${!colorlist[@]}"; do
+ sed -i "s/${colorlist[$i]} #/${colorvalues[$i]#\#}/g" "$STATE_DIR"/user/generated/terminal/sequences.txt
+ done
+
+ sed -i "s/\$alpha/$term_alpha/g" "$STATE_DIR/user/generated/terminal/sequences.txt"
+
+ for file in /dev/pts/*; do
+ if [[ $file =~ ^/dev/pts/[0-9]+$ ]]; then
+ {
+ cat "$STATE_DIR"/user/generated/terminal/sequences.txt >"$file"
+ } & disown || true
+ fi
+ done
+}
+
+apply_qt() {
+ sh "$CONFIG_DIR/scripts/kvantum/materialQT.sh" # generate kvantum theme
+ python "$CONFIG_DIR/scripts/kvantum/changeAdwColors.py" # apply config colors
+}
+
+apply_ags() {
+ pidof agsv1 && agsv1 run-js "handleStyles(false);"
+ pidof agsv1 && agsv1 run-js 'openColorScheme.value = true; Utils.timeout(2000, () => openColorScheme.value = false);'
+}
+
+apply_ags &
+apply_qt &
+apply_term &
diff --git a/.config/quickshell/scripts/cava/raw_output_config.txt b/.config/quickshell/scripts/cava/raw_output_config.txt
new file mode 100644
index 000000000..7760e4ea2
--- /dev/null
+++ b/.config/quickshell/scripts/cava/raw_output_config.txt
@@ -0,0 +1,17 @@
+[general]
+mode = waves
+framerate = 60
+autosens = 1
+bars = 50
+
+[output]
+method = raw
+raw_target = /dev/stdout
+data_format = ascii
+channels = mono
+mono_option = average
+
+[smoothing]
+noise_reduction = 20
+
+
diff --git a/.config/ags/scripts/color_generation/generate_colors_material.py b/.config/quickshell/scripts/generate_colors_material.py
similarity index 95%
rename from .config/ags/scripts/color_generation/generate_colors_material.py
rename to .config/quickshell/scripts/generate_colors_material.py
index 3755ae571..db6b1664b 100755
--- a/.config/ags/scripts/color_generation/generate_colors_material.py
+++ b/.config/quickshell/scripts/generate_colors_material.py
@@ -87,27 +87,26 @@ elif args.color is not None:
argb = hex_to_argb(args.color)
hct = Hct.from_int(argb)
-if args.scheme == 'fruitsalad':
+if args.scheme == 'scheme-fruit-salad':
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme
-elif args.scheme == 'expressive':
+elif args.scheme == 'scheme-expressive':
from materialyoucolor.scheme.scheme_expressive import SchemeExpressive as Scheme
-elif args.scheme == 'monochrome':
+elif args.scheme == 'scheme-monochrome':
from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome as Scheme
-elif args.scheme == 'rainbow':
+elif args.scheme == 'scheme-rainbow':
from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow as Scheme
-elif args.scheme == 'tonalspot':
+elif args.scheme == 'scheme-tonal-spot':
from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot as Scheme
-elif args.scheme == 'neutral':
+elif args.scheme == 'scheme-neutral':
from materialyoucolor.scheme.scheme_neutral import SchemeNeutral as Scheme
-elif args.scheme == 'fidelity':
+elif args.scheme == 'scheme-fidelity':
from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity as Scheme
-elif args.scheme == 'content':
+elif args.scheme == 'scheme-content':
from materialyoucolor.scheme.scheme_content import SchemeContent as Scheme
-elif args.scheme == 'vibrant':
+elif args.scheme == 'scheme-vibrant':
from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant as Scheme
else:
- from schemes.scheme_morevibrant import SchemeMoreVibrant as Scheme
-
+ from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot as Scheme
# Generate
scheme = Scheme(hct, darkmode, 0.0)
diff --git a/.config/ags/scripts/hyprland/get_keybinds.py b/.config/quickshell/scripts/hyprland/get_keybinds.py
similarity index 100%
rename from .config/ags/scripts/hyprland/get_keybinds.py
rename to .config/quickshell/scripts/hyprland/get_keybinds.py
diff --git a/.config/ags/scripts/kvantum/adwsvg.py b/.config/quickshell/scripts/kvantum/adwsvg.py
similarity index 96%
rename from .config/ags/scripts/kvantum/adwsvg.py
rename to .config/quickshell/scripts/kvantum/adwsvg.py
index 7f2ae8e2c..10ce1d150 100644
--- a/.config/ags/scripts/kvantum/adwsvg.py
+++ b/.config/quickshell/scripts/kvantum/adwsvg.py
@@ -38,7 +38,7 @@ def main():
xdg_config_home = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
xdg_state_home = os.environ.get("XDG_STATE_HOME", os.path.expanduser("~/.local/state"))
- scss_file = os.path.join(xdg_state_home, "ags", "scss", "_material.scss")
+ scss_file = os.path.join(xdg_state_home, "quickshell", "user", "generated", "material_colors.scss")
svg_path = os.path.join(xdg_config_home, "Kvantum", "Colloid", "Colloid.svg")
output_path = os.path.join(xdg_config_home, "Kvantum", "MaterialAdw", "MaterialAdw.svg")
diff --git a/.config/ags/scripts/kvantum/adwsvgDark.py b/.config/quickshell/scripts/kvantum/adwsvgDark.py
similarity index 97%
rename from .config/ags/scripts/kvantum/adwsvgDark.py
rename to .config/quickshell/scripts/kvantum/adwsvgDark.py
index 5e09d8360..9fb097740 100644
--- a/.config/ags/scripts/kvantum/adwsvgDark.py
+++ b/.config/quickshell/scripts/kvantum/adwsvgDark.py
@@ -38,7 +38,7 @@ def main():
xdg_config_home = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
xdg_state_home = os.environ.get("XDG_STATE_HOME", os.path.expanduser("~/.local/state"))
- scss_file = os.path.join(xdg_state_home, "ags", "scss", "_material.scss")
+ scss_file = os.path.join(xdg_state_home, "quickshell", "user", "generated", "material_colors.scss")
svg_path = os.path.join(xdg_config_home, "Kvantum", "Colloid", "ColloidDark.svg")
output_path = os.path.join(xdg_config_home, "Kvantum", "MaterialAdw", "MaterialAdw.svg")
diff --git a/.config/ags/scripts/kvantum/changeAdwColors.py b/.config/quickshell/scripts/kvantum/changeAdwColors.py
similarity index 96%
rename from .config/ags/scripts/kvantum/changeAdwColors.py
rename to .config/quickshell/scripts/kvantum/changeAdwColors.py
index a7d1e6b9d..26d067ad5 100644
--- a/.config/ags/scripts/kvantum/changeAdwColors.py
+++ b/.config/quickshell/scripts/kvantum/changeAdwColors.py
@@ -32,7 +32,7 @@ if __name__ == "__main__":
xdg_state_home = os.environ.get("XDG_STATE_HOME", os.path.expanduser("~/.local/state"))
config_file = os.path.join(xdg_config_home, "Kvantum", "MaterialAdw", "MaterialAdw.kvconfig")
- scss_file = os.path.join(xdg_state_home, "ags", "scss", "_material.scss")
+ scss_file = os.path.join(xdg_state_home, "quickshell", "user", "generated", "material_colors.scss")
# Define your mappings here
mappings = {
diff --git a/.config/ags/scripts/kvantum/materialQT.sh b/.config/quickshell/scripts/kvantum/materialQT.sh
similarity index 77%
rename from .config/ags/scripts/kvantum/materialQT.sh
rename to .config/quickshell/scripts/kvantum/materialQT.sh
index 7495fad37..3d1f8a7bf 100755
--- a/.config/ags/scripts/kvantum/materialQT.sh
+++ b/.config/quickshell/scripts/kvantum/materialQT.sh
@@ -3,18 +3,17 @@
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"
+CONFIG_DIR="$XDG_CONFIG_HOME/quickshell"
+CACHE_DIR="$XDG_CACHE_HOME/quickshell"
+STATE_DIR="$XDG_STATE_HOME/quickshell"
get_light_dark() {
- lightdark=""
- if [ ! -f "$STATE_DIR/user/colormode.txt" ]; then
- echo "" >"$STATE_DIR/user/colormode.txt"
+ current_mode=$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null | tr -d "'")
+ if [[ "$current_mode" == "prefer-dark" ]]; then
+ echo "dark"
else
- lightdark=$(sed -n '1p' "$STATE_DIR/user/colormode.txt")
+ echo "light"
fi
- echo "$lightdark"
}
apply_qt() {
diff --git a/.config/quickshell/scripts/switchwall.sh b/.config/quickshell/scripts/switchwall.sh
new file mode 100755
index 000000000..dffd9bd61
--- /dev/null
+++ b/.config/quickshell/scripts/switchwall.sh
@@ -0,0 +1,292 @@
+#!/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/quickshell"
+CACHE_DIR="$XDG_CACHE_HOME/quickshell"
+STATE_DIR="$XDG_STATE_HOME/quickshell"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+MATUGEN_DIR="$XDG_CONFIG_HOME/matugen"
+terminalscheme="$XDG_CONFIG_HOME/quickshell/scripts/terminal/scheme-base.json"
+
+pre_process() {
+ local mode_flag="$1"
+ # Set GNOME color-scheme if mode_flag is dark or light
+ if [[ "$mode_flag" == "dark" ]]; then
+ gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'
+ gsettings set org.gnome.desktop.interface gtk-theme 'adw-gtk3-dark'
+ elif [[ "$mode_flag" == "light" ]]; then
+ gsettings set org.gnome.desktop.interface color-scheme 'prefer-light'
+ gsettings set org.gnome.desktop.interface gtk-theme 'adw-gtk3'
+ fi
+
+ if [ ! -d "$CACHE_DIR"/user/generated ]; then
+ mkdir -p "$CACHE_DIR"/user/generated
+ fi
+}
+
+post_process() {
+ local screen_width="$1"
+ local screen_height="$2"
+ local wallpaper_path="$3"
+
+ # Determine the largest region on the wallpaper that's sufficiently un-busy to put widgets in
+ if [ ! -f "$MATUGEN_DIR/scripts/least_busy_region.py" ]; then
+ echo "Error: least_busy_region.py script not found in $MATUGEN_DIR/scripts/"
+ else
+ "$MATUGEN_DIR/scripts/least_busy_region.py" \
+ --screen-width "$screen_width" --screen-height "$screen_height" \
+ --width 300 --height 200 \
+ "$wallpaper_path" > "$STATE_DIR"/user/generated/wallpaper/least_busy_region.json
+ fi
+}
+
+check_and_prompt_upscale() {
+ local img="$1"
+ min_width_desired="$(hyprctl monitors -j | jq '([.[].width] | max)' | xargs)" # max monitor width
+ min_height_desired="$(hyprctl monitors -j | jq '([.[].height] | max)' | xargs)" # max monitor height
+
+ if command -v identify &>/dev/null && [ -f "$img" ]; then
+ local img_width img_height
+ if is_video "$img"; then # Not check resolution for videos, just let em pass
+ img_width=$min_width_desired
+ img_height=$min_height_desired
+ else
+ img_width=$(identify -format "%w" "$img" 2>/dev/null)
+ img_height=$(identify -format "%h" "$img" 2>/dev/null)
+ fi
+ if [[ "$img_width" -lt "$min_width_desired" || "$img_height" -lt "$min_height_desired" ]]; then
+ action=$(notify-send "Upscale?" \
+ "Image resolution (${img_width}x${img_height}) is lower than screen resolution (${min_width_desired}x${min_height_desired})" \
+ -A "open_upscayl=Open Upscayl"\
+ -a "Wallpaper switcher")
+ if [[ "$action" == "open_upscayl" ]]; then
+ if command -v upscayl &>/dev/null; then
+ nohup upscayl > /dev/null 2>&1 &
+ else
+ action2=$(notify-send \
+ -a "Wallpaper switcher" \
+ -c "im.error" \
+ -A "install_upscayl=Install Upscayl (Arch)" \
+ "Install Upscayl?" \
+ "yay -S upscayl-bin")
+ if [[ "$action2" == "install_upscayl" ]]; then
+ kitty -1 yay -S upscayl-bin
+ if command -v upscayl &>/dev/null; then
+ nohup upscayl > /dev/null 2>&1 &
+ fi
+ fi
+ fi
+ fi
+ fi
+ fi
+}
+
+THUMBNAIL_DIR="/tmp/mpvpaper_thumbnails"
+CUSTOM_DIR="$XDG_CONFIG_HOME/hypr/custom"
+RESTORE_SCRIPT_DIR="$CUSTOM_DIR/scripts"
+RESTORE_SCRIPT="$RESTORE_SCRIPT_DIR/__restore_video_wallpaper.sh"
+VIDEO_OPTS="no-audio loop hwdec=auto scale=bilinear interpolation=no video-sync=display-resample panscan=1.0 video-scale-x=1.0 video-scale-y=1.0 video-align-x=0.5 video-align-y=0.5"
+
+is_video() {
+ local extension="${1##*.}"
+ [[ "$extension" == "mp4" || "$extension" == "mkv" || "$extension" == "webm" ]] && return 0 || return 1
+}
+
+kill_existing_mpvpaper() {
+ pkill -f -9 mpvpaper || true
+}
+
+create_restore_script() {
+ local video_path=$1
+ cat > "$RESTORE_SCRIPT.tmp" << EOF
+#!/bin/bash
+# Generated by switchwall.sh - Don't modify it by yourself.
+# Time: $(date)
+
+pkill -f -9 mpvpaper
+
+for monitor in \$(hyprctl monitors -j | jq -r '.[] | .name'); do
+ mpvpaper -o "$VIDEO_OPTS" "\$monitor" "$video_path" &
+ sleep 0.1
+done
+EOF
+ mv "$RESTORE_SCRIPT.tmp" "$RESTORE_SCRIPT"
+ chmod +x "$RESTORE_SCRIPT"
+}
+
+remove_restore() {
+ cat > "$RESTORE_SCRIPT.tmp" << EOF
+#!/bin/bash
+# The content of this script will be generated by switchwall.sh - Don't modify it by yourself.
+EOF
+ mv "$RESTORE_SCRIPT.tmp" "$RESTORE_SCRIPT"
+}
+
+switch() {
+ imgpath="$1"
+ mode_flag="$2"
+ type_flag="$3"
+ color_flag="$4"
+ color="$5"
+ 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))
+
+ if [[ "$color_flag" == "1" ]]; then
+ matugen_args=(color hex "$color")
+ generate_colors_material_args=(--color "$color")
+ else
+ if [[ -z "$imgpath" ]]; then
+ echo 'Aborted'
+ exit 0
+ fi
+
+ check_and_prompt_upscale "$imgpath" &
+ kill_existing_mpvpaper
+
+ if is_video "$imgpath"; then
+ mkdir -p "$THUMBNAIL_DIR"
+
+ missing_deps=()
+ if ! command -v mpvpaper &> /dev/null; then
+ missing_deps+=("mpvpaper")
+ fi
+ if ! command -v ffmpeg &> /dev/null; then
+ missing_deps+=("ffmpeg")
+ fi
+ if [ ${#missing_deps[@]} -gt 0 ]; then
+ echo "Missing deps: ${missing_deps[*]}"
+ echo "Arch: sudo pacman -S ${missing_deps[*]}"
+ action=$(notify-send \
+ -a "Wallpaper switcher" \
+ -c "im.error" \
+ -A "install_arch=Install (Arch)" \
+ "Can't switch to video wallpaper" \
+ "Missing dependencies: ${missing_deps[*]}")
+ if [[ "$action" == "install_arch" ]]; then
+ kitty -1 sudo pacman -S "${missing_deps[*]}"
+ if command -v mpvpaper &>/dev/null && command -v ffmpeg &>/dev/null; then
+ notify-send 'Wallpaper switcher' 'Alright, try again!' -a "Wallpaper switcher"
+ fi
+ fi
+ exit 0
+ fi
+
+ local video_path="$imgpath"
+ monitors=$(hyprctl monitors -j | jq -r '.[] | .name')
+ for monitor in $monitors; do
+ mpvpaper -o "$VIDEO_OPTS" "$monitor" "$video_path" &
+ sleep 0.1
+ done
+
+ # Extract first frame for color generation
+ thumbnail="$THUMBNAIL_DIR/$(basename "$imgpath").jpg"
+ ffmpeg -y -i "$imgpath" -vframes 1 "$thumbnail" 2>/dev/null
+
+ if [ -f "$thumbnail" ]; then
+ matugen_args=(image "$thumbnail")
+ generate_colors_material_args=(--path "$thumbnail")
+ create_restore_script "$video_path"
+ else
+ echo "Cannot create image to colorgen"
+ remove_restore
+ exit 1
+ fi
+ else
+ matugen_args=(image "$imgpath")
+ generate_colors_material_args=(--path "$imgpath")
+ # Set wallpaper with swww
+ swww img "$imgpath" --transition-step 100 --transition-fps 120 \
+ --transition-type grow --transition-angle 30 --transition-duration 1 \
+ --transition-pos "$cursorposx, $cursorposy_inverted"
+ remove_restore
+ fi
+ fi
+
+ # Determine mode if not set
+ if [[ -z "$mode_flag" ]]; then
+ current_mode=$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null | tr -d "'")
+ if [[ "$current_mode" == "prefer-dark" ]]; then
+ mode_flag="dark"
+ else
+ mode_flag="light"
+ fi
+ fi
+
+ [[ -n "$mode_flag" ]] && matugen_args+=(--mode "$mode_flag") && generate_colors_material_args+=(--mode "$mode_flag")
+ [[ -n "$type_flag" ]] && matugen_args+=(--type "$type_flag") && generate_colors_material_args+=(--scheme "$type_flag")
+ generate_colors_material_args+=(--termscheme "$terminalscheme" --blend_bg_fg)
+ generate_colors_material_args+=(--cache "$STATE_DIR/user/color.txt")
+
+ pre_process "$mode_flag"
+
+ matugen "${matugen_args[@]}"
+ source "$(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate"
+ python "$SCRIPT_DIR/generate_colors_material.py" "${generate_colors_material_args[@]}" \
+ > "$STATE_DIR"/user/generated/material_colors.scss
+ "$SCRIPT_DIR"/applycolor.sh
+ deactivate
+
+ # Pass screen width, height, and wallpaper path to post_process
+ max_width_desired="$(hyprctl monitors -j | jq '([.[].width] | min)' | xargs)"
+ max_height_desired="$(hyprctl monitors -j | jq '([.[].height] | min)' | xargs)"
+ post_process "$max_width_desired" "$max_height_desired" "$imgpath"
+}
+
+main() {
+ imgpath=""
+ mode_flag=""
+ type_flag=""
+ color_flag=""
+ color=""
+ noswitch_flag=""
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --mode)
+ mode_flag="$2"
+ shift 2
+ ;;
+ --type)
+ type_flag="$2"
+ shift 2
+ ;;
+ --color)
+ color_flag="1"
+ if [[ "$2" =~ ^#?[A-Fa-f0-9]{6}$ ]]; then
+ color="$2"
+ shift 2
+ else
+ color=$(hyprpicker --no-fancy)
+ shift
+ fi
+ ;;
+ --noswitch)
+ noswitch_flag="1"
+ imgpath=$(swww query | awk -F 'image: ' '{print $2}')
+ shift
+ ;;
+ *)
+ if [[ -z "$imgpath" ]]; then
+ imgpath="$1"
+ fi
+ shift
+ ;;
+ esac
+ done
+
+ # Only prompt for wallpaper if not using --color and not using --noswitch and no imgpath set
+ if [[ -z "$imgpath" && -z "$color_flag" && -z "$noswitch_flag" ]]; then
+ cd "$(xdg-user-dir PICTURES)/Wallpapers" 2>/dev/null || cd "$(xdg-user-dir PICTURES)" || return 1
+ imgpath="$(kdialog --getopenfilename . --title 'Choose wallpaper')"
+ fi
+
+ switch "$imgpath" "$mode_flag" "$type_flag" "$color_flag" "$color"
+}
+
+main "$@"
diff --git a/.config/ags/scripts/templates/terminal/scheme-base.json b/.config/quickshell/scripts/terminal/scheme-base.json
similarity index 100%
rename from .config/ags/scripts/templates/terminal/scheme-base.json
rename to .config/quickshell/scripts/terminal/scheme-base.json
diff --git a/.config/ags/scripts/templates/terminal/sequences.txt b/.config/quickshell/scripts/terminal/sequences.txt
similarity index 65%
rename from .config/ags/scripts/templates/terminal/sequences.txt
rename to .config/quickshell/scripts/terminal/sequences.txt
index 40575f546..97459582e 100644
--- a/.config/ags/scripts/templates/terminal/sequences.txt
+++ b/.config/quickshell/scripts/terminal/sequences.txt
@@ -1 +1 @@
-]4;0;#$term0 #\]1;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 #\]11;#$term0 #\
+]4;0;#$term0 #\]1;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;[100]#$term0 #\]12;#$term7 #\]13;#$term7 #\]17;#$term7 #\]19;#$term0 #\]4;232;#$term7 #\]4;256;#$term7 #\]708;[100]#$term0 #\]11;#$term0 #\
\ No newline at end of file
diff --git a/.config/ags/scripts/wayland-idle-inhibitor.py b/.config/quickshell/scripts/wayland-idle-inhibitor.py
similarity index 96%
rename from .config/ags/scripts/wayland-idle-inhibitor.py
rename to .config/quickshell/scripts/wayland-idle-inhibitor.py
index ec74d09b1..9bdaabb04 100755
--- a/.config/ags/scripts/wayland-idle-inhibitor.py
+++ b/.config/quickshell/scripts/wayland-idle-inhibitor.py
@@ -1,4 +1,8 @@
#!/usr/bin/env -S\_/bin/sh\_-xc\_"source\_\$(eval\_echo\_\$ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate&&exec\_python\_-E\_"\$0"\_"\$@""
+
+# From https://github.com/stwa/wayland-idle-inhibitor
+# License: WTFPL Version 2
+
import sys
from dataclasses import dataclass
from signal import SIGINT, SIGTERM, signal
diff --git a/.config/quickshell/services/Ai.qml b/.config/quickshell/services/Ai.qml
new file mode 100644
index 000000000..dbb8869d7
--- /dev/null
+++ b/.config/quickshell/services/Ai.qml
@@ -0,0 +1,718 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/object_utils.js" as ObjectUtils
+import "root:/modules/common"
+import Quickshell;
+import Quickshell.Io;
+import Qt.labs.platform
+import QtQuick;
+
+/**
+ * Basic service to handle LLM chats. Supports Google's and OpenAI's API formats.
+ */
+Singleton {
+ id: root
+
+ readonly property string interfaceRole: "interface"
+ readonly property string apiKeyEnvVarName: "API_KEY"
+ property Component aiMessageComponent: AiMessageData {}
+ property string systemPrompt: ConfigOptions?.ai?.systemPrompt ?? ""
+ property var messages: []
+ property var messageIDs: []
+ property var messageByID: ({})
+ readonly property var apiKeys: KeyringStorage.keyringData?.apiKeys ?? {}
+ readonly property var apiKeysLoaded: KeyringStorage.loaded
+ property var postResponseHook
+ property real temperature: PersistentStates?.ai?.temperature ?? 0.5
+
+ function idForMessage(message) {
+ // Generate a unique ID using timestamp and random value
+ return Date.now().toString(36) + Math.random().toString(36).substr(2, 8);
+ }
+
+ function safeModelName(modelName) {
+ return modelName.replace(/:/g, "_").replace(/\./g, "_")
+ }
+
+ // Model properties:
+ // - name: Name of the model
+ // - icon: Icon name of the model
+ // - description: Description of the model
+ // - endpoint: Endpoint of the model
+ // - model: Model name of the model
+ // - requires_key: Whether the model requires an API key
+ // - key_id: The identifier of the API key. Use the same identifier for models that can be accessed with the same key.
+ // - key_get_link: Link to get an API key
+ // - key_get_description: Description of pricing and how to get an API key
+ // - api_format: The API format of the model. Can be "openai" or "gemini". Default is "openai".
+ // - tools: List of tools that the model can use. Each tool is an object with the tool name as the key and an empty object as the value.
+ // - extraParams: Extra parameters to be passed to the model. This is a JSON object.
+ property var models: {
+ "gemini-2.0-flash-search": {
+ "name": "Gemini 2.0 Flash (Search)",
+ "icon": "google-gemini-symbolic",
+ "description": qsTr("Online | Google's model\nGives up-to-date information with search."),
+ "homepage": "https://aistudio.google.com",
+ "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent",
+ "model": "gemini-2.0-flash",
+ "requires_key": true,
+ "key_id": "gemini",
+ "key_get_link": "https://aistudio.google.com/app/apikey",
+ "key_get_description": qsTr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"),
+ "api_format": "gemini",
+ "tools": [
+ {
+ "google_search": {}
+ },
+ ]
+ },
+ "gemini-2.0-flash-tools": {
+ "name": "Gemini 2.0 Flash (Tools)",
+ "icon": "google-gemini-symbolic",
+ "description": qsTr("Experimental | Online | Google's model\nCan do a little more but doesn't search quickly"),
+ "homepage": "https://aistudio.google.com",
+ "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent",
+ "model": "gemini-2.0-flash",
+ "requires_key": true,
+ "key_id": "gemini",
+ "key_get_link": "https://aistudio.google.com/app/apikey",
+ "key_get_description": qsTr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"),
+ "api_format": "gemini",
+ "tools": [
+ {
+ "functionDeclarations": [
+ {
+ "name": "switch_to_search_mode",
+ "description": "Search the web",
+ },
+ {
+ "name": "get_shell_config",
+ "description": "Get the desktop shell config file contents",
+ },
+ {
+ "name": "set_shell_config",
+ "description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string",
+ "description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.",
+ },
+ "value": {
+ "type": "string",
+ "description": "The value to set, e.g. `true`"
+ }
+ },
+ "required": ["key", "value"]
+ }
+ },
+ ]
+ }
+ ]
+ },
+ "gemini-2.5-flash-search": {
+ "name": "Gemini 2.5 Flash (Search)",
+ "icon": "google-gemini-symbolic",
+ "description": qsTr("Online | Google's model\nGives up-to-date information with search."),
+ "homepage": "https://aistudio.google.com",
+ "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:streamGenerateContent",
+ "model": "gemini-2.5-flash-preview-05-20",
+ "requires_key": true,
+ "key_id": "gemini",
+ "key_get_link": "https://aistudio.google.com/app/apikey",
+ "key_get_description": qsTr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"),
+ "api_format": "gemini",
+ "tools": [
+ {
+ "google_search": ({})
+ },
+ ]
+ },
+ "gemini-2.5-flash-tools": {
+ "name": "Gemini 2.5 Flash (Tools)",
+ "icon": "google-gemini-symbolic",
+ "description": qsTr("Experimental | Online | Google's model\nCan do a little more but doesn't search quickly"),
+ "homepage": "https://aistudio.google.com",
+ "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:streamGenerateContent",
+ "model": "gemini-2.5-flash-preview-05-20",
+ "requires_key": true,
+ "key_id": "gemini",
+ "key_get_link": "https://aistudio.google.com/app/apikey",
+ "key_get_description": qsTr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"),
+ "api_format": "gemini",
+ "tools": [
+ {
+ "functionDeclarations": [
+ {
+ "name": "switch_to_search_mode",
+ "description": "Search the web",
+ },
+ {
+ "name": "get_shell_config",
+ "description": "Get the desktop shell config file contents",
+ },
+ {
+ "name": "set_shell_config",
+ "description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "string",
+ "description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.",
+ },
+ "value": {
+ "type": "string",
+ "description": "The value to set, e.g. `true`"
+ }
+ },
+ "required": ["key", "value"]
+ }
+ },
+ ]
+ }
+ ]
+ },
+ "openrouter-llama4-maverick": {
+ "name": "Llama 4 Maverick",
+ "icon": "ollama-symbolic",
+ "description": StringUtils.format(qsTr("Online via {0} | {1}'s model"), "OpenRouter", "Meta"),
+ "homepage": "https://openrouter.ai/meta-llama/llama-4-maverick:free",
+ "endpoint": "https://openrouter.ai/api/v1/chat/completions",
+ "model": "meta-llama/llama-4-maverick:free",
+ "requires_key": true,
+ "key_id": "openrouter",
+ "key_get_link": "https://openrouter.ai/settings/keys",
+ "key_get_description": qsTr("**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key"),
+ },
+ "openrouter-deepseek-r1": {
+ "name": "DeepSeek R1",
+ "icon": "deepseek-symbolic",
+ "description": StringUtils.format(qsTr("Online via {0} | {1}'s model"), "OpenRouter", "DeepSeek"),
+ "homepage": "https://openrouter.ai/deepseek/deepseek-r1:free",
+ "endpoint": "https://openrouter.ai/api/v1/chat/completions",
+ "model": "deepseek/deepseek-r1:free",
+ "requires_key": true,
+ "key_id": "openrouter",
+ "key_get_link": "https://openrouter.ai/settings/keys",
+ "key_get_description": qsTr("**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key"),
+ },
+ }
+ property var modelList: Object.keys(root.models)
+ property var currentModelId: PersistentStates?.ai?.model || modelList[0]
+
+ Component.onCompleted: {
+ setModel(currentModelId, false); // Do necessary setup for model
+ getOllamaModels.running = true
+ }
+
+ function guessModelLogo(model) {
+ if (model.includes("llama")) return "ollama-symbolic";
+ if (model.includes("gemma")) return "google-gemini-symbolic";
+ if (model.includes("deepseek")) return "deepseek-symbolic";
+ if (/^phi\d*:/i.test(model)) return "microsoft-symbolic";
+ return "ollama-symbolic";
+ }
+
+ function guessModelName(model) {
+ const replaced = model.replace(/-/g, ' ').replace(/:/g, ' ');
+ let words = replaced.split(' ');
+ words[words.length - 1] = words[words.length - 1].replace(/(\d+)b$/, (_, num) => `${num}B`)
+ words = words.map((word) => {
+ return (word.charAt(0).toUpperCase() + word.slice(1))
+ });
+ if (words[words.length - 1] === "Latest") words.pop();
+ else words[words.length - 1] = `(${words[words.length - 1]})`; // Surround the last word with square brackets
+ const result = words.join(' ');
+ return result;
+ }
+
+ Process {
+ id: getOllamaModels
+ command: ["bash", "-c", `${Directories.config}/quickshell/scripts/ai/show-installed-ollama-models.sh`.replace(/file:\/\//, "")]
+ stdout: SplitParser {
+ onRead: data => {
+ try {
+ if (data.length === 0) return;
+ const dataJson = JSON.parse(data);
+ root.modelList = [...root.modelList, ...dataJson];
+ dataJson.forEach(model => {
+ const safeModelName = root.safeModelName(model);
+ root.models[safeModelName] = {
+ "name": guessModelName(model),
+ "icon": guessModelLogo(model),
+ "description": StringUtils.format(qsTr("Local Ollama model | {0}"), model),
+ "homepage": `https://ollama.com/library/${model}`,
+ "endpoint": "http://localhost:11434/v1/chat/completions",
+ "model": model,
+ }
+ });
+
+ root.modelList = Object.keys(root.models);
+
+ } catch (e) {
+ console.log("Could not fetch Ollama models:", e);
+ }
+ }
+ }
+ }
+
+ function addMessage(message, role) {
+ if (message.length === 0) return;
+ const aiMessage = aiMessageComponent.createObject(root, {
+ "role": role,
+ "content": message,
+ "thinking": false,
+ "done": true,
+ });
+ const id = idForMessage(aiMessage);
+ root.messageIDs = [...root.messageIDs, id];
+ root.messageByID[id] = aiMessage;
+ }
+
+ function removeMessage(index) {
+ if (index < 0 || index >= messageIDs.length) return;
+ const id = root.messageIDs[index];
+ root.messageIDs.splice(index, 1);
+ root.messageIDs = [...root.messageIDs];
+ delete root.messageByID[id];
+ }
+
+ function addApiKeyAdvice(model) {
+ root.addMessage(
+ StringUtils.format(qsTr('To set an API key, pass it with the command\n\nTo view the key, pass "get" with the command \n\n### For {0}:\n\n**Link**: {1}\n\n{2}'),
+ model.name, model.key_get_link, model.key_get_description ?? qsTr("No further instruction provided ")),
+ Ai.interfaceRole
+ );
+ }
+
+ function getModel() {
+ return models[currentModelId];
+ }
+
+ function setModel(modelId, feedback = true) {
+ if (!modelId) modelId = ""
+ modelId = modelId.toLowerCase()
+ if (modelList.indexOf(modelId) !== -1) {
+ PersistentStateManager.setState("ai.model", modelId);
+ if (feedback) root.addMessage(StringUtils.format(StringUtils.format("Model set to {0}"), models[modelId].name), root.interfaceRole)
+ if (models[modelId].requires_key) {
+ // If key not there show advice
+ if (root.apiKeysLoaded && (!root.apiKeys[models[modelId].key_id] || root.apiKeys[models[modelId].key_id].length === 0)) {
+ root.addApiKeyAdvice(models[modelId])
+ }
+ }
+ } else {
+ if (feedback) root.addMessage(qsTr("Invalid model. Supported: \n```\n") + modelList.join("\n```\n```\n"), Ai.interfaceRole) + "\n```"
+ }
+ if (models[modelId]?.requires_key) {
+ KeyringStorage.fetchKeyringData();
+ }
+ }
+
+ function getTemperature() {
+ return root.temperature;
+ }
+
+ function setTemperature(value) {
+ if (value == NaN || value < 0 || value > 2) {
+ root.addMessage(qsTr("Temperature must be between 0 and 2"), Ai.interfaceRole);
+ return;
+ }
+ PersistentStateManager.setState("ai.temperature", value);
+ root.temperature = value;
+ root.addMessage(StringUtils.format(qsTr("Temperature set to {0}"), value), Ai.interfaceRole);
+ }
+
+ function setApiKey(key) {
+ const model = models[currentModelId];
+ if (!model.requires_key) {
+ root.addMessage(StringUtils.format(qsTr("{0} does not require an API key"), model.name), Ai.interfaceRole);
+ return;
+ }
+ if (!key || key.length === 0) {
+ const model = models[currentModelId];
+ root.addApiKeyAdvice(model)
+ return;
+ }
+ KeyringStorage.setNestedField(["apiKeys", model.key_id], key.trim());
+ root.addMessage(StringUtils.format(qsTr("API key set for {0}"), model.name, Ai.interfaceRole));
+ }
+
+ function printApiKey() {
+ const model = models[currentModelId];
+ if (model.requires_key) {
+ const key = root.apiKeys[model.key_id];
+ if (key) {
+ root.addMessage(StringUtils.format(qsTr("API key:\n\n```txt\n{0}\n```"), key), Ai.interfaceRole);
+ } else {
+ root.addMessage(StringUtils.format(qsTr("No API key set for {0}"), model.name), Ai.interfaceRole);
+ }
+ } else {
+ root.addMessage(StringUtils.format(qsTr("{0} does not require an API key"), model.name), Ai.interfaceRole);
+ }
+ }
+
+ function printTemperature() {
+ root.addMessage(StringUtils.format(qsTr("Temperature: {0}"), root.temperature), Ai.interfaceRole);
+ }
+
+ function clearMessages() {
+ root.messageIDs = [];
+ root.messageByID = ({});
+ }
+
+ Process {
+ id: requester
+ property var baseCommand: ["bash", "-c"]
+ property var message
+ property bool isReasoning
+ property string apiFormat: "openai"
+ property string geminiBuffer: ""
+
+ function buildGeminiEndpoint(model) {
+ // console.log("ENDPOINT: " + model.endpoint + `?key=\$\{${root.apiKeyEnvVarName}\}`)
+ return model.endpoint + `?key=\$\{${root.apiKeyEnvVarName}\}`;
+ }
+
+ function buildOpenAIEndpoint(model) {
+ return model.endpoint;
+ }
+
+ function markDone() {
+ requester.message.done = true;
+ if (root.postResponseHook) {
+ root.postResponseHook();
+ root.postResponseHook = null; // Reset hook after use
+ }
+ }
+
+ function buildGeminiRequestData(model, messages) {
+ let baseData = {
+ "contents": messages.filter(message => (message.role != Ai.interfaceRole)).map(message => {
+ const geminiApiRoleName = (message.role === "assistant") ? "model" : message.role;
+ const usingSearch = model.tools[0].google_search != undefined
+ if (!usingSearch && message.functionCall != undefined && message.functionCall.length > 0) {
+ return {
+ "role": geminiApiRoleName,
+ "parts": [{
+ functionCall: {
+ "name": message.functionName,
+ }
+ }]
+ }
+ }
+ if (!usingSearch && message.functionResponse != undefined && message.functionResponse.length > 0) {
+ return {
+ "role": geminiApiRoleName,
+ "parts": [{
+ functionResponse: {
+ "name": message.functionName,
+ "response": { "content": message.functionResponse }
+ }
+ }]
+ }
+ }
+ return {
+ "role": geminiApiRoleName,
+ "parts": [{
+ text: message.content,
+ }]
+ }
+ }),
+ "tools": [
+ ...model.tools,
+ ],
+ "system_instruction": {
+ "parts": [{ text: root.systemPrompt }]
+ },
+ "generationConfig": {
+ // "temperature": root.temperature,
+ },
+ };
+ return model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData;
+ }
+
+ function buildOpenAIRequestData(model, messages) {
+ let baseData = {
+ "model": model.model,
+ "messages": [
+ {role: "system", content: root.systemPrompt},
+ ...messages.filter(message => (message.role != Ai.interfaceRole)).map(message => {
+ return {
+ "role": message.role,
+ "content": message.content,
+ }
+ }),
+ ],
+ "stream": true,
+ // "temperature": root.temperature,
+ };
+ return model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData;
+ }
+
+ function makeRequest() {
+ const model = models[currentModelId];
+ requester.apiFormat = model.api_format ?? "openai";
+
+ /* Put API key in environment variable */
+ if (model.requires_key) requester.environment[`${root.apiKeyEnvVarName}`] = root.apiKeys ? (root.apiKeys[model.key_id] ?? "") : ""
+
+ /* Build endpoint, request data */
+ const endpoint = (apiFormat === "gemini") ? buildGeminiEndpoint(model) : buildOpenAIEndpoint(model);
+ const messageArray = root.messageIDs.map(id => root.messageByID[id]);
+ const data = (apiFormat === "gemini") ? buildGeminiRequestData(model, messageArray) : buildOpenAIRequestData(model, messageArray);
+ // console.log("REQUEST DATA: ", JSON.stringify(data, null, 2));
+
+ let requestHeaders = {
+ "Content-Type": "application/json",
+ }
+
+ /* Create local message object */
+ requester.message = root.aiMessageComponent.createObject(root, {
+ "role": "assistant",
+ "model": currentModelId,
+ "content": "",
+ "thinking": true,
+ "done": false,
+ });
+ const id = idForMessage(requester.message);
+ root.messageIDs = [...root.messageIDs, id];
+ root.messageByID[id] = requester.message;
+
+ /* Build header string for curl */
+ let headerString = Object.entries(requestHeaders)
+ .filter(([k, v]) => v && v.length > 0)
+ .map(([k, v]) => `-H '${k}: ${v}'`)
+ .join(' ');
+
+ // console.log("Request headers: ", JSON.stringify(requestHeaders));
+ // console.log("Header string: ", headerString);
+
+ /* Create command string */
+ const requestCommandString = `curl --no-buffer "${endpoint}"`
+ + ` ${headerString}`
+ + ((apiFormat == "gemini") ? "" : ` -H "Authorization: Bearer \$\{${root.apiKeyEnvVarName}\}"`)
+ + ` -d '${StringUtils.shellSingleQuoteEscape(JSON.stringify(data))}'`
+ // console.log("Request command: ", requestCommandString);
+ requester.command = baseCommand.concat([requestCommandString]);
+
+ /* Reset vars and make the request */
+ requester.isReasoning = false
+ requester.running = true
+ }
+
+ function parseGeminiBuffer() {
+ // console.log("BUFFER DATA: ", requester.geminiBuffer);
+ try {
+ if (requester.geminiBuffer.length === 0) return;
+ const dataJson = JSON.parse(requester.geminiBuffer);
+ if (!dataJson.candidates) return;
+
+ if (dataJson.candidates[0]?.finishReason) {
+ requester.markDone();
+ }
+ // Function call handling
+ if (dataJson.candidates[0]?.content?.parts[0]?.functionCall) {
+ const functionCall = dataJson.candidates[0]?.content?.parts[0]?.functionCall;
+ requester.message.functionName = functionCall.name;
+ requester.message.functionCall = functionCall.name;
+ requester.message.content += `\n\n[[ Function: ${functionCall.name}(${JSON.stringify(functionCall.args, null, 2)}) ]]\n`;
+ root.handleGeminiFunctionCall(functionCall.name, functionCall.args);
+ return
+ }
+ // Normal text response
+ const responseContent = dataJson.candidates[0]?.content?.parts[0]?.text
+ requester.message.content += responseContent;
+ const annotationSources = dataJson.candidates[0]?.groundingMetadata?.groundingChunks?.map(chunk => {
+ return {
+ "type": "url_citation",
+ "text": chunk?.web?.title,
+ "url": chunk?.web?.uri,
+ }
+ });
+ const annotations = dataJson.candidates[0]?.groundingMetadata?.groundingSupports?.map(citation => {
+ return {
+ "type": "url_citation",
+ "start_index": citation.segment?.startIndex,
+ "end_index": citation.segment?.endIndex,
+ "text": citation?.segment.text,
+ "url": annotationSources[citation.groundingChunkIndices[0]]?.url,
+ "sources": citation.groundingChunkIndices
+ }
+ });
+ requester.message.annotationSources = annotationSources;
+ requester.message.annotations = annotations;
+ // console.log(JSON.stringify(requester.message, null, 2));
+ } catch (e) {
+ console.log("[AI] Could not parse response from stream: ", e);
+ requester.message.content += requester.geminiBuffer
+ } finally {
+ requester.geminiBuffer = "";
+ }
+ }
+
+ function handleGeminiResponseLine(line) {
+ if (line.startsWith("[")) {
+ requester.geminiBuffer += line.slice(1).trim();
+ } else if (line == "]") {
+ requester.geminiBuffer += line.slice(0, -1).trim();
+ parseGeminiBuffer();
+ } else if (line.startsWith(",")) { // end of one entry
+ parseGeminiBuffer();
+ } else {
+ requester.geminiBuffer += line.trim();
+ }
+ }
+
+ function handleOpenAIResponseLine(line) {
+ // Remove 'data: ' prefix if present and trim whitespace
+ let cleanData = line.trim();
+ if (cleanData.startsWith("data:")) {
+ cleanData = cleanData.slice(5).trim();
+ }
+ // console.log("Clean data: ", cleanData);
+ if (!cleanData || cleanData.startsWith(":")) return;
+
+ if (cleanData === "[DONE]") {
+ requester.markDone();
+ return;
+ }
+ const dataJson = JSON.parse(cleanData);
+
+ let newContent = "";
+ const responseContent = dataJson.choices[0]?.delta?.content || dataJson.message?.content;
+ const responseReasoning = dataJson.choices[0]?.delta?.reasoning || dataJson.choices[0]?.delta?.reasoning_content;
+
+ if (responseContent && responseContent.length > 0) {
+ if (requester.isReasoning) {
+ requester.isReasoning = false;
+ requester.message.content += "\n\n \n\n";
+ }
+ newContent = dataJson.choices[0]?.delta?.content || dataJson.message.content;
+ } else if (responseReasoning && responseReasoning.length > 0) {
+ // console.log("Reasoning content: ", dataJson.choices[0].delta.reasoning);
+ if (!requester.isReasoning) {
+ requester.isReasoning = true;
+ requester.message.content += "\n\n\n\n";
+ }
+ newContent = dataJson.choices[0].delta.reasoning || dataJson.choices[0].delta.reasoning_content;
+ }
+
+ requester.message.content += newContent;
+
+ if (dataJson.done) {
+ requester.markDone();
+ }
+ }
+
+ stdout: SplitParser {
+ onRead: data => {
+ // console.log("RAW DATA: ", data);
+ if (data.length === 0) return;
+
+ // Handle response line
+ if (requester.message.thinking) requester.message.thinking = false;
+ try {
+ if (requester.apiFormat === "gemini") {
+ requester.handleGeminiResponseLine(data);
+ }
+ else if (requester.apiFormat === "openai") {
+ requester.handleOpenAIResponseLine(data);
+ }
+ else {
+ console.log("Unknown API format: ", requester.apiFormat);
+ requester.message.content += data;
+ }
+ } catch (e) {
+ console.log("[AI] Could not parse response from stream: ", e);
+ requester.message.content += data;
+ }
+ }
+ }
+
+ onExited: (exitCode, exitStatus) => {
+ if (requester.apiFormat == "gemini") requester.parseGeminiBuffer();
+ else requester.markDone();
+
+ try { // to parse full response into json for error handling
+ // console.log("Full response: ", requester.message.content + "]");
+ const parsedResponse = JSON.parse(requester.message.content + "]");
+ requester.message.content = `\`\`\`json\n${JSON.stringify(parsedResponse, null, 2)}\n\`\`\``;
+ } catch (e) {
+ // console.log("[AI] Could not parse response on exit: ", e);
+ }
+
+ if (requester.message.content.includes("API key not valid")) {
+ root.addApiKeyAdvice(models[requester.message.model]);
+ }
+ }
+ }
+
+ function sendUserMessage(message) {
+ if (message.length === 0) return;
+ root.addMessage(message, "user");
+ requester.makeRequest();
+ }
+
+ function addFunctionOutputMessage(name, output) {
+ const aiMessage = aiMessageComponent.createObject(root, {
+ "role": "user",
+ "content": `[[ Output of ${name} ]]`,
+ "functionName": name,
+ "functionResponse": output,
+ "thinking": false,
+ "done": true,
+ "visibleToUser": false,
+ });
+ // console.log("Adding function output message: ", JSON.stringify(aiMessage));
+ const id = idForMessage(aiMessage);
+ root.messageIDs = [...root.messageIDs, id];
+ root.messageByID[id] = aiMessage;
+ }
+
+ function buildGeminiFunctionOutput(name, output) {
+ const functionResponsePart = {
+ "name": name,
+ "response": { "content": output }
+ }
+ return {
+ "role": "user",
+ "parts": [{
+ functionResponse: functionResponsePart,
+ }]
+ }
+ }
+
+ function handleGeminiFunctionCall(name, args) {
+ if (name === "switch_to_search_mode") {
+ if (root.currentModelId === "gemini-2.5-flash-tools") {
+ root.setModel("gemini-2.5-flash-search", false);
+ root.postResponseHook = () => root.setModel("gemini-2.5-flash-tools", false);
+ } else if (root.currentModelId === "gemini-2.0-flash-tools") {
+ root.setModel("gemini-2.0-flash-search", false);
+ root.postResponseHook = () => root.setModel("gemini-2.0-flash-tools", false);
+ }
+ addFunctionOutputMessage(name, qsTr("Switched to search mode. Continue with the user's request."))
+ requester.makeRequest();
+ } else if (name === "get_shell_config") {
+ const configJson = ObjectUtils.toPlainObject(ConfigOptions)
+ addFunctionOutputMessage(name, JSON.stringify(configJson));
+ requester.makeRequest();
+ } else if (name === "set_shell_config") {
+ if (!args.key || !args.value) {
+ addFunctionOutputMessage(name, qsTr("Invalid arguments. Must provide `key` and `value`."));
+ return;
+ }
+ const key = args.key;
+ const value = args.value;
+ ConfigLoader.setLiveConfigValue(key, value);
+ ConfigLoader.saveConfig();
+ }
+ else root.addMessage(qsTr("Unknown function call: {0}"), "assistant");
+ }
+
+}
diff --git a/.config/quickshell/services/AiMessageData.qml b/.config/quickshell/services/AiMessageData.qml
new file mode 100644
index 000000000..b5f208548
--- /dev/null
+++ b/.config/quickshell/services/AiMessageData.qml
@@ -0,0 +1,19 @@
+import "root:/modules/common"
+import QtQuick;
+
+/**
+ * Represents a message in an AI conversation. (Kind of) follows the OpenAI API message structure.
+ */
+QtObject {
+ property string role
+ property string content
+ property string model
+ property bool thinking: true
+ property bool done: false
+ property var annotations: []
+ property var annotationSources: []
+ property string functionName
+ property string functionCall
+ property string functionResponse
+ property bool visibleToUser: true
+}
diff --git a/.config/quickshell/services/AppSearch.qml b/.config/quickshell/services/AppSearch.qml
new file mode 100644
index 000000000..876df1838
--- /dev/null
+++ b/.config/quickshell/services/AppSearch.qml
@@ -0,0 +1,116 @@
+pragma Singleton
+
+import "root:/modules/common"
+import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
+import "root:/modules/common/functions/levendist.js" as Levendist
+import Quickshell
+import Quickshell.Io
+
+/**
+ * - Eases fuzzy searching for applications by name
+ * - Guesses icon name for window class name
+ */
+Singleton {
+ id: root
+ property bool sloppySearch: ConfigOptions?.search.sloppy ?? false
+ property real scoreThreshold: 0.2
+ property var substitutions: ({
+ "code-url-handler": "visual-studio-code",
+ "Code": "visual-studio-code",
+ "gnome-tweaks": "org.gnome.tweaks",
+ "pavucontrol-qt": "pavucontrol",
+ "wps": "wps-office2019-kprometheus",
+ "wpsoffice": "wps-office2019-kprometheus",
+ "footclient": "foot",
+ "zen": "zen-browser",
+ })
+ property var regexSubstitutions: [
+ {
+ "regex": /^steam_app_(\\d+)$/,
+ "replace": "steam_icon_$1"
+ },
+ {
+ "regex": /Minecraft.*/,
+ "replace": "minecraft"
+ },
+ {
+ "regex": /.*polkit.*/,
+ "replace": "system-lock-screen"
+ },
+ {
+ "regex": /gcr.prompter/,
+ "replace": "system-lock-screen"
+ }
+ ]
+
+ readonly property list list: Array.from(DesktopEntries.applications.values)
+ .sort((a, b) => a.name.localeCompare(b.name))
+
+ readonly property var preppedNames: list.map(a => ({
+ name: Fuzzy.prepare(`${a.name} `),
+ entry: a
+ }))
+
+ function fuzzyQuery(search: string): var { // Idk why list doesn't work
+ if (root.sloppySearch) {
+ const results = list.map(obj => ({
+ entry: obj,
+ score: Levendist.computeScore(obj.name.toLowerCase(), search.toLowerCase())
+ })).filter(item => item.score > root.scoreThreshold)
+ .sort((a, b) => b.score - a.score)
+ return results
+ .map(item => item.entry)
+ }
+
+ return Fuzzy.go(search, preppedNames, {
+ all: true,
+ key: "name"
+ }).map(r => {
+ return r.obj.entry
+ });
+ }
+
+ function iconExists(iconName) {
+ return (Quickshell.iconPath(iconName, true).length > 0)
+ && !iconName.includes("image-missing");
+ }
+
+ function guessIcon(str) {
+ if (!str || str.length == 0) return "image-missing";
+
+ // Normal substitutions
+ if (substitutions[str])
+ return substitutions[str];
+
+ // Regex substitutions
+ for (let i = 0; i < regexSubstitutions.length; i++) {
+ const substitution = regexSubstitutions[i];
+ const replacedName = str.replace(
+ substitution.regex,
+ substitution.replace,
+ );
+ if (replacedName != str) return replacedName;
+ }
+
+ // If it gets detected normally, no need to guess
+ if (iconExists(str)) return str;
+
+ let guessStr = str;
+ // Guess: Take only app name of reverse domain name notation
+ guessStr = str.split('.').slice(-1)[0].toLowerCase();
+ if (iconExists(guessStr)) return guessStr;
+ // Guess: normalize to kebab case
+ guessStr = str.toLowerCase().replace(/\s+/g, "-");
+ if (iconExists(guessStr)) return guessStr;
+ // Guess: First fuzze desktop entry match
+ const searchResults = root.fuzzyQuery(str);
+ if (searchResults.length > 0) {
+ const firstEntry = searchResults[0];
+ guessStr = firstEntry.icon
+ if (iconExists(guessStr)) return guessStr;
+ }
+
+ // Give up
+ return str;
+ }
+}
diff --git a/.config/quickshell/services/Audio.qml b/.config/quickshell/services/Audio.qml
new file mode 100644
index 000000000..2fd5e0cac
--- /dev/null
+++ b/.config/quickshell/services/Audio.qml
@@ -0,0 +1,51 @@
+import "root:/modules/common"
+import QtQuick
+import Quickshell
+import Quickshell.Services.Pipewire
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+/**
+ * A nice wrapper for default Pipewire audio sink and source.
+ */
+Singleton {
+ id: root
+
+ property bool ready: Pipewire.defaultAudioSink?.ready ?? false
+ property PwNode sink: Pipewire.defaultAudioSink
+ property PwNode source: Pipewire.defaultAudioSource
+
+ signal sinkProtectionTriggered(string reason);
+
+ PwObjectTracker {
+ objects: [sink, source]
+ }
+
+ Connections { // Protection against sudden volume changes
+ target: sink?.audio ?? null
+ property bool lastReady: false
+ property real lastVolume: 0
+ function onVolumeChanged() {
+ if (!ConfigOptions.audio.protection.enable) return;
+ if (!lastReady) {
+ lastVolume = sink.audio.volume;
+ lastReady = true;
+ return;
+ }
+ const newVolume = sink.audio.volume;
+ const maxAllowedIncrease = ConfigOptions.audio.protection.maxAllowedIncrease / 100;
+ const maxAllowed = ConfigOptions.audio.protection.maxAllowed / 100;
+
+ if (newVolume - lastVolume > maxAllowedIncrease) {
+ sink.audio.volume = lastVolume;
+ root.sinkProtectionTriggered("Illegal increment");
+ } else if (newVolume > maxAllowed) {
+ sink.audio.volume = lastVolume;
+ root.sinkProtectionTriggered("Exceeded max allowed");
+ }
+ lastVolume = sink.audio.volume;
+ }
+
+ }
+
+}
diff --git a/.config/quickshell/services/Battery.qml b/.config/quickshell/services/Battery.qml
new file mode 100644
index 000000000..08bdee7cd
--- /dev/null
+++ b/.config/quickshell/services/Battery.qml
@@ -0,0 +1,30 @@
+pragma Singleton
+
+import "root:/modules/common"
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+import Quickshell.Services.UPower
+
+Singleton {
+ property bool available: UPower.displayDevice.isLaptopBattery
+ property var chargeState: UPower.displayDevice.state
+ property bool isCharging: chargeState == UPowerDeviceState.Charging
+ property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge
+ property real percentage: UPower.displayDevice.percentage
+
+ property bool isLow: percentage <= ConfigOptions.battery.low / 100
+ property bool isCritical: percentage <= ConfigOptions.battery.critical / 100
+ property bool isSuspending: percentage <= ConfigOptions.battery.suspend / 100
+
+ property bool isLowAndNotCharging: isLow && !isCharging
+ property bool isCriticalAndNotCharging: isCritical && !isCharging
+
+ onIsLowAndNotChargingChanged: {
+ if (available && isLowAndNotCharging) Hyprland.dispatch(`exec notify-send "Low battery" "Consider plugging in your device" -u critical -a "Shell"`)
+ }
+
+ onIsCriticalAndNotChargingChanged: {
+ if (available && isCriticalAndNotCharging) Hyprland.dispatch(`exec notify-send "Critically low battery" "🙏 I beg for pleas charg\nAutomatic suspend triggers at ${ConfigOptions.battery.suspend}%" -u critical -a "Shell"`)
+ }
+}
diff --git a/.config/quickshell/services/Bluetooth.qml b/.config/quickshell/services/Bluetooth.qml
new file mode 100644
index 000000000..817bbc921
--- /dev/null
+++ b/.config/quickshell/services/Bluetooth.qml
@@ -0,0 +1,73 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import Quickshell;
+import Quickshell.Io;
+import QtQuick;
+
+/**
+ * Basic polled Bluetooth state.
+ */
+Singleton {
+ id: root
+
+ property int updateInterval: 1000
+ property string bluetoothDeviceName: ""
+ property string bluetoothDeviceAddress: ""
+ property bool bluetoothEnabled: false
+ property bool bluetoothConnected: false
+
+ function update() {
+ updateBluetoothDevice.running = true
+ updateBluetoothStatus.running = true
+ updateBluetoothEnabled.running = true
+ }
+
+ Timer {
+ interval: 10
+ running: true
+ repeat: true
+ onTriggered: {
+ update()
+ interval = root.updateInterval
+ }
+ }
+
+ // Check if Bluetooth is enabled (controller powered on)
+ Process {
+ id: updateBluetoothEnabled
+ command: ["sh", "-c", "bluetoothctl show | grep -q 'Powered: yes' && echo 1 || echo 0"]
+ running: true
+ stdout: SplitParser {
+ onRead: data => {
+ root.bluetoothEnabled = (parseInt(data) === 1)
+ }
+ }
+ }
+
+ // Get the name and address of the first connected Bluetooth device
+ Process {
+ id: updateBluetoothDevice
+ command: ["sh", "-c", "bluetoothctl info | awk -F': ' '/Name: /{name=$2} /Device /{addr=$2} END{print name \":\" addr}'"]
+ running: true
+ stdout: SplitParser {
+ onRead: data => {
+ let parts = data.split(":")
+ root.bluetoothDeviceName = parts[0] || ""
+ root.bluetoothDeviceAddress = parts[1] || ""
+ }
+ }
+ }
+
+ // Check if any device is connected
+ Process {
+ id: updateBluetoothStatus
+ command: ["sh", "-c", "bluetoothctl info | grep -q 'Connected: yes' && echo 1 || echo 0"]
+ running: true
+ stdout: SplitParser {
+ onRead: data => {
+ root.bluetoothConnected = (parseInt(data) === 1)
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/services/Booru.qml b/.config/quickshell/services/Booru.qml
new file mode 100644
index 000000000..49256bfa7
--- /dev/null
+++ b/.config/quickshell/services/Booru.qml
@@ -0,0 +1,468 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common"
+import Quickshell;
+import Quickshell.Io;
+import Qt.labs.platform
+import QtQuick;
+
+/**
+ * A service for interacting with various booru APIs.
+ */
+Singleton {
+ id: root
+ property Component booruResponseDataComponent: BooruResponseData {}
+
+ signal tagSuggestion(string query, var suggestions)
+
+ property string failMessage: qsTr("That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number")
+ property var responses: []
+ property int runningRequests: 0
+ property var defaultUserAgent: ConfigOptions?.networking?.userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
+ property var providerList: Object.keys(providers).filter(provider => provider !== "system" && providers[provider].api)
+ property var providers: {
+ "system": { "name": qsTr("System") },
+ "yandere": {
+ "name": "yande.re",
+ "url": "https://yande.re",
+ "api": "https://yande.re/post.json",
+ "description": qsTr("All-rounder | Good quality, decent quantity"),
+ "mapFunc": (response) => {
+ return response.map(item => {
+ return {
+ "id": item.id,
+ "width": item.width,
+ "height": item.height,
+ "aspect_ratio": item.width / item.height,
+ "tags": item.tags,
+ "rating": item.rating,
+ "is_nsfw": (item.rating != 's'),
+ "md5": item.md5,
+ "preview_url": item.preview_url,
+ "sample_url": item.sample_url ?? item.file_url,
+ "file_url": item.file_url,
+ "file_ext": item.file_ext,
+ "source": getWorkingImageSource(item.source) ?? item.file_url,
+ }
+ })
+ },
+ "tagSearchTemplate": "https://yande.re/tag.json?order=count&name={{query}}*",
+ "tagMapFunc": (response) => {
+ return response.map(item => {
+ return {
+ "name": item.name,
+ "count": item.count
+ }
+ })
+ }
+ },
+ "konachan": {
+ "name": "Konachan",
+ "url": "https://konachan.com",
+ "api": "https://konachan.com/post.json",
+ "description": qsTr("For desktop wallpapers | Good quality"),
+ "mapFunc": (response) => {
+ return response.map(item => {
+ return {
+ "id": item.id,
+ "width": item.width,
+ "height": item.height,
+ "aspect_ratio": item.width / item.height,
+ "tags": item.tags,
+ "rating": item.rating,
+ "is_nsfw": (item.rating != 's'),
+ "md5": item.md5,
+ "preview_url": item.preview_url,
+ "sample_url": item.sample_url ?? item.file_url,
+ "file_url": item.file_url,
+ "file_ext": item.file_ext,
+ "source": getWorkingImageSource(item.source) ?? item.file_url,
+ }
+ })
+ },
+ "tagSearchTemplate": "https://konachan.com/tag.json?order=count&name={{query}}*",
+ "tagMapFunc": (response) => {
+ return response.map(item => {
+ return {
+ "name": item.name,
+ "count": item.count
+ }
+ })
+ }
+ },
+ "zerochan": {
+ "name": "Zerochan",
+ "url": "https://www.zerochan.net",
+ "api": "https://www.zerochan.net/?json",
+ "description": qsTr("Clean stuff | Excellent quality, no NSFW"),
+ "mapFunc": (response) => {
+ response = response.items
+ return response.map(item => {
+ return {
+ "id": item.id,
+ "width": item.width,
+ "height": item.height,
+ "aspect_ratio": item.width / item.height,
+ "tags": item.tags.join(" "),
+ "rating": "safe", // Zerochan doesn't have nsfw
+ "is_nsfw": false,
+ "md5": item.md5,
+ "preview_url": item.thumbnail,
+ "sample_url": item.thumbnail,
+ "file_url": item.thumbnail,
+ "file_ext": "avif",
+ "source": getWorkingImageSource(item.source) ?? item.thumbnail,
+ "character": item.tag
+ }
+ })
+ }
+ },
+ "danbooru": {
+ "name": "Danbooru",
+ "url": "https://danbooru.donmai.us",
+ "api": "https://danbooru.donmai.us/posts.json",
+ "description": qsTr("The popular one | Best quantity, but quality can vary wildly"),
+ "mapFunc": (response) => {
+ return response.map(item => {
+ return {
+ "id": item.id,
+ "width": item.image_width,
+ "height": item.image_height,
+ "aspect_ratio": item.image_width / item.image_height,
+ "tags": item.tag_string,
+ "rating": item.rating,
+ "is_nsfw": (item.rating != 's'),
+ "md5": item.md5,
+ "preview_url": item.preview_file_url,
+ "sample_url": item.file_url ?? item.large_file_url,
+ "file_url": item.large_file_url,
+ "file_ext": item.file_ext,
+ "source": getWorkingImageSource(item.source) ?? item.file_url,
+ }
+ })
+ },
+ "tagSearchTemplate": "https://danbooru.donmai.us/tags.json?search[name_matches]={{query}}*",
+ "tagMapFunc": (response) => {
+ return response.map(item => {
+ return {
+ "name": item.name,
+ "count": item.post_count
+ }
+ })
+ }
+
+ },
+ "gelbooru": {
+ "name": "Gelbooru",
+ "url": "https://gelbooru.com",
+ "api": "https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1",
+ "description": qsTr("The hentai one | Great quantity, a lot of NSFW, quality varies wildly"),
+ "mapFunc": (response) => {
+ response = response.post
+ return response.map(item => {
+ return {
+ "id": item.id,
+ "width": item.width,
+ "height": item.height,
+ "aspect_ratio": item.width / item.height,
+ "tags": item.tags,
+ "rating": item.rating.replace('general', 's').charAt(0),
+ "is_nsfw": (item.rating != 's'),
+ "md5": item.md5,
+ "preview_url": item.preview_url,
+ "sample_url": item.sample_url ?? item.file_url,
+ "file_url": item.file_url,
+ "file_ext": item.file_url.split('.').pop(),
+ "source": getWorkingImageSource(item.source) ?? item.file_url,
+ }
+ })
+ },
+ "tagSearchTemplate": "https://gelbooru.com/index.php?page=dapi&s=tag&q=index&json=1&orderby=count&name_pattern={{query}}%",
+ "tagMapFunc": (response) => {
+ return response.tag.map(item => {
+ return {
+ "name": item.name,
+ "count": item.count
+ }
+ })
+ }
+ },
+ "waifu.im": {
+ "name": "waifu.im",
+ "url": "https://waifu.im",
+ "api": "https://api.waifu.im/search",
+ "description": qsTr("Waifus only | Excellent quality, limited quantity"),
+ "mapFunc": (response) => {
+ response = response.images
+ return response.map(item => {
+ return {
+ "id": item.image_id,
+ "width": item.width,
+ "height": item.height,
+ "aspect_ratio": item.width / item.height,
+ "tags": item.tags.map(tag => {return tag.name}).join(" "),
+ "rating": item.is_nsfw ? "e" : "s",
+ "is_nsfw": item.is_nsfw,
+ "md5": item.md5,
+ "preview_url": item.sample_url ?? item.url, // preview_url just says access denied (maybe i fucked up and sent too many requests idk)
+ "sample_url": item.url,
+ "file_url": item.url,
+ "file_ext": item.extension,
+ "source": getWorkingImageSource(item.source) ?? item.url,
+ }
+ })
+ },
+ "tagSearchTemplate": "https://api.waifu.im/tags",
+ "tagMapFunc": (response) => {
+ return [...response.versatile.map(item => {return {"name": item}}),
+ ...response.nsfw.map(item => {return {"name": item}})]
+ }
+ },
+ "t.alcy.cc": {
+ "name": "Alcy",
+ "url": "https://t.alcy.cc",
+ "api": "https://t.alcy.cc/",
+ "description": qsTr("Large images | God tier quality, no NSFW."),
+ "fixedTags": [
+ {
+ "name": "ycy",
+ "count": "General"
+ },
+ {
+ "name": "moez",
+ "count": "Moe"
+ },
+ {
+ "name": "ysz",
+ "count": "Genshin Impact"
+ },
+ {
+ "name": "fj",
+ "count": "Landscape"
+ },
+ {
+ "name": "bd",
+ "count": "Girl on white background"
+ },
+ {
+ "name": "xhl",
+ "count": "Shiggy"
+ },
+ ],
+ "manualParseFunc": (responseText) => {
+ // Alcy just returns image links, each on a new line
+ const lines = responseText.trim().split('\n');
+ return lines.map(line => {
+ return {
+ "id": Qt.md5(line),
+ // Alcy doesn't provide dimensions and images are often of god resolution
+ "width": 1000,
+ "height": 1000,
+ "aspect_ratio": 1, // Default aspect ratio
+ "tags": "[no tags]",
+ "rating": "s",
+ "is_nsfw": false,
+ "md5": Qt.md5(line),
+ "preview_url": line,
+ "sample_url": line,
+ "file_url": line,
+ "file_ext": line.split('.').pop(),
+ "source": "",
+ }
+ });
+ },
+ }
+ }
+ property var currentProvider: PersistentStates.booru.provider
+
+ function getWorkingImageSource(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 setProvider(provider) {
+ provider = provider.toLowerCase()
+ if (providerList.indexOf(provider) !== -1) {
+ PersistentStateManager.setState("booru.provider", provider)
+ root.addSystemMessage(qsTr("Provider set to ") + providers[provider].name
+ + (provider == "zerochan" ? qsTr(". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!") : ""))
+ } else {
+ root.addSystemMessage(qsTr("Invalid API provider. Supported: \n- ") + providerList.join("\n- "))
+ }
+ }
+
+ function clearResponses() {
+ responses = []
+ }
+
+ function addSystemMessage(message) {
+ responses = [...responses, root.booruResponseDataComponent.createObject(null, {
+ "provider": "system",
+ "tags": [],
+ "page": -1,
+ "images": [],
+ "message": `${message}`
+ })]
+ }
+
+ function constructRequestUrl(tags, nsfw=true, limit=20, page=1) {
+ var provider = providers[currentProvider]
+ var baseUrl = provider.api
+ var url = baseUrl
+ var tagString = tags.join(" ")
+ if (!nsfw && !(["zerochan", "waifu.im", "t.alcy.cc"].includes(currentProvider))) {
+ if (currentProvider == "gelbooru")
+ tagString += " rating:general";
+ else
+ tagString += " rating:safe";
+ }
+ var params = []
+ // Tags & limit
+ if (currentProvider === "zerochan") {
+ params.push("c=" + tagString) // zerochan doesn't have search in api, so we use color
+ params.push("l=" + limit)
+ params.push("s=" + "fav")
+ params.push("t=" + 1)
+ params.push("p=" + page)
+ }
+ else if (currentProvider === "waifu.im") {
+ var tagsArray = tagString.split(" ");
+ tagsArray.forEach(tag => {
+ params.push("included_tags=" + encodeURIComponent(tag));
+ });
+ params.push("limit=" + Math.min(limit, 30)) // Only admin can do > 30
+ params.push("is_nsfw=" + (nsfw ? "null" : "false")) // null is random
+ }
+ else if (currentProvider === "t.alcy.cc") {
+ url += tagString
+ params.push("json")
+ params.push("quantity=" + limit)
+ }
+ else {
+ params.push("tags=" + encodeURIComponent(tagString))
+ params.push("limit=" + limit)
+ if (currentProvider == "gelbooru") {
+ params.push("pid=" + page)
+ }
+ else {
+ params.push("page=" + page)
+ }
+ }
+ if (baseUrl.indexOf("?") === -1) {
+ url += "?" + params.join("&")
+ } else {
+ url += "&" + params.join("&")
+ }
+ return url
+ }
+
+ function makeRequest(tags, nsfw=false, limit=20, page=1) {
+ var url = constructRequestUrl(tags, nsfw, limit, page)
+ // console.log("[Booru] Making request to " + url)
+
+ const newResponse = root.booruResponseDataComponent.createObject(null, {
+ "provider": currentProvider,
+ "tags": tags,
+ "page": page,
+ "images": [],
+ "message": ""
+ })
+
+ var xhr = new XMLHttpRequest()
+ xhr.open("GET", url)
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
+ try {
+ // console.log("[Booru] Raw response: " + xhr.responseText)
+ const provider = providers[currentProvider]
+ let response;
+ if (provider.manualParseFunc) {
+ response = provider.manualParseFunc(xhr.responseText)
+ } else {
+ response = JSON.parse(xhr.responseText)
+ response = provider.mapFunc(response)
+ }
+ // console.log("[Booru] Mapped response: " + JSON.stringify(response))
+ newResponse.images = response
+ newResponse.message = response.length > 0 ? "" : root.failMessage
+
+ } catch (e) {
+ console.log("[Booru] Failed to parse response: " + e)
+ newResponse.message = root.failMessage
+ } finally {
+ root.runningRequests--;
+ root.responses = [...root.responses, newResponse]
+ }
+ }
+ else if (xhr.readyState === XMLHttpRequest.DONE) {
+ console.log("[Booru] Request failed with status: " + xhr.status)
+ }
+ }
+
+ try {
+ // Required for danbooru
+ if (currentProvider == "danbooru") {
+ xhr.setRequestHeader("User-Agent", defaultUserAgent)
+ }
+ else if (currentProvider == "zerochan") {
+ const userAgent = ConfigOptions?.sidebar?.booru?.zerochan?.username ? `Desktop sidebar booru viewer - username: ${ConfigOptions.sidebar.booru.zerochan.username}` : defaultUserAgent
+ xhr.setRequestHeader("User-Agent", userAgent)
+ }
+ root.runningRequests++;
+ xhr.send()
+ } catch (error) {
+ console.log("Could not set User-Agent:", error)
+ }
+ }
+
+ property var currentTagRequest: null
+ function triggerTagSearch(query) {
+ if (currentTagRequest) {
+ currentTagRequest.abort();
+ }
+
+ var provider = providers[currentProvider]
+ if (provider.fixedTags) {
+ root.tagSuggestion(query, provider.fixedTags)
+ return provider.fixedTags;
+ } else if (!provider.tagSearchTemplate) {
+ return
+ }
+ var url = provider.tagSearchTemplate.replace("{{query}}", encodeURIComponent(query))
+
+ var xhr = new XMLHttpRequest()
+ currentTagRequest = xhr
+ xhr.open("GET", url)
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
+ currentTagRequest = null
+ try {
+ // console.log("[Booru] Raw response: " + xhr.responseText)
+ var response = JSON.parse(xhr.responseText)
+ response = provider.tagMapFunc(response)
+ // console.log("[Booru] Mapped response: " + JSON.stringify(response))
+ root.tagSuggestion(query, response)
+ } catch (e) {
+ console.log("[Booru] Failed to parse response: " + e)
+ }
+ }
+ else if (xhr.readyState === XMLHttpRequest.DONE) {
+ console.log("[Booru] Request failed with status: " + xhr.status)
+ }
+ }
+
+ try {
+ // Required for danbooru
+ if (currentProvider == "danbooru") {
+ xhr.setRequestHeader("User-Agent", defaultUserAgent)
+ }
+ xhr.send()
+ } catch (error) {
+ console.log("Could not set User-Agent:", error)
+ }
+ }
+}
+
diff --git a/.config/quickshell/services/BooruResponseData.qml b/.config/quickshell/services/BooruResponseData.qml
new file mode 100644
index 000000000..38e1b8c76
--- /dev/null
+++ b/.config/quickshell/services/BooruResponseData.qml
@@ -0,0 +1,13 @@
+import "root:/modules/common"
+import QtQuick;
+
+/**
+ * A booru response.
+ */
+QtObject {
+ property string provider
+ property var tags
+ property var page
+ property var images
+ property string message
+}
diff --git a/.config/quickshell/services/Brightness.qml b/.config/quickshell/services/Brightness.qml
new file mode 100644
index 000000000..aba664aa9
--- /dev/null
+++ b/.config/quickshell/services/Brightness.qml
@@ -0,0 +1,152 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+// From https://github.com/caelestia-dots/shell/ (`quickshell` branch) with modifications.
+// License: GPLv3
+
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+import QtQuick
+
+/**
+ * For managing brightness of monitors. Supports both brightnessctl and ddcutil.
+ */
+Singleton {
+ id: root
+
+ signal brightnessChanged()
+
+ property var ddcMonitors: []
+ readonly property list monitors: Quickshell.screens.map(screen => monitorComp.createObject(root, {
+ screen
+ }))
+
+ function getMonitorForScreen(screen: ShellScreen): var {
+ return monitors.find(m => m.screen === screen);
+ }
+
+ function increaseBrightness(): void {
+ const focusedName = Hyprland.focusedMonitor.name;
+ const monitor = monitors.find(m => focusedName === m.screen.name);
+ if (monitor)
+ monitor.setBrightness(monitor.brightness + 0.05);
+ }
+
+ function decreaseBrightness(): void {
+ const focusedName = Hyprland.focusedMonitor.name;
+ const monitor = monitors.find(m => focusedName === m.screen.name);
+ if (monitor)
+ monitor.setBrightness(monitor.brightness - 0.05);
+ }
+
+ reloadableId: "brightness"
+
+ onMonitorsChanged: {
+ ddcMonitors = [];
+ ddcProc.running = true;
+ }
+
+ Process {
+ id: ddcProc
+
+ command: ["ddcutil", "detect", "--brief"]
+ stdout: SplitParser {
+ splitMarker: "\n\n"
+ onRead: data => {
+ if (data.startsWith("Display ")) {
+ const lines = data.split("\n").map(l => l.trim());
+ root.ddcMonitors.push({
+ model: lines.find(l => l.startsWith("Monitor:")).split(":")[2],
+ busNum: lines.find(l => l.startsWith("I2C bus:")).split("/dev/i2c-")[1]
+ });
+ }
+ }
+ }
+ onExited: root.ddcMonitorsChanged()
+ }
+
+ Process {
+ id: setProc
+ }
+
+ component BrightnessMonitor: QtObject {
+ id: monitor
+
+ required property ShellScreen screen
+ readonly property bool isDdc: root.ddcMonitors.some(m => m.model === screen.model)
+ readonly property string busNum: root.ddcMonitors.find(m => m.model === screen.model)?.busNum ?? ""
+ property real brightness
+ property bool ready: false
+
+ onBrightnessChanged: {
+ if (monitor.ready) {
+ root.brightnessChanged();
+ }
+ }
+
+ function initialize() {
+ monitor.ready = false;
+ initProc.command = isDdc ? ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] : ["sh", "-c", `echo "a b c $(brightnessctl g) $(brightnessctl m)"`];
+ initProc.running = true;
+ }
+
+ readonly property Process initProc: Process {
+ stdout: SplitParser {
+ onRead: data => {
+ const [, , , current, max] = data.split(" ");
+ monitor.brightness = parseInt(current) / parseInt(max);
+ monitor.ready = true;
+ }
+ }
+ }
+
+ function setBrightness(value: real): void {
+ value = Math.max(0.01, Math.min(1, value));
+ const rounded = Math.round(value * 100);
+ if (Math.round(brightness * 100) === rounded)
+ return;
+ brightness = value;
+ setProc.command = isDdc ? ["ddcutil", "-b", busNum, "setvcp", "10", rounded] : ["brightnessctl", "s", `${rounded}%`, "--quiet"];
+ setProc.startDetached();
+ }
+
+ Component.onCompleted: {
+ initialize();
+ }
+
+ onBusNumChanged: {
+ initialize();
+ }
+ }
+
+ Component {
+ id: monitorComp
+
+ BrightnessMonitor {}
+ }
+
+ IpcHandler {
+ target: "brightness"
+
+ function increment() {
+ onPressed: root.increaseBrightness()
+ }
+
+ function decrement() {
+ onPressed: root.decreaseBrightness()
+ }
+ }
+
+ GlobalShortcut {
+ name: "brightnessIncrease"
+ description: qsTr("Increase brightness")
+ onPressed: root.increaseBrightness()
+ }
+
+ GlobalShortcut {
+ name: "brightnessDecrease"
+ description: qsTr("Decrease brightness")
+ onPressed: root.decreaseBrightness()
+ }
+}
diff --git a/.config/quickshell/services/Cliphist.qml b/.config/quickshell/services/Cliphist.qml
new file mode 100644
index 000000000..bebafb102
--- /dev/null
+++ b/.config/quickshell/services/Cliphist.qml
@@ -0,0 +1,81 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
+import "root:/modules/common/functions/levendist.js" as Levendist
+import "root:/modules/common"
+import "root:/"
+import QtQuick
+import Quickshell
+import Quickshell.Io
+
+Singleton {
+ id: root
+ property bool sloppySearch: ConfigOptions?.search.sloppy ?? false
+ property real scoreThreshold: 0.2
+ property list entries: []
+ readonly property var preparedEntries: entries.map(a => ({
+ name: Fuzzy.prepare(`${a.replace(/^\s*\S+\s+/, "")}`),
+ entry: a
+ }))
+ function fuzzyQuery(search: string): var {
+ if (root.sloppySearch) {
+ const results = entries.slice(0, 100).map(str => ({
+ entry: str,
+ score: Levendist.computeTextMatchScore(str.toLowerCase(), search.toLowerCase())
+ })).filter(item => item.score > root.scoreThreshold)
+ .sort((a, b) => b.score - a.score)
+ return results
+ .map(item => item.entry)
+ }
+
+ return Fuzzy.go(search, preparedEntries, {
+ all: true,
+ key: "name"
+ }).map(r => {
+ return r.obj.entry
+ });
+ }
+
+ function refresh() {
+ readProc.buffer = []
+ readProc.running = true
+ }
+
+ Connections {
+ target: Quickshell
+ function onClipboardTextChanged() {
+ delayedUpdateTimer.restart()
+ }
+ }
+
+ Timer {
+ id: delayedUpdateTimer
+ interval: ConfigOptions.hacks.arbitraryRaceConditionDelay
+ repeat: false
+ onTriggered: {
+ root.refresh()
+ }
+ }
+
+ Process {
+ id: readProc
+ property list buffer: []
+
+ command: ["cliphist", "list"]
+
+ stdout: SplitParser {
+ onRead: (line) => {
+ readProc.buffer.push(line)
+ }
+ }
+
+ onExited: (exitCode, exitStatus) => {
+ if (exitCode === 0) {
+ root.entries = readProc.buffer
+ } else {
+ console.error("[Cliphist] Failed to refresh with code", exitCode, "and status", exitStatus)
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/services/ConfigLoader.qml b/.config/quickshell/services/ConfigLoader.qml
new file mode 100644
index 000000000..347e8f400
--- /dev/null
+++ b/.config/quickshell/services/ConfigLoader.qml
@@ -0,0 +1,138 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common"
+import "root:/modules/common/functions/file_utils.js" as FileUtils
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/object_utils.js" as ObjectUtils
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+import Qt.labs.platform
+
+/**
+ * Loads and manages the shell configuration file.
+ * The config file is by default at XDG_CONFIG_HOME/illogical-impulse/config.json.
+ * Automatically reloaded when the file changes.
+ */
+Singleton {
+ id: root
+ property string filePath: Directories.shellConfigPath
+ property bool firstLoad: true
+ property bool preventNextLoad: false
+ property var preventNextNotification: false
+
+ function loadConfig() {
+ configFileView.reload()
+ }
+
+ function applyConfig(fileContent) {
+ try {
+ if (fileContent.trim() === "") {
+ console.warn("[ConfigLoader] Config file is empty, skipping load.");
+ return;
+ }
+ const json = JSON.parse(fileContent);
+
+ ObjectUtils.applyToQtObject(ConfigOptions, json);
+ if (root.firstLoad) {
+ root.firstLoad = false;
+ root.preventNextLoad = true;
+ root.saveConfig(); // Make sure new properties are added to the user's config file
+ }
+ } catch (e) {
+ console.error("[ConfigLoader] Error reading file:", e);
+ console.log("[ConfigLoader] File content was:", fileContent);
+ Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration failed to load")}" "${root.filePath}"`)
+ return;
+
+ }
+ }
+
+ function setLiveConfigValue(nestedKey, value) {
+ let keys = nestedKey.split(".");
+ let obj = ConfigOptions;
+ let parents = [obj];
+
+ // Traverse and collect parent objects
+ for (let i = 0; i < keys.length - 1; ++i) {
+ if (!obj[keys[i]] || typeof obj[keys[i]] !== "object") {
+ obj[keys[i]] = {};
+ }
+ obj = obj[keys[i]];
+ parents.push(obj);
+ }
+
+ // Convert value to correct type using JSON.parse when safe
+ let convertedValue = value;
+ if (typeof value === "string") {
+ let trimmed = value.trim();
+ if (trimmed === "true" || trimmed === "false" || !isNaN(Number(trimmed))) {
+ try {
+ convertedValue = JSON.parse(trimmed);
+ } catch (e) {
+ convertedValue = value;
+ }
+ }
+ }
+
+ obj[keys[keys.length - 1]] = convertedValue;
+ }
+
+ function saveConfig() {
+ const plainConfig = ObjectUtils.toPlainObject(ConfigOptions)
+ Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(JSON.stringify(plainConfig, null, 2))}' > '${root.filePath}'`)
+ }
+
+ function setConfigValueAndSave(nestedKey, value, preventNextNotification = true) {
+ setLiveConfigValue(nestedKey, value);
+ root.preventNextNotification = preventNextNotification;
+ saveConfig();
+ }
+
+ Timer {
+ id: delayedFileRead
+ interval: ConfigOptions.hacks.arbitraryRaceConditionDelay
+ running: false
+ onTriggered: {
+ if (root.preventNextLoad) {
+ root.preventNextLoad = false;
+ return;
+ }
+ if (root.firstLoad) {
+ root.applyConfig(configFileView.text())
+ } else {
+ root.applyConfig(configFileView.text())
+ if (!root.preventNextNotification) {
+ // Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration reloaded")}" "${root.filePath}"`)
+ } else {
+ root.preventNextNotification = false;
+ }
+ }
+ }
+ }
+
+ FileView {
+ id: configFileView
+ path: Qt.resolvedUrl(root.filePath)
+ watchChanges: true
+ onFileChanged: {
+ this.reload()
+ delayedFileRead.start()
+ }
+ onLoadedChanged: {
+ const fileContent = configFileView.text()
+ delayedFileRead.start()
+ }
+ onLoadFailed: (error) => {
+ if(error == FileViewError.FileNotFound) {
+ console.log("[ConfigLoader] File not found, creating new file.")
+ root.saveConfig()
+ Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration created")}" "${root.filePath}"`)
+ } else {
+ Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration failed to load")}" "${root.filePath}"`)
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/services/DateTime.qml b/.config/quickshell/services/DateTime.qml
new file mode 100644
index 000000000..4f24e9447
--- /dev/null
+++ b/.config/quickshell/services/DateTime.qml
@@ -0,0 +1,52 @@
+import "root:/modules/common"
+import QtQuick
+import Quickshell
+import Quickshell.Io
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+/**
+ * A nice wrapper for date and time strings.
+ */
+Singleton {
+ property string time: Qt.formatDateTime(clock.date, ConfigOptions?.time.format ?? "hh:mm")
+ property string date: Qt.formatDateTime(clock.date, ConfigOptions?.time.dateFormat ?? "dddd, dd/MM")
+ property string collapsedCalendarFormat: Qt.formatDateTime(clock.date, "dd MMMM yyyy")
+ property string uptime: "0h, 0m"
+
+ SystemClock {
+ id: clock
+ precision: SystemClock.Minutes
+ }
+
+ Timer {
+ interval: 10
+ running: true
+ repeat: true
+ onTriggered: {
+ fileUptime.reload()
+ const textUptime = fileUptime.text()
+ const uptimeSeconds = Number(textUptime.split(" ")[0] ?? 0)
+
+ // Convert seconds to days, hours, and minutes
+ const days = Math.floor(uptimeSeconds / 86400)
+ const hours = Math.floor((uptimeSeconds % 86400) / 3600)
+ const minutes = Math.floor((uptimeSeconds % 3600) / 60)
+
+ // Build the formatted uptime string
+ let formatted = ""
+ if (days > 0) formatted += `${days}d`
+ if (hours > 0) formatted += `${formatted ? ", " : ""}${hours}h`
+ if (minutes > 0 || !formatted) formatted += `${formatted ? ", " : ""}${minutes}m`
+ uptime = formatted
+ interval = ConfigOptions?.resources?.updateInterval ?? 3000
+ }
+ }
+
+ FileView {
+ id: fileUptime
+
+ path: "/proc/uptime"
+ }
+
+}
diff --git a/.config/quickshell/services/Emojis.qml b/.config/quickshell/services/Emojis.qml
new file mode 100644
index 000000000..852c831b5
--- /dev/null
+++ b/.config/quickshell/services/Emojis.qml
@@ -0,0 +1,65 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
+import "root:/modules/common/functions/levendist.js" as Levendist
+import "root:/modules/common"
+import QtQuick
+import Quickshell
+import Quickshell.Io
+
+/**
+ * Emojis.
+ */
+Singleton {
+ id: root
+ property string emojiScriptPath: `${Directories.config}/hypr/hyprland/scripts/fuzzel-emoji.sh`
+ property string lineBeforeData: "### DATA ###"
+ property list list
+ readonly property var preparedEntries: list.map(a => ({
+ name: Fuzzy.prepare(`${a}`),
+ entry: a
+ }))
+ function fuzzyQuery(search: string): var {
+ if (root.sloppySearch) {
+ const results = entries.slice(0, 100).map(str => ({
+ entry: str,
+ score: Levendist.computeTextMatchScore(str.toLowerCase(), search.toLowerCase())
+ })).filter(item => item.score > root.scoreThreshold)
+ .sort((a, b) => b.score - a.score)
+ return results
+ .map(item => item.entry)
+ }
+
+ return Fuzzy.go(search, preparedEntries, {
+ all: true,
+ key: "name"
+ }).map(r => {
+ return r.obj.entry
+ });
+ }
+
+ function load() {
+ emojiFileView.reload()
+ }
+
+ function updateEmojis(fileContent) {
+ const lines = fileContent.split("\n")
+ const dataIndex = lines.indexOf(root.lineBeforeData)
+ if (dataIndex === -1) {
+ console.warn("No data section found in emoji script file.")
+ return
+ }
+ const emojis = lines.slice(dataIndex + 1).filter(line => line.trim() !== "")
+ root.list = emojis.map(line => line.trim())
+ }
+
+ FileView {
+ id: emojiFileView
+ path: Qt.resolvedUrl(root.emojiScriptPath)
+ onLoadedChanged: {
+ const fileContent = emojiFileView.text()
+ root.updateEmojis(fileContent)
+ }
+ }
+}
diff --git a/.config/quickshell/services/FirstRunExperience.qml b/.config/quickshell/services/FirstRunExperience.qml
new file mode 100644
index 000000000..eee995a55
--- /dev/null
+++ b/.config/quickshell/services/FirstRunExperience.qml
@@ -0,0 +1,36 @@
+pragma Singleton
+
+import "root:/modules/common/functions/file_utils.js" as FileUtils
+import "root:/modules/common"
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+
+Singleton {
+ id: root
+ property string firstRunFilePath: `${Directories.state}/user/first_run.txt`
+ property string firstRunFileContent: "This file is just here to confirm you've been greeted :>"
+ property string firstRunNotifSummary: "Welcome!"
+ property string firstRunNotifBody: "Hit Super+/ for a list of keybinds"
+ property string defaultWallpaperPath: FileUtils.trimFileProtocol(`${Directories.config}/quickshell/assets/images/default_wallpaper.png`)
+
+ function load() {
+ firstRunFileView.reload()
+ }
+
+ function handleFirstRun() {
+ Hyprland.dispatch(`exec notify-send '${root.firstRunNotifSummary}' '${root.firstRunNotifBody}' -a 'Shell'`)
+ Hyprland.dispatch(`exec '${Directories.wallpaperSwitchScriptPath}' '${root.defaultWallpaperPath}'`)
+ }
+
+ FileView {
+ id: firstRunFileView
+ path: Qt.resolvedUrl(firstRunFilePath)
+ onLoadFailed: (error) => {
+ if (error == FileViewError.FileNotFound) {
+ firstRunFileView.setText(root.firstRunFileContent)
+ root.handleFirstRun()
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/services/HyprlandData.qml b/.config/quickshell/services/HyprlandData.qml
new file mode 100644
index 000000000..2b88ad9cc
--- /dev/null
+++ b/.config/quickshell/services/HyprlandData.qml
@@ -0,0 +1,69 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+/**
+ * Provides access to some Hyprland data not available in Quickshell.Hyprland.
+ */
+Singleton {
+ id: root
+ property var windowList: []
+ property var addresses: []
+ property var windowByAddress: ({})
+ property var monitors: []
+
+ function updateWindowList() {
+ getClients.running = true
+ getMonitors.running = true
+ }
+
+ Component.onCompleted: {
+ updateWindowList()
+ }
+
+ Connections {
+ target: Hyprland
+
+ function onRawEvent(event) {
+ // Filter out redundant old v1 events for the same thing
+ if(event.name in [
+ "activewindow", "focusedmon", "monitoradded",
+ "createworkspace", "destroyworkspace", "moveworkspace",
+ "activespecial", "movewindow", "windowtitle"
+ ]) return ;
+ updateWindowList()
+ }
+ }
+
+ Process {
+ id: getClients
+ command: ["bash", "-c", "hyprctl clients -j | jq -c"]
+ stdout: SplitParser {
+ onRead: (data) => {
+ root.windowList = JSON.parse(data)
+ let tempWinByAddress = {}
+ for (var i = 0; i < root.windowList.length; ++i) {
+ var win = root.windowList[i]
+ tempWinByAddress[win.address] = win
+ }
+ root.windowByAddress = tempWinByAddress
+ root.addresses = root.windowList.map((win) => win.address)
+ }
+ }
+ }
+ Process {
+ id: getMonitors
+ command: ["bash", "-c", "hyprctl monitors -j | jq -c"]
+ stdout: SplitParser {
+ onRead: (data) => {
+ root.monitors = JSON.parse(data)
+ }
+ }
+ }
+}
+
diff --git a/.config/quickshell/services/HyprlandKeybinds.qml b/.config/quickshell/services/HyprlandKeybinds.qml
new file mode 100644
index 000000000..189ba76d5
--- /dev/null
+++ b/.config/quickshell/services/HyprlandKeybinds.qml
@@ -0,0 +1,73 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common"
+import "root:/modules/common/functions/file_utils.js" as FileUtils
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+/**
+ * A service that provides access to Hyprland keybinds.
+ * Uses the `get_keybinds.py` script to parse comments in config files in a certain format and convert to JSON.
+ */
+Singleton {
+ id: root
+ property string keybindParserPath: FileUtils.trimFileProtocol(`${Directories.config}/quickshell/scripts/hyprland/get_keybinds.py`)
+ property string defaultKeybindConfigPath: FileUtils.trimFileProtocol(`${Directories.config}/hypr/hyprland/keybinds.conf`)
+ property string userKeybindConfigPath: FileUtils.trimFileProtocol(`${Directories.config}/hypr/custom/keybinds.conf`)
+ property var defaultKeybinds: {"children": []}
+ property var userKeybinds: {"children": []}
+ property var keybinds: ({
+ children: [
+ ...(defaultKeybinds.children ?? []),
+ ...(userKeybinds.children ?? []),
+ ]
+ })
+
+ Connections {
+ target: Hyprland
+
+ function onRawEvent(event) {
+ if (event.name == "configreloaded") {
+ getDefaultKeybinds.running = true
+ getUserKeybinds.running = true
+ }
+ }
+ }
+
+ Process {
+ id: getDefaultKeybinds
+ running: true
+ command: [root.keybindParserPath, "--path", root.defaultKeybindConfigPath,]
+
+ stdout: SplitParser {
+ onRead: data => {
+ try {
+ root.defaultKeybinds = JSON.parse(data)
+ } catch (e) {
+ console.error("[CheatsheetKeybinds] Error parsing keybinds:", e)
+ }
+ }
+ }
+ }
+
+ Process {
+ id: getUserKeybinds
+ running: true
+ command: [root.keybindParserPath, "--path", root.userKeybindConfigPath]
+
+ stdout: SplitParser {
+ onRead: data => {
+ try {
+ root.userKeybinds = JSON.parse(data)
+ } catch (e) {
+ console.error("[CheatsheetKeybinds] Error parsing keybinds:", e)
+ }
+ }
+ }
+ }
+}
+
diff --git a/.config/quickshell/services/KeyringStorage.qml b/.config/quickshell/services/KeyringStorage.qml
new file mode 100644
index 000000000..3f3956f98
--- /dev/null
+++ b/.config/quickshell/services/KeyringStorage.qml
@@ -0,0 +1,119 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common"
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import Quickshell;
+import Quickshell.Io;
+import Qt.labs.platform
+import QtQuick;
+
+/**
+ * For storing sensitive data in the keyring.
+ * Use this for small data only, since it stores a JSON of the contents directly and doesn't use a database.
+ */
+Singleton {
+ id: root
+
+ property bool loaded: false
+ property var keyringData: ({})
+
+ property var properties: {
+ "application": "illogical-impulse",
+ "explanation": qsTr("For storing API keys and other sensitive information"),
+ }
+ property var propertiesAsArgs: Object.keys(root.properties).reduce(
+ function(arr, key) {
+ return arr.concat([key, root.properties[key]]);
+ }, []
+ )
+ property string keyringLabel: StringUtils.format(qsTr("{0} Safe Storage"), "illogical-impulse")
+
+ function setNestedField(path, value) {
+ if (!root.keyringData) root.keyringData = {};
+ let keys = path;
+ let obj = root.keyringData;
+ let parents = [obj];
+
+ // Traverse and collect parent objects
+ for (let i = 0; i < keys.length - 1; ++i) {
+ if (!obj[keys[i]] || typeof obj[keys[i]] !== "object") {
+ obj[keys[i]] = {};
+ }
+ obj = obj[keys[i]];
+ parents.push(obj);
+ }
+
+ // Set the value at the innermost key
+ obj[keys[keys.length - 1]] = value;
+
+ // Reassign each parent object from the bottom up to trigger change notifications
+ for (let i = keys.length - 2; i >= 0; --i) {
+ let parent = parents[i];
+ let key = keys[i];
+ // Shallow clone to change object identity (spread replaced with Object.assign)
+ parent[key] = Object.assign({}, parent[key]);
+ }
+
+ // Finally, reassign root.keyringData to trigger top-level change
+ root.keyringData = Object.assign({}, root.keyringData);
+
+ saveKeyringData();
+ }
+
+ function fetchKeyringData() {
+ // console.log("[KeyringStorage] Fetching keyring data...");
+ // console.log("[KeyringStorage] getData command:'" + getData.command.join("' '") + "'");
+ getData.running = true;
+ }
+
+ function saveKeyringData() {
+ saveData.stdinEnabled = true;
+ saveData.running = true;
+ }
+
+ Process {
+ id: saveData
+ command: [
+ "secret-tool", "store", "--label=" + keyringLabel,
+ ...propertiesAsArgs,
+ ]
+ onRunningChanged: {
+ if (saveData.running) {
+ // console.log("[KeyringStorage] Saving with command: '" + saveData.command.join("' '") + "'");
+ saveData.write(JSON.stringify(root.keyringData));
+ stdinEnabled = false // End input stream
+ }
+ }
+ }
+
+ Process {
+ id: getData
+ command: [ // We need to use echo for a newline so splitparser does parse
+ "bash", "-c", `echo $(secret-tool lookup 'application' 'illogical-impulse')`,
+ ]
+ stdout: SplitParser {
+ onRead: data => {
+ if(data.length === 0) return;
+ try {
+ root.keyringData = JSON.parse(data);
+ // console.log("[KeyringStorage] Keyring data fetched:", JSON.stringify(root.keyringData));
+ } catch (e) {
+ console.error("[KeyringStorage] Failed to get keyring data, reinitializing.");
+ root.keyringData = {};
+ saveKeyringData()
+ }
+ }
+ }
+ onExited: (exitCode, exitStatus) => {
+ // console.log("[KeyringStorage] Keyring data fetch process exited with code:", exitCode);
+ if (exitCode !== 0) {
+ console.error("[KeyringStorage] Failed to get keyring data, reinitializing.");
+ root.keyringData = {};
+ saveKeyringData()
+ }
+ root.loaded = true;
+ }
+ }
+
+}
diff --git a/.config/quickshell/services/LatexRenderer.qml b/.config/quickshell/services/LatexRenderer.qml
new file mode 100644
index 000000000..e7066fa4c
--- /dev/null
+++ b/.config/quickshell/services/LatexRenderer.qml
@@ -0,0 +1,87 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/file_utils.js" as FileUtils
+import "root:/modules/common"
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+import Qt.labs.platform
+
+/**
+ * Renders LaTeX snippets with MicroTeX.
+ * For every request:
+ * 1. Hash it
+ * 2. Check if the hash is already processed
+ * 3. If not, render it with MicroTeX and mark as processed
+ */
+Singleton {
+ id: root
+
+ readonly property var renderPadding: 4 // This is to prevent cutoff in the rendered images
+
+ property list processedHashes: []
+ property var processedExpressions: ({})
+ property var renderedImagePaths: ({})
+ property string microtexBinaryDir: "/opt/MicroTeX"
+ property string microtexBinaryName: "LaTeX"
+ property string latexOutputPath: Directories.latexOutput
+
+ signal renderFinished(string hash, string imagePath)
+
+ /**
+ * Requests rendering of a LaTeX expression.
+ * Returns the [hash, isNew]
+ */
+ function requestRender(expression) {
+ // 1. Hash it and initialize necessary variables
+ const hash = Qt.md5(expression)
+ const imagePath = `${latexOutputPath}/${hash}.svg`
+
+ // 2. Check if the hash is already processed
+ if (processedHashes.includes(hash)) {
+ // console.log("Already processed: " + hash)
+ renderFinished(hash, imagePath)
+ return [hash, false]
+ } else {
+ root.processedHashes.push(hash)
+ root.processedExpressions[hash] = expression
+ // console.log("Rendering expression: " + expression)
+ }
+
+ // 3. If not, render it with MicroTeX and mark as processed
+ // console.log(`[LatexRenderer] Rendering expression: ${expression} with hash: ${hash}`)
+ // console.log(` to file: ${imagePath}`)
+ // console.log(` with command: cd ${microtexBinaryDir} && ./${microtexBinaryName} -headless -input=${StringUtils.shellSingleQuoteEscape(expression)} -output=${imagePath} -textsize=${Appearance.font.pixelSize.normal} -padding=${renderPadding} -background=${Appearance.m3colors.m3tertiary} -foreground=${Appearance.m3colors.m3onTertiary} -maxwidth=0.85`)
+ const processQml = `
+ import Quickshell.Io
+ Process {
+ id: microtexProcess${hash}
+ running: true
+ command: [ "bash", "-c",
+ "cd ${root.microtexBinaryDir} && ./${root.microtexBinaryName} -headless '-input=${StringUtils.shellSingleQuoteEscape(StringUtils.escapeBackslashes(expression))}' "
+ + "'-output=${imagePath}' "
+ + "'-textsize=${Appearance.font.pixelSize.normal}' "
+ + "'-padding=${renderPadding}' "
+ // + "'-background=${Appearance.m3colors.m3tertiary}' "
+ + "'-foreground=${Appearance.colors.colOnLayer1}' "
+ + "-maxwidth=0.85 "
+ ]
+ // stdout: SplitParser {
+ // onRead: data => { console.log("MicroTeX: " + data) }
+ // }
+ onExited: (exitCode, exitStatus) => {
+ // console.log("[LatexRenderer] MicroTeX process exited with code: " + exitCode + ", status: " + exitStatus)
+ renderedImagePaths["${hash}"] = "${imagePath}"
+ root.renderFinished("${hash}", "${imagePath}")
+ microtexProcess${hash}.destroy()
+ }
+ }
+ `
+ // console.log("MicroTeX: " + processQml)
+ Qt.createQmlObject(processQml, root, `MicroTeXProcess_${hash}`)
+ return [hash, true]
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/services/MaterialThemeLoader.qml b/.config/quickshell/services/MaterialThemeLoader.qml
new file mode 100644
index 000000000..cd4eb686b
--- /dev/null
+++ b/.config/quickshell/services/MaterialThemeLoader.qml
@@ -0,0 +1,58 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common"
+import QtQuick
+import Quickshell
+import Quickshell.Io
+
+/**
+ * Automatically reloads generated material colors.
+ * It is necessary to run reapplyTheme() on startup because Singletons are lazily loaded.
+ */
+Singleton {
+ id: root
+ property string filePath: Directories.generatedMaterialThemePath
+
+ function reapplyTheme() {
+ themeFileView.reload()
+ }
+
+ function applyColors(fileContent) {
+ const json = JSON.parse(fileContent)
+ for (const key in json) {
+ if (json.hasOwnProperty(key)) {
+ // Convert snake_case to CamelCase
+ const camelCaseKey = key.replace(/_([a-z])/g, (g) => g[1].toUpperCase())
+ const m3Key = `m3${camelCaseKey}`
+ Appearance.m3colors[m3Key] = json[key]
+ }
+ }
+
+ Appearance.m3colors.darkmode = (Appearance.m3colors.m3background.hslLightness < 0.5)
+ }
+
+ Timer {
+ id: delayedFileRead
+ interval: ConfigOptions?.hacks?.arbitraryRaceConditionDelay ?? 100
+ repeat: false
+ running: false
+ onTriggered: {
+ root.applyColors(themeFileView.text())
+ }
+ }
+
+ FileView {
+ id: themeFileView
+ path: Qt.resolvedUrl(root.filePath)
+ watchChanges: true
+ onFileChanged: {
+ this.reload()
+ delayedFileRead.start()
+ }
+ onLoadedChanged: {
+ const fileContent = themeFileView.text()
+ root.applyColors(fileContent)
+ }
+ }
+}
diff --git a/.config/quickshell/services/MprisController.qml b/.config/quickshell/services/MprisController.qml
new file mode 100644
index 000000000..96aa5e80b
--- /dev/null
+++ b/.config/quickshell/services/MprisController.qml
@@ -0,0 +1,164 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+// From https://git.outfoxxed.me/outfoxxed/nixnew
+// It does not have a license, but the author is okay with redistribution.
+
+import QtQml.Models
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Services.Mpris
+
+/**
+ * A service that provides easy access to the active Mpris player.
+ */
+Singleton {
+ id: root;
+ property MprisPlayer trackedPlayer: null;
+ property MprisPlayer activePlayer: trackedPlayer ?? Mpris.players.values[0] ?? null;
+ signal trackChanged(reverse: bool);
+
+ property bool __reverse: false;
+
+ property var activeTrack;
+
+ Instantiator {
+ model: Mpris.players;
+
+ Connections {
+ required property MprisPlayer modelData;
+ target: modelData;
+
+ Component.onCompleted: {
+ if (root.trackedPlayer == null || modelData.isPlaying) {
+ root.trackedPlayer = modelData;
+ }
+ }
+
+ Component.onDestruction: {
+ if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) {
+ for (const player of Mpris.players.values) {
+ if (player.playbackState.isPlaying) {
+ root.trackedPlayer = player;
+ break;
+ }
+ }
+
+ if (trackedPlayer == null && Mpris.players.values.length != 0) {
+ trackedPlayer = Mpris.players.values[0];
+ }
+ }
+ }
+
+ function onPlaybackStateChanged() {
+ if (root.trackedPlayer !== modelData) root.trackedPlayer = modelData;
+ }
+ }
+ }
+
+ Connections {
+ target: activePlayer
+
+ function onPostTrackChanged() {
+ root.updateTrack();
+ }
+
+ function onTrackArtUrlChanged() {
+ // console.log("arturl:", activePlayer.trackArtUrl)
+ // root.updateTrack();
+ if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) {
+ // cantata likes to send cover updates *BEFORE* updating the track info.
+ // as such, art url changes shouldn't be able to break the reverse animation
+ const r = root.__reverse;
+ root.updateTrack();
+ root.__reverse = r;
+
+ }
+ }
+ }
+
+ onActivePlayerChanged: this.updateTrack();
+
+ function updateTrack() {
+ //console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`)
+ this.activeTrack = {
+ uniqueId: this.activePlayer?.uniqueId ?? 0,
+ artUrl: this.activePlayer?.trackArtUrl ?? "",
+ title: this.activePlayer?.trackTitle || qsTr("Unknown Title"),
+ artist: this.activePlayer?.trackArtist || qsTr("Unknown Artist"),
+ album: this.activePlayer?.trackAlbum || qsTr("Unknown Album"),
+ };
+
+ this.trackChanged(__reverse);
+ this.__reverse = false;
+ }
+
+ property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying;
+ property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false;
+ function togglePlaying() {
+ if (this.canTogglePlaying) this.activePlayer.togglePlaying();
+ }
+
+ property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false;
+ function previous() {
+ if (this.canGoPrevious) {
+ this.__reverse = true;
+ this.activePlayer.previous();
+ }
+ }
+
+ property bool canGoNext: this.activePlayer?.canGoNext ?? false;
+ function next() {
+ if (this.canGoNext) {
+ this.__reverse = false;
+ this.activePlayer.next();
+ }
+ }
+
+ property bool canChangeVolume: this.activePlayer && this.activePlayer.volumeSupported && this.activePlayer.canControl;
+
+ property bool loopSupported: this.activePlayer && this.activePlayer.loopSupported && this.activePlayer.canControl;
+ property var loopState: this.activePlayer?.loopState ?? MprisLoopState.None;
+ function setLoopState(loopState: var) {
+ if (this.loopSupported) {
+ this.activePlayer.loopState = loopState;
+ }
+ }
+
+ property bool shuffleSupported: this.activePlayer && this.activePlayer.shuffleSupported && this.activePlayer.canControl;
+ property bool hasShuffle: this.activePlayer?.shuffle ?? false;
+ function setShuffle(shuffle: bool) {
+ if (this.shuffleSupported) {
+ this.activePlayer.shuffle = shuffle;
+ }
+ }
+
+ function setActivePlayer(player: MprisPlayer) {
+ const targetPlayer = player ?? Mpris.players[0];
+ console.log(`[Mpris] Active player ${targetPlayer} << ${activePlayer}`)
+
+ if (targetPlayer && this.activePlayer) {
+ this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer);
+ } else {
+ // always animate forward if going to null
+ this.__reverse = false;
+ }
+
+ this.trackedPlayer = targetPlayer;
+ }
+
+ IpcHandler {
+ target: "mpris"
+
+ function pauseAll(): void {
+ for (const player of Mpris.players.values) {
+ if (player.canPause) player.pause();
+ }
+ }
+
+ function playPause(): void { root.togglePlaying(); }
+ function previous(): void { root.previous(); }
+ function next(): void { root.next(); }
+ }
+}
diff --git a/.config/quickshell/services/Network.qml b/.config/quickshell/services/Network.qml
new file mode 100644
index 000000000..50bfb671f
--- /dev/null
+++ b/.config/quickshell/services/Network.qml
@@ -0,0 +1,93 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import Quickshell
+import Quickshell.Io
+import QtQuick
+
+/**
+ * Simple polled network state service.
+ */
+Singleton {
+ id: root
+
+ property bool wifi: true
+ property bool ethernet: false
+ property int updateInterval: 1000
+ property string networkName: ""
+ property int networkStrength
+ property string materialSymbol: ethernet ? "lan" :
+ (Network.networkName.length > 0 && Network.networkName != "lo") ? (
+ Network.networkStrength > 80 ? "signal_wifi_4_bar" :
+ Network.networkStrength > 60 ? "network_wifi_3_bar" :
+ Network.networkStrength > 40 ? "network_wifi_2_bar" :
+ Network.networkStrength > 20 ? "network_wifi_1_bar" :
+ "signal_wifi_0_bar"
+ ) : "signal_wifi_off"
+ function update() {
+ updateConnectionType.startCheck();
+ updateNetworkName.running = true;
+ updateNetworkStrength.running = true;
+ }
+
+ Timer {
+ interval: 10
+ running: true
+ repeat: true
+ onTriggered: {
+ root.update();
+ interval = root.updateInterval;
+ }
+ }
+
+ Process {
+ id: updateConnectionType
+ property string buffer
+ command: ["sh", "-c", "nmcli -t -f NAME,TYPE,DEVICE c show --active"]
+ running: true
+ function startCheck() {
+ buffer = "";
+ updateConnectionType.running = true;
+ }
+ stdout: SplitParser {
+ onRead: data => {
+ updateConnectionType.buffer += data + "\n";
+ }
+ }
+ onExited: (exitCode, exitStatus) => {
+ const lines = updateConnectionType.buffer.trim().split('\n');
+ let hasEthernet = false;
+ let hasWifi = false;
+ lines.forEach(line => {
+ if (line.includes("ethernet"))
+ hasEthernet = true;
+ else if (line.includes("wireless"))
+ hasWifi = true;
+ });
+ root.ethernet = hasEthernet;
+ root.wifi = hasWifi;
+ }
+ }
+
+ Process {
+ id: updateNetworkName
+ command: ["sh", "-c", "nmcli -t -f NAME c show --active | head -1"]
+ running: true
+ stdout: SplitParser {
+ onRead: data => {
+ root.networkName = data;
+ }
+ }
+ }
+
+ Process {
+ id: updateNetworkStrength
+ running: true
+ command: ["sh", "-c", "nmcli -f IN-USE,SIGNAL,SSID device wifi | awk '/^\*/{if (NR!=1) {print $2}}'"]
+ stdout: SplitParser {
+ onRead: data => {
+ root.networkStrength = parseInt(data);
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/services/Notifications.qml b/.config/quickshell/services/Notifications.qml
new file mode 100644
index 000000000..75033292c
--- /dev/null
+++ b/.config/quickshell/services/Notifications.qml
@@ -0,0 +1,274 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common"
+import "root:/"
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Services.Notifications
+
+/**
+ * Provides extra features not in Quickshell.Services.Notifications:
+ * - Persistent storage
+ * - Popup notifications, with timeout
+ * - Notification groups by app
+ */
+Singleton {
+ id: root
+ component Notif: QtObject {
+ required property int id
+ property Notification notification
+ property list actions: notification?.actions.map((action) => ({
+ "identifier": action.identifier,
+ "text": action.text,
+ })) ?? []
+ property bool popup: false
+ property string appIcon: notification?.appIcon ?? ""
+ property string appName: notification?.appName ?? ""
+ property string body: notification?.body ?? ""
+ property string image: notification?.image ?? ""
+ property string summary: notification?.summary ?? ""
+ property double time
+ property string urgency: notification?.urgency.toString() ?? "normal"
+ property Timer timer
+ }
+
+ function notifToJSON(notif) {
+ return {
+ "id": notif.id,
+ "actions": notif.actions,
+ "appIcon": notif.appIcon,
+ "appName": notif.appName,
+ "body": notif.body,
+ "image": notif.image,
+ "summary": notif.summary,
+ "time": notif.time,
+ "urgency": notif.urgency,
+ }
+ }
+ function notifToString(notif) {
+ return JSON.stringify(notifToJSON(notif), null, 2);
+ }
+
+ component NotifTimer: Timer {
+ required property int id
+ interval: 5000
+ running: true
+ onTriggered: () => {
+ root.timeoutNotification(id);
+ destroy()
+ }
+ }
+
+ property bool silent: false
+ property var filePath: Directories.notificationsPath
+ property list list: []
+ property var popupList: list.filter((notif) => notif.popup);
+ property bool popupInhibited: (GlobalStates?.sidebarRightOpen ?? false) || silent
+ property var latestTimeForApp: ({})
+ Component {
+ id: notifComponent
+ Notif {}
+ }
+ Component {
+ id: notifTimerComponent
+ NotifTimer {}
+ }
+
+ function stringifyList(list) {
+ return JSON.stringify(list.map((notif) => notifToJSON(notif)), null, 2);
+ }
+
+ onListChanged: {
+ // Update latest time for each app
+ root.list.forEach((notif) => {
+ if (!root.latestTimeForApp[notif.appName] || notif.time > root.latestTimeForApp[notif.appName]) {
+ root.latestTimeForApp[notif.appName] = Math.max(root.latestTimeForApp[notif.appName] || 0, notif.time);
+ }
+ });
+ // Remove apps that no longer have notifications
+ Object.keys(root.latestTimeForApp).forEach((appName) => {
+ if (!root.list.some((notif) => notif.appName === appName)) {
+ delete root.latestTimeForApp[appName];
+ }
+ });
+ }
+
+ function appNameListForGroups(groups) {
+ return Object.keys(groups).sort((a, b) => {
+ // Sort by time, descending
+ return groups[b].time - groups[a].time;
+ });
+ }
+
+ function groupsForList(list) {
+ const groups = {};
+ list.forEach((notif) => {
+ if (!groups[notif.appName]) {
+ groups[notif.appName] = {
+ appName: notif.appName,
+ appIcon: notif.appIcon,
+ notifications: [],
+ time: 0
+ };
+ }
+ groups[notif.appName].notifications.push(notif);
+ // Always set to the latest time in the group
+ groups[notif.appName].time = latestTimeForApp[notif.appName] || notif.time;
+ });
+ return groups;
+ }
+
+ property var groupsByAppName: groupsForList(root.list)
+ property var popupGroupsByAppName: groupsForList(root.popupList)
+ property var appNameList: appNameListForGroups(root.groupsByAppName)
+ property var popupAppNameList: appNameListForGroups(root.popupGroupsByAppName)
+
+ // Quickshell's notification IDs starts at 1 on each run, while saved notifications
+ // can already contain higher IDs. This is for avoiding id collisions
+ property int idOffset
+ signal initDone();
+ signal notify(notification: var);
+ signal discard(id: var);
+ signal discardAll();
+ signal timeout(id: var);
+
+ NotificationServer {
+ id: notifServer
+ // actionIconsSupported: true
+ actionsSupported: true
+ bodyHyperlinksSupported: true
+ bodyImagesSupported: true
+ bodyMarkupSupported: true
+ bodySupported: true
+ imageSupported: true
+ keepOnReload: false
+ persistenceSupported: true
+
+ onNotification: (notification) => {
+ notification.tracked = true
+ const newNotifObject = notifComponent.createObject(root, {
+ "id": notification.id + root.idOffset,
+ "notification": notification,
+ "time": Date.now(),
+ });
+ root.list = [...root.list, newNotifObject];
+
+ // Popup
+ if (!root.popupInhibited) {
+ newNotifObject.popup = true;
+ newNotifObject.timer = notifTimerComponent.createObject(root, {
+ "id": newNotifObject.id,
+ "interval": notification.expireTimeout < 0 ? 5000 : notification.expireTimeout,
+ });
+ }
+
+ root.notify(newNotifObject);
+ // console.log(notifToString(newNotifObject));
+ notifFileView.setText(stringifyList(root.list));
+ }
+ }
+
+ function discardNotification(id) {
+ const index = root.list.findIndex((notif) => notif.id === id);
+ const notifServerIndex = notifServer.trackedNotifications.values.findIndex((notif) => notif.id + root.idOffset === id);
+ if (index !== -1) {
+ root.list.splice(index, 1);
+ notifFileView.setText(stringifyList(root.list));
+ triggerListChange()
+ }
+ if (notifServerIndex !== -1) {
+ notifServer.trackedNotifications.values[notifServerIndex].dismiss()
+ }
+ root.discard(id);
+ }
+
+ function discardAllNotifications() {
+ root.list = []
+ triggerListChange()
+ notifFileView.setText(stringifyList(root.list));
+ notifServer.trackedNotifications.values.forEach((notif) => {
+ notif.dismiss()
+ })
+ root.discardAll();
+ }
+
+ function timeoutNotification(id) {
+ const index = root.list.findIndex((notif) => notif.id === id);
+ if (root.list[index] != null)
+ root.list[index].popup = false;
+ root.timeout(id);
+ }
+
+ function timeoutAll() {
+ root.popupList.forEach((notif) => {
+ root.timeout(notif.id);
+ })
+ root.popupList.forEach((notif) => {
+ notif.popup = false;
+ });
+ }
+
+ function attemptInvokeAction(id, notifIdentifier) {
+ const notifServerIndex = notifServer.trackedNotifications.values.findIndex((notif) => notif.id + root.idOffset === id);
+ if (notifServerIndex !== -1) {
+ const notifServerNotif = notifServer.trackedNotifications.values[notifServerIndex];
+ const action = notifServerNotif.actions.find((action) => action.identifier === notifIdentifier);
+ action.invoke()
+ }
+ // else console.log("Notification not found in server: " + id)
+ // root.discard(id);
+ }
+
+ function triggerListChange() {
+ root.list = root.list.slice(0)
+ }
+
+ function refresh() {
+ notifFileView.reload()
+ }
+
+ Component.onCompleted: {
+ refresh()
+ }
+
+ FileView {
+ id: notifFileView
+ path: Qt.resolvedUrl(filePath)
+ onLoaded: {
+ const fileContents = notifFileView.text()
+ root.list = JSON.parse(fileContents).map((notif) => {
+ return notifComponent.createObject(root, {
+ "id": notif.id,
+ "actions": notif.actions,
+ "appIcon": notif.appIcon,
+ "appName": notif.appName,
+ "body": notif.body,
+ "image": notif.image,
+ "summary": notif.summary,
+ "time": notif.time,
+ "urgency": notif.urgency,
+ });
+ });
+ // Find largest id
+ let maxId = 0
+ root.list.forEach((notif) => {
+ maxId = Math.max(maxId, notif.id)
+ })
+
+ console.log("[Notifications] File loaded")
+ root.idOffset = maxId
+ root.initDone()
+ }
+ onLoadFailed: (error) => {
+ if(error == FileViewError.FileNotFound) {
+ console.log("[Notifications] File not found, creating new file.")
+ root.list = []
+ notifFileView.setText(stringifyList(root.list));
+ } else {
+ console.log("[Notifications] Error loading file: " + error)
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/services/PersistentStateManager.qml b/.config/quickshell/services/PersistentStateManager.qml
new file mode 100644
index 000000000..c3d1536ed
--- /dev/null
+++ b/.config/quickshell/services/PersistentStateManager.qml
@@ -0,0 +1,105 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common"
+import "root:/modules/common/functions/object_utils.js" as ObjectUtils
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+import Qt.labs.platform
+
+/**
+ * Manages persistent states across sessions.
+ * Run loadStates() once at startup to load the states, then use setState() and getState() to modify and access them.
+ */
+Singleton {
+ id: root
+ property string fileDir: Directories.state
+ property string fileName: "states.json"
+ property string filePath: `${root.fileDir}/${root.fileName}`
+ property bool allowWriteback: false
+
+ function getState(nestedKey) {
+ let keys = nestedKey.split(".");
+ let obj = PersistentStates;
+ for (let i = 0; i < keys.length; ++i) {
+ if (obj[keys[i]] === undefined) {
+ console.error(`[PersistentStateManager] Key "${keys[i]}" not found in PersistentStates`);
+ return null;
+ }
+ obj = obj[keys[i]];
+ }
+ return obj;
+ }
+
+ function setState(nestedKey, value) {
+ if (!root.allowWriteback) return;
+ let keys = nestedKey.split(".");
+ let obj = PersistentStates;
+ let parents = [obj];
+
+ // Traverse and collect parent objects
+ for (let i = 0; i < keys.length - 1; ++i) {
+ if (!obj[keys[i]] || typeof obj[keys[i]] !== "object") {
+ obj[keys[i]] = {};
+ }
+ obj = obj[keys[i]];
+ parents.push(obj);
+ }
+
+ // Set the value at the innermost key
+ obj[keys[keys.length - 1]] = value;
+
+ saveStates()
+ }
+
+ function loadStates() {
+ stateFileView.reload()
+ }
+
+ function saveStates() {
+ const plainStates = ObjectUtils.toPlainObject(PersistentStates)
+ stateFileView.setText(JSON.stringify(plainStates, null, 2))
+ }
+
+ function applyStates(fileContent) {
+ try {
+ const json = JSON.parse(fileContent);
+ ObjectUtils.applyToQtObject(PersistentStates, json);
+ root.allowWriteback = true
+ } catch (e) {
+ console.error("[PersistentStateManager] Error reading file:", e);
+ return;
+ }
+ }
+
+ Timer {
+ id: delayedFileRead
+ interval: ConfigOptions?.hacks?.arbitraryRaceConditionDelay ?? 100
+ repeat: false
+ running: false
+ onTriggered: {
+ root.applyStates(stateFileView.text())
+ }
+ }
+
+ FileView {
+ id: stateFileView
+ path: root.filePath
+ watchChanges: true
+ // onFileChanged: {
+ // console.log("[PersistentStateManager] File changed, reloading...")
+ // this.reload()
+ // delayedFileRead.start()
+ // }
+ onLoadedChanged: {
+ const fileContent = stateFileView.text()
+ root.applyStates(fileContent)
+ }
+ onLoadFailed: (error) => {
+ console.log("[PersistentStateManager] File not found, creating new file")
+ root.saveStates()
+ }
+ }
+}
diff --git a/.config/quickshell/services/ResourceUsage.qml b/.config/quickshell/services/ResourceUsage.qml
new file mode 100644
index 000000000..c9a501bc2
--- /dev/null
+++ b/.config/quickshell/services/ResourceUsage.qml
@@ -0,0 +1,62 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common"
+import QtQuick
+import Quickshell
+import Quickshell.Io
+
+/**
+ * Simple polled resource usage service with RAM, Swap, and CPU usage.
+ */
+Singleton {
+ property double memoryTotal: 1
+ property double memoryFree: 1
+ property double memoryUsed: memoryTotal - memoryFree
+ property double memoryUsedPercentage: memoryUsed / memoryTotal
+ property double swapTotal: 1
+ property double swapFree: 1
+ property double swapUsed: swapTotal - swapFree
+ property double swapUsedPercentage: swapUsed / swapTotal
+ property double cpuUsage: 0
+ property var previousCpuStats
+
+ Timer {
+ interval: 1
+ running: true
+ repeat: true
+ onTriggered: {
+ // Reload files
+ fileMeminfo.reload()
+ fileStat.reload()
+
+ // Parse memory and swap usage
+ const textMeminfo = fileMeminfo.text()
+ memoryTotal = Number(textMeminfo.match(/MemTotal: *(\d+)/)?.[1] ?? 1)
+ memoryFree = Number(textMeminfo.match(/MemAvailable: *(\d+)/)?.[1] ?? 0)
+ swapTotal = Number(textMeminfo.match(/SwapTotal: *(\d+)/)?.[1] ?? 1)
+ swapFree = Number(textMeminfo.match(/SwapFree: *(\d+)/)?.[1] ?? 0)
+
+ // Parse CPU usage
+ const textStat = fileStat.text()
+ const cpuLine = textStat.match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/)
+ if (cpuLine) {
+ const stats = cpuLine.slice(1).map(Number)
+ const total = stats.reduce((a, b) => a + b, 0)
+ const idle = stats[3]
+
+ if (previousCpuStats) {
+ const totalDiff = total - previousCpuStats.total
+ const idleDiff = idle - previousCpuStats.idle
+ cpuUsage = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0
+ }
+
+ previousCpuStats = { total, idle }
+ }
+ interval = ConfigOptions?.resources?.updateInterval ?? 3000
+ }
+ }
+
+ FileView { id: fileMeminfo; path: "/proc/meminfo" }
+ FileView { id: fileStat; path: "/proc/stat" }
+}
\ No newline at end of file
diff --git a/.config/quickshell/services/SystemInfo.qml b/.config/quickshell/services/SystemInfo.qml
new file mode 100644
index 000000000..ffd478b65
--- /dev/null
+++ b/.config/quickshell/services/SystemInfo.qml
@@ -0,0 +1,69 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import Quickshell.Io
+
+/**
+ * Provides some system info: distro, username.
+ */
+Singleton {
+ property string distroName: "Unknown"
+ property string distroId: "unknown"
+ property string distroIcon: "linux-symbolic"
+ property string username: "user"
+
+ Timer {
+ triggeredOnStart: true
+ interval: 1
+ running: true
+ repeat: false
+ onTriggered: {
+ getUsername.running = true
+ fileOsRelease.reload()
+ const textOsRelease = fileOsRelease.text()
+
+ // Extract the friendly name (PRETTY_NAME field, fallback to NAME)
+ const prettyNameMatch = textOsRelease.match(/^PRETTY_NAME="(.+?)"/m)
+ const nameMatch = textOsRelease.match(/^NAME="(.+?)"/m)
+ distroName = prettyNameMatch ? prettyNameMatch[1] : (nameMatch ? nameMatch[1].replace(/Linux/i, "").trim() : "Unknown")
+
+ // Extract the ID (LOGO field, fallback to "unknown")
+ const logoMatch = textOsRelease.match(/^LOGO=(.+)$/m)
+ distroId = logoMatch ? logoMatch[1].replace(/"/g, "") : "unknown"
+
+ // Update the distroIcon property based on distroId
+ switch (distroId) {
+ case "arch": distroIcon = "arch-symbolic"; break;
+ case "endeavouros": distroIcon = "endeavouros-symbolic"; break;
+ case "cachyos": distroIcon = "cachyos-symbolic"; break;
+ case "nixos": distroIcon = "nixos-symbolic"; break;
+ case "fedora": distroIcon = "fedora-symbolic"; break;
+ case "linuxmint":
+ case "ubuntu":
+ case "zorin":
+ case "popos": distroIcon = "ubuntu-symbolic"; break;
+ case "debian":
+ case "raspbian":
+ case "kali": distroIcon = "debian-symbolic"; break;
+ default: distroIcon = "linux-symbolic"; break;
+ }
+ }
+ }
+
+ Process {
+ id: getUsername
+ command: ["whoami"]
+ stdout: SplitParser {
+ onRead: data => {
+ username = data.trim()
+ }
+ }
+ }
+
+ FileView {
+ id: fileOsRelease
+ path: "/etc/os-release"
+ }
+}
\ No newline at end of file
diff --git a/.config/quickshell/services/Todo.qml b/.config/quickshell/services/Todo.qml
new file mode 100644
index 000000000..2cbf0d7dc
--- /dev/null
+++ b/.config/quickshell/services/Todo.qml
@@ -0,0 +1,88 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common"
+import Quickshell;
+import Quickshell.Io;
+import Qt.labs.platform
+import QtQuick;
+
+/**
+ * Simple to-do list manager.
+ * Each item is an object with "content" and "done" properties.
+ */
+Singleton {
+ id: root
+ property var filePath: Directories.todoPath
+ property var list: []
+
+ function addItem(item) {
+ list.push(item)
+ // Reassign to trigger onListChanged
+ root.list = list.slice(0)
+ todoFileView.setText(JSON.stringify(root.list))
+ }
+
+ function addTask(desc) {
+ const item = {
+ "content": desc,
+ "done": false,
+ }
+ addItem(item)
+ }
+
+ function markDone(index) {
+ if (index >= 0 && index < list.length) {
+ list[index].done = true
+ // Reassign to trigger onListChanged
+ root.list = list.slice(0)
+ todoFileView.setText(JSON.stringify(root.list))
+ }
+ }
+
+ function markUnfinished(index) {
+ if (index >= 0 && index < list.length) {
+ list[index].done = false
+ // Reassign to trigger onListChanged
+ root.list = list.slice(0)
+ todoFileView.setText(JSON.stringify(root.list))
+ }
+ }
+
+ function deleteItem(index) {
+ if (index >= 0 && index < list.length) {
+ list.splice(index, 1)
+ // Reassign to trigger onListChanged
+ root.list = list.slice(0)
+ todoFileView.setText(JSON.stringify(root.list))
+ }
+ }
+
+ function refresh() {
+ todoFileView.reload()
+ }
+
+ Component.onCompleted: {
+ refresh()
+ }
+
+ FileView {
+ id: todoFileView
+ path: Qt.resolvedUrl(root.filePath)
+ onLoaded: {
+ const fileContents = todoFileView.text()
+ root.list = JSON.parse(fileContents)
+ console.log("[To Do] File loaded")
+ }
+ onLoadFailed: (error) => {
+ if(error == FileViewError.FileNotFound) {
+ console.log("[To Do] File not found, creating new file.")
+ root.list = []
+ todoFileView.setText(JSON.stringify(root.list))
+ } else {
+ console.log("[To Do] Error loading file: " + error)
+ }
+ }
+ }
+}
+
diff --git a/.config/quickshell/services/Ydotool.qml b/.config/quickshell/services/Ydotool.qml
new file mode 100644
index 000000000..7cafcbbe2
--- /dev/null
+++ b/.config/quickshell/services/Ydotool.qml
@@ -0,0 +1,42 @@
+pragma Singleton
+
+import "root:/modules/common"
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+
+Singleton {
+ id: root
+ property int shiftMode: 0 // 0: off, 1: on, 2: lock
+ property list shiftKeys: [42, 54] // Keycodes for Shift keys (left and right)
+ property list altKeys: [56, 100] // Keycodes for Alt keys (left and right)
+ property list ctrlKeys: [29, 97] // Keycodes for Ctrl keys (left and right)
+
+ onShiftModeChanged: {
+ if (shiftMode === 0) {
+
+ }
+ }
+
+ function releaseAllKeys() {
+ const keycodes = Array.from(Array(249).keys());
+ const releaseCommand = `ydotool key --key-delay 0 ${keycodes.map(keycode => `${keycode}:0`).join(" ")}`
+ Hyprland.dispatch(`exec ${releaseCommand}`)
+ root.shiftMode = 0; // Reset shift mode
+ }
+
+ function releaseShiftKeys() {
+ const releaseCommand = `ydotool key --key-delay 0 ${root.shiftKeys.map(keycode => `${keycode}:0`).join(" ")}`
+ Hyprland.dispatch(`exec ${releaseCommand}`)
+ root.shiftMode = 0; // Reset shift mode
+ }
+
+ function press(keycode) {
+ Hyprland.dispatch(`exec ydotool key --key-delay 0 ${keycode}:1`);
+ }
+
+ function release(keycode) {
+ Hyprland.dispatch(`exec ydotool key --key-delay 0 ${keycode}:0`);
+ }
+}
+
diff --git a/.config/quickshell/shell.qml b/.config/quickshell/shell.qml
new file mode 100644
index 000000000..4edfde51f
--- /dev/null
+++ b/.config/quickshell/shell.qml
@@ -0,0 +1,73 @@
+//@ pragma UseQApplication
+//@ pragma Env QS_NO_RELOAD_POPUP=1
+//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic
+
+// Adjust this to make the shell smaller or larger
+//@ pragma Env QT_SCALE_FACTOR=1
+
+import "./modules/common/"
+import "./modules/backgroundWidgets/"
+import "./modules/bar/"
+import "./modules/cheatsheet/"
+import "./modules/dock/"
+import "./modules/mediaControls/"
+import "./modules/notificationPopup/"
+import "./modules/onScreenDisplay/"
+import "./modules/onScreenKeyboard/"
+import "./modules/overview/"
+import "./modules/screenCorners/"
+import "./modules/session/"
+import "./modules/sidebarLeft/"
+import "./modules/sidebarRight/"
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Window
+import Quickshell
+import "./services/"
+
+ShellRoot {
+ // Enable/disable modules here. False = not loaded at all, so rest assured
+ // no unnecessary stuff will take up memory if you decide to only use, say, the overview.
+ property bool enableBar: true
+ property bool enableBackgroundWidgets: true
+ property bool enableCheatsheet: true
+ property bool enableDock: false
+ property bool enableMediaControls: true
+ property bool enableNotificationPopup: true
+ property bool enableOnScreenDisplayBrightness: true
+ property bool enableOnScreenDisplayVolume: true
+ property bool enableOnScreenKeyboard: true
+ property bool enableOverview: true
+ property bool enableReloadPopup: true
+ property bool enableScreenCorners: true
+ property bool enableSession: true
+ property bool enableSidebarLeft: true
+ property bool enableSidebarRight: true
+
+ // Force initialization of some singletons
+ Component.onCompleted: {
+ MaterialThemeLoader.reapplyTheme()
+ ConfigLoader.loadConfig()
+ PersistentStateManager.loadStates()
+ Cliphist.refresh()
+ FirstRunExperience.load()
+ }
+
+ LazyLoader { active: enableBar; component: Bar {} }
+ LazyLoader { active: enableBackgroundWidgets; component: BackgroundWidgets {} }
+ LazyLoader { active: enableCheatsheet; component: Cheatsheet {} }
+ LazyLoader { active: enableDock; component: Dock {} }
+ LazyLoader { active: enableMediaControls; component: MediaControls {} }
+ LazyLoader { active: enableNotificationPopup; component: NotificationPopup {} }
+ LazyLoader { active: enableOnScreenDisplayBrightness; component: OnScreenDisplayBrightness {} }
+ LazyLoader { active: enableOnScreenDisplayVolume; component: OnScreenDisplayVolume {} }
+ LazyLoader { active: enableOnScreenKeyboard; component: OnScreenKeyboard {} }
+ LazyLoader { active: enableOverview; component: Overview {} }
+ LazyLoader { active: enableReloadPopup; component: ReloadPopup {} }
+ LazyLoader { active: enableScreenCorners; component: ScreenCorners {} }
+ LazyLoader { active: enableSession; component: Session {} }
+ LazyLoader { active: enableSidebarLeft; component: SidebarLeft {} }
+ LazyLoader { active: enableSidebarRight; component: SidebarRight {} }
+}
+
diff --git a/.config/starship.toml b/.config/starship.toml
index 751f2fd2c..5ed04489e 100644
--- a/.config/starship.toml
+++ b/.config/starship.toml
@@ -10,25 +10,25 @@ add_newline = false
# $character
# """
format = """
-$cmd_duration$directory $git_branch
+$cmd_duration $directory $git_branch
$character
"""
# Replace the "❯" symbol in the prompt with "➜"
[character] # The name of the module we are configuring is "character"
-success_symbol = "[• ](bold fg:green) "
-error_symbol = "[• ](bold fg:red) "
+success_symbol = "[ ](bold fg:blue)"
+error_symbol = "[ ](bold fg:red)"
# Disable the package module, hiding it from the prompt completely
[package]
disabled = true
[git_branch]
-style = "bg: green"
+style = "bg: cyan"
symbol = ""
-truncation_length = 4
+truncation_length = 12
truncation_symbol = ""
-format = "• [](bold fg:green)[$symbol $branch(:$remote_branch)](fg:black bg:green)[ ](bold fg:green)"
+format = " [](bold fg:cyan)[$symbol $branch(:$remote_branch)](fg:black bg:cyan)[ ](bold fg:cyan)"
[git_commit]
commit_hash_length = 4
@@ -52,7 +52,7 @@ deleted = " 🗑 "
[hostname]
ssh_only = false
-format = "[•$hostname](bg:cyan bold fg:black)[](bold fg:cyan )"
+format = "[•$hostname](bg:cyan bold fg:black)[](bold fg:cyan)"
trim_at = ".companyname.com"
disabled = false
@@ -82,8 +82,8 @@ home_symbol = " "
read_only = " "
style = "bg:green fg:black"
truncation_length = 6
-truncation_symbol = "••/"
-format = '[](bold fg:green)[$path ]($style)[](bold fg:green)'
+truncation_symbol = " ••/"
+format = '[](bold fg:green)[ $path]($style)[](bold fg:green)'
[directory.substitutions]
@@ -93,7 +93,8 @@ format = '[](bold fg:green)[$path ]($style)[](bold fg:green)'
"Music" = " "
"Pictures" = " "
"Videos" = " "
+"GitHub" = " "
[cmd_duration]
min_time = 0
-format = '[](bold fg:yellow)[ $duration](bold bg:yellow fg:black)[](bold fg:yellow) •• '
+format = '[](bold fg:yellow)[ $duration](bold bg:yellow fg:black)[](bold fg:yellow)'
diff --git a/.local/bin/rubyshot b/.local/bin/rubyshot
deleted file mode 100755
index 8431bd693..000000000
--- a/.local/bin/rubyshot
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/bash
-
-WORKSPACES="$(hyprctl monitors -j | jq -r 'map(.activeWorkspace.id)')"
-WINDOWS="$(hyprctl clients -j | jq -r --argjson workspaces "$WORKSPACES" 'map(select([.workspace.id] | inside($workspaces)))' )"
-GEOM=$(echo "$WINDOWS" | jq -r '.[] | "\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"' | slurp -f '%x %y %w %h')
-wayshot -s "$GEOM" --stdout ${#:+"$@"}
\ No newline at end of file
diff --git a/README.md b/README.md
index 0c3a78267..33ceb2cd4 100644
--- a/README.md
+++ b/README.md
@@ -35,17 +35,18 @@
```bash
bash <(curl -s "https://end-4.github.io/dots-hyprland-wiki/setup.sh")
```
- - **Manual** installation, other distros and more:
- - See the [Wiki](https://end-4.github.io/dots-hyprland-wiki/en/i-i/01setup/)
- - (_Available in: English, Vietnamese, and Simplified Chinese. Translations are welcome._)
-
- - **Default keybinds**: Parts similar to Windows and GNOME. Hit Super+/ for a list.
-
- Here's an image, just in case...
-
- 
-
+ If you are using fish shell (non-posix-compliant shell) then:
+ ```bash
+ bash -c "$(curl -s https://end-4.github.io/dots-hyprland-wiki/setup.sh)"
+ ```
+
+ - **Manual** installation, other distros and more:
+ - See the [Wiki](https://end-4.github.io/dots-hyprland-wiki/en/ii-qs/01setup/)
+
+ - **Default keybinds**: Should be somewhat familiar if you've used Windows or GNOME.
+ - For a list, hit `Super`+`/`
+ - For a terminal, hit `Super`+`Enter`
@@ -56,42 +57,37 @@
| Software | Purpose |
| ------------- | ------------- |
| [Hyprland](https://github.com/hyprwm/hyprland) | The compositor (for noobs, you can just call it a window manager) |
- | [AGS](https://github.com/Aylur/ags) | A GTK widget system, responsible for the status bar, sidebars, etc. |
- | [Fuzzel](https://mark.stosberg.com/fuzzel-a-great-dmenu-and-rofi-altenrative-for-wayland/) | For clipboard and emoji picker |
+ | [Quickshell](https://quickshell.outfoxxed.me/) | A QtQuick-based widget system, responsible for the status bar, sidebars, etc. |
+
- For a more comprehensive list of dependencies, see [scriptdata/dependencies.conf](https://github.com/end-4/dots-hyprland/blob/main/scriptdata/dependencies.conf)
-
- Help improve these dotfiles
-
- - Try the Quickshell-powered version at [`ii-qs` branch](https://github.com/end-4/dots-hyprland/tree/ii-qs) - It comes with major improvements, and you're free to make suggestions 👉 [#1276](https://github.com/end-4/dots-hyprland/pull/1276)
-
-
-
• screenshots •
-## Main branch (*illogical-impulse*)
+## illogical-impulseQuickshell
-### AI
-
-_Sidebar offers online and offline chat. Text selection summary is offline only for privacy. _
+| AI | Common widgets |
+|:---|:---------------|
+|  |  |
+| Window management | Weeb power |
+|  |  |
-### Notifications, music controls, system, calendar
-
-_On the sidebar: flicking the notification _
+By the way...
+- The funny notification positions are mimicking Android 16's dragging behavior
+- The clock on the wallpaper is automatically placed at the "least busy" region of the image
-### Intuitive window management
-
-_You can also drag and drop windows across workspaces _
+## illogical-impulseAGS (Deprecated)
-### Power to weebs
-
-_Get yande.re and konachan images from sidebar _
+| AI | Common widgets |
+|:---|:---------------|
+|  |  |
+| Window management | Weeb power |
+|  |  |
## Unsupported stuff
@@ -124,8 +120,10 @@ _Get yande.re and konachan images from sidebar _
- - [@clsty](https://github.com/clsty) for making an actually good install script + many other stuff that I neglect
+ - [@clsty](https://github.com/clsty) for making my work accessible by taking care of the install script and many other things
- [@midn8hustlr](https://github.com/midn8hustlr) for greatly improving the color generation system
+ - [@outfoxxed](https://github.com/outfoxxed/) for being extremely supportive in my Quickshell journey
+ - Quickshell: [Soramane](https://github.com/caelestia-dots/shell/), [FridayFaerie](https://github.com/FridayFaerie/quickshell), [nydragon](https://github.com/nydragon/nysh)
- AGS: [Aylur's config](https://github.com/Aylur/dotfiles/tree/ags-pre-ts), [kotontrion's config](https://github.com/kotontrion/dotfiles)
- EWW: [fufexan's config](https://github.com/fufexan/dotfiles) (he thanks more people there btw)
- AI bots for providing useful examples
diff --git a/arch-packages/illogical-impulse-agsv1-git/.gitignore b/arch-packages/illogical-impulse-agsv1-git/.gitignore
deleted file mode 100644
index 1faa43724..000000000
--- a/arch-packages/illogical-impulse-agsv1-git/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/ii-agsv1/
-/libgnome-volume-control/
diff --git a/arch-packages/illogical-impulse-agsv1-git/PKGBUILD b/arch-packages/illogical-impulse-agsv1-git/PKGBUILD
deleted file mode 100644
index 54a28984c..000000000
--- a/arch-packages/illogical-impulse-agsv1-git/PKGBUILD
+++ /dev/null
@@ -1,45 +0,0 @@
-# Modified from AUR package "aylurs-gtk-shell-git" maintained by kotontrion
-pkgname=illogical-impulse-agsv1-git
-_pkgname=ii-agsv1
-pkgver=r4.3e8d365
-pkgrel=4
-pkgdesc="Aylurs's Gtk Shell (AGS), patched for illogical-impulse dotfiles."
-arch=('x86_64')
-url="https://github.com/end-4/ii-agsv1"
-license=('GPL-3.0-only')
-makedepends=('git' 'gobject-introspection' 'meson' 'npm' 'typescript')
-depends=('gvfs' 'gjs' 'glib2' 'glib2-devel' 'glibc' 'gtk3' 'gtk-layer-shell' 'libpulse' 'pam' 'gnome-bluetooth-3.0' 'gammastep')
-optdepends=('greetd: required for greetd service'
- 'libdbusmenu-gtk3: required for systemtray service'
- 'libsoup3: required for the Utils.fetch feature'
- 'libnotify: required for sending notifications'
- 'networkmanager: required for network service'
- 'power-profiles-daemon: required for powerprofiles service'
- 'upower: required for battery service')
-conflicts=('illogical-impulse-agsv1')
-backup=('etc/pam.d/ags')
-source=("git+${url}.git")
-sha256sums=('SKIP')
-
-pkgver(){
- cd $srcdir/$_pkgname
- printf 'r%s.%s' "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
-}
-
-prepare() {
- cd $srcdir/$_pkgname
-}
-
-build() {
- cd $srcdir/$_pkgname
- npm install
- arch-meson build --libdir "lib/$_pkgname" -Dbuild_types=true
- meson compile -C build
-}
-
-package() {
- cd $srcdir/$_pkgname
- meson install -C build --destdir "$pkgdir"
- rm ${pkgdir}/usr/bin/ags
- ln -sf /usr/share/com.github.Aylur.ags/com.github.Aylur.ags ${pkgdir}/usr/bin/agsv1
-}
diff --git a/arch-packages/illogical-impulse-audio/PKGBUILD b/arch-packages/illogical-impulse-audio/PKGBUILD
index d2d1e7889..6cab7de53 100644
--- a/arch-packages/illogical-impulse-audio/PKGBUILD
+++ b/arch-packages/illogical-impulse-audio/PKGBUILD
@@ -5,10 +5,10 @@ pkgdesc='Illogical Impulse Audio Dependencies'
arch=(any)
license=(None)
depends=(
- pavucontrol
+ cava
+ pavucontrol-qt
wireplumber
libdbusmenu-gtk3
playerctl
- swww
)
diff --git a/arch-packages/illogical-impulse-backlight/PKGBUILD b/arch-packages/illogical-impulse-backlight/PKGBUILD
index fe216262c..2525be9d4 100644
--- a/arch-packages/illogical-impulse-backlight/PKGBUILD
+++ b/arch-packages/illogical-impulse-backlight/PKGBUILD
@@ -5,6 +5,8 @@ pkgdesc='Illogical Impulse Backlight Dependencies'
arch=(any)
license=(None)
depends=(
+ gammastep
+ geoclue
brightnessctl
ddcutil
)
diff --git a/arch-packages/illogical-impulse-basic/PKGBUILD b/arch-packages/illogical-impulse-basic/PKGBUILD
index ac74a3c36..c338727f9 100644
--- a/arch-packages/illogical-impulse-basic/PKGBUILD
+++ b/arch-packages/illogical-impulse-basic/PKGBUILD
@@ -11,14 +11,10 @@ depends=(
cliphist
cmake
curl
- fuzzel
rsync
wget
ripgrep
jq
- npm
meson
- typescript
- gjs
xdg-user-dirs
)
diff --git a/arch-packages/illogical-impulse-fonts-themes/PKGBUILD b/arch-packages/illogical-impulse-fonts-themes/PKGBUILD
index 038d53c4a..ed1a95070 100644
--- a/arch-packages/illogical-impulse-fonts-themes/PKGBUILD
+++ b/arch-packages/illogical-impulse-fonts-themes/PKGBUILD
@@ -6,19 +6,17 @@ arch=(any)
license=(None)
depends=(
adw-gtk-theme-git
- qt5ct
- qt6ct
- qt5-wayland
+ breeze-plus
+ eza
+ fish
fontconfig
+ kde-material-you-colors
+ kitty
+ matugen-bin
+ starship
ttf-readex-pro
ttf-jetbrains-mono-nerd
ttf-material-symbols-variable-git
- ttf-space-mono-nerd
ttf-rubik-vf
ttf-gabarito-git
- fish
- foot
- starship
- kvantum
- kvantum-qt5
)
diff --git a/arch-packages/illogical-impulse-gnome/PKGBUILD b/arch-packages/illogical-impulse-gnome/PKGBUILD
deleted file mode 100644
index 33af4b8ba..000000000
--- a/arch-packages/illogical-impulse-gnome/PKGBUILD
+++ /dev/null
@@ -1,12 +0,0 @@
-pkgname=illogical-impulse-gnome
-pkgver=1.0
-pkgrel=2
-pkgdesc='Illogical Impulse GNOME Dependencies'
-arch=(any)
-license=(None)
-depends=(
- polkit-gnome
- gnome-keyring
- gnome-control-center
- blueberry networkmanager
-)
diff --git a/arch-packages/illogical-impulse-gtk/PKGBUILD b/arch-packages/illogical-impulse-gtk/PKGBUILD
deleted file mode 100644
index 50a57d1c8..000000000
--- a/arch-packages/illogical-impulse-gtk/PKGBUILD
+++ /dev/null
@@ -1,18 +0,0 @@
-pkgname=illogical-impulse-gtk
-pkgver=1.0
-pkgrel=1
-pkgdesc='Illogical Impulse GTK Dependencies'
-arch=(any)
-license=(None)
-depends=(
- webp-pixbuf-loader
- gtk-layer-shell
- gtk3
- gtksourceview3
- gobject-introspection
- upower
- yad
- ydotool
- xdg-user-dirs-gtk
-)
-
diff --git a/arch-packages/illogical-impulse-hyprland/PKGBUILD b/arch-packages/illogical-impulse-hyprland/PKGBUILD
index c743af7ac..93c7b5f8e 100644
--- a/arch-packages/illogical-impulse-hyprland/PKGBUILD
+++ b/arch-packages/illogical-impulse-hyprland/PKGBUILD
@@ -12,8 +12,9 @@ depends=(
hyprland-qt-support
hyprland-qtutils
hyprlock
- xdg-desktop-portal-hyprland
hyprcursor
hyprwayland-scanner
hyprland
+ xdg-desktop-portal-hyprland
+ wl-clipboard
)
diff --git a/arch-packages/illogical-impulse-kde/PKGBUILD b/arch-packages/illogical-impulse-kde/PKGBUILD
new file mode 100644
index 000000000..b8b91c3f9
--- /dev/null
+++ b/arch-packages/illogical-impulse-kde/PKGBUILD
@@ -0,0 +1,14 @@
+pkgname=illogical-impulse-kde
+pkgver=1.0
+pkgrel=2
+pkgdesc='Illogical Impulse KDE Dependencies'
+arch=(any)
+license=(None)
+depends=(
+ bluedevil
+ gnome-keyring
+ networkmanager
+ plasma-nm
+ polkit-kde-agent
+ systemsettings
+)
diff --git a/arch-packages/illogical-impulse-python/PKGBUILD b/arch-packages/illogical-impulse-python/PKGBUILD
index fc1ac1e05..d9c6caa55 100644
--- a/arch-packages/illogical-impulse-python/PKGBUILD
+++ b/arch-packages/illogical-impulse-python/PKGBUILD
@@ -13,4 +13,5 @@ depends=(
libportal-gtk4
gobject-introspection
sassc
+ python-opencv
)
diff --git a/arch-packages/illogical-impulse-screencapture/PKGBUILD b/arch-packages/illogical-impulse-screencapture/PKGBUILD
index ab5911fb9..8feb1c0d0 100644
--- a/arch-packages/illogical-impulse-screencapture/PKGBUILD
+++ b/arch-packages/illogical-impulse-screencapture/PKGBUILD
@@ -7,7 +7,7 @@ license=(None)
depends=(
swappy
wf-recorder
- grim
+ hyprshot
tesseract
tesseract-data-eng
slurp
diff --git a/arch-packages/illogical-impulse-toolkit/PKGBUILD b/arch-packages/illogical-impulse-toolkit/PKGBUILD
new file mode 100644
index 000000000..6f12584a2
--- /dev/null
+++ b/arch-packages/illogical-impulse-toolkit/PKGBUILD
@@ -0,0 +1,26 @@
+pkgname=illogical-impulse-toolkit
+pkgver=1.0
+pkgrel=1
+pkgdesc='Illogical Impulse GTK/Qt Dependencies'
+arch=(any)
+license=(None)
+depends=(
+ kdialog
+ qt6-5compat
+ qt6-base
+ qt6-declarative
+ qt6-imageformats
+ qt6-multimedia
+ qt6-positioning
+ qt6-quicktimeline
+ qt6-sensors
+ qt6-svg
+ qt6-tools
+ qt6-translations
+ qt6-virtualkeyboard
+ qt6-wayland
+ syntax-highlighting
+ upower
+ wtype
+ ydotool
+)
diff --git a/arch-packages/illogical-impulse-widgets/PKGBUILD b/arch-packages/illogical-impulse-widgets/PKGBUILD
index cdff28667..868bea9e6 100644
--- a/arch-packages/illogical-impulse-widgets/PKGBUILD
+++ b/arch-packages/illogical-impulse-widgets/PKGBUILD
@@ -5,12 +5,15 @@ pkgdesc='Illogical Impulse Widget Dependencies'
arch=(any)
license=(None)
depends=(
- dart-sass
+ fuzzel
+ glib2 # for `gsettings` it seems?
hypridle
- hyprutils
+ hyprutils
hyprlock
- wlogout
- wl-clipboard
hyprpicker
nm-connection-editor
+ quickshell
+ swww
+ translate-shell
+ wlogout
)
diff --git a/diagnose b/diagnose
index 50c2cc000..df1d0dee4 100755
--- a/diagnose
+++ b/diagnose
@@ -38,16 +38,6 @@ ii_check_venv() {
which python
deactivate
}
-ii_check_ags() {
- pkill ags
- pkill agsv1
- agsv1 > ii_ags.log 2>&1 &
- GUI_PID=$!
- sleep 10
- kill $GUI_PID
- echo "AGS log saved to \"ii_ags.log\"."
-}
-#x ii_check_ags
e "Checking git repo info"
x git remote get-url origin
@@ -57,15 +47,14 @@ e "Checking distro"
x ii_check_distro
e "Checking variables"
-x declare -p XDG_BIN_HOME # ~/.local/bin
x declare -p XDG_CACHE_HOME # ~/.cache
x declare -p XDG_CONFIG_HOME # ~/.config
x declare -p XDG_DATA_HOME # ~/.local/share
x declare -p XDG_STATE_HOME # ~/.local/state
-x declare -p ILLOGICAL_IMPULSE_VIRTUAL_ENV # $XDG_STATE_HOME/ags/.venv
+x declare -p ILLOGICAL_IMPULSE_VIRTUAL_ENV # $XDG_STATE_HOME/quickshell/.venv
e "Checking directories/files"
-x ls -l ~/.local/state/ags/.venv
+x ls -l ~/.local/state/quickshell/.venv
#x cat ~/.config/ags/
#e "Checking command existence"
@@ -77,8 +66,6 @@ commands+=(ags agsv1)
e "Checking versions"
x Hyprland --version
-x ags --version
-x agsv1 --version
e "Finished. Output saved as \"$output_file\"."
if ! command -v curl 2>&1 >>/dev/null ;then echo "\"curl\" not found, pastebin upload unavailable.";exit;fi
diff --git a/install.sh b/install.sh
index 9836bb846..e22f912b4 100755
--- a/install.sh
+++ b/install.sh
@@ -15,6 +15,11 @@ prevent_sudo_or_root
startask () {
printf "\e[34m[$0]: Hi there! Before we start:\n"
+ printf '\n'
+ printf '[NEW] illogical-impulse is now powered by Quickshell. If you were using the old AGS version and would like to keep it, do not run this script.\n'
+ printf ' The AGS version, although uses less memory, has much worse performance. If you do not need (inconsistent) translations, the Quickshell version is recommended.\n'
+ printf ' If you would like it anyway, run the script in its branch instead: git checkout ii-ags && ./install.sh\n'
+ printf '\n'
printf 'This script 1. only works for ArchLinux and Arch-based distros.\n'
printf ' 2. does not handle system-level/hardware stuff like Nvidia drivers\n'
printf "\e[31m"
@@ -100,11 +105,10 @@ install-local-pkgbuild() {
}
# Install core dependencies from the meta-packages
-metapkgs=(./arch-packages/illogical-impulse-{audio,python,backlight,basic,fonts-themes,gnome,gtk,portal,screencapture,widgets})
-metapkgs+=(./arch-packages/illogical-impulse-agsv1-git)
+metapkgs=(./arch-packages/illogical-impulse-{audio,backlight,basic,fonts-themes,kde,portal,python,screencapture,toolkit,widgets})
metapkgs+=(./arch-packages/illogical-impulse-hyprland)
metapkgs+=(./arch-packages/illogical-impulse-microtex-git)
-metapkgs+=(./arch-packages/illogical-impulse-oneui4-icons-git)
+# metapkgs+=(./arch-packages/illogical-impulse-oneui4-icons-git)
[[ -f /usr/share/icons/Bibata-Modern-Classic/index.theme ]] || \
metapkgs+=(./arch-packages/illogical-impulse-bibata-modern-classic-bin)
@@ -119,31 +123,33 @@ showfun install-python-packages
v install-python-packages
## Optional dependencies
-# if pacman -Qs ^plasma-browser-integration$ ;then SKIP_PLASMAINTG=true;fi
-# case $SKIP_PLASMAINTG in
-# true) sleep 0;;
-# *)
-# if $ask;then
-# echo -e "\e[33m[$0]: NOTE: The size of \"plasma-browser-integration\" is about 250 MiB.\e[0m"
-# echo -e "\e[33mIt is needed if you want playtime of media in Firefox to be shown on the music controls widget.\e[0m"
-# echo -e "\e[33mInstall it? [y/N]\e[0m"
-# read -p "====> " p
-# else
-# p=y
-# fi
-# case $p in
-# y) x sudo pacman -S --needed --noconfirm plasma-browser-integration ;;
-# *) echo "Ok, won't install"
-# esac
-# ;;
-# esac
+if pacman -Qs ^plasma-browser-integration$ ;then SKIP_PLASMAINTG=true;fi
+case $SKIP_PLASMAINTG in
+ true) sleep 0;;
+ *)
+ if $ask;then
+ echo -e "\e[33m[$0]: NOTE: The size of \"plasma-browser-integration\" is about 600 MiB.\e[0m"
+ echo -e "\e[33mIt is needed if you want playtime of media in Firefox to be shown on the music controls widget.\e[0m"
+ echo -e "\e[33mInstall it? [y/N]\e[0m"
+ read -p "====> " p
+ else
+ p=y
+ fi
+ case $p in
+ y) x sudo pacman -S --needed --noconfirm plasma-browser-integration ;;
+ *) echo "Ok, won't install"
+ esac
+ ;;
+esac
v sudo usermod -aG video,i2c,input "$(whoami)"
v bash -c "echo i2c-dev | sudo tee /etc/modules-load.d/i2c-dev.conf"
+v sudo pacman -S archlinux-xdg-menu && XDG_MENU_PREFIX=arch- kbuildsycoca6; sudo ln -s /etc/xdg/menus/plasma-applications.menu /etc/xdg/menus/applications.menu
v systemctl --user enable ydotool --now
v sudo systemctl enable bluetooth --now
v gsettings set org.gnome.desktop.interface font-name 'Rubik 11'
v gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'
+v kwriteconfig6 --file kdeglobals --group KDE --key widgetStyle Darkly
#####################################################################################
@@ -156,11 +162,11 @@ v mkdir -p $XDG_BIN_HOME $XDG_CACHE_HOME $XDG_CONFIG_HOME $XDG_DATA_HOME
# original dotfiles and new ones in the SAME DIRECTORY
# (eg. in ~/.config/hypr) won't be mixed together
-# MISC (For .config/* but not AGS, not Fish, not Hyprland)
+# MISC (For .config/* but not fish, not Hyprland)
case $SKIP_MISCCONF in
true) sleep 0;;
*)
- for i in $(find .config/ -mindepth 1 -maxdepth 1 ! -name 'ags' ! -name 'fish' ! -name 'hypr' -exec basename {} \;); do
+ for i in $(find .config/ -mindepth 1 -maxdepth 1 ! -name 'fish' ! -name 'hypr' -exec basename {} \;); do
# i=".config/$i"
echo "[$0]: Found target: .config/$i"
if [ -d ".config/$i" ];then v rsync -av --delete ".config/$i/" "$XDG_CONFIG_HOME/$i/"
@@ -177,24 +183,6 @@ case $SKIP_FISH in
;;
esac
-# For AGS
-case $SKIP_AGS in
- true) sleep 0;;
- *)
- v rsync -av --delete --exclude '/user_options.jsonc' .config/ags/ "$XDG_CONFIG_HOME"/ags/
- t="$XDG_CONFIG_HOME/ags/user_options.jsonc"
- if [ -f $t ];then
- echo -e "\e[34m[$0]: \"$t\" already exists.\e[0m"
- # v cp -f .config/ags/user_options.jsonc $t.new
- existed_ags_opt=y
- else
- echo -e "\e[33m[$0]: \"$t\" does not exist yet.\e[0m"
- v cp .config/ags/user_options.jsonc $t
- existed_ags_opt=n
- fi
- ;;
-esac
-
# For Hyprland
case $SKIP_HYPRLAND in
true) sleep 0;;
@@ -203,15 +191,9 @@ case $SKIP_HYPRLAND in
t="$XDG_CONFIG_HOME/hypr/hyprland.conf"
if [ -f $t ];then
echo -e "\e[34m[$0]: \"$t\" already exists.\e[0m"
- if [ -f "$XDG_STATE_HOME/ags/user/firstrun.txt" ]
- then
- v cp -f .config/hypr/hyprland.conf $t.new
- existed_hypr_conf=y
- else
- v mv $t $t.old
- v cp -f .config/hypr/hyprland.conf $t
- existed_hypr_conf_firstrun=y
- fi
+ v mv $t $t.old
+ v cp -f .config/hypr/hyprland.conf $t
+ existed_hypr_conf_firstrun=y
else
echo -e "\e[33m[$0]: \"$t\" does not exist yet.\e[0m"
v cp .config/hypr/hyprland.conf $t
@@ -250,7 +232,7 @@ esac
# some foldes (eg. .local/bin) should be processed separately to avoid `--delete' for rsync,
# since the files here come from different places, not only about one program.
-v rsync -av ".local/bin/" "$XDG_BIN_HOME"
+# v rsync -av ".local/bin/" "$XDG_BIN_HOME" # No longer needed since scripts are no longer in ~/.local/bin
# Prevent hyprland from not fully loaded
sleep 1
@@ -261,10 +243,7 @@ grep -q 'source ${XDG_CONFIG_HOME:-~/.config}/zshrc.d/dots-hyprland.zsh' ~/.zshr
warn_files=()
warn_files_tests=()
-warn_files_tests+=(/usr/local/bin/ags)
-warn_files_tests+=(/usr/local/etc/pam.d/ags)
warn_files_tests+=(/usr/local/lib/{GUtils-1.0.typelib,Gvc-1.0.typelib,libgutils.so,libgvc.so})
-warn_files_tests+=(/usr/local/share/com.github.Aylur.ags)
warn_files_tests+=(/usr/local/share/fonts/TTF/Rubik{,-Italic}'[wght]'.ttf)
warn_files_tests+=(/usr/local/share/licenses/ttf-rubik)
warn_files_tests+=(/usr/local/share/fonts/TTF/Gabarito-{Black,Bold,ExtraBold,Medium,Regular,SemiBold}.ttf)
@@ -290,10 +269,6 @@ printf "\e[36mPress \e[30m\e[46m Ctrl+Super+T \e[0m\e[36m to select a wallpaper\
printf "\e[36mPress \e[30m\e[46m Super+/ \e[0m\e[36m for a list of keybinds\e[0m\n"
printf "\n"
-case $existed_ags_opt in
- y) printf "\n\e[33m[$0]: Warning: \"$XDG_CONFIG_HOME/ags/user_options.jsonc\" already existed before and we didn't overwrite it. \e[0m\n"
-# printf "\e[33mPlease use \"$XDG_CONFIG_HOME/ags/user_options.jsonc.new\" as a reference for a proper format.\e[0m\n"
-;;esac
case $existed_hypr_conf_firstrun in
y) printf "\n\e[33m[$0]: Warning: \"$XDG_CONFIG_HOME/hypr/hyprland.conf\" already existed before. As it seems it is your first run, we replaced it with a new one. \e[0m\n"
printf "\e[33mAs it seems it is your first run, we replaced it with a new one. The old one has been renamed to \"$XDG_CONFIG_HOME/hypr/hyprland.conf.old\".\e[0m\n"
@@ -312,7 +287,7 @@ case $existed_hyprlock_conf in
;;esac
if [[ -z "${ILLOGICAL_IMPULSE_VIRTUAL_ENV}" ]]; then
- printf "\n\e[31m[$0]: \!! Important \!! : Please ensure environment variable \e[0m \$ILLOGICAL_IMPULSE_VIRTUAL_ENV \e[31m is set to proper value (by default \"~/.local/state/ags/.venv\"), or AGS config will not work. We have already provided this configuration in ~/.config/hypr/hyprland/env.conf, but you need to ensure it is included in hyprland.conf, and also a restart is needed for applying it.\e[0m\n"
+ printf "\n\e[31m[$0]: \!! Important \!! : Please ensure environment variable \e[0m \$ILLOGICAL_IMPULSE_VIRTUAL_ENV \e[31m is set to proper value (by default \"~/.local/state/quickshell/.venv\"), or Quickshell config will not work. We have already provided this configuration in ~/.config/hypr/hyprland/env.conf, but you need to ensure it is included in hyprland.conf, and also a restart is needed for applying it.\e[0m\n"
fi
if [[ ! -z "${warn_files[@]}" ]]; then
diff --git a/licenses/LGPL-3.0.txt b/licenses/LGPL-3.0.txt
new file mode 100644
index 000000000..0a041280b
--- /dev/null
+++ b/licenses/LGPL-3.0.txt
@@ -0,0 +1,165 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser 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
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/licenses/MIT.txt b/licenses/MIT.txt
new file mode 100644
index 000000000..9a7392926
--- /dev/null
+++ b/licenses/MIT.txt
@@ -0,0 +1,9 @@
+MIT License
+
+Copyright
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/licenses/README.md b/licenses/README.md
new file mode 100644
index 000000000..02bf9af72
--- /dev/null
+++ b/licenses/README.md
@@ -0,0 +1,3 @@
+# Licenses
+
+This repository contains code from other repositories. Files containing such code should include a license notice, and a copy should be stored in this folder.
diff --git a/manual-install-helper.sh b/manual-install-helper.sh
index 663f3009a..d594f2c16 100755
--- a/manual-install-helper.sh
+++ b/manual-install-helper.sh
@@ -11,7 +11,6 @@ source ./scriptdata/installers
prevent_sudo_or_root
if command -v pacman >/dev/null 2>&1;then printf "\e[31m[$0]: pacman found, it seems that the system is ArchLinux or Arch-based distro. Aborting...\e[0m\n";exit 1;fi
-v install-agsv1
v install-Rubik
v install-Gabarito
v install-OneUI
diff --git a/scriptdata/installers b/scriptdata/installers
index 842ca9375..7de00454c 100644
--- a/scriptdata/installers
+++ b/scriptdata/installers
@@ -123,7 +123,7 @@ install-uv (){
# Both for Arch(based) and other distros.
install-python-packages (){
UV_NO_MODIFY_PATH=1
- ILLOGICAL_IMPULSE_VIRTUAL_ENV=$XDG_STATE_HOME/ags/.venv
+ ILLOGICAL_IMPULSE_VIRTUAL_ENV=$XDG_STATE_HOME/quickshell/.venv
x mkdir -p $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)
# we need python 3.12 https://github.com/python-pillow/Pillow/issues/8089
x uv venv --prompt .venv $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV) -p 3.12
diff --git a/scriptdata/requirements.txt b/scriptdata/requirements.txt
index 8c923e9c3..c2f380c4a 100644
--- a/scriptdata/requirements.txt
+++ b/scriptdata/requirements.txt
@@ -26,13 +26,11 @@ pycparser==2.22
# via cffi
pyproject-hooks==1.2.0
# via build
-# pywal==3.3.0
- # via -r scriptdata/requirements.in
pywayland==0.4.18
# via -r scriptdata/requirements.in
setproctitle==1.3.4
# via -r scriptdata/requirements.in
-setuptools==75.8.0
+setuptools==80.9.0
# via setuptools-scm
setuptools-scm==8.1.0
# via -r scriptdata/requirements.in
diff --git a/update.sh b/update.sh
new file mode 100755
index 000000000..098a6a0fa
--- /dev/null
+++ b/update.sh
@@ -0,0 +1,849 @@
+#!/usr/bin/env bash
+#
+# update.sh - Enhanced dotfiles update script
+#
+# Features:
+# - Pull latest commits from remote
+# - Rebuild packages if PKGBUILD files changed (user choice)
+# - Handle config file conflicts with user choices
+# - Respect .updateignore file for exclusions
+#
+set -uo pipefail
+
+# === Configuration ===
+FORCE_CHECK=false
+CHECK_PACKAGES=false
+REPO_DIR="$(cd "$(dirname $0)" &>/dev/null && pwd)"
+ARCH_PACKAGES_DIR="${REPO_DIR}/arch-packages"
+UPDATE_IGNORE_FILE="${REPO_DIR}/.updateignore"
+HOME_UPDATE_IGNORE_FILE="${HOME}/.updateignore"
+
+# Directories to monitor for changes
+MONITOR_DIRS=(".config" ".local/bin")
+
+# === Color Codes ===
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+PURPLE='\033[0;35m'
+NC='\033[0m' # No Color
+
+# === Helper Functions ===
+log_info() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+log_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+log_warning() {
+ echo -e "${YELLOW}[WARNING]${NC} $1"
+}
+
+log_error() {
+ echo -e "${RED}[ERROR]${NC} $1" >&2
+}
+
+log_header() {
+ echo -e "\n${PURPLE}=== $1 ===${NC}"
+}
+
+die() {
+ log_error "$1"
+ exit 1
+}
+
+# Function to safely read input with terminal compatibility
+safe_read() {
+ local prompt="$1"
+ local varname="$2"
+ local default="${3:-}"
+
+ # Simple approach: just use read with /dev/tty and handle errors
+ local input_value=""
+
+ # Display prompt and read from terminal
+ echo -n "$prompt"
+ if read input_value /dev/null || read input_value 2>/dev/null; then
+ eval "$varname='$input_value'"
+ return 0
+ else
+ # If read failed and we have a default, use it
+ if [[ -n "$default" ]]; then
+ echo
+ log_warning "Using default: $default"
+ eval "$varname='$default'"
+ return 0
+ else
+ echo
+ log_error "Failed to read input"
+ return 1
+ fi
+ fi
+}
+
+# Function to check if a file should be ignored
+should_ignore() {
+ local file_path="$1"
+ local relative_path="${file_path#$HOME/}"
+
+ # Also get path relative to repo for repo-level ignores
+ local repo_relative=""
+ if [[ "$file_path" == "$REPO_DIR"* ]]; then
+ repo_relative="${file_path#$REPO_DIR/}"
+ fi
+
+ # Check both repo and home ignore files
+ for ignore_file in "$UPDATE_IGNORE_FILE" "$HOME_UPDATE_IGNORE_FILE"; do
+ if [[ -f "$ignore_file" ]]; then
+ while IFS= read -r pattern || [[ -n "$pattern" ]]; do
+ # Skip empty lines and comments
+ [[ -z "$pattern" || "$pattern" =~ ^[[:space:]]*# ]] && continue
+ # Remove leading/trailing whitespace
+ pattern=$(echo "$pattern" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
+ [[ -z "$pattern" ]] && continue
+
+ # Handle different gitignore-style patterns
+ local should_skip=false
+
+ # Exact match
+ if [[ "$relative_path" == "$pattern" ]] || [[ "$repo_relative" == "$pattern" ]]; then
+ should_skip=true
+ fi
+
+ # Wildcard patterns (basic glob matching)
+ if [[ "$relative_path" == $pattern ]] || [[ "$repo_relative" == $pattern ]]; then
+ should_skip=true
+ fi
+
+ # Directory patterns (ending with /)
+ if [[ "$pattern" == */ ]]; then
+ local dir_pattern="${pattern%/}"
+ if [[ "$relative_path" == "$dir_pattern"/* ]] || [[ "$repo_relative" == "$dir_pattern"/* ]]; then
+ should_skip=true
+ fi
+ fi
+
+ # Patterns starting with / (from root)
+ if [[ "$pattern" == /* ]]; then
+ local root_pattern="${pattern#/}"
+ if [[ "$relative_path" == "$root_pattern" ]] || [[ "$relative_path" == "$root_pattern"/* ]] ||
+ [[ "$repo_relative" == "$root_pattern" ]] || [[ "$repo_relative" == "$root_pattern"/* ]]; then
+ should_skip=true
+ fi
+ fi
+
+ # Patterns with wildcards
+ if [[ "$pattern" == *"*"* ]]; then
+ if [[ "$relative_path" == $pattern ]] || [[ "$repo_relative" == $pattern ]]; then
+ should_skip=true
+ fi
+ # Also check if any parent directory matches
+ local temp_path="$relative_path"
+ while [[ "$temp_path" == */* ]]; do
+ temp_path="${temp_path%/*}"
+ if [[ "$temp_path" == $pattern ]]; then
+ should_skip=true
+ break
+ fi
+ done
+ fi
+
+ # Simple substring matching (for backward compatibility)
+ if [[ ! "$should_skip" == true ]]; then
+ if [[ "$file_path" == *"$pattern"* ]] || [[ "$relative_path" == *"$pattern"* ]]; then
+ should_skip=true
+ fi
+ fi
+
+ if [[ "$should_skip" == true ]]; then
+ return 0
+ fi
+ done <"$ignore_file"
+ fi
+ done
+ return 1
+}
+
+# Function to show file diff with syntax highlighting if possible
+show_diff() {
+ local file1="$1"
+ local file2="$2"
+
+ echo -e "\n${CYAN}Showing differences:${NC}"
+ echo -e "${CYAN}Old file: $file1${NC}"
+ echo -e "${CYAN}New file: $file2${NC}"
+ echo "----------------------------------------"
+
+ if command -v diff &>/dev/null; then
+ diff -u "$file1" "$file2" || true
+ else
+ echo "diff command not available"
+ fi
+ echo "----------------------------------------"
+}
+
+# Function to handle file conflicts
+handle_file_conflict() {
+ local repo_file="$1"
+ local home_file="$2"
+ local filename=$(basename "$home_file")
+ local dirname=$(dirname "$home_file")
+
+ echo -e "\n${YELLOW}Conflict detected:${NC} $home_file"
+ echo "Repository version differs from your local version."
+ echo
+ echo "Choose an action:"
+ echo "1) Replace local file with repository version"
+ echo "2) Keep local file unchanged"
+ echo "3) Backup local file as ${filename}.old, use repository version"
+ echo "4) Save repository version as ${filename}.new, keep local file"
+ echo "5) Show diff and decide"
+ echo "6) Skip this file"
+ echo
+
+ while true; do
+ if ! safe_read "Enter your choice (1-6): " choice "6"; then
+ echo
+ log_warning "Failed to read input. Skipping file."
+ return
+ fi
+
+ case $choice in
+ 1)
+ cp -p "$repo_file" "$home_file"
+ log_success "Replaced $home_file with repository version"
+ break
+ ;;
+ 2)
+ log_info "Keeping local version of $home_file"
+ break
+ ;;
+ 3)
+ mv "$home_file" "${dirname}/${filename}.old"
+ cp -p "$repo_file" "$home_file"
+ log_success "Backed up local file to ${filename}.old and updated with repository version"
+ break
+ ;;
+ 4)
+ cp -p "$repo_file" "${dirname}/${filename}.new"
+ log_success "Saved repository version as ${filename}.new, kept local file"
+ break
+ ;;
+ 5)
+ show_diff "$home_file" "$repo_file"
+ echo
+ echo "After reviewing the diff, choose:"
+ echo "r) Replace with repository version"
+ echo "k) Keep local version"
+ echo "b) Backup local and use repository version"
+ echo "n) Save repository version as .new"
+ echo "s) Skip this file"
+
+ if ! safe_read "Enter your choice (r/k/b/n/s): " subchoice "s"; then
+ echo
+ log_warning "Failed to read input. Skipping file."
+ return
+ fi
+
+ case $subchoice in
+ r)
+ cp -p "$repo_file" "$home_file"
+ log_success "Replaced $home_file with repository version"
+ break
+ ;;
+ k)
+ log_info "Keeping local version of $home_file"
+ break
+ ;;
+ b)
+ mv "$home_file" "${dirname}/${filename}.old"
+ cp -p "$repo_file" "$home_file"
+ log_success "Backed up local file to ${filename}.old and updated"
+ break
+ ;;
+ n)
+ cp -p "$repo_file" "${dirname}/${filename}.new"
+ log_success "Saved repository version as ${filename}.new"
+ break
+ ;;
+ s)
+ log_info "Skipping $home_file"
+ break
+ ;;
+ *)
+ echo "Invalid choice. Please try again."
+ ;;
+ esac
+ ;;
+ 6)
+ log_info "Skipping $home_file"
+ break
+ ;;
+ *)
+ echo "Invalid choice. Please enter 1-6."
+ ;;
+ esac
+ done
+}
+
+# Function to check if PKGBUILD has changed
+check_pkgbuild_changed() {
+ local pkg_dir="$1"
+ local pkgbuild_path="${pkg_dir}/PKGBUILD"
+
+ [[ ! -f "$pkgbuild_path" ]] && return 1
+
+ # Get the path relative to repo
+ local relative_path="${pkgbuild_path#$REPO_DIR/}"
+
+ # If force check is enabled, always return true
+ if [[ "$FORCE_CHECK" == true ]]; then
+ return 0
+ fi
+
+ # Check if file changed in the last pull
+ if git diff --name-only HEAD@{1} HEAD 2>/dev/null | grep -q "^${relative_path}$"; then
+ return 0
+ fi
+
+ return 1
+}
+
+# Function to list available packages
+list_packages() {
+ local available_packages=()
+ local changed_packages=()
+
+ if [[ ! -d "$ARCH_PACKAGES_DIR" ]]; then
+ log_warning "No arch-packages directory found"
+ return 1
+ fi
+
+ for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do
+ if [[ -f "${pkg_dir}/PKGBUILD" ]]; then
+ local pkg_name=$(basename "$pkg_dir")
+ available_packages+=("$pkg_name")
+
+ if check_pkgbuild_changed "$pkg_dir"; then
+ changed_packages+=("$pkg_name")
+ fi
+ fi
+ done
+
+ if [[ ${#available_packages[@]} -eq 0 ]]; then
+ log_info "No packages found in arch-packages directory"
+ return 1
+ fi
+
+ echo -e "\n${CYAN}Available packages:${NC}"
+ for pkg in "${available_packages[@]}"; do
+ if [[ " ${changed_packages[*]} " =~ " ${pkg} " ]]; then
+ echo -e " ${GREEN}● ${pkg}${NC} (PKGBUILD changed)"
+ else
+ echo -e " ○ ${pkg}"
+ fi
+ done
+
+ if [[ ${#changed_packages[@]} -gt 0 ]]; then
+ echo -e "\n${YELLOW}Packages with changed PKGBUILDs: ${changed_packages[*]}${NC}"
+ fi
+
+ return 0
+}
+
+# Function to build selected packages
+build_packages() {
+ local build_mode="$1" # "changed", "all", or "select"
+ local packages_to_build=()
+ local rebuilt_packages=0
+
+ case "$build_mode" in
+ "changed")
+ for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do
+ if [[ -f "${pkg_dir}/PKGBUILD" ]]; then
+ local pkg_name=$(basename "$pkg_dir")
+ if check_pkgbuild_changed "$pkg_dir"; then
+ packages_to_build+=("$pkg_name")
+ fi
+ fi
+ done
+ ;;
+ "all")
+ for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do
+ if [[ -f "${pkg_dir}/PKGBUILD" ]]; then
+ local pkg_name=$(basename "$pkg_dir")
+ packages_to_build+=("$pkg_name")
+ fi
+ done
+ ;;
+ "select")
+ echo -e "\nEnter package names separated by spaces (or 'all' for all packages):"
+ if ! safe_read "Packages to build: " user_selection ""; then
+ log_warning "Failed to read input. Skipping package builds."
+ return
+ fi
+
+ if [[ "$user_selection" == "all" ]]; then
+ for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do
+ if [[ -f "${pkg_dir}/PKGBUILD" ]]; then
+ local pkg_name=$(basename "$pkg_dir")
+ packages_to_build+=("$pkg_name")
+ fi
+ done
+ else
+ read -ra packages_to_build <<<"$user_selection"
+ fi
+ ;;
+ esac
+
+ if [[ ${#packages_to_build[@]} -eq 0 ]]; then
+ log_info "No packages selected for building"
+ return
+ fi
+
+ echo -e "\n${CYAN}Packages to build: ${packages_to_build[*]}${NC}"
+
+ if ! safe_read "Proceed with building these packages? (Y/n): " confirm "Y"; then
+ log_warning "Failed to read input. Skipping package builds."
+ return
+ fi
+
+ if [[ "$confirm" =~ ^[Nn]$ ]]; then
+ log_info "Package building cancelled by user"
+ return
+ fi
+
+ for pkg_name in "${packages_to_build[@]}"; do
+ local pkg_dir="${ARCH_PACKAGES_DIR}/${pkg_name}"
+
+ if [[ ! -d "$pkg_dir" || ! -f "${pkg_dir}/PKGBUILD" ]]; then
+ log_error "Package not found or missing PKGBUILD: $pkg_name"
+ continue
+ fi
+
+ log_info "Building package: $pkg_name"
+ cd "$pkg_dir" || continue
+
+ if makepkg -si --noconfirm; then
+ log_success "Successfully built and installed $pkg_name"
+ ((rebuilt_packages++))
+ else
+ log_error "Failed to build package $pkg_name"
+ fi
+
+ cd "$REPO_DIR" || die "Failed to return to repository directory"
+ done
+
+ if [[ $rebuilt_packages -eq 0 ]]; then
+ log_warning "No packages were successfully built"
+ else
+ log_success "Successfully rebuilt $rebuilt_packages package(s)"
+ fi
+}
+
+# Function to get list of changed files since last pull or all files if force check
+get_changed_files() {
+ local dir_path="$1"
+
+ if [[ "$FORCE_CHECK" == true ]]; then
+ # Return all files in the directory
+ find "$dir_path" -type f -print0 2>/dev/null
+ else
+ # Get files that changed in the last pull
+ local changed_files=()
+ while IFS= read -r file; do
+ local full_path="${REPO_DIR}/${file}"
+ # Check if file is in the directory we're processing
+ if [[ "$full_path" == "$dir_path"/* ]] && [[ -f "$full_path" ]]; then
+ printf '%s\0' "$full_path"
+ fi
+ done < <(git diff --name-only HEAD@{1} HEAD 2>/dev/null || true)
+
+ # If no files changed via git, but force_check is false, still check all files
+ # This handles the case where there were no new commits but files might differ
+ if ! git diff --quiet HEAD@{1} HEAD 2>/dev/null; then
+ : # Files were found via git diff
+ else
+ # No git changes detected, check all files anyway for local differences
+ find "$dir_path" -type f -print0 2>/dev/null
+ fi
+ fi
+}
+
+# Function to check if we have new commits
+has_new_commits() {
+ # Check if HEAD@{1} exists (meaning there was a previous commit)
+ if git rev-parse --verify HEAD@{1} &>/dev/null; then
+ # Check if HEAD and HEAD@{1} are different
+ [[ "$(git rev-parse HEAD)" != "$(git rev-parse HEAD@{1})" ]]
+ else
+ # No previous commit reference, assume we have commits
+ return 0
+ fi
+}
+
+# Main script starts here
+log_header "Dotfiles Update Script"
+
+check=true
+
+# Parse command line arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -f | --force)
+ FORCE_CHECK=true
+ log_info "Force check mode enabled - will check all files regardless of git changes"
+ shift
+ ;;
+ -p | --packages)
+ CHECK_PACKAGES=true
+ log_info "Package checking enabled"
+ shift
+ ;;
+ -h | --help)
+ echo "Usage: $0 [OPTIONS]"
+ echo ""
+ echo "Options:"
+ echo " -f, --force Force check all files even if no new commits"
+ echo " -p, --packages Enable package checking and building"
+ echo " -h, --help Show this help message"
+ echo ""
+ echo "This script updates your dotfiles by:"
+ echo " 1. Pulling latest changes from git remote"
+ echo " 2. Optionally rebuilding packages (if -p flag is used)"
+ echo " 3. Syncing configuration files"
+ echo " 4. Updating script permissions"
+ echo ""
+ echo "Package modes (when -p is used):"
+ echo " - If no PKGBUILDs changed: asks if you want to check packages anyway"
+ echo " - If PKGBUILDs changed: offers to build changed packages"
+ echo " - Interactive selection of packages to build"
+ exit 0
+ ;;
+ --skip-notice)
+ log_warning "Skipping notice about script being untested"
+ check=false
+ shift
+ ;;
+ *)
+ log_error "Unknown option: $1"
+ echo "Use --help for usage information"
+ exit 1
+ ;;
+ esac
+done
+
+if [[ "$check" == true ]]; then
+ log_warning "THIS SCRIPT IS NOT FULLY TESTED AND MAY CAUSE ISSUES!"
+ safe_read "BY CONTINUE YOU WILL USE IT AT YOUR OWN RISK (y/N): " response "N"
+
+ if [[ ! "$response" =~ ^[Yy]$ ]]; then
+ log_error "Update aborted by user"
+ exit 1
+ fi
+fi
+
+# Check if we're in a git repository
+cd "$REPO_DIR" || die "Failed to change to repository directory"
+
+if git rev-parse --is-inside-work-tree &>/dev/null; then
+ log_info "Running in git repository: $(git rev-parse --show-toplevel)"
+else
+ log_error "Not in a git repository. Please run this script from your dotfiles repository."
+ exit 1
+fi
+
+# Step 1: Pull latest commits
+log_header "Pulling Latest Changes"
+
+# Check current branch
+current_branch=$(git branch --show-current)
+if [[ -z "$current_branch" ]]; then
+ log_warning "In detached HEAD state. Checking out main/master branch..."
+ if git show-ref --verify --quiet refs/heads/main; then
+ git checkout main
+ current_branch="main"
+ elif git show-ref --verify --quiet refs/heads/master; then
+ git checkout master
+ current_branch="master"
+ else
+ die "Could not find main or master branch"
+ fi
+fi
+
+log_info "Current branch: $current_branch"
+
+# Check for uncommitted changes
+if ! git diff --quiet || ! git diff --cached --quiet; then
+ log_warning "You have uncommitted changes:"
+ git status --short
+ echo
+
+ if ! safe_read "Do you want to continue? This will stash your changes. (y/N): " response "N"; then
+ echo
+ log_error "Failed to read input. Aborting."
+ exit 1
+ fi
+
+ if [[ ! "$response" =~ ^[Yy]$ ]]; then
+ die "Aborted by user"
+ fi
+ git stash push -m "Auto-stash before update $(date)"
+ log_info "Changes stashed"
+fi
+
+# Check if remote exists
+if git remote get-url origin &>/dev/null; then
+ # Pull changes
+ log_info "Pulling changes from origin/$current_branch..."
+ if git pull; then
+ log_success "Successfully pulled latest changes"
+ else
+ log_warning "Failed to pull changes from remote. Continuing with local repository..."
+ log_info "You may need to resolve conflicts manually later."
+ fi
+else
+ log_warning "No remote 'origin' configured. Skipping pull operation."
+ log_info "This appears to be a local-only repository."
+fi
+
+# Step 2: Handle package building (only if requested)
+rebuilt_packages=0
+
+if [[ "$CHECK_PACKAGES" == true ]]; then
+ log_header "Package Management"
+
+ if [[ ! -d "$ARCH_PACKAGES_DIR" ]]; then
+ log_warning "No arch-packages directory found. Skipping package management."
+ else
+ # Check if any PKGBUILDs have changed
+ changed_pkgbuilds=()
+ for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do
+ if [[ -f "${pkg_dir}/PKGBUILD" ]]; then
+ local pkg_name=$(basename "$pkg_dir")
+ if check_pkgbuild_changed "$pkg_dir"; then
+ changed_pkgbuilds+=("$pkg_name")
+ fi
+ fi
+ done
+
+ if [[ ${#changed_pkgbuilds[@]} -gt 0 ]]; then
+ log_info "Found ${#changed_pkgbuilds[@]} package(s) with changed PKGBUILDs: ${changed_pkgbuilds[*]}"
+ echo
+ echo "Package build options:"
+ echo "1) Build only packages with changed PKGBUILDs"
+ echo "2) List all packages and select which to build"
+ echo "3) Build all packages"
+ echo "4) Skip package building"
+ echo
+
+ if safe_read "Choose an option (1-4): " pkg_choice "1"; then
+ case $pkg_choice in
+ 1)
+ build_packages "changed"
+ ;;
+ 2)
+ if list_packages; then
+ build_packages "select"
+ fi
+ ;;
+ 3)
+ build_packages "all"
+ ;;
+ 4 | *)
+ log_info "Skipping package building"
+ ;;
+ esac
+ else
+ log_warning "Failed to read input. Skipping package building."
+ fi
+ else
+ log_info "No PKGBUILDs have changed since last update."
+ echo
+ if safe_read "Do you want to check and build packages anyway? (y/N): " check_anyway "N"; then
+ if [[ "$check_anyway" =~ ^[Yy]$ ]]; then
+ if list_packages; then
+ echo
+ echo "Package build options:"
+ echo "1) Select specific packages to build"
+ echo "2) Build all packages"
+ echo "3) Skip package building"
+
+ if safe_read "Choose an option (1-3): " build_choice "3"; then
+ case $build_choice in
+ 1)
+ build_packages "select"
+ ;;
+ 2)
+ build_packages "all"
+ ;;
+ 3 | *)
+ log_info "Skipping package building"
+ ;;
+ esac
+ else
+ log_info "Skipping package building"
+ fi
+ fi
+ else
+ log_info "Skipping package management"
+ fi
+ else
+ log_info "Skipping package management"
+ fi
+ fi
+ fi
+else
+ log_header "Package Management"
+ log_info "Package checking disabled. Use -p or --packages flag to enable package management."
+
+ # Still show a hint if there are changed PKGBUILDs
+ if [[ -d "$ARCH_PACKAGES_DIR" ]]; then
+ changed_count=0
+ for pkg_dir in "$ARCH_PACKAGES_DIR"/*/; do
+ if [[ -f "${pkg_dir}/PKGBUILD" ]] && check_pkgbuild_changed "$pkg_dir"; then
+ ((changed_count++))
+ fi
+ done
+
+ if [[ $changed_count -gt 0 ]]; then
+ log_warning "Note: $changed_count package(s) have changed PKGBUILDs. Use -p flag to manage packages."
+ fi
+ fi
+fi
+
+# Step 3: Update configuration files
+log_header "Updating Configuration Files"
+
+# Check if we should process files
+process_files=false
+if [[ "$FORCE_CHECK" == true ]]; then
+ process_files=true
+ log_info "Force mode: checking all configuration files"
+elif has_new_commits; then
+ process_files=true
+ log_info "New commits detected: checking changed configuration files"
+else
+ log_info "No new commits found: checking for local file differences"
+ process_files=true # Always check for differences even without commits
+fi
+
+if [[ "$process_files" == true ]]; then
+ files_processed=0
+ files_updated=0
+ files_created=0
+
+ for dir_name in "${MONITOR_DIRS[@]}"; do
+ repo_dir_path="${REPO_DIR}/${dir_name}"
+ home_dir_path="${HOME}/${dir_name}"
+
+ if [[ ! -d "$repo_dir_path" ]]; then
+ log_warning "Repository directory not found: $repo_dir_path"
+ continue
+ fi
+
+ log_info "Processing directory: $dir_name"
+
+ # Create home directory if it doesn't exist
+ mkdir -p "$home_dir_path"
+
+ # Get files to process (changed files or all files based on mode)
+ while IFS= read -r -d '' repo_file; do
+ # Calculate relative path and corresponding home file path
+ rel_path="${repo_file#$repo_dir_path/}"
+ home_file="${home_dir_path}/${rel_path}"
+
+ # Check if file should be ignored
+ if should_ignore "$home_file"; then
+ continue
+ fi
+
+ ((files_processed++))
+
+ # Create directory structure if needed
+ mkdir -p "$(dirname "$home_file")"
+
+ if [[ -f "$home_file" ]]; then
+ # File exists, check if different
+ if ! cmp -s "$repo_file" "$home_file"; then
+ log_info "Found difference in: $rel_path"
+ handle_file_conflict "$repo_file" "$home_file"
+ ((files_updated++))
+ fi
+ else
+ # New file, copy it
+ cp -p "$repo_file" "$home_file"
+ log_success "Created new file: $home_file"
+ ((files_created++))
+ fi
+ done < <(get_changed_files "$repo_dir_path")
+ done
+
+ # Show processing summary
+ echo
+ log_info "File processing summary:"
+ log_info "- Files processed: $files_processed"
+ log_info "- Files with conflicts: $files_updated"
+ log_info "- New files created: $files_created"
+else
+ log_info "Skipping file updates (no changes detected and not in force mode)"
+fi
+
+# Step 4: Update script permissions
+log_header "Updating Script Permissions"
+
+if [[ -d "${REPO_DIR}/scriptdata" ]]; then
+ find "${REPO_DIR}/scriptdata" -type f -name "*.sh" -exec chmod +x {} \;
+ find "${REPO_DIR}/scriptdata" -type f -executable -exec chmod +x {} \;
+ log_success "Updated script permissions"
+fi
+
+# Make sure local bin scripts are executable
+if [[ -d "${HOME}/.local/bin" ]]; then
+ find "${HOME}/.local/bin" -type f -exec chmod +x {} \; 2>/dev/null || true
+ log_success "Updated ~/.local/bin script permissions"
+fi
+
+log_header "Update Complete"
+log_success "Dotfiles update completed successfully!"
+
+# Show summary
+echo
+echo -e "${CYAN}Summary:${NC}"
+echo "- Repository: $(git log -1 --pretty=format:'%h - %s (%cr)')"
+echo "- Branch: $current_branch"
+echo "- Mode: $([ "$FORCE_CHECK" == true ] && echo "Force check" || echo "Normal")"
+echo "- Package checking: $([ "$CHECK_PACKAGES" == true ] && echo "Enabled" || echo "Disabled")"
+
+if [[ $rebuilt_packages -gt 0 ]]; then
+ echo "- Packages rebuilt: $rebuilt_packages"
+fi
+
+if [[ "$process_files" == true ]]; then
+ echo "- Files processed: $files_processed"
+ echo "- Files updated/conflicted: $files_updated"
+ echo "- New files created: $files_created"
+fi
+
+echo "- Configuration directories: ${MONITOR_DIRS[*]}"
+
+# Remind about ignore files and show examples
+if [[ ! -f "$HOME_UPDATE_IGNORE_FILE" && ! -f "$UPDATE_IGNORE_FILE" ]]; then
+ echo
+ log_info "Tip: Create ignore files to exclude files from updates:"
+ echo " - Repository ignore: ${REPO_DIR}/.updateignore"
+ echo " - User ignore: ~/.updateignore"
+ echo
+ echo "Example patterns:"
+ echo " *.log # Ignore all .log files"
+ echo " .config/personal/ # Ignore entire directory"
+ echo " secret-config.conf # Ignore specific file"
+ echo " /temp-file # Ignore from root only"
+ echo " *secret* # Ignore files containing 'secret'"
+fi
+
+echo