From dbdb573ac53cb0a5416db3e254f8fe17953e0db1 Mon Sep 17 00:00:00 2001 From: Andrew Huynh Date: Thu, 9 Mar 2023 20:16:17 -0800 Subject: [PATCH 01/19] updateing repo URLs in README & constants --- README.md | 8 ++++---- crates/client/src/pages/updater.rs | 2 +- crates/shared/src/constants.rs | 4 ++-- crates/spyglass-lens/Cargo.toml | 4 ++-- crates/spyglass-plugin/Cargo.toml | 4 ++-- scripts/generate-version.py | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 00b6ecdb2..58553d707 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@

Download now: - + macOS (Intel/ARM) | - + Windows | - + Linux (AppImage)
@@ -27,7 +27,7 @@

- +

diff --git a/crates/client/src/pages/updater.rs b/crates/client/src/pages/updater.rs index dbc05e108..bf8995e3c 100644 --- a/crates/client/src/pages/updater.rs +++ b/crates/client/src/pages/updater.rs @@ -50,7 +50,7 @@ pub fn updater_page() -> Html {
- + {"Release Notes"} diff --git a/crates/shared/src/constants.rs b/crates/shared/src/constants.rs index edf888ad6..4e97c12b9 100644 --- a/crates/shared/src/constants.rs +++ b/crates/shared/src/constants.rs @@ -1,8 +1,8 @@ -pub const GITHUB_REPO_URL: &str = "https://github.com/a5huynh/spyglass"; +pub const GITHUB_REPO_URL: &str = "https://github.com/spyglass-search/spyglass"; pub const DISCORD_JOIN_URL: &str = "https://discord.gg/663wPVBSTB"; pub const PAYMENT_URL: &str = "https://buy.stripe.com/00g0216rG5ow3MkdQQ"; -pub const APP_USER_AGENT: &str = "spyglass (github.com/a5huynh/spyglass)"; +pub const APP_USER_AGENT: &str = "spyglass (github.com/spyglass-search/spyglass)"; pub const LENS_DIRECTORY_INDEX_URL: &str = "https://raw.githubusercontent.com/spyglass-search/lens-box/main/index.ron"; diff --git a/crates/spyglass-lens/Cargo.toml b/crates/spyglass-lens/Cargo.toml index 106ed0f24..831feae5d 100644 --- a/crates/spyglass-lens/Cargo.toml +++ b/crates/spyglass-lens/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.6" edition = "2021" authors = ["Andrew Huynh "] description = "A small library for reading/writing spyglass lens files." -homepage = "https://github.com/a5huynh/spyglass/tree/main/crates/spyglass-lens" -repository = "https://github.com/a5huynh/spyglass/tree/main/crates/spyglass-lens" +homepage = "https://github.com/spyglass-search/spyglass/tree/main/crates/spyglass-lens" +repository = "https://github.com/spyglass-search/spyglass/tree/main/crates/spyglass-lens" readme = "README.md" keywords = ["spyglass", "lens", "lenses"] license = "MIT" diff --git a/crates/spyglass-plugin/Cargo.toml b/crates/spyglass-plugin/Cargo.toml index b62b5250d..2a47b2487 100644 --- a/crates/spyglass-plugin/Cargo.toml +++ b/crates/spyglass-plugin/Cargo.toml @@ -3,8 +3,8 @@ name = "spyglass-plugin" version = "0.1.1" authors = ["Andrew Huynh "] description = "A small client-side library for writing spyglass plugins" -homepage = "https://github.com/a5huynh/spyglass/tree/main/crates/spyglass-plugin" -repository = "https://github.com/a5huynh/spyglass/tree/main/crates/spyglass-plugin" +homepage = "https://github.com/spyglass-search/spyglass/tree/main/crates/spyglass-plugin" +repository = "https://github.com/spyglass-search/spyglass/tree/main/crates/spyglass-plugin" readme = "README.md" keywords = ["spyglass", "webassembly", "wasm", "plugins"] edition = "2021" diff --git a/scripts/generate-version.py b/scripts/generate-version.py index aaba96a9c..076e22ed0 100644 --- a/scripts/generate-version.py +++ b/scripts/generate-version.py @@ -51,7 +51,7 @@ def main(): version = { 'version': latest['tag_name'].replace('v20', ''), - 'notes': f"See full release notes here: https://github.com/a5huynh/spyglass/releases/tag/{latest['tag_name']}", + 'notes': f"See full release notes here: https://github.com/spyglass-search/spyglass/releases/tag/{latest['tag_name']}", 'pub_date': latest['published_at'], 'platforms': platforms } From c059c11da95f78056bb021f487537fdfee0f20d5 Mon Sep 17 00:00:00 2001 From: Andrew Huynh Date: Thu, 9 Mar 2023 20:18:31 -0800 Subject: [PATCH 02/19] update repos/githubusercontent URLs --- crates/tauri/tauri.conf.json | 2 +- scripts/generate-version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/tauri/tauri.conf.json b/crates/tauri/tauri.conf.json index 420a35a13..e3ad63cbe 100644 --- a/crates/tauri/tauri.conf.json +++ b/crates/tauri/tauri.conf.json @@ -59,7 +59,7 @@ "endpoints": [ "https://update.spyglass.fyi/VERSION.json", "https://spyglass-update-check.spyglass.workers.dev", - "https://raw.githubusercontent.com/a5huynh/spyglass/main/VERSION.json" + "https://raw.githubusercontent.com/spyglass-search/spyglass/main/VERSION.json" ], "dialog": false, "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDZFREEwQzA5QjA1NjM5NDUKUldSRk9WYXdDUXphYm5keUUvY2V3VUh2cjl3YlB0MlBuV1NJd3VjUnk5ektjVTExY3JKVGdRNHUK" diff --git a/scripts/generate-version.py b/scripts/generate-version.py index 076e22ed0..3128d8a65 100644 --- a/scripts/generate-version.py +++ b/scripts/generate-version.py @@ -2,7 +2,7 @@ import requests import json -RELEASE_URL = 'https://api.github.com/repos/a5huynh/spyglass/releases' +RELEASE_URL = 'https://api.github.com/repos/spyglass-search/spyglass/releases' DARWIN_x86 = 'darwin-x86_64' DARWIN_ARM = 'darwin-aarch64' From afc2e36efc6946f5a392b1e76378a52d423fc507 Mon Sep 17 00:00:00 2001 From: Andrew Huynh Date: Thu, 9 Mar 2023 21:40:40 -0800 Subject: [PATCH 03/19] Changing to AGPL license (#368) --- LICENSE | 682 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 661 insertions(+), 21 deletions(-) diff --git a/LICENSE b/LICENSE index c9a93bfcb..be3f7b28e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,661 @@ -MIT License - -Copyright (c) 2022 Andrew Huynh - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. From 5f09f1d04c9e28c2bd6d675aebb54d1c5899286a Mon Sep 17 00:00:00 2001 From: Andrew Huynh Date: Fri, 10 Mar 2023 11:02:04 -0800 Subject: [PATCH 04/19] bugfix: lens updater check (#371) * chore: bumping VERSION.json to v2023.3.1 * reduce check time to daily * track different events for install vs update * remove old lens file before writing new one to disk --- VERSION.json | 22 +++++++++++----------- crates/shared/src/metrics.rs | 6 ++++++ crates/spyglass/src/search/lens.rs | 17 +++++++++++++++++ crates/tauri/src/constants.rs | 4 ++-- crates/tauri/src/plugins/lens_updater.rs | 24 +++++++++++++++++------- 5 files changed, 53 insertions(+), 20 deletions(-) diff --git a/VERSION.json b/VERSION.json index 584cdedf2..a0df8451f 100644 --- a/VERSION.json +++ b/VERSION.json @@ -1,23 +1,23 @@ { - "version": "23.2.4", - "notes": "See full release notes here: https://github.com/a5huynh/spyglass/releases/tag/v2023.2.4", - "pub_date": "2023-02-21T21:39:39Z", + "version": "23.3.1", + "notes": "See full release notes here: https://github.com/spyglass-search/spyglass/releases/tag/v2023.3.1", + "pub_date": "2023-03-10T04:10:25Z", "platforms": { "darwin-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYm5GUFk0djVnV0ZuTzZUWjE3QVpEb3ZoRFJPODRNVlR4UklFZ1oxSDFpeGxKSzZJMmFNZnUybzU2TW1pZm1WV0E4SDROUDNoN2lUTG5PVnZLZnZBRkE0PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjc2OTM3NzA2CWZpbGU6U3B5Z2xhc3MuYXBwLnRhci5negpncjR3QTlyRzMyQUE5TklvZHp4a0M4Ti83ZWQzK1dkRDJJU0tDMmNtUnVTRTIxWlpabVVNOXNIK2VGZERjdisycVc0N0xNemFjSDFDRjFqUXJCRXVCQT09Cg==", - "url": "https://github.com/a5huynh/spyglass/releases/download/v2023.2.4/Spyglass_universal.app.tar.gz" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYnIxcFZRYWRnNFhYaytnUzZoQnpoa1RoSXB6YXU0bnZ3VkVJZ0YzOXI5aG1kaCs1bWhQbEs2b2tUTVdGUTAyL1JKeDZaVTh3OG16NWowUGkza01oRkFFPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjc4NDEzMTU3CWZpbGU6U3B5Z2xhc3MuYXBwLnRhci5negpCbGhCVHI0Uk51b1JwVXpaY1dzbE83VE8vRnNJNlFjZGJabmc2bnhTa2l3K0UrR1F1SGtYY292em1hL0dBL29ZZU9DSCtreDltOUt6dVpPZkpCK1ZEdz09Cg==", + "url": "https://github.com/spyglass-search/spyglass/releases/download/v2023.3.1/Spyglass_universal.app.tar.gz" }, "darwin-aarch64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYm5GUFk0djVnV0ZuTzZUWjE3QVpEb3ZoRFJPODRNVlR4UklFZ1oxSDFpeGxKSzZJMmFNZnUybzU2TW1pZm1WV0E4SDROUDNoN2lUTG5PVnZLZnZBRkE0PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjc2OTM3NzA2CWZpbGU6U3B5Z2xhc3MuYXBwLnRhci5negpncjR3QTlyRzMyQUE5TklvZHp4a0M4Ti83ZWQzK1dkRDJJU0tDMmNtUnVTRTIxWlpabVVNOXNIK2VGZERjdisycVc0N0xNemFjSDFDRjFqUXJCRXVCQT09Cg==", - "url": "https://github.com/a5huynh/spyglass/releases/download/v2023.2.4/Spyglass_universal.app.tar.gz" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYnIxcFZRYWRnNFhYaytnUzZoQnpoa1RoSXB6YXU0bnZ3VkVJZ0YzOXI5aG1kaCs1bWhQbEs2b2tUTVdGUTAyL1JKeDZaVTh3OG16NWowUGkza01oRkFFPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjc4NDEzMTU3CWZpbGU6U3B5Z2xhc3MuYXBwLnRhci5negpCbGhCVHI0Uk51b1JwVXpaY1dzbE83VE8vRnNJNlFjZGJabmc2bnhTa2l3K0UrR1F1SGtYY292em1hL0dBL29ZZU9DSCtreDltOUt6dVpPZkpCK1ZEdz09Cg==", + "url": "https://github.com/spyglass-search/spyglass/releases/download/v2023.3.1/Spyglass_universal.app.tar.gz" }, "linux-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYm9HZlJheVd3OWRzMklnZ2dPb1I2emxwSHdzdndaRy9nTFJtSXZQNWpHRU9QNzhKVlMrckl2ZUZHUDQxNFFHZ1ZMdHBSL0o5d2tJVU9CVVlMUnBEZXdzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjc2OTM1MTQ0CWZpbGU6c3B5Z2xhc3NfMjMuMi40X2FtZDY0LkFwcEltYWdlLnRhci5negpNWURsN1ZBcFpwaVpubStYblpXdUFxdVRESmIyUkFJeTRkVVgrcjRGR1JDcTArVHh1SEJqNE10akNZR3RONk1VZTBQcnRDYVYvZ0hZVGNOeUUrLzRDdz09Cg==", - "url": "https://github.com/a5huynh/spyglass/releases/download/v2023.2.4/spyglass_23.2.4_amd64.AppImage.tar.gz" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYmpOdFdzc29sNGxjYUxUbDRRTlk5YUM3aGI4RlYzZU8xR0JYc2VKZi8zaDgySm1maGpWUlp4V2UrMzB0ekRsSFltSVZKM0J1NUNoZGdlRTBNR0tma1FzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjc4NDA5ODM1CWZpbGU6c3B5Z2xhc3NfMjMuMy4xX2FtZDY0LkFwcEltYWdlLnRhci5negpId1ZIQzVWY1IzcHBhK1daYml0bmUyQm9nYkQ5bk15dy9jZTA2RnF6S3BrRVVpNUM5dTc5NmwraVE3UmZScklnSXdiNnJsYTJmdG1hVjhXNDhNSTRCZz09Cg==", + "url": "https://github.com/spyglass-search/spyglass/releases/download/v2023.3.1/spyglass_23.3.1_amd64.AppImage.tar.gz" }, "windows-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYm9NdjNpdk16d3huK08zUFpWa2NyZUJOVmdrZis1WjR4czFSZWZOOW9ZeXhKNlNzeW1xOW9USk1MRERVdW84VldBRWFtaVpBNWtoMHhGbjJMWWw5VkFzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjc2OTM2MTUwCWZpbGU6U3B5Z2xhc3NfMjMuMi40X3g2NF9lbi1VUy5tc2kuemlwClVIOWRXakFyd095Wk1hQ2ZSdno4Z3RKVzZnNTFKaDQ2b0FXc1RNUmhuYXRFSzRiaFE3d0wzTFRaQTMycm0wMGhOY3FVNXVKQVZqT1JNOFRob213dENBPT0K", - "url": "https://github.com/a5huynh/spyglass/releases/download/v2023.2.4/Spyglass_23.2.4_x64_en-US.msi.zip" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSRk9WYXdDUXphYmt0dXFxZklsWXdGak4zYWRLSnp5ZzkzbHB4MVV0Tm5mRzErdk9LdjBINnJzOHRVZUF3SHpaOGtNenVkWUFtb2FndTZxRjNjbGRka3F5OS9YbmV4eVFBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjc4NDEwNTQyCWZpbGU6U3B5Z2xhc3NfMjMuMy4xX3g2NF9lbi1VUy5tc2kuemlwCkhsMTYzSG9OT0dHTmtBaFNlVzR0QUxMRzY4NGRRalU1enI1NmxOZVE1K2lRMzl4bGxiK2dWYTkwQWY2dlNWZWx3OGl1SDUvejZla2h3QTN0STVPRERRPT0K", + "url": "https://github.com/spyglass-search/spyglass/releases/download/v2023.3.1/Spyglass_23.3.1_x64_en-US.msi.zip" } } } \ No newline at end of file diff --git a/crates/shared/src/metrics.rs b/crates/shared/src/metrics.rs index e4045b8ed..2e9745cfe 100644 --- a/crates/shared/src/metrics.rs +++ b/crates/shared/src/metrics.rs @@ -23,6 +23,8 @@ pub enum Event { AuthorizeConnection { api_id: String }, #[strum(serialize = "install_lens")] InstallLens { lens: String }, + #[strum(serialize = "update_lens")] + UpdateLens { lens: String }, #[strum(serialize = "search")] Search { filters: Vec }, #[strum(serialize = "search_result")] @@ -96,6 +98,10 @@ impl Metrics { data.properties .insert("lens".into(), lens.to_owned().into()); } + Event::UpdateLens { lens } => { + data.properties + .insert("lens".into(), lens.to_owned().into()); + } Event::Search { filters } => { data.properties .insert("filter".into(), filters.to_owned().into()); diff --git a/crates/spyglass/src/search/lens.rs b/crates/spyglass/src/search/lens.rs index 4c9fb13bf..222389692 100644 --- a/crates/spyglass/src/search/lens.rs +++ b/crates/spyglass/src/search/lens.rs @@ -219,6 +219,23 @@ async fn install_lens_to_path( log::debug!("add {} to db: {:?}", config.name, model); log::info!("Installed new lens {}, new? {}", config.name, is_new); + // Find and remove the old lens + if let Ok(list) = std::fs::read_dir(lens_folder.clone()) { + for file in list.flatten() { + let path = file.path(); + if path.is_file() { + if let Ok(lens) = ron::de::from_str::( + &std::fs::read_to_string(path.clone()).unwrap_or_default(), + ) { + if lens.name == installable_lens.name { + let _ = std::fs::remove_file(path); + break; + } + } + } + } + } + // Write to disk if we've successfully add to the database fs::write(lens_folder.join(file_name), file_contents)?; Ok(()) diff --git a/crates/tauri/src/constants.rs b/crates/tauri/src/constants.rs index ae7e97362..a79dc5409 100644 --- a/crates/tauri/src/constants.rs +++ b/crates/tauri/src/constants.rs @@ -7,8 +7,8 @@ pub const MIN_WINDOW_HEIGHT: f64 = 480.0; // Check for a new version every 6 hours. 60 seconds * 60 minutes * 6 hours pub const VERSION_CHECK_INTERVAL_S: u64 = 60 * 60 * 6; -// Check on start & every hour for new lenses -pub const LENS_UPDATE_CHECK_INTERVAL_S: u64 = 60 * 60; +// Check on start & every day for new lenses +pub const LENS_UPDATE_CHECK_INTERVAL_S: u64 = 60 * 60 * 24; pub const SEARCH_WIN_NAME: &str = "main"; pub const SETTINGS_WIN_NAME: &str = "settings_window"; diff --git a/crates/tauri/src/plugins/lens_updater.rs b/crates/tauri/src/plugins/lens_updater.rs index 573d115a4..4aef04fb5 100644 --- a/crates/tauri/src/plugins/lens_updater.rs +++ b/crates/tauri/src/plugins/lens_updater.rs @@ -92,7 +92,7 @@ async fn check_for_lens_updates(app_handle: &AppHandle) -> anyhow::Result<()> { latest.download_url ); - if let Err(e) = handle_install_lens(app_handle, &latest.name).await { + if let Err(e) = handle_install_lens(app_handle, &latest.name, true).await { log::error!("Unable to install lens: {}", e); } else { lenses_updated += 1; @@ -144,7 +144,11 @@ async fn get_installed_lenses(app_handle: &AppHandle) -> anyhow::Result anyhow::Result<()> { +pub async fn handle_install_lens( + app_handle: &AppHandle, + name: &str, + is_update: bool, +) -> anyhow::Result<()> { log::debug!("Lens install requested {}", name); let mutex = app_handle .try_state::() @@ -154,11 +158,17 @@ pub async fn handle_install_lens(app_handle: &AppHandle, name: &str) -> anyhow:: match rpc.client.install_lens(name.to_string()).await { Ok(_) => { if let Some(metrics) = app_handle.try_state::() { - metrics - .track(Event::InstallLens { + let event = if is_update { + Event::InstallLens { lens: name.to_owned(), - }) - .await; + } + } else { + Event::UpdateLens { + lens: name.to_owned(), + } + }; + + metrics.track(event).await; } Ok(()) } @@ -173,7 +183,7 @@ pub async fn handle_install_lens(app_handle: &AppHandle, name: &str) -> anyhow:: #[tauri::command] pub async fn install_lens(win: tauri::Window, name: &str) -> Result<(), String> { let app_handle = win.app_handle(); - let _ = handle_install_lens(&app_handle, name).await; + let _ = handle_install_lens(&app_handle, name, false).await; let _ = app_handle.emit_all(ClientEvent::RefreshDiscover.as_ref(), Value::Null); Ok(()) } From 7f1afbed10b8d58c12ab950aa0235d720e1aebfe Mon Sep 17 00:00:00 2001 From: Andrew Huynh Date: Fri, 10 Mar 2023 11:12:27 -0800 Subject: [PATCH 05/19] chore: bumping VERSION.json to v2023.3.1 (#370) From e42a13b89bc6fd0b0236f836f77bcef1e2a5987e Mon Sep 17 00:00:00 2001 From: Andrew Huynh Date: Fri, 10 Mar 2023 11:21:06 -0800 Subject: [PATCH 06/19] chore: update README links --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 58553d707..2faa058e7 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@

Download now: - + macOS (Intel/ARM) | - + Windows | - + Linux (AppImage)
From a49df68d482a1ce46e47c8f54a281653b731f7ec Mon Sep 17 00:00:00 2001 From: Andrew Huynh Date: Fri, 10 Mar 2023 11:22:57 -0800 Subject: [PATCH 07/19] Update README, reddit syncing is now supported! --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2faa058e7..319211f18 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ - [x] Google Calendar events. - [x] Google Drive docs. - [x] GitHub repos, starred repos, & issues. +- [x] Reddit saved/upvoted posts. - [ ] Gmail - [ ] YouTube playlists & favorited. -- [ ] Reddit saved/upvoted posts. ## Introduction From e0c6be5d4a44760c6cb25431f21515e4295a98f9 Mon Sep 17 00:00:00 2001 From: travolin Date: Fri, 10 Mar 2023 16:12:57 -0800 Subject: [PATCH 08/19] Allow uuid to be set as env var for test systems (#372) Co-authored-by: Tago --- crates/shared/src/config.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/shared/src/config.rs b/crates/shared/src/config.rs index 2abe0bcc1..0db3f09af 100644 --- a/crates/shared/src/config.rs +++ b/crates/shared/src/config.rs @@ -29,6 +29,7 @@ pub const LEGACY_PLUGIN_FOLDERS: &[&str] = // The default extensions pub const DEFAULT_EXTENSIONS: &[&str] = &["docx", "html", "md", "txt", "ods", "xls", "xlsx"]; +const USER_UUID_ENV_VAR: &str = "SPYGLASS_STATIC_USER_UUID"; #[derive(Clone, Debug)] pub struct Config { @@ -669,8 +670,13 @@ impl Config { if uid_file.exists() { std::fs::read_to_string(uid_file).unwrap_or_default() } else { - // Generate a random ID and associate it with this machine for error/metrics. - let new_uid = Uuid::new_v4().as_hyphenated().to_string(); + let new_uid = match std::env::var(USER_UUID_ENV_VAR) { + Ok(uid) => uid, + Err(_) => { + // Generate a random ID and associate it with this machine for error/metrics. + Uuid::new_v4().as_hyphenated().to_string() + } + }; let _ = std::fs::write(uid_file, new_uid.clone()); new_uid } From e76eb457241cc5e26aa43b817335ef8248ccdbae Mon Sep 17 00:00:00 2001 From: Andrew Huynh Date: Fri, 10 Mar 2023 17:04:35 -0800 Subject: [PATCH 09/19] tweak: update lens results styling to match search results (#373) --- crates/client/public/main.css | 2 +- crates/client/src/components/result.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/client/public/main.css b/crates/client/public/main.css index 2a264abfd..1e9a597fe 100644 --- a/crates/client/public/main.css +++ b/crates/client/public/main.css @@ -1 +1 @@ -/*! tailwindcss v3.1.8 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.form-input,.form-multiselect,.form-select,.form-textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}.form-input:focus,.form-multiselect:focus,.form-select:focus,.form-textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}.form-input::-moz-placeholder,.form-textarea::-moz-placeholder{color:#6b7280;opacity:1}.form-input::placeholder,.form-textarea::placeholder{color:#6b7280;opacity:1}.form-input::-webkit-datetime-edit-fields-wrapper{padding:0}.form-input::-webkit-date-and-time-value{min-height:1.5em}.form-input::-webkit-datetime-edit,.form-input::-webkit-datetime-edit-day-field,.form-input::-webkit-datetime-edit-hour-field,.form-input::-webkit-datetime-edit-meridiem-field,.form-input::-webkit-datetime-edit-millisecond-field,.form-input::-webkit-datetime-edit-minute-field,.form-input::-webkit-datetime-edit-month-field,.form-input::-webkit-datetime-edit-second-field,.form-input::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.top-0{top:0}.bottom-0{bottom:0}.right-0{right:0}.bottom-8{bottom:2rem}.left-0{left:0}.left-1{left:.25rem}.top-1{top:.25rem}.z-50{z-index:50}.z-20{z-index:20}.z-40{z-index:40}.float-right{float:right}.m-auto{margin:auto}.mx-auto{margin-left:auto;margin-right:auto}.mx-3{margin-left:.75rem;margin-right:.75rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-auto{margin-top:auto;margin-bottom:auto}.my-4{margin-top:1rem;margin-bottom:1rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.ml-auto{margin-left:auto}.mt-2{margin-top:.5rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.-ml-16{margin-left:-4rem}.-mt-2{margin-top:-.5rem}.mb-6{margin-bottom:1.5rem}.mb-2{margin-bottom:.5rem}.mr-2{margin-right:.5rem}.mb-4{margin-bottom:1rem}.mt-4{margin-top:1rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-1{margin-left:.25rem}.mb-8{margin-bottom:2rem}.mr-1{margin-right:.25rem}.mt-auto{margin-top:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-4{height:1rem}.h-5{height:1.25rem}.h-3\.5{height:.875rem}.h-3{height:.75rem}.h-1{height:.25rem}.h-8{height:2rem}.h-12{height:3rem}.h-32{height:8rem}.h-screen{height:100vh}.h-10{height:2.5rem}.h-6{height:1.5rem}.h-16{height:4rem}.h-48{height:12rem}.h-20{height:5rem}.h-2{height:.5rem}.h-full{height:100%}.h-64{height:16rem}.h-40{height:10rem}.h-9{height:2.25rem}.h-\[128px\]{height:128px}.max-h-10{max-height:2.5rem}.max-h-screen{max-height:100vh}.max-h-\[640px\]{max-height:640px}.w-4{width:1rem}.w-5{width:1.25rem}.w-3\.5{width:.875rem}.w-3{width:.75rem}.w-full{width:100%}.w-32{width:8rem}.w-8{width:2rem}.w-12{width:3rem}.w-\[30rem\]{width:30rem}.w-1\/2{width:50%}.w-48{width:12rem}.w-auto{width:auto}.w-6{width:1.5rem}.w-16{width:4rem}.w-40{width:10rem}.w-20{width:5rem}.w-2{width:.5rem}.w-14{width:3.5rem}.w-\[196px\]{width:196px}.w-\[300px\]{width:300px}.w-9{width:2.25rem}.min-w-5{min-width:1.25rem}.flex-none{flex:none}.flex-auto{flex:1 1 auto}.flex-1{flex:1 1 0%}.grow{flex-grow:1}@-webkit-keyframes spin{to{transform:rotate(1turn)}}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}.animate-pulse{-webkit-animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite;animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.scroll-mt-2{scroll-margin-top:.5rem}.list-none{list-style-type:none}.list-disc{list-style-type:disc}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.flex-nowrap{flex-wrap:nowrap}.place-content-center{place-content:center}.place-content-start{place-content:start}.place-content-end{place-content:end}.place-items-center{place-items:center}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-4{gap:1rem}.gap-2{gap:.5rem}.gap-8{gap:2rem}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded-md{border-radius:.375rem}.rounded-3xl{border-radius:1.5rem}.rounded-lg{border-radius:.5rem}.rounded{border-radius:.25rem}.rounded-xl{border-radius:.75rem}.rounded-full{border-radius:9999px}.rounded-tl-lg{border-top-left-radius:.5rem}.border{border-width:1px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-l-2{border-left-width:2px}.border-l{border-left-width:1px}.border-neutral-600{--tw-border-opacity:1;border-color:rgb(82 82 82/var(--tw-border-opacity))}.border-red-700{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity))}.border-stone-900{--tw-border-opacity:1;border-color:rgb(28 25 23/var(--tw-border-opacity))}.border-green-500{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-neutral-500{--tw-border-opacity:1;border-color:rgb(115 115 115/var(--tw-border-opacity))}.border-neutral-900{--tw-border-opacity:1;border-color:rgb(23 23 23/var(--tw-border-opacity))}.border-neutral-700{--tw-border-opacity:1;border-color:rgb(64 64 64/var(--tw-border-opacity))}.border-stone-800{--tw-border-opacity:1;border-color:rgb(41 37 36/var(--tw-border-opacity))}.bg-transparent{background-color:initial}.bg-green-700{--tw-bg-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity))}.bg-stone-800{--tw-bg-opacity:1;background-color:rgb(41 37 36/var(--tw-bg-opacity))}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity))}.bg-neutral-700{--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.bg-cyan-500{--tw-bg-opacity:1;background-color:rgb(6 182 212/var(--tw-bg-opacity))}.bg-cyan-700{--tw-bg-opacity:1;background-color:rgb(14 116 144/var(--tw-bg-opacity))}.bg-neutral-800{--tw-bg-opacity:1;background-color:rgb(38 38 38/var(--tw-bg-opacity))}.bg-neutral-400{--tw-bg-opacity:1;background-color:rgb(163 163 163/var(--tw-bg-opacity))}.bg-cyan-900{--tw-bg-opacity:1;background-color:rgb(22 78 99/var(--tw-bg-opacity))}.bg-neutral-900{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity))}.bg-stone-900{--tw-bg-opacity:1;background-color:rgb(28 25 23/var(--tw-bg-opacity))}.bg-stone-700{--tw-bg-opacity:1;background-color:rgb(68 64 60/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.stroke-slate-400{stroke:#94a3b8}.p-4{padding:1rem}.p-0\.5{padding:.125rem}.p-0{padding:0}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-16{padding:4rem}.p-1\.5{padding:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-0{padding-top:0;padding-bottom:0}.px-8{padding-left:2rem;padding-right:2rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-12{padding-top:3rem;padding-bottom:3rem}.pl-1{padding-left:.25rem}.pb-1{padding-bottom:.25rem}.pl-2{padding-left:.5rem}.pr-2{padding-right:.5rem}.pl-6{padding-left:1.5rem}.pb-8{padding-bottom:2rem}.pb-2{padding-bottom:.5rem}.pt-4{padding-top:1rem}.pl-3{padding-left:.75rem}.pt-8{padding-top:2rem}.pb-16{padding-bottom:4rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-xs{font-size:.625rem}.text-sm{font-size:.75rem}.text-base{font-size:.875rem}.text-lg{font-size:1rem}.text-xl{font-size:1.125rem}.text-4xl{font-size:1.875rem}.text-\[8px\]{font-size:8px}.text-2xl{font-size:1.25rem}.text-5xl{font-size:2rem}.font-semibold{font-weight:600}.font-medium{font-weight:500}.font-bold{font-weight:700}.uppercase{text-transform:uppercase}.leading-5{line-height:1.25rem}.leading-relaxed{line-height:1.625}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-neutral-600{--tw-text-opacity:1;color:rgb(82 82 82/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-stone-400{--tw-text-opacity:1;color:rgb(168 162 158/var(--tw-text-opacity))}.text-neutral-400{--tw-text-opacity:1;color:rgb(163 163 163/var(--tw-text-opacity))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-cyan-500{--tw-text-opacity:1;color:rgb(6 182 212/var(--tw-text-opacity))}.text-cyan-600{--tw-text-opacity:1;color:rgb(8 145 178/var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgb(234 179 8/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.text-neutral-500{--tw-text-opacity:1;color:rgb(115 115 115/var(--tw-text-opacity))}.text-stone-500{--tw-text-opacity:1;color:rgb(120 113 108/var(--tw-text-opacity))}.caret-white{caret-color:#fff}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-cyan-500\/50{--tw-shadow-color:rgba(6,182,212,.5);--tw-shadow:var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}input:checked~.dot{transform:translateX(100%);background-color:#48bb78}*{scrollbar-width:thin;scrollbar-color:#404040 #171717}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:#171717}::-webkit-scrollbar-thumb{background:#404040;border-radius:100vh;border:2px solid #171717}::-webkit-scrollbar-thumb:hover{background:#164e63}mark{color:#fff;background-color:rgb(14 116 144/var(--tw-bg-opacity));padding-left:.125rem;padding-right:.125rem}.hover\:border-green-500:hover{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.hover\:bg-neutral-600:hover{--tw-bg-opacity:1;background-color:rgb(82 82 82/var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgb(185 28 28/var(--tw-bg-opacity))}.hover\:bg-green-900:hover{--tw-bg-opacity:1;background-color:rgb(20 83 45/var(--tw-bg-opacity))}.hover\:bg-cyan-600:hover{--tw-bg-opacity:1;background-color:rgb(8 145 178/var(--tw-bg-opacity))}.hover\:bg-stone-700:hover{--tw-bg-opacity:1;background-color:rgb(68 64 60/var(--tw-bg-opacity))}.hover\:bg-neutral-700:hover{--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.hover\:bg-stone-800:hover{--tw-bg-opacity:1;background-color:rgb(41 37 36/var(--tw-bg-opacity))}.hover\:text-red-600:hover{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:bg-neutral-700:active{--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.active\:bg-cyan-900:active{--tw-bg-opacity:1;background-color:rgb(22 78 99/var(--tw-bg-opacity))}.active\:outline-none:active{outline:2px solid transparent;outline-offset:2px}.group:hover .group-hover\:block{display:block}.group:hover .group-hover\:fill-red-400{fill:#f87171}.group:hover .group-hover\:stroke-white{stroke:#fff}.group:hover .group-hover\:text-neutral-400{--tw-text-opacity:1;color:rgb(163 163 163/var(--tw-text-opacity))} \ No newline at end of file +/*! tailwindcss v3.1.8 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.form-input,.form-multiselect,.form-select,.form-textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}.form-input:focus,.form-multiselect:focus,.form-select:focus,.form-textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}.form-input::-moz-placeholder,.form-textarea::-moz-placeholder{color:#6b7280;opacity:1}.form-input::placeholder,.form-textarea::placeholder{color:#6b7280;opacity:1}.form-input::-webkit-datetime-edit-fields-wrapper{padding:0}.form-input::-webkit-date-and-time-value{min-height:1.5em}.form-input::-webkit-datetime-edit,.form-input::-webkit-datetime-edit-day-field,.form-input::-webkit-datetime-edit-hour-field,.form-input::-webkit-datetime-edit-meridiem-field,.form-input::-webkit-datetime-edit-millisecond-field,.form-input::-webkit-datetime-edit-minute-field,.form-input::-webkit-datetime-edit-month-field,.form-input::-webkit-datetime-edit-second-field,.form-input::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.top-0{top:0}.bottom-0{bottom:0}.right-0{right:0}.bottom-8{bottom:2rem}.left-0{left:0}.left-1{left:.25rem}.top-1{top:.25rem}.z-50{z-index:50}.z-20{z-index:20}.z-40{z-index:40}.float-right{float:right}.m-auto{margin:auto}.mx-auto{margin-left:auto;margin-right:auto}.mx-3{margin-left:.75rem;margin-right:.75rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-auto{margin-top:auto;margin-bottom:auto}.my-4{margin-top:1rem;margin-bottom:1rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.ml-auto{margin-left:auto}.mt-2{margin-top:.5rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.-ml-16{margin-left:-4rem}.-mt-2{margin-top:-.5rem}.mb-6{margin-bottom:1.5rem}.mb-2{margin-bottom:.5rem}.mr-2{margin-right:.5rem}.mb-4{margin-bottom:1rem}.mt-4{margin-top:1rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-1{margin-left:.25rem}.mb-8{margin-bottom:2rem}.mr-1{margin-right:.25rem}.mt-auto{margin-top:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-4{height:1rem}.h-5{height:1.25rem}.h-3\.5{height:.875rem}.h-3{height:.75rem}.h-1{height:.25rem}.h-8{height:2rem}.h-12{height:3rem}.h-32{height:8rem}.h-screen{height:100vh}.h-10{height:2.5rem}.h-6{height:1.5rem}.h-16{height:4rem}.h-48{height:12rem}.h-20{height:5rem}.h-2{height:.5rem}.h-full{height:100%}.h-64{height:16rem}.h-40{height:10rem}.h-9{height:2.25rem}.h-\[128px\]{height:128px}.max-h-10{max-height:2.5rem}.max-h-screen{max-height:100vh}.max-h-\[640px\]{max-height:640px}.w-4{width:1rem}.w-5{width:1.25rem}.w-3\.5{width:.875rem}.w-3{width:.75rem}.w-full{width:100%}.w-32{width:8rem}.w-8{width:2rem}.w-12{width:3rem}.w-\[30rem\]{width:30rem}.w-1\/2{width:50%}.w-48{width:12rem}.w-auto{width:auto}.w-6{width:1.5rem}.w-16{width:4rem}.w-40{width:10rem}.w-20{width:5rem}.w-2{width:.5rem}.w-14{width:3.5rem}.w-\[196px\]{width:196px}.w-\[300px\]{width:300px}.w-9{width:2.25rem}.min-w-5{min-width:1.25rem}.flex-none{flex:none}.flex-auto{flex:1 1 auto}.flex-1{flex:1 1 0%}.grow{flex-grow:1}@-webkit-keyframes spin{to{transform:rotate(1turn)}}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}.animate-pulse{-webkit-animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite;animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.scroll-mt-2{scroll-margin-top:.5rem}.list-none{list-style-type:none}.list-disc{list-style-type:disc}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.flex-nowrap{flex-wrap:nowrap}.place-content-center{place-content:center}.place-content-start{place-content:start}.place-content-end{place-content:end}.place-items-center{place-items:center}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-4{gap:1rem}.gap-2{gap:.5rem}.gap-8{gap:2rem}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded-md{border-radius:.375rem}.rounded-3xl{border-radius:1.5rem}.rounded-lg{border-radius:.5rem}.rounded{border-radius:.25rem}.rounded-xl{border-radius:.75rem}.rounded-full{border-radius:9999px}.rounded-tl-lg{border-top-left-radius:.5rem}.border{border-width:1px}.border-b-2{border-bottom-width:2px}.border-t-2{border-top-width:2px}.border-l-2{border-left-width:2px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-neutral-600{--tw-border-opacity:1;border-color:rgb(82 82 82/var(--tw-border-opacity))}.border-red-700{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity))}.border-stone-900{--tw-border-opacity:1;border-color:rgb(28 25 23/var(--tw-border-opacity))}.border-green-500{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-neutral-500{--tw-border-opacity:1;border-color:rgb(115 115 115/var(--tw-border-opacity))}.border-neutral-900{--tw-border-opacity:1;border-color:rgb(23 23 23/var(--tw-border-opacity))}.border-neutral-800{--tw-border-opacity:1;border-color:rgb(38 38 38/var(--tw-border-opacity))}.border-neutral-700{--tw-border-opacity:1;border-color:rgb(64 64 64/var(--tw-border-opacity))}.border-stone-800{--tw-border-opacity:1;border-color:rgb(41 37 36/var(--tw-border-opacity))}.bg-transparent{background-color:initial}.bg-green-700{--tw-bg-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity))}.bg-stone-800{--tw-bg-opacity:1;background-color:rgb(41 37 36/var(--tw-bg-opacity))}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity))}.bg-neutral-700{--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.bg-cyan-500{--tw-bg-opacity:1;background-color:rgb(6 182 212/var(--tw-bg-opacity))}.bg-cyan-700{--tw-bg-opacity:1;background-color:rgb(14 116 144/var(--tw-bg-opacity))}.bg-neutral-800{--tw-bg-opacity:1;background-color:rgb(38 38 38/var(--tw-bg-opacity))}.bg-neutral-400{--tw-bg-opacity:1;background-color:rgb(163 163 163/var(--tw-bg-opacity))}.bg-cyan-900{--tw-bg-opacity:1;background-color:rgb(22 78 99/var(--tw-bg-opacity))}.bg-neutral-900{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity))}.bg-stone-900{--tw-bg-opacity:1;background-color:rgb(28 25 23/var(--tw-bg-opacity))}.bg-stone-700{--tw-bg-opacity:1;background-color:rgb(68 64 60/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.stroke-slate-400{stroke:#94a3b8}.p-4{padding:1rem}.p-0\.5{padding:.125rem}.p-0{padding:0}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-16{padding:4rem}.p-1\.5{padding:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-0{padding-top:0;padding-bottom:0}.px-8{padding-left:2rem;padding-right:2rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-12{padding-top:3rem;padding-bottom:3rem}.pl-1{padding-left:.25rem}.pb-1{padding-bottom:.25rem}.pl-2{padding-left:.5rem}.pr-2{padding-right:.5rem}.pl-6{padding-left:1.5rem}.pb-8{padding-bottom:2rem}.pb-2{padding-bottom:.5rem}.pt-4{padding-top:1rem}.pl-3{padding-left:.75rem}.pt-8{padding-top:2rem}.pb-16{padding-bottom:4rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-xs{font-size:.625rem}.text-sm{font-size:.75rem}.text-base{font-size:.875rem}.text-lg{font-size:1rem}.text-xl{font-size:1.125rem}.text-4xl{font-size:1.875rem}.text-\[8px\]{font-size:8px}.text-2xl{font-size:1.25rem}.text-5xl{font-size:2rem}.font-semibold{font-weight:600}.font-medium{font-weight:500}.font-bold{font-weight:700}.uppercase{text-transform:uppercase}.leading-5{line-height:1.25rem}.leading-relaxed{line-height:1.625}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-neutral-600{--tw-text-opacity:1;color:rgb(82 82 82/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-stone-400{--tw-text-opacity:1;color:rgb(168 162 158/var(--tw-text-opacity))}.text-neutral-400{--tw-text-opacity:1;color:rgb(163 163 163/var(--tw-text-opacity))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-cyan-500{--tw-text-opacity:1;color:rgb(6 182 212/var(--tw-text-opacity))}.text-cyan-600{--tw-text-opacity:1;color:rgb(8 145 178/var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgb(234 179 8/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.text-neutral-500{--tw-text-opacity:1;color:rgb(115 115 115/var(--tw-text-opacity))}.text-stone-500{--tw-text-opacity:1;color:rgb(120 113 108/var(--tw-text-opacity))}.placeholder-neutral-400::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(163 163 163/var(--tw-placeholder-opacity))}.placeholder-neutral-400::placeholder{--tw-placeholder-opacity:1;color:rgb(163 163 163/var(--tw-placeholder-opacity))}.caret-white{caret-color:#fff}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-cyan-500\/50{--tw-shadow-color:rgba(6,182,212,.5);--tw-shadow:var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}input:checked~.dot{transform:translateX(100%);background-color:#48bb78}*{scrollbar-width:thin;scrollbar-color:#404040 #171717}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:#171717}::-webkit-scrollbar-thumb{background:#404040;border-radius:100vh;border:2px solid #171717}::-webkit-scrollbar-thumb:hover{background:#164e63}mark{color:#fff;background-color:rgb(14 116 144/var(--tw-bg-opacity));padding-left:.125rem;padding-right:.125rem}.hover\:border-green-500:hover{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.hover\:bg-neutral-600:hover{--tw-bg-opacity:1;background-color:rgb(82 82 82/var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgb(185 28 28/var(--tw-bg-opacity))}.hover\:bg-green-900:hover{--tw-bg-opacity:1;background-color:rgb(20 83 45/var(--tw-bg-opacity))}.hover\:bg-cyan-600:hover{--tw-bg-opacity:1;background-color:rgb(8 145 178/var(--tw-bg-opacity))}.hover\:bg-stone-700:hover{--tw-bg-opacity:1;background-color:rgb(68 64 60/var(--tw-bg-opacity))}.hover\:bg-neutral-700:hover{--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.hover\:bg-stone-800:hover{--tw-bg-opacity:1;background-color:rgb(41 37 36/var(--tw-bg-opacity))}.hover\:text-red-600:hover{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:bg-neutral-700:active{--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.active\:bg-cyan-900:active{--tw-bg-opacity:1;background-color:rgb(22 78 99/var(--tw-bg-opacity))}.active\:outline-none:active{outline:2px solid transparent;outline-offset:2px}.group:hover .group-hover\:block{display:block}.group:hover .group-hover\:fill-red-400{fill:#f87171}.group:hover .group-hover\:stroke-white{stroke:#fff}.group:hover .group-hover\:text-neutral-400{--tw-text-opacity:1;color:rgb(163 163 163/var(--tw-text-opacity))} \ No newline at end of file diff --git a/crates/client/src/components/result.rs b/crates/client/src/components/result.rs index d414e1f72..fe14ce09d 100644 --- a/crates/client/src/components/result.rs +++ b/crates/client/src/components/result.rs @@ -214,11 +214,11 @@ pub fn lens_result_component(props: &LensResultProps) -> Html { let component_styles = classes!( "flex", "flex-col", - "border-t", - "border-neutral-600", - "px-8", - "py-4", + "p-2", + "mt-2", "text-white", + "rounded", + "scroll-mt-2", if is_selected { "bg-cyan-900" } else { From f2b2ce18752f4563c88070ff935fafcaadfdf95c Mon Sep 17 00:00:00 2001 From: travolin Date: Fri, 10 Mar 2023 18:19:42 -0800 Subject: [PATCH 10/19] Update Stats (#374) - Add OS to better tack os specific issues - Add event when local file processing is enabled or disabled - Add start spyglass event - Add total number of documents in index --------- Co-authored-by: Tago --- crates/shared/src/lib.rs | 2 ++ crates/shared/src/metrics.rs | 18 ++++++++++++++++++ crates/spyglass/src/api/handler/search.rs | 4 +++- crates/spyglass/src/filesystem/mod.rs | 8 ++++++++ crates/spyglass/src/main.rs | 5 +++++ 5 files changed, 36 insertions(+), 1 deletion(-) diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index c2b2669ae..751cef0a0 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -17,6 +17,8 @@ pub const OS_STR: &str = "mac"; pub const OS_STR: &str = "windows"; #[cfg(target_os = "linux")] pub const OS_STR: &str = "linux"; +#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows",)))] +pub const OS_STR: &str = "Unknown"; pub const MAC_OS: &str = "mac"; pub const WINDOWS_OS: &str = "windows"; diff --git a/crates/shared/src/metrics.rs b/crates/shared/src/metrics.rs index 2e9745cfe..006011a73 100644 --- a/crates/shared/src/metrics.rs +++ b/crates/shared/src/metrics.rs @@ -5,6 +5,8 @@ use std::collections::HashMap; use strum_macros::{AsRefStr, Display}; +use crate::OS_STR; + #[allow(dead_code)] const ENDPOINT: &str = "https://api.mixpanel.com/track"; const PROJECT_TOKEN: &str = "51d84766a0838458d63998f1e4566d3b"; @@ -23,6 +25,12 @@ pub enum Event { AuthorizeConnection { api_id: String }, #[strum(serialize = "install_lens")] InstallLens { lens: String }, + #[strum(serialize = "spyglass_started")] + SpyglassStarted, + #[strum(serialize = "local_file_scanning_enabled")] + LocalFileScanningEnabled, + #[strum(serialize = "local_file_scanning_disabled")] + LocalFileScanningDisabled, #[strum(serialize = "update_lens")] UpdateLens { lens: String }, #[strum(serialize = "search")] @@ -30,6 +38,7 @@ pub enum Event { #[strum(serialize = "search_result")] SearchResult { num_results: usize, + num_docs: u64, domains: Vec, wall_time_ms: u64, }, @@ -49,6 +58,7 @@ impl EventProps { properties.insert("token".into(), PROJECT_TOKEN.into()); properties.insert("time".into(), chrono::Utc::now().timestamp().into()); properties.insert("distinct_id".into(), uid.into()); + properties.insert("$os".into(), OS_STR.into()); properties.insert( "$insert_id".into(), uuid::Uuid::new_v4().as_hyphenated().to_string().into(), @@ -108,11 +118,14 @@ impl Metrics { } Event::SearchResult { num_results, + num_docs, domains, wall_time_ms, } => { data.properties .insert("num_results".into(), num_results.to_owned().into()); + data.properties + .insert("num_docs".into(), num_docs.to_owned().into()); data.properties .insert("domains".into(), domains.to_owned().into()); data.properties @@ -122,6 +135,11 @@ impl Metrics { data.properties .insert("current_version".into(), current_version.as_str().into()); } + Event::LocalFileScanningEnabled + | Event::LocalFileScanningDisabled + | Event::SpyglassStarted => { + //noop + } } #[cfg(not(debug_assertions))] diff --git a/crates/spyglass/src/api/handler/search.rs b/crates/spyglass/src/api/handler/search.rs index d87836a23..d95f436a5 100644 --- a/crates/spyglass/src/api/handler/search.rs +++ b/crates/spyglass/src/api/handler/search.rs @@ -214,9 +214,10 @@ pub async fn search_docs( .duration_since(start) .map_or_else(|_| 0, |duration| duration.as_millis() as u64); + let num_docs = searcher.num_docs(); let meta = SearchMeta { query: search_req.query.clone(), - num_docs: searcher.num_docs() as u32, + num_docs: num_docs as u32, wall_time_ms: wall_time_ms as u32, }; @@ -225,6 +226,7 @@ pub async fn search_docs( .metrics .track(metrics::Event::SearchResult { num_results: results.len(), + num_docs, domains: domains.iter().cloned().collect(), wall_time_ms, }) diff --git a/crates/spyglass/src/filesystem/mod.rs b/crates/spyglass/src/filesystem/mod.rs index 717a3b3ce..439b53275 100644 --- a/crates/spyglass/src/filesystem/mod.rs +++ b/crates/spyglass/src/filesystem/mod.rs @@ -606,6 +606,10 @@ pub async fn configure_watcher(state: AppState) { if *enabled { log::info!("📂 Loading local file watcher"); + state + .metrics + .track(shared::metrics::Event::LocalFileScanningEnabled) + .await; let extension = utils::get_supported_file_extensions(&state); let paths = utils::get_search_directories(&state); @@ -651,6 +655,10 @@ pub async fn configure_watcher(state: AppState) { } } else { log::info!("❌ Local file watcher is disabled"); + state + .metrics + .track(shared::metrics::Event::LocalFileScanningDisabled) + .await; let mut watcher = state.file_watcher.lock().await; if let Some(watcher) = watcher.as_mut() { diff --git a/crates/spyglass/src/main.rs b/crates/spyglass/src/main.rs index ad6f41019..6f56e0a37 100644 --- a/crates/spyglass/src/main.rs +++ b/crates/spyglass/src/main.rs @@ -269,6 +269,11 @@ async fn start_backend(state: AppState, config: Config) { plugin_cmd_rx, )); + state + .metrics + .track(shared::metrics::Event::SpyglassStarted) + .await; + // Gracefully handle shutdowns match signal::ctrl_c().await { Ok(()) => { From ad857d547d708653e27551ec1fc63b0ad46e4893 Mon Sep 17 00:00:00 2001 From: travolin Date: Fri, 10 Mar 2023 21:27:55 -0800 Subject: [PATCH 11/19] Add term count to stats (#375) Co-authored-by: Tago --- crates/shared/src/metrics.rs | 4 ++++ crates/spyglass/src/api/handler/search.rs | 14 +++++++++++--- crates/spyglass/src/search/mod.rs | 18 ++++++++++++++---- crates/spyglass/src/search/query.rs | 21 +++++++++++++++++++++ 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/crates/shared/src/metrics.rs b/crates/shared/src/metrics.rs index 006011a73..1e81b1170 100644 --- a/crates/shared/src/metrics.rs +++ b/crates/shared/src/metrics.rs @@ -39,6 +39,7 @@ pub enum Event { SearchResult { num_results: usize, num_docs: u64, + term_count: i32, domains: Vec, wall_time_ms: u64, }, @@ -119,6 +120,7 @@ impl Metrics { Event::SearchResult { num_results, num_docs, + term_count, domains, wall_time_ms, } => { @@ -126,6 +128,8 @@ impl Metrics { .insert("num_results".into(), num_results.to_owned().into()); data.properties .insert("num_docs".into(), num_docs.to_owned().into()); + data.properties + .insert("term_count".into(), term_count.to_owned().into()); data.properties .insert("domains".into(), domains.to_owned().into()); data.properties diff --git a/crates/spyglass/src/api/handler/search.rs b/crates/spyglass/src/api/handler/search.rs index d95f436a5..94bd2fcd8 100644 --- a/crates/spyglass/src/api/handler/search.rs +++ b/crates/spyglass/src/api/handler/search.rs @@ -5,7 +5,7 @@ use entities::sea_orm::{ self, prelude::*, sea_query::Expr, FromQueryResult, JoinType, QueryOrder, QuerySelect, }; use jsonrpsee::core::Error; -use libspyglass::search::{document_to_struct, Searcher}; +use libspyglass::search::{document_to_struct, QueryStats, Searcher}; use libspyglass::state::AppState; use libspyglass::task::{CleanupTask, ManagerCommand}; use shared::metrics; @@ -161,8 +161,15 @@ pub async fn search_docs( .map(|model| model.id as u64) .collect::>(); - let docs = - Searcher::search_with_lens(state.db.clone(), &tag_ids, index, &search_req.query).await; + let mut stats = QueryStats::new(); + let docs = Searcher::search_with_lens( + state.db.clone(), + &tag_ids, + index, + &search_req.query, + &mut stats, + ) + .await; let mut results: Vec = Vec::new(); let mut missing: Vec<(String, String)> = Vec::new(); @@ -227,6 +234,7 @@ pub async fn search_docs( .track(metrics::Event::SearchResult { num_results: results.len(), num_docs, + term_count: stats.term_count, domains: domains.iter().cloned().collect(), wall_time_ms, }) diff --git a/crates/spyglass/src/search/mod.rs b/crates/spyglass/src/search/mod.rs index 627ee665b..7391482d1 100644 --- a/crates/spyglass/src/search/mod.rs +++ b/crates/spyglass/src/search/mod.rs @@ -27,6 +27,8 @@ pub mod lens; mod query; mod utils; +pub use query::QueryStats; + type Score = f32; type SearchResult = (Score, DocAddress); @@ -307,6 +309,7 @@ impl Searcher { applied_lenses: &Vec, searcher: &Searcher, query_string: &str, + stats: &mut QueryStats, ) -> Vec { let start_timer = Instant::now(); @@ -337,6 +340,7 @@ impl Searcher { applied_lenses, tag_boosts.into_iter(), favorite_boost, + stats, ); let collector = TopDocs::with_limit(5); @@ -532,7 +536,7 @@ pub fn document_to_struct(doc: &Document) -> anyhow::Result { #[cfg(test)] mod test { - use crate::search::{DocumentUpdate, IndexPath, Searcher}; + use crate::search::{DocumentUpdate, IndexPath, QueryStats, Searcher}; use entities::models::create_connection; use shared::config::{Config, LensConfig}; @@ -641,8 +645,10 @@ mod test { let mut searcher = Searcher::with_index(&IndexPath::Memory).expect("Unable to open index"); _build_test_index(&mut searcher); + let mut stats = QueryStats::new(); let query = "salinas"; - let results = Searcher::search_with_lens(db, &vec![2_u64], &searcher, query).await; + let results = + Searcher::search_with_lens(db, &vec![2_u64], &searcher, query, &mut stats).await; assert_eq!(results.len(), 1); } @@ -660,9 +666,11 @@ mod test { let mut searcher = Searcher::with_index(&IndexPath::Memory).expect("Unable to open index"); + let mut stats = QueryStats::new(); _build_test_index(&mut searcher); let query = "salinas"; - let results = Searcher::search_with_lens(db, &vec![2_u64], &searcher, query).await; + let results = + Searcher::search_with_lens(db, &vec![2_u64], &searcher, query, &mut stats).await; assert_eq!(results.len(), 1); } @@ -681,8 +689,10 @@ mod test { let mut searcher = Searcher::with_index(&IndexPath::Memory).expect("Unable to open index"); _build_test_index(&mut searcher); + let mut stats = QueryStats::new(); let query = "salinasd"; - let results = Searcher::search_with_lens(db, &vec![2_u64], &searcher, query).await; + let results = + Searcher::search_with_lens(db, &vec![2_u64], &searcher, query, &mut stats).await; assert_eq!(results.len(), 0); } } diff --git a/crates/spyglass/src/search/query.rs b/crates/spyglass/src/search/query.rs index 0d354038a..4cdd3f6ca 100644 --- a/crates/spyglass/src/search/query.rs +++ b/crates/spyglass/src/search/query.rs @@ -7,6 +7,23 @@ use super::DocFields; type QueryVec = Vec<(Occur, Box)>; +#[derive(Clone, Debug)] +pub struct QueryStats { + pub term_count: i32, +} + +impl Default for QueryStats { + fn default() -> Self { + Self::new() + } +} + +impl QueryStats { + pub fn new() -> Self { + QueryStats { term_count: -1 } + } +} + fn _boosted_term(term: Term, boost: Score) -> Box { Box::new(BoostQuery::new( Box::new(TermQuery::new( @@ -22,6 +39,7 @@ fn _boosted_phrase(terms: Vec, boost: Score) -> Box { Box::new(BoostQuery::new(Box::new(PhraseQuery::new(terms)), boost)) } +#[allow(clippy::too_many_arguments)] pub fn build_query( schema: Schema, tokenizers: TokenizerManager, @@ -33,6 +51,7 @@ pub fn build_query( tag_boosts: I, // Id of favorited boost favorite_boost: Option, + stats: &mut QueryStats, ) -> BooleanQuery where I: Iterator, @@ -40,6 +59,8 @@ where let content_terms = terms_for_field(&schema, &tokenizers, query_string, fields.content); let title_terms: Vec = terms_for_field(&schema, &tokenizers, query_string, fields.title); + stats.term_count = content_terms.len() as i32; + let mut term_query: QueryVec = Vec::new(); // Boost exact matches to the full query string From d2bbe85dce6c4ec3545196a128eadcdd2ebb06ae Mon Sep 17 00:00:00 2001 From: travolin Date: Tue, 14 Mar 2023 19:37:48 -0700 Subject: [PATCH 12/19] Configuration Auto Reload (#376) - Update configuration to indicate which properties require a restart - Update configuration GUI to not restart unless required - Update server to provide updated configuration to all components - Add global shortcut configuration --------- Co-authored-by: Tago --- Cargo.lock | 78 ++++++++++ crates/client/public/fixtures.js | 4 +- crates/client/public/glue.js | 4 +- .../client/src/components/forms/keybinding.rs | 147 ++++++++++++++++++ crates/client/src/components/forms/mod.rs | 21 +++ .../client/src/components/forms/pathlist.rs | 2 + .../client/src/components/forms/stringlist.rs | 1 + crates/client/src/components/forms/text.rs | 1 + crates/client/src/components/forms/toggle.rs | 1 + crates/client/src/components/mod.rs | 37 +++++ .../client/src/components/user_action_list.rs | 43 +---- crates/client/src/main.rs | 4 +- crates/client/src/pages/plugin_manager.rs | 1 + crates/client/src/pages/settings.rs | 8 +- .../client/src/pages/wizard/indexing_help.rs | 1 + crates/entities/src/models/crawl_queue.rs | 14 +- crates/shared/Cargo.toml | 1 + crates/shared/src/config.rs | 41 +++-- crates/shared/src/form.rs | 29 ++++ crates/shared/src/keyboard.rs | 24 +-- crates/spyglass-rpc/src/lib.rs | 13 +- crates/spyglass/Cargo.toml | 2 + crates/spyglass/src/api/handler/mod.rs | 36 +++-- crates/spyglass/src/api/mod.rs | 19 ++- crates/spyglass/src/connection/gdrive.rs | 2 +- crates/spyglass/src/crawler/bootstrap.rs | 2 +- crates/spyglass/src/filesystem/mod.rs | 1 + crates/spyglass/src/filesystem/utils.rs | 2 + crates/spyglass/src/main.rs | 7 +- .../spyglass/src/pipeline/default_pipeline.rs | 2 +- crates/spyglass/src/plugin/exports.rs | 2 +- crates/spyglass/src/state.rs | 19 ++- crates/spyglass/src/task.rs | 66 ++++++-- crates/spyglass/src/task/manager.rs | 4 +- crates/spyglass/src/task/worker.rs | 4 +- crates/tauri/Cargo.toml | 1 + crates/tauri/src/cmd.rs | 55 +++++-- crates/tauri/src/cmd/settings.rs | 32 +++- crates/tauri/src/main.rs | 122 +++++++++------ 39 files changed, 654 insertions(+), 199 deletions(-) create mode 100644 crates/client/src/components/forms/keybinding.rs diff --git a/Cargo.lock b/Cargo.lock index deafc7f5b..4927ed475 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,6 +113,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "arrayvec" version = "0.5.2" @@ -1397,6 +1403,28 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "diff-struct" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a699d33d273c6226fd3e8f310a9c90ca94f72be84f7aed95b22bb676f34fc90" +dependencies = [ + "diff_derive", + "num", + "serde", +] + +[[package]] +name = "diff_derive" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe165e7ead196bbbf44c7ce11a7a21157b5c002ce46d7098ff9c556784a4912d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.9.0" @@ -3941,6 +3969,40 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -3972,6 +4034,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -3979,6 +4052,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", + "num-bigint", "num-integer", "num-traits", ] @@ -6127,6 +6201,7 @@ dependencies = [ "anyhow", "bitflags", "chrono", + "diff-struct", "directories", "log", "num-format", @@ -6275,6 +6350,7 @@ version = "23.3.1" dependencies = [ "addr", "anyhow", + "arc-swap", "auth_core", "bytes", "calamine", @@ -6282,6 +6358,7 @@ dependencies = [ "clap 4.1.4", "console-subscriber", "dashmap", + "diff-struct", "digest 0.10.6", "directories", "docx", @@ -6347,6 +6424,7 @@ dependencies = [ "anyhow", "auto-launch", "cocoa", + "diff-struct", "fix-path-env", "jsonrpsee", "log", diff --git a/crates/client/public/fixtures.js b/crates/client/public/fixtures.js index 6e4dcad6a..564ca7ed1 100644 --- a/crates/client/public/fixtures.js +++ b/crates/client/public/fixtures.js @@ -484,8 +484,8 @@ export async function recrawl_domain(domain) { return await invoke("recrawl_domain", { domain }); } -export async function save_user_settings(settings) { - return await invoke("save_user_settings", { settings }); +export async function save_user_settings(settings, restart) { + return await invoke("save_user_settings", { settings, restart }); } export async function searchDocs(lenses, query) { diff --git a/crates/client/public/glue.js b/crates/client/public/glue.js index 2b3fdbd67..43b7154e1 100644 --- a/crates/client/public/glue.js +++ b/crates/client/public/glue.js @@ -17,8 +17,8 @@ export async function recrawl_domain(domain) { return await invoke('recrawl_domain', { domain }); } -export async function save_user_settings(settings) { - return await invoke('save_user_settings', { settings: Object.fromEntries(settings) }); +export async function save_user_settings(settings, restart) { + return await invoke('save_user_settings', { settings: Object.fromEntries(settings), restart }); } export async function searchDocs(lenses, query) { diff --git a/crates/client/src/components/forms/keybinding.rs b/crates/client/src/components/forms/keybinding.rs new file mode 100644 index 000000000..bfc42241d --- /dev/null +++ b/crates/client/src/components/forms/keybinding.rs @@ -0,0 +1,147 @@ +use shared::{ + accelerator, + keyboard::{KeyCode, ModifiersState}, +}; +use web_sys::HtmlInputElement; +use yew::prelude::*; + +use crate::components::{KeyComponent, ModifierIcon}; + +use super::FormFieldProps; +use crate::components::forms::SettingChangeEvent; +use std::str::FromStr; + +pub enum Msg { + HandleInput, + KeyDown(KeyboardEvent), +} + +pub struct KeyBinding { + value: String, + node_ref: NodeRef, +} + +impl Component for KeyBinding { + type Message = Msg; + type Properties = FormFieldProps; + + fn create(ctx: &Context) -> Self { + let props = ctx.props(); + + Self { + value: props.value.clone(), + node_ref: NodeRef::default(), + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + let props = ctx.props(); + + match msg { + Msg::HandleInput => { + if let Some(el) = self.node_ref.cast::() { + self.value = el.value(); + props.onchange.emit(SettingChangeEvent { + setting_name: props.name.clone(), + new_value: self.value.clone(), + restart_required: props.restart_required, + }); + } + + true + } + Msg::KeyDown(evt) => { + if evt.alt_key() || evt.ctrl_key() || evt.meta_key() { + let mut val = String::from(""); + if evt.meta_key() { + val.push_str("Cmd+"); + } + + if evt.ctrl_key() { + val.push_str("Ctrl+"); + } + + if evt.alt_key() { + val.push_str("Alt+"); + } + + if evt.shift_key() { + val.push_str("Shift+"); + } + + let mut modifier = ModifiersState::empty(); + modifier.set(ModifiersState::ALT, evt.alt_key()); + modifier.set(ModifiersState::CONTROL, evt.ctrl_key()); + modifier.set(ModifiersState::SHIFT, evt.shift_key()); + modifier.set(ModifiersState::SUPER, evt.meta_key()); + + if let Ok(key) = KeyCode::from_str(evt.key().to_uppercase().as_str()) { + match key { + KeyCode::Unidentified(_) => { + if let Ok(key) = + KeyCode::from_str(evt.code().to_uppercase().as_str()) + { + match key { + KeyCode::Unidentified(_) => (), + _ => { + val.push_str(key.to_str()); + evt.prevent_default(); + } + } + } + } + _ => { + val.push_str(key.to_str()); + } + } + } + + self.value = val; + + props.onchange.emit(SettingChangeEvent { + setting_name: props.name.clone(), + new_value: self.value.clone(), + restart_required: props.restart_required, + }); + } + true + } + } + } + + fn view(&self, ctx: &Context) -> Html { + let link = ctx.link(); + + let key_binding = if let Ok(acc) = accelerator::parse_accelerator( + self.value.as_str(), + crate::utils::get_os().to_string().as_str(), + ) { + html! { + <> + + {acc.key.to_str()} + + } + } else { + html! { + {"Invalid"} + } + }; + + html! { +
+ + + {key_binding} +
+ } + } +} diff --git a/crates/client/src/components/forms/mod.rs b/crates/client/src/components/forms/mod.rs index 4c6633d62..1286ee766 100644 --- a/crates/client/src/components/forms/mod.rs +++ b/crates/client/src/components/forms/mod.rs @@ -2,6 +2,7 @@ use yew::prelude::*; use shared::form::{FormType, SettingOpts}; +mod keybinding; mod pathlist; mod stringlist; mod text; @@ -12,16 +13,20 @@ pub use stringlist::*; pub use text::*; pub use toggle::*; +use crate::components::forms::keybinding::KeyBinding; + #[derive(Clone)] pub struct SettingChangeEvent { pub setting_name: String, pub new_value: String, + pub restart_required: bool, } #[derive(Properties, PartialEq)] pub struct FormFieldProps { pub name: String, pub value: String, + pub restart_required: bool, pub onchange: Callback, } @@ -60,6 +65,7 @@ impl FormElement { } @@ -69,6 +75,7 @@ impl FormElement { } @@ -78,6 +85,7 @@ impl FormElement { } @@ -87,6 +95,7 @@ impl FormElement { } @@ -96,6 +105,7 @@ impl FormElement { } @@ -105,6 +115,17 @@ impl FormElement { + } + } + FormType::KeyBinding => { + html! { + } diff --git a/crates/client/src/components/forms/pathlist.rs b/crates/client/src/components/forms/pathlist.rs index b6ee80e42..ad36fe9ff 100644 --- a/crates/client/src/components/forms/pathlist.rs +++ b/crates/client/src/components/forms/pathlist.rs @@ -27,6 +27,7 @@ impl PathField { props.onchange.emit(SettingChangeEvent { setting_name: props.name.clone(), new_value: self.path.display().to_string(), + restart_required: props.restart_required, }); } } @@ -156,6 +157,7 @@ impl PathList { props.onchange.emit(SettingChangeEvent { setting_name: props.name.clone(), new_value, + restart_required: props.restart_required, }); } } diff --git a/crates/client/src/components/forms/stringlist.rs b/crates/client/src/components/forms/stringlist.rs index 6cd9f2530..82542fc72 100644 --- a/crates/client/src/components/forms/stringlist.rs +++ b/crates/client/src/components/forms/stringlist.rs @@ -25,6 +25,7 @@ impl StringList { props.onchange.emit(SettingChangeEvent { setting_name: props.name.clone(), new_value, + restart_required: props.restart_required, }); } } diff --git a/crates/client/src/components/forms/text.rs b/crates/client/src/components/forms/text.rs index b40000111..b444e03c7 100644 --- a/crates/client/src/components/forms/text.rs +++ b/crates/client/src/components/forms/text.rs @@ -36,6 +36,7 @@ impl Component for Text { props.onchange.emit(SettingChangeEvent { setting_name: props.name.clone(), new_value: self.value.clone(), + restart_required: props.restart_required, }); } diff --git a/crates/client/src/components/forms/toggle.rs b/crates/client/src/components/forms/toggle.rs index 76213f92f..dc719e793 100644 --- a/crates/client/src/components/forms/toggle.rs +++ b/crates/client/src/components/forms/toggle.rs @@ -31,6 +31,7 @@ impl Component for Toggle { props.onchange.emit(SettingChangeEvent { setting_name: props.name.clone(), new_value, + restart_required: props.restart_required, }); } diff --git a/crates/client/src/components/mod.rs b/crates/client/src/components/mod.rs index 105d29906..208b0c446 100644 --- a/crates/client/src/components/mod.rs +++ b/crates/client/src/components/mod.rs @@ -6,8 +6,11 @@ pub mod result; pub mod tag; pub mod tooltip; pub mod user_action_list; +use shared::keyboard::ModifiersState; use yew::{prelude::*, virtual_dom::AttrValue}; +use crate::utils::{self, OsName}; + #[derive(Properties, PartialEq, Eq)] pub struct SelectLensProps { pub lens: Vec, @@ -147,3 +150,37 @@ pub fn txt_bubble(props: &KeyCodeProps) -> Html {
} } + +#[derive(Properties, PartialEq)] +pub struct ModifierProps { + pub modifier: ModifiersState, +} + +#[function_component(ModifierIcon)] +pub fn modifier_icon(props: &ModifierProps) -> Html { + let mut nodes: Vec = Vec::new(); + + if props.modifier.control_key() { + nodes.push(html! { {"CTRL"} }); + } + + if props.modifier.super_key() { + match utils::get_os() { + OsName::MacOS => { + nodes.push(html! { }) + } + _ => nodes + .push(html! { }), + } + } + + if props.modifier.alt_key() { + nodes.push(html! { {"ALT"} }); + } + + if props.modifier.shift_key() { + nodes.push(html! { {"SHIFT"} }); + } + + html! { <>{nodes} } +} diff --git a/crates/client/src/components/user_action_list.rs b/crates/client/src/components/user_action_list.rs index 3afcf07a7..21ce2d514 100644 --- a/crates/client/src/components/user_action_list.rs +++ b/crates/client/src/components/user_action_list.rs @@ -1,11 +1,8 @@ use crate::components::icons::{ArrowTopRightOnSquare, BookOpen, ClipboardDocumentIcon}; -use crate::components::{icons, KeyComponent}; -use crate::utils::{self, OsName}; +use crate::components::{KeyComponent, ModifierIcon}; +use crate::utils; use shared::accelerator; -use shared::{ - config::{self, UserAction, UserActionDefinition}, - keyboard::ModifiersState, -}; +use shared::config::{self, UserAction, UserActionDefinition}; use yew::function_component; use yew::prelude::*; @@ -34,40 +31,6 @@ pub struct UserActionProps { pub onclick: Callback, } -#[derive(Properties, PartialEq)] -pub struct ModifierProps { - pub modifier: ModifiersState, -} - -#[function_component(ModifierIcon)] -fn modifier_icon(props: &ModifierProps) -> Html { - let mut nodes: Vec = Vec::new(); - - if props.modifier.control_key() { - nodes.push(html! { {"CTRL"} }); - } - - if props.modifier.super_key() { - match utils::get_os() { - OsName::MacOS => { - nodes.push(html! { }) - } - _ => nodes - .push(html! { }), - } - } - - if props.modifier.alt_key() { - nodes.push(html! { {"ALT"} }); - } - - if props.modifier.shift_key() { - nodes.push(html! { {"SHIFT"} }); - } - - html! { <>{nodes} } -} - #[function_component(UserActionComponent)] fn user_action(props: &UserActionProps) -> Html { let component_styles = classes!( diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index 036255c7f..009c5262e 100644 --- a/crates/client/src/main.rs +++ b/crates/client/src/main.rs @@ -30,7 +30,7 @@ extern "C" { pub async fn delete_doc(id: String) -> Result<(), JsValue>; #[wasm_bindgen(catch)] - pub async fn save_user_settings(settings: JsValue) -> Result; + pub async fn save_user_settings(settings: JsValue, restart: bool) -> Result; #[wasm_bindgen(js_name = "searchDocs", catch)] pub async fn search_docs(lenses: JsValue, query: String) -> Result; @@ -72,7 +72,7 @@ extern "C" { pub async fn delete_doc(id: String) -> Result<(), JsValue>; #[wasm_bindgen(catch)] - pub async fn save_user_settings(settings: JsValue) -> Result; + pub async fn save_user_settings(settings: JsValue, restart: bool) -> Result; #[wasm_bindgen(js_name = "searchDocs", catch)] pub async fn search_docs(lenses: JsValue, query: String) -> Result; diff --git a/crates/client/src/pages/plugin_manager.rs b/crates/client/src/pages/plugin_manager.rs index 1605066bb..ae4e46dda 100644 --- a/crates/client/src/pages/plugin_manager.rs +++ b/crates/client/src/pages/plugin_manager.rs @@ -62,6 +62,7 @@ pub fn plugin_comp(props: &PluginProps) -> Html {
diff --git a/crates/client/src/pages/settings.rs b/crates/client/src/pages/settings.rs index c9b1798af..f49b97d20 100644 --- a/crates/client/src/pages/settings.rs +++ b/crates/client/src/pages/settings.rs @@ -27,6 +27,7 @@ pub struct UserSettingsPage { errors: HashMap, changes: HashMap, has_changes: bool, + restart_required: bool, req_settings: RequestState, } @@ -61,6 +62,7 @@ impl Component for UserSettingsPage { changes: HashMap::new(), errors: HashMap::new(), has_changes: false, + restart_required: false, req_settings: RequestState::NotStarted, } } @@ -79,14 +81,16 @@ impl Component for UserSettingsPage { Msg::HandleOnChange(evt) => { self.has_changes = true; self.changes.insert(evt.setting_name, evt.new_value); + self.restart_required |= evt.restart_required; true } Msg::HandleSave => { let changes = self.changes.clone(); // Send changes to backend to be validated & saved. + let restart_required = self.restart_required; if let Ok(ser) = serde_wasm_bindgen::to_value(&changes) { link.send_future(async move { - if let Err(res) = save_user_settings(ser).await { + if let Err(res) = save_user_settings(ser, restart_required).await { if let Ok(errors) = serde_wasm_bindgen::from_value::>(res) { @@ -149,7 +153,7 @@ impl Component for UserSettingsPage { {"Show Folder"} - {"Save & Restart"} + { if self.restart_required {"Save & Restart"} else {"Save"} }
diff --git a/crates/client/src/pages/wizard/indexing_help.rs b/crates/client/src/pages/wizard/indexing_help.rs index cc84aab6f..4eae1072e 100644 --- a/crates/client/src/pages/wizard/indexing_help.rs +++ b/crates/client/src/pages/wizard/indexing_help.rs @@ -55,6 +55,7 @@ pub fn index_files_help(props: &IndexFilesHelpProps) -> Html { label: "Enable local file searching".into(), value: serde_json::to_string(&props.toggle_file_indexer).unwrap_or_default(), form_type: FormType::Bool, + restart_required: false, help_text: None, }; diff --git a/crates/entities/src/models/crawl_queue.rs b/crates/entities/src/models/crawl_queue.rs index d3a0a822d..9c98cc76b 100644 --- a/crates/entities/src/models/crawl_queue.rs +++ b/crates/entities/src/models/crawl_queue.rs @@ -215,7 +215,7 @@ pub async fn num_queued( Ok(res) } -fn gen_dequeue_sql(user_settings: UserSettings) -> Statement { +fn gen_dequeue_sql(user_settings: &UserSettings) -> Statement { Statement::from_sql_and_values( DbBackend::Sqlite, include_str!("sql/dequeue.sqlx"), @@ -288,7 +288,7 @@ pub async fn num_of_files_in_progress(db: &DatabaseConnection) -> anyhow::Result /// Get the next url in the crawl queue pub async fn dequeue( db: &DatabaseConnection, - user_settings: UserSettings, + user_settings: &UserSettings, ) -> anyhow::Result, sea_orm::DbErr> { // Check for inflight limits if let Limit::Finite(inflight_crawl_limit) = user_settings.inflight_crawl_limit { @@ -339,7 +339,7 @@ pub async fn dequeue( /// Get the next url in the crawl queue pub async fn dequeue_files( db: &DatabaseConnection, - user_settings: UserSettings, + user_settings: &UserSettings, ) -> anyhow::Result, sea_orm::DbErr> { // Check for inflight limits if let Limit::Finite(inflight_crawl_limit) = user_settings.inflight_crawl_limit { @@ -1033,7 +1033,7 @@ mod test { #[test] fn test_priority_sql() { let settings = UserSettings::default(); - let sql = gen_dequeue_sql(settings); + let sql = gen_dequeue_sql(&settings); assert_eq!( sql.to_string(), "WITH\nindexed AS (\n SELECT\n domain,\n count(*) as count\n FROM indexed_document\n GROUP BY domain\n),\ninflight AS (\n SELECT\n domain,\n count(*) as count\n FROM crawl_queue\n WHERE status = \"Processing\"\n GROUP BY domain\n)\nSELECT\n cq.*\nFROM crawl_queue cq\nLEFT JOIN indexed ON indexed.domain = cq.domain\nLEFT JOIN inflight ON inflight.domain = cq.domain\nWHERE\n COALESCE(indexed.count, 0) < 500000 AND\n COALESCE(inflight.count, 0) < 2 AND\n status = \"Queued\" and\n url not like \"file%\"\nORDER BY\n cq.updated_at ASC" @@ -1166,7 +1166,7 @@ mod test { .await .unwrap(); - let queue = crawl_queue::dequeue(&db, settings).await.unwrap(); + let queue = crawl_queue::dequeue(&db, &settings).await.unwrap(); assert!(queue.is_some()); assert_eq!(queue.unwrap().url, url[0]); @@ -1203,14 +1203,14 @@ mod test { ..Default::default() }; doc.save(&db).await.unwrap(); - let queue = crawl_queue::dequeue(&db, settings).await.unwrap(); + let queue = crawl_queue::dequeue(&db, &settings).await.unwrap(); assert!(queue.is_some()); let settings = UserSettings { domain_crawl_limit: Limit::Finite(1), ..Default::default() }; - let queue = crawl_queue::dequeue(&db, settings).await.unwrap(); + let queue = crawl_queue::dequeue(&db, &settings).await.unwrap(); assert!(queue.is_none()); } diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index c49e48a2a..8e2022246 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] anyhow = "1.0" bitflags = "1.3.2" +diff-struct = "0.5.1" directories = "4.0" log = "0.4" regex = "1" diff --git a/crates/shared/src/config.rs b/crates/shared/src/config.rs index 0db3f09af..58b96276c 100644 --- a/crates/shared/src/config.rs +++ b/crates/shared/src/config.rs @@ -1,4 +1,5 @@ pub use crate::keyboard::{KeyCode, ModifiersState}; +use diff::Diff; use directories::{ProjectDirs, UserDirs}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; @@ -44,7 +45,7 @@ impl Default for Config { } } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, Diff)] pub enum Limit { Infinite, Finite(u32), @@ -67,7 +68,7 @@ impl Limit { pub type PluginSettings = HashMap>; -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, Diff)] pub struct FileSystemSettings { #[serde(default)] pub enable_filesystem_scanning: bool, @@ -115,7 +116,7 @@ impl Default for FileSystemSettings { } // Enum of actions the user can take when a document is selected -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Diff)] pub enum UserAction { OpenApplication(String, String), CopyToClipboard(String), @@ -123,7 +124,7 @@ pub enum UserAction { // The user action settings configuration provides the ability // for the user to define custom behavior for a document. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Diff)] pub struct UserActionSettings { pub actions: Vec, pub context_actions: Vec, @@ -206,7 +207,7 @@ impl Default for UserActionSettings { // Defines context specific actions. A context specific action // is a list of actions that are only valid when the document selected // matches the defined context. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Diff)] pub struct ContextActions { // Defines what context must be matched for the actions to be valid pub context: ContextFilter, @@ -321,7 +322,7 @@ impl ContextActions { // Filter definition used to define what documents should match // against the context. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Diff)] pub struct ContextFilter { // Includes documents that match any of the defined tags pub has_tag: Option>, @@ -338,7 +339,7 @@ pub struct ContextFilter { } // The definition for an action -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Diff)] pub struct UserActionDefinition { pub label: String, pub status_msg: Option, @@ -358,7 +359,7 @@ impl UserActionDefinition { } } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, Diff)] pub struct UserSettings { /// Number of pages allowed per domain. Sub-domains are treated as /// separate domains. @@ -441,42 +442,56 @@ impl From for Vec<(String, SettingOpts)> { label: "Data Directory".into(), value: settings.data_directory.to_str().map_or(String::new(), |s| s.to_string()), form_type: FormType::Path, + restart_required: true, help_text: Some("The data directory is where your index, lenses, plugins, and logs are stored. This will require a restart.".into()) }), ("_.disable_autolaunch".into(), SettingOpts { label: "Disable Autolaunch".into(), value: serde_json::to_string(&settings.disable_autolaunch).expect("Unable to ser autolaunch value"), form_type: FormType::Bool, + restart_required: false, help_text: Some("Prevents Spyglass from automatically launching when your computer first starts up.".into()) }), + ("_.shortcut".into(), SettingOpts { + label: "Global Shortcut".into(), + value: settings.shortcut.clone(), + form_type: FormType::KeyBinding, + restart_required: false, + help_text: Some("Defines the global keyboard shortcut used to open the Spyglass search window.".into()) + }), ("_.disable_telemetry".into(), SettingOpts { label: "Disable Telemetry".into(), value: serde_json::to_string(&settings.disable_telemetry).expect("Unable to ser autolaunch value"), form_type: FormType::Bool, - help_text: Some("Stop sending data to any 3rd-party service. See https://spyglass.fyi/telemetry for more info.".into()) + restart_required: false, + help_text: Some("Stop sending data to any 3rd-party service. See https://spyglass.fyi/telemetry for more info. This will require a restart.".into()) }), ("_.port".into(), SettingOpts { label: "Spyglass Daemon Port".into(), value: settings.port.to_string(), form_type: FormType::Number, - help_text: Some("Port number used by the Spyglass background services. Only change this if you already have another serive running on this port.".into()) + restart_required: true, + help_text: Some("Port number used by the Spyglass background services. Only change this if you already have another server running on this port. This will require a restart.".into()) }), ("_.filesystem_settings.enable_filesystem_scanning".into(), SettingOpts { label: "Enable Filesystem Indexing".into(), value: settings.filesystem_settings.enable_filesystem_scanning.to_string(), form_type: FormType::Bool, + restart_required: false, help_text: Some("Enables and disables local filesystem indexing. When enabled configured folders will be scanned and indexed. Any supported file types will have their contents indexed.".into()) }), ("_.filesystem_settings.watched_paths".into(), SettingOpts { label: "Folder List".into(), value: serde_json::to_string(&settings.filesystem_settings.watched_paths).unwrap_or(String::from("[]")), form_type: FormType::PathList, - help_text: Some("List of folders that will be crawled & indexed. These folders will be crawled recursively, so you only need to specifiy the parent folder.".into()) + restart_required: false, + help_text: Some("List of folders that will be crawled & indexed. These folders will be crawled recursively, so you only need to specify the parent folder.".into()) }), ("_.filesystem_settings.supported_extensions".into(), SettingOpts { label: "Extension List".into(), value: serde_json::to_string(&settings.filesystem_settings.supported_extensions).unwrap_or(String::from("[]")), form_type: FormType::StringList, + restart_required: false, help_text: Some("List of file types to index.".into()) }), ]; @@ -488,8 +503,9 @@ impl From for Vec<(String, SettingOpts)> { label: "Max number of crawlers".into(), value: val.to_string(), form_type: FormType::Number, + restart_required: true, help_text: Some( - "Maximum number of concurrent crawlers in total used by Spyglass".into(), + "Maximum number of concurrent crawlers in total used by Spyglass, This will require a restart".into(), ), }, )); @@ -502,6 +518,7 @@ impl From for Vec<(String, SettingOpts)> { label: "Max number crawlers per domain".into(), value: val.to_string(), form_type: FormType::Number, + restart_required: false, help_text: Some( "Maximum number of concurrent crawlers used per site/app.".into(), ), diff --git a/crates/shared/src/form.rs b/crates/shared/src/form.rs index 9f9e74fd5..0eb04154a 100644 --- a/crates/shared/src/form.rs +++ b/crates/shared/src/form.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; use std::path::Path; use strum_macros::{Display, EnumString}; +use crate::{accelerator, config::KeyCode, MAC_OS}; + #[derive(Clone, Debug, Display, EnumString, PartialEq, Serialize, Deserialize, Eq)] pub enum FormType { Bool, @@ -11,6 +13,7 @@ pub enum FormType { PathList, StringList, Text, + KeyBinding, } impl FormType { @@ -73,6 +76,31 @@ impl FormType { Ok(value.into()) } + FormType::KeyBinding => { + if value.is_empty() { + return Err("Value cannot be empty".into()); + } + + match accelerator::parse_accelerator(value, MAC_OS) { + Ok(acc) => { + if !acc.mods.alt_key() && !acc.mods.control_key() && !acc.mods.super_key() { + return Err("Global key binding must have at least one modifier key (ALT, CMD, CTRL)".into()); + } + + if let KeyCode::Unidentified(_) = acc.key { + return Err("Invalid key code binding".into()); + } + } + Err(error) => { + return Err(format!( + "Value does not represent a valid key binding {:?}", + error + )); + } + } + + Ok(value.to_owned()) + } } } } @@ -82,5 +110,6 @@ pub struct SettingOpts { pub label: String, pub value: String, pub form_type: FormType, + pub restart_required: bool, pub help_text: Option, } diff --git a/crates/shared/src/keyboard.rs b/crates/shared/src/keyboard.rs index a6d09c09a..27315842e 100644 --- a/crates/shared/src/keyboard.rs +++ b/crates/shared/src/keyboard.rs @@ -665,16 +665,16 @@ impl FromStr for KeyCode { "[" | "BRACKETLEFT" => KeyCode::BracketLeft, "]" | "BRACKETRIGHT" => KeyCode::BracketRight, "," | "COMMA" => KeyCode::Comma, - "0" => KeyCode::Digit0, - "1" => KeyCode::Digit1, - "2" => KeyCode::Digit2, - "3" => KeyCode::Digit3, - "4" => KeyCode::Digit4, - "5" => KeyCode::Digit5, - "6" => KeyCode::Digit6, - "7" => KeyCode::Digit7, - "8" => KeyCode::Digit8, - "9" => KeyCode::Digit9, + "0" | "DIGIT0" => KeyCode::Digit0, + "1" | "DIGIT1" => KeyCode::Digit1, + "2" | "DIGIT2" => KeyCode::Digit2, + "3" | "DIGIT3" => KeyCode::Digit3, + "4" | "DIGIT4" => KeyCode::Digit4, + "5" | "DIGIT5" => KeyCode::Digit5, + "6" | "DIGIT6" => KeyCode::Digit6, + "7" | "DIGIT7" => KeyCode::Digit7, + "8" | "DIGIT8" => KeyCode::Digit8, + "9" | "DIGIT9" => KeyCode::Digit9, "NUM0" | "NUMPAD0" => KeyCode::Numpad0, "NUM1" | "NUMPAD1" => KeyCode::Numpad1, "NUM2" | "NUMPAD2" => KeyCode::Numpad2, @@ -685,8 +685,8 @@ impl FromStr for KeyCode { "NUM7" | "NUMPAD7" => KeyCode::Numpad7, "NUM8" | "NUMPAD8" => KeyCode::Numpad8, "NUM9" | "NUMPAD9" => KeyCode::Numpad9, - "=" => KeyCode::Equal, - "-" => KeyCode::Minus, + "=" | "EQUAL" => KeyCode::Equal, + "-" | "MINUS" => KeyCode::Minus, "PLUS" => KeyCode::Plus, "." | "PERIOD" => KeyCode::Period, "'" | "QUOTE" => KeyCode::Quote, diff --git a/crates/spyglass-rpc/src/lib.rs b/crates/spyglass-rpc/src/lib.rs index 1a33f3d8c..7b75179e3 100644 --- a/crates/spyglass-rpc/src/lib.rs +++ b/crates/spyglass-rpc/src/lib.rs @@ -1,5 +1,6 @@ use jsonrpsee::core::Error; use jsonrpsee::proc_macros::rpc; +use shared::config::UserSettings; use std::collections::HashMap; use shared::request::{BatchDocumentRequest, RawDocumentRequest, SearchLensesParam, SearchParam}; @@ -74,15 +75,21 @@ pub trait Rpc { #[method(name = "search_lenses")] async fn search_lenses(&self, query: SearchLensesParam) -> Result; + #[method(name = "update_user_settings")] + async fn update_user_settings( + &self, + user_settings: UserSettings, + ) -> Result; + + #[method(name = "user_settings")] + async fn user_settings(&self) -> Result; + #[method(name = "toggle_pause")] async fn toggle_pause(&self, is_paused: bool) -> Result<(), Error>; #[method(name = "toggle_plugin")] async fn toggle_plugin(&self, name: String, enabled: bool) -> Result<(), Error>; - #[method(name = "toggle_filesystem")] - async fn toggle_filesystem(&self, enabled: bool) -> Result<(), Error>; - #[method(name = "uninstall_lens")] async fn uninstall_lens(&self, name: String) -> Result<(), Error>; } diff --git a/crates/spyglass/Cargo.toml b/crates/spyglass/Cargo.toml index d1f785327..02eb13c63 100644 --- a/crates/spyglass/Cargo.toml +++ b/crates/spyglass/Cargo.toml @@ -7,12 +7,14 @@ default-run = "spyglass" [dependencies] addr = "0.15.3" anyhow = "1.0" +arc-swap = "1.6.0" bytes = "1.2.1" calamine = "0.19.1" chrono = { version = "0.4.23", features = ["serde"] } clap = { version = "4.0.32", features = ["derive"] } console-subscriber = { version = "0.1.8", optional = true } dashmap = "5.2" +diff-struct = "0.5.1" digest = "0.10" directories = "4.0" docx = { git = "https://github.com/spyglass-search/docx-rs", branch = "master"} diff --git a/crates/spyglass/src/api/handler/mod.rs b/crates/spyglass/src/api/handler/mod.rs index 25c8a967d..436573066 100644 --- a/crates/spyglass/src/api/handler/mod.rs +++ b/crates/spyglass/src/api/handler/mod.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use directories::UserDirs; use entities::get_library_stats; use entities::models::crawl_queue::{CrawlStatus, EnqueueSettings}; @@ -17,9 +18,9 @@ use libspyglass::filesystem; use libspyglass::plugin::PluginCommand; use libspyglass::search::Searcher; use libspyglass::state::AppState; -use libspyglass::task::{AppPause, ManagerCommand}; +use libspyglass::task::{AppPause, UserSettingsChange}; use num_format::{Locale, ToFormattedString}; -use shared::config::{self, Config}; +use shared::config::{self, Config, UserSettings}; use shared::metrics::Event; use shared::request::{BatchDocumentRequest, RawDocType, RawDocumentRequest}; use shared::response::{ @@ -69,7 +70,7 @@ pub async fn add_document_batch(state: &AppState, req: &BatchDocumentRequest) -> &state.db, &req.urls, &[], - &state.user_settings, + &state.user_settings.load(), &overrides, None, ) @@ -150,7 +151,7 @@ pub async fn add_raw_document(state: &AppState, req: &RawDocumentRequest) -> Res &state.db, &[req.url.clone()], &[], - &state.user_settings, + &state.user_settings.load(), &overrides, None, ) @@ -546,17 +547,26 @@ pub async fn toggle_plugin(state: AppState, name: String, enabled: bool) -> Resu Ok(()) } -#[instrument(skip(state))] -pub async fn toggle_filesystem(state: AppState, enabled: bool) -> Result<(), Error> { - let mut cmd_tx = state.manager_cmd_tx.lock().await; - match &mut *cmd_tx { - Some(cmd_tx) => { - let _ = cmd_tx.send(ManagerCommand::ToggleFilesystem(enabled)); - } - None => {} +#[instrument(skip(app, _config))] +pub async fn update_user_settings( + app: &AppState, + _config: &Config, + user_settings: &UserSettings, +) -> Result { + if let Err(error) = app + .config_cmd_tx + .lock() + .await + .send(UserSettingsChange::SettingsChanged(user_settings.clone())) + { + return Err(anyhow!(error).into()); } + Ok(user_settings.clone()) +} - Ok(()) +#[instrument(skip(app))] +pub async fn user_settings(app: &AppState) -> Result { + Ok(app.user_settings.load().as_ref().clone()) } #[instrument(skip(state))] diff --git a/crates/spyglass/src/api/mod.rs b/crates/spyglass/src/api/mod.rs index 3b88154cf..6ee7037dd 100644 --- a/crates/spyglass/src/api/mod.rs +++ b/crates/spyglass/src/api/mod.rs @@ -6,7 +6,7 @@ use jsonrpsee::server::{ServerBuilder, ServerHandle}; use libspyglass::search::{self, Searcher}; use libspyglass::state::AppState; use libspyglass::task::{CollectTask, ManagerCommand}; -use shared::config::Config; +use shared::config::{Config, UserSettings}; use shared::request::{BatchDocumentRequest, RawDocumentRequest, SearchLensesParam, SearchParam}; use shared::response::{self as resp, DefaultIndices, LibraryStats}; use spyglass_rpc::RpcServer; @@ -182,20 +182,27 @@ impl RpcServer for SpyglassRpc { handler::toggle_plugin(self.state.clone(), name, enabled).await } - async fn toggle_filesystem(&self, enabled: bool) -> Result<(), Error> { - handler::toggle_filesystem(self.state.clone(), enabled).await - } - async fn uninstall_lens(&self, name: String) -> Result<(), Error> { handler::uninstall_lens(self.state.clone(), &self.config, &name).await } + + async fn update_user_settings(&self, settings: UserSettings) -> Result { + handler::update_user_settings(&self.state, &self.config, &settings).await + } + + async fn user_settings(&self) -> Result { + handler::user_settings(&self.state).await + } } pub async fn start_api_server( state: AppState, config: Config, ) -> anyhow::Result<(SocketAddr, ServerHandle)> { - let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), state.user_settings.port); + let server_addr = SocketAddr::new( + IpAddr::V4(Ipv4Addr::LOCALHOST), + state.user_settings.load_full().port, + ); let server = ServerBuilder::default().build(server_addr).await?; let rpc_module = SpyglassRpc { diff --git a/crates/spyglass/src/connection/gdrive.rs b/crates/spyglass/src/connection/gdrive.rs index f40537af2..0b381345f 100644 --- a/crates/spyglass/src/connection/gdrive.rs +++ b/crates/spyglass/src/connection/gdrive.rs @@ -151,7 +151,7 @@ impl Connection for DriveConnection { &state.db, &to_download, &[], - &state.user_settings, + &state.user_settings.load(), &enqueue_settings, None, ) diff --git a/crates/spyglass/src/crawler/bootstrap.rs b/crates/spyglass/src/crawler/bootstrap.rs index 12d6817f1..c5916c8c5 100644 --- a/crates/spyglass/src/crawler/bootstrap.rs +++ b/crates/spyglass/src/crawler/bootstrap.rs @@ -117,7 +117,7 @@ pub async fn bootstrap( db, &urls, &[lens.clone()], - settings, + &settings.load(), &overrides, pipeline.clone(), ) diff --git a/crates/spyglass/src/filesystem/mod.rs b/crates/spyglass/src/filesystem/mod.rs index 439b53275..084120870 100644 --- a/crates/spyglass/src/filesystem/mod.rs +++ b/crates/spyglass/src/filesystem/mod.rs @@ -601,6 +601,7 @@ pub async fn configure_watcher(state: AppState) { // temp use plugin configuration let enabled = &state .user_settings + .load() .filesystem_settings .enable_filesystem_scanning; diff --git a/crates/spyglass/src/filesystem/utils.rs b/crates/spyglass/src/filesystem/utils.rs index 58865352b..c0dd74ff6 100644 --- a/crates/spyglass/src/filesystem/utils.rs +++ b/crates/spyglass/src/filesystem/utils.rs @@ -137,6 +137,7 @@ pub fn last_modified_time(path: &Path) -> DateTime { pub fn get_search_directories(state: &AppState) -> Vec { state .user_settings + .load() .filesystem_settings .watched_paths .clone() @@ -148,6 +149,7 @@ pub fn get_supported_file_extensions(state: &AppState) -> HashSet { HashSet::from_iter( state .user_settings + .load() .filesystem_settings .supported_extensions .iter() diff --git a/crates/spyglass/src/main.rs b/crates/spyglass/src/main.rs index 6f56e0a37..d3c29a009 100644 --- a/crates/spyglass/src/main.rs +++ b/crates/spyglass/src/main.rs @@ -181,6 +181,7 @@ async fn start_backend(state: AppState, config: Config) { let (worker_cmd_tx, worker_cmd_rx) = mpsc::channel( state .user_settings + .load() .inflight_crawl_limit .value() .try_into() @@ -234,6 +235,9 @@ async fn start_backend(state: AppState, config: Config) { manager_cmd_rx, )); + // Work scheduler + let config_handle = tokio::spawn(task::config_task(state.clone())); + // Crawlers let worker_handle = tokio::spawn(task::worker_task( state.clone(), @@ -304,6 +308,7 @@ async fn start_backend(state: AppState, config: Config) { manager_handle, worker_handle, pm_handle, - lens_watcher_handle + lens_watcher_handle, + config_handle ); } diff --git a/crates/spyglass/src/pipeline/default_pipeline.rs b/crates/spyglass/src/pipeline/default_pipeline.rs index 0f7a412eb..5f227aaea 100644 --- a/crates/spyglass/src/pipeline/default_pipeline.rs +++ b/crates/spyglass/src/pipeline/default_pipeline.rs @@ -103,7 +103,7 @@ async fn start_crawl( &state.db, &to_enqueue, &lenses, - &state.user_settings, + &state.user_settings.load(), &Default::default(), Some(pipeline_name.to_owned()), ) diff --git a/crates/spyglass/src/plugin/exports.rs b/crates/spyglass/src/plugin/exports.rs index e4c537120..8bd404a96 100644 --- a/crates/spyglass/src/plugin/exports.rs +++ b/crates/spyglass/src/plugin/exports.rs @@ -239,7 +239,7 @@ fn handle_plugin_enqueue(env: &PluginEnv, urls: &Vec) { &state.db.clone(), &urls, &[], - &state.user_settings, + &state.user_settings.load(), &EnqueueSettings { force_allow: true, ..Default::default() diff --git a/crates/spyglass/src/state.rs b/crates/spyglass/src/state.rs index 85ec4e72c..23d47b367 100644 --- a/crates/spyglass/src/state.rs +++ b/crates/spyglass/src/state.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use arc_swap::ArcSwap; use dashmap::DashMap; use entities::models::create_connection; use entities::sea_orm::DatabaseConnection; @@ -8,7 +9,7 @@ use tokio::sync::Mutex; use tokio::sync::{broadcast, mpsc}; use crate::filesystem::SpyglassFileWatcher; -use crate::task::AppShutdown; +use crate::task::{AppShutdown, UserSettingsChange}; use crate::{ pipeline::PipelineCommand, plugin::{PluginCommand, PluginManager}, @@ -24,13 +25,14 @@ pub struct AppState { pub app_state: Arc>, pub lenses: Arc>, pub pipelines: Arc>, - pub user_settings: UserSettings, + pub user_settings: Arc>, pub index: Searcher, pub metrics: Metrics, pub config: Config, // Task scheduler command/control pub manager_cmd_tx: Arc>>>, pub shutdown_cmd_tx: Arc>>, + pub config_cmd_tx: Arc>>, // Pause/unpause worker pool. pub pause_cmd_tx: Arc>>>, // Plugin command/control @@ -72,11 +74,12 @@ impl AppState { } let (shutdown_tx, _) = broadcast::channel::(16); + let (config_tx, _) = broadcast::channel::(16); AppState { db, app_state: Arc::new(app_state), - user_settings: config.user_settings.clone(), + user_settings: Arc::new(ArcSwap::from_pointee(config.user_settings.clone())), metrics: Metrics::new( &Config::machine_identifier(), config.user_settings.disable_telemetry, @@ -86,6 +89,7 @@ impl AppState { pipelines: Arc::new(pipelines), index, shutdown_cmd_tx: Arc::new(Mutex::new(shutdown_tx)), + config_cmd_tx: Arc::new(Mutex::new(config_tx)), pause_cmd_tx: Arc::new(Mutex::new(None)), plugin_cmd_tx: Arc::new(Mutex::new(None)), pipeline_cmd_tx: Arc::new(Mutex::new(None)), @@ -99,7 +103,10 @@ impl AppState { log::debug!("reloading config..."); let config = Config::new(); - self.user_settings = config.user_settings.clone(); + self.user_settings + .store(Arc::new(config.user_settings.clone())); + + // self.user_settings = config.user_settings.clone(); self.config = config; } @@ -155,6 +162,7 @@ impl AppStateBuilder { }; let (shutdown_tx, _) = broadcast::channel::(16); + let (config_tx, _) = broadcast::channel::(16); AppState { app_state: Arc::new(DashMap::new()), @@ -173,8 +181,9 @@ impl AppStateBuilder { plugin_cmd_tx: Arc::new(Mutex::new(None)), plugin_manager: Arc::new(Mutex::new(PluginManager::new())), shutdown_cmd_tx: Arc::new(Mutex::new(shutdown_tx)), + config_cmd_tx: Arc::new(Mutex::new(config_tx)), file_watcher: Arc::new(Mutex::new(None)), - user_settings, + user_settings: Arc::new(ArcSwap::from_pointee(user_settings)), } } diff --git a/crates/spyglass/src/task.rs b/crates/spyglass/src/task.rs index a386418ca..182a6d841 100644 --- a/crates/spyglass/src/task.rs +++ b/crates/spyglass/src/task.rs @@ -1,7 +1,7 @@ use entities::models::{bootstrap_queue, connection}; use notify::event::ModifyKind; use notify::{EventKind, RecursiveMode, Watcher}; -use shared::config::{Config, LensConfig}; +use shared::config::{Config, LensConfig, UserSettings, UserSettingsDiff}; use std::sync::atomic::Ordering; use std::sync::{atomic::AtomicI32, Arc}; use std::time::Duration; @@ -14,6 +14,7 @@ use crate::search::lens::{load_lenses, read_lenses}; use crate::search::Searcher; use crate::state::AppState; use crate::task::worker::FetchResult; +use diff::Diff; mod manager; pub mod worker; @@ -46,6 +47,11 @@ pub struct CleanupTask { pub missing_docs: Vec<(String, String)>, } +#[derive(Clone, Debug)] +pub enum UserSettingsChange { + SettingsChanged(UserSettings), +} + /// Tell the manager to schedule some tasks #[derive(Clone, Debug)] pub enum ManagerCommand { @@ -54,7 +60,6 @@ pub enum ManagerCommand { /// General database cleanup command that sends a worker /// task request for cleanup CleanupDatabase(CleanupTask), - ToggleFilesystem(bool), } /// Send tasks to the worker @@ -135,16 +140,6 @@ pub async fn manager_task( // first tick always completes immediately. queue_check_interval.tick().await; } - }, - ManagerCommand::ToggleFilesystem (enabled) => { - let mut state = state.clone(); - if let Ok(mut loaded_settings) = Config::load_user_settings() { - loaded_settings.filesystem_settings.enable_filesystem_scanning = enabled; - let _ = Config::save_user_settings(&loaded_settings); - state.user_settings = loaded_settings; - } - - filesystem::configure_watcher(state).await; } } } @@ -168,6 +163,53 @@ pub async fn manager_task( } } +/// Manages the worker pool, scheduling tasks based on type/priority/etc. +#[tracing::instrument(skip_all)] +pub async fn config_task(mut state: AppState) { + log::info!("Staring Configuration Watcher"); + + let mut shutdown_rx = state.shutdown_cmd_tx.lock().await.subscribe(); + let mut config_rx = state.config_cmd_tx.lock().await.subscribe(); + + loop { + tokio::select! { + cmd = config_rx.recv() => { + if let Ok(cmd) = cmd { + match cmd { + UserSettingsChange::SettingsChanged (new_settings) => { + log::debug!("User Settings Updated {:?}", new_settings); + let old_config = state.user_settings.load_full(); + + if Config::save_user_settings(&new_settings).is_ok() { + state.reload_config(); + let diff = new_settings.diff(&old_config); + + process_filesystem_changes(&state, &diff).await; + } + } + } + } + } + _ = shutdown_rx.recv() => { + log::info!("🛑 Shutting down configuration watcher"); + return; + } + }; + } +} + +// Processes any needed filesystem configuration changes +async fn process_filesystem_changes(state: &AppState, diff: &UserSettingsDiff) { + let fs_diff = &diff.filesystem_settings; + if fs_diff.enable_filesystem_scanning.is_some() + || !fs_diff.supported_extensions.0.is_empty() + || !fs_diff.watched_paths.0.is_empty() + { + // fs configuration has changed update fs + filesystem::configure_watcher(state.clone()).await; + } +} + /// Grabs a task pub async fn worker_task( state: AppState, diff --git a/crates/spyglass/src/task/manager.rs b/crates/spyglass/src/task/manager.rs index 5f915cb1b..0503e7433 100644 --- a/crates/spyglass/src/task/manager.rs +++ b/crates/spyglass/src/task/manager.rs @@ -10,7 +10,7 @@ use crate::state::AppState; pub async fn check_for_jobs(state: &AppState, queue: &mpsc::Sender) -> bool { let mut started_task = None; // Do we have any crawl tasks? - match crawl_queue::dequeue(&state.db, state.user_settings.clone()).await { + match crawl_queue::dequeue(&state.db, &state.user_settings.load()).await { Ok(Some(task)) => { match &task.pipeline { Some(pipeline) => { @@ -44,7 +44,7 @@ pub async fn check_for_jobs(state: &AppState, queue: &mpsc::Sender { match &task.pipeline { Some(pipeline) => { diff --git a/crates/spyglass/src/task/worker.rs b/crates/spyglass/src/task/worker.rs index e3d70e2ff..17852a49a 100644 --- a/crates/spyglass/src/task/worker.rs +++ b/crates/spyglass/src/task/worker.rs @@ -195,7 +195,7 @@ pub async fn process_crawl( &state.db, &to_enqueue, &lenses, - &state.user_settings, + &state.user_settings.load_full(), &EnqueueSettings { tags: task_tags.clone(), ..Default::default() @@ -226,7 +226,7 @@ pub async fn process_crawl( #[tracing::instrument(skip(state))] pub async fn handle_fetch(state: AppState, task: CrawlTask) -> FetchResult { - let crawler = Crawler::new(state.user_settings.domain_crawl_limit.value()); + let crawler = Crawler::new(state.user_settings.load().domain_crawl_limit.value()); let result = crawler.fetch_by_job(&state, task.id, true).await; match result { diff --git a/crates/tauri/Cargo.toml b/crates/tauri/Cargo.toml index 21617cb3c..cd68da92f 100644 --- a/crates/tauri/Cargo.toml +++ b/crates/tauri/Cargo.toml @@ -17,6 +17,7 @@ tauri-build = { version = "1.0.0", features = [] } [dependencies] anyhow = "1.0" auto-launch = "0.4.0" +diff-struct = "0.5.1" jsonrpsee = { version = "0.16.2", features = ["http-client"] } log = "0.4" migration = { path = "../migrations" } diff --git a/crates/tauri/src/cmd.rs b/crates/tauri/src/cmd.rs index 7696c4d38..321a0047d 100644 --- a/crates/tauri/src/cmd.rs +++ b/crates/tauri/src/cmd.rs @@ -10,7 +10,7 @@ use tauri::{ClipboardManager, Manager}; use crate::window::show_discover_window; use crate::PauseState; use crate::{open_folder, rpc, window}; -use shared::config::Config; +use shared::config::{Config, UserSettings}; use shared::{event::ClientEvent, request, response}; use spyglass_rpc::RpcClient; @@ -283,16 +283,6 @@ pub async fn toggle_plugin(window: tauri::Window, name: &str, enabled: bool) -> Ok(()) } -#[tauri::command] -pub async fn toggle_filesystem(window: tauri::Window, enabled: bool) -> Result<(), String> { - if let Some(rpc) = window.app_handle().try_state::() { - let rpc = rpc.lock().await; - let _ = rpc.client.toggle_filesystem(enabled).await; - } - - Ok(()) -} - #[tauri::command] pub async fn update_and_restart(window: tauri::Window) -> Result<(), String> { let app_handle = window.app_handle(); @@ -360,11 +350,13 @@ pub async fn wizard_finished( ) -> Result<(), String> { let mut current_settings = config.user_settings.clone(); current_settings.run_wizard = true; + current_settings + .filesystem_settings + .enable_filesystem_scanning = toggle_file_indexer; - let _ = Config::save_user_settings(¤t_settings); - - // Turn on/off the filesystem indexer - let _ = toggle_filesystem(win.clone(), toggle_file_indexer).await; + if let Err(error) = update_user_settings(win.clone(), ¤t_settings).await { + log::error!("Error saving initial settings {:?}", error); + } // close wizard window if let Some(window) = win.get_window(crate::constants::WIZARD_WIN_NAME) { @@ -392,3 +384,36 @@ pub async fn default_indices(win: tauri::Window) -> Result Result { + if let Some(rpc) = win.app_handle().try_state::() { + let rpc = rpc.lock().await; + return match rpc.client.update_user_settings(settings.clone()).await { + Ok(settings) => { + return Ok(settings); + } + Err(error) => Err(error.to_string()), + }; + } + + Err(String::from("Unable to access user settings")) +} + +#[tauri::command] +pub async fn user_settings(win: tauri::Window) -> Result { + if let Some(rpc) = win.app_handle().try_state::() { + let rpc = rpc.lock().await; + return match rpc.client.user_settings().await { + Ok(settings) => { + return Ok(settings); + } + Err(error) => Err(error.to_string()), + }; + } + + Err(String::from("Unable to access user settings")) +} diff --git a/crates/tauri/src/cmd/settings.rs b/crates/tauri/src/cmd/settings.rs index dee29c6ee..7c60aa1cc 100644 --- a/crates/tauri/src/cmd/settings.rs +++ b/crates/tauri/src/cmd/settings.rs @@ -14,9 +14,11 @@ pub async fn save_user_settings( window: tauri::Window, config: State<'_, Config>, settings: HashMap, + restart: bool, ) -> Result<(), HashMap> { let mut current_settings = Config::load_user_settings().unwrap_or_else(|_| config.user_settings.clone()); + let orig_settings = current_settings.clone(); let config_list: Vec<(String, SettingOpts)> = config.user_settings.clone().into(); let setting_configs: HashMap = config_list.into_iter().collect(); @@ -25,7 +27,6 @@ pub async fn save_user_settings( let plugin_configs = config.load_plugin_config(); let mut fields_updated: usize = 0; - // Loop through each updated settings value sent from the front-end and // validate the values. for (key, value) in settings.iter() { @@ -42,6 +43,9 @@ pub async fn save_user_settings( "data_directory" => { current_settings.data_directory = PathBuf::from(val); } + "shortcut" => { + current_settings.shortcut = val; + } "disable_autolaunch" => { current_settings.disable_autolaunch = serde_json::from_str(value).unwrap_or_default(); @@ -121,10 +125,22 @@ pub async fn save_user_settings( // Only save settings if everything is valid. if errors.is_empty() && fields_updated > 0 { - let _ = Config::save_user_settings(¤t_settings); - let app = window.app_handle(); - app.restart(); - Ok(()) + match crate::cmd::update_user_settings(window.clone(), ¤t_settings).await { + Ok(updates) => { + if restart { + let app = window.app_handle(); + app.restart(); + } else { + crate::configuration_updated(window, orig_settings, updates); + } + Ok(()) + } + Err(error) => { + let mut map = HashMap::new(); + map.insert(String::from("all"), error); + Err(map) + } + } } else { Err(errors) } @@ -142,10 +158,12 @@ pub async fn load_action_settings( #[tauri::command] pub async fn load_user_settings( - _: tauri::Window, + window: tauri::Window, config: State<'_, Config>, ) -> Result, String> { - let current_settings = Config::load_user_settings().expect("Unable to read user settings"); + let current_settings = crate::cmd::user_settings(window) + .await + .expect("Unable to read user settings"); let plugin_configs = config.load_plugin_config(); let mut list: Vec<(String, SettingOpts)> = current_settings.clone().into(); diff --git a/crates/tauri/src/main.rs b/crates/tauri/src/main.rs index 05039a06c..1097af08c 100644 --- a/crates/tauri/src/main.rs +++ b/crates/tauri/src/main.rs @@ -15,14 +15,15 @@ use rpc::RpcMutex; use tauri::api::process::current_binary; use tauri::{ AppHandle, Env, GlobalShortcutManager, Manager, PathResolver, RunEvent, SystemTray, - SystemTrayEvent, + SystemTrayEvent, Window, }; use tokio::sync::broadcast; use tokio::time::Duration; use tracing_log::LogTracer; use tracing_subscriber::{fmt, layer::SubscriberExt, EnvFilter}; -use shared::config::Config; +use diff::Diff; +use shared::config::{Config, UserSettings}; use shared::metrics::{Event, Metrics}; use spyglass_rpc::RpcClient; @@ -77,25 +78,7 @@ fn main() -> Result<(), Box> { ))) }; - // Check and register this app to run on boot - let binary = current_binary(&Env::default()); - if let Ok(path) = binary { - // NOTE: See how this works: https://github.com/Teamwork/node-auto-launch#how-does-it-work - if let Ok(auto) = AutoLaunchBuilder::new() - .set_app_name("Spyglass Search") - .set_app_path(&path.display().to_string()) - .set_use_launch_agent(true) - .build() - { - if !config.user_settings.disable_autolaunch && cfg!(not(debug_assertions)) { - if let Ok(false) = auto.is_enabled() { - let _ = auto.enable(); - } - } else if let Ok(true) = auto.is_enabled() { - let _ = auto.disable(); - } - } - } + update_auto_launch(&config.user_settings); let file_appender = tracing_appender::rolling::daily(config.logs_dir(), "client.log"); let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); @@ -185,35 +168,7 @@ fn main() -> Result<(), Box> { config.user_settings.disable_telemetry, )); - // Register global shortcut - let mut shortcuts = app_handle.global_shortcut_manager(); - match shortcuts.is_registered(&config.user_settings.shortcut) { - Ok(is_registered) => { - if !is_registered { - log::info!("Registering {} as shortcut", &config.user_settings.shortcut); - let app_hand = app_handle; - if let Err(e) = - shortcuts.register(&config.user_settings.shortcut, move || { - let window = window::get_searchbar(&app_hand); - window::show_search_bar(&window); - }) - { - window::alert( - &startup_win, - "Error registering global shortcut", - &format!("{e}"), - ); - } - } - } - Err(e) => { - window::alert( - &startup_win, - "Error registering global shortcut", - &format!("{e}"), - ); - } - } + register_global_shortcut(&startup_win, &app_handle, &config.user_settings); Ok(()) }) @@ -307,6 +262,73 @@ fn main() -> Result<(), Box> { Ok(()) } +// Applies updated configuration to the client +pub fn configuration_updated( + window: Window, + old_configuration: UserSettings, + new_configuration: UserSettings, +) { + let diff = old_configuration.diff(&new_configuration); + + if diff.disable_autolaunch.is_some() { + update_auto_launch(&new_configuration); + } + + if diff.shortcut.is_some() { + register_global_shortcut(&window, &window.app_handle(), &new_configuration); + } +} + +// Helper used to update the global shortcut +fn register_global_shortcut(window: &Window, app_handle: &AppHandle, settings: &UserSettings) { + // Register global shortcut + let mut shortcuts = app_handle.global_shortcut_manager(); + if let Err(error) = shortcuts.unregister_all() { + log::info!("Unable to unregister all shortcuts {:?}", error); + } + + match shortcuts.is_registered(&settings.shortcut) { + Ok(is_registered) => { + if !is_registered { + log::info!("Registering {} as shortcut", &settings.shortcut); + let app_hand = app_handle.clone(); + if let Err(e) = shortcuts.register(&settings.shortcut, move || { + let window = window::get_searchbar(&app_hand); + window::show_search_bar(&window); + }) { + window::alert(window, "Error registering global shortcut", &format!("{e}")); + } + } + } + Err(e) => { + window::alert(window, "Error registering global shortcut", &format!("{e}")); + } + } +} + +// Helper method used to update the auto launch configuration +pub fn update_auto_launch(user_settings: &UserSettings) { + // Check and register this app to run on boot + let binary = current_binary(&Env::default()); + if let Ok(path) = binary { + // NOTE: See how this works: https://github.com/Teamwork/node-auto-launch#how-does-it-work + if let Ok(auto) = AutoLaunchBuilder::new() + .set_app_name("Spyglass Search") + .set_app_path(&path.display().to_string()) + .set_use_launch_agent(true) + .build() + { + if !user_settings.disable_autolaunch && cfg!(not(debug_assertions)) { + if let Ok(false) = auto.is_enabled() { + let _ = auto.enable(); + } + } else if let Ok(true) = auto.is_enabled() { + let _ = auto.disable(); + } + } + } +} + async fn pause_crawler(app: AppHandle, menu_id: String) { if let Some(rpc) = app.try_state::() { let pause_state = app.state::>().inner(); From 28b34f190133a91e40458de45dde9ccfce4a5466 Mon Sep 17 00:00:00 2001 From: Andrew Huynh Date: Tue, 14 Mar 2023 20:54:26 -0700 Subject: [PATCH 13/19] feature: friendly 0 results screen & feedback request (#377) * fixtures: open up URLs & add a trigger word to show a blank result * detect when no results are found & show a feedback button * point user to "Discover Lenses" or "Add Integrations" if they have no docs in their library yet * update css * add navigate cmd to tauri backend to open up settings pages from client * we use the term connection not integration --- crates/client/public/fixtures.js | 18 +++++-- crates/client/public/main.css | 2 +- crates/client/src/components/result.rs | 64 ++++++++++++++++++++++- crates/client/src/pages/search.rs | 71 ++++++++++++++++---------- crates/shared/src/constants.rs | 2 + crates/shared/src/event.rs | 7 +++ crates/tauri/src/cmd.rs | 6 +++ crates/tauri/src/main.rs | 7 +-- crates/tauri/src/window.rs | 2 +- 9 files changed, 140 insertions(+), 39 deletions(-) diff --git a/crates/client/public/fixtures.js b/crates/client/public/fixtures.js index 564ca7ed1..50c4c5135 100644 --- a/crates/client/public/fixtures.js +++ b/crates/client/public/fixtures.js @@ -6,12 +6,18 @@ export let invoke = async (func_name, params) => { console.log(`calling: ${func_name} w/`, params); if (func_name == "search_docs") { + let meta = { + query: params.query, + num_docs: 426552, + wall_time_ms: 1234, + }; + + if (params.query == "blank") { + return { meta, results: [] }; + } + return { - meta: { - query: params.query, - num_docs: 426552, - wall_time_ms: 1234, - }, + meta, results: [ { doc_id: "123", @@ -461,6 +467,8 @@ export let invoke = async (func_name, params) => { "/Applications", ], }; + } else if (func_name == "open_result") { + window.open(params.url); } return []; diff --git a/crates/client/public/main.css b/crates/client/public/main.css index 1e9a597fe..d6014c65d 100644 --- a/crates/client/public/main.css +++ b/crates/client/public/main.css @@ -1 +1 @@ -/*! tailwindcss v3.1.8 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.form-input,.form-multiselect,.form-select,.form-textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}.form-input:focus,.form-multiselect:focus,.form-select:focus,.form-textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}.form-input::-moz-placeholder,.form-textarea::-moz-placeholder{color:#6b7280;opacity:1}.form-input::placeholder,.form-textarea::placeholder{color:#6b7280;opacity:1}.form-input::-webkit-datetime-edit-fields-wrapper{padding:0}.form-input::-webkit-date-and-time-value{min-height:1.5em}.form-input::-webkit-datetime-edit,.form-input::-webkit-datetime-edit-day-field,.form-input::-webkit-datetime-edit-hour-field,.form-input::-webkit-datetime-edit-meridiem-field,.form-input::-webkit-datetime-edit-millisecond-field,.form-input::-webkit-datetime-edit-minute-field,.form-input::-webkit-datetime-edit-month-field,.form-input::-webkit-datetime-edit-second-field,.form-input::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.top-0{top:0}.bottom-0{bottom:0}.right-0{right:0}.bottom-8{bottom:2rem}.left-0{left:0}.left-1{left:.25rem}.top-1{top:.25rem}.z-50{z-index:50}.z-20{z-index:20}.z-40{z-index:40}.float-right{float:right}.m-auto{margin:auto}.mx-auto{margin-left:auto;margin-right:auto}.mx-3{margin-left:.75rem;margin-right:.75rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-auto{margin-top:auto;margin-bottom:auto}.my-4{margin-top:1rem;margin-bottom:1rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.ml-auto{margin-left:auto}.mt-2{margin-top:.5rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.-ml-16{margin-left:-4rem}.-mt-2{margin-top:-.5rem}.mb-6{margin-bottom:1.5rem}.mb-2{margin-bottom:.5rem}.mr-2{margin-right:.5rem}.mb-4{margin-bottom:1rem}.mt-4{margin-top:1rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-1{margin-left:.25rem}.mb-8{margin-bottom:2rem}.mr-1{margin-right:.25rem}.mt-auto{margin-top:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-4{height:1rem}.h-5{height:1.25rem}.h-3\.5{height:.875rem}.h-3{height:.75rem}.h-1{height:.25rem}.h-8{height:2rem}.h-12{height:3rem}.h-32{height:8rem}.h-screen{height:100vh}.h-10{height:2.5rem}.h-6{height:1.5rem}.h-16{height:4rem}.h-48{height:12rem}.h-20{height:5rem}.h-2{height:.5rem}.h-full{height:100%}.h-64{height:16rem}.h-40{height:10rem}.h-9{height:2.25rem}.h-\[128px\]{height:128px}.max-h-10{max-height:2.5rem}.max-h-screen{max-height:100vh}.max-h-\[640px\]{max-height:640px}.w-4{width:1rem}.w-5{width:1.25rem}.w-3\.5{width:.875rem}.w-3{width:.75rem}.w-full{width:100%}.w-32{width:8rem}.w-8{width:2rem}.w-12{width:3rem}.w-\[30rem\]{width:30rem}.w-1\/2{width:50%}.w-48{width:12rem}.w-auto{width:auto}.w-6{width:1.5rem}.w-16{width:4rem}.w-40{width:10rem}.w-20{width:5rem}.w-2{width:.5rem}.w-14{width:3.5rem}.w-\[196px\]{width:196px}.w-\[300px\]{width:300px}.w-9{width:2.25rem}.min-w-5{min-width:1.25rem}.flex-none{flex:none}.flex-auto{flex:1 1 auto}.flex-1{flex:1 1 0%}.grow{flex-grow:1}@-webkit-keyframes spin{to{transform:rotate(1turn)}}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}.animate-pulse{-webkit-animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite;animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.scroll-mt-2{scroll-margin-top:.5rem}.list-none{list-style-type:none}.list-disc{list-style-type:disc}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.flex-nowrap{flex-wrap:nowrap}.place-content-center{place-content:center}.place-content-start{place-content:start}.place-content-end{place-content:end}.place-items-center{place-items:center}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-4{gap:1rem}.gap-2{gap:.5rem}.gap-8{gap:2rem}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded-md{border-radius:.375rem}.rounded-3xl{border-radius:1.5rem}.rounded-lg{border-radius:.5rem}.rounded{border-radius:.25rem}.rounded-xl{border-radius:.75rem}.rounded-full{border-radius:9999px}.rounded-tl-lg{border-top-left-radius:.5rem}.border{border-width:1px}.border-b-2{border-bottom-width:2px}.border-t-2{border-top-width:2px}.border-l-2{border-left-width:2px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-neutral-600{--tw-border-opacity:1;border-color:rgb(82 82 82/var(--tw-border-opacity))}.border-red-700{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity))}.border-stone-900{--tw-border-opacity:1;border-color:rgb(28 25 23/var(--tw-border-opacity))}.border-green-500{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-neutral-500{--tw-border-opacity:1;border-color:rgb(115 115 115/var(--tw-border-opacity))}.border-neutral-900{--tw-border-opacity:1;border-color:rgb(23 23 23/var(--tw-border-opacity))}.border-neutral-800{--tw-border-opacity:1;border-color:rgb(38 38 38/var(--tw-border-opacity))}.border-neutral-700{--tw-border-opacity:1;border-color:rgb(64 64 64/var(--tw-border-opacity))}.border-stone-800{--tw-border-opacity:1;border-color:rgb(41 37 36/var(--tw-border-opacity))}.bg-transparent{background-color:initial}.bg-green-700{--tw-bg-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity))}.bg-stone-800{--tw-bg-opacity:1;background-color:rgb(41 37 36/var(--tw-bg-opacity))}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity))}.bg-neutral-700{--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.bg-cyan-500{--tw-bg-opacity:1;background-color:rgb(6 182 212/var(--tw-bg-opacity))}.bg-cyan-700{--tw-bg-opacity:1;background-color:rgb(14 116 144/var(--tw-bg-opacity))}.bg-neutral-800{--tw-bg-opacity:1;background-color:rgb(38 38 38/var(--tw-bg-opacity))}.bg-neutral-400{--tw-bg-opacity:1;background-color:rgb(163 163 163/var(--tw-bg-opacity))}.bg-cyan-900{--tw-bg-opacity:1;background-color:rgb(22 78 99/var(--tw-bg-opacity))}.bg-neutral-900{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity))}.bg-stone-900{--tw-bg-opacity:1;background-color:rgb(28 25 23/var(--tw-bg-opacity))}.bg-stone-700{--tw-bg-opacity:1;background-color:rgb(68 64 60/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.stroke-slate-400{stroke:#94a3b8}.p-4{padding:1rem}.p-0\.5{padding:.125rem}.p-0{padding:0}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-16{padding:4rem}.p-1\.5{padding:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-0{padding-top:0;padding-bottom:0}.px-8{padding-left:2rem;padding-right:2rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-12{padding-top:3rem;padding-bottom:3rem}.pl-1{padding-left:.25rem}.pb-1{padding-bottom:.25rem}.pl-2{padding-left:.5rem}.pr-2{padding-right:.5rem}.pl-6{padding-left:1.5rem}.pb-8{padding-bottom:2rem}.pb-2{padding-bottom:.5rem}.pt-4{padding-top:1rem}.pl-3{padding-left:.75rem}.pt-8{padding-top:2rem}.pb-16{padding-bottom:4rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-xs{font-size:.625rem}.text-sm{font-size:.75rem}.text-base{font-size:.875rem}.text-lg{font-size:1rem}.text-xl{font-size:1.125rem}.text-4xl{font-size:1.875rem}.text-\[8px\]{font-size:8px}.text-2xl{font-size:1.25rem}.text-5xl{font-size:2rem}.font-semibold{font-weight:600}.font-medium{font-weight:500}.font-bold{font-weight:700}.uppercase{text-transform:uppercase}.leading-5{line-height:1.25rem}.leading-relaxed{line-height:1.625}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-neutral-600{--tw-text-opacity:1;color:rgb(82 82 82/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-stone-400{--tw-text-opacity:1;color:rgb(168 162 158/var(--tw-text-opacity))}.text-neutral-400{--tw-text-opacity:1;color:rgb(163 163 163/var(--tw-text-opacity))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-cyan-500{--tw-text-opacity:1;color:rgb(6 182 212/var(--tw-text-opacity))}.text-cyan-600{--tw-text-opacity:1;color:rgb(8 145 178/var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgb(234 179 8/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.text-neutral-500{--tw-text-opacity:1;color:rgb(115 115 115/var(--tw-text-opacity))}.text-stone-500{--tw-text-opacity:1;color:rgb(120 113 108/var(--tw-text-opacity))}.placeholder-neutral-400::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(163 163 163/var(--tw-placeholder-opacity))}.placeholder-neutral-400::placeholder{--tw-placeholder-opacity:1;color:rgb(163 163 163/var(--tw-placeholder-opacity))}.caret-white{caret-color:#fff}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-cyan-500\/50{--tw-shadow-color:rgba(6,182,212,.5);--tw-shadow:var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}input:checked~.dot{transform:translateX(100%);background-color:#48bb78}*{scrollbar-width:thin;scrollbar-color:#404040 #171717}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:#171717}::-webkit-scrollbar-thumb{background:#404040;border-radius:100vh;border:2px solid #171717}::-webkit-scrollbar-thumb:hover{background:#164e63}mark{color:#fff;background-color:rgb(14 116 144/var(--tw-bg-opacity));padding-left:.125rem;padding-right:.125rem}.hover\:border-green-500:hover{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.hover\:bg-neutral-600:hover{--tw-bg-opacity:1;background-color:rgb(82 82 82/var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgb(185 28 28/var(--tw-bg-opacity))}.hover\:bg-green-900:hover{--tw-bg-opacity:1;background-color:rgb(20 83 45/var(--tw-bg-opacity))}.hover\:bg-cyan-600:hover{--tw-bg-opacity:1;background-color:rgb(8 145 178/var(--tw-bg-opacity))}.hover\:bg-stone-700:hover{--tw-bg-opacity:1;background-color:rgb(68 64 60/var(--tw-bg-opacity))}.hover\:bg-neutral-700:hover{--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.hover\:bg-stone-800:hover{--tw-bg-opacity:1;background-color:rgb(41 37 36/var(--tw-bg-opacity))}.hover\:text-red-600:hover{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:bg-neutral-700:active{--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.active\:bg-cyan-900:active{--tw-bg-opacity:1;background-color:rgb(22 78 99/var(--tw-bg-opacity))}.active\:outline-none:active{outline:2px solid transparent;outline-offset:2px}.group:hover .group-hover\:block{display:block}.group:hover .group-hover\:fill-red-400{fill:#f87171}.group:hover .group-hover\:stroke-white{stroke:#fff}.group:hover .group-hover\:text-neutral-400{--tw-text-opacity:1;color:rgb(163 163 163/var(--tw-text-opacity))} \ No newline at end of file +/*! tailwindcss v3.1.8 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.form-input,.form-multiselect,.form-select,.form-textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}.form-input:focus,.form-multiselect:focus,.form-select:focus,.form-textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}.form-input::-moz-placeholder,.form-textarea::-moz-placeholder{color:#6b7280;opacity:1}.form-input::placeholder,.form-textarea::placeholder{color:#6b7280;opacity:1}.form-input::-webkit-datetime-edit-fields-wrapper{padding:0}.form-input::-webkit-date-and-time-value{min-height:1.5em}.form-input::-webkit-datetime-edit,.form-input::-webkit-datetime-edit-day-field,.form-input::-webkit-datetime-edit-hour-field,.form-input::-webkit-datetime-edit-meridiem-field,.form-input::-webkit-datetime-edit-millisecond-field,.form-input::-webkit-datetime-edit-minute-field,.form-input::-webkit-datetime-edit-month-field,.form-input::-webkit-datetime-edit-second-field,.form-input::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.top-0{top:0}.bottom-0{bottom:0}.right-0{right:0}.bottom-8{bottom:2rem}.left-0{left:0}.left-1{left:.25rem}.top-1{top:.25rem}.z-50{z-index:50}.z-20{z-index:20}.z-40{z-index:40}.float-right{float:right}.m-auto{margin:auto}.mx-auto{margin-left:auto;margin-right:auto}.mx-3{margin-left:.75rem;margin-right:.75rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-auto{margin-top:auto;margin-bottom:auto}.my-4{margin-top:1rem;margin-bottom:1rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.ml-auto{margin-left:auto}.mt-2{margin-top:.5rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.-ml-16{margin-left:-4rem}.-mt-2{margin-top:-.5rem}.mb-6{margin-bottom:1.5rem}.mb-2{margin-bottom:.5rem}.mr-2{margin-right:.5rem}.mb-4{margin-bottom:1rem}.mt-4{margin-top:1rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-1{margin-left:.25rem}.mb-8{margin-bottom:2rem}.mr-1{margin-right:.25rem}.mt-auto{margin-top:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-4{height:1rem}.h-5{height:1.25rem}.h-3\.5{height:.875rem}.h-3{height:.75rem}.h-1{height:.25rem}.h-8{height:2rem}.h-12{height:3rem}.h-32{height:8rem}.h-screen{height:100vh}.h-10{height:2.5rem}.h-6{height:1.5rem}.h-16{height:4rem}.h-48{height:12rem}.h-20{height:5rem}.h-2{height:.5rem}.h-full{height:100%}.h-64{height:16rem}.h-40{height:10rem}.h-9{height:2.25rem}.h-\[128px\]{height:128px}.max-h-10{max-height:2.5rem}.max-h-screen{max-height:100vh}.max-h-\[640px\]{max-height:640px}.w-4{width:1rem}.w-5{width:1.25rem}.w-3\.5{width:.875rem}.w-3{width:.75rem}.w-full{width:100%}.w-32{width:8rem}.w-8{width:2rem}.w-12{width:3rem}.w-\[30rem\]{width:30rem}.w-1\/2{width:50%}.w-48{width:12rem}.w-auto{width:auto}.w-6{width:1.5rem}.w-16{width:4rem}.w-40{width:10rem}.w-20{width:5rem}.w-2{width:.5rem}.w-14{width:3.5rem}.w-\[196px\]{width:196px}.w-\[300px\]{width:300px}.w-9{width:2.25rem}.min-w-5{min-width:1.25rem}.flex-none{flex:none}.flex-auto{flex:1 1 auto}.flex-1{flex:1 1 0%}.grow{flex-grow:1}@-webkit-keyframes spin{to{transform:rotate(1turn)}}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}.animate-pulse{-webkit-animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite;animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.scroll-mt-2{scroll-margin-top:.5rem}.list-none{list-style-type:none}.list-disc{list-style-type:disc}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.flex-nowrap{flex-wrap:nowrap}.place-content-center{place-content:center}.place-content-start{place-content:start}.place-content-end{place-content:end}.place-items-center{place-items:center}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-4{gap:1rem}.gap-2{gap:.5rem}.gap-8{gap:2rem}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded-md{border-radius:.375rem}.rounded-3xl{border-radius:1.5rem}.rounded-lg{border-radius:.5rem}.rounded{border-radius:.25rem}.rounded-xl{border-radius:.75rem}.rounded-full{border-radius:9999px}.rounded-tl-lg{border-top-left-radius:.5rem}.border{border-width:1px}.border-b-2{border-bottom-width:2px}.border-t-2{border-top-width:2px}.border-l-2{border-left-width:2px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-neutral-600{--tw-border-opacity:1;border-color:rgb(82 82 82/var(--tw-border-opacity))}.border-red-700{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity))}.border-stone-900{--tw-border-opacity:1;border-color:rgb(28 25 23/var(--tw-border-opacity))}.border-green-500{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-neutral-500{--tw-border-opacity:1;border-color:rgb(115 115 115/var(--tw-border-opacity))}.border-neutral-900{--tw-border-opacity:1;border-color:rgb(23 23 23/var(--tw-border-opacity))}.border-neutral-800{--tw-border-opacity:1;border-color:rgb(38 38 38/var(--tw-border-opacity))}.border-neutral-700{--tw-border-opacity:1;border-color:rgb(64 64 64/var(--tw-border-opacity))}.border-stone-800{--tw-border-opacity:1;border-color:rgb(41 37 36/var(--tw-border-opacity))}.bg-transparent{background-color:initial}.bg-green-700{--tw-bg-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity))}.bg-stone-800{--tw-bg-opacity:1;background-color:rgb(41 37 36/var(--tw-bg-opacity))}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity))}.bg-neutral-700{--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.bg-cyan-500{--tw-bg-opacity:1;background-color:rgb(6 182 212/var(--tw-bg-opacity))}.bg-cyan-700{--tw-bg-opacity:1;background-color:rgb(14 116 144/var(--tw-bg-opacity))}.bg-neutral-800{--tw-bg-opacity:1;background-color:rgb(38 38 38/var(--tw-bg-opacity))}.bg-neutral-400{--tw-bg-opacity:1;background-color:rgb(163 163 163/var(--tw-bg-opacity))}.bg-cyan-900{--tw-bg-opacity:1;background-color:rgb(22 78 99/var(--tw-bg-opacity))}.bg-neutral-900{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity))}.bg-stone-900{--tw-bg-opacity:1;background-color:rgb(28 25 23/var(--tw-bg-opacity))}.bg-stone-700{--tw-bg-opacity:1;background-color:rgb(68 64 60/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.stroke-slate-400{stroke:#94a3b8}.p-4{padding:1rem}.p-0\.5{padding:.125rem}.p-0{padding:0}.p-2{padding:.5rem}.p-1{padding:.25rem}.p-16{padding:4rem}.p-1\.5{padding:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-0{padding-top:0;padding-bottom:0}.px-8{padding-left:2rem;padding-right:2rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-12{padding-top:3rem;padding-bottom:3rem}.pl-1{padding-left:.25rem}.pb-1{padding-bottom:.25rem}.pl-2{padding-left:.5rem}.pr-2{padding-right:.5rem}.pl-6{padding-left:1.5rem}.pb-8{padding-bottom:2rem}.pb-2{padding-bottom:.5rem}.pt-4{padding-top:1rem}.pl-3{padding-left:.75rem}.pt-8{padding-top:2rem}.pb-16{padding-bottom:4rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-xs{font-size:.625rem}.text-sm{font-size:.75rem}.text-base{font-size:.875rem}.text-lg{font-size:1rem}.text-xl{font-size:1.125rem}.text-4xl{font-size:1.875rem}.text-\[8px\]{font-size:8px}.text-2xl{font-size:1.25rem}.text-5xl{font-size:2rem}.font-semibold{font-weight:600}.font-medium{font-weight:500}.font-bold{font-weight:700}.uppercase{text-transform:uppercase}.leading-5{line-height:1.25rem}.leading-relaxed{line-height:1.625}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-neutral-600{--tw-text-opacity:1;color:rgb(82 82 82/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-stone-400{--tw-text-opacity:1;color:rgb(168 162 158/var(--tw-text-opacity))}.text-neutral-400{--tw-text-opacity:1;color:rgb(163 163 163/var(--tw-text-opacity))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-cyan-500{--tw-text-opacity:1;color:rgb(6 182 212/var(--tw-text-opacity))}.text-cyan-600{--tw-text-opacity:1;color:rgb(8 145 178/var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgb(234 179 8/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.text-neutral-500{--tw-text-opacity:1;color:rgb(115 115 115/var(--tw-text-opacity))}.text-stone-500{--tw-text-opacity:1;color:rgb(120 113 108/var(--tw-text-opacity))}.placeholder-neutral-400::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(163 163 163/var(--tw-placeholder-opacity))}.placeholder-neutral-400::placeholder{--tw-placeholder-opacity:1;color:rgb(163 163 163/var(--tw-placeholder-opacity))}.caret-white{caret-color:#fff}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-cyan-500\/50{--tw-shadow-color:rgba(6,182,212,.5);--tw-shadow:var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}input:checked~.dot{transform:translateX(100%);background-color:#48bb78}*{scrollbar-width:thin;scrollbar-color:#404040 #171717}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:#171717}::-webkit-scrollbar-thumb{background:#404040;border-radius:100vh;border:2px solid #171717}::-webkit-scrollbar-thumb:hover{background:#164e63}mark{color:#fff;background-color:rgb(14 116 144/var(--tw-bg-opacity));padding-left:.125rem;padding-right:.125rem}.hover\:border-green-500:hover{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.hover\:bg-neutral-600:hover{--tw-bg-opacity:1;background-color:rgb(82 82 82/var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgb(185 28 28/var(--tw-bg-opacity))}.hover\:bg-green-900:hover{--tw-bg-opacity:1;background-color:rgb(20 83 45/var(--tw-bg-opacity))}.hover\:bg-cyan-600:hover{--tw-bg-opacity:1;background-color:rgb(8 145 178/var(--tw-bg-opacity))}.hover\:bg-stone-700:hover{--tw-bg-opacity:1;background-color:rgb(68 64 60/var(--tw-bg-opacity))}.hover\:bg-neutral-700:hover{--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.hover\:bg-stone-800:hover{--tw-bg-opacity:1;background-color:rgb(41 37 36/var(--tw-bg-opacity))}.hover\:text-red-600:hover{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:bg-neutral-700:active{--tw-bg-opacity:1;background-color:rgb(64 64 64/var(--tw-bg-opacity))}.active\:bg-cyan-900:active{--tw-bg-opacity:1;background-color:rgb(22 78 99/var(--tw-bg-opacity))}.active\:outline-none:active{outline:2px solid transparent;outline-offset:2px}.group:hover .group-hover\:block{display:block}.group:hover .group-hover\:fill-red-400{fill:#f87171}.group:hover .group-hover\:stroke-white{stroke:#fff}.group:hover .group-hover\:text-neutral-400{--tw-text-opacity:1;color:rgb(163 163 163/var(--tw-text-opacity))} \ No newline at end of file diff --git a/crates/client/src/components/result.rs b/crates/client/src/components/result.rs index fe14ce09d..d665a1872 100644 --- a/crates/client/src/components/result.rs +++ b/crates/client/src/components/result.rs @@ -1,12 +1,15 @@ use js_sys::decode_uri_component; use url::Url; -use yew::prelude::*; +use yew::{platform::spawn_local, prelude::*}; +use yew_router::Routable; use super::{ - icons, + btn, icons, tag::{Tag, TagIcon}, }; +use crate::{pages::Tab, tauri_invoke, Route}; use shared::response::{LensResult, SearchResult}; +use shared::{constants::FEEDBACK_FORM, event}; #[derive(Properties, PartialEq)] pub struct SearchResultProps { @@ -279,3 +282,60 @@ fn shorten_file_path(url: &Url, max_segments: usize, show_file_name: bool) -> Op None } + +#[derive(Properties, PartialEq)] +pub struct FeedbackProps { + pub query: String, + pub num_docs: u32, +} + +#[function_component(FeedbackResult)] +pub fn feedback_result(props: &FeedbackProps) -> Html { + html! { +
+
{"No results found 😭"}
+ {if props.num_docs > 0 { + html! { +
+ {"Help us improve our results."} + + {"Click to send feedback"} + +
+ } + } else { + let discover_cb = Callback::from(move |_| { + spawn_local(async move { + let route = Route::SettingsPage { tab: Tab::Discover }; + let _ = tauri_invoke::( + event::ClientInvoke::Navigate, + event::NavigateParams { page: route.to_path() } + ).await; + }); + }); + + let add_cb = Callback::from(move |_| { + spawn_local(async move { + let route = Route::SettingsPage { tab: Tab::ConnectionsManager }; + let _ = tauri_invoke::( + event::ClientInvoke::Navigate, + event::NavigateParams { page: route.to_path() } + ).await; + }); + }); + + html! { +
+ {"You don't currently have any documents in your library."} + + {"Discover Lenses"} + + + {"Add Connection"} + +
+ } + }} +
+ } +} diff --git a/crates/client/src/pages/search.rs b/crates/client/src/pages/search.rs index ea7594a52..3716695d6 100644 --- a/crates/client/src/pages/search.rs +++ b/crates/client/src/pages/search.rs @@ -19,7 +19,7 @@ use shared::{ use crate::components::user_action_list::{self, ActionsList, DEFAULT_ACTION_LABEL}; use crate::components::{ icons, - result::{LensResultItem, SearchResultItem}, + result::{FeedbackResult, LensResultItem, SearchResultItem}, KeyComponent, SelectedLens, }; use crate::{invoke, listen, resize_window, search_docs, search_lenses, tauri_invoke, utils}; @@ -800,27 +800,38 @@ impl Component for SearchPage { let link = ctx.link(); let results = match self.result_display { - ResultDisplay::None => html! { }, + ResultDisplay::None => html! {}, ResultDisplay::Docs => { - self.docs_results - .iter() - .enumerate() - .map(|(idx, res)| { - let is_selected = idx == self.selected_idx && !self.action_menu_button_selected; - let open_msg = Msg::OpenResult(res.to_owned()); - html! { - - } - }) - .collect::() - }, + // If we are unable to find anything, let's attempt to get some + // feedback from the user. + if self.docs_results.is_empty() { + let num_docs = &self.search_meta.as_ref().map_or_else(|| 0, |x| x.num_docs); + html! { } + } else { + let html = self + .docs_results + .iter() + .enumerate() + .map(|(idx, res)| { + let is_selected = + idx == self.selected_idx && !self.action_menu_button_selected; + let open_msg = Msg::OpenResult(res.to_owned()); + html! { + + } + }) + .collect::(); + + html! {
{html}
} + } + } ResultDisplay::Lens => { - self.lens_results + let html = self.lens_results .iter() .enumerate() .map(|(idx, res)| { @@ -829,7 +840,9 @@ impl Component for SearchPage { } }) - .collect::() + .collect::(); + + html! {
{html}
} } }; @@ -959,13 +972,17 @@ impl Component for SearchPage { tabindex="-1" />
- { if !self.docs_results.is_empty() || !self.lens_results.is_empty() { - html! { -
-
{results}
-
+ { + if self.result_display != ResultDisplay::None { + html! { +
+
{results}
+
+ } + } else { + html! { } } - } else { html!{} }} + }
{search_meta} diff --git a/crates/shared/src/constants.rs b/crates/shared/src/constants.rs index 4e97c12b9..b914f18ae 100644 --- a/crates/shared/src/constants.rs +++ b/crates/shared/src/constants.rs @@ -12,3 +12,5 @@ pub const FIREFOX_EXT_LINK: &str = "https://addons.mozilla.org/en-US/firefox/add // SOON // pub const OPERA_EXT_LINK: &str = ""; // pub const EDGE_EXT_LINK: &str = ""; + +pub const FEEDBACK_FORM: &str = "https://forms.gle/7UWP8gvhnwBbwF3KA"; diff --git a/crates/shared/src/event.rs b/crates/shared/src/event.rs index 77951e384..d31902768 100644 --- a/crates/shared/src/event.rs +++ b/crates/shared/src/event.rs @@ -79,6 +79,8 @@ pub enum ClientInvoke { UpdateAndRestart, #[strum(serialize = "wizard_finished")] WizardFinished, + #[strum(serialize = "navigate")] + Navigate, } #[derive(Deserialize, Serialize)] @@ -124,3 +126,8 @@ pub struct WizardFinishedParams { #[serde(rename(serialize = "toggleFileIndexer"))] pub toggle_file_indexer: bool, } + +#[derive(Deserialize, Serialize)] +pub struct NavigateParams { + pub page: String, +} diff --git a/crates/tauri/src/cmd.rs b/crates/tauri/src/cmd.rs index 321a0047d..055a13b02 100644 --- a/crates/tauri/src/cmd.rs +++ b/crates/tauri/src/cmd.rs @@ -417,3 +417,9 @@ pub async fn user_settings(win: tauri::Window) -> Result { Err(String::from("Unable to access user settings")) } + +#[tauri::command] +pub async fn navigate(win: tauri::Window, page: String) -> Result<(), String> { + super::window::_show_tab(&win.app_handle(), &page); + Ok(()) +} diff --git a/crates/tauri/src/main.rs b/crates/tauri/src/main.rs index 1097af08c..d8eb4fa32 100644 --- a/crates/tauri/src/main.rs +++ b/crates/tauri/src/main.rs @@ -108,6 +108,7 @@ fn main() -> Result<(), Box> { .invoke_handler(tauri::generate_handler![ cmd::authorize_connection, cmd::choose_folder, + cmd::copy_to_clipboard, cmd::default_indices, cmd::delete_doc, cmd::escape, @@ -115,19 +116,19 @@ fn main() -> Result<(), Box> { cmd::get_shortcut, cmd::list_connections, cmd::list_plugins, - cmd::load_user_settings, cmd::load_action_settings, + cmd::load_user_settings, + cmd::navigate, cmd::network_change, cmd::open_folder_path, cmd::open_lens_folder, cmd::open_plugins_folder, cmd::open_result, - cmd::copy_to_clipboard, cmd::open_settings_folder, cmd::recrawl_domain, cmd::resize_window, - cmd::revoke_connection, cmd::resync_connection, + cmd::revoke_connection, cmd::save_user_settings, cmd::search_docs, cmd::search_lenses, diff --git a/crates/tauri/src/window.rs b/crates/tauri/src/window.rs index 4bf2f201f..656ff9dce 100644 --- a/crates/tauri/src/window.rs +++ b/crates/tauri/src/window.rs @@ -141,7 +141,7 @@ fn show_window(window: &Window) { let _ = window.center(); } -fn _show_tab(app: &AppHandle, tab_url: &str) { +pub fn _show_tab(app: &AppHandle, tab_url: &str) { let window = if let Some(window) = app.get_window(constants::SETTINGS_WIN_NAME) { window } else { From 0d3b748dff5fe787722c0368d346ec1892e073fb Mon Sep 17 00:00:00 2001 From: Andrew Huynh Date: Wed, 15 Mar 2023 19:37:58 -0700 Subject: [PATCH 14/19] feature: notify user when lens install/connection syncs are complete (#378) * prepping for notifs * switching to websocket rpc so we can support pubsub * adding deps needed for pubsub support * basic layout of pubsub * current an AppEvent channel and broadcast when the server is connected so we can handle subscriptions * use AppState builder instead of copy + pasting * add a `publish_event` function to the AppState & publish events when a lens is installed/uninstalled * add event for connection sync/updates * no quotes in a notif & cargo fmt * readd for index path & create dirs --- Cargo.lock | 147 ++++++++++++++++-- crates/spyglass-rpc/Cargo.toml | 1 + crates/spyglass-rpc/src/lib.rs | 20 ++- crates/spyglass/src/api/handler/mod.rs | 12 +- crates/spyglass/src/api/mod.rs | 60 ++++++- crates/spyglass/src/connection/mod.rs | 10 ++ .../spyglass/src/pipeline/cache_pipeline.rs | 8 +- crates/spyglass/src/state.rs | 89 +++++------ crates/spyglass/src/task.rs | 12 +- crates/tauri/Cargo.toml | 8 +- crates/tauri/src/main.rs | 16 +- crates/tauri/src/plugins/lens_updater.rs | 12 +- crates/tauri/src/plugins/mod.rs | 1 + crates/tauri/src/plugins/notify.rs | 109 +++++++++++++ crates/tauri/src/plugins/startup.rs | 9 +- crates/tauri/src/rpc.rs | 16 +- crates/tauri/src/window.rs | 1 - 17 files changed, 429 insertions(+), 102 deletions(-) create mode 100644 crates/tauri/src/plugins/notify.rs diff --git a/Cargo.lock b/Cargo.lock index 4927ed475..fbefe8afd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1756,6 +1756,18 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "findshlibs" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" +dependencies = [ + "cc", + "lazy_static", + "libc", + "winapi", +] + [[package]] name = "fix-path-env" version = "0.0.0" @@ -5889,10 +5901,28 @@ dependencies = [ "httpdate", "native-tls", "reqwest", - "sentry-backtrace", - "sentry-contexts", - "sentry-core", - "sentry-panic", + "sentry-backtrace 0.29.2", + "sentry-contexts 0.29.2", + "sentry-core 0.29.2", + "sentry-panic 0.29.2", + "tokio", + "ureq", +] + +[[package]] +name = "sentry" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5ce6d3512e2617c209ec1e86b0ca2fea06454cd34653c91092bf0f3ec41f8e3" +dependencies = [ + "httpdate", + "native-tls", + "reqwest", + "sentry-backtrace 0.30.0", + "sentry-contexts 0.30.0", + "sentry-core 0.30.0", + "sentry-debug-images", + "sentry-panic 0.30.0", "tokio", "ureq", ] @@ -5906,7 +5936,19 @@ dependencies = [ "backtrace", "once_cell", "regex", - "sentry-core", + "sentry-core 0.29.2", +] + +[[package]] +name = "sentry-backtrace" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7fe408d4d1f8de188a9309916e02e129cbe51ca19e55badea5a64899399b1a" +dependencies = [ + "backtrace", + "once_cell", + "regex", + "sentry-core 0.30.0", ] [[package]] @@ -5919,7 +5961,21 @@ dependencies = [ "libc", "os_info", "rustc_version 0.4.0", - "sentry-core", + "sentry-core 0.29.2", + "uname", +] + +[[package]] +name = "sentry-contexts" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5695096a059a89973ec541062d331ff4c9aeef9c2951416c894f0fff76340e7d" +dependencies = [ + "hostname", + "libc", + "os_info", + "rustc_version 0.4.0", + "sentry-core 0.30.0", "uname", ] @@ -5931,19 +5987,53 @@ checksum = "fc43eb7e4e3a444151a0fe8a0e9ce60eabd905dae33d66e257fa26f1b509c1bd" dependencies = [ "once_cell", "rand 0.8.5", - "sentry-types", + "sentry-types 0.29.2", "serde", "serde_json", ] +[[package]] +name = "sentry-core" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b22828bfd118a7b660cf7a155002a494755c0424cebb7061e4743ecde9c7dbc" +dependencies = [ + "once_cell", + "rand 0.8.5", + "sentry-types 0.30.0", + "serde", + "serde_json", +] + +[[package]] +name = "sentry-debug-images" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9164d44a2929b1b7670afd7e87552514b70d3ae672ca52884639373d912a3d" +dependencies = [ + "findshlibs", + "once_cell", + "sentry-core 0.30.0", +] + [[package]] name = "sentry-panic" version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccab4fab11e3e63c45f4524bee2e75cde39cdf164cb0b0cbe6ccd1948ceddf66" dependencies = [ - "sentry-backtrace", - "sentry-core", + "sentry-backtrace 0.29.2", + "sentry-core 0.29.2", +] + +[[package]] +name = "sentry-panic" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4ced2a7a8c14899d58eec402d946f69d5ed26a3fc363a7e8b1e5cb88473a01" +dependencies = [ + "sentry-backtrace 0.30.0", + "sentry-core 0.30.0", ] [[package]] @@ -5952,7 +6042,18 @@ version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46ebf58c2bd1d97152c316ff170ee736c59a16967738aeb8ed0d79b80e3713ae" dependencies = [ - "sentry-core", + "sentry-core 0.29.2", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "sentry-tracing" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4eda5496b64975306ce37b7ccdc5f264fd1da25c1d5aac324b460edab29ded" +dependencies = [ + "sentry-core 0.30.0", "tracing-core", "tracing-subscriber", ] @@ -5974,6 +6075,23 @@ dependencies = [ "uuid 1.2.2", ] +[[package]] +name = "sentry-types" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360ee3270f7a4a1eee6c667f7d38360b995431598a73b740dfe420da548d9cc9" +dependencies = [ + "debugid", + "getrandom 0.2.8", + "hex", + "serde", + "serde_json", + "thiserror", + "time 0.3.17", + "url", + "uuid 1.2.2", +] + [[package]] name = "serde" version = "1.0.152" @@ -6389,8 +6507,8 @@ dependencies = [ "reqwest", "ron", "rusqlite", - "sentry", - "sentry-tracing", + "sentry 0.29.2", + "sentry-tracing 0.29.2", "serde", "serde_json", "sha2 0.10.6", @@ -6434,8 +6552,8 @@ dependencies = [ "open", "reqwest", "ron", - "sentry", - "sentry-tracing", + "sentry 0.30.0", + "sentry-tracing 0.30.0", "serde", "serde_json", "shared", @@ -6563,6 +6681,7 @@ name = "spyglass-rpc" version = "0.1.0" dependencies = [ "jsonrpsee", + "serde", "shared", ] diff --git a/crates/spyglass-rpc/Cargo.toml b/crates/spyglass-rpc/Cargo.toml index 0758c95db..79cdc1787 100644 --- a/crates/spyglass-rpc/Cargo.toml +++ b/crates/spyglass-rpc/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] # We only need the macros functionality for the shared library +serde = "1.0" jsonrpsee = { version = "0.16.2", features = ["full"] } shared = { path = "../shared" } diff --git a/crates/spyglass-rpc/src/lib.rs b/crates/spyglass-rpc/src/lib.rs index 7b75179e3..b2b301e67 100644 --- a/crates/spyglass-rpc/src/lib.rs +++ b/crates/spyglass-rpc/src/lib.rs @@ -1,13 +1,26 @@ use jsonrpsee::core::Error; use jsonrpsee::proc_macros::rpc; +use serde::{Deserialize, Serialize}; use shared::config::UserSettings; -use std::collections::HashMap; - use shared::request::{BatchDocumentRequest, RawDocumentRequest, SearchLensesParam, SearchParam}; use shared::response::{ AppStatus, DefaultIndices, LensResult, LibraryStats, ListConnectionResult, PluginResult, SearchLensesResp, SearchResults, }; +use std::collections::HashMap; + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] +pub enum RpcEventType { + ConnectionSyncFinished, + LensUninstalled, + LensInstalled, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RpcEvent { + pub event_type: RpcEventType, + pub payload: String, +} /// Rpc trait #[rpc(server, client, namespace = "spyglass")] @@ -92,4 +105,7 @@ pub trait Rpc { #[method(name = "uninstall_lens")] async fn uninstall_lens(&self, name: String) -> Result<(), Error>; + + #[subscription(name = "subscribe_events", item = RpcEvent)] + fn subscribe_events(&self, events: Vec); } diff --git a/crates/spyglass/src/api/handler/mod.rs b/crates/spyglass/src/api/handler/mod.rs index 436573066..0387eb943 100644 --- a/crates/spyglass/src/api/handler/mod.rs +++ b/crates/spyglass/src/api/handler/mod.rs @@ -1,3 +1,4 @@ +use super::response; use anyhow::anyhow; use directories::UserDirs; use entities::get_library_stats; @@ -27,14 +28,13 @@ use shared::response::{ AppStatus, DefaultIndices, InstallStatus, LensResult, LibraryStats, ListConnectionResult, PluginResult, SupportedConnection, UserConnection, }; +use spyglass_rpc::{RpcEvent, RpcEventType}; use std::collections::HashMap; use std::path::PathBuf; use std::str::FromStr; use tracing::instrument; use url::Url; -use super::response; - pub mod search; pub async fn add_document_batch(state: &AppState, req: &BatchDocumentRequest) -> Result<(), Error> { @@ -603,6 +603,14 @@ pub async fn uninstall_lens(state: AppState, config: &Config, name: &str) -> Res if let Some((_, config)) = config { let _ = bootstrap_queue::dequeue(&state.db, &config.name).await; } + + state + .publish_event(&RpcEvent { + event_type: RpcEventType::LensUninstalled, + payload: format!("{} lens uninstalled", name), + }) + .await; + Ok(()) } diff --git a/crates/spyglass/src/api/mod.rs b/crates/spyglass/src/api/mod.rs index 6ee7037dd..a3ad91fda 100644 --- a/crates/spyglass/src/api/mod.rs +++ b/crates/spyglass/src/api/mod.rs @@ -3,14 +3,16 @@ use entities::models::indexed_document; use entities::sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter}; use jsonrpsee::core::{async_trait, Error}; use jsonrpsee::server::{ServerBuilder, ServerHandle}; +use jsonrpsee::types::{SubscriptionEmptyError, SubscriptionResult}; +use jsonrpsee::SubscriptionSink; use libspyglass::search::{self, Searcher}; use libspyglass::state::AppState; use libspyglass::task::{CollectTask, ManagerCommand}; use shared::config::{Config, UserSettings}; use shared::request::{BatchDocumentRequest, RawDocumentRequest, SearchLensesParam, SearchParam}; use shared::response::{self as resp, DefaultIndices, LibraryStats}; -use spyglass_rpc::RpcServer; -use std::collections::HashMap; +use spyglass_rpc::{RpcEventType, RpcServer}; +use std::collections::{HashMap, HashSet}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; mod handler; @@ -24,7 +26,7 @@ pub struct SpyglassRpc { #[async_trait] impl RpcServer for SpyglassRpc { fn protocol_version(&self) -> Result { - Ok("0.1.1".into()) + Ok("0.1.2".into()) } async fn add_raw_document(&self, req: RawDocumentRequest) -> Result<(), Error> { @@ -193,6 +195,55 @@ impl RpcServer for SpyglassRpc { async fn user_settings(&self) -> Result { handler::user_settings(&self.state).await } + + fn subscribe_events( + &self, + mut sink: SubscriptionSink, + events: Vec, + ) -> SubscriptionResult { + let res = sink.accept(); + if res.is_err() { + log::warn!("Unable to accept subscription: {:?}", res); + return Err(SubscriptionEmptyError); + } + + // Spawn a new task that listens for events in the channel and sends them out + let rpc_event_channel = self.state.rpc_events.clone(); + let shutdown_cmd_tx = self.state.shutdown_cmd_tx.clone(); + tokio::spawn(async move { + let mut receiver = rpc_event_channel + .lock() + .expect("rpc_events held by another thread") + .subscribe(); + let mut shutdown = shutdown_cmd_tx.lock().await.subscribe(); + + let events: HashSet = events.clone().into_iter().collect(); + log::debug!("SUBSCRIBED TO: {:?}", events); + loop { + tokio::select! { + _ = shutdown.recv() => { + return; + } + res = receiver.recv() => { + match res { + Ok(event) => { + if events.contains(&event.event_type) { + if let Err(err) = sink.send(&event) { + log::warn!("unable to send to sub: {err}"); + } + } + }, + Err(err) => { + log::warn!("eror recev: {:?}", err); + } + } + } + } + } + }); + + Ok(()) + } } pub async fn start_api_server( @@ -204,9 +255,8 @@ pub async fn start_api_server( state.user_settings.load_full().port, ); let server = ServerBuilder::default().build(server_addr).await?; - let rpc_module = SpyglassRpc { - state, + state: state.clone(), config: config.clone(), }; let addr = server.local_addr()?; diff --git a/crates/spyglass/src/connection/mod.rs b/crates/spyglass/src/connection/mod.rs index 2473b9266..e6a998664 100644 --- a/crates/spyglass/src/connection/mod.rs +++ b/crates/spyglass/src/connection/mod.rs @@ -131,6 +131,16 @@ async fn handle_sync_credentials( }); } +pub fn api_id_to_label(api_id: &str) -> String { + match api_id { + "calendar.google.com" => gcal::TITLE.to_string(), + "drive.google.com" => gdrive::TITLE.to_string(), + "api.github.com" => github::TITLE.to_string(), + "oauth.reddit.com" => reddit::TITLE.to_string(), + _ => "Unknown".into(), + } +} + /// Load a connection for sync/crawls pub async fn load_connection( state: &AppState, diff --git a/crates/spyglass/src/pipeline/cache_pipeline.rs b/crates/spyglass/src/pipeline/cache_pipeline.rs index 87290dc2e..c1fe97c29 100644 --- a/crates/spyglass/src/pipeline/cache_pipeline.rs +++ b/crates/spyglass/src/pipeline/cache_pipeline.rs @@ -103,5 +103,11 @@ pub async fn process_update(state: AppState, lens: &LensConfig, cache_path: Path // attempt to remove processed cache file let _ = cache::delete_cache(&cache_path); - log::debug!("Processing Cache Took: {:?}", now.elapsed().as_millis()) + log::debug!("Processing Cache Took: {:?}", now.elapsed().as_millis()); + state + .publish_event(&spyglass_rpc::RpcEvent { + event_type: spyglass_rpc::RpcEventType::LensInstalled, + payload: format!("{} lens installed!", lens.label()), + }) + .await; } diff --git a/crates/spyglass/src/state.rs b/crates/spyglass/src/state.rs index 23d47b367..3d36a53fe 100644 --- a/crates/spyglass/src/state.rs +++ b/crates/spyglass/src/state.rs @@ -1,9 +1,9 @@ -use std::sync::Arc; - use arc_swap::ArcSwap; use dashmap::DashMap; use entities::models::create_connection; use entities::sea_orm::DatabaseConnection; +use spyglass_rpc::RpcEvent; +use std::sync::Arc; use tokio::sync::mpsc::error::SendError; use tokio::sync::Mutex; use tokio::sync::{broadcast, mpsc}; @@ -33,6 +33,8 @@ pub struct AppState { pub manager_cmd_tx: Arc>>>, pub shutdown_cmd_tx: Arc>>, pub config_cmd_tx: Arc>>, + // Client events + pub rpc_events: Arc>>, // Pause/unpause worker pool. pub pause_cmd_tx: Arc>>>, // Plugin command/control @@ -49,54 +51,19 @@ impl AppState { .await .expect("Unable to connect to database"); - log::debug!("Loading index from: {:?}", config.index_dir()); - let index_dir = config.index_dir(); - if !index_dir.exists() { - let _ = std::fs::create_dir_all(index_dir); - } - - let index = Searcher::with_index(&IndexPath::LocalPath(config.index_dir())) - .expect("Unable to open index."); - - // TODO: Load from saved preferences - let app_state = DashMap::new(); - app_state.insert("paused".to_string(), "false".to_string()); - - // Convert into dashmap - let lenses = DashMap::new(); - for (key, value) in config.lenses.iter() { - lenses.insert(key.clone(), value.clone()); - } - - let pipelines = DashMap::new(); - for (key, value) in config.pipelines.iter() { - pipelines.insert(key.clone(), value.clone()); - } - - let (shutdown_tx, _) = broadcast::channel::(16); - let (config_tx, _) = broadcast::channel::(16); - - AppState { - db, - app_state: Arc::new(app_state), - user_settings: Arc::new(ArcSwap::from_pointee(config.user_settings.clone())), - metrics: Metrics::new( - &Config::machine_identifier(), - config.user_settings.disable_telemetry, - ), - config: config.clone(), - lenses: Arc::new(lenses), - pipelines: Arc::new(pipelines), - index, - shutdown_cmd_tx: Arc::new(Mutex::new(shutdown_tx)), - config_cmd_tx: Arc::new(Mutex::new(config_tx)), - pause_cmd_tx: Arc::new(Mutex::new(None)), - plugin_cmd_tx: Arc::new(Mutex::new(None)), - pipeline_cmd_tx: Arc::new(Mutex::new(None)), - plugin_manager: Arc::new(Mutex::new(PluginManager::new())), - manager_cmd_tx: Arc::new(Mutex::new(None)), - file_watcher: Arc::new(Mutex::new(None)), - } + AppStateBuilder::new() + .with_db(db) + .with_index(&IndexPath::LocalPath(config.index_dir())) + .with_lenses(&config.lenses.values().cloned().collect()) + .with_pipelines( + &config + .pipelines + .values() + .cloned() + .collect::>(), + ) + .with_user_settings(&config.user_settings) + .build() } pub fn reload_config(&mut self) { @@ -122,6 +89,15 @@ impl AppState { let cmd_tx = cmd_tx.as_ref().expect("Manager channel not open"); cmd_tx.send(task) } + + pub async fn publish_event(&self, event: &RpcEvent) { + log::debug!("publishing event: {:?}", event); + + let rpc_sub = self.rpc_events.lock().unwrap(); + if let Err(err) = rpc_sub.send(event.clone()) { + log::error!("error sending event: {:?}", err); + } + } } #[derive(Default)] @@ -163,6 +139,7 @@ impl AppStateBuilder { let (shutdown_tx, _) = broadcast::channel::(16); let (config_tx, _) = broadcast::channel::(16); + let (rpc_events, _) = broadcast::channel::(10); AppState { app_state: Arc::new(DashMap::new()), @@ -180,6 +157,7 @@ impl AppStateBuilder { pipelines: Arc::new(pipelines), plugin_cmd_tx: Arc::new(Mutex::new(None)), plugin_manager: Arc::new(Mutex::new(PluginManager::new())), + rpc_events: Arc::new(std::sync::Mutex::new(rpc_events)), shutdown_cmd_tx: Arc::new(Mutex::new(shutdown_tx)), config_cmd_tx: Arc::new(Mutex::new(config_tx)), file_watcher: Arc::new(Mutex::new(None)), @@ -201,12 +179,23 @@ impl AppStateBuilder { self } + pub fn with_pipelines(&mut self, pipelines: &[PipelineConfiguration]) -> &mut Self { + self.pipelines = Some(pipelines.to_owned()); + self + } + pub fn with_user_settings(&mut self, user_settings: &UserSettings) -> &mut Self { self.user_settings = Some(user_settings.to_owned()); self } pub fn with_index(&mut self, index: &IndexPath) -> &mut Self { + if let IndexPath::LocalPath(path) = &index { + if !path.exists() { + let _ = std::fs::create_dir_all(path); + } + } + self.index = Some(Searcher::with_index(index).expect("Unable to open index")); self } diff --git a/crates/spyglass/src/task.rs b/crates/spyglass/src/task.rs index 182a6d841..60f4ce75a 100644 --- a/crates/spyglass/src/task.rs +++ b/crates/spyglass/src/task.rs @@ -2,12 +2,13 @@ use entities::models::{bootstrap_queue, connection}; use notify::event::ModifyKind; use notify::{EventKind, RecursiveMode, Watcher}; use shared::config::{Config, LensConfig, UserSettings, UserSettingsDiff}; +use spyglass_rpc::{RpcEvent, RpcEventType}; use std::sync::atomic::Ordering; use std::sync::{atomic::AtomicI32, Arc}; use std::time::Duration; use tokio::sync::{broadcast, mpsc}; -use crate::connection::load_connection; +use crate::connection::{api_id_to_label, load_connection}; use crate::crawler::bootstrap; use crate::filesystem; use crate::search::lens::{load_lenses, read_lenses}; @@ -286,6 +287,15 @@ pub async fn worker_task( Ok(mut conn) => { let last_sync = if is_first_sync { None } else { Some(connection.updated_at) }; conn.as_mut().sync(&state, last_sync).await; + + let api_label = api_id_to_label(&api_id); + let postfix = if is_first_sync { "finished" } else { "updated" }; + let payload = format!("{} ({}) {}", api_label, account, postfix); + + state.publish_event(&RpcEvent { + event_type: RpcEventType::ConnectionSyncFinished, + payload, + }).await; } Err(err) => log::warn!("Unable to sync w/ connection: {account}@{api_id} - {err}"), } diff --git a/crates/tauri/Cargo.toml b/crates/tauri/Cargo.toml index cd68da92f..21009fa23 100644 --- a/crates/tauri/Cargo.toml +++ b/crates/tauri/Cargo.toml @@ -18,22 +18,22 @@ tauri-build = { version = "1.0.0", features = [] } anyhow = "1.0" auto-launch = "0.4.0" diff-struct = "0.5.1" -jsonrpsee = { version = "0.16.2", features = ["http-client"] } +jsonrpsee = { version = "0.16.2", features = ["ws-client"] } log = "0.4" migration = { path = "../migrations" } num-format = "0.4" open = "3" reqwest = { version = "0.11", features = ["json"] } ron = "0.8" -sentry = "0.29.0" -sentry-tracing = "0.29.0" +sentry = "0.30.0" +sentry-tracing = "0.30.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" shared = { path = "../shared", features = ["metrics"] } spyglass-rpc = { path = "../spyglass-rpc" } strum = "0.24" strum_macros = "0.24" -tauri = { version = "1.2.4", features = ["api-all", "clipboard", "devtools", "macos-private-api", "system-tray", "updater"] } +tauri = { version = "1.2.4", features = ["api-all", "clipboard", "devtools", "macos-private-api", "notification", "system-tray", "updater"] } tokio = "1" tokio-retry = "0.3" tracing = "0.1" diff --git a/crates/tauri/src/main.rs b/crates/tauri/src/main.rs index d8eb4fa32..dcfc325c8 100644 --- a/crates/tauri/src/main.rs +++ b/crates/tauri/src/main.rs @@ -56,7 +56,10 @@ const SPYGLASS_LEVEL: &str = "spyglass_app=INFO"; const SPYGLASS_LEVEL: &str = "spyglass_app=DEBUG"; #[derive(Clone)] -pub struct AppShutdown; +pub enum AppEvent { + BackendConnected, + Shutdown, +} type PauseState = AtomicBool; fn main() -> Result<(), Box> { @@ -104,6 +107,7 @@ fn main() -> Result<(), Box> { let _ = fix_path_env::fix(); let app = tauri::Builder::default() .plugin(plugins::lens_updater::init()) + .plugin(plugins::notify::init()) .plugin(plugins::startup::init()) .invoke_handler(tauri::generate_handler![ cmd::authorize_connection, @@ -142,8 +146,8 @@ fn main() -> Result<(), Box> { let app_handle = app.app_handle(); let startup_win = window::show_startup_window(&app_handle); - let (shutdown_tx, _) = broadcast::channel::(1); - app.manage(shutdown_tx); + let (appevent_channel, _) = broadcast::channel::(1); + app.manage(appevent_channel); let config = Config::new(); log::info!("Loading prefs from: {:?}", Config::prefs_dir()); @@ -251,8 +255,8 @@ fn main() -> Result<(), Box> { app.run(|app_handle, e| match e { RunEvent::ExitRequested { .. } => { // Do some cleanup for long running tasks - let shutdown_tx = app_handle.state::>(); - let _ = shutdown_tx.send(AppShutdown); + let shutdown_tx = app_handle.state::>(); + let _ = shutdown_tx.send(AppEvent::Shutdown); } RunEvent::Exit { .. } => { log::info!("😔 bye bye"); @@ -384,7 +388,7 @@ async fn check_version_interval(current_version: String, app_handle: AppHandle) let mut interval = tokio::time::interval(Duration::from_secs(constants::VERSION_CHECK_INTERVAL_S)); - let shutdown_tx = app_handle.state::>(); + let shutdown_tx = app_handle.state::>(); let mut shutdown = shutdown_tx.subscribe(); let metrics = app_handle.try_state::(); diff --git a/crates/tauri/src/plugins/lens_updater.rs b/crates/tauri/src/plugins/lens_updater.rs index 4aef04fb5..9af4100c7 100644 --- a/crates/tauri/src/plugins/lens_updater.rs +++ b/crates/tauri/src/plugins/lens_updater.rs @@ -8,7 +8,7 @@ use tauri::{ use tokio::sync::broadcast; use tokio::time::{self, Duration}; -use crate::{constants, rpc, AppShutdown}; +use crate::{constants, rpc, AppEvent}; use serde_json::Value; use shared::response::{InstallableLens, LensResult}; use shared::{ @@ -38,14 +38,16 @@ pub fn init() -> TauriPlugin { )); let app_handle = app_handle.clone(); - let shutdown_tx = app_handle.state::>(); + let shutdown_tx = app_handle.state::>(); let mut shutdown = shutdown_tx.subscribe(); loop { tokio::select! { - _ = shutdown.recv() => { - log::info!("🛑 Shutting down lens updater"); - return; + event = shutdown.recv() => { + if let Ok(AppEvent::Shutdown) = event { + log::info!("🛑 Shutting down lens updater"); + return; + } }, _ = interval.tick() => { let _ = check_for_lens_updates(&app_handle).await; diff --git a/crates/tauri/src/plugins/mod.rs b/crates/tauri/src/plugins/mod.rs index 64081c1a9..2aea4a24e 100644 --- a/crates/tauri/src/plugins/mod.rs +++ b/crates/tauri/src/plugins/mod.rs @@ -1,2 +1,3 @@ pub mod lens_updater; +pub mod notify; pub mod startup; diff --git a/crates/tauri/src/plugins/notify.rs b/crates/tauri/src/plugins/notify.rs new file mode 100644 index 000000000..a1476d54a --- /dev/null +++ b/crates/tauri/src/plugins/notify.rs @@ -0,0 +1,109 @@ +use crate::window::notify; +use crate::{rpc, AppEvent}; +use anyhow::anyhow; +use jsonrpsee::core::client::Subscription; +use spyglass_rpc::{RpcClient, RpcEvent, RpcEventType}; +use tauri::{ + async_runtime::JoinHandle, + plugin::{Builder, TauriPlugin}, + AppHandle, Manager, RunEvent, Wry, +}; +use tokio::sync::broadcast; + +pub struct NotificationHandler(JoinHandle<()>); + +pub fn init() -> TauriPlugin { + Builder::new("tauri-plugin-notify") + .on_event(|app_handle, event| match event { + RunEvent::Ready => { + log::info!("starting notify plugin"); + let handle = + tauri::async_runtime::spawn(setup_notification_handler(app_handle.clone())); + app_handle.manage(NotificationHandler(handle)); + } + RunEvent::Exit => { + let app_handle = app_handle.clone(); + if let Some(handle) = app_handle.try_state::() { + handle.0.abort(); + } + } + _ => {} + }) + .build() +} + +async fn _subscribe(app: &AppHandle) -> anyhow::Result> { + let rpc = match app.try_state::() { + Some(rpc) => rpc, + None => return Err(anyhow!("Server not available")), + }; + + let rpc = rpc.lock().await; + let sub = rpc + .client + .subscribe_events(vec![ + RpcEventType::ConnectionSyncFinished, + RpcEventType::LensInstalled, + RpcEventType::LensUninstalled, + ]) + .await?; + + Ok(sub) +} + +async fn setup_notification_handler(app: AppHandle) { + let app_events = app.state::>(); + let mut channel = app_events.subscribe(); + + // wait for RPC server connection + log::info!("waiting for backend..."); + match channel.recv().await { + Ok(AppEvent::BackendConnected) => {} + _ => return, + } + + let mut sub = match _subscribe(&app).await { + Ok(sub) => sub, + Err(err) => { + log::warn!("Unable to subscribe to backend events: {err}"); + return; + } + }; + + log::info!("subscribed to events from server!"); + loop { + tokio::select! { + event = channel.recv() => { + if let Ok(AppEvent::Shutdown) = event { + log::info!("🛑 Shutting down notify plugin"); + return; + } + }, + event = sub.next() => { + match event { + Some(Ok(event)) => { + log::debug!("received event: {:?}", event); + let (title, blurb) = match &event.event_type { + RpcEventType::ConnectionSyncFinished => ("Sync Completed", event.payload), + RpcEventType::LensInstalled => ("Lens Installed", event.payload), + RpcEventType::LensUninstalled => ("Lens Removed", event.payload), + }; + + let _ = notify(&app, title, &blurb); + }, + Some(Err(err)) => log::warn!("error listening to event: {:?}", err), + // channel dropped, attempt to reconnect + None => { + sub = match _subscribe(&app).await { + Ok(sub) => sub, + Err(err) => { + log::warn!("Unable to subscribe to backend events: {err}"); + return; + } + }; + } + } + } + } + } +} diff --git a/crates/tauri/src/plugins/startup.rs b/crates/tauri/src/plugins/startup.rs index 93b654f70..002db24f7 100644 --- a/crates/tauri/src/plugins/startup.rs +++ b/crates/tauri/src/plugins/startup.rs @@ -11,7 +11,7 @@ use shared::config::Config; use crate::rpc::SpyglassServerClient; use crate::window::show_wizard_window; -use crate::{rpc::RpcMutex, window, AppShutdown}; +use crate::{rpc::RpcMutex, window, AppEvent}; pub struct StartupProgressText(std::sync::Mutex); impl StartupProgressText { @@ -100,11 +100,14 @@ async fn run_and_check_backend(app_handle: AppHandle) { let rpc_mutex = RpcMutex::new(Mutex::new(rpc)); app_handle.manage(rpc_mutex.clone()); - let shutdown_tx = app_handle.state::>(); + // Let plugins know the server is connected. + let app_events = app_handle.state::>(); + let _ = app_events.send(AppEvent::BackendConnected); + // Watch and restart backend if it goes down tauri::async_runtime::spawn(SpyglassServerClient::daemon_eyes( rpc_mutex, - shutdown_tx.subscribe(), + app_events.subscribe(), )); // Will cancel and clear any interval checks in the client diff --git a/crates/tauri/src/rpc.rs b/crates/tauri/src/rpc.rs index 26e0b4d32..2de8c3040 100644 --- a/crates/tauri/src/rpc.rs +++ b/crates/tauri/src/rpc.rs @@ -1,9 +1,8 @@ +use jsonrpsee::ws_client::{WsClient, WsClientBuilder}; use std::sync::{ atomic::{AtomicU8, Ordering}, Arc, }; - -use jsonrpsee::http_client::{HttpClient, HttpClientBuilder}; use tauri::api::dialog::blocking::message; use tauri::async_runtime::JoinHandle; use tauri::{ @@ -17,12 +16,12 @@ use tokio_retry::Retry; use shared::config::Config; use spyglass_rpc::RpcClient; -use crate::{window, AppShutdown}; +use crate::{window, AppEvent}; pub type RpcMutex = Arc>; pub struct SpyglassServerClient { - pub client: HttpClient, + pub client: WsClient, pub endpoint: String, pub sidecar_handle: Option>, pub restarts: AtomicU8, @@ -30,10 +29,11 @@ pub struct SpyglassServerClient { } /// Build client & attempt a connection to the health check endpoint. -async fn try_connect(endpoint: &str) -> anyhow::Result { - match HttpClientBuilder::default() +async fn try_connect(endpoint: &str) -> anyhow::Result { + match WsClientBuilder::default() .request_timeout(std::time::Duration::from_secs(30)) .build(endpoint) + .await { Ok(client) => { // Wait until we have a connection @@ -55,7 +55,7 @@ async fn try_connect(endpoint: &str) -> anyhow::Result { impl SpyglassServerClient { /// Monitors the health of the backend & recreates it necessary. - pub async fn daemon_eyes(rpc: RpcMutex, mut shutdown: broadcast::Receiver) { + pub async fn daemon_eyes(rpc: RpcMutex, mut shutdown: broadcast::Receiver) { let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5)); loop { tokio::select! { @@ -84,7 +84,7 @@ impl SpyglassServerClient { } pub async fn new(config: &Config, app_handle: &AppHandle) -> Self { - let endpoint = format!("http://127.0.0.1:{}", config.user_settings.port); + let endpoint = format!("ws://127.0.0.1:{}", config.user_settings.port); log::info!("Connecting to backend @ {}", &endpoint); // Only startup & manage sidecar in release mode. diff --git a/crates/tauri/src/window.rs b/crates/tauri/src/window.rs index 656ff9dce..6ca11840b 100644 --- a/crates/tauri/src/window.rs +++ b/crates/tauri/src/window.rs @@ -251,7 +251,6 @@ pub fn alert(window: &Window, title: &str, message: &str) { .show(|_| {}); } -#[allow(dead_code)] pub fn notify(_app: &AppHandle, title: &str, body: &str) -> anyhow::Result<()> { #[cfg(target_os = "macos")] { From 9970521adae5fc0e2100950d18821aebfd614e9c Mon Sep 17 00:00:00 2001 From: Andrew Huynh Date: Thu, 16 Mar 2023 09:55:42 -0700 Subject: [PATCH 15/19] show number of total paths we're walking & don't consider ourselves syncing (#379) unless we have queued paths. --- crates/spyglass/src/api/handler/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/spyglass/src/api/handler/mod.rs b/crates/spyglass/src/api/handler/mod.rs index 0387eb943..9e05d3811 100644 --- a/crates/spyglass/src/api/handler/mod.rs +++ b/crates/spyglass/src/api/handler/mod.rs @@ -401,13 +401,13 @@ async fn build_filesystem_information( let failed: u32 = stats.failed as u32; let total_finished = indexed + failed; - let mut status = InstallStatus::Finished { num_docs: indexed }; - if total_finished < total_paths { + if stats.enqueued > 0 && total_finished < total_paths { let percent = (((indexed * 100) / total_paths) as i32).min(100); let status_msg = format!( - "Processing {} of many", - indexed.to_formatted_string(&Locale::en) + "Processing {} of {}", + indexed.to_formatted_string(&Locale::en), + total_paths.to_formatted_string(&Locale::en), ); let status_msg = match path { Some(path) => format!("{}. Walking {path}.", status_msg), From 56b34a18120df5679458ee17fc434aca65b888a6 Mon Sep 17 00:00:00 2001 From: travolin Date: Thu, 16 Mar 2023 11:41:32 -0700 Subject: [PATCH 16/19] Update system tray menu when shortcut changes (#380) Co-authored-by: Joel Bredeson --- crates/tauri/src/main.rs | 12 +++++++++++- crates/tauri/src/menu.rs | 9 ++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/tauri/src/main.rs b/crates/tauri/src/main.rs index dcfc325c8..37b94ccad 100644 --- a/crates/tauri/src/main.rs +++ b/crates/tauri/src/main.rs @@ -141,7 +141,9 @@ fn main() -> Result<(), Box> { cmd::wizard_finished, ]) .menu(menu::get_app_menu()) - .system_tray(SystemTray::new().with_menu(menu::get_tray_menu(&ctx, &config.clone()))) + .system_tray( + SystemTray::new().with_menu(menu::get_tray_menu(&ctx, &config.user_settings.clone())), + ) .setup(move |app| { let app_handle = app.app_handle(); let startup_win = window::show_startup_window(&app_handle); @@ -281,6 +283,14 @@ pub fn configuration_updated( if diff.shortcut.is_some() { register_global_shortcut(&window, &window.app_handle(), &new_configuration); + let ctx = tauri::generate_context!(); + if let Err(error) = window + .app_handle() + .tray_handle() + .set_menu(menu::get_tray_menu(&ctx, &new_configuration)) + { + log::error!("Error updating system tray {:?}", error); + } } } diff --git a/crates/tauri/src/menu.rs b/crates/tauri/src/menu.rs index 14cddb0ac..7b20e24fe 100644 --- a/crates/tauri/src/menu.rs +++ b/crates/tauri/src/menu.rs @@ -1,4 +1,4 @@ -use shared::config::Config; +use shared::config::UserSettings; use strum_macros::{Display, EnumString}; use tauri::{ utils::assets::EmbeddedAssets, Context, CustomMenuItem, Menu, SystemTrayMenu, @@ -28,9 +28,12 @@ pub enum MenuID { INSTALL_FIREFOX_EXT, } -pub fn get_tray_menu(ctx: &Context, config: &Config) -> SystemTrayMenu { +pub fn get_tray_menu( + ctx: &Context, + user_settings: &UserSettings, +) -> SystemTrayMenu { let show = CustomMenuItem::new(MenuID::SHOW_SEARCHBAR.to_string(), "Show search") - .accelerator(config.user_settings.shortcut.clone()); + .accelerator(user_settings.shortcut.clone()); let pause = CustomMenuItem::new(MenuID::CRAWL_STATUS.to_string(), "⏸ Pause indexing"); let quit = CustomMenuItem::new(MenuID::QUIT.to_string(), "Quit"); From 9f1771e9733258b0bff1fc22af8d93fd0130752b Mon Sep 17 00:00:00 2001 From: travolin Date: Thu, 16 Mar 2023 13:29:39 -0700 Subject: [PATCH 17/19] Update to remove use of context (#381) Co-authored-by: Joel Bredeson --- crates/tauri/src/main.rs | 13 ++++++++----- crates/tauri/src/menu.rs | 10 +++------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/crates/tauri/src/main.rs b/crates/tauri/src/main.rs index 37b94ccad..2b5efe5d3 100644 --- a/crates/tauri/src/main.rs +++ b/crates/tauri/src/main.rs @@ -141,9 +141,10 @@ fn main() -> Result<(), Box> { cmd::wizard_finished, ]) .menu(menu::get_app_menu()) - .system_tray( - SystemTray::new().with_menu(menu::get_tray_menu(&ctx, &config.user_settings.clone())), - ) + .system_tray(SystemTray::new().with_menu(menu::get_tray_menu( + ctx.package_info(), + &config.user_settings.clone(), + ))) .setup(move |app| { let app_handle = app.app_handle(); let startup_win = window::show_startup_window(&app_handle); @@ -283,11 +284,13 @@ pub fn configuration_updated( if diff.shortcut.is_some() { register_global_shortcut(&window, &window.app_handle(), &new_configuration); - let ctx = tauri::generate_context!(); if let Err(error) = window .app_handle() .tray_handle() - .set_menu(menu::get_tray_menu(&ctx, &new_configuration)) + .set_menu(menu::get_tray_menu( + window.app_handle().package_info(), + &new_configuration, + )) { log::error!("Error updating system tray {:?}", error); } diff --git a/crates/tauri/src/menu.rs b/crates/tauri/src/menu.rs index 7b20e24fe..14b3e83fb 100644 --- a/crates/tauri/src/menu.rs +++ b/crates/tauri/src/menu.rs @@ -1,8 +1,7 @@ use shared::config::UserSettings; use strum_macros::{Display, EnumString}; use tauri::{ - utils::assets::EmbeddedAssets, Context, CustomMenuItem, Menu, SystemTrayMenu, - SystemTrayMenuItem, SystemTraySubmenu, + CustomMenuItem, Menu, PackageInfo, SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu, }; #[cfg(not(target_os = "linux"))] use tauri::{MenuItem, Submenu}; @@ -28,10 +27,7 @@ pub enum MenuID { INSTALL_FIREFOX_EXT, } -pub fn get_tray_menu( - ctx: &Context, - user_settings: &UserSettings, -) -> SystemTrayMenu { +pub fn get_tray_menu(package_info: &PackageInfo, user_settings: &UserSettings) -> SystemTrayMenu { let show = CustomMenuItem::new(MenuID::SHOW_SEARCHBAR.to_string(), "Show search") .accelerator(user_settings.shortcut.clone()); @@ -44,7 +40,7 @@ pub fn get_tray_menu( let app_version: String = if cfg!(debug_assertions) { "🚧 dev-build 🚧".into() } else { - format!("v20{}", ctx.package_info().version) + format!("v20{}", package_info.version) }; let mut tray = SystemTrayMenu::new(); From 1faef511bb9a6eb023208ac86d141d6db53ce36c Mon Sep 17 00:00:00 2001 From: Andrew Huynh Date: Thu, 16 Mar 2023 14:09:02 -0700 Subject: [PATCH 18/19] return guards so that logging successfully flushes if application is closed (#383) --- crates/spyglass/src/main.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/spyglass/src/main.rs b/crates/spyglass/src/main.rs index d3c29a009..2e7792645 100644 --- a/crates/spyglass/src/main.rs +++ b/crates/spyglass/src/main.rs @@ -5,12 +5,12 @@ use libspyglass::pipeline; use libspyglass::plugin; use libspyglass::state::AppState; use libspyglass::task::{self, AppPause, AppShutdown, ManagerCommand}; -#[allow(unused_imports)] -use migration::Migrator; +use sentry::ClientInitGuard; use shared::config::{self, Config}; use std::io; use tokio::signal; use tokio::sync::{broadcast, mpsc}; +use tracing_appender::non_blocking::WorkerGuard; use tracing_log::LogTracer; use tracing_subscriber::{fmt, layer::SubscriberExt, EnvFilter}; @@ -36,7 +36,7 @@ struct CliArgs { } #[cfg(feature = "tokio-console")] -pub fn setup_logging(_config: &Config) { +pub fn setup_logging(_config: &Config) -> (Option, Option) { let subscriber = tracing_subscriber::registry() .with( EnvFilter::from_default_env() @@ -49,12 +49,14 @@ pub fn setup_logging(_config: &Config) { .spawn(), ); tracing::subscriber::set_global_default(subscriber).expect("Unable to set a global subscriber"); + + (None, None) } #[cfg(not(feature = "tokio-console"))] -pub fn setup_logging(config: &Config) { +pub fn setup_logging(config: &Config) -> (Option, Option) { #[cfg(not(debug_assertions))] - let _guard = if config.user_settings.disable_telemetry { + let sentry_guard = if config.user_settings.disable_telemetry { None } else { Some(sentry::init(( @@ -66,9 +68,11 @@ pub fn setup_logging(config: &Config) { }, ))) }; + #[cfg(debug_assertions)] + let sentry_guard = None; let file_appender = tracing_appender::rolling::daily(config.logs_dir(), "server.log"); - let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + let (non_blocking, tracing_guard) = tracing_appender::non_blocking(file_appender); let subscriber = tracing_subscriber::registry() .with( @@ -92,6 +96,8 @@ pub fn setup_logging(config: &Config) { .with(fmt::Layer::new().with_ansi(false).with_writer(non_blocking)) .with(sentry_tracing::layer()); tracing::subscriber::set_global_default(subscriber).expect("Unable to set a global subscriber"); + + (Some(tracing_guard), sentry_guard) } #[tokio::main(flavor = "multi_thread")] @@ -99,7 +105,7 @@ async fn main() -> Result<(), ()> { let mut config = Config::new(); let args = CliArgs::parse(); - setup_logging(&config); + let (_trace_guard, _sentry_guard) = setup_logging(&config); LogTracer::init().expect("Unable to initialize LogTracer"); log::info!("Loading prefs from: {:?}", Config::prefs_dir()); @@ -115,7 +121,7 @@ async fn main() -> Result<(), ()> { // Run any migrations, only on headless mode. #[cfg(debug_assertions)] { - use migration::DbErr; + use migration::{DbErr, Migrator}; let migration_status: Result<(), DbErr> = match Migrator::run_migrations().await { Ok(_) => Ok(()), Err(e) => { From 9f827a3da395727185219ebf8d080937c6509ce2 Mon Sep 17 00:00:00 2001 From: Andrew Huynh Date: Thu, 16 Mar 2023 15:05:19 -0700 Subject: [PATCH 19/19] release: v2023.3.2 (#382) --- Cargo.lock | 2 +- crates/spyglass/Cargo.toml | 2 +- crates/tauri/tauri.conf.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbefe8afd..da412e552 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6464,7 +6464,7 @@ dependencies = [ [[package]] name = "spyglass" -version = "23.3.1" +version = "23.3.2" dependencies = [ "addr", "anyhow", diff --git a/crates/spyglass/Cargo.toml b/crates/spyglass/Cargo.toml index 02eb13c63..d8be470e7 100644 --- a/crates/spyglass/Cargo.toml +++ b/crates/spyglass/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spyglass" -version = "23.3.1" +version = "23.3.2" edition = "2021" default-run = "spyglass" diff --git a/crates/tauri/tauri.conf.json b/crates/tauri/tauri.conf.json index e3ad63cbe..a8a429a11 100644 --- a/crates/tauri/tauri.conf.json +++ b/crates/tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "package": { "productName": "Spyglass", - "version": "23.3.1" + "version": "23.3.2" }, "build": { "distDir": "../client/dist",