├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .gitlab-ci.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── build.rs ├── data ├── com.system76.CosmicAppLibrary.desktop ├── com.system76.CosmicAppLibrary.metainfo.xml ├── icons │ ├── app-source-flatpak.svg │ ├── app-source-local-symbolic.svg │ ├── app-source-nix.svg │ ├── app-source-snap.svg │ ├── app-source-system-symbolic.svg │ ├── com.system76.CosmicAppLibrary.svg │ └── justfile └── justfile ├── debian ├── changelog ├── control ├── copyright ├── install ├── rules └── source │ └── format ├── flake.lock ├── flake.nix ├── hooks └── pre-commit.hook ├── i18n.toml ├── i18n ├── af │ └── cosmic_app_library.ftl ├── ar │ └── cosmic_app_library.ftl ├── be │ └── cosmic_app_library.ftl ├── bg │ └── cosmic_app_library.ftl ├── ca │ └── cosmic_app_library.ftl ├── cs │ └── cosmic_app_library.ftl ├── da │ └── cosmic_app_library.ftl ├── de │ └── cosmic_app_library.ftl ├── el │ └── cosmic_app_library.ftl ├── en-GB │ └── cosmic_app_library.ftl ├── en │ └── cosmic_app_library.ftl ├── eo │ └── cosmic_app_library.ftl ├── es-419 │ └── cosmic_app_library.ftl ├── es-MX │ └── cosmic_app_library.ftl ├── es │ └── cosmic_app_library.ftl ├── et │ └── cosmic_app_library.ftl ├── fa │ └── cosmic_app_library.ftl ├── fi │ └── cosmic_app_library.ftl ├── fr │ └── cosmic_app_library.ftl ├── fy │ └── cosmic_app_library.ftl ├── ga │ └── cosmic_app_library.ftl ├── gd │ └── cosmic_app_library.ftl ├── he │ └── cosmic_app_library.ftl ├── hi │ └── cosmic_app_library.ftl ├── hr │ └── cosmic_app_library.ftl ├── hu │ └── cosmic_app_library.ftl ├── id │ └── cosmic_app_library.ftl ├── ie │ └── cosmic_app_library.ftl ├── it │ └── cosmic_app_library.ftl ├── ja │ └── cosmic_app_library.ftl ├── jv │ └── cosmic_app_library.ftl ├── kn │ └── cosmic_app_library.ftl ├── ko │ └── cosmic_app_library.ftl ├── li │ └── cosmic_app_library.ftl ├── lt │ └── cosmic_app_library.ftl ├── nb │ └── cosmic_app_library.ftl ├── nl │ └── cosmic_app_library.ftl ├── pl │ └── cosmic_app_library.ftl ├── pt-BR │ └── cosmic_app_library.ftl ├── pt │ └── cosmic_app_library.ftl ├── ro │ └── cosmic_app_library.ftl ├── ru │ └── cosmic_app_library.ftl ├── sk │ └── cosmic_app_library.ftl ├── sr-Cyrl │ └── cosmic_app_library.ftl ├── sr-Latn │ └── cosmic_app_library.ftl ├── sr │ └── cosmic_app_library.ftl ├── sv │ └── cosmic_app_library.ftl ├── ta │ └── cosmic_app_library.ftl ├── th │ └── cosmic_app_library.ftl ├── tr │ └── cosmic_app_library.ftl ├── uk │ └── cosmic_app_library.ftl ├── vi │ └── cosmic_app_library.ftl ├── zh-CN │ └── cosmic_app_library.ftl └── zh-TW │ └── cosmic_app_library.ftl ├── justfile ├── rust-toolchain.toml └── src ├── app.rs ├── app_group.rs ├── config.rs ├── icon_cache.rs ├── localize.rs ├── main.rs ├── subscriptions ├── desktop_files.rs └── mod.rs └── widgets ├── application.rs └── mod.rs /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | **Cosmic-app-library version:** 10 | 11 | 12 | **Issue/Bug description:** 13 | 14 | **Steps to reproduce:** 15 | 16 | **Expected behavior:** 17 | 18 | **Other notes:** 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [master_jammy] 4 | pull_request: 5 | 6 | name: CI 7 | 8 | jobs: 9 | rustfmt: 10 | name: Rustfmt 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: stable 18 | override: true 19 | components: rustfmt 20 | - name: Create blank versions of configured file 21 | run: echo -e "" >> src/config.rs 22 | - name: Run cargo fmt 23 | run: cargo fmt --all -- --check 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | build/ 3 | _build/ 4 | builddir/ 5 | build-aux/app 6 | build-aux/.flatpak-builder/ 7 | src/config.rs 8 | *.ui.in~ 9 | *.ui~ 10 | .flatpak/ 11 | .flatpak-builder/ 12 | flatpak_app/ 13 | vendor 14 | vendor.tar 15 | .cargo/ 16 | /result 17 | 18 | debian/* 19 | !debian/*install 20 | !debian/*postinst 21 | !debian/changelog 22 | !debian/copyright 23 | !debian/control 24 | !debian/links 25 | !debian/rules 26 | !debian/source 27 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - check 3 | - test 4 | 5 | flatpak: 6 | image: 'quay.io/gnome_infrastructure/gnome-runtime-images:gnome-42' 7 | stage: test 8 | tags: 9 | - flatpak 10 | variables: 11 | BUNDLE: "cosmic-app-library-nightly.flatpak" 12 | MANIFEST_PATH: "build-aux/com.system76.CosmicAppLibrary.Devel.json" 13 | FLATPAK_MODULE: "cosmic-app-library" 14 | APP_ID: "com.system76.CosmicAppLibrary.Devel" 15 | RUNTIME_REPO: "https://nightly.gnome.org/gnome-nightly.flatpakrepo" 16 | script: 17 | - > 18 | xvfb-run -a -s "-screen 0 1024x768x24" 19 | flatpak-builder --keep-build-dirs --user --disable-rofiles-fuse flatpak_app --repo=repo ${BRANCH:+--default-branch=$BRANCH} ${MANIFEST_PATH} 20 | - flatpak build-bundle repo ${BUNDLE} --runtime-repo=${RUNTIME_REPO} ${APP_ID} ${BRANCH} 21 | artifacts: 22 | name: 'Flatpak artifacts' 23 | expose_as: 'Get Flatpak bundle here' 24 | when: 'always' 25 | paths: 26 | - "${BUNDLE}" 27 | - '.flatpak-builder/build/${FLATPAK_MODULE}/_flatpak_build/meson-logs/meson-log.txt' 28 | - '.flatpak-builder/build/${FLATPAK_MODULE}/_flatpak_build/meson-logs/testlog.txt' 29 | expire_in: 14 days 30 | 31 | # Configure and run rustfmt 32 | # Exits and builds fails if on bad format 33 | rustfmt: 34 | image: "rust:slim" 35 | script: 36 | - rustup component add rustfmt 37 | # Create blank versions of our configured files 38 | # so rustfmt does not yell about non-existent files or completely empty files 39 | - echo -e "" >> src/config.rs 40 | - rustc -Vv && cargo -Vv 41 | - cargo fmt --version 42 | - cargo fmt --all -- --color=always --check 43 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cosmic-app-library" 3 | version = "0.1.0" 4 | authors = ["Ashley Wulber "] 5 | edition = "2024" 6 | [features] 7 | default = ["wgpu"] 8 | wgpu = ["libcosmic/wgpu"] 9 | 10 | [dependencies] 11 | zbus = "5.10" 12 | libcosmic = { git = "https://github.com/pop-os/libcosmic/", features = [ 13 | "dbus-config", 14 | "desktop", 15 | "winit", 16 | "wayland", 17 | "tokio", 18 | "single-instance", 19 | "desktop-systemd-scope", 20 | "xdg-portal", 21 | ] } 22 | tokio = { version = "1.47", features = ["sync", "rt", "process"] } 23 | sctk = { package = "smithay-client-toolkit", version = "0.20.0" } 24 | pretty_env_logger = "0.5" 25 | log = "0.4" 26 | futures = "0.3.31" 27 | # Application i18n 28 | i18n-embed = { version = "0.16", features = [ 29 | "fluent-system", 30 | "desktop-requester", 31 | ] } 32 | i18n-embed-fl = "0.10" 33 | rust-embed = "8.7" 34 | shlex = "1.3.0" 35 | serde = { version = "1.0.219", features = ["derive"] } 36 | ron = "0.11" 37 | notify = "*" 38 | anyhow = "1.0" 39 | itertools = "0.14" 40 | url = "2.5" 41 | nix = "0.30" 42 | clap = { version = "4.5", features = ["derive"] } 43 | switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" } 44 | cosmic-app-list-config = { git = "https://github.com/pop-os/cosmic-applets" } 45 | 46 | [profile.release] 47 | lto = "thin" 48 | 49 | # [patch."https://github.com/pop-os/libcosmic/"] 50 | # libcosmic = { git = "https://github.com/pop-os/libcosmic//", rev = "e838616" } 51 | # cosmic-config = { git = "https://github.com/pop-os/libcosmic//", rev = "e838616" } 52 | 53 | # cosmic-config = { path = "../../libcosmic/cosmic-config/" } 54 | # libcosmic = { path = "../../libcosmic/" } 55 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU General Public License 2 | ========================== 3 | 4 | _Version 3, 29 June 2007_ 5 | _Copyright © 2007 Free Software Foundation, Inc. <>_ 6 | 7 | Everyone is permitted to copy and distribute verbatim copies of this license 8 | document, but changing it is not allowed. 9 | 10 | ## Preamble 11 | 12 | The GNU General Public License is a free, copyleft license for software and other 13 | kinds of works. 14 | 15 | The licenses for most software and other practical works are designed to take away 16 | your freedom to share and change the works. By contrast, the GNU General Public 17 | License is intended to guarantee your freedom to share and change all versions of a 18 | program--to make sure it remains free software for all its users. We, the Free 19 | Software Foundation, use the GNU General Public License for most of our software; it 20 | applies also to any other work released this way by its authors. You can apply it to 21 | your programs, too. 22 | 23 | When we speak of free software, we are referring to freedom, not price. Our General 24 | Public Licenses are designed to make sure that you have the freedom to distribute 25 | copies of free software (and charge for them if you wish), that you receive source 26 | code or can get it if you want it, that you can change the software or use pieces of 27 | it in new free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you these rights or 30 | asking you to surrender the rights. Therefore, you have certain responsibilities if 31 | you distribute copies of the software, or if you modify it: responsibilities to 32 | respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether gratis or for a fee, 35 | you must pass on to the recipients the same freedoms that you received. You must make 36 | sure that they, too, receive or can get the source code. And you must show them these 37 | terms so they know their rights. 38 | 39 | Developers that use the GNU GPL protect your rights with two steps: **(1)** assert 40 | copyright on the software, and **(2)** offer you this License giving you legal permission 41 | to copy, distribute and/or modify it. 42 | 43 | For the developers' and authors' protection, the GPL clearly explains that there is 44 | no warranty for this free software. For both users' and authors' sake, the GPL 45 | requires that modified versions be marked as changed, so that their problems will not 46 | be attributed erroneously to authors of previous versions. 47 | 48 | Some devices are designed to deny users access to install or run modified versions of 49 | the software inside them, although the manufacturer can do so. This is fundamentally 50 | incompatible with the aim of protecting users' freedom to change the software. The 51 | cosmic-systematic pattern of such abuse occurs in the area of products for individuals to 52 | use, which is precisely where it is most unacceptable. Therefore, we have designed 53 | this version of the GPL to prohibit the practice for those products. If such problems 54 | arise substantially in other domains, we stand ready to extend this provision to 55 | those domains in future versions of the GPL, as needed to protect the freedom of 56 | users. 57 | 58 | Finally, every program is threatened constantly by software patents. States should 59 | not allow patents to restrict development and use of software on general-purpose 60 | computers, but in those that do, we wish to avoid the special danger that patents 61 | applied to a free program could make it effectively proprietary. To prevent this, the 62 | GPL assures that patents cannot be used to render the program non-free. 63 | 64 | The precise terms and conditions for copying, distribution and modification follow. 65 | 66 | ## TERMS AND CONDITIONS 67 | 68 | ### 0. Definitions 69 | 70 | “This License” refers to version 3 of the GNU General Public License. 71 | 72 | “Copyright” also means copyright-like laws that apply to other kinds of 73 | works, such as semiconductor masks. 74 | 75 | “The Program” refers to any copyrightable work licensed under this 76 | License. Each licensee is addressed as “you”. “Licensees” and 77 | “recipients” may be individuals or organizations. 78 | 79 | To “modify” a work means to copy from or adapt all or part of the work in 80 | a fashion requiring copyright permission, other than the making of an exact copy. The 81 | resulting work is called a “modified version” of the earlier work or a 82 | work “based on” the earlier work. 83 | 84 | A “covered work” means either the unmodified Program or a work based on 85 | the Program. 86 | 87 | To “propagate” a work means to do anything with it that, without 88 | permission, would make you directly or secondarily liable for infringement under 89 | applicable copyright law, except executing it on a computer or modifying a private 90 | copy. Propagation includes copying, distribution (with or without modification), 91 | making available to the public, and in some countries other activities as well. 92 | 93 | To “convey” a work means any kind of propagation that enables other 94 | parties to make or receive copies. Mere interaction with a user through a computer 95 | network, with no transfer of a copy, is not conveying. 96 | 97 | An interactive user interface displays “Appropriate Legal Notices” to the 98 | extent that it includes a convenient and prominently visible feature that **(1)** 99 | displays an appropriate copyright notice, and **(2)** tells the user that there is no 100 | warranty for the work (except to the extent that warranties are provided), that 101 | licensees may convey the work under this License, and how to view a copy of this 102 | License. If the interface presents a list of user commands or options, such as a 103 | menu, a prominent item in the list meets this criterion. 104 | 105 | ### 1. Source Code 106 | 107 | The “source code” for a work means the preferred form of the work for 108 | making modifications to it. “Object code” means any non-source form of a 109 | work. 110 | 111 | A “Standard Interface” means an interface that either is an official 112 | standard defined by a recognized standards body, or, in the case of interfaces 113 | specified for a particular programming language, one that is widely used among 114 | developers working in that language. 115 | 116 | The “System Libraries” of an executable work include anything, other than 117 | the work as a whole, that **(a)** is included in the normal form of packaging a Major 118 | Component, but which is not part of that Major Component, and **(b)** serves only to 119 | enable use of the work with that Major Component, or to implement a Standard 120 | Interface for which an implementation is available to the public in source code form. 121 | A “Major Component”, in this context, means a major essential component 122 | (kernel, window system, and so on) of the specific operating system (if any) on which 123 | the executable work runs, or a compiler used to produce the work, or an object code 124 | interpreter used to run it. 125 | 126 | The “Corresponding Source” for a work in object code form means all the 127 | source code needed to generate, install, and (for an executable work) run the object 128 | code and to modify the work, including scripts to control those activities. However, 129 | it does not include the work's System Libraries, or general-purpose tools or 130 | generally available free programs which are used unmodified in performing those 131 | activities but which are not part of the work. For example, Corresponding Source 132 | includes interface definition files associated with source files for the work, and 133 | the source code for shared libraries and dynamically linked subprograms that the work 134 | is specifically designed to require, such as by intimate data communication or 135 | control flow between those subprograms and other parts of the work. 136 | 137 | The Corresponding Source need not include anything that users can regenerate 138 | automatically from other parts of the Corresponding Source. 139 | 140 | The Corresponding Source for a work in source code form is that same work. 141 | 142 | ### 2. Basic Permissions 143 | 144 | All rights granted under this License are granted for the term of copyright on the 145 | Program, and are irrevocable provided the stated conditions are met. This License 146 | explicitly affirms your unlimited permission to run the unmodified Program. The 147 | output from running a covered work is covered by this License only if the output, 148 | given its content, constitutes a covered work. This License acknowledges your rights 149 | of fair use or other equivalent, as provided by copyright law. 150 | 151 | You may make, run and propagate covered works that you do not convey, without 152 | conditions so long as your license otherwise remains in force. You may convey covered 153 | works to others for the sole purpose of having them make modifications exclusively 154 | for you, or provide you with facilities for running those works, provided that you 155 | comply with the terms of this License in conveying all material for which you do not 156 | control copyright. Those thus making or running the covered works for you must do so 157 | exclusively on your behalf, under your direction and control, on terms that prohibit 158 | them from making any copies of your copyrighted material outside their relationship 159 | with you. 160 | 161 | Conveying under any other circumstances is permitted solely under the conditions 162 | stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 163 | 164 | ### 3. Protecting Users' Legal Rights From Anti-Circumvention Law 165 | 166 | No covered work shall be deemed part of an effective technological measure under any 167 | applicable law fulfilling obligations under article 11 of the WIPO copyright treaty 168 | adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention 169 | of such measures. 170 | 171 | When you convey a covered work, you waive any legal power to forbid circumvention of 172 | technological measures to the extent such circumvention is effected by exercising 173 | rights under this License with respect to the covered work, and you disclaim any 174 | intention to limit operation or modification of the work as a means of enforcing, 175 | against the work's users, your or third parties' legal rights to forbid circumvention 176 | of technological measures. 177 | 178 | ### 4. Conveying Verbatim Copies 179 | 180 | You may convey verbatim copies of the Program's source code as you receive it, in any 181 | medium, provided that you conspicuously and appropriately publish on each copy an 182 | appropriate copyright notice; keep intact all notices stating that this License and 183 | any non-permissive terms added in accord with section 7 apply to the code; keep 184 | intact all notices of the absence of any warranty; and give all recipients a copy of 185 | this License along with the Program. 186 | 187 | You may charge any price or no price for each copy that you convey, and you may offer 188 | support or warranty protection for a fee. 189 | 190 | ### 5. Conveying Modified Source Versions 191 | 192 | You may convey a work based on the Program, or the modifications to produce it from 193 | the Program, in the form of source code under the terms of section 4, provided that 194 | you also meet all of these conditions: 195 | 196 | * **a)** The work must carry prominent notices stating that you modified it, and giving a 197 | relevant date. 198 | * **b)** The work must carry prominent notices stating that it is released under this 199 | License and any conditions added under section 7. This requirement modifies the 200 | requirement in section 4 to “keep intact all notices”. 201 | * **c)** You must license the entire work, as a whole, under this License to anyone who 202 | comes into possession of a copy. This License will therefore apply, along with any 203 | applicable section 7 additional terms, to the whole of the work, and all its parts, 204 | regardless of how they are packaged. This License gives no permission to license the 205 | work in any other way, but it does not invalidate such permission if you have 206 | separately received it. 207 | * **d)** If the work has interactive user interfaces, each must display Appropriate Legal 208 | Notices; however, if the Program has interactive interfaces that do not display 209 | Appropriate Legal Notices, your work need not make them do so. 210 | 211 | A compilation of a covered work with other separate and independent works, which are 212 | not by their nature extensions of the covered work, and which are not combined with 213 | it such as to form a larger program, in or on a volume of a storage or distribution 214 | medium, is called an “aggregate” if the compilation and its resulting 215 | copyright are not used to limit the access or legal rights of the compilation's users 216 | beyond what the individual works permit. Inclusion of a covered work in an aggregate 217 | does not cause this License to apply to the other parts of the aggregate. 218 | 219 | ### 6. Conveying Non-Source Forms 220 | 221 | You may convey a covered work in object code form under the terms of sections 4 and 222 | 5, provided that you also convey the machine-readable Corresponding Source under the 223 | terms of this License, in one of these ways: 224 | 225 | * **a)** Convey the object code in, or embodied in, a physical product (including a 226 | physical distribution medium), accompanied by the Corresponding Source fixed on a 227 | durable physical medium customarily used for software interchange. 228 | * **b)** Convey the object code in, or embodied in, a physical product (including a 229 | physical distribution medium), accompanied by a written offer, valid for at least 230 | three years and valid for as long as you offer spare parts or customer support for 231 | that product model, to give anyone who possesses the object code either **(1)** a copy of 232 | the Corresponding Source for all the software in the product that is covered by this 233 | License, on a durable physical medium customarily used for software interchange, for 234 | a price no more than your reasonable cost of physically performing this conveying of 235 | source, or **(2)** access to copy the Corresponding Source from a network server at no 236 | charge. 237 | * **c)** Convey individual copies of the object code with a copy of the written offer to 238 | provide the Corresponding Source. This alternative is allowed only occasionally and 239 | noncommercially, and only if you received the object code with such an offer, in 240 | accord with subsection 6b. 241 | * **d)** Convey the object code by offering access from a designated place (gratis or for 242 | a charge), and offer equivalent access to the Corresponding Source in the same way 243 | through the same place at no further charge. You need not require recipients to copy 244 | the Corresponding Source along with the object code. If the place to copy the object 245 | code is a network server, the Corresponding Source may be on a different server 246 | (operated by you or a third party) that supports equivalent copying facilities, 247 | provided you maintain clear directions next to the object code saying where to find 248 | the Corresponding Source. Regardless of what server hosts the Corresponding Source, 249 | you remain obligated to ensure that it is available for as long as needed to satisfy 250 | these requirements. 251 | * **e)** Convey the object code using peer-to-peer transmission, provided you inform 252 | other peers where the object code and Corresponding Source of the work are being 253 | offered to the general public at no charge under subsection 6d. 254 | 255 | A separable portion of the object code, whose source code is excluded from the 256 | Corresponding Source as a System Library, need not be included in conveying the 257 | object code work. 258 | 259 | A “User Product” is either **(1)** a “consumer product”, which 260 | means any tangible personal property which is normally used for personal, family, or 261 | household purposes, or **(2)** anything designed or sold for incorporation into a 262 | dwelling. In determining whether a product is a consumer product, doubtful cases 263 | shall be resolved in favor of coverage. For a particular product received by a 264 | particular user, “normally used” refers to a typical or common use of 265 | that class of product, regardless of the status of the particular user or of the way 266 | in which the particular user actually uses, or expects or is expected to use, the 267 | product. A product is a consumer product regardless of whether the product has 268 | substantial commercial, industrial or non-consumer uses, unless such uses represent 269 | the only significant mode of use of the product. 270 | 271 | “Installation Information” for a User Product means any methods, 272 | procedures, authorization keys, or other information required to install and execute 273 | modified versions of a covered work in that User Product from a modified version of 274 | its Corresponding Source. The information must suffice to ensure that the continued 275 | functioning of the modified object code is in no case prevented or interfered with 276 | solely because modification has been made. 277 | 278 | If you convey an object code work under this section in, or with, or specifically for 279 | use in, a User Product, and the conveying occurs as part of a transaction in which 280 | the right of possession and use of the User Product is transferred to the recipient 281 | in perpetuity or for a fixed term (regardless of how the transaction is 282 | characterized), the Corresponding Source conveyed under this section must be 283 | accompanied by the Installation Information. But this requirement does not apply if 284 | neither you nor any third party retains the ability to install modified object code 285 | on the User Product (for example, the work has been installed in ROM). 286 | 287 | The requirement to provide Installation Information does not include a requirement to 288 | continue to provide support service, warranty, or updates for a work that has been 289 | modified or installed by the recipient, or for the User Product in which it has been 290 | modified or installed. Access to a network may be denied when the modification itself 291 | materially and adversely affects the operation of the network or violates the rules 292 | and protocols for communication across the network. 293 | 294 | Corresponding Source conveyed, and Installation Information provided, in accord with 295 | this section must be in a format that is publicly documented (and with an 296 | implementation available to the public in source code form), and must require no 297 | special password or key for unpacking, reading or copying. 298 | 299 | ### 7. Additional Terms 300 | 301 | “Additional permissions” are terms that supplement the terms of this 302 | License by making exceptions from one or more of its conditions. Additional 303 | permissions that are applicable to the entire Program shall be treated as though they 304 | were included in this License, to the extent that they are valid under applicable 305 | law. If additional permissions apply only to part of the Program, that part may be 306 | used separately under those permissions, but the entire Program remains governed by 307 | this License without regard to the additional permissions. 308 | 309 | When you convey a copy of a covered work, you may at your option remove any 310 | additional permissions from that copy, or from any part of it. (Additional 311 | permissions may be written to require their own removal in certain cases when you 312 | modify the work.) You may place additional permissions on material, added by you to a 313 | covered work, for which you have or can give appropriate copyright permission. 314 | 315 | Notwithstanding any other provision of this License, for material you add to a 316 | covered work, you may (if authorized by the copyright holders of that material) 317 | supplement the terms of this License with terms: 318 | 319 | * **a)** Disclaiming warranty or limiting liability differently from the terms of 320 | sections 15 and 16 of this License; or 321 | * **b)** Requiring preservation of specified reasonable legal notices or author 322 | attributions in that material or in the Appropriate Legal Notices displayed by works 323 | containing it; or 324 | * **c)** Prohibiting misrepresentation of the origin of that material, or requiring that 325 | modified versions of such material be marked in reasonable ways as different from the 326 | original version; or 327 | * **d)** Limiting the use for publicity purposes of names of licensors or authors of the 328 | material; or 329 | * **e)** Declining to grant rights under trademark law for use of some trade names, 330 | trademarks, or service marks; or 331 | * **f)** Requiring indemnification of licensors and authors of that material by anyone 332 | who conveys the material (or modified versions of it) with contractual assumptions of 333 | liability to the recipient, for any liability that these contractual assumptions 334 | directly impose on those licensors and authors. 335 | 336 | All other non-permissive additional terms are considered “further 337 | restrictions” within the meaning of section 10. If the Program as you received 338 | it, or any part of it, contains a notice stating that it is governed by this License 339 | along with a term that is a further restriction, you may remove that term. If a 340 | license document contains a further restriction but permits relicensing or conveying 341 | under this License, you may add to a covered work material governed by the terms of 342 | that license document, provided that the further restriction does not survive such 343 | relicensing or conveying. 344 | 345 | If you add terms to a covered work in accord with this section, you must place, in 346 | the relevant source files, a statement of the additional terms that apply to those 347 | files, or a notice indicating where to find the applicable terms. 348 | 349 | Additional terms, permissive or non-permissive, may be stated in the form of a 350 | separately written license, or stated as exceptions; the above requirements apply 351 | either way. 352 | 353 | ### 8. Termination 354 | 355 | You may not propagate or modify a covered work except as expressly provided under 356 | this License. Any attempt otherwise to propagate or modify it is void, and will 357 | automatically terminate your rights under this License (including any patent licenses 358 | granted under the third paragraph of section 11). 359 | 360 | However, if you cease all violation of this License, then your license from a 361 | particular copyright holder is reinstated **(a)** provisionally, unless and until the 362 | copyright holder explicitly and finally terminates your license, and **(b)** permanently, 363 | if the copyright holder fails to notify you of the violation by some reasonable means 364 | prior to 60 days after the cessation. 365 | 366 | Moreover, your license from a particular copyright holder is reinstated permanently 367 | if the copyright holder notifies you of the violation by some reasonable means, this 368 | is the first time you have received notice of violation of this License (for any 369 | work) from that copyright holder, and you cure the violation prior to 30 days after 370 | your receipt of the notice. 371 | 372 | Termination of your rights under this section does not terminate the licenses of 373 | parties who have received copies or rights from you under this License. If your 374 | rights have been terminated and not permanently reinstated, you do not qualify to 375 | receive new licenses for the same material under section 10. 376 | 377 | ### 9. Acceptance Not Required for Having Copies 378 | 379 | You are not required to accept this License in order to receive or run a copy of the 380 | Program. Ancillary propagation of a covered work occurring solely as a consequence of 381 | using peer-to-peer transmission to receive a copy likewise does not require 382 | acceptance. However, nothing other than this License grants you permission to 383 | propagate or modify any covered work. These actions infringe copyright if you do not 384 | accept this License. Therefore, by modifying or propagating a covered work, you 385 | indicate your acceptance of this License to do so. 386 | 387 | ### 10. Automatic Licensing of Downstream Recipients 388 | 389 | Each time you convey a covered work, the recipient automatically receives a license 390 | from the original licensors, to run, modify and propagate that work, subject to this 391 | License. You are not responsible for enforcing compliance by third parties with this 392 | License. 393 | 394 | An “entity transaction” is a transaction transferring control of an 395 | organization, or substantially all assets of one, or subdividing an organization, or 396 | merging organizations. If propagation of a covered work results from an entity 397 | transaction, each party to that transaction who receives a copy of the work also 398 | receives whatever licenses to the work the party's predecessor in interest had or 399 | could give under the previous paragraph, plus a right to possession of the 400 | Corresponding Source of the work from the predecessor in interest, if the predecessor 401 | has it or can get it with reasonable efforts. 402 | 403 | You may not impose any further restrictions on the exercise of the rights granted or 404 | affirmed under this License. For example, you may not impose a license fee, royalty, 405 | or other charge for exercise of rights granted under this License, and you may not 406 | initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging 407 | that any patent claim is infringed by making, using, selling, offering for sale, or 408 | importing the Program or any portion of it. 409 | 410 | ### 11. Patents 411 | 412 | A “contributor” is a copyright holder who authorizes use under this 413 | License of the Program or a work on which the Program is based. The work thus 414 | licensed is called the contributor's “contributor version”. 415 | 416 | A contributor's “essential patent claims” are all patent claims owned or 417 | controlled by the contributor, whether already acquired or hereafter acquired, that 418 | would be infringed by some manner, permitted by this License, of making, using, or 419 | selling its contributor version, but do not include claims that would be infringed 420 | only as a consequence of further modification of the contributor version. For 421 | purposes of this definition, “control” includes the right to grant patent 422 | sublicenses in a manner consistent with the requirements of this License. 423 | 424 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent license 425 | under the contributor's essential patent claims, to make, use, sell, offer for sale, 426 | import and otherwise run, modify and propagate the contents of its contributor 427 | version. 428 | 429 | In the following three paragraphs, a “patent license” is any express 430 | agreement or commitment, however denominated, not to enforce a patent (such as an 431 | express permission to practice a patent or covenant not to sue for patent 432 | infringement). To “grant” such a patent license to a party means to make 433 | such an agreement or commitment not to enforce a patent against the party. 434 | 435 | If you convey a covered work, knowingly relying on a patent license, and the 436 | Corresponding Source of the work is not available for anyone to copy, free of charge 437 | and under the terms of this License, through a publicly available network server or 438 | other readily accessible means, then you must either **(1)** cause the Corresponding 439 | Source to be so available, or **(2)** arrange to deprive yourself of the benefit of the 440 | patent license for this particular work, or **(3)** arrange, in a manner consistent with 441 | the requirements of this License, to extend the patent license to downstream 442 | recipients. “Knowingly relying” means you have actual knowledge that, but 443 | for the patent license, your conveying the covered work in a country, or your 444 | recipient's use of the covered work in a country, would infringe one or more 445 | identifiable patents in that country that you have reason to believe are valid. 446 | 447 | If, pursuant to or in connection with a single transaction or arrangement, you 448 | convey, or propagate by procuring conveyance of, a covered work, and grant a patent 449 | license to some of the parties receiving the covered work authorizing them to use, 450 | propagate, modify or convey a specific copy of the covered work, then the patent 451 | license you grant is automatically extended to all recipients of the covered work and 452 | works based on it. 453 | 454 | A patent license is “discriminatory” if it does not include within the 455 | scope of its coverage, prohibits the exercise of, or is conditioned on the 456 | non-exercise of one or more of the rights that are specifically granted under this 457 | License. You may not convey a covered work if you are a party to an arrangement with 458 | a third party that is in the business of distributing software, under which you make 459 | payment to the third party based on the extent of your activity of conveying the 460 | work, and under which the third party grants, to any of the parties who would receive 461 | the covered work from you, a discriminatory patent license **(a)** in connection with 462 | copies of the covered work conveyed by you (or copies made from those copies), or **(b)** 463 | primarily for and in connection with specific products or compilations that contain 464 | the covered work, unless you entered into that arrangement, or that patent license 465 | was granted, prior to 28 March 2007. 466 | 467 | Nothing in this License shall be construed as excluding or limiting any implied 468 | license or other defenses to infringement that may otherwise be available to you 469 | under applicable patent law. 470 | 471 | ### 12. No Surrender of Others' Freedom 472 | 473 | If conditions are imposed on you (whether by court order, agreement or otherwise) 474 | that contradict the conditions of this License, they do not excuse you from the 475 | conditions of this License. If you cannot convey a covered work so as to satisfy 476 | simultaneously your obligations under this License and any other pertinent 477 | obligations, then as a consequence you may not convey it at all. For example, if you 478 | agree to terms that obligate you to collect a royalty for further conveying from 479 | those to whom you convey the Program, the only way you could satisfy both those terms 480 | and this License would be to refrain entirely from conveying the Program. 481 | 482 | ### 13. Use with the GNU Affero General Public License 483 | 484 | Notwithstanding any other provision of this License, you have permission to link or 485 | combine any covered work with a work licensed under version 3 of the GNU Affero 486 | General Public License into a single combined work, and to convey the resulting work. 487 | The terms of this License will continue to apply to the part which is the covered 488 | work, but the special requirements of the GNU Affero General Public License, section 489 | 13, concerning interaction through a network will apply to the combination as such. 490 | 491 | ### 14. Revised Versions of this License 492 | 493 | The Free Software Foundation may publish revised and/or new versions of the GNU 494 | General Public License from time to time. Such new versions will be similar in spirit 495 | to the present version, but may differ in detail to address new problems or concerns. 496 | 497 | Each version is given a distinguishing version number. If the Program specifies that 498 | a certain numbered version of the GNU General Public License “or any later 499 | version” applies to it, you have the option of following the terms and 500 | conditions either of that numbered version or of any later version published by the 501 | Free Software Foundation. If the Program does not specify a version number of the GNU 502 | General Public License, you may choose any version ever published by the Free 503 | Software Foundation. 504 | 505 | If the Program specifies that a proxy can decide which future versions of the GNU 506 | General Public License can be used, that proxy's public statement of acceptance of a 507 | version permanently authorizes you to choose that version for the Program. 508 | 509 | Later license versions may give you additional or different permissions. However, no 510 | additional obligations are imposed on any author or copyright holder as a result of 511 | your choosing to follow a later version. 512 | 513 | ### 15. Disclaimer of Warranty 514 | 515 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 516 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 517 | PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER 518 | EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 519 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE 520 | QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 521 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 522 | 523 | ### 16. Limitation of Liability 524 | 525 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY 526 | COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS 527 | PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, 528 | INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 529 | PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE 530 | OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE 531 | WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 532 | POSSIBILITY OF SUCH DAMAGES. 533 | 534 | ### 17. Interpretation of Sections 15 and 16 535 | 536 | If the disclaimer of warranty and limitation of liability provided above cannot be 537 | given local legal effect according to their terms, reviewing courts shall apply local 538 | law that most closely approximates an absolute waiver of all civil liability in 539 | connection with the Program, unless a warranty or assumption of liability accompanies 540 | a copy of the Program in return for a fee. 541 | 542 | _END OF TERMS AND CONDITIONS_ 543 | 544 | ## How to Apply These Terms to Your New Programs 545 | 546 | If you develop a new program, and you want it to be of the greatest possible use to 547 | the public, the best way to achieve this is to make it free software which everyone 548 | can redistribute and change under these terms. 549 | 550 | To do so, attach the following notices to the program. It is safest to attach them 551 | to the start of each source file to most effectively state the exclusion of warranty; 552 | and each file should have at least the “copyright” line and a pointer to 553 | where the full notice is found. 554 | 555 | 556 | Copyright (C) 557 | 558 | This program is free software: you can redistribute it and/or modify 559 | it under the terms of the GNU General Public License as published by 560 | the Free Software Foundation, either version 3 of the License, or 561 | (at your option) any later version. 562 | 563 | This program is distributed in the hope that it will be useful, 564 | but WITHOUT ANY WARRANTY; without even the implied warranty of 565 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 566 | GNU General Public License for more details. 567 | 568 | You should have received a copy of the GNU General Public License 569 | along with this program. If not, see . 570 | 571 | Also add information on how to contact you by electronic and paper mail. 572 | 573 | If the program does terminal interaction, make it output a short notice like this 574 | when it starts in an interactive mode: 575 | 576 | Copyright (C) 577 | This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. 578 | This is free software, and you are welcome to redistribute it 579 | under certain conditions; type 'show c' for details. 580 | 581 | The hypothetical commands `show w` and `show c` should show the appropriate parts of 582 | the General Public License. Of course, your program's commands might be different; 583 | for a GUI interface, you would use an “about box”. 584 | 585 | You should also get your employer (if you work as a programmer) or school, if any, to 586 | sign a “copyright disclaimer” for the program, if necessary. For more 587 | information on this, and how to apply and follow the GNU GPL, see 588 | <>. 589 | 590 | The GNU General Public License does not permit incorporating your program into 591 | proprietary programs. If your program is a subroutine library, you may consider it 592 | more useful to permit linking proprietary applications with the library. If this is 593 | what you want to do, use the GNU Lesser General Public License instead of this 594 | License. But first, please read 595 | <>. 596 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cosmic App Library 2 | 3 | Cosmic App Library is an application launcher for the COSMIC desktop that lists all installed applications in a grid. 4 | 5 | ## Building/Installing 6 | 7 | ```just 8 | # build 9 | just build-release 10 | # install 11 | just install 12 | ``` 13 | 14 | ```just 15 | # uninstall 16 | just uninstall 17 | ``` -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() {} 2 | -------------------------------------------------------------------------------- /data/com.system76.CosmicAppLibrary.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Applications 3 | Name[ar]=تطبيقات 4 | Name[cs]=Aplikace 5 | Name[zh_CN]=应用程序库 6 | Name[pl]=Aplikacje 7 | Name[hu]=Alkalmazások 8 | Name[pt_BR]=Aplicativos 9 | Name[pt]=Aplicativos 10 | Name[ru]=Приложения 11 | Name[sk]=Aplikácie 12 | Name[es]=Aplicaciones 13 | Comment=COSMIC App Library shell application 14 | Comment[ar]=تطبيق صدفة مكتبة تطبيقات COSMIC 15 | Comment[cs]=Shell aplikace knihovny aplikací COSMIC 16 | Comment[zh_CN]=COSMIC应用程序库命令行程序 17 | Comment[pl]=Aplikacja powłoki Biblioteka aplikacji COSMIC 18 | Comment[hu]=A COSMIC alkalmazáskönyvtár felületalkalmazása 19 | Comment[pt_BR]=Biblioteca de aplicativos 20 | Comment[pt]=Biblioteca de aplicativos 21 | Comment[ru]=Библиотека приложений COSMIC 22 | Comment[sk]=Shell aplikácia knižnice aplikácií COSMIC 23 | Comment[es]=Aplicación shell de biblioteca para aplicaciones COSMIC 24 | Type=Application 25 | Exec=cosmic-app-library 26 | Terminal=false 27 | Categories=COSMIC; 28 | # Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! 29 | Keywords=COSMIC;ICED; 30 | # Translators: Do NOT translate or transliterate this text (this is an icon file name)! 31 | Icon=com.system76.CosmicAppLibrary 32 | StartupNotify=true 33 | NoDisplay=true 34 | -------------------------------------------------------------------------------- /data/com.system76.CosmicAppLibrary.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.system76.CosmicAppLibrary 5 | CC0 6 | 7 | 8 | COSMIC App Library 9 | مكتبة تطبيقات COSMIC 10 | Knihovna aplikací COSMIC 11 | COSMIC Alkalmazáskönyvtár 12 | Biblioteca de Aplicativos 13 | Biblioteca de Aplicativos 14 | Библиотека приложений COSMIC 15 | Knižnica aplikácií COSMIC 16 | Biblioteca de aplicaciones COSMIC 17 | COSMIC desktop application library 18 | مكتبة تطبيقات سطح المكتب COSMIC 19 | Knihovna aplikací pro prostředí COSMIC 20 | A COSMIC asztali környezet alkalmazáskönyvtára 21 | Biblioteca de aplicativos do desktop COSMIC 22 | Biblioteca de aplicativos do desktop COSMIC 23 | Библиотека приложений оболочки COSMIC 24 | Knižnica aplikácií pre pracovné prostredie COSMIC 25 | Biblioteca de aplicaciones para el escritorio COSMIC 26 | 27 |

The default application library for the COSMIC desktop

28 |

مكتبة التطبيقات المبدئية لسطح مكتب COSMIC

29 |

Výchozí knihovna aplikací pro prostředí COSMIC

30 |

A COSMIC asztali környezet alapértelmezett alkalmazáskönyvtára

31 |

A aplicação padrão de biblioteca de aplicativos do desktop COSMIC

32 |

A aplicação padrão de biblioteca de aplicativos do desktop COSMIC

33 |

Библиотека приложений по умолчанию для оболочки COSMIC

34 |

Predvolená knižnica aplikácií pre pracovné prostredie COSMIC

35 |

Biblioteca de aplicaciones para el escritorio COSMIC

36 |
37 | 43 | https://github.com/pop-os/cosmic-applibrary 44 | https://github.com/pop-os/cosmic-applibrary/issues 45 | 46 | 47 | 48 | 49 | 50 | 54 | ModernToolkit 55 | HiDpiIcon 56 | 57 | Ashley Wulber 58 | ashley@system76.com 59 | cosmic-app-library 60 | com.system76.CosmicAppLibrary.desktop 61 |
62 | -------------------------------------------------------------------------------- /data/icons/app-source-flatpak.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /data/icons/app-source-local-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /data/icons/app-source-nix.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /data/icons/app-source-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/icons/app-source-system-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /data/icons/com.system76.CosmicAppLibrary.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | 59 | 60 | 61 | 62 | 64 | 65 | 66 | 67 | 69 | 70 | 71 | 72 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /data/icons/justfile: -------------------------------------------------------------------------------- 1 | appid := env_var('APPID') 2 | install-dir := env_var('INSTALL_DIR') 3 | 4 | scalable-src := appid + '.svg' 5 | scalable-dst := install-dir / 'icons' / 'hicolor' / 'scalable' / 'apps' / scalable-src 6 | 7 | install: 8 | install -Dm0644 {{scalable-src}} {{scalable-dst}} 9 | 10 | 11 | uninstall: 12 | rm {{scalable-dst}} 13 | -------------------------------------------------------------------------------- /data/justfile: -------------------------------------------------------------------------------- 1 | appid := env_var('APPID') 2 | install-dir := env_var('INSTALL_DIR') 3 | 4 | desktop-src := appid + '.desktop' 5 | desktop-dst := install-dir / 'applications' / desktop-src 6 | 7 | metainfo-src := appid + '.metainfo.xml' 8 | metainfo-dst := install-dir / 'metainfo' / metainfo-src 9 | 10 | install: 11 | install -Dm0644 {{desktop-src}} {{desktop-dst}} 12 | install -Dm0644 {{metainfo-src}} {{metainfo-dst}} 13 | 14 | uninstall: 15 | rm {{desktop-dst}} {{metainfo-dst}} -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | cosmic-app-library (0.1.0) UNRELEASED; urgency=medium 2 | 3 | * Initial release. 4 | 5 | -- Ashley Wulber Thu, 07 Apr 2022 09:39:19 -0700 6 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: cosmic-app-library 2 | Section: admin 3 | Priority: optional 4 | Maintainer: Ashley Wulber 5 | Build-Depends: 6 | debhelper (>= 11), 7 | debhelper-compat (= 11), 8 | rust-all, 9 | just, 10 | pkg-config, 11 | libxkbcommon-dev, 12 | libwayland-dev, 13 | Standards-Version: 4.3.0 14 | Homepage: https://github.com/pop-os/cosmic-app-library 15 | 16 | Package: cosmic-app-library 17 | Architecture: amd64 arm64 18 | Depends: 19 | ${misc:Depends}, 20 | ${shlibs:Depends} 21 | Description: Cosmic App Library 22 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: cosmic-app-library 3 | Source: https://github.com/pop-os/cosmic-app-library 4 | 5 | Files: * 6 | Copyright: Copyright 2022 System76 7 | License: GPL-3.0 -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | /usr/bin/cosmic-app-library 2 | /usr/share/applications/com.system76.CosmicAppLibrary.desktop 3 | /usr/share/icons/hicolor/scalable/apps/com.system76.CosmicAppLibrary.svg 4 | /usr/share/icons/hicolor/symbolic/apps/com.system76.CosmicAppLibrary-symbolic.svg 5 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | DESTDIR = debian/cosmic-app-library 4 | CLEAN ?= 1 5 | VENDOR ?= 1 6 | 7 | 8 | %: 9 | dh $@ 10 | 11 | override_dh_shlibdeps: 12 | dh_shlibdeps --dpkg-shlibdeps-params=--ignore-missing-info 13 | 14 | override_dh_auto_clean: 15 | if test "${CLEAN}" = "1"; then \ 16 | cargo clean; \ 17 | fi 18 | 19 | if ! ischroot && test "${VENDOR}" = "1"; then \ 20 | mkdir -p .cargo; \ 21 | cargo vendor --sync Cargo.toml | head -n -1 > .cargo/config; \ 22 | echo 'directory = "vendor"' >> .cargo/config; \ 23 | tar pcf vendor.tar vendor; \ 24 | rm -rf vendor; \ 25 | fi 26 | 27 | override_dh_auto_build: 28 | just build-vendored 29 | 30 | override_dh_install: 31 | just rootdir=$(DESTDIR) install -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "crane": { 4 | "inputs": { 5 | "flake-compat": "flake-compat", 6 | "flake-utils": "flake-utils", 7 | "nixpkgs": [ 8 | "nixpkgs" 9 | ], 10 | "rust-overlay": "rust-overlay" 11 | }, 12 | "locked": { 13 | "lastModified": 1687310026, 14 | "narHash": "sha256-20RHFbrnC+hsG4Hyeg/58LvQAK7JWfFItTPFAFamu8E=", 15 | "owner": "ipetkov", 16 | "repo": "crane", 17 | "rev": "116b32c30b5ff28e49f4fcbeeb1bbe3544593204", 18 | "type": "github" 19 | }, 20 | "original": { 21 | "owner": "ipetkov", 22 | "repo": "crane", 23 | "type": "github" 24 | } 25 | }, 26 | "fenix": { 27 | "inputs": { 28 | "nixpkgs": [ 29 | "nixpkgs" 30 | ], 31 | "rust-analyzer-src": "rust-analyzer-src" 32 | }, 33 | "locked": { 34 | "lastModified": 1687674253, 35 | "narHash": "sha256-wXpJrXGIcGbwyxF1ky1VyPg8YoVuAkoj57+kSRJ5yek=", 36 | "owner": "nix-community", 37 | "repo": "fenix", 38 | "rev": "447bb10577505fb657724a7c02d406bfdec431ed", 39 | "type": "github" 40 | }, 41 | "original": { 42 | "owner": "nix-community", 43 | "repo": "fenix", 44 | "type": "github" 45 | } 46 | }, 47 | "flake-compat": { 48 | "flake": false, 49 | "locked": { 50 | "lastModified": 1673956053, 51 | "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", 52 | "owner": "edolstra", 53 | "repo": "flake-compat", 54 | "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", 55 | "type": "github" 56 | }, 57 | "original": { 58 | "owner": "edolstra", 59 | "repo": "flake-compat", 60 | "type": "github" 61 | } 62 | }, 63 | "flake-utils": { 64 | "inputs": { 65 | "systems": "systems" 66 | }, 67 | "locked": { 68 | "lastModified": 1685518550, 69 | "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", 70 | "owner": "numtide", 71 | "repo": "flake-utils", 72 | "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", 73 | "type": "github" 74 | }, 75 | "original": { 76 | "owner": "numtide", 77 | "repo": "flake-utils", 78 | "type": "github" 79 | } 80 | }, 81 | "flake-utils_2": { 82 | "inputs": { 83 | "systems": "systems_2" 84 | }, 85 | "locked": { 86 | "lastModified": 1687709756, 87 | "narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=", 88 | "owner": "numtide", 89 | "repo": "flake-utils", 90 | "rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7", 91 | "type": "github" 92 | }, 93 | "original": { 94 | "owner": "numtide", 95 | "repo": "flake-utils", 96 | "type": "github" 97 | } 98 | }, 99 | "nix-filter": { 100 | "locked": { 101 | "lastModified": 1687178632, 102 | "narHash": "sha256-HS7YR5erss0JCaUijPeyg2XrisEb959FIct3n2TMGbE=", 103 | "owner": "numtide", 104 | "repo": "nix-filter", 105 | "rev": "d90c75e8319d0dd9be67d933d8eb9d0894ec9174", 106 | "type": "github" 107 | }, 108 | "original": { 109 | "owner": "numtide", 110 | "repo": "nix-filter", 111 | "type": "github" 112 | } 113 | }, 114 | "nixpkgs": { 115 | "locked": { 116 | "lastModified": 1687701825, 117 | "narHash": "sha256-aMC9hqsf+4tJL7aJWSdEUurW2TsjxtDcJBwM9Y4FIYM=", 118 | "owner": "NixOS", 119 | "repo": "nixpkgs", 120 | "rev": "07059ee2fa34f1598758839b9af87eae7f7ae6ea", 121 | "type": "github" 122 | }, 123 | "original": { 124 | "owner": "NixOS", 125 | "ref": "nixpkgs-unstable", 126 | "repo": "nixpkgs", 127 | "type": "github" 128 | } 129 | }, 130 | "root": { 131 | "inputs": { 132 | "crane": "crane", 133 | "fenix": "fenix", 134 | "flake-utils": "flake-utils_2", 135 | "nix-filter": "nix-filter", 136 | "nixpkgs": "nixpkgs" 137 | } 138 | }, 139 | "rust-analyzer-src": { 140 | "flake": false, 141 | "locked": { 142 | "lastModified": 1687543309, 143 | "narHash": "sha256-8oHRXbZ/G3JNtNgdgmZbG+0njutvKYugXxqTwCfeDHM=", 144 | "owner": "rust-lang", 145 | "repo": "rust-analyzer", 146 | "rev": "6ba2590541fb284555596e8b7967b05aaa576c22", 147 | "type": "github" 148 | }, 149 | "original": { 150 | "owner": "rust-lang", 151 | "ref": "nightly", 152 | "repo": "rust-analyzer", 153 | "type": "github" 154 | } 155 | }, 156 | "rust-overlay": { 157 | "inputs": { 158 | "flake-utils": [ 159 | "crane", 160 | "flake-utils" 161 | ], 162 | "nixpkgs": [ 163 | "crane", 164 | "nixpkgs" 165 | ] 166 | }, 167 | "locked": { 168 | "lastModified": 1685759304, 169 | "narHash": "sha256-I3YBH6MS3G5kGzNuc1G0f9uYfTcNY9NYoRc3QsykLk4=", 170 | "owner": "oxalica", 171 | "repo": "rust-overlay", 172 | "rev": "c535b4f3327910c96dcf21851bbdd074d0760290", 173 | "type": "github" 174 | }, 175 | "original": { 176 | "owner": "oxalica", 177 | "repo": "rust-overlay", 178 | "type": "github" 179 | } 180 | }, 181 | "systems": { 182 | "locked": { 183 | "lastModified": 1681028828, 184 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 185 | "owner": "nix-systems", 186 | "repo": "default", 187 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 188 | "type": "github" 189 | }, 190 | "original": { 191 | "owner": "nix-systems", 192 | "repo": "default", 193 | "type": "github" 194 | } 195 | }, 196 | "systems_2": { 197 | "locked": { 198 | "lastModified": 1681028828, 199 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 200 | "owner": "nix-systems", 201 | "repo": "default", 202 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 203 | "type": "github" 204 | }, 205 | "original": { 206 | "owner": "nix-systems", 207 | "repo": "default", 208 | "type": "github" 209 | } 210 | } 211 | }, 212 | "root": "root", 213 | "version": 7 214 | } 215 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Application library for the COSMIC desktop environment"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | nix-filter.url = "github:numtide/nix-filter"; 8 | crane = { 9 | url = "github:ipetkov/crane"; 10 | inputs.nixpkgs.follows = "nixpkgs"; 11 | }; 12 | fenix = { 13 | url = "github:nix-community/fenix"; 14 | inputs.nixpkgs.follows = "nixpkgs"; 15 | }; 16 | }; 17 | 18 | outputs = { self, nixpkgs, flake-utils, nix-filter, crane, fenix }: 19 | flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" ] (system: 20 | let 21 | pkgs = nixpkgs.legacyPackages.${system}; 22 | craneLib = crane.lib.${system}.overrideToolchain fenix.packages.${system}.stable.toolchain; 23 | pkgDef = { 24 | src = nix-filter.lib.filter { 25 | root = ./.; 26 | include = [ 27 | ./src 28 | ./Cargo.toml 29 | ./Cargo.lock 30 | ./i18n 31 | ./i18n.toml 32 | ./data 33 | ./justfile 34 | ]; 35 | }; 36 | nativeBuildInputs = with pkgs; [ 37 | just 38 | pkg-config 39 | autoPatchelfHook 40 | ]; 41 | buildInputs = with pkgs; [ 42 | libxkbcommon 43 | glib 44 | gtk4 45 | desktop-file-utils 46 | ]; 47 | runtimeDependencies = with pkgs; [ 48 | wayland 49 | libglvnd # For libEGL 50 | ]; 51 | }; 52 | 53 | cargoArtifacts = craneLib.buildDepsOnly pkgDef; 54 | cosmic-applibrary = craneLib.buildPackage (pkgDef // { 55 | inherit cargoArtifacts; 56 | }); 57 | in { 58 | checks = { 59 | inherit cosmic-applibrary; 60 | }; 61 | 62 | apps.default = flake-utils.lib.mkApp { 63 | drv = cosmic-applibrary; 64 | }; 65 | packages.default = cosmic-applibrary.overrideAttrs (oldAttrs: rec { 66 | buildPhase = '' 67 | just prefix=$out build-release 68 | ''; 69 | installPhase = '' 70 | just prefix=$out install 71 | ''; 72 | }); 73 | 74 | devShells.default = pkgs.mkShell rec { 75 | inputsFrom = builtins.attrValues self.checks.${system}; 76 | LD_LIBRARY_PATH = pkgs.lib.strings.makeLibraryPath (builtins.concatMap (d: d.runtimeDependencies) inputsFrom); 77 | }; 78 | }); 79 | 80 | nixConfig = { 81 | # Cache for the Rust toolchain in fenix 82 | extra-substituters = [ "https://nix-community.cachix.org" ]; 83 | extra-trusted-public-keys = [ "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" ]; 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /hooks/pre-commit.hook: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Source: https://gitlab.gnome.org/GNOME/fractal/blob/master/hooks/pre-commit.hook 3 | 4 | install_rustfmt() { 5 | if ! which rustup &> /dev/null; then 6 | curl https://sh.rustup.rs -sSf | sh -s -- -y 7 | export PATH=$PATH:$HOME/.cargo/bin 8 | if ! which rustup &> /dev/null; then 9 | echo "Failed to install rustup. Performing the commit without style checking." 10 | exit 0 11 | fi 12 | fi 13 | 14 | if ! rustup component list|grep rustfmt &> /dev/null; then 15 | echo "Installing rustfmt…" 16 | rustup component add rustfmt 17 | fi 18 | } 19 | 20 | if ! which cargo >/dev/null 2>&1 || ! cargo fmt --help >/dev/null 2>&1; then 21 | echo "Unable to check the project’s code style, because rustfmt could not be run." 22 | 23 | if [ ! -t 1 ]; then 24 | # No input is possible 25 | echo "Performing commit." 26 | exit 0 27 | fi 28 | 29 | echo "" 30 | echo "y: Install rustfmt via rustup" 31 | echo "n: Don't install rustfmt and perform the commit" 32 | echo "Q: Don't install rustfmt and abort the commit" 33 | 34 | echo "" 35 | while true 36 | do 37 | echo -n "Install rustfmt via rustup? [y/n/Q]: "; read yn < /dev/tty 38 | case $yn in 39 | [Yy]* ) install_rustfmt; break;; 40 | [Nn]* ) echo "Performing commit."; exit 0;; 41 | [Qq]* | "" ) echo "Aborting commit."; exit -1 >/dev/null 2>&1;; 42 | * ) echo "Invalid input";; 43 | esac 44 | done 45 | 46 | fi 47 | 48 | echo "--Checking style--" 49 | cargo fmt --all -- --check 50 | if test $? != 0; then 51 | echo "--Checking style fail--" 52 | echo "Please fix the above issues, either manually or by running: cargo fmt --all" 53 | 54 | exit -1 55 | else 56 | echo "--Checking style pass--" 57 | fi 58 | -------------------------------------------------------------------------------- /i18n.toml: -------------------------------------------------------------------------------- 1 | fallback_language = "en" 2 | 3 | [fluent] 4 | assets_dir = "i18n" -------------------------------------------------------------------------------- /i18n/af/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/af/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/ar/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = مكتبة تطبيقات COSMIC 2 | cosmic-library-home = مكتبة المنزل 3 | cosmic-office = مكتب 4 | cosmic-system = نظام 5 | cosmic-utilities = أدوات 6 | new-group = أنشئ مجلد 7 | name = الاسم 8 | ok = حسنًا 9 | save = احفظ 10 | cancel = ألغِ 11 | search-placeholder = ...اكتب للبحث عن التطبيقات 12 | new-group-placeholder = اسم المجلد 13 | pin-to-app-tray = ثبِّت في شريط التطبيقات 14 | run = شغِّل 15 | run-on = {$gpu} شغِّل على 16 | run-on-default = (المبدئي) 17 | remove = انقل إلى مكتبة المنزل 18 | create-new = أنشئ جديد 19 | add-group = أضِف مجموعة 20 | delete = احذف 21 | rename = غيّر الاسم 22 | delete-folder = حذف المجلد؟ 23 | .msg = .سيؤدي حذف هذا المجلد إلى نقل أيقونات التطبيقات إلى مكتبة المنزل 24 | flatpak = فلاتباك 25 | snap = سناب 26 | system = النظام 27 | local = محلي 28 | nix = نيكس 29 | -------------------------------------------------------------------------------- /i18n/be/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cancel = Скасаваць 2 | -------------------------------------------------------------------------------- /i18n/bg/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = Библиотека за програми на COSMIC 2 | cosmic-library-home = Начало 3 | cosmic-office = Офис 4 | cosmic-system = Системни 5 | cosmic-utilities = Инструменти 6 | new-group = Нова папка 7 | name = Име 8 | ok = Добре 9 | save = Запазване 10 | cancel = Отказване 11 | search-placeholder = Текст за търсене на програми... 12 | new-group-placeholder = Име на папката 13 | pin-to-app-tray = Добавяне в тавата за програми 14 | run = Изпълняване 15 | run-on = Изпълняване с {$gpu} 16 | run-on-default = (Стандартно) 17 | remove = Преместване в началната страница 18 | create-new = Създаване на нова папка 19 | add-group = Добавяне на група 20 | delete = Изтриване 21 | rename = Преименуване 22 | delete-folder = Изтриване на папката? 23 | .msg = Изтриването на тази папка ще премести иконките на програмата в началната страница на библиотека. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = Системни 27 | local = Локални 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/ca/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/ca/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/cs/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = Knihovna aplikací COSMIC 2 | cosmic-library-home = Domov 3 | cosmic-office = Kancelář 4 | cosmic-system = Systém 5 | cosmic-utilities = Nástroje 6 | new-group = Vytvořit složku 7 | name = Název 8 | ok = OK 9 | save = Uložit 10 | cancel = Zrušit 11 | search-placeholder = Vyhledat aplikace... 12 | new-group-placeholder = Název složky 13 | pin-to-app-tray = Připnout do panelu aplikací 14 | run = Spustit 15 | run-on = Spustit na {$gpu} 16 | run-on-default = (Výchozí) 17 | remove = Přesunout do složky Domov 18 | create-new = Vytvořit novou složku 19 | add-group = Přidat složku 20 | delete = Odstranit 21 | rename = Přejmenovat 22 | delete-folder = Vymazat složku? 23 | .msg = Odstraněním této složky se všechny aplikace přesunou do složky Domov. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = Systém 27 | local = Lokální 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/da/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/da/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/de/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = COSMIC App Library 2 | cosmic-library-home = Bibliothek-Startseite 3 | cosmic-office = Büro 4 | cosmic-system = System 5 | cosmic-utilities = Dienstprogramme 6 | new-group = Ordner erstellen 7 | name = Name 8 | ok = OK 9 | save = Speichern 10 | cancel = Abbrechen 11 | search-placeholder = Nach Apps suchen... 12 | new-group-placeholder = Ordnername 13 | pin-to-app-tray = An die App-Ablage anheften 14 | run = Ausführen 15 | run-on = Ausführen auf {$gpu} 16 | run-on-default = (Standard) 17 | remove = In Bibliothek-Startseite verschieben 18 | add-group = Gruppe erstellen 19 | create-new = Neuen Ordner erstellen 20 | delete = Löschen 21 | rename = Umbenennen 22 | delete-folder = Ordner löschen? 23 | .msg = Durch das Löschen dieses Ordners werden die App-Symbole in die Bibliothek-Startseite verschoben. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = System 27 | local = Lokal 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/el/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/el/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/en-GB/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/en-GB/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/en/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = COSMIC App Library 2 | cosmic-library-home = Library Home 3 | cosmic-office = Office 4 | cosmic-system = System 5 | cosmic-utilities = Utilities 6 | new-group = Create Folder 7 | name = Name 8 | ok = Ok 9 | save = Save 10 | cancel = Cancel 11 | search-placeholder = Type to search apps... 12 | new-group-placeholder = Folder Name 13 | pin-to-app-tray = Pin to App Tray 14 | run = Run 15 | run-on = Run on {$gpu} 16 | run-on-default = (Default) 17 | remove = Move to library home 18 | create-new = Create new folder 19 | add-group = Add group 20 | delete = Delete 21 | rename = Rename 22 | delete-folder = Delete folder? 23 | .msg = Deleting this folder will move the application icons to Library home. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = System 27 | local = Local 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/eo/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/eo/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/es-419/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = Biblioteca de aplicaciones de COSMIC 2 | cosmic-library-home = Inicio de la biblioteca 3 | cosmic-office = Oficina 4 | cosmic-system = Sistema 5 | cosmic-utilities = Utilidades 6 | new-group = Crear carpeta 7 | name = Nombre 8 | ok = OK 9 | save = Guardar 10 | cancel = Cancelar 11 | search-placeholder = Escribe para buscar aplicaciones... 12 | new-group-placeholder = Nombre de la carpeta 13 | run = Ejecutar 14 | run-on = Ejecutar en { $gpu } 15 | run-on-default = (Default) 16 | remove = Eliminar 17 | create-new = Crear nueva carpeta 18 | delete = Eliminar 19 | rename = Renombrar 20 | delete-folder = ¿Eliminar carpeta? 21 | .msg = Borrar esta carpeta moverá los íconos de las aplicaciones al inicio de la biblioteca. 22 | flatpak = Flatpak 23 | snap = Snap 24 | system = Sistema 25 | local = Local 26 | nix = Nix 27 | pin-to-app-tray = Fijar a la lista de aplicaciones 28 | add-group = Añadir conjunto 29 | -------------------------------------------------------------------------------- /i18n/es-MX/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/es-MX/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/es/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = Biblioteca de aplicaciones Cosmic 2 | cosmic-library-home = Inicio 3 | cosmic-office = Oficina 4 | cosmic-system = Sistema 5 | cosmic-utilities = Utilidades 6 | new-group = Nuevo grupo 7 | name = Nombre 8 | ok = Vale 9 | save = Guardar 10 | cancel = Cancelar 11 | search-placeholder = Escriba para buscar aplicaciones... 12 | new-group-placeholder = Nombre del grupo 13 | pin-to-app-tray = Fijar a la bandeja 14 | run = Ejecutar 15 | run-on = Ejecutar en {$gpu} 16 | run-on-default = (Predeterminado) 17 | remove = Mover a la carpeta principal 18 | create-new = Crear nuevo grupo 19 | add-group = Añadir grupo 20 | delete = Eliminar 21 | rename = Renombrar 22 | delete-folder = ¿Eliminar este grupo? 23 | .msg = Eliminar este grupo moverá los íconos de aplicación al grupo Inicio. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = Sistema 27 | local = Local 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/et/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | save = Salvesta 2 | cosmic-office = Kontoritöö 3 | cosmic-system = Süsteem 4 | cosmic-utilities = Tarvikud 5 | new-group = Loo kaust 6 | name = Nimi 7 | ok = Sobib 8 | cancel = Katkesta 9 | flatpak = Flatpak 10 | snap = Snap 11 | system = Süsteem 12 | local = Kohalik 13 | nix = Nix 14 | new-group-placeholder = Kausta nimi 15 | create-new = Lisa uus kaust 16 | add-group = Lisa grupp 17 | delete = Kustuta 18 | rename = Muuda nime 19 | search-placeholder = Rakenduste otsimiseks kirjuta midagi... 20 | run-on-default = (Vaikimisi) 21 | run = Käivita 22 | -------------------------------------------------------------------------------- /i18n/fa/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = کتابخانه برنامه‌های COSMIC 2 | cosmic-library-home = خانه کتابخانه 3 | cosmic-office = اداری 4 | cosmic-system = سیستم 5 | cosmic-utilities = ابزارها 6 | new-group = ایجاد پوشه 7 | name = نام 8 | ok = تأیید 9 | save = ذخیره 10 | cancel = لغو 11 | search-placeholder = برای جستجوی برنامه‌ها تایپ کنید... 12 | new-group-placeholder = نام پوشه 13 | pin-to-app-tray = سنجاق کردن به سینی برنامه‌ها 14 | run = اجرا 15 | run-on = اجرا روی {$gpu} 16 | run-on-default = (پیش‌فرض) 17 | remove = انتقال به خانه کتابخانه 18 | create-new = ایجاد پوشه جدید 19 | add-group = افزودن گروه 20 | delete = حذف 21 | rename = تغییر نام 22 | delete-folder = پوشه حذف شود؟ 23 | .msg = حذف این پوشه باعث انتقال آیکون‌های برنامه به خانه کتابخانه می‌شود. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = سیستم 27 | local = محلی 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/fi/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/fi/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/fr/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = Librairie d'application COSMIC 2 | cosmic-library-home = Accueil 3 | cosmic-office = Office 4 | cosmic-system = Système 5 | cosmic-utilities = Utilitaires 6 | new-group = Nouveau groupe 7 | name = Nom 8 | ok = Ok 9 | save = Enregistrer 10 | cancel = Annuler 11 | search-placeholder = Taper pour rechercher des applications... 12 | new-group-placeholder = Nom du dossier 13 | pin-to-app-tray = Épingler à la barre des applis 14 | run = Lancer 15 | run-on = Lancer avec {$gpu} 16 | run-on-default = (Par défaut) 17 | remove = Déplacer dans le dossier d'accueil 18 | create-new = Créer un nouveau dossier 19 | add-group = Nouveau groupe 20 | delete = Supprimer 21 | rename = Renommer 22 | delete-folder = Supprimer le dossier ? 23 | .msg = La suppression de ce dossier déplacera les icônes des applications vers l'accueil de la logithèque. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = Système 27 | local = Local 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/fy/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/fy/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/ga/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = Leabharlann Aipeanna COSMIC 2 | cosmic-library-home = Baile Leabharlainne 3 | cosmic-office = Oifig 4 | cosmic-system = Córas 5 | cosmic-utilities = Fóntais 6 | new-group = Cruthaigh Fillteán 7 | name = Ainm 8 | ok = Ceart go leor 9 | save = Sábháil 10 | cancel = Cealaigh 11 | search-placeholder = Clóscríobh chun aipeanna a chuardach... 12 | new-group-placeholder = Ainm an fhillteáin 13 | pin-to-app-tray = Greamaigh den Tráidire Aipeanna 14 | run = Rith 15 | run-on = Rith ar {$gpu} 16 | run-on-default = (Réamhshocrú) 17 | remove = Bog go baile na leabharlainne 18 | create-new = Cruthaigh fillteán nua 19 | add-group = Cuir grúpa leis 20 | delete = Scrios 21 | rename = Athainmnigh 22 | delete-folder = Scrios fillteán? 23 | .msg = Má scriostar an fillteán seo, bogfar deilbhíní na n-aipeanna go baile na leabharlainne. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = Córas 27 | local = Áitiúil 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/gd/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/gd/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/he/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/he/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/hi/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = कास्मिक ऐप लाइब्रेरी 2 | cosmic-library-home = लाइब्रेरी होम 3 | cosmic-office = ऑफिस 4 | cosmic-system = सिस्टम 5 | cosmic-utilities = यूटिलिटीज 6 | new-group = फ़ोल्डर बनाएँ 7 | name = नाम 8 | ok = ठीक 9 | save = सहेजें 10 | cancel = रद्द करें 11 | search-placeholder = ऐप्स खोजने के लिए टाइप करें... 12 | new-group-placeholder = फ़ोल्डर का नाम 13 | pin-to-app-tray = ऐप ट्रे में पिन करें 14 | run = चलाएँ 15 | run-on = {$gpu} पर चलाएँ 16 | run-on-default = (डिफ़ॉल्ट) 17 | remove = लाइब्रेरी होम पर ले जाएँ 18 | create-new = नया फ़ोल्डर बनाएँ 19 | add-group = समूह जोड़ें 20 | delete = हटाएँ 21 | rename = नाम बदलें 22 | delete-folder = फ़ोल्डर हटाएँ? 23 | .msg = इस फ़ोल्डर को हटाने से एप्लिकेशन आइकन लाइब्रेरी होम में स्थानांतरित हो जाएगा। 24 | flatpak = फ्लैटपैक 25 | snap = स्नैप 26 | system = सिस्टम 27 | local = स्थानीय 28 | nix = निक्स 29 | -------------------------------------------------------------------------------- /i18n/hr/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/hr/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/hu/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = COSMIC Alkalmazáskönyvtár 2 | cosmic-library-home = Könyvtár Kezdőlap 3 | cosmic-office = Iroda 4 | cosmic-system = Rendszer 5 | cosmic-utilities = Segédprogramok 6 | new-group = Mappa létrehozása 7 | name = Név 8 | ok = Ok 9 | save = Mentés 10 | cancel = Mégse 11 | search-placeholder = Írj az alkalmazások kereséséhez... 12 | new-group-placeholder = Mappa neve 13 | pin-to-app-tray = Rögzítés az Alkalmazástálcára 14 | run = Futtatás 15 | run-on = Futtatás ezen: {$gpu} 16 | run-on-default = (Alapértelmezett) 17 | remove = Áthelyezés a Könyvtár kezdőlapjára 18 | create-new = Új mappa létrehozása 19 | add-group = Csoport hozzáadása 20 | delete = Törlés 21 | rename = Átnevezés 22 | delete-folder = Törlöd a mappát? 23 | .msg = A mappa törlésével az alkalmazásikonok a Könyvtár kezdőlapjára kerülnek. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = Rendszer 27 | local = Helyi 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/id/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/id/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/ie/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/ie/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/it/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = Libreria applicazioni COSMIC 2 | cosmic-library-home = Home 3 | cosmic-office = Ufficio 4 | cosmic-system = Sistema 5 | cosmic-utilities = Utilità 6 | new-group = Nuovo gruppo 7 | name = Nome 8 | ok = Ok 9 | save = Salva 10 | cancel = Annulla 11 | search-placeholder = Digita per cercare... 12 | new-group-placeholder = Nome gruppo 13 | pin-to-app-tray = Fissa sulla barra delle app 14 | run = Esegui 15 | run-on = Esegui usando: {$gpu} 16 | run-on-default = (Default) 17 | remove = Rimuovi 18 | create-new = Crea nuova cartella 19 | add-group = Aggiungi gruppo 20 | delete = Elimina 21 | rename = Rinomina 22 | delete-folder = Cancellare il gruppo? 23 | .msg = L'eliminazione di questo gruppo sposterà tutte le applicazioni nel gruppo principale. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = Sistema 27 | local = Locale 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/ja/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = COSMICアプリライブラリ 2 | cosmic-library-home = ライブラリホーム 3 | cosmic-office = オフィス 4 | cosmic-system = システム 5 | cosmic-utilities = ユティリティ 6 | new-group = フォルダ 7 | name = 名前 8 | ok = OK 9 | save = 保存 10 | cancel = キャンセル 11 | search-placeholder = アプリを検索するために入力して下さい... 12 | new-group-placeholder = フォルダ名 13 | pin-to-app-tray = アプリトレーにピン留めする 14 | run = 実行 15 | run-on = {$gpu}で実行 16 | run-on-default =(デフォルト) 17 | remove = ライブラリホームに移動 18 | create-new = 新しいフォルダを作成 19 | add-group = グループを追加 20 | delete = 削除 21 | rename = 名前を変更 22 | delete-folder = フォルダを削除しますか? 23 | .msg = このフォルダを削除すると、アプリケーションのアイコンはライブラリホームに移動されます。 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = システム 27 | local = ローカル 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/jv/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/jv/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/kn/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = ಕಾಸ್ಮಿಕ್ ಅಪ್ಲಿಕೇಶನ್ ಗ್ರಂಥಾಲಯ 2 | cosmic-library-home = ಗ್ರಂಥಾಲಯದ ಮನೆ 3 | cosmic-office = ಆಫೀಸ್ 4 | cosmic-system = ಸಿಸ್ಟಮ್ 5 | cosmic-utilities = ಉಪಕರಣಗಳು 6 | new-group = ಫೋಲ್ಡರ್ ರಚಿಸಿ 7 | name = ಹೆಸರು 8 | ok = ಒಪ್ಪಿಗೆ 9 | save = ಉಳಿಸಿ 10 | cancel = ರದ್ದು ಮಾಡಿ 11 | search-placeholder = ಅಪ್ಲಿಕೇಶನ್‌ಗಳನ್ನು ಹುಡುಕಲು ಟೈಪ್ ಮಾಡಿ... 12 | new-group-placeholder = ಫೋಲ್ಡರ್ ಹೆಸರು 13 | pin-to-app-tray = ಅಪ್ಲಿಕೇಶನ್ ಟ್ರೇಗೆ ಪಿನ್ ಮಾಡಿ 14 | run = ಕಾರ್ಯಗತಗೊಳಿಸಿ 15 | run-on = {$gpu} ಮೇಲೆ ಕಾರ್ಯಗತಗೊಳಿಸಿ 16 | run-on-default = (ಡೀಫಾಲ್ಟ್) 17 | remove = ಗ್ರಂಥಾಲಯದ ಮನೆಯ ಕಡೆಗೆ ಒಯ್ಯಿ 18 | create-new = ಹೊಸ ಫೋಲ್ಡರ್ ರಚಿಸಿ 19 | add-group = ಗುಂಪು ಸೇರಿಸಿ 20 | delete = ಅಳಿಸಿ 21 | rename = ಪುನಃ ಹೆಸರಿಸಿ 22 | delete-folder = ಫೋಲ್ಡರ್ ಅಳಿಸುವುದೆ? 23 | .msg = ಈ ಫೋಲ್ಡರ್ ಅನ್ನು ಅಳಿಸುವಾಗ ಅಪ್ಲಿಕೇಶನ್ ಐಕಾನ್ಗಳನ್ನು ಗ್ರಂಥಾಲಯದ ಮನೆಯ ಕಡೆಗೆ ಒಯ್ಯಲಾಗುತ್ತದೆ. 24 | flatpak = ಫ್ಲಾಟ್‌ಪಾಕ್ 25 | snap = ಸ್ನಾಪ್ 26 | system = ಸಿಸ್ಟಮ್ 27 | local = ಸ್ಥಳೀಯ 28 | nix = ನಿಕ್ಸ್ 29 | -------------------------------------------------------------------------------- /i18n/ko/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = 코스믹 프로그램 라이브러리 2 | cosmic-library-home = 라이브러리 홈 3 | cosmic-office = 오피스 4 | cosmic-system = 시스템 5 | cosmic-utilities = 유틸리티 6 | new-group = 폴더 만들기 7 | name = 이름 8 | ok = 확인 9 | cancel = 취소 10 | -------------------------------------------------------------------------------- /i18n/li/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/li/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/lt/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/lt/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/nb/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = COSMIC App-Bibliotek 2 | cosmic-library-home = Hjem 3 | cosmic-office = Kontor 4 | cosmic-system = System 5 | cosmic-utilities = Verktøy 6 | new-group = Ny mappe 7 | name = Navn 8 | ok = Ok 9 | save = Lagre 10 | cancel = Avbryt 11 | search-placeholder = Trykk for å søke etter apper... 12 | new-group-placeholder = Mappenavn 13 | pin-to-app-tray = Fest til appskuffen 14 | run = Kjør 15 | run-on = Kjør på {$gpu} 16 | run-on-default = (Standard) 17 | remove = Flytt til hjem 18 | create-new = Lag ny mappe 19 | add-group = Ny gruppe 20 | delete = Slett 21 | rename = Gi nytt navn 22 | delete-folder = Slett mappe? 23 | .msg = .msg = Sletter du mappen, havner programikonene på Bibliotekets hovedside. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = System 27 | local = Lokalt 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/nl/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = COSMIC Appbibliotheek 2 | cosmic-library-home = Startpagina 3 | cosmic-office = Bureau 4 | cosmic-system = Systeem 5 | cosmic-utilities = Hulpprogramma's 6 | new-group = Tabblad toevoegen 7 | name = Naam 8 | ok = Oké 9 | save = Opslaan 10 | cancel = Annuleren 11 | search-placeholder = Typ om naar toepassingen te zoeken... 12 | new-group-placeholder = Nieuw tabblad 13 | pin-to-app-tray = Aan de toepassingsbalk vastmaken 14 | run = Uitvoeren 15 | run-on = Op {$gpu} uitvoeren 16 | run-on-default = (Standaard) 17 | remove = Naar startpagina verplaatsen 18 | create-new = Nieuw tabblad toevoegen 19 | add-group = Tabblad toevoegen 20 | delete = Verwijderen 21 | rename = Hernoemen 22 | delete-folder = Tabblad verwijderen? 23 | .msg = Als u dit tabblad verwijderd, zullen de toepassingspictogrammen naar de startpagina gaan. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = Systeem 27 | local = Lokaal 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/pl/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = Biblioteka Aplikacji COSMIC 2 | cosmic-library-home = Strona Domowa Biblioteki 3 | cosmic-office = Biuro 4 | cosmic-system = System 5 | cosmic-utilities = Narzędzia 6 | new-group = Stwórz Katalog 7 | name = Nazwa 8 | ok = Ok 9 | save = Zapisz 10 | cancel = Anuluj 11 | search-placeholder = Zacznij pisać by wyszukać aplikację... 12 | new-group-placeholder = Nazwa Katalogu 13 | pin-to-app-tray = Przypnij do Zasobnika Apek 14 | run = Uruchom 15 | run-on = Uruchom na {$gpu} 16 | run-on-default = (Domyślnie) 17 | remove = Usuń 18 | create-new = Stwórz nowy katalog 19 | add-group = Dodaj katalog 20 | delete = Usuń 21 | rename = Zmień nazwę 22 | delete-folder = Usunąć katalog? 23 | .msg = Usunięcie tego katalogu przeniesie ikony aplicacji do strony domowej Biblioteki. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = System 27 | local = Lokalny 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/pt-BR/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = Biblioteca de Aplicativos 2 | cosmic-library-home = Aplicativos 3 | cosmic-office = Escritório 4 | cosmic-system = Sistema 5 | cosmic-utilities = Utilitários 6 | new-group = Novo Grupo 7 | name = Nome 8 | ok = OK 9 | save = Salvar 10 | cancel = Cancelar 11 | search-placeholder = Digite para procurar aplicativos... 12 | new-group-placeholder = Nome do grupo 13 | pin-to-app-tray = Fixar em aplicativos favoritos 14 | run = Executar 15 | run-on = Executar em { $gpu } 16 | run-on-default = (Padrão) 17 | remove = Mover para a página inicial 18 | create-new = Criar novo grupo 19 | add-group = Adicionar grupo 20 | delete = Apagar 21 | rename = Renomear 22 | delete-folder = Apagar grupo? 23 | .msg = Ao apagar este grupo, os ícones dos aplicativos serão movidos para a página inicial da biblioteca. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = Sistema 27 | local = Local 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/pt/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = Biblioteca de Aplicações COSMIC 2 | cosmic-library-home = Biblioteca Principal 3 | cosmic-office = Escritório 4 | cosmic-system = Sistema 5 | cosmic-utilities = Utilitários 6 | new-group = Criar Pasta 7 | name = Nome 8 | ok = Ok 9 | save = Guardar 10 | cancel = Cancelar 11 | search-placeholder = Escreve para procurar por aplicações... 12 | new-group-placeholder = Nome da Pasta 13 | pin-to-app-tray = Fixar no painel 14 | run = Executar 15 | run-on = Executar em {$gpu} 16 | run-on-default = (Padrão) 17 | remove = Mover para a biblioteca principal 18 | create-new = Criar nova pasta 19 | add-group = Adicionar grupo 20 | delete = Apagar 21 | rename = Renomear 22 | delete-folder = Apagar pasta? 23 | .msg = Apagar esta pasta vai mover os icons das aplicações para a Biblioteca principal. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = Sistema 27 | local = Local 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/ro/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = Biblioteca de aplicații COSMIC 2 | cosmic-library-home = Acasă 3 | cosmic-office = Birou 4 | cosmic-system = Sistem 5 | cosmic-utilities = Utilitare 6 | new-group = Creează un dosar 7 | name = Nume 8 | ok = Ok 9 | save = Salvează 10 | cancel = Anulează 11 | search-placeholder = Scrie pentru a căuta aplicații... 12 | new-group-placeholder = Numele dosarului 13 | pin-to-app-tray = Fixează în bara de aplicații 14 | run = Rulează 15 | run-on = Rulează pe {$gpu} 16 | run-on-default = (Implicit) 17 | remove = Mută în biblioteca de acasă 18 | create-new = Creează un dosar nou 19 | add-group = Adaugă un grup 20 | delete = Șterge 21 | rename = Redenumește 22 | delete-folder = Șterge dosarul? 23 | .msg = Ștergerea acestui dosar va muta pictogramele aplicațiilor în Biblioteca „Acasă”. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = Sistem 27 | local = Local 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/ru/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = Библиотека приложений COSMIC 2 | cosmic-library-home = Главная 3 | cosmic-office = Офис 4 | cosmic-system = Система 5 | cosmic-utilities = Утилиты 6 | new-group = Новая папка 7 | name = Имя 8 | ok = ОК 9 | save = Сохранить 10 | cancel = Отменить 11 | search-placeholder = Введите текст для поиска приложений... 12 | new-group-placeholder = Имя папки 13 | pin-to-app-tray = Прикрепить на панель приложений 14 | run = Запустить 15 | run-on = Запустить на { $gpu } 16 | run-on-default = (по умолчанию) 17 | remove = Убрать 18 | create-new = Создать новую папку 19 | add-group = Добавить группу 20 | delete = Удалить 21 | rename = Переименовать 22 | delete-folder = Удалить папку? 23 | .msg = Удаление этой папки перенесёт значки приложений из неё в папку «Главная». 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = Системное 27 | local = Локальное 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/sk/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = Knižnica aplikácií COSMIC 2 | cosmic-library-home = Domov 3 | cosmic-office = Kancelária 4 | cosmic-system = Systém 5 | cosmic-utilities = Nástroje 6 | new-group = Vytvoriť priečinok 7 | name = Meno 8 | ok = OK 9 | save = Uložiť 10 | cancel = Zrušiť 11 | search-placeholder = Vyhľadať aplikácie... 12 | new-group-placeholder = Názov priečinku 13 | pin-to-app-tray = Pripnúť do panelu aplikácií 14 | run = Spustiť 15 | run-on = Spustiť na {$gpu} 16 | run-on-default = (Predvolené) 17 | remove = Presunúť do priečinku Domov 18 | create-new = Vytvoriť nový priečinok 19 | add-group = Pridať priečinok 20 | delete = Odstrániť 21 | rename = Premenovať 22 | delete-folder = Vymazať priečinok? 23 | .msg = Odstránením tohto priečinku sa všetky aplikácie presunú do priečinka Domov. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = Systém 27 | local = Miestne 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/sr-Cyrl/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = COSMIC Библиотека апликација 2 | cosmic-library-home = Почетна 3 | cosmic-office = Канцеларија 4 | cosmic-system = Систем 5 | cosmic-utilities = Алати 6 | new-group = Направи фасциклу 7 | name = Име 8 | ok = У реду 9 | save = Сачувај 10 | cancel = Поништи 11 | search-placeholder = Куцајте за претрагу апликација... 12 | new-group-placeholder = Име фасцикле 13 | run = Покрени 14 | run-on = Покрени на {$gpu} 15 | run-on-default = (подразумевано) 16 | remove = Уклони 17 | create-new = Направи нову фасциклу 18 | delete = Избриши 19 | rename = Преименуј 20 | delete-folder = Избриши фасциклу? 21 | .msg = Брисањем ове фасцикле, апликације ће се преместити на почетну страницу библиотеке. 22 | -------------------------------------------------------------------------------- /i18n/sr-Latn/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = COSMIC Biblioteka aplikacija 2 | cosmic-library-home = Početna 3 | cosmic-office = Kancelarija 4 | cosmic-system = Sistem 5 | cosmic-utilities = Alati 6 | new-group = Napravi fasciklu 7 | name = Ime 8 | ok = U redu 9 | save = Sačuvaj 10 | cancel = Poništi 11 | search-placeholder = Kucajte za pretragu aplikacija... 12 | new-group-placeholder = Ime fascikle 13 | run = Pokreni 14 | run-on = Pokreni na {$gpu} 15 | run-on-default = (podrazumevano) 16 | remove = Ukloni 17 | create-new = Napravi novu fasciklu 18 | delete = Izbriši 19 | rename = Preimenuj 20 | delete-folder = Izbriši fasciklu? 21 | .msg = Brisanjem ove fascikle, aplikacije će se premestiti na početnu stranicu biblioteke. 22 | -------------------------------------------------------------------------------- /i18n/sr/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/sr/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/sv/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | osmic-app-library = COSMIC App Bibliotek 2 | cosmic-library-home = Hem 3 | cosmic-office = Kontor 4 | cosmic-system = System 5 | cosmic-utilities = Verktyg 6 | new-group = Skapa mapp 7 | name = Namn 8 | ok = Ok 9 | save = Spara 10 | cancel = Avbryt 11 | search-placeholder = Skriv för att söka efter program... 12 | new-group-placeholder = Mappnamn 13 | pin-to-app-tray = Fäst i programfält 14 | run = Kör 15 | run-on = Kör på { $gpu } 16 | run-on-default = (Standard) 17 | remove = Flytta till Bibliotek hem 18 | create-new = Skapa ny mapp 19 | add-group = Lägg till grupp 20 | delete = Radera 21 | rename = Byt namn 22 | delete-folder = Radera mapp? 23 | .msg = Om du tar bort den här mappen flyttas programikonerna till bibliotekets hem. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = System 27 | local = Lokalt 28 | nix = Nix 29 | cosmic-app-library = COSMIC Programbibliotek 30 | -------------------------------------------------------------------------------- /i18n/ta/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/ta/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/th/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/th/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/tr/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = COSMIC Uygulama Kitaplığı 2 | cosmic-library-home = Ana Sayfa 3 | cosmic-office = Ofis 4 | cosmic-system = Sistem 5 | cosmic-utilities = Araçlar 6 | new-group = Klasör Oluştur 7 | name = Ad 8 | ok = Tamam 9 | save = Kaydet 10 | cancel = Vazgeç 11 | search-placeholder = Uygulamaları aramak için yazın... 12 | new-group-placeholder = Klasör Adı 13 | pin-to-app-tray = Uygulamayı sistem tepsisine iliştir 14 | run = Çalıştır 15 | run-on = { $gpu } üzerinde çalıştır 16 | run-on-default = (Öntanımlı) 17 | remove = Kütüphane ana sayfasına taşı 18 | create-new = Yeni klasör oluştur 19 | add-group = Grup Ekle 20 | delete = Sil 21 | rename = Yeniden Adlandır 22 | delete-folder = Klasör silinsin mi? 23 | .msg = Bu klasörün silinmesi uygulama simgelerini Kitaplık ana sayfasına taşıyacaktır. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = Sistem 27 | local = Yerel 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/uk/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = Бібліотека застосунків COSMIC 2 | cosmic-library-home = Домівка бібліотеки 3 | cosmic-office = Офіс 4 | cosmic-system = Система 5 | cosmic-utilities = Утиліти 6 | new-group = Створити теку 7 | name = Назва 8 | ok = Ok 9 | save = Зберегти 10 | cancel = Скасувати 11 | search-placeholder = Пишіть для пошуку застосунків... 12 | new-group-placeholder = Назва теки 13 | pin-to-app-tray = Пришпилити до лотка застосунків 14 | run = Запустити 15 | run-on = Запустити на {$gpu} 16 | run-on-default = (типовій) 17 | remove = Перемістити до домівки бібліотеки 18 | create-new = Створити нову теку 19 | add-group = Додати категорію 20 | delete = Вилучити 21 | rename = Перейменувати 22 | delete-folder = Вилучити теку? 23 | .msg = Вилучення цієї теки призведе до переміщення піктограм застосунків до домівки бібліотеки. 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = Система 27 | local = Локальне 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/vi/cosmic_app_library.ftl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/cosmic-applibrary/7344f878b7cea48598707cb331f589ac3ddfd797/i18n/vi/cosmic_app_library.ftl -------------------------------------------------------------------------------- /i18n/zh-CN/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = COSMIC 应用程序库 2 | cosmic-library-home = 主页 3 | cosmic-office = 办公 4 | cosmic-system = 系统 5 | cosmic-utilities = 工具 6 | new-group = 新建文件夹 7 | name = 名称 8 | ok = 确定 9 | save = 保存 10 | cancel = 取消 11 | search-placeholder = 输入以搜索应用程序... 12 | new-group-placeholder = 文件夹名称 13 | pin-to-app-tray = 固定到程序坞 14 | run = 运行 15 | run-on = 运行于 {$gpu} 16 | run-on-default = (默认) 17 | remove = 移动到主页 18 | create-new = 新建文件夹 19 | add-group = 添加文件夹 20 | delete = 删除 21 | rename = 重命名 22 | delete-folder = 删除文件夹? 23 | .msg = 删除此文件夹将会把应用程序图标移动到主页。 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = 系统 27 | local = 本地 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /i18n/zh-TW/cosmic_app_library.ftl: -------------------------------------------------------------------------------- 1 | cosmic-app-library = COSMIC 應用程式庫 2 | cosmic-library-home = 主頁 3 | cosmic-office = 辦公 4 | cosmic-system = 系統 5 | cosmic-utilities = 工具 6 | new-group = 建立資料夾 7 | name = 名稱 8 | ok = 確認 9 | save = 儲存 10 | cancel = 取消 11 | search-placeholder = 輸入以搜尋應用程式... 12 | new-group-placeholder = 資料夾名稱 13 | pin-to-app-tray = 釘選到應用程式列 14 | run = 執行 15 | run-on = 執行於 {$gpu} 16 | run-on-default = (預設) 17 | remove = 移動到應用程式庫主頁 18 | create-new = 建立新資料夾 19 | add-group = 新增群組 20 | delete = 刪除 21 | rename = 重新命名 22 | delete-folder = 刪除資料夾? 23 | .msg = 刪除此資料夾將會把應用程式圖標移動到應用程式庫主頁。 24 | flatpak = Flatpak 25 | snap = Snap 26 | system = 系統 27 | local = 本地 28 | nix = Nix 29 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | name := 'cosmic-app-library' 2 | export APPID := 'com.system76.CosmicAppLibrary' 3 | 4 | rootdir := '' 5 | prefix := '/usr' 6 | 7 | base-dir := absolute_path(clean(rootdir / prefix)) 8 | 9 | export INSTALL_DIR := base-dir / 'share' 10 | 11 | cargo-target-dir := env('CARGO_TARGET_DIR', 'target') 12 | bin-src := cargo-target-dir / 'release' / name 13 | bin-dst := base-dir / 'bin' / name 14 | 15 | # Default recipe which runs `just build-release` 16 | default: build-release 17 | 18 | # Runs `cargo clean` 19 | clean: 20 | cargo clean 21 | 22 | # `cargo clean` and removes vendored dependencies 23 | clean-dist: clean 24 | rm -rf .cargo vendor vendor.tar 25 | 26 | # Compiles with debug profile 27 | build-debug *args: 28 | cargo build {{args}} 29 | 30 | # Compiles with release profile 31 | build-release *args: (build-debug '--release' args) 32 | 33 | # Compiles release profile with vendored dependencies 34 | build-vendored *args: vendor-extract (build-release '--frozen --offline' args) 35 | 36 | # Runs a clippy check 37 | check *args: 38 | cargo clippy --all-features {{args}} -- -W clippy::pedantic 39 | 40 | # Runs a clippy check with JSON message format 41 | check-json: (check '--message-format=json') 42 | 43 | # Installs files 44 | install: 45 | install -Dm0755 {{bin-src}} {{bin-dst}} 46 | @just data/install 47 | @just data/icons/install 48 | 49 | # Uninstalls installed files 50 | uninstall: 51 | rm {{bin-dst}} 52 | @just data/uninstall 53 | @just data/icons/uninstall 54 | 55 | # Vendor dependencies locally 56 | vendor: 57 | mkdir -p .cargo 58 | cargo vendor --sync Cargo.toml \ 59 | | head -n -1 > .cargo/config 60 | echo 'directory = "vendor"' >> .cargo/config 61 | tar pcf vendor.tar vendor 62 | rm -rf vendor 63 | 64 | # Extracts vendored dependencies 65 | vendor-extract: 66 | #!/usr/bin/env sh 67 | rm -rf vendor 68 | tar pxf vendor.tar 69 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.85.1" 3 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fmt::{Debug, Display}, 4 | path::{Path, PathBuf}, 5 | rc::Rc, 6 | sync::{Arc, LazyLock}, 7 | time::{Duration, Instant}, 8 | }; 9 | 10 | use clap::Parser; 11 | use cosmic::{ 12 | Element, 13 | app::{Core, CosmicFlags, Settings, Task}, 14 | cctk::sctk::{ 15 | self, 16 | data_device_manager::data_offer::DataDeviceOfferInner, 17 | shell::wlr_layer::{Anchor, KeyboardInteractivity}, 18 | }, 19 | cosmic_config::{Config, CosmicConfigEntry}, 20 | cosmic_theme::Spacing, 21 | dbus_activation, 22 | desktop::{DesktopEntryData, fde::PathSource, load_desktop_file}, 23 | iced::{ 24 | self, Alignment, Color, Length, Limits, Size, Subscription, 25 | alignment::Horizontal, 26 | event::{listen_with, wayland::OverlapNotifyEvent}, 27 | executor, 28 | id::Id, 29 | /*wayland::actions::{ 30 | data_device::ActionInner, 31 | },*/ 32 | widget::{column, container, horizontal_rule, row, scrollable, text}, 33 | window::Event as WindowEvent, 34 | }, 35 | iced_core::{ 36 | Border, Padding, Rectangle, Shadow, 37 | alignment::Vertical, 38 | keyboard::{Key, key::Named}, 39 | widget::operation::{ 40 | self, 41 | focusable::{find_focused, focus}, 42 | }, 43 | }, 44 | iced_runtime::{ 45 | self, 46 | core::{ 47 | event::{ 48 | PlatformSpecific, 49 | wayland::{self, LayerEvent}, 50 | }, 51 | window::Id as SurfaceId, 52 | }, 53 | dnd::end_dnd, 54 | platform_specific::wayland::{ 55 | layer_surface::SctkLayerSurfaceSettings, 56 | popup::{SctkPopupSettings, SctkPositioner}, 57 | }, 58 | }, 59 | iced_widget::{horizontal_space, mouse_area, scrollable::RelativeOffset, vertical_space}, 60 | iced_winit::commands::{ 61 | self, 62 | activation::request_token, 63 | layer_surface::{destroy_layer_surface, get_layer_surface}, 64 | overlap_notify::overlap_notify, 65 | popup::destroy_popup, 66 | }, 67 | keyboard_nav, surface, 68 | theme::{self, Button, TextInput}, 69 | widget::{ 70 | self, Column, 71 | autosize::autosize, 72 | button::{self, Catalog as ButtonStyleSheet}, 73 | divider, 74 | dnd_destination::dnd_destination_for_data, 75 | icon::{self, from_name}, 76 | search_input, svg, 77 | text::body, 78 | text_input, tooltip, 79 | }, 80 | }; 81 | use cosmic_app_list_config::AppListConfig; 82 | use itertools::Itertools; 83 | use log::error; 84 | use serde::{Deserialize, Serialize}; 85 | use switcheroo_control::Gpu; 86 | 87 | use crate::{ 88 | app_group::AppLibraryConfig, 89 | fl, 90 | subscriptions::desktop_files::desktop_files, 91 | widgets::application::{AppletString, ApplicationButton}, 92 | }; 93 | 94 | // popovers should show options, but also the desktop info options 95 | // should be a way to add apps to groups 96 | // should be a way to remove apps from groups 97 | 98 | static SEARCH_ID: LazyLock = LazyLock::new(|| Id::new("search")); 99 | static EDIT_GROUP_ID: LazyLock = LazyLock::new(|| Id::new("edit_group")); 100 | static NEW_GROUP_ID: LazyLock = LazyLock::new(|| Id::new("new_group")); 101 | static SUBMIT_DELETE_ID: LazyLock = LazyLock::new(|| Id::new("cancel_delete")); 102 | 103 | static CREATE_NEW: LazyLock = LazyLock::new(|| fl!("create-new")); 104 | static ADD_GROUP: LazyLock = LazyLock::new(|| fl!("add-group")); 105 | static SEARCH_PLACEHOLDER: LazyLock = LazyLock::new(|| fl!("search-placeholder")); 106 | static NEW_GROUP_PLACEHOLDER: LazyLock = LazyLock::new(|| fl!("new-group-placeholder")); 107 | static SAVE: LazyLock = LazyLock::new(|| fl!("save")); 108 | static CANCEL: LazyLock = LazyLock::new(|| fl!("cancel")); 109 | static RUN: LazyLock = LazyLock::new(|| fl!("run")); 110 | static REMOVE: LazyLock = LazyLock::new(|| fl!("remove")); 111 | static FLATPAK: LazyLock = LazyLock::new(|| fl!("flatpak")); 112 | static LOCAL: LazyLock = LazyLock::new(|| fl!("local")); 113 | static NIX: LazyLock = LazyLock::new(|| fl!("nix")); 114 | static SNAP: LazyLock = LazyLock::new(|| fl!("snap")); 115 | static SYSTEM: LazyLock = LazyLock::new(|| fl!("system")); 116 | 117 | pub(crate) static WINDOW_ID: LazyLock = LazyLock::new(|| SurfaceId::unique()); 118 | static NEW_GROUP_WINDOW_ID: LazyLock = LazyLock::new(|| SurfaceId::unique()); 119 | static NEW_GROUP_AUTOSIZE_ID: LazyLock = 120 | LazyLock::new(|| cosmic::widget::Id::unique()); 121 | static DELETE_GROUP_WINDOW_ID: LazyLock = LazyLock::new(|| SurfaceId::unique()); 122 | static DELETE_GROUP_AUTOSIZE_ID: LazyLock = 123 | LazyLock::new(|| cosmic::widget::Id::unique()); 124 | pub(crate) static MENU_ID: LazyLock = LazyLock::new(|| SurfaceId::unique()); 125 | pub(crate) static MENU_AUTOSIZE_ID: LazyLock = 126 | LazyLock::new(|| cosmic::widget::Id::unique()); 127 | 128 | #[derive(Parser, Debug, Serialize, Deserialize, Clone)] 129 | #[command(author, version, about, long_about = None)] 130 | #[command(propagate_version = true)] 131 | pub struct Args {} 132 | 133 | #[derive(Debug, Serialize, Deserialize, Clone)] 134 | pub struct LauncherCommands; 135 | 136 | impl ToString for LauncherCommands { 137 | fn to_string(&self) -> String { 138 | ron::ser::to_string(self).unwrap() 139 | } 140 | } 141 | 142 | impl CosmicFlags for Args { 143 | type SubCommand = LauncherCommands; 144 | type Args = Vec; 145 | 146 | fn action(&self) -> Option<&LauncherCommands> { 147 | None 148 | } 149 | } 150 | 151 | pub fn run() -> cosmic::iced::Result { 152 | cosmic::app::run_single_instance::( 153 | Settings::default() 154 | .antialiasing(true) 155 | .client_decorations(true) 156 | .debug(false) 157 | .default_text_size(16.0) 158 | .scale_factor(1.0) 159 | .no_main_window(true) 160 | .exit_on_close(false), 161 | Args::parse(), 162 | ) 163 | } 164 | 165 | pub struct AppSource(PathSource); 166 | 167 | impl AppSource { 168 | pub fn as_icon(&self) -> Option { 169 | let name = match &self.0 { 170 | PathSource::Local | PathSource::LocalDesktop => "app-source-local-symbolic", 171 | PathSource::System | PathSource::SystemLocal => "app-source-system-symbolic", 172 | PathSource::LocalFlatpak | PathSource::SystemFlatpak => "app-source-flatpak", 173 | PathSource::SystemSnap => "app-source-snap", 174 | PathSource::Nix | PathSource::LocalNix => "app-source-nix", 175 | PathSource::Other(_) => return None, 176 | }; 177 | let handle = crate::icon_cache::icon_cache_handle(name, 16); 178 | let symbolic = handle.symbolic; 179 | 180 | Some(icon::icon(handle).size(16).class(if symbolic { 181 | cosmic::theme::Svg::Custom(Rc::new(|t| { 182 | let color = t.cosmic().on_primary_component_color().into(); 183 | svg::Style { color: Some(color) } 184 | })) 185 | } else { 186 | cosmic::theme::Svg::Default 187 | })) 188 | } 189 | } 190 | 191 | impl<'a> From<&'a Path> for AppSource { 192 | fn from(path: &'a Path) -> Self { 193 | AppSource(PathSource::guess_from(path)) 194 | } 195 | } 196 | 197 | impl<'a> Display for AppSource { 198 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 199 | write!( 200 | f, 201 | "{:.7}", 202 | match &self.0 { 203 | PathSource::Local | PathSource::LocalDesktop => LOCAL.as_str(), 204 | PathSource::SystemFlatpak | PathSource::LocalFlatpak => FLATPAK.as_str(), 205 | PathSource::SystemSnap => SNAP.as_str(), 206 | PathSource::Nix | PathSource::LocalNix => NIX.as_str(), 207 | PathSource::System | PathSource::SystemLocal => SYSTEM.as_str(), 208 | PathSource::Other(s) => s.as_str(), 209 | } 210 | ) 211 | } 212 | } 213 | 214 | struct CosmicAppLibrary { 215 | search_value: String, 216 | entry_path_input: Vec>, 217 | all_entries: Vec>, 218 | menu: Option, 219 | helper: Option, 220 | config: AppLibraryConfig, 221 | cur_group: usize, 222 | active_surface: bool, 223 | locale: Option, 224 | edit_name: Option, 225 | new_group: Option, 226 | dnd_icon: Option, 227 | offer_group: Option, 228 | waiting_for_filtered: bool, 229 | scroll_offset: f32, 230 | core: Core, 231 | group_to_delete: Option, 232 | gpus: Option>, 233 | last_hide: Option, 234 | duplicates: HashMap, 235 | app_list_config: AppListConfig, 236 | overlap: HashMap, 237 | margin: f32, 238 | height: f32, 239 | needs_clear: bool, 240 | focused_id: Option, 241 | entry_ids: Vec, 242 | scrollable_id: widget::Id, 243 | } 244 | 245 | impl Default for CosmicAppLibrary { 246 | fn default() -> Self { 247 | Self { 248 | search_value: Default::default(), 249 | entry_path_input: Default::default(), 250 | all_entries: Default::default(), 251 | menu: Default::default(), 252 | helper: Default::default(), 253 | config: Default::default(), 254 | cur_group: Default::default(), 255 | active_surface: Default::default(), 256 | locale: Default::default(), 257 | edit_name: Default::default(), 258 | new_group: Default::default(), 259 | dnd_icon: Default::default(), 260 | offer_group: Default::default(), 261 | waiting_for_filtered: Default::default(), 262 | scroll_offset: Default::default(), 263 | core: Default::default(), 264 | group_to_delete: Default::default(), 265 | gpus: Default::default(), 266 | last_hide: Default::default(), 267 | duplicates: Default::default(), 268 | app_list_config: Default::default(), 269 | overlap: Default::default(), 270 | margin: Default::default(), 271 | height: Default::default(), 272 | needs_clear: Default::default(), 273 | focused_id: Default::default(), 274 | entry_ids: Default::default(), 275 | scrollable_id: widget::Id::unique(), 276 | } 277 | } 278 | } 279 | 280 | async fn try_get_gpus() -> Option> { 281 | let connection = zbus::Connection::system().await.ok()?; 282 | let proxy = switcheroo_control::SwitcherooControlProxy::new(&connection) 283 | .await 284 | .ok()?; 285 | 286 | if !proxy.has_dual_gpu().await.ok()? { 287 | return None; 288 | } 289 | 290 | let gpus = proxy.get_gpus().await.ok()?; 291 | if gpus.is_empty() { 292 | return None; 293 | } 294 | Some(gpus) 295 | } 296 | 297 | impl CosmicAppLibrary { 298 | pub fn activate(&mut self) -> Task { 299 | if self.active_surface { 300 | return self.hide(); 301 | } else if !self 302 | .last_hide 303 | .is_some_and(|i| i.elapsed() < Duration::from_millis(100)) 304 | { 305 | self.edit_name = None; 306 | self.search_value = "".to_string(); 307 | self.active_surface = true; 308 | self.scroll_offset = 0.0; 309 | self.cur_group = 0; 310 | self.load_apps(); 311 | self.needs_clear = true; 312 | let fetch_gpus = Task::perform(try_get_gpus(), |gpus| { 313 | cosmic::Action::App(Message::GpuUpdate(gpus)) 314 | }); 315 | return Task::batch(vec![ 316 | get_layer_surface(SctkLayerSurfaceSettings { 317 | id: WINDOW_ID.clone(), 318 | keyboard_interactivity: KeyboardInteractivity::Exclusive, 319 | anchor: Anchor::all(), 320 | namespace: "app-library".into(), 321 | size: Some((None, None)), 322 | exclusive_zone: -1, 323 | ..Default::default() 324 | }), 325 | overlap_notify(WINDOW_ID.clone(), true), 326 | fetch_gpus, 327 | ]) 328 | .chain(text_input::focus(SEARCH_ID.clone())) 329 | .chain( 330 | iced_runtime::task::widget(find_focused()) 331 | .map(|id| cosmic::Action::App(Message::UpdateFocused(Some(id)))), 332 | ); 333 | } 334 | Task::none() 335 | } 336 | 337 | fn handle_overlap(&mut self) { 338 | if !self.active_surface { 339 | return; 340 | } 341 | 342 | let mid_height = self.height / 2.; 343 | self.margin = 0.; 344 | 345 | for o in self.overlap.values() { 346 | if self.margin + mid_height < o.y 347 | || self.margin > o.y + o.height 348 | || mid_height < o.y + o.height 349 | { 350 | continue; 351 | } 352 | 353 | self.margin = o.y + o.height; 354 | } 355 | } 356 | } 357 | 358 | #[derive(Clone, Debug)] 359 | enum Message { 360 | UpdateFocused(Option), 361 | InputChanged(String), 362 | KeyboardNav(keyboard_nav::Action), 363 | PrevRow, 364 | NextRow, 365 | Layer(LayerEvent, SurfaceId), 366 | Hide, 367 | ActivateApp(usize, Option), 368 | StartCurAppFocus, 369 | ActivationToken(Option, String, String, Option, bool), 370 | SelectGroup(usize), 371 | Delete(usize), 372 | ConfirmDelete, 373 | CancelDelete, 374 | StartEditName(String), 375 | EditName(String), 376 | SubmitName, 377 | StartNewGroup, 378 | NewGroup(String), 379 | SubmitNewGroup, 380 | CancelNewGroup, 381 | LoadApps, 382 | FilterApps(String, Vec>), 383 | OpenContextMenu(Rectangle, usize), 384 | CloseContextMenu, 385 | SelectAction(MenuAction), 386 | StartDrag(usize), 387 | FinishDrag(bool), 388 | CancelDrag, 389 | StartDndOffer(usize), 390 | FinishDndOffer(usize, Option), 391 | LeaveDndOffer(usize), 392 | ScrollYOffset(f32), 393 | GpuUpdate(Option>), 394 | PinToAppTray(usize), 395 | UnPinFromAppTray(usize), 396 | AppListConfig(AppListConfig), 397 | Opened(Size, SurfaceId), 398 | Overlap(OverlapNotifyEvent), 399 | Surface(surface::Action), 400 | } 401 | 402 | #[derive(Clone)] 403 | struct DndCommand(Arc DataDeviceOfferInner>>); 404 | 405 | impl Debug for DndCommand { 406 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 407 | f.debug_struct("DndCommand").finish() 408 | } 409 | } 410 | 411 | #[derive(Clone, Debug)] 412 | enum MenuAction { 413 | Remove, 414 | DesktopAction(String), 415 | } 416 | 417 | pub fn menu_button<'a, Message: Clone + 'a>( 418 | content: impl Into>, 419 | ) -> cosmic::widget::Button<'a, Message> { 420 | cosmic::widget::button::custom(content) 421 | .class(Button::AppletMenu) 422 | .padding(menu_control_padding()) 423 | .width(Length::Fill) 424 | } 425 | 426 | pub fn menu_control_padding() -> Padding { 427 | let theme = cosmic::theme::active(); 428 | let cosmic = theme.cosmic(); 429 | [cosmic.space_xxs(), cosmic.space_m()].into() 430 | } 431 | 432 | impl CosmicAppLibrary { 433 | pub fn load_apps(&mut self) { 434 | let xdg_current_desktop = std::env::var("XDG_CURRENT_DESKTOP").ok(); 435 | self.all_entries = cosmic::desktop::load_applications( 436 | self.locale.as_slice(), 437 | false, 438 | xdg_current_desktop.as_deref(), 439 | ) 440 | .into_iter() 441 | .map(Arc::new) 442 | .collect(); 443 | self.all_entries.sort_by(|a, b| a.name.cmp(&b.name)); 444 | 445 | self.entry_path_input = 446 | self.config 447 | .filtered(self.cur_group, &self.search_value, &self.all_entries); 448 | 449 | // collect duplicates 450 | self.duplicates.clear(); 451 | self.duplicates = self 452 | .all_entries 453 | .iter() 454 | .enumerate() 455 | .fold( 456 | (std::mem::take(&mut self.duplicates), 0, "", ""), 457 | |(mut dups, cur_count, cur_name, cur_id): (HashMap<_, _>, usize, &str, &str), 458 | (i, e)| { 459 | if cur_name.to_lowercase().trim() == e.name.to_lowercase().trim() 460 | || e.id == cur_id 461 | { 462 | if cur_count == 1 { 463 | // insert previous entry 464 | if let Some(path) = self.all_entries[i - 1].path.as_ref() { 465 | let source = AppSource::from(path.as_ref()); 466 | dups.insert(path.clone(), source); 467 | } 468 | } 469 | if let Some(path) = e.path.as_ref() { 470 | let source = AppSource::from(path.as_ref()); 471 | dups.insert(path.clone(), source); 472 | } 473 | (dups, cur_count + 1, cur_name, cur_id) 474 | } else { 475 | (dups, 1, e.name.as_str(), e.id.as_str()) 476 | } 477 | }, 478 | ) 479 | .0; 480 | self.entry_ids = (0..self.entry_path_input.len()) 481 | .map(|_| widget::Id::unique()) 482 | .collect(); 483 | } 484 | 485 | fn filter_apps(&mut self) -> Task { 486 | let config = self.config.clone(); 487 | let all_entries = self.all_entries.clone(); 488 | let cur_group = self.cur_group; 489 | let input = self.search_value.clone(); 490 | if !self.waiting_for_filtered { 491 | self.waiting_for_filtered = true; 492 | iced::Task::perform( 493 | async move { 494 | let mut apps = config.filtered(cur_group, &input, &all_entries); 495 | apps.sort_by(|a, b| a.name.cmp(&b.name)); 496 | (input, apps) 497 | }, 498 | |(input, apps)| Message::FilterApps(input, apps), 499 | ) 500 | .map(cosmic::Action::App) 501 | } else { 502 | iced::Task::none() 503 | } 504 | } 505 | 506 | pub fn hide(&mut self) -> Task { 507 | // cancel existing dnd if it exists then try again... 508 | if self.dnd_icon.take().is_some() { 509 | return Task::batch(vec![ 510 | end_dnd(), 511 | Task::perform(async {}, |_| cosmic::Action::App(Message::Hide)), 512 | ]); 513 | } 514 | self.focused_id = None; 515 | self.entry_ids.clear(); 516 | self.active_surface = false; 517 | self.new_group = None; 518 | self.search_value.clear(); 519 | self.edit_name = None; 520 | self.cur_group = 0; 521 | self.menu = None; 522 | self.group_to_delete = None; 523 | self.scroll_offset = 0.0; 524 | iced::Task::batch(vec![ 525 | text_input::focus(SEARCH_ID.clone()), 526 | destroy_popup(MENU_ID.clone()), 527 | destroy_layer_surface(NEW_GROUP_WINDOW_ID.clone()), 528 | destroy_layer_surface(DELETE_GROUP_WINDOW_ID.clone()), 529 | destroy_layer_surface(WINDOW_ID.clone()), 530 | ]) 531 | } 532 | 533 | fn activate_app( 534 | &mut self, 535 | i: usize, 536 | gpu_idx: Option, 537 | ) -> Task<::Message> { 538 | self.edit_name = None; 539 | if let Some(de) = self.entry_path_input.get(i) { 540 | let app_id = de.id.clone(); 541 | let exec = de.exec.clone().unwrap(); 542 | let terminal = de.terminal; 543 | return request_token( 544 | Some(String::from(::APP_ID)), 545 | Some(WINDOW_ID.clone()), 546 | ) 547 | .map(move |t| { 548 | cosmic::Action::App(Message::ActivationToken( 549 | t, 550 | app_id.clone(), 551 | exec.clone(), 552 | gpu_idx, 553 | terminal, 554 | )) 555 | }); 556 | } else { 557 | Task::none() 558 | } 559 | } 560 | } 561 | 562 | impl cosmic::Application for CosmicAppLibrary { 563 | type Message = Message; 564 | type Executor = executor::Default; 565 | type Flags = Args; 566 | const APP_ID: &'static str = "com.system76.CosmicAppLibrary"; 567 | 568 | fn core(&self) -> &Core { 569 | &self.core 570 | } 571 | 572 | fn update(&mut self, message: Message) -> Task { 573 | match message { 574 | Message::UpdateFocused(id) => { 575 | self.focused_id = id; 576 | let i = self 577 | .focused_id 578 | .as_ref() 579 | .and_then(|focused| self.entry_ids.iter().position(|i| i == focused)) 580 | .unwrap_or(0); 581 | let y = 582 | ((i / 7) as f32 / ((self.entry_path_input.len() / 7) as f32).max(1.)).max(0.0); 583 | 584 | return iced_runtime::task::widget(operation::scrollable::snap_to( 585 | self.scrollable_id.clone(), 586 | RelativeOffset { x: 0., y }, 587 | )); 588 | } 589 | Message::KeyboardNav(message) => match message { 590 | keyboard_nav::Action::FocusNext => { 591 | return iced::Task::batch(vec![ 592 | iced::widget::focus_next() 593 | .map(|id| cosmic::Action::App(Message::UpdateFocused(id))), 594 | iced_runtime::task::widget(find_focused()) 595 | .map(|id| cosmic::Action::App(Message::UpdateFocused(Some(id)))), 596 | ]); 597 | } 598 | keyboard_nav::Action::FocusPrevious => { 599 | return iced::Task::batch(vec![ 600 | iced::widget::focus_previous() 601 | .map(|id| cosmic::Action::App(Message::UpdateFocused(id))), 602 | iced_runtime::task::widget(find_focused()) 603 | .map(|id| cosmic::Action::App(Message::UpdateFocused(Some(id)))), 604 | ]); 605 | } 606 | keyboard_nav::Action::Escape => return self.on_escape(), 607 | keyboard_nav::Action::Search => return self.on_search(), 608 | 609 | keyboard_nav::Action::Fullscreen => {} 610 | }, 611 | 612 | Message::PrevRow => { 613 | let mut i = self 614 | .focused_id 615 | .as_ref() 616 | .and_then(|focused| self.entry_ids.iter().position(|i| i == focused)) 617 | .unwrap_or(self.entry_ids.len().saturating_add(6)); 618 | if i == 0 { 619 | self.focused_id = None; 620 | 621 | return iced::Task::batch(vec![ 622 | iced::widget::focus_previous() 623 | .map(|id| cosmic::Action::App(Message::UpdateFocused(id))), 624 | iced_runtime::task::widget(find_focused()) 625 | .map(|id| cosmic::Action::App(Message::UpdateFocused(Some(id)))), 626 | ]); 627 | } 628 | i = i.saturating_sub(7); 629 | let y = 630 | ((i / 7) as f32 / ((self.entry_path_input.len() / 7) as f32).max(1.)).max(0.0); 631 | 632 | let Some(focused) = self.entry_ids.get(i).cloned() else { 633 | return Task::none(); 634 | }; 635 | self.focused_id = Some(focused.clone()); 636 | return Task::batch(vec![ 637 | iced_runtime::task::widget(focus(focused)) 638 | .map(|id| cosmic::Action::App(Message::UpdateFocused(Some(id)))), 639 | iced_runtime::task::widget(operation::scrollable::snap_to( 640 | self.scrollable_id.clone(), 641 | RelativeOffset { x: 0., y }, 642 | )), 643 | ]); 644 | } 645 | Message::NextRow => { 646 | let mut i: i32 = self 647 | .focused_id 648 | .as_ref() 649 | .and_then(|focused| self.entry_ids.iter().position(|i| i == focused)) 650 | .map(|i| i as i32) 651 | .unwrap_or(-7); 652 | if i == self.entry_ids.len() as i32 - 1 { 653 | self.focused_id = None; 654 | return iced::Task::batch(vec![ 655 | iced::widget::focus_next() 656 | .map(|id| cosmic::Action::App(Message::UpdateFocused(id))), 657 | iced_runtime::task::widget(find_focused()) 658 | .map(|id| cosmic::Action::App(Message::UpdateFocused(Some(id)))), 659 | ]); 660 | } 661 | i += 7; 662 | i = i.min(self.entry_ids.len() as i32 - 1); 663 | let Some(focused) = self.entry_ids.get(i as usize).cloned() else { 664 | return Task::none(); 665 | }; 666 | self.focused_id = Some(focused.clone()); 667 | let y = 668 | ((i / 7) as f32 / ((self.entry_path_input.len() / 7) as f32).max(1.)).max(0.0); 669 | 670 | return Task::batch(vec![ 671 | iced_runtime::task::widget(operation::scrollable::snap_to( 672 | self.scrollable_id.clone(), 673 | RelativeOffset { x: 0., y }, 674 | )), 675 | iced_runtime::task::widget(focus(focused)) 676 | .map(|id| cosmic::Action::App(Message::UpdateFocused(Some(id)))), 677 | ]); 678 | } 679 | Message::InputChanged(value) => { 680 | self.search_value = value; 681 | return self.filter_apps(); 682 | } 683 | Message::Layer(e, id) => match e { 684 | LayerEvent::Focused => { 685 | if self.menu.is_none() { 686 | if id == WINDOW_ID.clone() { 687 | return text_input::focus(SEARCH_ID.clone()); 688 | } else if id == DELETE_GROUP_WINDOW_ID.clone() { 689 | return button::focus(SUBMIT_DELETE_ID.clone()); 690 | } else if id == NEW_GROUP_WINDOW_ID.clone() { 691 | return text_input::focus(NEW_GROUP_ID.clone()); 692 | } 693 | } 694 | } 695 | LayerEvent::Unfocused => { 696 | self.last_hide = Some(Instant::now()); 697 | if self.active_surface 698 | && id == WINDOW_ID.clone() 699 | && self.menu.is_none() 700 | && self.new_group.is_none() 701 | && self.group_to_delete.is_none() 702 | { 703 | return self.hide(); 704 | } 705 | } 706 | LayerEvent::Done if id == WINDOW_ID.clone() => { 707 | // no need for commands here 708 | _ = self.hide(); 709 | } 710 | _ => {} 711 | }, 712 | Message::Hide => { 713 | return self.hide(); 714 | } 715 | Message::ActivateApp(i, gpu_idx) => { 716 | return self.activate_app(i, gpu_idx); 717 | } 718 | Message::StartCurAppFocus => { 719 | let i = if self 720 | .focused_id 721 | .as_ref() 722 | .is_some_and(|cur_focus| cur_focus == &*SEARCH_ID) 723 | { 724 | 0 725 | } else if let Some(i) = self 726 | .focused_id 727 | .as_ref() 728 | .and_then(|focus| self.entry_ids.iter().position(|id| focus == id)) 729 | { 730 | i 731 | } else { 732 | 0 733 | }; 734 | let gpu_idx = None; 735 | return self.activate_app(i, gpu_idx); 736 | } 737 | Message::ActivationToken(token, app_id, exec, gpu_idx, terminal) => { 738 | let mut env_vars = Vec::new(); 739 | if let Some(token) = token { 740 | env_vars.push(("XDG_ACTIVATION_TOKEN".to_string(), token.clone())); 741 | env_vars.push(("DESKTOP_STARTUP_ID".to_string(), token)); 742 | } 743 | if let (Some(gpus), Some(idx)) = (self.gpus.as_ref(), gpu_idx) { 744 | env_vars.extend(gpus[idx].environment.clone().into_iter()); 745 | } 746 | tokio::spawn(async move { 747 | cosmic::desktop::spawn_desktop_exec(exec, env_vars, Some(&app_id), terminal) 748 | .await 749 | }); 750 | return self.update(Message::Hide); 751 | } 752 | Message::SelectGroup(i) => { 753 | self.edit_name = None; 754 | self.search_value.clear(); 755 | self.cur_group = i; 756 | self.scroll_offset = 0.0; 757 | self.scrollable_id = Id::new( 758 | self.config 759 | .groups() 760 | .get(self.cur_group) 761 | .map(|g| g.name.clone()) 762 | .unwrap_or_else(|| "unknown-group".to_string()), 763 | ); 764 | let mut cmds = vec![self.filter_apps()]; 765 | if self.cur_group == 0 { 766 | cmds.push(text_input::focus(SEARCH_ID.clone())); 767 | } 768 | return iced::Task::batch(cmds); 769 | } 770 | Message::LoadApps => { 771 | return self.filter_apps(); 772 | } 773 | Message::Delete(group) => { 774 | self.group_to_delete = Some(group); 775 | return Task::batch(vec![ 776 | get_layer_surface(SctkLayerSurfaceSettings { 777 | id: DELETE_GROUP_WINDOW_ID.clone(), 778 | keyboard_interactivity: KeyboardInteractivity::Exclusive, 779 | anchor: Anchor::empty(), 780 | namespace: "dialog".into(), 781 | size: None, 782 | ..Default::default() 783 | }), 784 | button::focus(SUBMIT_DELETE_ID.clone()), 785 | ]); 786 | } 787 | Message::EditName(name) => { 788 | self.edit_name = Some(name); 789 | } 790 | Message::SubmitName => { 791 | if let Some(name) = self.edit_name.take() { 792 | self.config.set_name(self.cur_group, name); 793 | } 794 | if let Some(helper) = self.helper.as_ref() { 795 | if let Err(err) = self.config.write_entry(helper) { 796 | error!("{:?}", err); 797 | } 798 | } 799 | } 800 | Message::StartEditName(name) => { 801 | self.edit_name = Some(name); 802 | return text_input::focus(EDIT_GROUP_ID.clone()); 803 | } 804 | Message::StartNewGroup => { 805 | if self.new_group.is_some() { 806 | return Task::none(); 807 | } 808 | self.new_group = Some(String::new()); 809 | return Task::batch(vec![ 810 | get_layer_surface(SctkLayerSurfaceSettings { 811 | id: NEW_GROUP_WINDOW_ID.clone(), 812 | keyboard_interactivity: KeyboardInteractivity::Exclusive, 813 | anchor: Anchor::empty(), 814 | namespace: "dialog".into(), 815 | size: None, 816 | ..Default::default() 817 | }), 818 | text_input::focus(NEW_GROUP_ID.clone()), 819 | ]); 820 | } 821 | Message::NewGroup(group_name) => { 822 | self.new_group = Some(group_name); 823 | } 824 | Message::SubmitNewGroup => { 825 | if let Some(group_name) = self.new_group.take() { 826 | self.config.add(group_name); 827 | } 828 | if let Some(helper) = self.helper.as_ref() { 829 | if let Err(err) = self.config.write_entry(helper) { 830 | error!("{:?}", err); 831 | } 832 | } 833 | return destroy_layer_surface(NEW_GROUP_WINDOW_ID.clone()); 834 | } 835 | Message::CancelNewGroup => { 836 | self.new_group = None; 837 | return destroy_layer_surface(NEW_GROUP_WINDOW_ID.clone()); 838 | } 839 | Message::OpenContextMenu(rect, i) => { 840 | if self.menu.take().is_some() { 841 | return destroy_popup(MENU_ID.clone()); 842 | } else { 843 | self.menu = Some(i); 844 | return commands::popup::get_popup(SctkPopupSettings { 845 | parent: WINDOW_ID.clone(), 846 | id: MENU_ID.clone(), 847 | positioner: SctkPositioner { 848 | size: None, 849 | size_limits: Limits::NONE.min_width(1.0).min_height(1.0).max_width(300.0).max_height(800.0), 850 | anchor_rect: Rectangle { 851 | x: rect.x as i32, 852 | y: rect.y as i32 - self.scroll_offset as i32, 853 | width: rect.width as i32, 854 | height: rect.height as i32, 855 | }, 856 | anchor: 857 | sctk::reexports::protocols::xdg::shell::client::xdg_positioner::Anchor::Right, 858 | gravity: sctk::reexports::protocols::xdg::shell::client::xdg_positioner::Gravity::Right, 859 | reactive: true, 860 | ..Default::default() 861 | }, 862 | grab: false, 863 | parent_size: None, 864 | close_with_children: true, 865 | input_zone: None, 866 | }); 867 | } 868 | } 869 | Message::CloseContextMenu => { 870 | self.menu = None; 871 | return commands::popup::destroy_popup(MENU_ID.clone()); 872 | } 873 | Message::SelectAction(action) => { 874 | self.menu = None; 875 | let mut tasks = vec![commands::popup::destroy_popup(MENU_ID.clone())]; 876 | if let Some(info) = self.menu.take().and_then(|i| self.entry_path_input.get(i)) { 877 | match action { 878 | MenuAction::Remove => { 879 | self.config.remove_entry(self.cur_group, &info.id); 880 | if let Some(helper) = self.helper.as_ref() { 881 | if let Err(err) = self.config.write_entry(helper) { 882 | error!("{:?}", err); 883 | } 884 | } 885 | tasks.push(self.filter_apps()); 886 | } 887 | MenuAction::DesktopAction(exec) => { 888 | let mut exec = shlex::Shlex::new(&exec); 889 | 890 | let mut cmd = match exec.next() { 891 | Some(cmd) if !cmd.contains('=') => { 892 | tokio::process::Command::new(cmd) 893 | } 894 | _ => return Task::none(), 895 | }; 896 | for arg in exec { 897 | // TODO handle "%" args here if necessary? 898 | if !arg.starts_with('%') { 899 | cmd.arg(arg); 900 | } 901 | } 902 | let _ = cmd.spawn(); 903 | return self.hide(); 904 | } 905 | } 906 | } 907 | return cosmic::Task::batch(tasks); 908 | } 909 | Message::StartDrag(i) => { 910 | self.dnd_icon = Some(i); 911 | } 912 | Message::FinishDrag(copy) => { 913 | if !copy { 914 | if let Some(info) = self 915 | .dnd_icon 916 | .take() 917 | .and_then(|i| self.entry_path_input.get(i)) 918 | { 919 | self.config.remove_entry(self.cur_group, &info.id); 920 | if let Some(helper) = self.helper.as_ref() { 921 | if let Err(err) = self.config.write_entry(helper) { 922 | error!("{:?}", err); 923 | } 924 | } 925 | return self.filter_apps(); 926 | } 927 | } 928 | } 929 | Message::CancelDrag => { 930 | self.dnd_icon = None; 931 | } 932 | Message::StartDndOffer(i) => { 933 | self.offer_group = Some(i); 934 | } 935 | Message::FinishDndOffer(i, entry) => { 936 | self.offer_group = None; 937 | let Some(entry) = entry else { 938 | return Task::none(); 939 | }; 940 | self.config.add_entry(i, &entry.id); 941 | if let Some(helper) = self.helper.as_ref() { 942 | if let Err(err) = self.config.write_entry(helper) { 943 | error!("{:?}", err); 944 | } 945 | } 946 | } 947 | Message::LeaveDndOffer(i) => { 948 | self.offer_group = self.offer_group.filter(|g| *g != i); 949 | } 950 | Message::ScrollYOffset(y) => { 951 | self.scroll_offset = y; 952 | } 953 | Message::ConfirmDelete => { 954 | let mut cmds = vec![destroy_layer_surface(DELETE_GROUP_WINDOW_ID.clone())]; 955 | if let Some(group) = self.group_to_delete.take() { 956 | self.config.remove(group); 957 | if let Some(helper) = self.helper.as_ref() { 958 | if let Err(err) = self.config.write_entry(helper) { 959 | error!("{:?}", err); 960 | } 961 | } 962 | self.cur_group = 0; 963 | cmds.push(self.filter_apps()); 964 | } 965 | return Task::batch(cmds); 966 | } 967 | Message::CancelDelete => { 968 | self.group_to_delete = None; 969 | return destroy_layer_surface(DELETE_GROUP_WINDOW_ID.clone()); 970 | } 971 | Message::FilterApps(input, filtered_apps) => { 972 | self.entry_path_input = filtered_apps; 973 | self.entry_ids = (0..self.entry_path_input.len()) 974 | .map(|_| widget::Id::unique()) 975 | .collect(); 976 | self.waiting_for_filtered = false; 977 | if self.search_value != input { 978 | return self.filter_apps(); 979 | } 980 | } 981 | Message::GpuUpdate(gpus) => { 982 | self.gpus = gpus; 983 | } 984 | Message::PinToAppTray(usize) => { 985 | let pinned_id = self.entry_path_input.get(usize).map(|e| e.id.clone()); 986 | if let Some((pinned_id, app_list_helper)) = pinned_id 987 | .zip(Config::new(cosmic_app_list_config::APP_ID, AppListConfig::VERSION).ok()) 988 | { 989 | self.app_list_config.add_pinned(pinned_id, &app_list_helper); 990 | } 991 | self.menu = None; 992 | return commands::popup::destroy_popup(MENU_ID.clone()); 993 | } 994 | Message::UnPinFromAppTray(usize) => { 995 | let pinned_id = self.entry_path_input.get(usize).map(|e| e.id.clone()); 996 | if let Some((pinned_id, app_list_helper)) = pinned_id 997 | .zip(Config::new(cosmic_app_list_config::APP_ID, AppListConfig::VERSION).ok()) 998 | { 999 | self.app_list_config 1000 | .remove_pinned(&pinned_id, &app_list_helper); 1001 | } 1002 | self.menu = None; 1003 | return commands::popup::destroy_popup(MENU_ID.clone()); 1004 | } 1005 | Message::AppListConfig(config) => { 1006 | self.app_list_config = config; 1007 | } 1008 | Message::Opened(size, window_id) => { 1009 | if window_id == WINDOW_ID.clone() { 1010 | self.height = size.height; 1011 | self.handle_overlap(); 1012 | } 1013 | } 1014 | Message::Overlap(overlap_notify_event) => match overlap_notify_event { 1015 | OverlapNotifyEvent::OverlapLayerAdd { 1016 | identifier, 1017 | namespace, 1018 | logical_rect, 1019 | exclusive, 1020 | .. 1021 | } => { 1022 | if self.needs_clear { 1023 | self.needs_clear = false; 1024 | self.overlap.clear(); 1025 | } 1026 | if exclusive > 0 || namespace == "Dock" || namespace == "Panel" { 1027 | self.overlap.insert(identifier, logical_rect); 1028 | } 1029 | self.handle_overlap(); 1030 | } 1031 | OverlapNotifyEvent::OverlapLayerRemove { identifier } => { 1032 | self.overlap.remove(&identifier); 1033 | self.handle_overlap(); 1034 | } 1035 | _ => {} 1036 | }, 1037 | Message::Surface(a) => { 1038 | return cosmic::task::message(cosmic::Action::Cosmic( 1039 | cosmic::app::Action::Surface(a), 1040 | )); 1041 | } 1042 | } 1043 | Task::none() 1044 | } 1045 | 1046 | fn dbus_activation(&mut self, msg: dbus_activation::Message) -> Task { 1047 | if matches!(msg.msg, dbus_activation::Details::Activate) { 1048 | self.activate() 1049 | } else { 1050 | Task::none() 1051 | } 1052 | } 1053 | 1054 | fn view(&self) -> Element { 1055 | unimplemented!() 1056 | } 1057 | 1058 | fn view_window(&self, id: SurfaceId) -> Element { 1059 | let Spacing { 1060 | space_none, 1061 | space_xxs, 1062 | space_xs, 1063 | space_s, 1064 | space_m, 1065 | space_l, 1066 | space_xxl, 1067 | .. 1068 | } = theme::active().cosmic().spacing; 1069 | 1070 | if id == MENU_ID.clone() { 1071 | let Some((menu, i)) = self 1072 | .menu 1073 | .as_ref() 1074 | .and_then(|i| self.entry_path_input.get(*i).map(|e| (e, i))) 1075 | else { 1076 | return container(horizontal_space()) 1077 | .width(Length::Fixed(1.0)) 1078 | .height(Length::Fixed(1.0)) 1079 | .into(); 1080 | }; 1081 | 1082 | let mut list_column = Vec::new(); 1083 | 1084 | if let Some(gpus) = self.gpus.as_ref() { 1085 | for (j, gpu) in gpus.iter().enumerate() { 1086 | let default_idx = if menu.prefers_dgpu { 1087 | gpus.iter().position(|gpu| !gpu.default).unwrap_or(0) 1088 | } else { 1089 | gpus.iter().position(|gpu| gpu.default).unwrap_or(0) 1090 | }; 1091 | list_column.push( 1092 | menu_button(body(format!( 1093 | "{} {}", 1094 | fl!("run-on", gpu = gpu.name.clone()), 1095 | if j == default_idx { 1096 | fl!("run-on-default") 1097 | } else { 1098 | String::new() 1099 | } 1100 | ))) 1101 | .on_press(Message::ActivateApp(*i, Some(j))) 1102 | .into(), 1103 | ) 1104 | } 1105 | } else { 1106 | list_column.push( 1107 | menu_button(body(RUN.clone())) 1108 | .on_press(Message::ActivateApp(*i, None)) 1109 | .into(), 1110 | ); 1111 | } 1112 | 1113 | if menu.desktop_actions.len() > 0 { 1114 | list_column.push(divider::horizontal::light().into()); 1115 | for action in menu.desktop_actions.iter() { 1116 | list_column.push( 1117 | menu_button(body(&action.name)) 1118 | .on_press(Message::SelectAction( 1119 | MenuAction::DesktopAction(action.exec.clone()).into(), 1120 | )) 1121 | .into(), 1122 | ); 1123 | } 1124 | } 1125 | 1126 | // add to pinned 1127 | let svg_accent = Rc::new(|theme: &cosmic::Theme| { 1128 | let color = theme.cosmic().accent_color().into(); 1129 | svg::Style { color: Some(color) } 1130 | }); 1131 | let is_pinned = self.app_list_config.favorites.iter().any(|p| p == &menu.id); 1132 | let pin_to_app_tray = menu_button( 1133 | if is_pinned { 1134 | row![ 1135 | icon::icon(icon::from_name("checkbox-checked-symbolic").size(16).into()) 1136 | .class(cosmic::theme::Svg::Custom(svg_accent.clone())), 1137 | body(fl!("pin-to-app-tray")) 1138 | ] 1139 | } else { 1140 | row![horizontal_space().width(16.0), body(fl!("pin-to-app-tray"))] 1141 | } 1142 | .spacing(space_xxs), 1143 | ) 1144 | .on_press(if is_pinned { 1145 | Message::UnPinFromAppTray(*i) 1146 | } else { 1147 | Message::PinToAppTray(*i) 1148 | }); 1149 | list_column.push(divider::horizontal::light().into()); 1150 | list_column.push(pin_to_app_tray.into()); 1151 | 1152 | if self.cur_group > 0 { 1153 | list_column.push(divider::horizontal::light().into()); 1154 | list_column.push( 1155 | menu_button(body(REMOVE.clone())) 1156 | .on_press(Message::SelectAction(MenuAction::Remove)) 1157 | .into(), 1158 | ); 1159 | } 1160 | 1161 | return autosize( 1162 | container(scrollable(Column::with_children(list_column))) 1163 | .padding([8, 0]) 1164 | .class(theme::Container::Custom(Box::new(|theme| { 1165 | container::Style { 1166 | text_color: Some(theme.cosmic().on_bg_color().into()), 1167 | background: Some(Color::from(theme.cosmic().background.base).into()), 1168 | border: Border { 1169 | color: theme.cosmic().bg_divider().into(), 1170 | radius: theme.cosmic().corner_radii.radius_m.into(), 1171 | width: 1.0, 1172 | }, 1173 | shadow: Shadow::default(), 1174 | icon_color: Some(theme.cosmic().on_bg_color().into()), 1175 | } 1176 | }))) 1177 | .width(Length::Shrink) 1178 | .height(Length::Shrink) 1179 | .align_x(Horizontal::Center) 1180 | .align_y(Vertical::Top), 1181 | MENU_AUTOSIZE_ID.clone(), 1182 | ) 1183 | .max_height(800.) 1184 | .max_width(300.) 1185 | .into(); 1186 | } 1187 | if id == NEW_GROUP_WINDOW_ID.clone() { 1188 | let Some(group_name) = self.new_group.as_ref() else { 1189 | return container(horizontal_space()) 1190 | .width(Length::Fixed(1.0)) 1191 | .height(Length::Fixed(1.0)) 1192 | .into(); 1193 | }; 1194 | let dialog = column![ 1195 | container(text(CREATE_NEW.as_str()).size(24)) 1196 | .align_x(Horizontal::Left) 1197 | .width(Length::Fixed(432.0)), 1198 | text_input("", group_name) 1199 | .label(&*NEW_GROUP_PLACEHOLDER) 1200 | .on_input(Message::NewGroup) 1201 | .on_submit(|_| Message::SubmitNewGroup) 1202 | .width(Length::Fixed(432.0)) 1203 | .size(14) 1204 | .id(NEW_GROUP_ID.clone()), 1205 | container( 1206 | row![ 1207 | button::custom( 1208 | container(text(CANCEL.to_string()).size(14.0)) 1209 | .width(Length::Shrink) 1210 | .align_x(Horizontal::Center) 1211 | .width(Length::Fill) 1212 | ) 1213 | .on_press(Message::CancelNewGroup) 1214 | .padding([space_xxs, space_s]) 1215 | .width(142), 1216 | button::custom( 1217 | container(text(SAVE.to_string()).size(14.0)) 1218 | .width(Length::Shrink) 1219 | .align_x(Horizontal::Center) 1220 | .width(Length::Fill) 1221 | ) 1222 | .class(Button::Suggested) 1223 | .on_press(Message::SubmitNewGroup) 1224 | .padding([space_xxs, space_s]) 1225 | .width(142), 1226 | ] 1227 | .spacing(space_s) 1228 | ) 1229 | .width(Length::Fixed(432.0)) 1230 | .align_x(Horizontal::Right) 1231 | ] 1232 | .align_x(Alignment::Center) 1233 | .spacing(space_s); 1234 | return autosize( 1235 | container(dialog) 1236 | .class(theme::Container::Custom(Box::new(|theme| { 1237 | container::Style { 1238 | text_color: Some(theme.cosmic().on_bg_color().into()), 1239 | icon_color: Some(theme.cosmic().on_bg_color().into()), 1240 | background: Some(Color::from(theme.cosmic().background.base).into()), 1241 | border: Border { 1242 | color: theme.cosmic().bg_divider().into(), 1243 | radius: theme.cosmic().corner_radii.radius_m.into(), 1244 | width: 1.0, 1245 | }, 1246 | shadow: Shadow::default(), 1247 | } 1248 | }))) 1249 | .width(Length::Shrink) 1250 | .height(Length::Shrink) 1251 | .padding(space_s), 1252 | NEW_GROUP_AUTOSIZE_ID.clone(), 1253 | ) 1254 | .into(); 1255 | } 1256 | if id == DELETE_GROUP_WINDOW_ID.clone() { 1257 | let dialog = column![ 1258 | row![ 1259 | container( 1260 | icon::icon(icon::from_name("edit-delete-symbolic").into()) 1261 | .width(Length::Fixed(48.0)) 1262 | .height(Length::Fixed(48.0)) 1263 | ) 1264 | .padding(8), 1265 | column![ 1266 | text(fl!("delete-folder")).size(24), 1267 | text(fl!("delete-folder", "msg")) 1268 | ] 1269 | .spacing(8) 1270 | .width(Length::Fixed(360.0)) 1271 | ] 1272 | .spacing(16), 1273 | container( 1274 | row![ 1275 | button::custom( 1276 | container(text(CANCEL.to_string()).size(14.0)) 1277 | .width(Length::Shrink) 1278 | .align_x(Horizontal::Center) 1279 | .width(Length::Fill) 1280 | ) 1281 | .on_press(Message::CancelDelete) 1282 | .padding([space_xxs, space_m]) 1283 | .width(142), 1284 | button::custom( 1285 | container(text(fl!("delete")).size(14.0)) 1286 | .width(Length::Shrink) 1287 | .align_x(Horizontal::Center) 1288 | .width(Length::Fill) 1289 | ) 1290 | .id(SUBMIT_DELETE_ID.clone()) 1291 | .class(Button::Destructive) 1292 | .on_press(Message::ConfirmDelete) 1293 | .padding([space_xxs, space_m]) 1294 | .width(142), 1295 | ] 1296 | .spacing(space_s) 1297 | ) 1298 | .width(Length::Fixed(432.0)) 1299 | .align_x(Horizontal::Right) 1300 | ] 1301 | .align_x(Alignment::Center) 1302 | .spacing(space_l); 1303 | return autosize( 1304 | container(dialog) 1305 | .class(theme::Container::Custom(Box::new(|theme| { 1306 | container::Style { 1307 | text_color: Some(theme.cosmic().on_bg_color().into()), 1308 | icon_color: Some(theme.cosmic().on_bg_color().into()), 1309 | background: Some(Color::from(theme.cosmic().background.base).into()), 1310 | border: Border { 1311 | color: theme.cosmic().bg_divider().into(), 1312 | radius: theme.cosmic().corner_radii.radius_m.into(), 1313 | width: 1.0, 1314 | }, 1315 | shadow: Shadow::default(), 1316 | } 1317 | }))) 1318 | .width(Length::Shrink) 1319 | .height(Length::Shrink) 1320 | .padding(space_m), 1321 | DELETE_GROUP_AUTOSIZE_ID.clone(), 1322 | ) 1323 | .into(); 1324 | } 1325 | 1326 | let cur_group = self.config.groups()[self.cur_group]; 1327 | let top_row = if self.cur_group == 0 { 1328 | row![ 1329 | container( 1330 | search_input(SEARCH_PLACEHOLDER.as_str(), self.search_value.as_str()) 1331 | .on_input(Message::InputChanged) 1332 | .on_paste(Message::InputChanged) 1333 | .on_submit(|_| Message::StartCurAppFocus) 1334 | .style(TextInput::Search) 1335 | .width(Length::Fixed(400.0)) 1336 | .size(14) 1337 | .id(SEARCH_ID.clone()) 1338 | ) 1339 | .align_y(Vertical::Center) 1340 | .height(Length::Fixed(96.0)) 1341 | ] 1342 | .align_y(Alignment::Center) 1343 | .spacing(space_xxs) 1344 | } else { 1345 | row![ 1346 | horizontal_space().width(Length::FillPortion(1)), 1347 | if let Some(edit_name) = self.edit_name.as_ref() { 1348 | container( 1349 | text_input(cur_group.name(), edit_name) 1350 | .on_input(Message::EditName) 1351 | .on_paste(Message::EditName) 1352 | .on_clear(Message::EditName(String::new())) 1353 | .on_submit(|_| Message::SubmitName) 1354 | .id(EDIT_GROUP_ID.clone()) 1355 | .width(Length::Fixed(200.0)) 1356 | .size(14), 1357 | ) 1358 | } else { 1359 | container(text(cur_group.name()).size(24)) 1360 | }, 1361 | row![ 1362 | horizontal_space(), 1363 | tooltip( 1364 | { 1365 | let mut b = button::custom( 1366 | icon::icon(icon::from_name("edit-symbolic").into()) 1367 | .width(Length::Fixed(32.0)) 1368 | .height(Length::Fixed(32.0)), 1369 | ) 1370 | .padding(space_xs) 1371 | .class(Button::Icon); 1372 | if self.edit_name.is_none() { 1373 | b = b.on_press(Message::StartEditName(cur_group.name())); 1374 | } 1375 | container(b) 1376 | .height(Length::Fixed(96.0)) 1377 | .align_y(Vertical::Center) 1378 | }, 1379 | text(fl!("rename")), 1380 | tooltip::Position::Bottom 1381 | ), 1382 | tooltip( 1383 | container( 1384 | button::custom( 1385 | icon::icon(icon::from_name("edit-delete-symbolic").into()) 1386 | .width(Length::Fixed(32.0)) 1387 | .height(Length::Fixed(32.0)), 1388 | ) 1389 | .padding(space_xs) 1390 | .class(Button::Icon) 1391 | .on_press(Message::Delete(self.cur_group)) 1392 | ) 1393 | .height(Length::Fixed(96.0)) 1394 | .align_y(Vertical::Center), 1395 | text(fl!("delete")), 1396 | tooltip::Position::Bottom 1397 | ) 1398 | ] 1399 | .spacing(space_xxs) 1400 | .width(Length::FillPortion(1)) 1401 | ] 1402 | .padding([0, space_l]) 1403 | .align_y(Alignment::Center) 1404 | }; 1405 | 1406 | // TODO grid widget in libcosmic 1407 | let app_grid_list: Vec<_> = self 1408 | .entry_path_input 1409 | .iter() 1410 | .zip(self.entry_ids.iter()) 1411 | .enumerate() 1412 | .map(|(i, (entry, id))| { 1413 | let gpu_idx = self.gpus.as_ref().map(|gpus| { 1414 | if entry.prefers_dgpu { 1415 | gpus.iter().position(|gpu| !gpu.default).unwrap_or(0) 1416 | } else { 1417 | gpus.iter().position(|gpu| gpu.default).unwrap_or(0) 1418 | } 1419 | }); 1420 | let dup = entry 1421 | .path 1422 | .as_ref() 1423 | .and_then(|path| self.duplicates.get(path)); 1424 | let selected = self.menu.is_some_and(|m| m == i); 1425 | 1426 | let b = ApplicationButton::new( 1427 | id.clone(), 1428 | &entry, 1429 | move |rect| Message::OpenContextMenu(rect, i), 1430 | if self.menu.is_none() { 1431 | Some(Message::ActivateApp(i, gpu_idx)) 1432 | } else if selected { 1433 | Some(Message::CloseContextMenu) 1434 | } else { 1435 | None 1436 | }, 1437 | // TODO add icon and text if duplicated 1438 | dup, 1439 | selected, 1440 | self.menu.is_none().then_some(Message::StartDrag(i)), 1441 | self.menu.is_none().then_some(Message::FinishDrag(false)), 1442 | self.menu.is_none().then_some(Message::CancelDrag), 1443 | ); 1444 | 1445 | b.into() 1446 | }) 1447 | .chunks(7) 1448 | .into_iter() 1449 | .map(|row_chunk| { 1450 | let mut new_row = row_chunk.collect_vec(); 1451 | let missing = 7 - new_row.len(); 1452 | if missing > 0 { 1453 | new_row.push( 1454 | iced::widget::horizontal_space() 1455 | .width(Length::FillPortion(missing.try_into().unwrap())) 1456 | .into(), 1457 | ); 1458 | } 1459 | row(new_row).spacing(space_xxs).into() 1460 | }) 1461 | .collect(); 1462 | 1463 | let app_scrollable = container( 1464 | scrollable( 1465 | column(app_grid_list) 1466 | .width(Length::Fill) 1467 | .spacing(space_xxs) 1468 | .padding([space_none, space_xxl, space_xxs, space_xxl]), 1469 | ) 1470 | .on_scroll(|viewport| Message::ScrollYOffset(viewport.absolute_offset().y)) 1471 | .id(self.scrollable_id.clone()) 1472 | .height(Length::Fill), 1473 | ) 1474 | .max_height(444.0); 1475 | 1476 | // TODO use the spacing variables from the theme 1477 | let (group_icon_size, h_padding, group_width, chunks) = if self.config.groups().len() > 15 { 1478 | (16.0, space_xxs, 96.0, 11) 1479 | } else { 1480 | (32.0, space_s, 128.0, 8) 1481 | }; 1482 | let group_height = 1483 | group_icon_size + 20.0 + (space_none as f32) + (space_xxs as f32) + (space_s as f32); 1484 | 1485 | let mut add_group_btn = Some( 1486 | button::custom( 1487 | column![ 1488 | container( 1489 | icon::icon(icon::from_name("folder-new-symbolic").into()) 1490 | .width(Length::Fixed(group_icon_size)) 1491 | .height(Length::Fixed(group_icon_size)) 1492 | ) 1493 | .padding(space_xxs), 1494 | text(fl!("add-group")).size(14.0).width(Length::Shrink) 1495 | ] 1496 | .align_x(Alignment::Center) 1497 | .width(Length::Fill), 1498 | ) 1499 | .height(Length::Fixed(group_height)) 1500 | .width(Length::Fixed(group_width)) 1501 | .class(theme::Button::IconVertical) 1502 | .padding([space_none, h_padding, space_xxs, h_padding]) 1503 | .on_press(Message::StartNewGroup), 1504 | ); 1505 | let mut group_rows: Vec<_> = self 1506 | .config 1507 | .groups() 1508 | .chunks(chunks) 1509 | .enumerate() 1510 | .map(|(chunk, groups)| { 1511 | let mut group_row = row![] 1512 | .spacing(space_xxs) 1513 | .padding([space_s, space_none]) 1514 | .align_y(Alignment::Center); 1515 | for (i, group) in groups.iter().enumerate() { 1516 | let i = i + chunk * chunks; 1517 | let group_button = dnd_destination_for_data::( 1518 | button::custom( 1519 | column![ 1520 | container( 1521 | icon::icon(from_name(group.icon.clone()).into()) 1522 | .width(Length::Fixed(group_icon_size)) 1523 | .height(Length::Fixed(group_icon_size)) 1524 | ) 1525 | .padding(space_xxs), 1526 | text(group.name()).size(14).width(Length::Shrink) 1527 | ] 1528 | .align_x(Alignment::Center) 1529 | .width(Length::Fill), 1530 | ) 1531 | .height(Length::Fixed(group_height)) 1532 | .width(Length::Fixed(group_width)) 1533 | .class( 1534 | if self.offer_group == Some(i) 1535 | || (self.cur_group == i && self.offer_group.is_none()) 1536 | { 1537 | // TODO customize the IconVertical to highlight in the way we need 1538 | Button::Custom { 1539 | active: Box::new(|focused, theme| { 1540 | let s = 1541 | theme.pressed(focused, false, &Button::IconVertical); 1542 | s 1543 | }), 1544 | disabled: Box::new(|theme| { 1545 | let s = theme.disabled(&Button::IconVertical); 1546 | s 1547 | }), 1548 | hovered: Box::new(|focused, theme| { 1549 | let s = 1550 | theme.hovered(focused, false, &Button::IconVertical); 1551 | s 1552 | }), 1553 | pressed: Box::new(|focused, theme| { 1554 | let s = 1555 | theme.pressed(focused, false, &Button::IconVertical); 1556 | s 1557 | }), 1558 | } 1559 | } else { 1560 | Button::IconVertical 1561 | }, 1562 | ) 1563 | .padding([space_none, h_padding, space_xxs, h_padding]) 1564 | .on_press_maybe(self.menu.is_none().then_some(Message::SelectGroup(i))), 1565 | move |data, _| { 1566 | Message::FinishDndOffer( 1567 | i, 1568 | data.and_then(|data| load_desktop_file(&[], data.0)), 1569 | ) 1570 | }, 1571 | ) 1572 | .on_enter(move |_, _, _| Message::StartDndOffer(i)) 1573 | .on_leave(move || Message::LeaveDndOffer(i)); 1574 | 1575 | group_row = group_row.push(group_button); 1576 | } 1577 | if groups.len() < chunks { 1578 | group_row = group_row.push(add_group_btn.take().unwrap()); 1579 | } 1580 | group_row 1581 | }) 1582 | .collect(); 1583 | 1584 | if let Some(add_group_button) = add_group_btn.take() { 1585 | group_rows.push( 1586 | row![add_group_button] 1587 | .spacing(8) 1588 | .padding([space_s, space_none]) 1589 | .align_y(Alignment::Center), 1590 | ); 1591 | }; 1592 | let group_rows = 1593 | Column::with_children(group_rows.into_iter().map(|r| r.into()).collect_vec()); 1594 | 1595 | let content = column![ 1596 | top_row, 1597 | app_scrollable, 1598 | container(horizontal_rule(1)) 1599 | .padding([space_none, space_xxl]) 1600 | .width(Length::Fill), 1601 | group_rows 1602 | ] 1603 | .align_x(Alignment::Center); 1604 | 1605 | let window = container(content) 1606 | .height(Length::Fill) 1607 | .max_height(685) 1608 | .max_width(1200.0) 1609 | .class(theme::Container::Custom(Box::new(|theme| { 1610 | container::Style { 1611 | text_color: Some(theme.cosmic().on_bg_color().into()), 1612 | background: Some(Color::from(theme.cosmic().background.base).into()), 1613 | border: Border { 1614 | radius: theme.cosmic().corner_radii.radius_m.into(), 1615 | width: 1.0, 1616 | color: theme.cosmic().bg_divider().into(), 1617 | }, 1618 | shadow: Shadow::default(), 1619 | icon_color: Some(theme.cosmic().on_bg_color().into()), 1620 | } 1621 | }))) 1622 | .center_x(Length::Fill); 1623 | row![ 1624 | mouse_area( 1625 | container(horizontal_space().width(Length::Fixed(1.0))) 1626 | .width(Length::Fill) 1627 | .height(Length::Fill) 1628 | ) 1629 | .on_press(Message::Hide), 1630 | container( 1631 | column![ 1632 | mouse_area( 1633 | container(vertical_space()) 1634 | .width(Length::Fill) 1635 | .height(Length::Fixed(self.margin + 16.)) 1636 | ) 1637 | .on_press(Message::Hide), 1638 | container( 1639 | mouse_area(window) 1640 | .on_release(Message::CloseContextMenu) 1641 | .on_right_release(Message::CloseContextMenu) 1642 | ) 1643 | .width(Length::Shrink) 1644 | .height(Length::Shrink), 1645 | mouse_area( 1646 | container(vertical_space()) 1647 | .width(Length::Fill) 1648 | .height(Length::Fill) 1649 | ) 1650 | .on_press(Message::Hide) 1651 | ] 1652 | .height(Length::Fill) 1653 | ) 1654 | .max_width(1200.0) 1655 | .width(Length::Shrink) 1656 | .height(Length::Fill), 1657 | mouse_area( 1658 | container(horizontal_space().width(Length::Fixed(1.0))) 1659 | .width(Length::Fill) 1660 | .height(Length::Fill) 1661 | ) 1662 | .on_press(Message::Hide), 1663 | ] 1664 | .width(Length::Fill) 1665 | .height(Length::Fill) 1666 | .into() 1667 | } 1668 | 1669 | fn subscription(&self) -> Subscription { 1670 | Subscription::batch( 1671 | vec![ 1672 | desktop_files(0).map(|_| Message::LoadApps), 1673 | listen_with(|e, status, id| match e { 1674 | cosmic::iced::Event::PlatformSpecific(PlatformSpecific::Wayland( 1675 | wayland::Event::Layer(e, _, id), 1676 | )) => Some(Message::Layer(e, id)), 1677 | cosmic::iced::Event::PlatformSpecific(PlatformSpecific::Wayland( 1678 | wayland::Event::OverlapNotify(event), 1679 | )) => Some(Message::Overlap(event)), 1680 | cosmic::iced::Event::Keyboard(cosmic::iced::keyboard::Event::KeyReleased { 1681 | key: Key::Named(Named::Escape), 1682 | modifiers: _mods, 1683 | .. 1684 | }) => Some(Message::Hide), 1685 | cosmic::iced::Event::Mouse(iced::mouse::Event::ButtonPressed(_)) 1686 | if id == WINDOW_ID.clone() => 1687 | { 1688 | Some(Message::CloseContextMenu) 1689 | } 1690 | cosmic::iced::Event::Keyboard(iced::keyboard::Event::KeyPressed { 1691 | key, 1692 | text: _, 1693 | modifiers, 1694 | .. 1695 | }) => match key { 1696 | Key::Character(c) if modifiers.control() && (c == "p" || c == "k") => { 1697 | Some(Message::PrevRow) 1698 | } 1699 | Key::Character(c) if modifiers.control() && (c == "n" || c == "j") => { 1700 | Some(Message::NextRow) 1701 | } 1702 | Key::Character(c) if modifiers.control() && (c == "f" || c == "l") => { 1703 | Some(Message::KeyboardNav(keyboard_nav::Action::FocusNext)) 1704 | } 1705 | Key::Character(c) if modifiers.control() && (c == "b" || c == "h") => { 1706 | Some(Message::KeyboardNav(keyboard_nav::Action::FocusPrevious)) 1707 | } 1708 | Key::Named(Named::ArrowUp) 1709 | if matches!(status, iced::event::Status::Ignored) => 1710 | { 1711 | Some(Message::PrevRow) 1712 | } 1713 | Key::Named(Named::ArrowDown) 1714 | if matches!(status, iced::event::Status::Ignored) => 1715 | { 1716 | Some(Message::NextRow) 1717 | } 1718 | Key::Named(Named::ArrowLeft) 1719 | if matches!(status, iced::event::Status::Ignored) => 1720 | { 1721 | Some(Message::KeyboardNav(keyboard_nav::Action::FocusPrevious)) 1722 | } 1723 | Key::Named(Named::ArrowRight) 1724 | if matches!(status, iced::event::Status::Ignored) => 1725 | { 1726 | Some(Message::KeyboardNav(keyboard_nav::Action::FocusNext)) 1727 | } 1728 | _ => None, 1729 | }, 1730 | cosmic::iced::Event::Window(WindowEvent::Opened { position: _, size }) => { 1731 | Some(Message::Opened(size, id)) 1732 | } 1733 | _ => None, 1734 | }), 1735 | keyboard_nav::subscription().map(|a| Message::KeyboardNav(a)), 1736 | self.core 1737 | .watch_config::( 1738 | cosmic_app_list_config::APP_ID, 1739 | ) 1740 | .map(|config| Message::AppListConfig(config.config)), 1741 | ] 1742 | .into_iter(), 1743 | ) 1744 | } 1745 | 1746 | fn core_mut(&mut self) -> &mut Core { 1747 | &mut self.core 1748 | } 1749 | 1750 | fn init(mut core: Core, _flags: Args) -> (Self, iced::Task>) { 1751 | core.set_keyboard_nav(false); 1752 | let helper = AppLibraryConfig::helper(); 1753 | 1754 | let mut config: AppLibraryConfig = helper 1755 | .as_ref() 1756 | .map(|helper| { 1757 | AppLibraryConfig::get_entry(helper).unwrap_or_else(|(errors, config)| { 1758 | for err in errors { 1759 | error!("{:?}", err); 1760 | } 1761 | config 1762 | }) 1763 | }) 1764 | .unwrap_or_default(); 1765 | config.groups.sort(); 1766 | let scrollable_id = Id::new( 1767 | config 1768 | .groups() 1769 | .get(0) 1770 | .map(|g| g.name.clone()) 1771 | .unwrap_or_else(|| "unknown-group".to_string()), 1772 | ); 1773 | let self_ = Self { 1774 | locale: std::env::var("LANG") 1775 | .ok() 1776 | .and_then(|l| l.split(".").next().map(str::to_string)), 1777 | config, 1778 | core, 1779 | helper, 1780 | last_hide: None, 1781 | margin: 0., 1782 | overlap: HashMap::new(), 1783 | height: 100., 1784 | scrollable_id, 1785 | ..Default::default() 1786 | }; 1787 | 1788 | (self_, Task::none()) 1789 | } 1790 | } 1791 | -------------------------------------------------------------------------------- /src/app_group.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::Arc, vec}; 2 | 3 | use cosmic::{ 4 | cosmic_config::{self, CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry}, 5 | desktop::DesktopEntryData, 6 | }; 7 | use serde::{Deserialize, Serialize}; 8 | use std::sync::LazyLock; 9 | 10 | use crate::{config::APP_ID, fl}; 11 | 12 | static HOME: LazyLock<[AppGroup; 1]> = LazyLock::new(|| { 13 | [AppGroup { 14 | name: "cosmic-library-home".to_string(), 15 | icon: "user-home-symbolic".to_string(), 16 | filter: FilterType::None, 17 | }] 18 | }); 19 | 20 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] 21 | pub enum FilterType { 22 | /// A list of application IDs to include in the group. 23 | AppIds(Vec), 24 | Categories { 25 | categories: Vec, 26 | /// The ID of applications which may not match the categories, but should be included anyway. 27 | exclude: Vec, 28 | /// The ID of applications which should be excluded from the results. 29 | include: Vec, 30 | }, 31 | /// No filter is applied. 32 | /// This is intended for use with Home. 33 | None, 34 | } 35 | 36 | impl Default for FilterType { 37 | fn default() -> Self { 38 | FilterType::AppIds(Vec::new()) 39 | } 40 | } 41 | 42 | impl Ord for FilterType { 43 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 44 | match (self, other) { 45 | (FilterType::AppIds(_), FilterType::AppIds(_)) => std::cmp::Ordering::Equal, 46 | (FilterType::None, FilterType::None) => std::cmp::Ordering::Equal, 47 | (FilterType::Categories { .. }, FilterType::Categories { .. }) => { 48 | std::cmp::Ordering::Equal 49 | } 50 | (FilterType::Categories { .. } | FilterType::None, FilterType::AppIds(_)) => { 51 | std::cmp::Ordering::Less 52 | } 53 | (FilterType::AppIds(_), FilterType::Categories { .. } | FilterType::None) => { 54 | std::cmp::Ordering::Greater 55 | } 56 | (FilterType::Categories { .. }, FilterType::None) => std::cmp::Ordering::Greater, 57 | (FilterType::None, FilterType::Categories { .. }) => std::cmp::Ordering::Less, 58 | } 59 | } 60 | } 61 | 62 | impl PartialOrd for FilterType { 63 | fn partial_cmp(&self, other: &Self) -> Option { 64 | Some(self.cmp(other)) 65 | } 66 | } 67 | 68 | // Object holding the state 69 | #[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] 70 | pub struct AppGroup { 71 | pub name: String, 72 | pub icon: String, 73 | pub filter: FilterType, 74 | // pub popup: bool, 75 | } 76 | 77 | impl PartialOrd for AppGroup { 78 | fn partial_cmp(&self, other: &Self) -> Option { 79 | Some(self.cmp(other)) 80 | } 81 | } 82 | 83 | impl Ord for AppGroup { 84 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 85 | match (&self.filter, &other.filter) { 86 | (FilterType::AppIds(_), FilterType::AppIds(_)) => { 87 | self.name.to_lowercase().cmp(&other.name.to_lowercase()) 88 | } 89 | (a, b) => a.cmp(b), 90 | } 91 | } 92 | } 93 | 94 | impl AppGroup { 95 | pub fn filtered( 96 | &self, 97 | input_value: &str, 98 | exceptions: &[Self], 99 | all_entries: &[Arc], 100 | ) -> Vec> { 101 | all_entries 102 | .iter() 103 | .filter(|de| { 104 | let mut keep_de = self.matches(de); 105 | keep_de &= if input_value.is_empty() { 106 | !exceptions.iter().any(|x| x.matches(de)) 107 | } else { 108 | de.name.to_lowercase().contains(&input_value.to_lowercase()) 109 | || de 110 | .categories 111 | .iter() 112 | .any(|acat| acat.to_lowercase() == input_value.to_lowercase()) 113 | }; 114 | keep_de 115 | }) 116 | .cloned() 117 | .collect() 118 | } 119 | 120 | fn matches(&self, entry: &DesktopEntryData) -> bool { 121 | match &self.filter { 122 | FilterType::AppIds(names) => names.iter().any(|id| id == &entry.id), 123 | FilterType::Categories { 124 | categories, 125 | include, 126 | exclude, 127 | .. 128 | } => { 129 | categories.iter().any(|cat| { 130 | entry 131 | .categories 132 | .iter() 133 | .any(|acat| acat.to_lowercase() == cat.to_lowercase()) 134 | }) && exclude.iter().all(|id| id != &entry.id) 135 | || include.iter().any(|id| id == &entry.id) 136 | } 137 | FilterType::None => true, 138 | } 139 | } 140 | 141 | pub fn name(&self) -> String { 142 | if &self.name == "cosmic-library-home" { 143 | fl!("cosmic-library-home") 144 | } else if &self.name == "cosmic-office" { 145 | fl!("cosmic-office") 146 | } else if &self.name == "cosmic-system" { 147 | fl!("cosmic-system") 148 | } else if &self.name == "cosmic-utilities" { 149 | fl!("cosmic-utilities") 150 | } else { 151 | self.name.clone() 152 | } 153 | } 154 | } 155 | 156 | #[derive(Debug, Clone, Serialize, Deserialize, CosmicConfigEntry)] 157 | pub struct AppLibraryConfig { 158 | pub(crate) groups: Vec, 159 | } 160 | 161 | impl AppLibraryConfig { 162 | pub fn version() -> u64 { 163 | 1 164 | } 165 | 166 | pub fn helper() -> Option { 167 | cosmic_config::Config::new(APP_ID, Self::version()).ok() 168 | } 169 | 170 | pub fn add(&mut self, name: String) { 171 | self.groups.push(AppGroup { 172 | name, 173 | icon: "folder-symbolic".to_string(), 174 | filter: FilterType::AppIds(Vec::new()), 175 | }); 176 | self.groups.sort(); 177 | } 178 | 179 | pub fn remove(&mut self, i: usize) { 180 | if i == 0 { 181 | return; 182 | } 183 | if i - 1 < self.groups.len() { 184 | self.groups.remove(i - 1); 185 | } 186 | } 187 | 188 | pub fn set_name(&mut self, i: usize, name: String) { 189 | if i == 0 { 190 | return; 191 | } 192 | if i - 1 < self.groups.len() { 193 | self.groups[i - 1].name = name; 194 | } 195 | } 196 | 197 | pub fn remove_entry(&mut self, i: usize, id: &str) { 198 | if i == 0 { 199 | return; 200 | } 201 | if let Some(group) = self.groups.get_mut(i - 1) { 202 | match &mut group.filter { 203 | FilterType::AppIds(ids) => ids.retain(|conf_id| conf_id != id), 204 | FilterType::Categories { 205 | exclude, include, .. 206 | } => { 207 | include.retain(|conf_id| conf_id != id); 208 | exclude.retain(|conf_id| conf_id != id); 209 | exclude.push(id.to_string()); 210 | } 211 | FilterType::None => {} 212 | } 213 | } 214 | if i - 1 < self.groups.len() { 215 | if let FilterType::AppIds(ids) = &mut self.groups[i - 1].filter { 216 | ids.retain(|x| x != id); 217 | } 218 | } 219 | } 220 | 221 | pub fn add_entry(&mut self, i: usize, id: &str) { 222 | if i > 0 && i - 1 < self.groups.len() { 223 | if let FilterType::AppIds(ids) = &mut self.groups[i - 1].filter { 224 | if ids.iter().all(|s| s != id) { 225 | ids.push(id.to_string()); 226 | } 227 | } else if let FilterType::Categories { 228 | exclude, include, .. 229 | } = &mut self.groups[i - 1].filter 230 | { 231 | include.retain(|conf_id| conf_id != id); 232 | exclude.retain(|conf_id| conf_id != id); 233 | include.push(id.to_string()); 234 | } 235 | } else { 236 | // add to filter of all groups, forcing it to the Home group 237 | for group in &mut self.groups { 238 | match &mut group.filter { 239 | FilterType::AppIds(ids) => { 240 | ids.retain(|conf_id| conf_id != id); 241 | } 242 | FilterType::Categories { 243 | categories: _, 244 | exclude, 245 | include, 246 | } => { 247 | include.retain(|conf_id| conf_id != id); 248 | if exclude.iter().all(|conf_id| conf_id != id) { 249 | exclude.push(id.to_string()); 250 | } 251 | } 252 | FilterType::None => {} 253 | } 254 | } 255 | } 256 | } 257 | 258 | pub fn groups(&self) -> Vec<&AppGroup> { 259 | HOME.iter().chain(&self.groups).collect() 260 | } 261 | 262 | pub fn filtered( 263 | &self, 264 | i: usize, 265 | input_value: &str, 266 | entries: &Vec>, 267 | ) -> Vec> { 268 | if i == 0 { 269 | HOME[0].filtered(input_value, &self.groups, entries) 270 | } else { 271 | self._filtered(i - 1, input_value, entries) 272 | } 273 | } 274 | 275 | pub fn _filtered( 276 | &self, 277 | i: usize, 278 | input_value: &str, 279 | entries: &Vec>, 280 | ) -> Vec> { 281 | self.groups 282 | .get(i) 283 | .map(|g| g.filtered(input_value, &Vec::new(), entries)) 284 | .unwrap_or_default() 285 | } 286 | } 287 | 288 | impl Default for AppLibraryConfig { 289 | fn default() -> Self { 290 | AppLibraryConfig { 291 | groups: vec![ 292 | AppGroup { 293 | name: "cosmic-office".to_string(), 294 | icon: "folder-symbolic".to_string(), 295 | filter: FilterType::Categories { 296 | categories: vec!["Office".to_string()], 297 | include: vec![ 298 | "org.gnome.Totem".to_string(), 299 | "org.gnome.eog".to_string(), 300 | "simple-scan".to_string(), 301 | "thunderbird".to_string(), 302 | ], 303 | exclude: Vec::new(), 304 | }, 305 | }, 306 | AppGroup { 307 | name: "cosmic-system".to_string(), 308 | icon: "folder-symbolic".to_string(), 309 | filter: FilterType::Categories { 310 | categories: vec!["System".to_string()], 311 | include: vec![ 312 | "gnome-language-selector".to_string(), 313 | "im-config".to_string(), 314 | "org.freedesktop.IBus.Setup".to_string(), 315 | "system76-driver".to_string(), 316 | ], 317 | exclude: vec![ 318 | "com.system76.CosmicStore".to_string(), 319 | "com.system76.CosmicTerm".to_string(), 320 | ], 321 | }, 322 | }, 323 | AppGroup { 324 | name: "cosmic-utilities".to_string(), 325 | icon: "folder-symbolic".to_string(), 326 | filter: FilterType::Categories { 327 | categories: vec!["Utility".to_string()], 328 | include: vec!["nm-connection-editor".to_string()], 329 | exclude: vec![ 330 | "com.system76.CosmicEdit".to_string(), 331 | "com.system76.CosmicFiles".to_string(), 332 | ], 333 | }, 334 | }, 335 | ], 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | pub const APP_ID: &str = "com.system76.CosmicAppLibrary"; 2 | pub const VERSION: &str = "0.1.0"; 3 | -------------------------------------------------------------------------------- /src/icon_cache.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | use cosmic::widget::icon; 4 | use std::{ 5 | collections::HashMap, 6 | sync::{Mutex, OnceLock}, 7 | }; 8 | 9 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 10 | pub struct IconCacheKey { 11 | name: &'static str, 12 | size: u16, 13 | } 14 | 15 | pub struct IconCache { 16 | cache: HashMap, 17 | } 18 | 19 | impl IconCache { 20 | pub fn new() -> Self { 21 | let mut cache = HashMap::new(); 22 | 23 | macro_rules! bundle { 24 | ($name:expr, $size:expr) => { 25 | let data: &'static [u8] = include_bytes!(concat!("../data/icons/", $name, ".svg")); 26 | cache.insert( 27 | IconCacheKey { 28 | name: $name, 29 | size: $size, 30 | }, 31 | icon::from_svg_bytes(data).symbolic($name.ends_with("-symbolic")), 32 | ); 33 | }; 34 | } 35 | 36 | bundle!("app-source-flatpak", 16); 37 | bundle!("app-source-local-symbolic", 16); 38 | bundle!("app-source-snap", 16); 39 | bundle!("app-source-nix", 16); 40 | bundle!("app-source-system-symbolic", 16); 41 | 42 | Self { cache } 43 | } 44 | 45 | pub fn get(&mut self, name: &'static str, size: u16) -> icon::Handle { 46 | self.cache 47 | .entry(IconCacheKey { name, size }) 48 | .or_insert_with(|| { 49 | icon::from_name(name) 50 | .size(size) 51 | .symbolic(name.ends_with("-symbolic")) 52 | .handle() 53 | }) 54 | .clone() 55 | } 56 | } 57 | 58 | static ICON_CACHE: OnceLock> = OnceLock::new(); 59 | 60 | pub fn icon_cache_handle(name: &'static str, size: u16) -> icon::Handle { 61 | let mut icon_cache = ICON_CACHE 62 | .get_or_init(|| Mutex::new(IconCache::new())) 63 | .lock() 64 | .unwrap(); 65 | icon_cache.get(name, size) 66 | } 67 | -------------------------------------------------------------------------------- /src/localize.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 System76 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | use i18n_embed::{ 5 | DefaultLocalizer, LanguageLoader, Localizer, 6 | fluent::{FluentLanguageLoader, fluent_language_loader}, 7 | }; 8 | use rust_embed::RustEmbed; 9 | use std::sync::LazyLock; 10 | 11 | #[derive(RustEmbed)] 12 | #[folder = "i18n/"] 13 | struct Localizations; 14 | 15 | pub static LANGUAGE_LOADER: LazyLock = LazyLock::new(|| { 16 | let loader: FluentLanguageLoader = fluent_language_loader!(); 17 | 18 | loader 19 | .load_fallback_language(&Localizations) 20 | .expect("Error while loading fallback language"); 21 | 22 | loader 23 | }); 24 | 25 | #[macro_export] 26 | macro_rules! fl { 27 | ($message_id:literal) => {{ 28 | i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id) 29 | }}; 30 | 31 | ($message_id:literal, $($args:expr),*) => {{ 32 | i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *) 33 | }}; 34 | } 35 | 36 | // Get the `Localizer` to be used for localizing this library. 37 | pub fn localizer() -> Box { 38 | Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) 39 | } 40 | 41 | pub fn localize() { 42 | let localizer = localizer(); 43 | let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages(); 44 | 45 | if let Err(error) = localizer.select(&requested_languages) { 46 | eprintln!("Error while loading language for App List {}", error); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[rustfmt::skip] 2 | mod config; 3 | mod app; 4 | mod app_group; 5 | mod icon_cache; 6 | mod localize; 7 | mod subscriptions; 8 | mod widgets; 9 | 10 | use config::APP_ID; 11 | use log::info; 12 | 13 | use localize::localize; 14 | 15 | use crate::config::VERSION; 16 | 17 | // TODO watch the desktop dirs for changes and update the list of apps on change 18 | 19 | fn main() -> cosmic::iced::Result { 20 | // Initialize logger 21 | pretty_env_logger::init(); 22 | info!("Cosmic App Library ({})", APP_ID); 23 | info!("Version: {}", VERSION); 24 | // Prepare i18n 25 | localize(); 26 | 27 | app::run() 28 | } 29 | -------------------------------------------------------------------------------- /src/subscriptions/desktop_files.rs: -------------------------------------------------------------------------------- 1 | use cosmic::{ 2 | iced::{Subscription, stream}, 3 | iced_futures::futures::{self, SinkExt}, 4 | }; 5 | use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; 6 | use std::fmt::Debug; 7 | use std::hash::Hash; 8 | use tokio::sync::mpsc; 9 | 10 | #[derive(Debug, Clone, Copy)] 11 | pub enum Event { 12 | Changed, 13 | } 14 | 15 | pub fn desktop_files( 16 | id: I, 17 | ) -> cosmic::iced::Subscription { 18 | Subscription::run_with_id( 19 | id, 20 | stream::channel(50, move |mut output| async move { 21 | let handle = tokio::runtime::Handle::current(); 22 | let (tx, mut rx) = mpsc::channel(4); 23 | let mut last_update = std::time::Instant::now(); 24 | 25 | // Automatically select the best implementation for your platform. 26 | // You can also access each implementation directly e.g. INotifyWatcher. 27 | let watcher = RecommendedWatcher::new( 28 | move |res: Result| { 29 | if let Ok(event) = res { 30 | match event.kind { 31 | EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => { 32 | let now = std::time::Instant::now(); 33 | if now.duration_since(last_update).as_secs() > 3 { 34 | _ = handle.block_on(tx.send(())); 35 | last_update = now; 36 | } 37 | } 38 | 39 | _ => (), 40 | } 41 | } 42 | }, 43 | Config::default(), 44 | ); 45 | 46 | if let Ok(mut watcher) = watcher { 47 | for path in cosmic::desktop::fde::default_paths() { 48 | let _ = watcher.watch(path.as_ref(), RecursiveMode::Recursive); 49 | } 50 | 51 | while rx.recv().await.is_some() { 52 | _ = output.send(Event::Changed).await; 53 | } 54 | } 55 | 56 | futures::future::pending().await 57 | }), 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/subscriptions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod desktop_files; 2 | -------------------------------------------------------------------------------- /src/widgets/application.rs: -------------------------------------------------------------------------------- 1 | //! A widget that can be dragged and dropped. 2 | 3 | use core::str; 4 | use std::{borrow::Cow, cell::RefCell, iter, path::PathBuf, str::FromStr}; 5 | 6 | use cosmic::desktop::IconSourceExt; 7 | use cosmic::{ 8 | iced::{ 9 | Size, Vector, 10 | alignment::Vertical, 11 | clipboard::mime::{AllowedMimeTypes, AsMimeTypes}, 12 | }, 13 | iced_core::alignment::Horizontal, 14 | widget::dnd_source, 15 | }; 16 | 17 | use cosmic::iced_core::{ 18 | Alignment, Clipboard, Event, Length, Rectangle, Shell, Widget, event, layout, mouse, overlay, 19 | renderer, widget, 20 | }; 21 | 22 | use cosmic::{ 23 | Element, 24 | desktop::DesktopEntryData, 25 | iced::widget::{column, text}, 26 | iced_core::widget::{Operation, Tree, tree}, 27 | theme, 28 | widget::{button, container}, 29 | }; 30 | 31 | use crate::app::AppSource; 32 | 33 | pub const MIME_TYPE: &str = "text/uri-list"; 34 | const DRAG_THRESHOLD: f32 = 25.0; 35 | /// A widget that can be dragged and dropped. 36 | #[allow(missing_debug_implementations)] 37 | pub struct ApplicationButton<'a, Message> { 38 | path: PathBuf, 39 | 40 | content: Element<'a, Message>, 41 | 42 | on_right_release: Box Message + 'a>, 43 | 44 | // Optional icon, and text 45 | source_icon: Option>, 46 | } 47 | 48 | impl<'a, Message: Clone + 'static> ApplicationButton<'a, Message> { 49 | /// Creates a new [`ApplicationButton`]. 50 | #[must_use] 51 | pub fn new( 52 | widget_id: widget::Id, 53 | DesktopEntryData { 54 | name, 55 | icon: image, 56 | path, 57 | .. 58 | }: &'a DesktopEntryData, 59 | on_right_release: impl Fn(Rectangle) -> Message + 'a, 60 | on_pressed: Option, 61 | source: Option<&AppSource>, 62 | selected: bool, 63 | on_start: Option, 64 | on_finish: Option, 65 | on_cancel: Option, 66 | ) -> Self { 67 | let cosmic::cosmic_theme::Spacing { 68 | space_xxs, space_s, .. 69 | } = theme::active().cosmic().spacing; 70 | 71 | let (source_icon, source_suffix_len) = match source { 72 | Some(source) => { 73 | let source_name = source.to_string(); 74 | ( 75 | source.as_icon().map(|i| { 76 | Element::from( 77 | container(i) 78 | .class(cosmic::theme::Container::Card) 79 | .width(Length::Fixed(24.0)) 80 | .height(Length::Fixed(24.0)) 81 | .align_x(Horizontal::Center) 82 | .align_y(Vertical::Center), 83 | ) 84 | }), 85 | source_name.len().saturating_add(3), // 3 for the parentheses 86 | ) 87 | } 88 | None => (None, 0), 89 | }; 90 | let max_name_len = 27 - source_suffix_len; 91 | let name = if name.len() > max_name_len { 92 | if let Some(source) = source { 93 | format!("{name:.17}... ({source})") 94 | } else { 95 | format!("{name:.24}...") 96 | } 97 | } else { 98 | if let Some(source) = source { 99 | format!("{name} ({source})") 100 | } else { 101 | name.to_string() 102 | } 103 | }; 104 | let path_ = path.clone(); 105 | let image_clone = image.clone(); 106 | let content = dnd_source( 107 | button::custom( 108 | column![ 109 | image 110 | .as_cosmic_icon() 111 | .width(Length::Fixed(72.0)) 112 | .height(Length::Fixed(72.0)), 113 | container(text(name).size(14.0).width(Length::Shrink)) 114 | .align_x(Horizontal::Center) 115 | .width(Length::Fill) 116 | .height(Length::Fixed(40.0)) 117 | ] 118 | .width(Length::Fixed(120.0)) 119 | .height(Length::Fixed(120.0)) 120 | .spacing(space_xxs) 121 | .align_x(Alignment::Center) 122 | .width(Length::Fill), 123 | ) 124 | .id(widget_id) 125 | .selected(selected) 126 | .width(Length::FillPortion(1)) 127 | .class(theme::Button::IconVertical) 128 | .padding(space_s) 129 | .on_press_maybe(on_pressed.clone()), 130 | ) 131 | .drag_icon(move |_| { 132 | ( 133 | image_clone 134 | .as_cosmic_icon() 135 | .width(Length::Fixed(72.0)) 136 | .height(Length::Fixed(72.0)) 137 | .into(), 138 | tree::State::None, 139 | cosmic::iced::Vector::ZERO, 140 | ) 141 | }) 142 | .drag_content(move || AppletString(path_.clone().unwrap())) 143 | .on_start(on_start) 144 | .on_cancel(on_cancel) 145 | .on_finish(on_finish) 146 | .into(); 147 | Self { 148 | path: path.clone().unwrap(), 149 | content, 150 | on_right_release: Box::new(on_right_release), 151 | 152 | source_icon, 153 | } 154 | } 155 | } 156 | 157 | impl<'a, Message> From> for Element<'a, Message> 158 | where 159 | Message: Clone + 'a, 160 | { 161 | fn from(dnd_source: ApplicationButton<'a, Message>) -> Element<'a, Message> { 162 | Element::new(dnd_source) 163 | } 164 | } 165 | 166 | impl<'a, Message> Widget 167 | for ApplicationButton<'a, Message> 168 | where 169 | Message: Clone, 170 | { 171 | fn children(&self) -> Vec { 172 | iter::once(Tree::new(&self.content)) 173 | .chain(self.source_icon.as_ref().map(|i| Tree::new(i))) 174 | .collect() 175 | } 176 | 177 | fn diff(&mut self, tree: &mut Tree) { 178 | let mut children: Vec<_> = iter::once(&mut self.content) 179 | .chain(self.source_icon.as_mut()) 180 | .collect(); 181 | tree.diff_children(children.as_mut_slice()); 182 | } 183 | 184 | fn size(&self) -> cosmic::iced_core::Size { 185 | self.content.as_widget().size() 186 | } 187 | 188 | fn layout( 189 | &self, 190 | tree: &mut Tree, 191 | renderer: &cosmic::Renderer, 192 | limits: &layout::Limits, 193 | ) -> layout::Node { 194 | let size = self.size(); 195 | let tree = RefCell::new(tree); 196 | layout( 197 | renderer, 198 | limits, 199 | size.width, 200 | size.height, 201 | u32::MAX, 202 | u32::MAX, 203 | |renderer, limits| { 204 | let content_state = &mut tree.borrow_mut().children[0]; 205 | self.content 206 | .as_widget() 207 | .layout(content_state, renderer, limits) 208 | }, 209 | self.source_icon.as_ref(), 210 | |renderer, limits, icon| { 211 | let icon_state = &mut tree.borrow_mut().children[1]; 212 | icon.as_widget().layout(icon_state, renderer, limits) 213 | }, 214 | ) 215 | } 216 | 217 | fn draw( 218 | &self, 219 | tree: &Tree, 220 | renderer: &mut cosmic::Renderer, 221 | theme: &cosmic::theme::Theme, 222 | renderer_style: &renderer::Style, 223 | layout: layout::Layout<'_>, 224 | cursor_position: mouse::Cursor, 225 | viewport: &Rectangle, 226 | ) { 227 | use cosmic::iced_core::Renderer; 228 | self.content.as_widget().draw( 229 | &tree.children[0], 230 | renderer, 231 | theme, 232 | renderer_style, 233 | layout.children().next().unwrap(), 234 | cursor_position, 235 | viewport, 236 | ); 237 | 238 | if let Some((icon, (l, bounds))) = self.source_icon.as_ref().zip( 239 | layout 240 | .children() 241 | .nth(1) 242 | .and_then(|l| viewport.intersection(&l.bounds()).map(|b| (l, b))), 243 | ) { 244 | renderer.with_layer(bounds, |renderer| { 245 | icon.as_widget().draw( 246 | &tree.children[1], 247 | renderer, 248 | theme, 249 | renderer_style, 250 | l, 251 | cursor_position, 252 | viewport, 253 | ) 254 | }); 255 | } 256 | } 257 | 258 | fn operate( 259 | &self, 260 | tree: &mut Tree, 261 | layout: layout::Layout<'_>, 262 | renderer: &cosmic::Renderer, 263 | operation: &mut dyn Operation<()>, 264 | ) { 265 | operation.container(None, layout.bounds(), &mut |operation| { 266 | self.content.as_widget().operate( 267 | &mut tree.children[0], 268 | layout.children().next().unwrap(), 269 | renderer, 270 | operation, 271 | ); 272 | }); 273 | } 274 | 275 | fn overlay<'b>( 276 | &'b mut self, 277 | tree: &'b mut Tree, 278 | layout: layout::Layout<'_>, 279 | renderer: &cosmic::Renderer, 280 | translation: Vector, 281 | ) -> Option> { 282 | self.content.as_widget_mut().overlay( 283 | &mut tree.children[0], 284 | layout.children().next().unwrap(), 285 | renderer, 286 | translation, 287 | ) 288 | } 289 | 290 | fn tag(&self) -> tree::Tag { 291 | tree::Tag::of::() 292 | } 293 | 294 | fn state(&self) -> tree::State { 295 | tree::State::new(State::default()) 296 | } 297 | 298 | fn on_event( 299 | &mut self, 300 | tree: &mut Tree, 301 | event: Event, 302 | layout: layout::Layout<'_>, 303 | cursor_position: mouse::Cursor, 304 | renderer: &cosmic::Renderer, 305 | clipboard: &mut dyn Clipboard, 306 | shell: &mut Shell<'_, Message>, 307 | viewport: &Rectangle, 308 | ) -> event::Status { 309 | let ret = self.content.as_widget_mut().on_event( 310 | &mut tree.children[0], 311 | event.clone(), 312 | layout.children().next().unwrap(), 313 | cursor_position, 314 | renderer, 315 | clipboard, 316 | shell, 317 | viewport, 318 | ); 319 | 320 | let state = tree.state.downcast_mut::(); 321 | 322 | if cursor_position.is_over(layout.bounds()) { 323 | match &event { 324 | Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => { 325 | state.right_press = true; 326 | return event::Status::Captured; 327 | } 328 | Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) => { 329 | if state.right_press { 330 | shell.publish(self.on_right_release.as_ref()(layout.bounds())); 331 | state.right_press = false; 332 | return event::Status::Captured; 333 | } 334 | } 335 | _ => {} 336 | } 337 | } 338 | 339 | ret 340 | } 341 | 342 | fn mouse_interaction( 343 | &self, 344 | tree: &Tree, 345 | layout: layout::Layout<'_>, 346 | cursor_position: mouse::Cursor, 347 | viewport: &Rectangle, 348 | renderer: &cosmic::Renderer, 349 | ) -> mouse::Interaction { 350 | self.content.as_widget().mouse_interaction( 351 | &tree.children[0], 352 | layout.children().next().unwrap(), 353 | cursor_position, 354 | viewport, 355 | renderer, 356 | ) 357 | } 358 | } 359 | 360 | /// Computes the layout of a [`ApplicationButton`]. 361 | pub fn layout<'a, Renderer, M>( 362 | renderer: &Renderer, 363 | limits: &layout::Limits, 364 | width: Length, 365 | height: Length, 366 | max_height: u32, 367 | max_width: u32, 368 | layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, 369 | icon: Option<&Element<'a, M>>, 370 | layout_icon: impl FnOnce(&Renderer, &layout::Limits, &Element<'a, M>) -> layout::Node, 371 | ) -> layout::Node { 372 | let limits = limits 373 | .loose() 374 | .max_height(max_height as f32) 375 | .max_width(max_width as f32) 376 | .width(width) 377 | .height(height); 378 | 379 | let content = layout_content(renderer, &limits); 380 | let size = limits.resolve(width, height, content.size()); 381 | let mut children = vec![content]; 382 | let app_icon_node = &children[0].children()[0].children()[0].children()[0]; 383 | if let Some(icon) = icon { 384 | let app_icon_size = app_icon_node.size(); 385 | let mut icon_node = layout_icon( 386 | renderer, 387 | &layout::Limits::new(Size::new(24., 24.), Size::new(24., 24.)), 388 | icon, 389 | ); 390 | icon_node = icon_node.move_to(app_icon_node.bounds().position()); 391 | // translate to the bottom right corner 392 | icon_node = icon_node.translate(Vector::new(app_icon_size.width, app_icon_size.height)); 393 | 394 | children.push(icon_node); 395 | } 396 | 397 | layout::Node::with_children(size, children) 398 | } 399 | 400 | /// A string which can be sent to the clipboard or drag-and-dropped. 401 | #[derive(Debug, Clone)] 402 | pub struct AppletString(pub PathBuf); 403 | 404 | impl AllowedMimeTypes for AppletString { 405 | fn allowed() -> std::borrow::Cow<'static, [String]> { 406 | std::borrow::Cow::Owned(vec![MIME_TYPE.to_string()]) 407 | } 408 | } 409 | 410 | impl TryFrom<(Vec, String)> for AppletString { 411 | type Error = anyhow::Error; 412 | 413 | fn try_from((value, mime): (Vec, String)) -> Result { 414 | if mime == MIME_TYPE { 415 | Ok(AppletString( 416 | url::Url::from_str(str::from_utf8(&value)?)? 417 | .to_file_path() 418 | .map_err(|_| anyhow::anyhow!("Invalid file path"))?, 419 | )) 420 | } else { 421 | Err(anyhow::anyhow!("Invalid mime")) 422 | } 423 | } 424 | } 425 | 426 | impl AsMimeTypes for AppletString { 427 | fn available(&self) -> std::borrow::Cow<'static, [String]> { 428 | std::borrow::Cow::Owned(vec![MIME_TYPE.to_string()]) 429 | } 430 | 431 | fn as_bytes(&self, mime_type: &str) -> Option> { 432 | if mime_type != MIME_TYPE { 433 | return None; 434 | } 435 | Some(Cow::Owned( 436 | url::Url::from_file_path(self.0.clone()) 437 | .ok()? 438 | .to_string() 439 | .into_bytes(), 440 | )) 441 | } 442 | } 443 | 444 | #[derive(Debug, Default, Clone)] 445 | pub struct State { 446 | right_press: bool, 447 | } 448 | -------------------------------------------------------------------------------- /src/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod application; 2 | --------------------------------------------------------------------------------