refactor: make the whole thing more generic

This commit is contained in:
2024-04-02 16:28:57 +02:00
parent 7b648e1955
commit 651f3ad808
193 changed files with 763 additions and 521 deletions

View File

@@ -0,0 +1,29 @@
{ config, ... }: {
xdg.enable = true;
xdg.userDirs.enable = true;
home.sessionVariables = {
HISTFILE = "${config.xdg.stateHome}/bash/history";
NPM_CONFIG_USERCONFIG = "${config.xdg.configHome}/npm/config";
NPM_CONFIG_CACHE = "${config.xdg.cacheHome}/npm";
NPM_CONFIG_TMP = "${config.xdg.stateHome}/npm";
WINEPREFIX = "${config.xdg.configHome}/wineprefixes/default";
_JAVA_OPTGRADLE_USER_HOMEIONS =
''-Djava.util.prefs.userRoot="${config.xdg.configHome}"/java'';
GRADLE_USER_HOME = "${config.xdg.configHome}/gradle";
DVDCSS_CACHE = "${config.xdg.cacheHome}/dvdcss";
DOCKER_CONFIG = "${config.xdg.configHome}/docker";
PYTHON_HISTORY = "${config.xdg.stateHome}/python/history";
PYTHONCACHEPREFIX = "${config.xdg.cacheHome}/python";
PYTHONUSERBASE = "${config.xdg.dataHome}/python";
WGETRC = "${config.xdg.configHome}/wgetrc";
XCOMPOSEFILE = "${config.xdg.configHome}/X11/xcompose";
XCOMPOSECACHE = "${config.xdg.cacheHome}/X11/xcompose";
};
gtk.gtk2.configLocation = "${config.xdg.configHome}/gtk-2.0/gtkrc";
programs.gpg.homedir = "${config.xdg.configHome}/gnupg";
programs.zsh.dotDir = "${config.xdg.configHome}/zsh";
programs.zsh.history.path = "${config.xdg.stateHome}/zsh/history";
xdg.configFile.wgetrc.text = ''
hsts-file="${config.xdg.cacheHome}/wget-hsts"
'';
}

View File

@@ -0,0 +1,677 @@
{ config, ... }: {
xdg.mimeApps = {
enable = true;
defaultApplications = {
.3dm x-world/x-3dmf
.3dmf x-world/x-3dmf
.7z application/x-7z-compressed
.a application/octet-stream
.aab application/x-authorware-bin
.aam application/x-authorware-map
.aas application/x-authorware-seg
.abc text/vnd.abc
.acgi text/html
.afl video/animaflex
.ai application/postscript
.aif audio/aiff
.aif audio/x-aiff
.aifc audio/aiff
.aifc audio/x-aiff
.aiff audio/aiff
.aiff audio/x-aiff
.aim application/x-aim
.aip text/x-audiosoft-intra
.ani application/x-navi-animation
.aos application/x-nokia-9000-communicator-add-on-software
.aps application/mime
.arc application/octet-stream
.arj application/arj
.arj application/octet-stream
.art image/x-jg
.asf video/x-ms-asf
.asm text/x-asm
.asp text/asp
.asx application/x-mplayer2
.asx video/x-ms-asf
.asx video/x-ms-asf-plugin
.au audio/basic
.au audio/x-au
.avi application/x-troff-msvideo
.avi video/avi
.avi video/msvideo
.avi video/x-msvideo
.avs video/avs-video
.bcpio application/x-bcpio
.bin application/mac-binary
.bin application/macbinary
.bin application/octet-stream
.bin application/x-binary
.bin application/x-macbinary
.bm image/bmp
.bmp image/bmp
.bmp image/x-windows-bmp
.boo application/book
.book application/book
.boz application/x-bzip2
.bsh application/x-bsh
.bz application/x-bzip
.bz2 application/x-bzip2
.c text/plain
.c text/x-c
.c++ text/plain
.cat application/vnd.ms-pki.seccat
.cc text/plain
.cc text/x-c
.ccad application/clariscad
.cco application/x-cocoa
.cdf application/cdf
.cdf application/x-cdf
.cdf application/x-netcdf
.cer application/pkix-cert
.cer application/x-x509-ca-cert
.cha application/x-chat
.chat application/x-chat
.class application/java
.class application/java-byte-code
.class application/x-java-class
.com application/octet-stream
.com text/plain
.conf text/plain
.cpio application/x-cpio
.cpp text/x-c
.cpt application/mac-compactpro
.cpt application/x-compactpro
.cpt application/x-cpt
.crl application/pkcs-crl
.crl application/pkix-crl
.crt application/pkix-cert
.crt application/x-x509-ca-cert
.crt application/x-x509-user-cert
.csh application/x-csh
.csh text/x-script.csh
.css application/x-pointplus
.css text/css
.csv text/csv
.cxx text/plain
.dcr application/x-director
.deepv application/x-deepv
.def text/plain
.der application/x-x509-ca-cert
.dif video/x-dv
.dir application/x-director
.dl video/dl
.dl video/x-dl
.doc application/msword
.docx application/vnd.openxmlformats-officedocument.wordprocessingml.document
.dot application/msword
.dp application/commonground
.drw application/drafting
.dump application/octet-stream
.dv video/x-dv
.dvi application/x-dvi
.dwf drawing/x-dwf (old)
.dwf model/vnd.dwf
.dwg application/acad
.dwg image/vnd.dwg
.dwg image/x-dwg
.dxf application/dxf
.dxf image/vnd.dwg
.dxf image/x-dwg
.dxr application/x-director
.el text/x-script.elisp
.elc application/x-bytecode.elisp (compiled elisp)
.elc application/x-elc
.env application/x-envoy
.eot application/vnd.ms-fontobject
.eps application/postscript
.es application/x-esrehber
.etx text/x-setext
.evy application/envoy
.evy application/x-envoy
.exe application/octet-stream
.f text/plain
.f text/x-fortran
.f77 text/x-fortran
.f90 text/plain
.f90 text/x-fortran
.fdf application/vnd.fdf
.fif application/fractals
.fif image/fif
.flac audio/flac
.fli video/fli
.fli video/x-fli
.flo image/florian
.flx text/vnd.fmi.flexstor
.fmf video/x-atomic3d-feature
.for text/plain
.for text/x-fortran
.fpx image/vnd.fpx
.fpx image/vnd.net-fpx
.frl application/freeloader
.funk audio/make
.g text/plain
.g3 image/g3fax
.gif image/gif
.gl video/gl
.gl video/x-gl
.gsd audio/x-gsm
.gsm audio/x-gsm
.gsp application/x-gsp
.gss application/x-gss
.gtar application/x-gtar
.gz application/x-compressed
.gz application/x-gzip
.gzip application/x-gzip
.gzip multipart/x-gzip
.h text/plain
.h text/x-h
.hdf application/x-hdf
.help application/x-helpfile
.hgl application/vnd.hp-hpgl
.hh text/plain
.hh text/x-h
.hlb text/x-script
.hlp application/hlp
.hlp application/x-helpfile
.hlp application/x-winhelp
.hpg application/vnd.hp-hpgl
.hpgl application/vnd.hp-hpgl
.hqx application/binhex
.hqx application/binhex4
.hqx application/mac-binhex
.hqx application/mac-binhex40
.hqx application/x-binhex40
.hqx application/x-mac-binhex40
.hta application/hta
.htc text/x-component
.htm text/html
.html text/html
.htmls text/html
.htt text/webviewhtml
.htx text/html
.ice x-conference/x-cooltalk
.ico image/x-icon
.ics text/calendar
.idc text/plain
.ief image/ief
.iefs image/ief
.iges application/iges
.iges model/iges
.igs application/iges
.igs model/iges
.ima application/x-ima
.imap application/x-httpd-imap
.inf application/inf
.ins application/x-internett-signup
.ip application/x-ip2
.isu video/x-isvideo
.it audio/it
.iv application/x-inventor
.ivr i-world/i-vrml
.ivy application/x-livescreen
.jam audio/x-jam
.jav text/plain
.jav text/x-java-source
.java text/plain
.java text/x-java-source
.jcm application/x-java-commerce
.jfif image/jpeg
.jfif image/pjpeg
.jfif-tbnl image/jpeg
.jpe image/jpeg
.jpe image/pjpeg
.jpeg image/jpeg
.jpeg image/pjpeg
.jpg image/jpeg
.jpg image/pjpeg
.jps image/x-jps
.js application/x-javascript
.js application/javascript
.js application/ecmascript
.js text/javascript
.js text/ecmascript
.json application/json
.jut image/jutvision
.kar audio/midi
.kar music/x-karaoke
.ksh application/x-ksh
.ksh text/x-script.ksh
.la audio/nspaudio
.la audio/x-nspaudio
.lam audio/x-liveaudio
.latex application/x-latex
.lha application/lha
.lha application/octet-stream
.lha application/x-lha
.lhx application/octet-stream
.list text/plain
.lma audio/nspaudio
.lma audio/x-nspaudio
.log text/plain
.lsp application/x-lisp
.lsp text/x-script.lisp
.lst text/plain
.lsx text/x-la-asf
.ltx application/x-latex
.lzh application/octet-stream
.lzh application/x-lzh
.lzx application/lzx
.lzx application/octet-stream
.lzx application/x-lzx
.m text/plain
.m text/x-m
.m1v video/mpeg
.m2a audio/mpeg
.m2v video/mpeg
.m3u audio/x-mpequrl
.man application/x-troff-man
.map application/x-navimap
.mar text/plain
.mbd application/mbedlet
.mc$ application/x-magic-cap-package-1.0
.mcd application/mcad
.mcd application/x-mathcad
.mcf image/vasa
.mcf text/mcf
.mcp application/netmc
.me application/x-troff-me
.mht message/rfc822
.mhtml message/rfc822
.mid application/x-midi
.mid audio/midi
.mid audio/x-mid
.mid audio/x-midi
.mid music/crescendo
.mid x-music/x-midi
.midi application/x-midi
.midi audio/midi
.midi audio/x-mid
.midi audio/x-midi
.midi music/crescendo
.midi x-music/x-midi
.mif application/x-frame
.mif application/x-mif
.mime message/rfc822
.mime www/mime
.mjf audio/x-vnd.audioexplosion.mjuicemediafile
.mjpg video/x-motion-jpeg
.mka audio/x-matroska
.mkv video/x-matroska
.mm application/base64
.mm application/x-meme
.mme application/base64
.mod audio/mod
.mod audio/x-mod
.moov video/quicktime
.mov video/quicktime
.movie video/x-sgi-movie
.mp2 audio/mpeg
.mp2 audio/x-mpeg
.mp2 video/mpeg
.mp2 video/x-mpeg
.mp2 video/x-mpeq2a
.mp3 audio/mpeg3
.mp3 audio/x-mpeg-3
.mp3 video/mpeg
.mp3 video/x-mpeg
.mp4 video/mp4
.mpa audio/mpeg
.mpa video/mpeg
.mpc application/x-project
.mpe video/mpeg
.mpeg video/mpeg
.mpg audio/mpeg
.mpg video/mpeg
.mpga audio/mpeg
.mpp application/vnd.ms-project
.mpt application/x-project
.mpv application/x-project
.mpx application/x-project
.mrc application/marc
.ms application/x-troff-ms
.mv video/x-sgi-movie
.my audio/make
.mzz application/x-vnd.audioexplosion.mzz
.nap image/naplps
.naplps image/naplps
.nc application/x-netcdf
.ncm application/vnd.nokia.configuration-message
.nif image/x-niff
.niff image/x-niff
.nix application/x-mix-transfer
.nsc application/x-conference
.nvd application/x-navidoc
.o application/octet-stream
.oda application/oda
.ogg audio/ogg
.ogg video/ogg
.omc application/x-omc
.omcd application/x-omcdatamaker
.omcr application/x-omcregerator
.otf font/otf
.p text/x-pascal
.p10 application/pkcs10
.p10 application/x-pkcs10
.p12 application/pkcs-12
.p12 application/x-pkcs12
.p7a application/x-pkcs7-signature
.p7c application/pkcs7-mime
.p7c application/x-pkcs7-mime
.p7m application/pkcs7-mime
.p7m application/x-pkcs7-mime
.p7r application/x-pkcs7-certreqresp
.p7s application/pkcs7-signature
.part application/pro_eng
.pas text/pascal
.pbm image/x-portable-bitmap
.pcl application/vnd.hp-pcl
.pcl application/x-pcl
.pct image/x-pict
.pcx image/x-pcx
.pdb chemical/x-pdb
.pdf application/pdf
.pfunk audio/make
.pfunk audio/make.my.funk
.pgm image/x-portable-graymap
.pgm image/x-portable-greymap
.pic image/pict
.pict image/pict
.pkg application/x-newton-compatible-pkg
.pko application/vnd.ms-pki.pko
.pl text/plain
.pl text/x-script.perl
.plx application/x-pixclscript
.pm image/x-xpixmap
.pm text/x-script.perl-module
.pm4 application/x-pagemaker
.pm5 application/x-pagemaker
.png image/png
.pnm application/x-portable-anymap
.pnm image/x-portable-anymap
.pot application/mspowerpoint
.pot application/vnd.ms-powerpoint
.pov model/x-pov
.ppa application/vnd.ms-powerpoint
.ppm image/x-portable-pixmap
.pps application/mspowerpoint
.pps application/vnd.ms-powerpoint
.ppt application/mspowerpoint
.ppt application/powerpoint
.ppt application/vnd.ms-powerpoint
.ppt application/x-mspowerpoint
.pptx application/vnd.openxmlformats-officedocument.presentationml.presentation
.ppz application/mspowerpoint
.pre application/x-freelance
.prt application/pro_eng
.ps application/postscript
.psd application/octet-stream
.pvu paleovu/x-pv
.pwz application/vnd.ms-powerpoint
.py text/x-script.phyton
.pyc application/x-bytecode.python
.qcp audio/vnd.qcelp
.qd3 x-world/x-3dmf
.qd3d x-world/x-3dmf
.qif image/x-quicktime
.qt video/quicktime
.qtc video/x-qtc
.qti image/x-quicktime
.qtif image/x-quicktime
.ra audio/x-pn-realaudio
.ra audio/x-pn-realaudio-plugin
.ra audio/x-realaudio
.ram audio/x-pn-realaudio
.ras application/x-cmu-raster
.ras image/cmu-raster
.ras image/x-cmu-raster
.rast image/cmu-raster
.rar application/vnd.rar
.rexx text/x-script.rexx
.rf image/vnd.rn-realflash
.rgb image/x-rgb
.rm application/vnd.rn-realmedia
.rm audio/x-pn-realaudio
.rmi audio/mid
.rmm audio/x-pn-realaudio
.rmp audio/x-pn-realaudio
.rmp audio/x-pn-realaudio-plugin
.rng application/ringing-tones
.rng application/vnd.nokia.ringing-tone
.rnx application/vnd.rn-realplayer
.roff application/x-troff
.rp image/vnd.rn-realpix
.rpm audio/x-pn-realaudio-plugin
.rt text/richtext
.rt text/vnd.rn-realtext
.rtf application/rtf
.rtf application/x-rtf
.rtf text/richtext
.rtx application/rtf
.rtx text/richtext
.rv video/vnd.rn-realvideo
.s text/x-asm
.s3m audio/s3m
.saveme application/octet-stream
.sbk application/x-tbook
.scm application/x-lotusscreencam
.scm text/x-script.guile
.scm text/x-script.scheme
.scm video/x-scm
.sdml text/plain
.sdp application/sdp
.sdp application/x-sdp
.sdr application/sounder
.sea application/sea
.sea application/x-sea
.set application/set
.sgm text/sgml
.sgm text/x-sgml
.sgml text/sgml
.sgml text/x-sgml
.sh application/x-bsh
.sh application/x-sh
.sh application/x-shar
.sh text/x-script.sh
.shar application/x-bsh
.shar application/x-shar
.shtml text/html
.shtml text/x-server-parsed-html
.sid audio/x-psid
.sit application/x-sit
.sit application/x-stuffit
.skd application/x-koan
.skm application/x-koan
.skp application/x-koan
.skt application/x-koan
.sl application/x-seelogo
.smi application/smil
.smil application/smil
.snd audio/basic
.snd audio/x-adpcm
.sol application/solids
.spc application/x-pkcs7-certificates
.spc text/x-speech
.spl application/futuresplash
.spr application/x-sprite
.sprite application/x-sprite
.src application/x-wais-source
.ssi text/x-server-parsed-html
.ssm application/streamingmedia
.sst application/vnd.ms-pki.certstore
.step application/step
.stl application/sla
.stl application/vnd.ms-pki.stl
.stl application/x-navistyle
.stp application/step
.sv4cpio application/x-sv4cpio
.sv4crc application/x-sv4crc
.svf image/vnd.dwg
.svf image/x-dwg
.svg image/svg+xml
.svr application/x-world
.svr x-world/x-svr
.swf application/x-shockwave-flash
.t application/x-troff
.talk text/x-speech
.tar application/x-tar
.tbk application/toolbook
.tbk application/x-tbook
.tcl application/x-tcl
.tcl text/x-script.tcl
.tcsh text/x-script.tcsh
.tex application/x-tex
.texi application/x-texinfo
.texinfo application/x-texinfo
.text application/plain
.text text/plain
.tgz application/gnutar
.tgz application/x-compressed
.tif image/tiff
.tif image/x-tiff
.tiff image/tiff
.tiff image/x-tiff
.tr application/x-troff
.ts video/mp2t
.tsi audio/tsp-audio
.tsp application/dsptype
.tsp audio/tsplayer
.tsv text/tab-separated-values
.turbot image/florian
.txt text/plain
.uil text/x-uil
.uni text/uri-list
.unis text/uri-list
.unv application/i-deas
.uri text/uri-list
.uris text/uri-list
.ustar application/x-ustar
.ustar multipart/x-ustar
.uu application/octet-stream
.uu text/x-uuencode
.uue text/x-uuencode
.vcd application/x-cdlink
.vcs text/x-vcalendar
.vda application/vda
.vdo video/vdo
.vew application/groupwise
.viv video/vivo
.viv video/vnd.vivo
.vivo video/vivo
.vivo video/vnd.vivo
.vmd application/vocaltec-media-desc
.vmf application/vocaltec-media-file
.voc audio/voc
.voc audio/x-voc
.vos video/vosaic
.vox audio/voxware
.vqe audio/x-twinvq-plugin
.vqf audio/x-twinvq
.vql audio/x-twinvq-plugin
.vrml application/x-vrml
.vrml model/vrml
.vrml x-world/x-vrml
.vrt x-world/x-vrt
.vsd application/x-visio
.vst application/x-visio
.vsw application/x-visio
.w60 application/wordperfect6.0
.w61 application/wordperfect6.1
.w6w application/msword
.wav audio/wav
.wav audio/x-wav
.wb1 application/x-qpro
.wbmp image/vnd.wap.wbmp
.web application/vnd.xara
.webm video/webm
.webp image/webp
.wiz application/msword
.wk1 application/x-123
.wmf windows/metafile
.wml text/vnd.wap.wml
.wmlc application/vnd.wap.wmlc
.wmls text/vnd.wap.wmlscript
.wmlsc application/vnd.wap.wmlscriptc
.word application/msword
.woff font/woff
.woff2 font/woff2
.wp application/wordperfect
.wp5 application/wordperfect
.wp5 application/wordperfect6.0
.wp6 application/wordperfect
.wpd application/wordperfect
.wpd application/x-wpwin
.wq1 application/x-lotus
.wri application/mswrite
.wri application/x-wri
.wrl application/x-world
.wrl model/vrml
.wrl x-world/x-vrml
.wrz model/vrml
.wrz x-world/x-vrml
.wsc text/scriplet
.wsrc application/x-wais-source
.wtk application/x-wintalk
.xbm image/x-xbitmap
.xbm image/x-xbm
.xbm image/xbm
.xdr video/x-amt-demorun
.xgz xgl/drawing
.xif image/vnd.xiff
.xl application/excel
.xla application/excel
.xla application/x-excel
.xla application/x-msexcel
.xlb application/excel
.xlb application/vnd.ms-excel
.xlb application/x-excel
.xlc application/excel
.xlc application/vnd.ms-excel
.xlc application/x-excel
.xld application/excel
.xld application/x-excel
.xlk application/excel
.xlk application/x-excel
.xll application/excel
.xll application/vnd.ms-excel
.xll application/x-excel
.xlm application/excel
.xlm application/vnd.ms-excel
.xlm application/x-excel
.xls application/excel
.xls application/vnd.ms-excel
.xls application/x-excel
.xls application/x-msexcel
.xlt application/excel
.xlt application/x-excel
.xlv application/excel
.xlv application/x-excel
.xlw application/excel
.xlw application/vnd.ms-excel
.xlw application/x-excel
.xlw application/x-msexcel
.xm audio/xm
.xml application/xml
.xml text/xml
.xmz xgl/movie
.xpix application/x-vnd.ls-xpix
.xpm image/x-xpixmap
.xpm image/xpm
.x-png image/png
.xlsx application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
.xsr video/x-amt-showrun
.xwd image/x-xwd
.xwd image/x-xwindowdump
.xyz chemical/x-pdb
.yaml application/x-yaml
.yml application/x-yaml
.z application/x-compress
.z application/x-compressed
.zip application/x-compressed
.zip application/x-zip-compressed
.zip application/zip
.zip multipart/x-zip
.zoo application/octet-stream
.zsh text/x-script.zsh
};
};
}

View File

@@ -0,0 +1,19 @@
{ username, desktop, stateVersion, osConfig, inputs, ... }: {
home = {
stateVersion = osConfig.system.stateVersion;
inherit username;
homeDirectory = "/home/${username}";
};
imports = [
inputs.ags.homeManagerModules.default
inputs.nixvim.homeManagerModules.nixvim
inputs.anyrun.homeManagerModules.default
./clean-home-dir.nix
./programs/neovide.nix
# ./default-apps.nix
./packages
./programs
./services
./desktops/hyprland
];
}

View File

@@ -0,0 +1,77 @@
env:
es2021: true
extends: eslint:recommended
overrides: []
parserOptions:
ecmaVersion: latest
sourceType: "module"
rules:
arrow-parens:
- error
- as-needed
comma-dangle:
- error
- always-multiline
comma-spacing:
- error
- before: false
after: true
comma-style:
- error
- last
curly:
- error
- multi-or-nest
- consistent
dot-location:
- error
- property
eol-last: error
indent:
- error
- 4
- SwitchCase: 1
keyword-spacing:
- error
- before: true
lines-between-class-members:
- error
- always
- exceptAfterSingleLine: true
padded-blocks:
- error
- never
- allowSingleLineBlocks: false
prefer-const: error
quotes:
- error
- single
- avoidEscape: true
semi:
- error
- always
nonblock-statement-body-position:
- error
- below
no-trailing-spaces:
- error
array-bracket-spacing:
- error
- never
key-spacing:
- error
- beforeColon: false
afterColon: true
object-curly-spacing:
- error
- always
no-useless-escape:
- off
globals:
pkg: readonly
ags: readonly
ARGV: readonly
imports: readonly
print: readonly
console: readonly
logError: readonly

View File

@@ -0,0 +1,5 @@
node_modules
types
package-lock.json
weather_key
setup.sh

View File

@@ -0,0 +1,14 @@
extends: stylelint-config-standard-scss
ignoreFiles:
- "**/*.js"
- "**/*.ts"
rules:
selector-type-no-unknown: null
declaration-empty-line-before: null
no-descending-specificity: null
selector-pseudo-class-no-unknown: null
color-function-notation: legacy
alpha-value-notation: number
scss/operator-no-unspaced: null
scss/no-global-function-names: null
scss/dollar-variable-empty-line-before: null

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

View File

@@ -0,0 +1,2 @@
import { default as main } from "./js/main.js";
export default main;

View File

@@ -0,0 +1,96 @@
import PopupWindow from "../misc/PopupWindow.js";
import icons from "../icons.js";
const pkg = JSON.parse(Utils.readFile(App.configDir + "/package.json"));
const show = JSON.parse(
Utils.readFile(Utils.CACHE_DIR + "/show_about") || "true",
);
const dontShow = () =>
Utils.writeFile("false", Utils.CACHE_DIR + "/show_about");
const avatar = App.configDir + "/assets/aylur.jpg";
/**
* @param {Object} o
* @param {string} o.label
* @param {string} o.link
*/
const LinkButton = ({ label, link }) =>
Widget.Button({
on_clicked: () => Utils.execAsync(["xdg-open", link]),
child: Widget.Box({
children: [
Widget.Label({ label, hexpand: true, xalign: 0 }),
Widget.Icon(icons.ui.link),
],
}),
});
export default () =>
PopupWindow({
name: "about",
transition: "slide_down",
child: Widget.Box({
vertical: true,
class_name: "window-content",
children: [
Widget.Box({
class_name: "avatar",
hpack: "center",
css: `background-image: url('${avatar}');`,
}),
Widget.Box({
vertical: true,
class_name: "labels vertical",
children: [
Widget.Label({
class_name: "title",
label: pkg.description,
}),
Widget.Label({
class_name: "author",
label: pkg.author,
}),
Widget.Label({
class_name: "version",
hpack: "center",
label: pkg.version,
}),
],
}),
Widget.Box({
class_name: "buttons",
vertical: true,
vexpand: true,
vpack: "end",
children: [
LinkButton({
label: "Support me on Ko-fi",
link: pkg.kofi,
}),
LinkButton({
label: "Report an Issue",
link: pkg.bugs.url,
}),
],
}),
Widget.Button({
class_name: "dont-show",
on_clicked: () => {
dontShow();
App.toggleWindow("about");
},
child: Widget.Box({
children: [
Widget.Label("Don't show again"),
Widget.Box({ hexpand: true }),
Widget.Icon(icons.ui.close),
],
}),
}),
],
}),
});
export function showAbout(force = false) {
if (show || force) App.toggleWindow("about");
}

View File

@@ -0,0 +1,44 @@
import options from "../options.js";
/** @param {import('resource:///com/github/Aylur/ags/service/applications.js').Application} app */
export default (app) => {
const title = Widget.Label({
class_name: "title",
label: app.name,
xalign: 0,
vpack: "center",
truncate: "end",
});
const description = Widget.Label({
class_name: "description",
label: app.description || "",
wrap: true,
xalign: 0,
justification: "left",
vpack: "center",
});
const icon = Widget.Icon({
icon: Utils.lookUpIcon(app.icon_name || "") ? app.icon_name || "" : "",
size: options.applauncher.icon_size.bind("value"),
});
const textBox = Widget.Box({
vertical: true,
vpack: "center",
children: app.description ? [title, description] : [title],
});
return Widget.Button({
class_name: "app-item",
attribute: app,
child: Widget.Box({
children: [icon, textBox],
}),
on_clicked: () => {
App.closeWindow("applauncher");
app.launch();
},
});
};

View File

@@ -0,0 +1,90 @@
import Applications from "resource:///com/github/Aylur/ags/service/applications.js";
import PopupWindow from "../misc/PopupWindow.js";
import AppItem from "./AppItem.js";
import icons from "../icons.js";
import { launchApp } from "../utils.js";
import options from "../options.js";
const WINDOW_NAME = "applauncher";
const Applauncher = () => {
const mkItems = () => [
Widget.Separator({ hexpand: true }),
...Applications.query("").flatMap((app) =>
Widget.Revealer({
setup: (w) => (w.attribute = { app, revealer: w }),
child: Widget.Box({
vertical: true,
children: [
Widget.Separator({ hexpand: true }),
AppItem(app),
Widget.Separator({ hexpand: true }),
],
}),
}),
),
Widget.Separator({ hexpand: true }),
];
let items = mkItems();
const list = Widget.Box({
class_name: "app-list",
vertical: true,
children: items,
});
const entry = Widget.Entry({
hexpand: true,
primary_icon_name: icons.apps.search,
// set some text so on-change works the first time
text: "-",
on_accept: ({ text }) => {
const list = Applications.query(text || "");
if (list[0]) {
App.toggleWindow(WINDOW_NAME);
launchApp(list[0]);
}
},
on_change: ({ text }) =>
items.map((item) => {
if (item.attribute) {
const { app, revealer } = item.attribute;
revealer.reveal_child = app.match(text);
}
}),
});
return Widget.Box({
vertical: true,
children: [
entry,
Widget.Scrollable({
hscroll: "never",
child: list,
}),
],
setup: (self) =>
self.hook(App, (_, win, visible) => {
if (win !== WINDOW_NAME) return;
entry.text = "-";
entry.text = "";
if (visible) {
entry.grab_focus();
} else {
items = mkItems();
list.children = items;
}
}),
});
};
export default () =>
PopupWindow({
name: WINDOW_NAME,
transition: "slide_down",
child: Applauncher(),
anchor: options.applauncher.anchor.bind("value"),
});

View File

@@ -0,0 +1,34 @@
/**
* @typedef {Object} PanelButtonProps
* @property {import('types/widgets/button').ButtonProps['child']} content
* @property {string=} window
*/
/**
* @param {import('types/widgets/button').ButtonProps & PanelButtonProps} o
*/
export default ({ class_name, content, window = "", setup, ...rest }) =>
Widget.Button({
class_name: `panel-button ${class_name}`,
child: Widget.Box({ children: [content] }),
setup: (self) => {
let open = false;
self.hook(App, (_, win, visible) => {
if (win !== window) return;
if (open && !visible) {
open = false;
self.toggleClassName("active", false);
}
if (visible) {
open = true;
self.toggleClassName("active");
}
});
if (setup) setup(self);
},
...rest,
});

View File

@@ -0,0 +1,113 @@
import SystemTray from "resource:///com/github/Aylur/ags/service/systemtray.js";
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import Mpris from "resource:///com/github/Aylur/ags/service/mpris.js";
import Battery from "resource:///com/github/Aylur/ags/service/battery.js";
import OverviewButton from "./buttons/OverviewButton.js";
import Workspaces from "./buttons/Workspaces.js";
import FocusedClient from "./buttons/FocusedClient.js";
import MediaIndicator from "./buttons/MediaIndicator.js";
import DateButton from "./buttons/DateButton.js";
import NotificationIndicator from "./buttons/NotificationIndicator.js";
import SysTray from "./buttons/SysTray.js";
import ColorPicker from "./buttons/ColorPicker.js";
import SystemIndicators from "./buttons/SystemIndicators.js";
import PowerMenu from "./buttons/PowerMenu.js";
import ScreenRecord from "./buttons/ScreenRecord.js";
import BatteryBar from "./buttons/BatteryBar.js";
import SubMenu from "./buttons/SubMenu.js";
import Recorder from "../services/screenrecord.js";
// import * as System from './buttons/System.js';
// import Taskbar from './buttons/Taskbar.js';
import options from "../options.js";
const submenuItems = Variable(1);
SystemTray.connect("changed", () => {
submenuItems.setValue(SystemTray.items.length + 1);
});
/**
* @template {import('types/service').default} T
* @param {T=} service
* @param {(service: T) => boolean=} condition
*/
const SeparatorDot = (service, condition) =>
Widget.Separator({
vpack: "center",
setup: (self) => {
const visibility = () => {
if (!options.bar.separators.value) return (self.visible = false);
self.visible =
condition && service
? condition(service)
: options.bar.separators.value;
};
if (service && condition) self.hook(service, visibility);
self.on("draw", visibility);
self.bind("visible", options.bar.separators);
},
});
const Start = () =>
Widget.Box({
class_name: "start",
children: [
OverviewButton(),
SeparatorDot(),
Workspaces(),
SeparatorDot(),
FocusedClient(),
Widget.Box({ hexpand: true }),
NotificationIndicator(),
SeparatorDot(Notifications, (n) => n.notifications.length > 0 || n.dnd),
],
});
const Center = () =>
Widget.Box({
class_name: "center",
children: [DateButton()],
});
const End = () =>
Widget.Box({
class_name: "end",
children: [
SeparatorDot(Mpris, (m) => m.players.length > 0),
MediaIndicator(),
Widget.Box({ hexpand: true }),
SubMenu({
items: submenuItems,
children: [SysTray(), ColorPicker()],
}),
SeparatorDot(),
ScreenRecord(),
SeparatorDot(Recorder, (r) => r.recording),
SystemIndicators(),
SeparatorDot(Battery, (b) => b.available),
SeparatorDot(),
PowerMenu(),
],
});
/** @param {number} monitor */
export default (monitor) =>
Widget.Window({
name: `bar${monitor}`,
class_name: "transparent",
exclusivity: "exclusive",
monitor,
anchor: options.bar.position
.bind("value")
.transform((pos) => [pos, "left", "right"]),
child: Widget.CenterBox({
class_name: "panel",
start_widget: Start(),
center_widget: Center(),
end_widget: End(),
}),
});

View File

@@ -0,0 +1,89 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Battery from "resource:///com/github/Aylur/ags/service/battery.js";
import icons from "../../icons.js";
import FontIcon from "../../misc/FontIcon.js";
import options from "../../options.js";
import PanelButton from "../PanelButton.js";
const Indicator = () =>
Widget.Stack({
children: {
false: Widget.Icon({ icon: Battery.bind("icon_name") }),
true: FontIcon(icons.battery.charging),
},
visible: options.battery.bar.show_icon.bind("value"),
setup: (self) =>
self.hook(Battery, () => {
self.shown = `${Battery.charging || Battery.charged}`;
}),
});
const PercentLabel = () =>
Widget.Revealer({
transition: "slide_right",
reveal_child: options.battery.show_percentage.bind("value"),
child: Widget.Label({
label: Battery.bind("percent").transform((p) => `${p}%`),
}),
});
const LevelBar = () =>
Widget.LevelBar({
value: Battery.bind("percent").transform((p) => p / 100),
setup: (self) =>
self.hook(options.battery.bar.full, () => {
const full = options.battery.bar.full.value;
self.vpack = full ? "fill" : "center";
self.hpack = full ? "fill" : "center";
}),
});
const WholeButton = () =>
Widget.Overlay({
class_name: "whole-button",
child: LevelBar(),
pass_through: true,
overlays: [
Widget.Box({
hpack: "center",
children: [
FontIcon({
icon: icons.battery.charging,
visible: Battery.bind("charging"),
}),
Widget.Box({
hpack: "center",
vpack: "center",
child: PercentLabel(),
}),
],
}),
],
});
export default () =>
PanelButton({
class_name: "battery-bar",
on_clicked: () => {
const v = options.battery.show_percentage.value;
options.battery.show_percentage.value = !v;
},
content: Widget.Box({
visible: Battery.bind("available"),
children: options.battery.bar.full
.bind("value")
.transform((full) =>
full ? [WholeButton()] : [Indicator(), PercentLabel(), LevelBar()],
),
setup: (self) =>
self.hook(Battery, (w) => {
w.toggleClassName("charging", Battery.charging || Battery.charged);
w.toggleClassName(
"medium",
Battery.percent < options.battery.medium.value,
);
w.toggleClassName("low", Battery.percent < options.battery.low.value);
w.toggleClassName("half", Battery.percent < 48);
}),
}),
});

View File

@@ -0,0 +1,26 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Colors from "../../services/colorpicker.js";
import PanelButton from "../PanelButton.js";
import Gdk from "gi://Gdk";
export default () =>
PanelButton({
class_name: "color-picker",
content: Widget.Icon("color-select-symbolic"),
tooltip_text: Colors.bind("colors").transform((v) => `${v.length} colors`),
on_clicked: () => Colors.pick(),
on_secondary_click: (btn) => {
if (Colors.colors.length === 0) return;
Widget.Menu({
class_name: "colorpicker",
children: Colors.colors.map((color) =>
Widget.MenuItem({
child: Widget.Label(color),
css: `background-color: ${color}`,
on_activate: () => Colors.wlCopy(color),
}),
),
}).popup_at_widget(btn, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null);
},
});

View File

@@ -0,0 +1,11 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import Clock from "../../misc/Clock.js";
import PanelButton from "../PanelButton.js";
export default ({ format = "%R - %x" } = {}) =>
PanelButton({
class_name: "dashboard panel-button",
on_clicked: () => App.toggleWindow("dashboard"),
window: "dashboard",
content: Clock({ format }),
});

View File

@@ -0,0 +1,44 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import PanelButton from "../PanelButton.js";
import options from "../../options.js";
import { substitute } from "../../utils.js";
export const ClientLabel = () =>
Widget.Label({
label: Hyprland.active.client.bind("class").transform((c) => {
const { titles } = options.substitutions;
return substitute(titles, c);
}),
});
export const ClientIcon = () =>
Widget.Icon({
setup: (self) =>
self.hook(Hyprland.active.client, () => {
const { icons } = options.substitutions;
const { client } = Hyprland.active;
const classIcon = substitute(icons, client.class) + "-symbolic";
const titleIcon = substitute(icons, client.class) + "-symbolic";
const hasTitleIcon = Utils.lookUpIcon(titleIcon);
const hasClassIcon = Utils.lookUpIcon(classIcon);
if (hasClassIcon) self.icon = classIcon;
if (hasTitleIcon) self.icon = titleIcon;
self.visible = !!(hasTitleIcon || hasClassIcon);
}),
});
export default () =>
PanelButton({
class_name: "focused-client",
content: Widget.Box({
tooltip_text: Hyprland.active.bind("client").transform((c) => c.title),
children: [ClientIcon(), ClientLabel()],
}),
});

View File

@@ -0,0 +1,74 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Mpris from "resource:///com/github/Aylur/ags/service/mpris.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import HoverRevealer from "../../misc/HoverRevealer.js";
import * as mpris from "../../misc/mpris.js";
import options from "../../options.js";
export const getPlayer = (name = options.mpris.preferred.value) =>
Mpris.getPlayer(name) || Mpris.players[0] || null;
/**
* @param {Object} o
* @param {import('types/service/mpris').MprisPlayer} o.player
* @param {import('../../misc/HoverRevealer').HoverRevealProps['direction']=} o.direction
*/
const Indicator = ({ player, direction = "right" }) =>
HoverRevealer({
class_name: `media panel-button ${player.name}`,
direction,
on_primary_click: () => player.playPause(),
on_scroll_up: () => player.next(),
on_scroll_down: () => player.previous(),
on_secondary_click: () => player.playPause(),
indicator: mpris.PlayerIcon(player),
child: Widget.Label({
vexpand: true,
truncate: "end",
max_width_chars: 40,
label: player
.bind("track_title")
.transform(
() => `${player.track_artists.join(", ")} - ${player.track_title}`,
),
}),
setupRevealer: (self) => {
let current = "";
self.hook(player, () => {
if (current === player.track_title) return;
current = player.track_title;
self.reveal_child = true;
Utils.timeout(3000, () => {
self.reveal_child = false;
});
});
},
});
/**
* @param {Object} o
* @param {import('../../misc/HoverRevealer').HoverRevealProps['direction']=} o.direction
*/
export default ({ direction = "right" } = {}) => {
let current = null;
const update = (box) => {
const player = getPlayer();
box.visible = !!player;
if (!player) {
current = null;
return;
}
if (current === player) return;
current = player;
box.children = [Indicator({ player, direction })];
};
return Widget.Box()
.hook(options.mpris.preferred, update)
.hook(Mpris, update, "notify::players");
};

View File

@@ -0,0 +1,50 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import icons from "../../icons.js";
import HoverRevealer from "../../misc/HoverRevealer.js";
/**
* @param {Object} o
* @param {import('../../misc/HoverRevealer').HoverRevealProps['direction']=} o.direction
*/
export default ({ direction = "left" } = {}) =>
HoverRevealer({
class_name: "notifications panel-button",
setupEventBox: (box) =>
box
.on("button-press-event", () => App.openWindow("dashboard"))
.hook(
Notifications,
() =>
(box.visible =
Notifications.notifications.length > 0 || Notifications.dnd),
),
setupRevealer: (self) =>
self.hook(Notifications, () => {
let title = "";
const summary = Notifications.notifications[0]?.summary;
if (title === summary) return;
title = summary;
self.reveal_child = true;
Utils.timeout(3000, () => {
self.reveal_child = false;
});
}),
direction,
indicator: Widget.Icon({
icon: Notifications.bind("dnd").transform(
(dnd) => icons.notifications[dnd ? "silent" : "noisy"],
),
}),
child: Widget.Label({
truncate: "end",
max_width_chars: 40,
label: Notifications.bind("notifications").transform(
(n) => n.reverse()[0]?.summary || "",
),
}),
});

View File

@@ -0,0 +1,17 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import PanelButton from "../PanelButton.js";
import FontIcon from "../../misc/FontIcon.js";
import { distroIcon } from "../../variables.js";
import options from "../../options.js";
export default () =>
PanelButton({
class_name: "overview",
window: "overview",
on_clicked: () => App.toggleWindow("overview"),
content: FontIcon({
label: options.bar.icon.bind("value").transform((v) => {
return v === "distro-icon" ? distroIcon : v;
}),
}),
});

View File

@@ -0,0 +1,11 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import icons from "../../icons.js";
import PanelButton from "../PanelButton.js";
export default () =>
PanelButton({
class_name: "powermenu",
content: Widget.Icon(icons.powermenu.shutdown),
on_clicked: () => App.openWindow("powermenu"),
});

View File

@@ -0,0 +1,23 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import PanelButton from "../PanelButton.js";
import Recorder from "../../services/screenrecord.js";
import icons from "../../icons.js";
export default () =>
PanelButton({
class_name: "recorder",
on_clicked: () => Recorder.stop(),
visible: Recorder.bind("recording"),
content: Widget.Box({
children: [
Widget.Icon(icons.recorder.recording),
Widget.Label({
label: Recorder.bind("timer").transform((time) => {
const sec = time % 60;
const min = Math.floor(time / 60);
return `${min}:${sec < 10 ? "0" + sec : sec}`;
}),
}),
],
}),
});

View File

@@ -0,0 +1,65 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Variable from "resource:///com/github/Aylur/ags/variable.js";
import icons from "../../icons.js";
import options from "../../options.js";
/**
* @param {import('types/widgets/revealer').default} revealer
* @param {'left' | 'right' | 'up' | 'down'} direction
* @param {import('types/variable').Variable<number>} items
*/
const Arrow = (revealer, direction, items) => {
let deg = 0;
const icon = Widget.Icon({
icon: icons.ui.arrow[direction],
});
const animate = () => {
const t = options.transition.value / 20;
const step = revealer.reveal_child ? 10 : -10;
for (let i = 0; i < 18; ++i) {
Utils.timeout(t * i, () => {
deg += step;
icon.setCss(`-gtk-icon-transform: rotate(${deg}deg);`);
});
}
};
return Widget.Button({
class_name: "panel-button sub-menu",
tooltip_text: items.bind().transform((v) => `${v} Items`),
on_clicked: () => {
animate();
revealer.reveal_child = !revealer.reveal_child;
},
child: icon,
});
};
/**
* @param {Object} o
* @param {import('types/widgets/box').default['children']} o.children
* @param {'left' | 'right' | 'up' | 'down'=} o.direction
* @param {import('types/variable').Variable} o.items
*/
export default ({ children, direction = "left", items = Variable(0) }) => {
const posStart = direction === "up" || direction === "left";
const posEnd = direction === "down" || direction === "right";
const revealer = Widget.Revealer({
transition: `slide_${direction}`,
child: Widget.Box({
children,
}),
});
return Widget.Box({
vertical: direction === "up" || direction === "down",
children: [
posStart && revealer,
Arrow(revealer, direction, items),
posEnd && revealer,
],
});
};

View File

@@ -0,0 +1,44 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import SystemTray from "resource:///com/github/Aylur/ags/service/systemtray.js";
import PanelButton from "../PanelButton.js";
import Gdk from "gi://Gdk";
/** @param {import('types/service/systemtray').TrayItem} item */
const SysTrayItem = (item) =>
PanelButton({
class_name: "tray-item",
content: Widget.Icon({ icon: item.bind("icon") }),
tooltip_markup: item.bind("tooltip_markup"),
setup: (self) => {
const id = item.menu?.connect("popped-up", (menu) => {
self.toggleClassName("active");
menu.connect("notify::visible", (menu) => {
self.toggleClassName("active", menu.visible);
});
menu.disconnect(id);
});
if (id) self.connect("destroy", () => item.menu?.disconnect(id));
},
// @ts-expect-error popup_at_widget missing from types?
on_primary_click: (btn) =>
item.menu?.popup_at_widget(
btn,
Gdk.Gravity.SOUTH,
Gdk.Gravity.NORTH,
null,
),
// @ts-expect-error popup_at_widget missing from types?
on_secondary_click: (btn) =>
item.menu?.popup_at_widget(
btn,
Gdk.Gravity.SOUTH,
Gdk.Gravity.NORTH,
null,
),
});
export default () =>
Widget.Box().bind("children", SystemTray, "items", (i) => i.map(SysTrayItem));

View File

@@ -0,0 +1,50 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import PanelButton from "../PanelButton.js";
import * as variables from "../../variables.js";
import icons from "../../icons.js";
/** @param {'cpu' | 'ram'} type */
const System = (type) => {
const icon = Widget.Icon({
class_name: "icon",
icon: icons.system[type],
});
const progress = Widget.Box({
class_name: "progress",
child: Widget.CircularProgress({
value: variables[type].bind(),
}),
});
const revealer = Widget.Revealer({
transition: "slide_right",
child: Widget.Label({
label: variables[type].bind("value").transform((v) => {
return ` ${type}: ${Math.round(v * 100)}%`;
}),
}),
});
return PanelButton({
class_name: `system ${type}`,
on_clicked: () => (revealer.reveal_child = !revealer.reveal_child),
content: Widget.EventBox({
on_hover: () => (revealer.reveal_child = true),
on_hover_lost: () => (revealer.reveal_child = false),
child: Widget.Box({
children: [
icon,
Widget.Box({
class_name: "revealer",
child: revealer,
}),
progress,
],
}),
}),
});
};
export const CPU = () => System("cpu");
export const RAM = () => System("ram");

View File

@@ -0,0 +1,134 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import Bluetooth from "resource:///com/github/Aylur/ags/service/bluetooth.js";
import Audio from "resource:///com/github/Aylur/ags/service/audio.js";
import Network from "resource:///com/github/Aylur/ags/service/network.js";
import HoverRevealer from "../../misc/HoverRevealer.js";
import PanelButton from "../PanelButton.js";
import Asusctl from "../../services/asusctl.js";
import Indicator from "../../services/onScreenIndicator.js";
import icons from "../../icons.js";
import FontIcon from "../../misc/FontIcon.js";
const ProfileIndicator = () =>
Widget.Icon()
.bind("visible", Asusctl, "profile", (p) => p !== "Balanced")
.bind("icon", Asusctl, "profile", (i) => icons.asusctl.profile[i]);
const ModeIndicator = () =>
FontIcon()
.bind("visible", Asusctl, "mode", (m) => m !== "Hybrid")
.bind("icon", Asusctl, "mode", (i) => icons.asusctl.mode[i]);
const MicrophoneIndicator = () =>
Widget.Icon().hook(
Audio,
(icon) => {
if (!Audio.microphone) return;
const { muted, low, medium, high } = icons.audio.mic;
/** @type {Array<[number, string]>} */
const cons = [
[67, high],
[34, medium],
[1, low],
[0, muted],
];
icon.icon =
cons.find(([n]) => n <= Audio.microphone.volume * 100)?.[1] || "";
icon.visible = Audio.recorders.length > 0 || Audio.microphone.is_muted;
},
"microphone-changed",
);
const DNDIndicator = () =>
Widget.Icon({
visible: Notifications.bind("dnd"),
icon: icons.notifications.silent,
});
const BluetoothDevicesIndicator = () =>
Widget.Box().hook(
Bluetooth,
(box) => {
box.children = Bluetooth.connectedDevices.map(({ iconName, name }) =>
HoverRevealer({
indicator: Widget.Icon(iconName + "-symbolic"),
child: Widget.Label(name),
}),
);
box.visible = Bluetooth.connectedDevices.length > 0;
},
"notify::connected-devices",
);
const BluetoothIndicator = () =>
Widget.Icon({
class_name: "bluetooth",
icon: icons.bluetooth.enabled,
visible: Bluetooth.bind("enabled"),
});
const NetworkIndicator = () =>
Widget.Icon().hook(Network, (self) => {
const icon = Network[Network.primary || "wifi"]?.iconName;
self.icon = icon || "";
self.visible = !!icon;
});
const AudioIndicator = () =>
Widget.Icon().hook(
Audio,
(self) => {
if (!Audio.speaker) return;
const { muted, low, medium, high, overamplified } = icons.audio.volume;
if (Audio.speaker.is_muted) return (self.icon = muted);
/** @type {Array<[number, string]>} */
const cons = [
[101, overamplified],
[67, high],
[34, medium],
[1, low],
[0, muted],
];
self.icon =
cons.find(([n]) => n <= Audio.speaker.volume * 100)?.[1] || "";
},
"speaker-changed",
);
export default () =>
PanelButton({
class_name: "quicksettings panel-button",
on_clicked: () => App.toggleWindow("quicksettings"),
setup: (self) =>
self.hook(App, (_, win, visible) => {
self.toggleClassName("active", win === "quicksettings" && visible);
}),
on_scroll_up: () => {
Audio.speaker.volume += 0.02;
Indicator.speaker();
},
on_scroll_down: () => {
Audio.speaker.volume -= 0.02;
Indicator.speaker();
},
content: Widget.Box({
children: [
Asusctl?.available && ProfileIndicator(),
Asusctl?.available && ModeIndicator(),
DNDIndicator(),
BluetoothDevicesIndicator(),
BluetoothIndicator(),
NetworkIndicator(),
AudioIndicator(),
MicrophoneIndicator(),
],
}),
});

View File

@@ -0,0 +1,31 @@
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import Applications from "resource:///com/github/Aylur/ags/service/applications.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import PanelButton from "../PanelButton.js";
import { launchApp } from "../../utils.js";
import icons from "../../icons.js";
const focus = ({ address }) =>
Hyprland.sendMessage(`dispatch focuswindow address:${address}`);
/** @param {import('types/widgets/box').default} box */
const setChildren = (box) =>
(box.children = Hyprland.clients.map((client) => {
if (Hyprland.active.workspace.id !== client.workspace.id) return;
for (const app of Applications.list) {
if (client.class && app.match(client.class)) {
return PanelButton({
content: Widget.Icon(app.icon_name || icons.fallback.executable),
tooltip_text: app.name,
on_primary_click: () => focus(client),
on_middle_click: () => launchApp(app),
});
}
}
}));
export default () =>
Widget.Box()
.hook(Hyprland, setChildren, "notify::clients")
.hook(Hyprland, setChildren, "notify::active");

View File

@@ -0,0 +1,58 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import options from "../../options.js";
import { range } from "../../utils.js";
/** @param {any} arg */
const dispatch = (arg) => Utils.execAsync(`hyprctl dispatch workspace ${arg}`);
const Workspaces = () => {
const ws = options.workspaces.value;
return Widget.Box({
children: range(ws || 20).map((i) =>
Widget.Button({
attribute: i,
on_clicked: () => dispatch(i),
child: Widget.Label({
label: `${i}`,
class_name: "indicator",
vpack: "center",
}),
setup: (self) =>
self.hook(Hyprland, () => {
self.toggleClassName("active", Hyprland.active.workspace.id === i);
self.toggleClassName(
"occupied",
(Hyprland.getWorkspace(i)?.windows || 0) > 0,
);
}),
}),
),
setup: (box) => {
if (ws === 0) {
box.hook(Hyprland.active.workspace, () =>
box.children.map((btn) => {
btn.visible = Hyprland.workspaces.some(
(ws) => ws.id === btn.attribute,
);
}),
);
}
},
});
};
export default () =>
Widget.EventBox({
class_name: "workspaces panel-button",
child: Widget.Box({
// its nested like this to keep it consistent with other PanelButton widgets
child: Widget.EventBox({
on_scroll_up: () => dispatch("m+1"),
on_scroll_down: () => dispatch("m-1"),
class_name: "eventbox",
child: options.workspaces.bind("value").transform(Workspaces),
}),
}),
});

View File

@@ -0,0 +1,26 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import DateColumn from "./DateColumn.js";
import NotificationColumn from "./NotificationColumn.js";
import PopupWindow from "../misc/PopupWindow.js";
import options from "../options.js";
export default () =>
PopupWindow({
name: "dashboard",
setup: (self) =>
self.hook(options.bar.position, () => {
self.anchor = [options.bar.position.value];
if (options.bar.position.value === "top")
self.transition = "slide_down";
if (options.bar.position.value === "bottom")
self.transition = "slide_up";
}),
child: Widget.Box({
children: [
NotificationColumn(),
Widget.Separator({ orientation: 1 }),
DateColumn(),
],
}),
});

View File

@@ -0,0 +1,63 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import icons from "../icons.js";
import Clock from "../misc/Clock.js";
import * as vars from "../variables.js";
import options from "../options.js";
/**
* @param {'cpu' | 'ram' | 'temp'} type
* @param {string} title
* @param {string} unit
*/
const SysProgress = (type, title, unit) =>
Widget.Box({
class_name: `circular-progress-box ${type}`,
hexpand: true,
tooltip_text: vars[type]
.bind("value")
.transform((v) => `${title}: ${Math.floor(v * 100)}${unit}`),
child: Widget.CircularProgress({
hexpand: true,
class_name: `circular-progress ${type}`,
child: Widget.Icon(icons.system[type]),
start_at: 0.75,
value: vars[type].bind(),
rounded: options.radii.bind("value").transform((v) => v > 0),
}),
});
export default () =>
Widget.Box({
vertical: true,
class_name: "datemenu vertical",
children: [
Widget.Box({
class_name: "clock-box",
vertical: true,
children: [
Clock({ format: "%H:%M" }),
Widget.Label({
class_name: "uptime",
label: vars.uptime.bind("value").transform((t) => `uptime: ${t}`),
}),
],
}),
Widget.Box({
class_name: "calendar",
children: [
Widget.Calendar({
hexpand: true,
hpack: "center",
}),
],
}),
Widget.Box({
class_name: "system-info horizontal",
children: [
SysProgress("cpu", "Cpu", "%"),
SysProgress("ram", "Ram", "%"),
SysProgress("temp", "Temperature", "°"),
],
}),
],
});

View File

@@ -0,0 +1,81 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import icons from "../icons.js";
import Notification from "../misc/Notification.js";
import { timeout } from "resource:///com/github/Aylur/ags/utils.js";
const ClearButton = () =>
Widget.Button({
on_clicked: () => {
const list = Array.from(Notifications.notifications);
for (let i = 0; i < list.length; i++)
timeout(50 * i, () => list[i]?.close());
},
sensitive: Notifications.bind("notifications").transform(
(n) => n.length > 0,
),
child: Widget.Box({
children: [
Widget.Label("Clear "),
Widget.Icon({
icon: Notifications.bind("notifications").transform(
(n) => icons.trash[n.length > 0 ? "full" : "empty"],
),
}),
],
}),
});
const Header = () =>
Widget.Box({
class_name: "header",
children: [
Widget.Label({ label: "Notifications", hexpand: true, xalign: 0 }),
ClearButton(),
],
});
const NotificationList = () =>
Widget.Box({
vertical: true,
vexpand: true,
children: Notifications.bind("notifications").transform((n) =>
n.reverse().map(Notification),
),
visible: Notifications.bind("notifications").transform((n) => n.length > 0),
});
const Placeholder = () =>
Widget.Box({
class_name: "placeholder",
vertical: true,
vpack: "center",
hpack: "center",
vexpand: true,
hexpand: true,
visible: Notifications.bind("notifications").transform(
(n) => n.length === 0,
),
children: [
Widget.Icon(icons.notifications.silent),
Widget.Label("Your inbox is empty"),
],
});
export default () =>
Widget.Box({
class_name: "notifications",
vertical: true,
children: [
Header(),
Widget.Scrollable({
vexpand: true,
class_name: "notification-scrollable",
child: Widget.Box({
class_name: "notification-list",
vertical: true,
children: [NotificationList(), Placeholder()],
}),
}),
],
});

View File

@@ -0,0 +1,69 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Clock from "../misc/Clock.js";
import DesktopMenu from "./DesktopMenu.js";
import options from "../options.js";
const DesktopClock = () =>
Widget.Box({
class_name: "clock-box-shadow",
child: Widget.CenterBox({
class_name: "clock-box",
start_widget: Clock({
class_name: "clock",
hpack: "center",
format: "%H",
}),
center_widget: Widget.Box({
class_name: "separator-box",
vertical: true,
hexpand: true,
hpack: "center",
children: [
Widget.Separator({ vpack: "center", vexpand: true }),
Widget.Separator({ vpack: "center", vexpand: true }),
],
}),
end_widget: Clock({
class_name: "clock",
hpack: "center",
format: "%M",
}),
}),
});
const Desktop = () =>
Widget.EventBox({
on_secondary_click: (_, event) => DesktopMenu().popup_at_pointer(event),
child: Widget.Box({
vertical: true,
vexpand: true,
hexpand: true,
visible: options.desktop.clock.enable.bind("value"),
setup: (self) =>
self.hook(options.desktop.clock.position, () => {
const [hpack = "center", vpack = "center", offset = 64] =
options.desktop.clock.position.value.split(" ") || [];
// @ts-expect-error
self.hpack = hpack;
self.vpack = vpack;
self.setCss(`margin: ${Number(offset)}px;`);
}),
children: [
DesktopClock(),
Clock({ format: "%B %e. %A", class_name: "date" }),
],
}),
});
/** @param {number} monitor */
export default (monitor) =>
Widget.Window({
monitor,
keymode: "on-demand",
name: `desktop${monitor}`,
layer: "background",
class_name: "desktop",
anchor: ["top", "bottom", "left", "right"],
child: Desktop(),
});

View File

@@ -0,0 +1,66 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import PowerMenu from "../services/powermenu.js";
import icons from "../icons.js";
import Gtk from "gi://Gtk";
import { openSettings } from "../settings/theme.js";
/**
* @param {string} label
* @param {string} icon
* @param {import('types/widgets/menu').MenuItemProps['on_activate']} on_activate
*/
const Item = (label, icon, on_activate) =>
Widget.MenuItem({
on_activate,
child: Widget.Box({
children: [
Widget.Icon(icon),
Widget.Label({
label,
hexpand: true,
xalign: 0,
}),
],
}),
});
export default () =>
Widget.Menu({
class_name: "desktop-menu",
children: [
Widget.MenuItem({
child: Widget.Box({
children: [
Widget.Icon(icons.powermenu.shutdown),
Widget.Label({
label: "System",
hexpand: true,
xalign: 0,
}),
],
}),
submenu: Widget.Menu({
children: [
Item("Shutdown", icons.powermenu.shutdown, () =>
PowerMenu.action("shutdown"),
),
Item("Log Out", icons.powermenu.logout, () =>
PowerMenu.action("logout"),
),
Item("Reboot", icons.powermenu.reboot, () =>
PowerMenu.action("reboot"),
),
Item("Sleep", icons.powermenu.sleep, () =>
PowerMenu.action("reboot"),
),
],
}),
}),
Item("Applications", icons.apps.apps, () =>
App.openWindow("applauncher"),
),
new Gtk.SeparatorMenuItem(),
Item("Settings", icons.ui.settings, openSettings),
],
});

View File

@@ -0,0 +1,142 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import Applications from "resource:///com/github/Aylur/ags/service/applications.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import icons from "../icons.js";
import options from "../options.js";
import { launchApp, range } from "../utils.js";
const focus = ({ address }) =>
Hyprland.sendMessage(`dispatch focuswindow address:${address}`);
/** @param {import('types/widgets/button').ButtonProps & { icon: string, pinned?: boolean }} o */
const AppButton = ({ icon, pinned = false, ...rest }) => {
const indicators = Widget.Box({
vpack: "end",
hpack: "center",
children: range(5, 0).map(() =>
Widget.Box({
class_name: "indicator",
visible: false,
}),
),
});
return Widget.Button({
...rest,
attribute: indicators,
child: Widget.Box({
class_name: "box",
child: Widget.Overlay({
child: Widget.Icon({
icon,
size: options.desktop.dock.icon_size.bind("value"),
}),
pass_through: true,
overlays: pinned ? [indicators] : [],
}),
}),
});
};
const Taskbar = () =>
Widget.Box({
children: Hyprland.bind("clients").transform((c) =>
c.map((client) => {
for (const appName of options.desktop.dock.pinned_apps.value) {
if (client.class.toLowerCase().includes(appName.toLowerCase()))
return null;
}
for (const app of Applications.list) {
if (
(client.title && app.match(client.title)) ||
(client.class && app.match(client.class))
) {
return AppButton({
icon: app.icon_name || "",
tooltip_text: app.name,
on_primary_click: () => focus(client),
on_middle_click: () => launchApp(app),
});
}
}
}),
),
});
const PinnedApps = () =>
Widget.Box({
class_name: "pins",
homogeneous: true,
children: options.desktop.dock.pinned_apps.bind("value").transform((v) =>
v
.map((term) => ({ app: Applications.query(term)?.[0], term }))
.filter(({ app }) => app)
.map(({ app, term }) =>
AppButton({
pinned: true,
icon: app.icon_name || "",
on_primary_click: () => {
for (const client of Hyprland.clients) {
if (client.class.toLowerCase().includes(term))
return focus(client);
}
launchApp(app);
},
on_middle_click: () => launchApp(app),
tooltip_text: app.name,
setup: (button) =>
button.hook(Hyprland, () => {
const running = Hyprland.clients.filter((client) =>
client.class.toLowerCase().includes(term),
);
const focused = running.find(
(client) => client.address === Hyprland.active.client.address,
);
const index = running.findIndex((c) => c === focused);
for (let i = 0; i < 5; ++i) {
const indicator = button.attribute.children[i];
indicator.visible = i < running.length;
indicator.toggleClassName("focused", i === index);
}
button.set_tooltip_text(
running.length === 1 ? running[0].title : app.name,
);
}),
}),
),
),
});
export default () => {
const pinnedapps = PinnedApps();
const taskbar = Taskbar();
const applauncher = AppButton({
class_name: "launcher nonrunning",
icon: icons.apps.apps,
tooltip_text: "Applications",
on_clicked: () => App.toggleWindow("applauncher"),
});
const separator = Widget.Separator({
vpack: "center",
hpack: "center",
orientation: 1,
setup: (self) =>
self.hook(
taskbar,
() => {
self.visible = taskbar.children.length > 0;
},
"notify::children",
),
});
return Widget.Box({
class_name: "dock",
children: [applauncher, pinnedapps, separator, taskbar],
});
};

View File

@@ -0,0 +1,44 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import Dock from "./Dock.js";
import options from "../options.js";
/** @param {number} monitor */
export default (monitor) => {
const revealer = Widget.Revealer({
transition: "slide_up",
child: Dock(),
setup: (self) => {
const update = () => {
const ws = Hyprland.getWorkspace(Hyprland.active.workspace.id);
if (Hyprland.getMonitor(monitor)?.name === ws?.monitor)
self.reveal_child = ws?.windows === 0;
};
self
.hook(Hyprland, update, "client-added")
.hook(Hyprland, update, "client-removed")
.hook(Hyprland.active.workspace, update);
},
});
return Widget.Window({
monitor,
name: `dock${monitor}`,
class_name: "floating-dock",
anchor: ["bottom"],
child: Widget.Box({
children: [
revealer,
Widget.Box({
class_name: "padding",
css: "padding: 2px;",
}),
],
}),
setup: (self) =>
self
.on("enter-notify-event", () => (revealer.reveal_child = true))
.on("leave-notify-event", () => (revealer.reveal_child = false))
.bind("visible", options.bar.position, "value", (v) => v !== "bottom"),
});
};

View File

@@ -0,0 +1,122 @@
export default {
lock: "system-lock-screen-symbolic",
fallback: {
executable: "application-x-executable-symbolic",
},
audio: {
mic: {
muted: "microphone-disabled-symbolic",
low: "microphone-sensitivity-low-symbolic",
medium: "microphone-sensitivity-medium-symbolic",
high: "microphone-sensitivity-high-symbolic",
},
volume: {
muted: "audio-volume-muted-symbolic",
low: "audio-volume-low-symbolic",
medium: "audio-volume-medium-symbolic",
high: "audio-volume-high-symbolic",
overamplified: "audio-volume-overamplified-symbolic",
},
type: {
headset: "audio-headphones-symbolic",
speaker: "audio-speakers-symbolic",
card: "audio-card-symbolic",
},
mixer: "",
},
asusctl: {
profile: {
Balanced: "power-profile-balanced-symbolic",
Quiet: "power-profile-power-saver-symbolic",
Performance: "power-profile-performance-symbolic",
},
mode: {
Integrated: "",
Hybrid: "󰢮",
},
},
apps: {
apps: "view-app-grid-symbolic",
search: "folder-saved-search-symbolic",
},
battery: {
charging: "󱐋",
warning: "battery-empty-symbolic",
},
bluetooth: {
enabled: "bluetooth-active-symbolic",
disabled: "bluetooth-disabled-symbolic",
},
brightness: {
indicator: "display-brightness-symbolic",
keyboard: "keyboard-brightness-symbolic",
screen: "display-brightness-symbolic",
},
powermenu: {
sleep: "weather-clear-night-symbolic",
reboot: "system-reboot-symbolic",
logout: "system-log-out-symbolic",
shutdown: "system-shutdown-symbolic",
},
recorder: {
recording: "media-record-symbolic",
},
notifications: {
noisy: "preferences-system-notifications-symbolic",
silent: "notifications-disabled-symbolic",
},
trash: {
full: "user-trash-full-symbolic",
empty: "user-trash-symbolic",
},
mpris: {
fallback: "audio-x-generic-symbolic",
shuffle: {
enabled: "󰒟",
disabled: "󰒟",
},
loop: {
none: "󰓦",
track: "󰓦",
playlist: "󰑐",
},
playing: "󰏦",
paused: "󰐍",
stopped: "󰐍",
prev: "󰒮",
next: "󰒭",
},
ui: {
colorpicker: "color-select-symbolic",
close: "window-close-symbolic",
info: "info-symbolic",
menu: "open-menu-symbolic",
link: "external-link-symbolic",
settings: "emblem-system-symbolic",
tick: "object-select-symbolic",
arrow: {
right: "pan-end-symbolic",
left: "pan-start-symbolic",
down: "pan-down-symbolic",
up: "pan-up-symbolic",
},
},
system: {
cpu: "org.gnome.SystemMonitor-symbolic",
ram: "drive-harddisk-solidstate-symbolic",
temp: "temperature-symbolic",
},
dialog: {
Search: "",
Applauncher: "󰵆",
Bar: "",
Border: "󰃇",
Color: "󰏘",
Desktop: "",
Font: "",
General: "󰒓",
Miscellaneous: "󰠱",
Theme: "󰃟",
Notifications: "󰂚 ",
},
};

View File

@@ -0,0 +1,63 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Avatar from "../misc/Avatar.js";
import Lockscreen from "../services/lockscreen.js";
import Layer from "gi://GtkLayerShell";
const PasswordEntry = () =>
Widget.Box({
children: [
Widget.Entry({
setup: (self) => self.hook(Lockscreen, () => (self.text = ""), "lock"),
visibility: false,
placeholder_text: "Password",
on_accept: ({ text }) => Lockscreen.auth(text || ""),
hpack: "center",
hexpand: true,
}),
Widget.Spinner({
active: true,
vpack: "center",
setup: (self) =>
self.hook(
Lockscreen,
(_, auth) => (self.visible = auth),
"authenticating",
),
}),
],
});
/** @param {number} monitor */
export default (monitor) => {
const win = Widget.Window({
name: `lockscreen${monitor}`,
class_name: "lockscreen",
monitor,
layer: "overlay",
visible: false,
setup: (self) =>
self.hook(Lockscreen, (_, lock) => (self.visible = lock), "lock"),
child: Widget.Box({
css: "min-width: 3000px; min-height: 2000px;",
class_name: "shader",
child: Widget.Box({
class_name: "content",
vertical: true,
hexpand: true,
vexpand: true,
hpack: "center",
vpack: "center",
children: [
Avatar({
hpack: "center",
vpack: "center",
}),
PasswordEntry(),
],
}),
}),
});
Layer.set_keyboard_mode(win, Layer.KeyboardMode.EXCLUSIVE);
return win;
};

View File

@@ -0,0 +1,7 @@
#! /usr/bin/env python
import pam
import sys
import getpass
print(pam.authenticate(getpass.getuser(), sys.argv[1]));

View File

@@ -0,0 +1,43 @@
import Dashboard from "./dashboard/Dashboard.js";
import Desktop from "./desktop/Desktop.js";
import Lockscreen from "./lockscreen/Lockscreen.js";
import Notifications from "./notifications/Notifications.js";
import OSD from "./osd/OSD.js";
import Overview from "./overview/Overview.js";
import PowerMenu from "./powermenu/PowerMenu.js";
import QuickSettings from "./quicksettings/QuickSettings.js";
import ScreenCorners from "./screencorner/ScreenCorners.js";
import TopBar from "./bar/TopBar.js";
import Verification from "./powermenu/Verification.js";
import About from "./about/about.js";
import { init } from "./settings/setup.js";
import { forMonitors } from "./utils.js";
import { initWallpaper } from "./settings/wallpaper.js";
import options from "./options.js";
initWallpaper();
const windows = () => [
forMonitors(Desktop),
forMonitors(Lockscreen),
forMonitors(Notifications),
forMonitors(OSD),
forMonitors(ScreenCorners),
forMonitors(TopBar),
Dashboard(),
Overview(),
PowerMenu(),
QuickSettings(),
Verification(),
About(),
];
export default {
onConfigParsed: init,
windows: windows().flat(1),
closeWindowDelay: {
quicksettings: options.transition.value,
dashboard: options.transition.value,
},
};

View File

@@ -0,0 +1,16 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import options from "../options.js";
/** @param {import('types/widgets/box').BoxProps=} props */
export default (props) =>
Widget.Box({ ...props, class_name: "avatar" })
.hook(options.desktop.avatar, (box) =>
box.setCss(`
background-image: url('${options.desktop.avatar.value}');
background-size: cover;
`),
)
.on("size-allocate", (box) => {
const h = box.get_allocated_height();
box.set_size_request(Math.ceil(h * 1.1), -1);
});

View File

@@ -0,0 +1,14 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Battery from "resource:///com/github/Aylur/ags/service/battery.js";
export default () =>
Widget.Icon({
class_name: "battery",
icon: Battery.bind("icon_name"),
setup: (icon) =>
icon.hook(Battery, () => {
icon.toggleClassName("charging", Battery.charging);
icon.toggleClassName("charged", Battery.charged);
icon.toggleClassName("low", Battery.percent < 30);
}),
});

View File

@@ -0,0 +1,17 @@
import { clock } from "../variables.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
/**
* @param {import('types/widgets/label').Props & {
* format?: string,
* interval?: number,
* }} o
*/
export default ({ format = "%H:%M:%S %B %e. %A", ...rest } = {}) =>
Widget.Label({
class_name: "clock",
label: clock.bind("value").transform((time) => {
return time.format(format) || "wrong format";
}),
...rest,
});

View File

@@ -0,0 +1,48 @@
import Gtk from "gi://Gtk?version=3.0";
import { subclass, register } from "resource:///com/github/Aylur/ags/widget.js";
import AgsLabel from "resource:///com/github/Aylur/ags/widgets/label.js";
class FontIcon extends AgsLabel {
static {
register(this);
}
/** @param {string | import('types/widgets/label').Props<any> & { icon?: string }} params */
constructor(params = "") {
// @ts-expect-error
const { icon = "", ...rest } = params;
super(typeof params === "string" ? {} : rest);
this.toggleClassName("font-icon");
if (typeof params === "object") this.icon = icon;
if (typeof params === "string") this.icon = params;
}
get icon() {
return this.label;
}
set icon(icon) {
this.label = icon;
}
get size() {
return this.get_style_context().get_property(
"font-size",
Gtk.StateFlags.NORMAL,
);
}
/** @returns {[number, number]} */
vfunc_get_preferred_height() {
return [this.size, this.size];
}
/** @returns {[number, number]} */
vfunc_get_preferred_width() {
return [this.size, this.size];
}
}
export default subclass(FontIcon);

View File

@@ -0,0 +1,62 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
/**
* @typedef {import('types/widgets/eventbox').EventBoxProps & {
* indicator?: import('types/widgets/box').BoxProps['child']
* direction?: 'left' | 'right' | 'down' | 'up'
* duration?: number
* setupRevealer?: (rev: ReturnType<typeof Widget.Revealer>) => void
* setupEventBox?: (rev: ReturnType<typeof Widget.EventBox>) => void
* }} HoverRevealProps
*/
/**
* @param {HoverRevealProps} props
*/
export default ({
indicator,
child,
direction = "left",
duration = 300,
setupEventBox,
setupRevealer,
...rest
}) => {
let open = false;
const vertical = direction === "down" || direction === "up";
const posStart = direction === "down" || direction === "right";
const posEnd = direction === "up" || direction === "left";
const revealer = Widget.Revealer({
transition: `slide_${direction}`,
setup: setupRevealer,
transition_duration: duration,
child,
});
const eventbox = Widget.EventBox({
...rest,
setup: setupEventBox,
on_hover: () => {
if (open) return;
revealer.reveal_child = true;
Utils.timeout(duration, () => (open = true));
},
on_hover_lost: () => {
if (!open) return;
revealer.reveal_child = false;
open = false;
},
child: Widget.Box({
vertical,
children: [posStart && indicator, revealer, posEnd && indicator],
}),
});
return Widget.Box({
children: [eventbox],
});
};

View File

@@ -0,0 +1,63 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import RegularWindow from "./RegularWindow.js";
import Gtk from "gi://Gtk";
export default () => {
const selected = Widget.Label({
css: "font-size: 1.2em;",
});
const flowbox = Widget.FlowBox({
min_children_per_line: 10,
setup: (self) => {
self.connect("child-activated", (_, child) => {
selected.label = child.get_child().iconName;
});
Gtk.IconTheme.get_default()
.list_icons(null)
.sort()
.map((icon) => {
!icon.endsWith(".symbolic") &&
self.insert(
Widget.Icon({
icon,
size: 38,
}),
-1,
);
});
self.show_all();
},
});
const entry = Widget.Entry({
on_change: ({ text }) =>
flowbox.get_children().forEach((child) => {
child.visible = child.get_child().iconName.includes(text);
}),
});
return RegularWindow({
name: "icons",
visible: true,
child: Widget.Box({
css: "padding: 30px;",
spacing: 20,
vertical: true,
children: [
entry,
Widget.Scrollable({
hscroll: "never",
vscroll: "always",
hexpand: true,
vexpand: true,
css: "min-width: 500px;" + "min-height: 500px;",
child: flowbox,
}),
selected,
],
}),
});
};

View File

@@ -0,0 +1,132 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import GLib from "gi://GLib";
/** @param {import('types/service/notifications').Notification} n */
const NotificationIcon = ({ app_entry, app_icon, image }) => {
if (image) {
return Widget.Box({
vpack: "start",
hexpand: false,
class_name: "icon img",
css: `
background-image: url("${image}");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
min-width: 78px;
min-height: 78px;
`,
});
}
let icon = "dialog-information-symbolic";
if (Utils.lookUpIcon(app_icon)) icon = app_icon;
if (Utils.lookUpIcon(app_entry || "")) icon = app_entry || "";
return Widget.Box({
vpack: "start",
hexpand: false,
class_name: "icon",
css: `
min-width: 78px;
min-height: 78px;
`,
child: Widget.Icon({
icon,
size: 58,
hpack: "center",
hexpand: true,
vpack: "center",
vexpand: true,
}),
});
};
/** @param {import('types/service/notifications').Notification} notification */
export default (notification) => {
const content = Widget.Box({
class_name: "content",
children: [
NotificationIcon(notification),
Widget.Box({
hexpand: true,
vertical: true,
children: [
Widget.Box({
children: [
Widget.Label({
class_name: "title",
xalign: 0,
justification: "left",
hexpand: true,
max_width_chars: 24,
truncate: "end",
wrap: true,
label: notification.summary,
use_markup: true,
}),
Widget.Label({
class_name: "time",
vpack: "start",
label: GLib.DateTime.new_from_unix_local(
notification.time,
).format("%H:%M"),
}),
Widget.Button({
class_name: "close-button",
vpack: "start",
child: Widget.Icon("window-close-symbolic"),
on_clicked: () => notification.close(),
}),
],
}),
Widget.Label({
class_name: "description",
hexpand: true,
use_markup: true,
xalign: 0,
justification: "left",
label: notification.body,
wrap: true,
}),
],
}),
],
});
const actionsbox = Widget.Revealer({
transition: "slide_down",
child: Widget.EventBox({
child: Widget.Box({
class_name: "actions horizontal",
children: notification.actions.map((action) =>
Widget.Button({
class_name: "action-button",
on_clicked: () => notification.invoke(action.id),
hexpand: true,
child: Widget.Label(action.label),
}),
),
}),
}),
});
return Widget.EventBox({
class_name: `notification ${notification.urgency}`,
vexpand: false,
on_primary_click: () => notification.dismiss(),
on_hover() {
actionsbox.reveal_child = true;
},
on_hover_lost() {
actionsbox.reveal_child = true;
notification.dismiss();
},
child: Widget.Box({
vertical: true,
children: [content, notification.actions.length > 0 && actionsbox],
}),
});
};

View File

@@ -0,0 +1,83 @@
import AgsWindow from "resource:///com/github/Aylur/ags/widgets/window.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import options from "../options.js";
import GObject from "gi://GObject";
const keyGrabber = Widget.Window({
name: "key-grabber",
popup: true,
anchor: ["top", "left", "right", "bottom"],
css: "background-color: transparent;",
visible: false,
exclusivity: "ignore",
keymode: "on-demand",
layer: "top",
attribute: { list: [] },
setup: (self) =>
self.on("notify::visible", ({ visible }) => {
if (!visible)
self.attribute?.list.forEach((name) => App.closeWindow(name));
}),
child: Widget.EventBox({ vexpand: true }).on("button-press-event", () => {
App.closeWindow("key-grabber");
keyGrabber.attribute?.list.forEach((name) => App.closeWindow(name));
}),
});
// add before any PopupWindow is instantiated
App.addWindow(keyGrabber);
export class PopupWindow extends AgsWindow {
static {
GObject.registerClass(this);
}
constructor({ name, child, transition = "none", visible = false, ...rest }) {
super({
...rest,
name,
popup: true,
keymode: "exclusive",
layer: "overlay",
class_names: ["popup-window", name],
});
child.toggleClassName("window-content");
this.revealer = Widget.Revealer({
transition,
child,
transition_duration: options.transition.value,
setup: (self) =>
self.hook(App, (_, wname, visible) => {
if (wname === name) this.revealer.reveal_child = visible;
}),
});
this.child = Widget.Box({
css: "padding: 1px;",
child: this.revealer,
});
this.show_all();
this.visible = visible;
keyGrabber.bind("visible", this, "visible");
keyGrabber.attribute?.list.push(name);
}
set transition(dir) {
this.revealer.transition = dir;
}
get transition() {
return this.revealer.transition;
}
}
/** @param {import('types/widgets/window').WindowProps & {
* name: string
* child: import('types/widgets/box').default
* transition?: import('types/widgets/revealer').RevealerProps['transition']
* }} config
*/
export default (config) => new PopupWindow(config);

View File

@@ -0,0 +1,60 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
/** @param {import('types/widgets/box').BoxProps & {
* width: number
* height: number
* }} o */
export default ({
height = 18,
width = 180,
vertical = false,
child,
...props
}) => {
const fill = Widget.Box({
class_name: "fill",
hexpand: vertical,
vexpand: !vertical,
hpack: vertical ? "fill" : "start",
vpack: vertical ? "end" : "fill",
children: [child],
});
let fill_size = 0;
return Widget.Box({
...props,
class_name: "progress",
css: `
min-width: ${width}px;
min-height: ${height}px;
`,
children: [fill],
attribute: (value) => {
if (value < 0) return;
const axis = vertical ? "height" : "width";
const axisv = vertical ? height : width;
const min = vertical ? width : height;
const preferred = (axisv - min) * value + min;
if (!fill_size) {
fill_size = preferred;
fill.setCss(`min-${axis}: ${preferred}px;`);
return;
}
const frames = 10;
const goal = preferred - fill_size;
const step = goal / frames;
for (let i = 0; i < frames; ++i) {
Utils.timeout(5 * i, () => {
fill_size += step;
fill.setCss(`min-${axis}: ${fill_size}px`);
});
}
},
});
};

View File

@@ -0,0 +1,4 @@
import { subclass } from "resource:///com/github/Aylur/ags/widget.js";
import Gtk from "gi://Gtk";
export default subclass(Gtk.Window, "RegularWindow");

View File

@@ -0,0 +1,263 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import icons from "../icons.js";
import { blurImg } from "../utils.js";
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/box').BoxProps=} props
*/
export const CoverArt = (player, props) =>
Widget.Box({
...props,
class_name: "cover",
css: player
.bind("cover_path")
.transform((p) => `background-image: url("${p}")`),
});
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/box').BoxProps=} props
*/
export const BlurredCoverArt = (player, props) =>
Widget.Box({
...props,
class_name: "blurred-cover",
setup: (self) =>
self.hook(
player,
(box) =>
blurImg(player.cover_path).then((img) => {
img && box.setCss(`background-image: url("${img}")`);
}),
"notify::cover-path",
),
});
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/label').Props=} props
*/
export const TitleLabel = (player, props) =>
Widget.Label({
...props,
class_name: "title",
label: player.bind("track_title"),
});
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/label').Props=} props
*/
export const ArtistLabel = (player, props) =>
Widget.Label({
...props,
class_name: "artist",
label: player.bind("track_artists").transform((a) => a.join(", ") || ""),
});
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/icon').Props & { symbolic?: boolean }=} props
*/
export const PlayerIcon = (player, { symbolic = true, ...props } = {}) =>
Widget.Icon({
...props,
class_name: "player-icon",
tooltip_text: player.identity || "",
setup: (self) =>
self.hook(player, (icon) => {
const name = `${player.entry}${symbolic ? "-symbolic" : ""}`;
Utils.lookUpIcon(name)
? (icon.icon = name)
: (icon.icon = icons.mpris.fallback);
}),
});
/**
* @param {import('types/service/mpris').MprisPlayer} player
* @param {import('types/widgets/slider').SliderProps=} props
*/
export const PositionSlider = (player, props) =>
Widget.Slider({
...props,
class_name: "position-slider",
draw_value: false,
on_change: ({ value }) => (player.position = player.length * value),
setup: (self) => {
const update = () => {
if (self.dragging) return;
self.visible = player.length > 0;
if (player.length > 0) self.value = player.position / player.length;
};
self.hook(player, update);
self.hook(player, update, "position");
self.poll(1000, update);
},
});
/** @param {number} length */
function lengthStr(length) {
const min = Math.floor(length / 60);
const sec = Math.floor(length % 60);
const sec0 = sec < 10 ? "0" : "";
return `${min}:${sec0}${sec}`;
}
/** @param {import('types/service/mpris').MprisPlayer} player */
export const PositionLabel = (player) =>
Widget.Label({
setup: (self) => {
const update = (_, time) => {
player.length > 0
? (self.label = lengthStr(time || player.position))
: (self.visible = !!player);
};
self.hook(player, update, "position");
self.poll(1000, update);
},
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const LengthLabel = (player) =>
Widget.Label({
label: player.bind("length").transform((l) => lengthStr(l)),
visible: player.bind("length").transform((l) => l > 0),
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const Slash = (player) =>
Widget.Label({
label: "/",
visible: player.bind("length").transform((l) => l > 0),
});
/**
* @param {Object} o
* @param {import('types/service/mpris').MprisPlayer} o.player
* @param {import('types/widgets/stack').StackProps['children']} o.children
* @param {'shuffle' | 'loop' | 'playPause' | 'previous' | 'next'} o.onClick
* @param {string} o.prop
* @param {string} o.canProp
* @param {any} o.cantValue
*/
const PlayerButton = ({
player,
children,
onClick,
prop,
canProp,
cantValue,
}) =>
Widget.Button({
child: Widget.Stack({ children }).bind(
"shown",
player,
prop,
(p) => `${p}`,
),
on_clicked: () => player[onClick](),
visible: player.bind(canProp).transform((c) => c !== cantValue),
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const ShuffleButton = (player) =>
PlayerButton({
player,
children: {
true: Widget.Label({
class_name: "shuffle enabled",
label: icons.mpris.shuffle.enabled,
}),
false: Widget.Label({
class_name: "shuffle disabled",
label: icons.mpris.shuffle.disabled,
}),
},
onClick: "shuffle",
prop: "shuffle-status",
canProp: "shuffle-status",
cantValue: null,
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const LoopButton = (player) =>
PlayerButton({
player,
children: {
None: Widget.Label({
class_name: "loop none",
label: icons.mpris.loop.none,
}),
Track: Widget.Label({
class_name: "loop track",
label: icons.mpris.loop.track,
}),
Playlist: Widget.Label({
class_name: "loop playlist",
label: icons.mpris.loop.playlist,
}),
},
onClick: "loop",
prop: "loop-status",
canProp: "loop-status",
cantValue: null,
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const PlayPauseButton = (player) =>
PlayerButton({
player,
children: {
Playing: Widget.Label({
class_name: "playing",
label: icons.mpris.playing,
}),
Paused: Widget.Label({
class_name: "paused",
label: icons.mpris.paused,
}),
Stopped: Widget.Label({
class_name: "stopped",
label: icons.mpris.stopped,
}),
},
onClick: "playPause",
prop: "play-back-status",
canProp: "can-play",
cantValue: false,
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const PreviousButton = (player) =>
PlayerButton({
player,
children: {
true: Widget.Label({
class_name: "previous",
label: icons.mpris.prev,
}),
},
onClick: "previous",
prop: "can-go-prev",
canProp: "can-go-prev",
cantValue: false,
});
/** @param {import('types/service/mpris').MprisPlayer} player */
export const NextButton = (player) =>
PlayerButton({
player,
children: {
true: Widget.Label({
class_name: "next",
label: icons.mpris.next,
}),
},
onClick: "next",
prop: "can-go-next",
canProp: "can-go-next",
cantValue: false,
});

View File

@@ -0,0 +1,68 @@
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Notification from "../misc/Notification.js";
import options from "../options.js";
/** @param {import('types/widgets/revealer').default} parent */
const Popups = (parent) => {
const map = new Map();
const onDismissed = (_, id, force = false) => {
if (!id || !map.has(id)) return;
if (map.get(id).isHovered() && !force) return;
if (map.size - 1 === 0) parent.reveal_child = false;
Utils.timeout(200, () => {
map.get(id)?.destroy();
map.delete(id);
});
};
/** @param {import('types/widgets/box').default} box */
const onNotified = (box, id) => {
if (!id || Notifications.dnd) return;
const n = Notifications.getNotification(id);
if (!n) return;
if (options.notifications.black_list.value.includes(n.app_name || ""))
return;
map.delete(id);
map.set(id, Notification(n));
box.children = Array.from(map.values()).reverse();
Utils.timeout(10, () => {
parent.reveal_child = true;
});
};
return Widget.Box({ vertical: true })
.hook(Notifications, onNotified, "notified")
.hook(Notifications, onDismissed, "dismissed")
.hook(Notifications, (box, id) => onDismissed(box, id, true), "closed");
};
/** @param {import('types/widgets/revealer').RevealerProps['transition']} transition */
const PopupList = (transition = "slide_down") =>
Widget.Box({
css: "padding: 1px",
children: [
Widget.Revealer({
transition,
setup: (self) => (self.child = Popups(self)),
}),
],
});
/** @param {number} monitor */
export default (monitor) =>
Widget.Window({
monitor,
name: `notifications${monitor}`,
class_name: "notifications",
anchor: options.notifications.position.bind("value"),
child: PopupList(),
});

View File

@@ -0,0 +1,303 @@
/**
* An object holding Options that are Variables with cached values.
*
* to update an option at runtime simply run
* ags -r "options.path.to.option.setValue('value')"
*
* resetting:
* ags -r "options.reset()"
*/
import {
Option,
resetOptions,
getValues,
apply,
getOptions,
} from "./settings/option.js";
import { USER } from "resource:///com/github/Aylur/ags/utils.js";
import themes from "./themes.js";
export default {
reset: resetOptions,
values: getValues,
apply: apply,
list: getOptions,
spacing: Option(9),
padding: Option(8),
radii: Option(9),
popover_padding_multiplier: Option(1.4, {
category: "General",
note: "popover-padding: padding × this",
type: "float",
unit: "",
}),
color: {
red: Option("#e55f86", { scss: "red" }),
green: Option("#00D787", { scss: "green" }),
yellow: Option("#EBFF71", { scss: "yellow" }),
blue: Option("#51a4e7", { scss: "blue" }),
magenta: Option("#9077e7", { scss: "magenta" }),
teal: Option("#51e6e6", { scss: "teal" }),
orange: Option("#E79E64", { scss: "orange" }),
},
theme: {
name: Option(themes[0].name, {
category: "exclude",
note: "Name to show as active in quicktoggles",
}),
icon: Option(themes[0].icon, {
category: "exclude",
note: "Icon to show as active in quicktoggles",
}),
scheme: Option("dark", {
enums: ["dark", "light"],
type: "enum",
note: "Color scheme to set on Gtk apps: 'ligth' or 'dark'",
title: "Color Scheme",
scss: "color-scheme",
}),
bg: Option("#171717", {
title: "Background Color",
scss: "bg-color",
}),
fg: Option("#eeeeee", {
title: "Foreground Color",
scss: "fg-color",
}),
accent: {
accent: Option("$blue", {
category: "Theme",
title: "Accent Color",
scss: "accent",
}),
fg: Option("#141414", {
category: "Theme",
title: "Accent Foreground Color",
scss: "accent-fg",
}),
gradient: Option("to right, $accent, lighten($accent, 6%)", {
category: "Theme",
title: "Accent Linear Gradient",
scss: "accent-gradient",
}),
},
widget: {
bg: Option("$fg-color", {
category: "Theme",
title: "Widget Background Color",
scss: "_widget-bg",
}),
opacity: Option(94, {
category: "Theme",
title: "Widget Background Opacity",
unit: "",
scss: "widget-opacity",
}),
},
},
border: {
color: Option("$fg-color", {
category: "Border",
title: "Border Color",
scss: "_border-color",
}),
opacity: Option(97, {
category: "Border",
title: "Border Opacity",
unit: "",
}),
width: Option(1, {
category: "Border",
title: "Border Width",
}),
},
hypr: {
inactive_border: Option("rgba(333333ff)", {
category: "Border",
title: "Border on Inactive Windows",
scss: "exclude",
}),
wm_gaps_multiplier: Option(2.4, {
category: "General",
scss: "wm-gaps-multiplier",
note: "wm-gaps: padding × this",
type: "float",
unit: "",
}),
},
// TODO: use this on revealers
transition: Option(200, {
category: "exclude",
note: "Transition time on aminations in ms, e.g on hover",
unit: "ms",
}),
font: {
font: Option("Ubuntu Nerd Font", {
type: "font",
title: "Font",
scss: "font",
}),
mono: Option("Mononoki Nerd Font", {
title: "Monospaced Font",
scss: "mono-font",
}),
size: Option(13, {
scss: "font-size",
unit: "pt",
}),
},
applauncher: {
width: Option(500),
height: Option(500),
icon_size: Option(52),
},
bar: {
position: Option("top", {
enums: ["top", "bottom"],
type: "enum",
}),
style: Option("normal", {
enums: ["floating", "normal", "separated"],
type: "enum",
}),
flat_buttons: Option(true, { scss: "bar-flat-buttons" }),
separators: Option(true),
icon: Option("distro-icon", {
note: '"distro-icon" or a single font',
}),
},
battery: {
show_percentage: Option(true, {
persist: true,
noReload: false,
category: "exclude",
}),
bar: {
show_icon: Option(true, { category: "Bar" }),
width: Option(70, { category: "Bar" }),
height: Option(14, { category: "Bar" }),
full: Option(false, { category: "Bar" }),
},
low: Option(30, { category: "Bar" }),
medium: Option(50, { category: "Bar" }),
},
desktop: {
wallpaper: {
fg: Option("#fff", { scss: "wallpaper-fg" }),
img: Option(themes[0].options["desktop.wallpaper.img"], {
scssFormat: (v) => `"${v}"`,
type: "img",
}),
},
avatar: Option(`/var/lib/AccountsService/icons/${USER}`, {
scssFormat: (v) => `"${v}"`,
type: "img",
note: "displayed in quicksettings and locksreen",
}),
screen_corners: Option(true, { scss: "screen-corners" }),
clock: {
enable: Option(true),
position: Option("center center", {
note: "halign valign",
}),
},
drop_shadow: Option(true, { scss: "drop-shadow" }),
shadow: Option("rgba(0, 0, 0, .6)", { scss: "shadow" }),
dock: {
icon_size: Option(56),
pinned_apps: Option(
[
"firefox",
"org.wezfurlong.wezterm",
"org.gnome.Nautilus",
"org.gnome.Calendar",
"obsidian",
"transmission-gtk",
"caprine",
"teams-for-linux",
"discord",
"spotify",
"com.usebottles.bottles",
"org.gnome.Software",
],
{ scss: "exclude" },
),
},
},
notifications: {
black_list: Option(["Spotify"], { note: "app-name | entry" }),
position: Option(["top"], { note: "anchor" }),
width: Option(450),
},
dashboard: {
sys_info_size: Option(70, {
category: "Desktop",
scss: "sys-info-size",
}),
},
mpris: {
black_list: Option(["Caprine"], {
category: "Bar",
title: "List of blacklisted mpris players",
note: "filters for bus-name, name, identity, entry",
}),
preferred: Option("spotify", {
category: "Bar",
title: "Preferred player",
}),
},
workspaces: Option(10, {
category: "Bar",
title: "No. workspaces on bar and overview",
note: "Set it to 0 to make it dynamic",
}),
temperature: "/sys/class/thermal/thermal_zone0/temp",
systemFetchInterval: 5000,
brightnessctlKBD: "asus::kbd_backlight",
substitutions: {
icons: [
["transmission-gtk", "transmission"],
["blueberry.py", "bluetooth"],
["Caprine", "facebook-messenger"],
["", "preferences-desktop-display"],
],
titles: [
["com.github.Aylur.ags", "AGS"],
["transmission-gtk", "Transmission"],
["com.obsproject.Studio", "OBS"],
["com.usebottles.bottles", "Bottles"],
["com.github.wwmm.easyeffects", "Easy Effects"],
["org.gnome.TextEditor", "Text Editor"],
["org.gnome.design.IconLibrary", "Icon Library"],
["blueberry.py", "Blueberry"],
["org.wezfurlong.wezterm", "Wezterm"],
["com.raggesilver.BlackBox", "BlackBox"],
["firefox", "Firefox"],
["org.gnome.Nautilus", "Files"],
["libreoffice-writer", "Writer"],
["", "Desktop"],
],
},
};

View File

@@ -0,0 +1,60 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import FontIcon from "../misc/FontIcon.js";
import Progress from "../misc/Progress.js";
import Indicator from "../services/onScreenIndicator.js";
export const OnScreenIndicator = ({ height = 300, width = 48 } = {}) =>
Widget.Box({
class_name: "indicator",
css: "padding: 1px;",
child: Widget.Revealer({
transition: "slide_left",
setup: (self) =>
self.hook(Indicator, (_, value) => {
self.reveal_child = value > -1;
}),
child: Progress({
width,
height,
vertical: true,
setup: (self) =>
self.hook(Indicator, (_, value) => self.attribute(value)),
child: Widget.Stack({
vpack: "start",
hpack: "center",
hexpand: false,
children: {
true: Widget.Icon({
hpack: "center",
size: width,
setup: (w) =>
w.hook(Indicator, (_, _v, name) => (w.icon = name || "")),
}),
false: FontIcon({
hpack: "center",
hexpand: true,
css: `font-size: ${width}px;`,
setup: (w) =>
w.hook(Indicator, (_, _v, name) => (w.label = name || "")),
}),
},
setup: (self) =>
self.hook(Indicator, (_, _v, name) => {
self.shown = `${!!Utils.lookUpIcon(name)}`;
}),
}),
}),
}),
});
/** @param {number} monitor */
export default (monitor) =>
Widget.Window({
name: `indicator${monitor}`,
monitor,
class_name: "indicator",
layer: "overlay",
anchor: ["right"],
child: OnScreenIndicator(),
});

View File

@@ -0,0 +1,57 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import { createSurfaceFromWidget, substitute } from "../utils.js";
import Gdk from "gi://Gdk";
import Gtk from "gi://Gtk?version=3.0";
import options from "../options.js";
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import icons from "../icons.js";
const SCALE = 0.08;
const TARGET = [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)];
/** @param {string} args */
const dispatch = (args) => Hyprland.sendMessage(`dispatch ${args}`);
/** @param {string} str */
const icon = (str) => {
const icon = substitute(options.substitutions.icons, str);
if (Utils.lookUpIcon(icon)) return icon;
console.warn("no icon", icon);
return icons.fallback.executable;
};
export default ({ address, size: [w, h], class: c, title }) =>
Widget.Button({
class_name: "client",
tooltip_text: `${title}`,
child: Widget.Icon({
css: `
min-width: ${w * SCALE}px;
min-height: ${h * SCALE}px;
`,
icon: icon(c),
}),
on_secondary_click: () => dispatch(`closewindow address:${address}`),
on_clicked: () =>
dispatch(`focuswindow address:${address}`).then(() =>
App.closeWindow("overview"),
),
setup: (btn) =>
btn
.on("drag-data-get", (_w, _c, data) =>
data.set_text(address, address.length),
)
.on("drag-begin", (_, context) => {
Gtk.drag_set_icon_surface(context, createSurfaceFromWidget(btn));
btn.toggleClassName("hidden", true);
})
.on("drag-end", () => btn.toggleClassName("hidden", false))
.drag_source_set(
Gdk.ModifierType.BUTTON1_MASK,
TARGET,
Gdk.DragAction.COPY,
),
});

View File

@@ -0,0 +1,52 @@
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import PopupWindow from "../misc/PopupWindow.js";
import Workspace from "./Workspace.js";
import options from "../options.js";
import { range } from "../utils.js";
const ws = options.workspaces;
const Overview = () =>
Widget.Box({
children: [Workspace(0)], // for type infer
setup: (self) =>
Utils.idle(() => {
self.hook(ws, () => {
self.children = range(ws.value).map(Workspace);
update(self);
children(self);
});
self.hook(Hyprland, update);
self.hook(Hyprland, children, "notify::workspaces");
update(self);
children(self);
}),
});
/** @param {ReturnType<typeof Overview>} box */
const update = (box) => {
if (!box.get_parent()?.visible) return;
Hyprland.sendMessage("j/clients")
.then((clients) => {
box.children.forEach((ws) => {
ws.attribute(JSON.parse(clients));
});
})
.catch(console.error);
};
/** @param {import('types/widgets/box').default} box */
const children = (box) => {
if (ws.value === 0) {
box.children = Hyprland.workspaces
.sort((ws1, ws2) => ws1.id - ws2.id)
.map(({ id }) => Workspace(id));
}
};
export default () =>
PopupWindow({
name: "overview",
child: Overview(),
});

View File

@@ -0,0 +1,60 @@
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";
import Gdk from "gi://Gdk";
import Gtk from "gi://Gtk?version=3.0";
import Client from "./Client.js";
const SCALE = 0.08;
const TARGET = [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)];
/** @param {string} args */
const dispatch = (args) => Utils.execAsync(`hyprctl dispatch ${args}`);
/** @param {number} index */
export default (index) => {
const fixed = Gtk.Fixed.new();
return Widget.Box({
class_name: "workspace",
vpack: "center",
css: `
min-width: ${3840 * SCALE}px;
min-height: ${2160 * SCALE}px;
`,
setup: (box) =>
box.hook(Hyprland, () => {
box.toggleClassName("active", Hyprland.active.workspace.id === index);
}),
child: Widget.EventBox({
hexpand: true,
vexpand: true,
on_primary_click: () => dispatch(`workspace ${index}`),
setup: (eventbox) => {
eventbox.drag_dest_set(
Gtk.DestDefaults.ALL,
TARGET,
Gdk.DragAction.COPY,
);
eventbox.connect("drag-data-received", (_w, _c, _x, _y, data) => {
dispatch(`movetoworkspacesilent ${index},address:${data.get_text()}`);
});
},
child: fixed,
}),
/** @param {Array<import('types/service/hyprland').Client>} clients */
attribute: (clients) => {
fixed.get_children().forEach((ch) => ch.destroy());
clients
.filter(({ workspace: { id } }) => id === index)
.forEach((c) => {
c.at[0] -= Hyprland.getMonitor(c.monitor)?.x || 0;
c.at[1] -= Hyprland.getMonitor(c.monitor)?.y || 0;
c.mapped && fixed.put(Client(c), c.at[0] * SCALE, c.at[1] * SCALE);
});
fixed.show_all();
},
});
};

View File

@@ -0,0 +1,31 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import icons from "../icons.js";
import PowerMenu from "../services/powermenu.js";
import ShadedPopup from "./ShadedPopup.js";
/**
* @param {'sleep' | 'reboot' | 'logout' | 'shutdown'} action
* @param {string} label
*/
const SysButton = (action, label) =>
Widget.Button({
on_clicked: () => PowerMenu.action(action),
child: Widget.Box({
vertical: true,
children: [Widget.Icon(icons.powermenu[action]), Widget.Label(label)],
}),
});
export default () =>
ShadedPopup({
name: "powermenu",
expand: true,
child: Widget.Box({
children: [
SysButton("sleep", "Sleep"),
SysButton("reboot", "Reboot"),
SysButton("logout", "Log Out"),
SysButton("shutdown", "Shutdown"),
],
}),
});

View File

@@ -0,0 +1,44 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
/** @param {string} windowName */
const Padding = (windowName) =>
Widget.EventBox({
class_name: "padding",
hexpand: true,
vexpand: true,
setup: (w) =>
w.on("button-press-event", () => App.toggleWindow(windowName)),
});
/**
* @template {import('gi://Gtk?version=3.0').default.Widget} T
* @param {import('types/widgets/window').WindowProps<T> & {
* name: string
* child: import('types/widgets/box').default
* }} o
*/
export default ({ name, child, ...rest }) =>
Widget.Window({
...rest,
class_names: ["popup-window", name],
name,
visible: false,
popup: true,
keymode: "on-demand",
setup() {
child.toggleClassName("window-content");
},
child: Widget.CenterBox({
class_name: "shader",
css: "min-width: 5000px; min-height: 3000px;",
start_widget: Padding(name),
end_widget: Padding(name),
center_widget: Widget.CenterBox({
vertical: true,
start_widget: Padding(name),
end_widget: Padding(name),
center_widget: child,
}),
}),
});

View File

@@ -0,0 +1,46 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import PowerMenu from "../services/powermenu.js";
import ShadedPopup from "./ShadedPopup.js";
export default () =>
ShadedPopup({
name: "verification",
expand: true,
child: Widget.Box({
vertical: true,
children: [
Widget.Box({
class_name: "text-box",
vertical: true,
children: [
Widget.Label({
class_name: "title",
label: PowerMenu.bind("title"),
}),
Widget.Label({
class_name: "desc",
label: "Are you sure?",
}),
],
}),
Widget.Box({
class_name: "buttons horizontal",
vexpand: true,
vpack: "end",
homogeneous: true,
children: [
Widget.Button({
child: Widget.Label("No"),
on_clicked: () => App.toggleWindow("verification"),
}),
Widget.Button({
child: Widget.Label("Yes"),
on_clicked: () => Utils.exec(PowerMenu.cmd),
}),
],
}),
],
}),
});

View File

@@ -0,0 +1,65 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Header from "./widgets/Header.js";
import PopupWindow from "../misc/PopupWindow.js";
import { Volume, Microhone, SinkSelector, AppMixer } from "./widgets/Volume.js";
import { NetworkToggle, WifiSelection } from "./widgets/Network.js";
import { BluetoothToggle, BluetoothDevices } from "./widgets/Bluetooth.js";
import { ThemeToggle, ThemeSelector } from "./widgets/Theme.js";
import { ProfileToggle, ProfileSelector } from "./widgets/AsusProfile.js";
import Media from "./widgets/Media.js";
import Brightness from "./widgets/Brightness.js";
import DND from "./widgets/DND.js";
import MicMute from "./widgets/MicMute.js";
import options from "../options.js";
const Row = (toggles = [], menus = []) =>
Widget.Box({
vertical: true,
children: [
Widget.Box({
class_name: "row horizontal",
children: toggles,
}),
...menus,
],
});
const Homogeneous = (toggles) =>
Widget.Box({
homogeneous: true,
children: toggles,
});
export default () =>
PopupWindow({
name: "quicksettings",
setup: (self) =>
self.hook(options.bar.position, () => {
self.anchor = ["right", options.bar.position.value];
if (options.bar.position.value === "top")
self.transition = "slide_down";
if (options.bar.position.value === "bottom")
self.transition = "slide_up";
}),
child: Widget.Box({
vertical: true,
children: [
Header(),
Widget.Box({
class_name: "sliders-box vertical",
vertical: true,
children: [
Row([Volume()], [SinkSelector(), AppMixer()]),
Microhone(),
Brightness(),
],
}),
Row(
[Homogeneous([ThemeToggle(), BluetoothToggle()]), MicMute()],
[ThemeSelector(), BluetoothDevices()],
),
Media(),
],
}),
});

View File

@@ -0,0 +1,134 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import Variable from "resource:///com/github/Aylur/ags/variable.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import icons from "../icons.js";
/** name of the currently opened menu */
export const opened = Variable("");
App.connect("window-toggled", (_, name, visible) => {
if (name === "quicksettings" && !visible)
Utils.timeout(500, () => (opened.value = ""));
});
/**
* @param {string} name - menu name
* @param {(() => void) | false=} activate
*/
export const Arrow = (name, activate) => {
let deg = 0;
let iconOpened = false;
const icon = Widget.Icon(icons.ui.arrow.right).hook(opened, () => {
if (
(opened.value === name && !iconOpened) ||
(opened.value !== name && iconOpened)
) {
const step = opened.value === name ? 10 : -10;
iconOpened = !iconOpened;
for (let i = 0; i < 9; ++i) {
Utils.timeout(15 * i, () => {
deg += step;
icon.setCss(`-gtk-icon-transform: rotate(${deg}deg);`);
});
}
}
});
return Widget.Button({
child: icon,
on_clicked: () => {
opened.value = opened.value === name ? "" : name;
if (typeof activate === "function") activate();
},
});
};
/**
* @param {Object} o
* @param {string} o.name - menu name
* @param {import('gi://Gtk').Gtk.Widget} o.icon
* @param {import('gi://Gtk').Gtk.Widget} o.label
* @param {() => void} o.activate
* @param {() => void} o.deactivate
* @param {boolean=} o.activateOnArrow
* @param {[import('gi://GObject').GObject.Object, () => boolean]} o.connection
*/
export const ArrowToggleButton = ({
name,
icon,
label,
activate,
deactivate,
activateOnArrow = true,
connection: [service, condition],
}) =>
Widget.Box({
class_name: "toggle-button",
setup: (self) =>
self.hook(service, () => {
self.toggleClassName("active", condition());
}),
children: [
Widget.Button({
child: Widget.Box({
hexpand: true,
class_name: "label-box horizontal",
children: [icon, label],
}),
on_clicked: () => {
if (condition()) {
deactivate();
if (opened.value === name) opened.value = "";
} else {
activate();
}
},
}),
Arrow(name, activateOnArrow && activate),
],
});
/**
* @param {Object} o
* @param {string} o.name - menu name
* @param {import('gi://Gtk').Gtk.Widget} o.icon
* @param {import('gi://Gtk').Gtk.Widget} o.title
* @param {import('gi://Gtk').Gtk.Widget[]} o.content
*/
export const Menu = ({ name, icon, title, content }) =>
Widget.Revealer({
transition: "slide_down",
reveal_child: opened.bind().transform((v) => v === name),
child: Widget.Box({
class_names: ["menu", name],
vertical: true,
children: [
Widget.Box({
class_name: "title horizontal",
children: [icon, title],
}),
Widget.Separator(),
...content,
],
}),
});
/**
* @param {Object} o
* @param {import('gi://Gtk').Gtk.Widget} o.icon
* @param {() => void} o.toggle
* @param {[import('gi://GObject').GObject.Object, () => boolean]} o.connection
*/
export const SimpleToggleButton = ({
icon,
toggle,
connection: [service, condition],
}) =>
Widget.Button({
class_name: "simple-toggle",
setup: (self) =>
self.hook(service, () => {
self.toggleClassName("active", condition());
}),
child: icon,
on_clicked: toggle,
});

View File

@@ -0,0 +1,61 @@
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import icons from "../../icons.js";
import Asusctl from "../../services/asusctl.js";
import { ArrowToggleButton, Menu } from "../ToggleButton.js";
export const ProfileToggle = () =>
ArrowToggleButton({
name: "asusctl-profile",
icon: Widget.Icon({
icon: Asusctl.bind("profile").transform((p) => icons.asusctl.profile[p]),
}),
label: Widget.Label({
label: Asusctl.bind("profile"),
}),
connection: [Asusctl, () => Asusctl.profile !== "Balanced"],
activate: () => Asusctl.setProfile("Quiet"),
deactivate: () => Asusctl.setProfile("Balanced"),
activateOnArrow: false,
});
export const ProfileSelector = () =>
Menu({
name: "asusctl-profile",
icon: Widget.Icon({
icon: Asusctl.bind("profile").transform((p) => icons.asusctl.profile[p]),
}),
title: Widget.Label("Profile Selector"),
content: [
Widget.Box({
vertical: true,
hexpand: true,
children: [
Widget.Box({
vertical: true,
children: Asusctl.profiles.map((prof) =>
Widget.Button({
on_clicked: () => Asusctl.setProfile(prof),
child: Widget.Box({
children: [
Widget.Icon(icons.asusctl.profile[prof]),
Widget.Label(prof),
],
}),
}),
),
}),
],
}),
Widget.Separator(),
Widget.Button({
on_clicked: () => Utils.execAsync("rog-control-center"),
child: Widget.Box({
children: [
Widget.Icon(icons.ui.settings),
Widget.Label("Rog Control Center"),
],
}),
}),
],
});

View File

@@ -0,0 +1,74 @@
import Bluetooth from "resource:///com/github/Aylur/ags/service/bluetooth.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import icons from "../../icons.js";
import { Menu, ArrowToggleButton } from "../ToggleButton.js";
export const BluetoothToggle = () =>
ArrowToggleButton({
name: "bluetooth",
icon: Widget.Icon({
icon: Bluetooth.bind("enabled").transform(
(p) => icons.bluetooth[p ? "enabled" : "disabled"],
),
}),
label: Widget.Label({
truncate: "end",
setup: (self) =>
self.hook(Bluetooth, () => {
if (!Bluetooth.enabled) return (self.label = "Disabled");
if (Bluetooth.connected_devices.length === 0)
return (self.label = "Not Connected");
if (Bluetooth.connected_devices.length === 1)
return (self.label = Bluetooth.connected_devices[0].alias);
self.label = `${Bluetooth.connected_devices.length} Connected`;
}),
}),
connection: [Bluetooth, () => Bluetooth.enabled],
deactivate: () => (Bluetooth.enabled = false),
activate: () => (Bluetooth.enabled = true),
});
/** @param {import('types/service/bluetooth').BluetoothDevice} device */
const DeviceItem = (device) =>
Widget.Box({
children: [
Widget.Icon(device.icon_name + "-symbolic"),
Widget.Label(device.name),
Widget.Label({
label: `${device.battery_percentage}%`,
visible: device.bind("battery_percentage").transform((p) => p > 0),
}),
Widget.Box({ hexpand: true }),
Widget.Spinner({
active: device.bind("connecting"),
visible: device.bind("connecting"),
}),
Widget.Switch({
active: device.connected,
visible: device.bind("connecting").transform((p) => !p),
setup: (self) =>
self.on("notify::active", () => {
device.setConnection(self.active);
}),
}),
],
});
export const BluetoothDevices = () =>
Menu({
name: "bluetooth",
icon: Widget.Icon(icons.bluetooth.disabled),
title: Widget.Label("Bluetooth"),
content: [
Widget.Box({
hexpand: true,
vertical: true,
children: Bluetooth.bind("devices").transform((ds) =>
ds.filter((d) => d.name).map(DeviceItem),
),
}),
],
});

View File

@@ -0,0 +1,24 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import icons from "../../icons.js";
import Brightness from "../../services/brightness.js";
const BrightnessSlider = () =>
Widget.Slider({
draw_value: false,
hexpand: true,
value: Brightness.bind("screen"),
on_change: ({ value }) => (Brightness.screen = value),
});
export default () =>
Widget.Box({
children: [
Widget.Button({
child: Widget.Icon(icons.brightness.indicator),
tooltip_text: Brightness.bind("screen").transform(
(v) => `Screen Brightness: ${Math.floor(v * 100)}%`,
),
}),
BrightnessSlider(),
],
});

View File

@@ -0,0 +1,15 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import icons from "../../icons.js";
import { SimpleToggleButton } from "../ToggleButton.js";
export default () =>
SimpleToggleButton({
icon: Widget.Icon({
icon: Notifications.bind("dnd").transform(
(dnd) => icons.notifications[dnd ? "silent" : "noisy"],
),
}),
toggle: () => (Notifications.dnd = !Notifications.dnd),
connection: [Notifications, () => Notifications.dnd],
});

View File

@@ -0,0 +1,50 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Battery from "resource:///com/github/Aylur/ags/service/battery.js";
import PowerMenu from "../../services/powermenu.js";
import Lockscreen from "../../services/lockscreen.js";
import Avatar from "../../misc/Avatar.js";
import icons from "../../icons.js";
import { openSettings } from "../../settings/theme.js";
import { uptime } from "../../variables.js";
import DND from "./DND.js";
export default () =>
Widget.Box({
class_name: "header horizontal",
children: [
Avatar(),
Widget.Box({
hpack: "end",
vpack: "center",
hexpand: true,
children: [
/*Widget.Box({
class_name: "battery horizontal",
children: [
Widget.Icon({ icon: Battery.bind("icon_name") }),
Widget.Label({
label: Battery.bind("percent").transform((p) => `${p}%`),
}),
],
}),*/
DND(),
Widget.Label({
class_name: "uptime",
label: uptime.bind().transform((v) => `up: ${v}`),
}),
Widget.Button({
on_clicked: openSettings,
child: Widget.Icon(icons.ui.settings),
}),
Widget.Button({
on_clicked: () => Lockscreen.lockscreen(),
child: Widget.Icon(icons.lock),
}),
Widget.Button({
on_clicked: () => PowerMenu.action("shutdown"),
child: Widget.Icon(icons.powermenu.shutdown),
}),
],
}),
],
});

View File

@@ -0,0 +1,91 @@
import Mpris from "resource:///com/github/Aylur/ags/service/mpris.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import * as mpris from "../../misc/mpris.js";
import options from "../../options.js";
/** @param {import('types/service/mpris').MprisPlayer} player */
const Footer = (player) =>
Widget.CenterBox({
class_name: "footer-box",
start_widget: Widget.Box({
class_name: "position",
children: [
mpris.PositionLabel(player),
mpris.Slash(player),
mpris.LengthLabel(player),
],
}),
center_widget: Widget.Box({
class_name: "controls",
children: [
mpris.ShuffleButton(player),
mpris.PreviousButton(player),
mpris.PlayPauseButton(player),
mpris.NextButton(player),
mpris.LoopButton(player),
],
}),
end_widget: mpris.PlayerIcon(player, {
symbolic: false,
hexpand: true,
hpack: "end",
}),
});
/** @param {import('types/service/mpris').MprisPlayer} player */
const TextBox = (player) =>
Widget.Box({
children: [
mpris.CoverArt(player, {
hpack: "end",
hexpand: false,
}),
Widget.Box({
hexpand: true,
vertical: true,
class_name: "labels",
children: [
mpris.TitleLabel(player, {
xalign: 0,
justification: "left",
wrap: true,
}),
mpris.ArtistLabel(player, {
xalign: 0,
justification: "left",
wrap: true,
}),
],
}),
],
});
/** @param {import('types/service/mpris').MprisPlayer} player */
const PlayerBox = (player) =>
Widget.Box({
class_name: `player ${player.name}`,
child: mpris.BlurredCoverArt(player, {
hexpand: true,
child: Widget.Box({
hexpand: true,
vertical: true,
children: [
TextBox(player),
mpris.PositionSlider(player),
Footer(player),
],
}),
}),
});
export default () =>
Widget.Box({
vertical: true,
class_name: "media vertical",
visible: Mpris.bind("players").transform((p) => p.length > 0),
children: Mpris.bind("players").transform((ps) =>
ps
.filter((p) => !options.mpris.black_list.value.includes(p.identity))
.map(PlayerBox),
),
});

View File

@@ -0,0 +1,19 @@
import Audio from "resource:///com/github/Aylur/ags/service/audio.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import icons from "../../icons.js";
import { SimpleToggleButton } from "../ToggleButton.js";
export default () =>
SimpleToggleButton({
icon: Widget.Icon().hook(
Audio,
(self) => {
self.icon = Audio.microphone?.is_muted
? icons.audio.mic.muted
: icons.audio.mic.high;
},
"microphone-changed",
),
toggle: () => (Audio.microphone.is_muted = !Audio.microphone.is_muted),
connection: [Audio, () => Audio.microphone?.is_muted || false],
});

View File

@@ -0,0 +1,71 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Network from "resource:///com/github/Aylur/ags/service/network.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import icons from "../../icons.js";
import { Menu, ArrowToggleButton } from "../ToggleButton.js";
import Applications from "resource:///com/github/Aylur/ags/service/applications.js";
export const NetworkToggle = () =>
ArrowToggleButton({
name: "network",
icon: Widget.Icon({
icon: Network.wifi.bind("icon_name"),
}),
label: Widget.Label({
truncate: "end",
label: Network.wifi
.bind("ssid")
.transform((ssid) => ssid || "Not Connected"),
}),
connection: [Network, () => Network.wifi.enabled],
deactivate: () => (Network.wifi.enabled = false),
activate: () => {
Network.wifi.enabled = true;
Network.wifi.scan();
},
});
export const WifiSelection = () =>
Menu({
name: "network",
icon: Widget.Icon({
icon: Network.wifi.bind("icon_name"),
}),
title: Widget.Label("Wifi Selection"),
content: [
Widget.Box({
vertical: true,
setup: (self) =>
self.hook(
Network,
() =>
(self.children = Network.wifi?.access_points.map((ap) =>
Widget.Button({
on_clicked: () =>
Utils.execAsync(`nmcli device wifi connect ${ap.bssid}`),
child: Widget.Box({
children: [
Widget.Icon(ap.iconName),
Widget.Label(ap.ssid || ""),
ap.active &&
Widget.Icon({
icon: icons.ui.tick,
hexpand: true,
hpack: "end",
}),
],
}),
}),
)),
),
}),
Widget.Separator(),
Widget.Button({
on_clicked: () =>
Applications.query("gnome-control-center")?.[0].launch(),
child: Widget.Box({
children: [Widget.Icon(icons.ui.settings), Widget.Label("Network")],
}),
}),
],
});

View File

@@ -0,0 +1,55 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import { ArrowToggleButton, Menu, opened } from "../ToggleButton.js";
import themes from "../../themes.js";
import icons from "../../icons.js";
import options from "../../options.js";
import { setTheme, openSettings } from "../../settings/theme.js";
export const ThemeToggle = () =>
ArrowToggleButton({
name: "theme",
icon: Widget.Label().bind("label", options.theme.icon),
label: Widget.Label().bind("label", options.theme.name),
connection: [opened, () => opened.value === "theme"],
activate: () => opened.setValue("theme"),
activateOnArrow: false,
deactivate: () => {},
});
export const ThemeSelector = () =>
Menu({
name: "theme",
icon: Widget.Label().bind("label", options.theme.icon),
title: Widget.Label("Theme Selector"),
content: [
...themes.map(({ name, icon }) =>
Widget.Button({
on_clicked: () => setTheme(name),
child: Widget.Box({
children: [
Widget.Label(icon),
Widget.Label(name),
Widget.Icon({
icon: icons.ui.tick,
hexpand: true,
hpack: "end",
visible: options.theme.name
.bind("value")
.transform((v) => v === name),
}),
],
}),
}),
),
Widget.Separator(),
Widget.Button({
on_clicked: openSettings,
child: Widget.Box({
children: [
Widget.Icon(icons.ui.settings),
Widget.Label("Theme Settings"),
],
}),
}),
],
});

View File

@@ -0,0 +1,158 @@
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 icons from "../../icons.js";
import FontIcon from "../../misc/FontIcon.js";
import { getAudioTypeIcon } from "../../utils.js";
import { Arrow } from "../ToggleButton.js";
import { Menu } from "../ToggleButton.js";
/** @param {'speaker' | 'microphone'=} type */
const VolumeIndicator = (type = "speaker") =>
Widget.Button({
on_clicked: () => (Audio[type].is_muted = !Audio[type].is_muted),
child: Widget.Icon().hook(Audio[type], (icon) => {
icon.icon =
type === "speaker"
? getAudioTypeIcon(Audio[type].icon_name || "")
: icons.audio.mic.high;
icon.tooltip_text = `Volume ${Math.floor(Audio[type].volume * 100)}%`;
}),
});
/** @param {'speaker' | 'microphone'=} type */
const VolumeSlider = (type = "speaker") =>
Widget.Slider({
hexpand: true,
draw_value: false,
on_change: ({ value }) => (Audio[type].volume = value),
setup: (self) =>
self.hook(Audio[type], () => {
self.value = Audio[type].volume || 0;
}),
});
export const Volume = () =>
Widget.Box({
children: [
VolumeIndicator("speaker"),
VolumeSlider("speaker"),
Widget.Box({
vpack: "center",
child: Arrow("sink-selector"),
}),
Widget.Box({
vpack: "center",
child: Arrow("app-mixer"),
visible: Audio.bind("apps").transform((a) => a.length > 0),
}),
],
});
export const Microhone = () =>
Widget.Box({
class_name: "slider horizontal",
visible: Audio.bind("recorders").transform((a) => a.length > 0),
children: [VolumeIndicator("microphone"), VolumeSlider("microphone")],
});
/** @param {import('types/service/audio').Stream} stream */
const MixerItem = (stream) =>
Widget.Box({
hexpand: true,
class_name: "mixer-item horizontal",
children: [
Widget.Icon({
tooltip_text: stream.bind("name").transform((n) => n || ""),
icon: stream.bind("name").transform((n) => {
return Utils.lookUpIcon(n || "") ? n || "" : icons.mpris.fallback;
}),
}),
Widget.Box({
vertical: true,
children: [
Widget.Label({
xalign: 0,
truncate: "end",
label: stream.bind("description").transform((d) => d || ""),
}),
Widget.Slider({
hexpand: true,
draw_value: false,
value: stream.bind("volume"),
on_change: ({ value }) => (stream.volume = value),
}),
],
}),
Widget.Label({
xalign: 1,
label: stream.bind("volume").transform((v) => `${Math.floor(v * 100)}`),
}),
],
});
/** @param {import('types/service/audio').Stream} stream */
const SinkItem = (stream) =>
Widget.Button({
hexpand: true,
on_clicked: () => (Audio.speaker = stream),
child: Widget.Box({
children: [
Widget.Icon({
icon: getAudioTypeIcon(stream.icon_name || ""),
tooltip_text: stream.icon_name,
}),
Widget.Label(
(stream.description || "").split(" ").slice(0, 4).join(" "),
),
Widget.Icon({
icon: icons.ui.tick,
hexpand: true,
hpack: "end",
visible: Audio.speaker
.bind("stream")
.transform((s) => s === stream.stream),
}),
],
}),
});
const SettingsButton = () =>
Widget.Button({
on_clicked: () => Utils.execAsync("pavucontrol"),
hexpand: true,
child: Widget.Box({
children: [Widget.Icon(icons.ui.settings), Widget.Label("Settings")],
}),
});
export const AppMixer = () =>
Menu({
name: "app-mixer",
icon: FontIcon(icons.audio.mixer),
title: Widget.Label("App Mixer"),
content: [
Widget.Box({
vertical: true,
children: Audio.bind("apps").transform((a) => a.map(MixerItem)),
}),
Widget.Separator(),
SettingsButton(),
],
});
export const SinkSelector = () =>
Menu({
name: "sink-selector",
icon: Widget.Icon(icons.audio.type.headset),
title: Widget.Label("Sink Selector"),
content: [
Widget.Box({
vertical: true,
children: Audio.bind("speakers").transform((a) => a.map(SinkItem)),
}),
Widget.Separator(),
SettingsButton(),
],
});

View File

@@ -0,0 +1,75 @@
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Gtk from "gi://Gtk";
import options from "../options.js";
/** @param {'topleft' | 'topright' | 'bottomleft' | 'bottomright'} place */
const Corner = (place) =>
Widget.DrawingArea({
class_name: "corner",
hexpand: true,
vexpand: true,
hpack: place.includes("left") ? "start" : "end",
vpack: place.includes("top") ? "start" : "end",
setup: (self) =>
self
.hook(options.radii, () => {
const r = options.radii.value * 2;
self.set_size_request(r, r);
})
.connect("draw", (self, cr) => {
const context = self.get_style_context();
const c = context.get_property(
"background-color",
Gtk.StateFlags.NORMAL,
);
const r = context.get_property(
"border-radius",
Gtk.StateFlags.NORMAL,
);
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();
}),
});
/** @type {Array<'topleft' | 'topright' | 'bottomleft' | 'bottomright'>} */
const places = ["topleft", "topright", "bottomleft", "bottomright"];
/** @param {number} monitor */
export default (monitor) =>
places.map((place) =>
Widget.Window({
name: `corner${monitor}${place}`,
monitor,
class_name: "corner",
anchor: [
place.includes("top") ? "top" : "bottom",
place.includes("right") ? "right" : "left",
],
visible: options.desktop.screen_corners.bind("value"),
child: Corner(place),
}),
);

View File

@@ -0,0 +1,70 @@
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Service from "resource:///com/github/Aylur/ags/service.js";
class Asusctl extends Service {
static {
Service.register(
this,
{},
{
profile: ["string", "r"],
mode: ["string", "r"],
},
);
}
profiles = /** @type {const} */ (["Performance", "Balanced", "Quiet"]);
#profile = "Balanced";
#mode = "Hyprid";
nextProfile() {
Utils.execAsync("asusctl profile -n")
.then(() => {
this.#profile = Utils.exec("asusctl profile -p").split(" ")[3];
this.changed("profile");
})
.catch(console.error);
}
/** @param {'Performance' | 'Balanced' | 'Quiet'} prof */
setProfile(prof) {
Utils.execAsync(`asusctl profile --profile-set ${prof}`)
.then(() => {
this.#profile = prof;
this.changed("profile");
})
.catch(console.error);
}
nextMode() {
Utils.execAsync(
`supergfxctl -m ${this.#mode === "Hybrid" ? "Integrated" : "Hybrid"}`,
)
.then(() => {
this.#mode = Utils.exec("supergfxctl -g");
this.changed("profile");
})
.catch(console.error);
}
constructor() {
super();
if (Utils.exec("which asusctl")) {
this.available = true;
this.#profile = Utils.exec("asusctl profile -p").split(" ")[3];
Utils.execAsync("supergfxctl -g").then((mode) => (this.#mode = mode));
} else {
this.available = false;
}
}
get profile() {
return this.#profile;
}
get mode() {
return this.#mode;
}
}
export default new Asusctl();

View File

@@ -0,0 +1,77 @@
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Service from "resource:///com/github/Aylur/ags/service.js";
import options from "../options.js";
import { dependencies } from "../utils.js";
const KBD = options.brightnessctlKBD;
class Brightness extends Service {
static {
Service.register(
this,
{},
{
screen: ["float", "rw"],
kbd: ["int", "rw"],
},
);
}
#kbd = 0;
#kbdMax = 3;
#screen = 0;
get kbd() {
return this.#kbd;
}
get screen() {
return this.#screen;
}
set kbd(value) {
if (!dependencies(["brightnessctl"])) return;
if (value < 0 || value > this.#kbdMax) return;
Utils.execAsync(`brightnessctl -d ${KBD} s ${value} -q`)
.then(() => {
this.#kbd = value;
this.changed("kbd");
})
.catch(console.error);
}
set screen(percent) {
if (!dependencies(["gbmonctl"])) return;
if (percent < 0) percent = 0;
if (percent > 1) percent = 1;
Utils.execAsync(
`gbmonctl --prop brightness -val ${Math.min(
Math.max(Math.floor(percent * 100), 0),
100,
)}`,
)
.then(() => {
this.#screen = percent;
this.changed("screen");
})
.catch(console.error);
}
constructor() {
super();
if (dependencies(["brightnessctl"])) {
this.#kbd = Number(Utils.exec(`brightnessctl -d ${KBD} g`));
this.#kbdMax = Number(Utils.exec(`brightnessctl -d ${KBD} m`));
this.#screen =
Number(Utils.exec("brightnessctl g")) /
Number(Utils.exec("brightnessctl m"));
}
}
}
export default new Brightness();

View File

@@ -0,0 +1,73 @@
import { Variable } from "resource:///com/github/Aylur/ags/variable.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Service from "resource:///com/github/Aylur/ags/service.js";
import { dependencies } from "../utils.js";
import icons from "../icons.js";
const COLORS_CACHE = Utils.CACHE_DIR + "/colorpicker.json";
class Colors extends Service {
static {
Service.register(
this,
{},
{
colors: ["jsobject"],
},
);
}
/** @type {Variable<string[]>} */
#colors = new Variable([]);
get colors() {
return this.#colors.value;
}
#notifID = 0;
constructor() {
super();
this.#colors.connect("changed", () => this.changed("colors"));
Utils.readFileAsync(COLORS_CACHE)
.then((out) => this.#colors.setValue(JSON.parse(out || "[]")))
.catch(() => print("no colorpicker cache found"));
}
/** @param {string} color */
wlCopy(color) {
Utils.execAsync(["wl-copy", color]).catch((err) => console.error(err));
}
async pick() {
if (!dependencies(["hyprpicker"])) return;
const color = await Utils.execAsync("hyprpicker");
if (!color) return;
this.wlCopy(color);
const list = this.#colors.value;
if (!list.includes(color)) {
list.push(color);
if (list.length > 10) list.shift();
this.#colors.value = list;
Utils.writeFile(JSON.stringify(list, null, 2), COLORS_CACHE).catch(
(err) => console.error(err),
);
}
const n = await Utils.notify({
id: this.#notifID,
iconName: icons.ui.colorpicker,
summary: color,
actions: {
Copy: () => this.wlCopy(color),
},
});
this.#notifID = n.id;
}
}
export default new Colors();

View File

@@ -0,0 +1,30 @@
import Service from "resource:///com/github/Aylur/ags/service.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import App from "resource:///com/github/Aylur/ags/app.js";
const authpy = App.configDir + "/js/lockscreen/auth.py";
class Lockscreen extends Service {
static {
Service.register(this, {
lock: ["boolean"],
authenticating: ["boolean"],
});
}
lockscreen() {
this.emit("lock", true);
}
/** @param {string} password */
auth(password) {
this.emit("authenticating", true);
Utils.execAsync([authpy, password])
.then((out) => {
this.emit("lock", out !== "True");
this.emit("authenticating", false);
})
.catch((err) => console.error(err));
}
}
export default new Lockscreen();

View File

@@ -0,0 +1,58 @@
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Service from "resource:///com/github/Aylur/ags/service.js";
import Audio from "resource:///com/github/Aylur/ags/service/audio.js";
import icons from "../icons.js";
import { getAudioTypeIcon } from "../utils.js";
import Brightness from "./brightness.js";
class Indicator extends Service {
static {
Service.register(this, {
popup: ["double", "string"],
});
}
#delay = 1500;
#count = 0;
/**
* @param {number} value - 0 < v < 1
* @param {string} icon
*/
popup(value, icon) {
this.emit("popup", value, icon);
this.#count++;
Utils.timeout(this.#delay, () => {
this.#count--;
if (this.#count === 0) this.emit("popup", -1, icon);
});
}
speaker() {
this.popup(
Audio.speaker?.volume || 0,
getAudioTypeIcon(Audio.speaker?.icon_name || ""),
);
}
display() {
// brightness is async, so lets wait a bit
Utils.timeout(10, () =>
this.popup(Brightness.screen, icons.brightness.screen),
);
}
kbd() {
// brightness is async, so lets wait a bit
Utils.timeout(10, () =>
this.popup((Brightness.kbd * 33 + 1) / 100, icons.brightness.keyboard),
);
}
connect(event = "popup", callback) {
return super.connect(event, callback);
}
}
export default new Indicator();

View File

@@ -0,0 +1,43 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import Service from "resource:///com/github/Aylur/ags/service.js";
class PowerMenu extends Service {
static {
Service.register(
this,
{},
{
title: ["string"],
cmd: ["string"],
},
);
}
#title = "";
#cmd = "";
get title() {
return this.#title;
}
get cmd() {
return this.#cmd;
}
/** @param {'sleep' | 'reboot' | 'logout' | 'shutdown'} action */
action(action) {
[this.#cmd, this.#title] = {
sleep: ["systemctl suspend", "Sleep"],
reboot: ["systemctl reboot", "Reboot"],
logout: ["pkill Hyprland", "Log Out"],
shutdown: ["shutdown now", "Shutdown"],
}[action];
this.notify("cmd");
this.notify("title");
this.emit("changed");
App.closeWindow("powermenu");
App.openWindow("verification");
}
}
export default new PowerMenu();

View File

@@ -0,0 +1,112 @@
import Service from "resource:///com/github/Aylur/ags/service.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import GLib from "gi://GLib";
import { dependencies } from "../utils.js";
const now = () => GLib.DateTime.new_now_local().format("%Y-%m-%d_%H-%M-%S");
class Recorder extends Service {
static {
Service.register(
this,
{},
{
timer: ["int"],
recording: ["boolean"],
},
);
}
#path = GLib.get_home_dir() + "/Videos/Screencasting";
#file = "";
#interval = 0;
recording = false;
timer = 0;
async start() {
if (!dependencies(["slurp", "wf-recorder"])) return;
if (this.recording) return;
const area = await Utils.execAsync("slurp");
Utils.ensureDirectory(this.#path);
this.#file = `${this.#path}/${now()}.mp4`;
Utils.execAsync(["wf-recorder", "-g", area, "-f", this.#file]);
this.recording = true;
this.changed("recording");
this.timer = 0;
this.#interval = Utils.interval(1000, () => {
this.changed("timer");
this.timer++;
});
}
async stop() {
if (!dependencies(["notify-send"])) return;
if (!this.recording) return;
Utils.execAsync("killall -INT wf-recorder");
this.recording = false;
this.changed("recording");
GLib.source_remove(this.#interval);
const res = await Utils.execAsync([
"notify-send",
"-A",
"files=Show in Files",
"-A",
"view=View",
"-i",
"video-x-generic-symbolic",
"Screenrecord",
this.#file,
]);
if (res === "files") Utils.execAsync("xdg-open " + this.#path);
if (res === "view") Utils.execAsync("xdg-open " + this.#file);
}
async screenshot(full = false) {
if (!dependencies(["slurp", "wayshot"])) return;
const path = GLib.get_home_dir() + "/Pictures/Screenshots";
const file = `${path}/${now()}.png`;
Utils.ensureDirectory(path);
await Utils.execAsync(
["wayshot", "-f", file].concat(
full ? [] : ["-s", await Utils.execAsync("slurp")],
),
);
Utils.execAsync(["bash", "-c", `wl-copy < ${file}`]);
const res = await Utils.execAsync([
"notify-send",
"-A",
"files=Show in Files",
"-A",
"view=View",
"-A",
"edit=Edit",
"-i",
file,
"Screenshot",
file,
]);
if (res === "files") Utils.execAsync("xdg-open " + path);
if (res === "view") Utils.execAsync("xdg-open " + file);
if (res === "edit") Utils.execAsync(["swappy", "-f", file]);
App.closeWindow("dashboard");
}
}
export default new Recorder();

View File

@@ -0,0 +1,297 @@
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import App from "resource:///com/github/Aylur/ags/app.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import RegularWindow from "../misc/RegularWindow.js";
import Variable from "resource:///com/github/Aylur/ags/variable.js";
import icons from "../icons.js";
import { getOptions, getValues } from "./option.js";
import options from "../options.js";
const optionsList = getOptions();
const categories = Array.from(
new Set(optionsList.map((opt) => opt.category)),
).filter((category) => category !== "exclude");
const currentPage = Variable(categories[0]);
const search = Variable("");
const showSearch = Variable(false);
showSearch.connect("changed", ({ value }) => {
if (!value) search.value = "";
});
/** @param {import('./option.js').Opt<string>} opt */
const EnumSetter = (opt) => {
const lbl = Widget.Label().bind("label", opt);
const step = (dir = 1) => {
const i = opt.enums.findIndex((i) => i === lbl.label);
opt.setValue(
dir > 0
? i + dir > opt.enums.length - 1
? opt.enums[0]
: opt.enums[i + dir]
: i + dir < 0
? opt.enums[opt.enums.length - 1]
: opt.enums[i + dir],
true,
);
};
const next = Widget.Button({
child: Widget.Icon(icons.ui.arrow.right),
on_clicked: () => step(+1),
});
const prev = Widget.Button({
child: Widget.Icon(icons.ui.arrow.left),
on_clicked: () => step(-1),
});
return Widget.Box({
class_name: "enum-setter",
children: [prev, lbl, next],
});
};
/** @param {import('./option.js').Opt} opt */
const Setter = (opt) => {
switch (opt.type) {
case "number":
return Widget.SpinButton({
setup(self) {
self.set_range(0, 1000);
self.set_increments(1, 5);
self.on("value-changed", () => opt.setValue(self.value, true));
self.hook(opt, () => (self.value = opt.value));
},
});
case "float":
case "object":
return Widget.Entry({
on_accept: (self) => opt.setValue(JSON.parse(self.text || ""), true),
setup: (self) =>
self.hook(opt, () => (self.text = JSON.stringify(opt.value))),
});
case "string":
return Widget.Entry({
on_accept: (self) => opt.setValue(self.text, true),
setup: (self) => self.hook(opt, () => (self.text = opt.value)),
});
case "enum":
return EnumSetter(opt);
case "boolean":
return Widget.Switch()
.on("notify::active", (self) => opt.setValue(self.active, true))
.hook(opt, (self) => (self.active = opt.value));
case "img":
return Widget.FileChooserButton().on("selection-changed", (self) => {
opt.setValue(self.get_uri()?.replace("file://", ""), true);
});
case "font":
return Widget.FontButton({
show_size: false,
use_size: false,
setup: (self) =>
self
.on("notify::font", ({ font }) => opt.setValue(font, true))
.hook(opt, () => (self.font = opt.value)),
});
default:
return Widget.Label({
label: "no setter with type " + opt.type,
});
}
};
/** @param {import('./option.js').Opt} opt */
const Row = (opt) =>
Widget.Box({
class_name: "row",
attribute: opt,
children: [
Widget.Box({
vertical: true,
vpack: "center",
children: [
opt.title &&
Widget.Label({
xalign: 0,
class_name: "summary",
label: opt.title,
}),
Widget.Label({
xalign: 0,
class_name: "id",
label: `id: "${opt.id}"`,
}),
],
}),
Widget.Box({ hexpand: true }),
Widget.Box({
vpack: "center",
vertical: true,
children: [
Widget.Box({
hpack: "end",
child: Setter(opt),
}),
opt.note &&
Widget.Label({
xalign: 1,
class_name: "note",
label: opt.note,
}),
],
}),
],
});
/** @param {string} category */
const Page = (category) =>
Widget.Scrollable({
vexpand: true,
class_name: "page",
child: Widget.Box({
class_name: "page-content vertical",
vertical: true,
setup: (self) =>
self.hook(search, () => {
for (const child of self.children) {
child.visible =
child.attribute.id.includes(search.value) ||
child.attribute.title.includes(search.value) ||
child.attribute.note.includes(search.value);
}
}),
children: optionsList
.filter((opt) => opt.category.includes(category))
.map(Row),
}),
});
const sidebar = Widget.Revealer({
reveal_child: search.bind().transform((v) => !v),
transition: "slide_right",
child: Widget.Box({
hexpand: false,
vertical: true,
children: [
Widget.Box({
class_name: "sidebar-header",
children: [
Widget.Button({
hexpand: true,
label: icons.dialog.Search + " Search",
on_clicked: () => (showSearch.value = !showSearch.value),
}),
Widget.Button({
hpack: "end",
child: Widget.Icon(icons.ui.info),
on_clicked: () => App.toggleWindow("about"),
}),
],
}),
Widget.Scrollable({
vexpand: true,
hscroll: "never",
child: Widget.Box({
class_name: "sidebar-box vertical",
vertical: true,
children: [
...categories.map((name) =>
Widget.Button({
label: (icons.dialog[name] || "") + " " + name,
xalign: 0,
class_name: currentPage
.bind()
.transform((v) => `${v === name ? "active" : ""}`),
on_clicked: () => currentPage.setValue(name),
}),
),
],
}),
}),
Widget.Box({
class_name: "sidebar-footer",
child: Widget.Button({
class_name: "copy",
child: Widget.Label({
label: " Save",
xalign: 0,
}),
hexpand: true,
on_clicked: () => {
Utils.execAsync(["wl-copy", getValues()]);
Utils.execAsync([
"notify-send",
"-i",
"preferences-desktop-theme-symbolic",
"Theme copied to clipboard",
'To save it permanently, make a new theme in <span weight="bold">themes.js</span>',
]);
},
}),
}),
],
}),
});
const searchEntry = Widget.Revealer({
transition: "slide_down",
reveal_child: showSearch.bind(),
transition_duration: options.transition.bind("value"),
child: Widget.Entry({
setup: (self) =>
self.hook(showSearch, () => {
if (!showSearch.value) self.text = "";
if (showSearch.value) self.grab_focus();
}),
hexpand: true,
class_name: "search",
placeholder_text: "Search Options",
secondary_icon_name: icons.apps.search,
on_change: ({ text }) => (search.value = text || ""),
}),
});
const categoriesStack = Widget.Stack({
transition: "slide_left_right",
children: categories.reduce((obj, name) => {
obj[name] = Page(name);
return obj;
}, {}),
shown: currentPage.bind(),
visible: search.bind().transform((v) => !v),
});
const searchPage = Widget.Box({
visible: search.bind().transform((v) => !!v),
child: Page(""),
});
export default RegularWindow({
name: "settings-dialog",
title: "Settings",
setup: (win) =>
win
.on("delete-event", () => {
win.hide();
return true;
})
.on("key-press-event", (_, event) => {
if (event.get_keyval()[1] === imports.gi.Gdk.KEY_Escape) {
showSearch.setValue(false);
search.setValue("");
}
})
.set_default_size(800, 500),
child: Widget.Box({
children: [
sidebar,
Widget.Box({
vertical: true,
children: [searchEntry, categoriesStack, searchPage],
}),
],
}),
});

View File

@@ -0,0 +1,40 @@
import Mpris from "resource:///com/github/Aylur/ags/service/mpris.js";
export async function globals() {
try {
globalThis.options = (await import("../options.js")).default;
globalThis.iconBrowser = (await import("../misc/IconBrowser.js")).default;
globalThis.app = (
await import("resource:///com/github/Aylur/ags/app.js")
).default;
globalThis.audio = (
await import("resource:///com/github/Aylur/ags/service/audio.js")
).default;
globalThis.recorder = (await import("../services/screenrecord.js")).default;
globalThis.brightness = (await import("../services/brightness.js")).default;
globalThis.indicator = (
await import("../services/onScreenIndicator.js")
).default;
globalThis.app = (
await import("resource:///com/github/Aylur/ags/app.js")
).default;
Mpris.players.forEach((player) => {
player.connect("changed", (player) => {
globalThis.mpris = player || Mpris.players[0];
});
});
Mpris.connect("player-added", (mpris, bus) => {
mpris.getPlayer(bus)?.connect("changed", (player) => {
globalThis.mpris = player || Mpris.players[0];
});
});
Mpris.connect("player-closed", () => {
globalThis.mpris = Mpris.players[0];
});
} catch (error) {
logError(error);
}
}

View File

@@ -0,0 +1,69 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import Hyprland from "resource:///com/github/Aylur/ags/service/hyprland.js";
import options from "../options.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
const noIgnorealpha = ["verification", "powermenu", "lockscreen"];
/** @param {Array<string>} batch */
function sendBatch(batch) {
const cmd = batch
.filter((x) => !!x)
.map((x) => `keyword ${x}`)
.join("; ");
Hyprland.sendMessage(`[[BATCH]]/${cmd}`);
}
/** @param {string} scss */
function getColor(scss) {
if (scss.includes("#")) return scss.replace("#", "");
if (scss.includes("$")) {
const opt = options
.list()
.find((opt) => opt.scss === scss.replace("$", ""));
return opt?.value.replace("#", "") || "ff0000";
}
}
export function hyprlandInit() {
sendBatch(
App.windows.flatMap(({ name }) => [
`layerrule blur, ${name}`,
noIgnorealpha.some((skip) => name?.includes(skip))
? ""
: `layerrule ignorealpha 0.3, ${name}`,
]),
);
}
export async function setupHyprland() {
/*Hyprland.event("activewindowv2", async (addr) => {
const client = Hyprland.getClient(addr);
if (!client.pinned || !client.floating) return;
const x = client.at[0];
console.log(
await Utils.execAsync(`hyprctl dispatch moveactive exact ${x} 80`),
);
});*/
const wm_gaps = Math.floor(
options.hypr.wm_gaps_multiplier.value * options.spacing.value,
);
const border_width = options.border.width.value;
const radii = options.radii.value;
const drop_shadow = options.desktop.drop_shadow.value;
const inactive_border = options.hypr.inactive_border.value;
const accent = getColor(options.theme.accent.accent.value);
sendBatch([
`general:border_size ${border_width}`,
`general:gaps_out ${wm_gaps}`,
`general:gaps_in ${Math.floor(wm_gaps / 2)}`,
`general:col.active_border rgba(${accent}ff)`,
`general:col.inactive_border ${inactive_border}`,
`decoration:rounding ${radii}`,
`decoration:drop_shadow ${drop_shadow ? "yes" : "no"}`,
]);
}

View File

@@ -0,0 +1,198 @@
import {
CACHE_DIR,
readFile,
writeFile,
} from "resource:///com/github/Aylur/ags/utils.js";
import { exec } from "resource:///com/github/Aylur/ags/utils.js";
import options from "../options.js";
import Service from "resource:///com/github/Aylur/ags/service.js";
import { reloadScss } from "./scss.js";
import { setupHyprland } from "./hyprland.js";
const CACHE_FILE = CACHE_DIR + "/options.json";
/** object that holds the overriedden values */
let cacheObj = JSON.parse(readFile(CACHE_FILE) || "{}");
/**
* @template T
* @typedef {Object} OptionConfig
* @property {string=} scss - name of scss variable set to "exclude" to not include it in the generated scss file
* @property {string=} unit - scss unit on numbers, default is "px"
* @property {string=} title
* @property {string=} note
* @property {string=} category
* @property {boolean=} noReload - don't reload css & hyprland on change
* @property {boolean=} persist - ignore reset call
* @property {'object' | 'string' | 'img' | 'number' | 'float' | 'font' | 'enum' =} type
* @property {Array<string> =} enums
* @property {(value: T) => any=} format
* @property {(value: T) => any=} scssFormat
*/
/** @template T */
export class Opt extends Service {
static {
Service.register(
this,
{},
{
value: ["jsobject"],
},
);
}
#value;
#scss = "";
unit = "px";
noReload = false;
persist = false;
id = "";
title = "";
note = "";
type = "";
category = "";
/** @type {Array<string>} */
enums = [];
/** @type {(v: T) => any} */
format = (v) => v;
/** @type {(v: T) => any} */
scssFormat = (v) => v;
/**
* @param {T} value
* @param {OptionConfig<T> =} config
*/
constructor(value, config) {
super();
this.#value = value;
this.defaultValue = value;
this.type = typeof value;
if (config) Object.keys(config).forEach((c) => (this[c] = config[c]));
import("../options.js").then(this.#init.bind(this));
}
set scss(scss) {
this.#scss = scss;
}
get scss() {
return this.#scss || this.id.split(".").join("-").split("_").join("-");
}
#init() {
getOptions(); // sets the ids as a side effect
if (cacheObj[this.id] !== undefined) this.setValue(cacheObj[this.id]);
const words = this.id
.split(".")
.flatMap((w) => w.split("_"))
.map((word) => word.charAt(0).toUpperCase() + word.slice(1));
this.title ||= words.join(" ");
this.category ||= words.length === 1 ? "General" : words.at(0) || "General";
this.connect("changed", () => {
cacheObj[this.id] = this.value;
writeFile(JSON.stringify(cacheObj, null, 2), CACHE_FILE);
});
}
get value() {
return this.#value;
}
set value(value) {
this.setValue(value);
}
/** @param {T} value */
setValue(value, reload = false) {
if (typeof value !== typeof this.defaultValue) {
console.error(
Error(
`WrongType: Option "${this.id}" can't be set to ${value}, ` +
`expected "${typeof this.defaultValue}", but got "${typeof value}"`,
),
);
return;
}
if (this.value !== value) {
this.#value = this.format(value);
this.changed("value");
if (reload && !this.noReload) {
reloadScss();
setupHyprland();
}
}
}
reset(reload = false) {
if (!this.persist) this.setValue(this.defaultValue, reload);
}
}
/**
* @template T
* @param {T} value
* @param {OptionConfig<T> =} config
* @returns {Opt<T>}
*/
export function Option(value, config) {
return new Opt(value, config);
}
/** @returns {Array<Opt<any>>} */
export function getOptions(object = options, path = "") {
return Object.keys(object).flatMap((key) => {
/** @type Option<any> */
const obj = object[key];
const id = path ? path + "." + key : key;
if (obj instanceof Opt) {
obj.id = id;
return obj;
}
if (typeof obj === "object") return getOptions(obj, id);
return [];
});
}
export function resetOptions() {
exec(`rm -rf ${CACHE_FILE}`);
cacheObj = {};
getOptions().forEach((opt) => opt.reset());
}
export function getValues() {
const obj = {};
for (const opt of getOptions()) {
if (opt.category !== "exclude") obj[opt.id] = opt.value;
}
return JSON.stringify(obj, null, 2);
}
/** @param {string | object} config */
export function apply(config) {
const options = getOptions();
const settings = typeof config === "string" ? JSON.parse(config) : config;
for (const id of Object.keys(settings)) {
const opt = options.find((opt) => opt.id === id);
if (!opt) {
print(`No option with id: "${id}"`);
continue;
}
opt.setValue(settings[id]);
}
}

View File

@@ -0,0 +1,62 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import { getOptions } from "./option.js";
export function scssWatcher() {
return Utils.subprocess(
[
"inotifywait",
"--recursive",
"--event",
"create,modify",
"-m",
App.configDir + "/scss",
],
reloadScss,
() => print("missing dependancy for css hotreload: inotify-tools"),
);
}
/**
* generate an scss file that makes every option available as a variable
* based on the passed scss parameter or the path in the object
*
* e.g
* options.bar.style.value => $bar-style
*/
export async function reloadScss() {
const opts = getOptions();
const vars = opts.map((opt) => {
if (opt.scss === "exclude") return "";
const unit = typeof opt.value === "number" ? opt.unit : "";
const value = opt.scssFormat ? opt.scssFormat(opt.value) : opt.value;
return `$${opt.scss}: ${value}${unit};`;
});
const bar_style = opts.find((opt) => opt.id === "bar.style")?.value || "";
const additional =
bar_style === "normal"
? "//"
: `
window#quicksettings .window-content {
margin-right: $wm-gaps;
}
`;
try {
const tmp = "/tmp/ags/scss";
Utils.ensureDirectory(tmp);
await Utils.writeFile(vars.join("\n"), `${tmp}/options.scss`);
await Utils.writeFile(additional, `${tmp}/additional.scss`);
await Utils.execAsync(
`sassc ${App.configDir}/scss/main.scss ${tmp}/style.css`,
);
App.resetCss();
App.applyCss(`${tmp}/style.css`);
} catch (error) {
if (error instanceof Error) console.error(error.message);
if (typeof error === "string") console.error(error);
}
}

View File

@@ -0,0 +1,126 @@
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Battery from "resource:///com/github/Aylur/ags/service/battery.js";
import Notifications from "resource:///com/github/Aylur/ags/service/notifications.js";
import options from "../options.js";
import icons from "../icons.js";
import { reloadScss } from "./scss.js";
import { wallpaper } from "./wallpaper.js";
import { hyprlandInit, setupHyprland } from "./hyprland.js";
import { globals } from "./globals.js";
import { showAbout } from "../about/about.js";
import Gtk from "gi://Gtk?version=3.0";
export function init() {
notificationBlacklist();
warnOnLowBattery();
globals();
tmux();
kitty();
gsettigsColorScheme();
gtkFontSettings();
dependandOptions();
reloadScss();
hyprlandInit();
setupHyprland();
wallpaper();
showAbout();
}
function dependandOptions() {
options.bar.style.connect("changed", ({ value }) => {
if (value !== "normal")
options.desktop.screen_corners.setValue(false, true);
});
}
function kitty() {
if (!Utils.exec("which kitty")) return;
console.log("kitty");
options.theme.scheme.connect("changed", ({ value }) =>
Utils.execAsync(
`kitty +kitten themes --reload-in=all --config-file-name /home/theaninova/.config/kitty/current-colors.conf Catppuccin-${
value === "light" ? "Latte" : "Frappe"
}`,
),
);
}
function tmux() {
if (!Utils.exec("which tmux")) return;
/** @param {string} scss */
function getColor(scss) {
if (scss.includes("#")) return scss;
if (scss.includes("$")) {
const opt = options
.list()
.find((opt) => opt.scss === scss.replace("$", ""));
return opt?.value;
}
}
options.theme.accent.accent.connect("changed", ({ value }) =>
Utils.execAsync(`tmux set @main_accent ${getColor(value)}`).catch((err) =>
console.error(err.message),
),
);
}
function gsettigsColorScheme() {
if (!Utils.exec("which gsettings")) return;
options.theme.scheme.connect("changed", ({ value }) => {
const gsettings = "gsettings set org.gnome.desktop.interface color-scheme";
Utils.execAsync(`${gsettings} "prefer-${value}"`).catch((err) =>
console.error(err.message),
);
});
}
function gtkFontSettings() {
const settings = Gtk.Settings.get_default();
if (!settings) {
console.error(Error("Gtk.Settings unavailable"));
return;
}
const callback = () => {
const { size, font } = options.font;
settings.gtk_font_name = `${font.value} ${size.value}`;
};
options.font.font.connect("notify::value", callback);
options.font.size.connect("notify::value", callback);
}
function notificationBlacklist() {
Notifications.connect("notified", (_, id) => {
const n = Notifications.getNotification(id);
options.notifications.black_list.value.forEach((item) => {
if (n?.app_name.includes(item) || n?.app_entry?.includes(item)) n.close();
});
});
}
function warnOnLowBattery() {
Battery.connect("notify::percent", () => {
const low = options.battery.low.value;
if (
Battery.percent !== low ||
Battery.percent !== low / 2 ||
!Battery.charging
)
return;
Utils.execAsync([
"notify-send",
`${Battery.percent}% Battery Percentage`,
"-i",
icons.battery.warning,
"-u",
"critical",
]);
});
}

View File

@@ -0,0 +1,68 @@
import App from "resource:///com/github/Aylur/ags/app.js";
import options from "../options.js";
import themes from "../themes.js";
import { reloadScss } from "./scss.js";
import { setupHyprland } from "./hyprland.js";
import { wallpaper } from "./wallpaper.js";
/** @param {string} name */
export function setTheme(name) {
options.reset();
const theme = themes.find((t) => t.name === name);
if (!theme) return print("No theme named " + name);
options.apply(theme.options);
reloadScss();
setupHyprland();
wallpaper();
}
export const WP = App.configDir + "/assets/";
export const lightColors = {
"theme.scheme": "light",
"color.red": "#d20f39",
"color.green": "#40a02b",
"color.yellow": "#df8e1d",
"color.blue": "#1e66f5",
"color.magenta": "#8839ef",
"color.teal": "#179299",
"color.orange": "#fe640b",
"theme.bg": "transparentize(#eff1f5, 0.3)",
"theme.fg": "#4c4f69",
};
export const darkColors = {
"theme.scheme": "dark",
"color.red": "#e78284",
"color.green": "#a6d189",
"color.yellow": "#e5c890",
"color.blue": "#8caaee",
"color.magenta": "#ca9ee6",
"color.teal": "#81c8be",
"color.orange": "#ef9f76",
"theme.bg": "transparentize(#303446, 0.3)",
"theme.fg": "#c6d0f5",
};
export const Theme = ({ name, icon = " ", ...options }) => ({
name,
icon,
options: {
"theme.name": name,
"theme.icon": icon,
...options,
},
});
let settingsDialog;
export async function openSettings() {
if (settingsDialog) return settingsDialog.present();
try {
settingsDialog = (await import("./SettingsDialog.js")).default;
settingsDialog.present();
} catch (error) {
if (error instanceof Error) console.error(error.message);
}
}

View File

@@ -0,0 +1,19 @@
import options from "../options.js";
import { exec, execAsync } from "resource:///com/github/Aylur/ags/utils.js";
import { dependencies } from "../utils.js";
export function initWallpaper() {
if (dependencies(["swww"])) {
exec("swww init");
options.desktop.wallpaper.img.connect("changed", wallpaper);
}
}
export function wallpaper() {
if (!dependencies(["swww"])) return;
execAsync(["swww", "img", options.desktop.wallpaper.img.value]).catch((err) =>
console.error(err),
);
}

View File

@@ -0,0 +1,77 @@
/**
* A Theme is a set of options that will be applied
* ontop of the default values. see options.js for possible options
*/
import { Theme, WP, lightColors, darkColors } from "./settings/theme.js";
export default [
Theme({
name: "Frappé",
icon: "󰄛",
"desktop.screen_corners": false,
"desktop.clock.enable": false,
"bar.style": "separated",
"bar.separators": false,
"desktop.wallpaper.img":
WP + "wallpapers/Lakeside/lakeside_2019_midnight.png",
...darkColors,
}),
Theme({
name: "Latte",
icon: "󰄛",
"desktop.screen_corners": false,
"desktop.clock.enable": false,
"bar.style": "separated",
"bar.separators": false,
"desktop.wallpaper.img":
WP + "wallpapers/Lakeside/Lakeside_2019_Teal_NoDeer_UHD2.png",
...lightColors,
"theme.widget.bg": "$accent",
"theme.widget.opacity": 64,
}),
/*Theme({
name: "Leaves",
icon: "󰌪",
"desktop.wallpaper.img": WP + "leaves.jpg",
"theme.accent.accent": "$green",
"theme.accent.gradient": "to right, $accent, darken($accent, 14%)",
"theme.widget.opacity": 92,
"border.opacity": 86,
"theme.bg": "transparentize(#171717, 0.3)",
"bar.style": "floating",
radii: 0,
}),
Theme({
name: "Ivory",
icon: "󰟆",
...lightColors,
"desktop.wallpaper.img": WP + "ivory.png",
"desktop.wallpaper.fg": "$bg_color",
"desktop.screen_corners": false,
"bar.style": "separated",
"theme.widget.bg": "$accent",
"theme.widget.opacity": 64,
"desktop.drop_shadow": false,
"border.width": 2,
"border.opacity": 0,
"theme.accent.gradient": "to right, $accent, darken($accent, 6%)",
"hypr.inactive_border": "rgba(111111FF)",
"bar.separators": false,
}),
Theme({
name: "Space",
icon: "",
"desktop.wallpaper.img": WP + "space.jpg",
spacing: 11,
padding: 10,
radii: 12,
"theme.accent.accent": "$magenta",
"desktop.screen_corners": false,
"desktop.clock.enable": false,
"bar.separators": false,
"bar.icon": "",
"theme.bg": "transparentize(#171717, 0.3)",
"theme.widget.opacity": 95,
"bar.flat_buttons": false,
}),*/
];

Some files were not shown because too many files have changed in this diff Show More