├── .dockerignore ├── .gitignore ├── assets ├── popup.png ├── main-window.png └── timer-for-harvest.desktop ├── Dockerfile.debian-buster ├── .github └── workflows │ ├── test.yml │ └── build.yml ├── src ├── main.rs ├── app.rs ├── popup.rs ├── ui.rs └── lib.rs ├── .rpm └── timer-for-harvest.spec ├── LICENSE ├── Cargo.toml ├── tests └── lib.rs ├── README.md ├── CHANGELOG.md └── Cargo.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | target/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | /config.json 4 | *.swp 5 | -------------------------------------------------------------------------------- /assets/popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frenkel/timer-for-harvest/HEAD/assets/popup.png -------------------------------------------------------------------------------- /assets/main-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frenkel/timer-for-harvest/HEAD/assets/main-window.png -------------------------------------------------------------------------------- /assets/timer-for-harvest.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Type=Application 4 | Terminal=false 5 | Exec=timer-for-harvest 6 | Name=Timer for Harvest 7 | Icon=org.gnome.clocks 8 | -------------------------------------------------------------------------------- /Dockerfile.debian-buster: -------------------------------------------------------------------------------- 1 | FROM debian:buster 2 | RUN apt-get update && \ 3 | apt-get dist-upgrade -y && \ 4 | DEBIAN_FRONTEND=noninteractive apt-get install -y cargo libssl-dev libgtk-3-dev && \ 5 | rm -rf /var/lib/apt/lists/* 6 | 7 | RUN useradd -m user 8 | USER user 9 | WORKDIR /home/user 10 | 11 | RUN cargo install cargo-deb 12 | COPY --chown=user . . 13 | RUN cargo deb 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push] 3 | env: 4 | CARGO_TERM_COLOR: always 5 | jobs: 6 | cargo-test: 7 | runs-on: ubuntu-22.04 8 | steps: 9 | - run: sudo apt-get install -y libssl-dev libgtk-3-dev 10 | 11 | - uses: actions/checkout@v2 12 | - uses: actions/cache@v2 13 | with: 14 | key: target-dir-cache 15 | path: target/ 16 | 17 | - run: cargo build 18 | - run: cargo test 19 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod popup; 3 | mod ui; 4 | 5 | use app::App; 6 | use std::env::args; 7 | use std::sync::mpsc; 8 | use timer_for_harvest::Harvest; 9 | use ui::Ui; 10 | 11 | fn main() -> Result<(), Box> { 12 | let args: Vec = args().collect(); 13 | 14 | if args.len() == 2 && &args[1] == "--version" { 15 | println!("{}", Harvest::user_agent()); 16 | } else { 17 | let (to_ui, from_app) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); 18 | let (to_app, from_ui) = mpsc::channel(); 19 | 20 | let app = App::new(to_ui); 21 | let ui = Ui::new(to_app); 22 | 23 | App::handle_ui_signals(app, from_ui); 24 | Ui::handle_app_signals(ui, from_app); 25 | } 26 | 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /.rpm/timer-for-harvest.spec: -------------------------------------------------------------------------------- 1 | %define __spec_install_post %{nil} 2 | %define __os_install_post %{_dbpath}/brp-compress 3 | %define debug_package %{nil} 4 | 5 | Name: timer-for-harvest 6 | Summary: Timer for Harvest 7 | Version: @@VERSION@@ 8 | Release: @@RELEASE@@ 9 | License: BSD-2-Clause 10 | Group: Applications/System 11 | Source0: %{name}-%{version}.tar.gz 12 | URL: https://github.com/frenkel/timer-for-harvest 13 | 14 | BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root 15 | 16 | %description 17 | %{summary} 18 | 19 | %prep 20 | %setup -q 21 | 22 | %install 23 | rm -rf %{buildroot} 24 | mkdir -p %{buildroot} 25 | cp -a * %{buildroot} 26 | 27 | %clean 28 | rm -rf %{buildroot} 29 | 30 | %files 31 | %defattr(-,root,root,-) 32 | %{_bindir}/* 33 | /usr/share/applications/timer-for-harvest.desktop 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, Frank Groeneveld 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "timer-for-harvest" 3 | description = "Timer for Harvest" 4 | homepage = "https://github.com/frenkel/timer-for-harvest" 5 | version = "0.3.11" 6 | authors = ["Frank Groeneveld "] 7 | edition = "2018" 8 | readme = "README.md" 9 | license = "BSD-2-Clause" 10 | 11 | [package.metadata.deb] 12 | extended-description = "Harvest client implemented using GTK+ and Rust." 13 | license-file = ["LICENSE", "3"] 14 | assets = [ 15 | ["target/release/timer-for-harvest", "usr/bin/", "755"], 16 | ["assets/timer-for-harvest.desktop", "usr/share/applications/", "644"], 17 | ] 18 | depends = "libgtk-3-0, libssl1.1, desktop-file-utils" 19 | 20 | [package.metadata.rpm.cargo] 21 | buildflags = ["--release"] 22 | 23 | [package.metadata.rpm.targets] 24 | timer-for-harvest = { path = "/usr/bin/timer-for-harvest" } 25 | 26 | [package.metadata.rpm.files] 27 | "../assets/timer-for-harvest.desktop" = { path = "/usr/share/applications/timer-for-harvest.desktop" } 28 | 29 | [dependencies] 30 | reqwest = { version = "0.11.10", features = ["json", "blocking"] } 31 | serde = { version = "1.0.137", features = ["derive"] } 32 | serde_json = "1.0.81" 33 | chrono = "0.4.9" 34 | glib-sys = "0.9.1" 35 | hyper = "0.14.12" 36 | dirs = "2.0.2" 37 | resolv = { git = "https://github.com/mikedilger/resolv-rs", rev = "63fce7c9c9b88a7c2c453bcf90c1eabb67500449" } 38 | version-compare = "0.0.10" 39 | gtk = { version = "0.7.0", features = ["v3_22"] } 40 | gdk = { version = "0.11.0", features = ["v3_22"] } 41 | gio = { version = "0.7.0", features = ["v2_44"] } 42 | glib = { version = "0.8.0", features = ["v2_44"] } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build packages 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | jobs: 10 | old_build: 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - run: sudo apt-get install -y libssl-dev libgtk-3-dev 14 | - run: cargo install cargo-deb cargo-rpm 15 | 16 | - uses: actions/checkout@v2 17 | - uses: actions/cache@v2 18 | with: 19 | key: target-dir-cache-release 20 | path: target/ 21 | 22 | # needed on older glibc 23 | - run: sed -i 's/resolv.*/resolv = "0.2.0"/' Cargo.toml 24 | 25 | - run: cargo deb 26 | - run: cargo rpm build 27 | 28 | - run: mv target/debian/timer-for-harvest_*_amd64.deb . 29 | - run: mv target/release/rpmbuild/RPMS/x86_64/timer-for-harvest*.rpm . 30 | - run: sha256sum *.deb *.rpm > SHA256SUM 31 | 32 | - uses: actions/upload-artifact@v2 33 | with: 34 | name: packages 35 | path: | 36 | timer-for-harvest_*_amd64.deb 37 | timer-for-harvest*.rpm 38 | SHA256SUM 39 | build: 40 | runs-on: ubuntu-22.04 41 | steps: 42 | - run: sudo apt-get install -y libssl-dev libgtk-3-dev 43 | - run: cargo install cargo-deb cargo-rpm 44 | 45 | - uses: actions/checkout@v2 46 | - uses: actions/cache@v2 47 | with: 48 | key: target-dir-cache-release 49 | path: target/ 50 | 51 | - run: cargo deb 52 | - run: cargo rpm build 53 | 54 | - run: mv target/debian/timer-for-harvest_*_amd64.deb . 55 | - run: mv target/release/rpmbuild/RPMS/x86_64/timer-for-harvest*.rpm . 56 | - run: sha256sum *.deb *.rpm > SHA256SUM 57 | 58 | - uses: actions/upload-artifact@v2 59 | with: 60 | name: packages 61 | path: | 62 | timer-for-harvest_*_amd64.deb 63 | timer-for-harvest*.rpm 64 | SHA256SUM -------------------------------------------------------------------------------- /tests/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod test { 3 | #[test] 4 | fn should_convert_duration_correctly() { 5 | assert_eq!("1:00", timer_for_harvest::f32_to_duration_str(1.0)); 6 | assert_eq!("0:01", timer_for_harvest::f32_to_duration_str(1.0 / 60.0)); 7 | assert_eq!("0:05", timer_for_harvest::f32_to_duration_str(5.0 / 60.0)); 8 | assert_eq!("0:10", timer_for_harvest::f32_to_duration_str(10.0 / 60.0)); 9 | assert_eq!("1:00", timer_for_harvest::f32_to_duration_str(59.9 / 60.0)); 10 | } 11 | 12 | #[test] 13 | fn should_not_crash_duration_str_to_f32() { 14 | assert_eq!(0.0, timer_for_harvest::duration_str_to_f32("0:00")); 15 | assert_eq!(1.5, timer_for_harvest::duration_str_to_f32("1:30")); 16 | assert_eq!(1.0, timer_for_harvest::duration_str_to_f32("1")); 17 | } 18 | 19 | #[test] 20 | fn should_parse_account_id() { 21 | assert_eq!( 22 | "123", 23 | timer_for_harvest::parse_account_details("GET /?access_token=abc&scope=harvest%3A123") 24 | .1 25 | ); 26 | assert_eq!( 27 | "123", 28 | timer_for_harvest::parse_account_details("GET /?scope=harvest%3A123&access_token=abc") 29 | .1 30 | ); 31 | } 32 | 33 | #[test] 34 | fn should_parse_access_token() { 35 | assert_eq!( 36 | "abc", 37 | timer_for_harvest::parse_account_details("GET /?access_token=abc&scope=harvest%3A123") 38 | .0 39 | ); 40 | assert_eq!( 41 | "abc", 42 | timer_for_harvest::parse_account_details("GET /?scope=harvest%3A123&access_token=abc") 43 | .0 44 | ); 45 | } 46 | 47 | #[test] 48 | fn should_parse_expires_in() { 49 | assert_eq!( 50 | "123", 51 | timer_for_harvest::parse_account_details("GET /?expires_in=123&scope=harvest%3A456").2 52 | ); 53 | assert_eq!( 54 | "123", 55 | timer_for_harvest::parse_account_details("GET /?scope=harvest%3A456&expires_in=123").2 56 | ); 57 | } 58 | 59 | #[test] 60 | fn should_format_timeentry_notes_for_list() { 61 | assert_eq!("Lorem Ipsum - 1234567890 - 1234567890 - 1234567890 - 1234567890 - 1234567890 - 1...", timer_for_harvest::format_timeentry_notes_for_list(&"Lorem Ipsum\n\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890", None)); 62 | assert_eq!("Lorem Ipsum - 1...", timer_for_harvest::format_timeentry_notes_for_list(&"Lorem Ipsum\n\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890\n1234567890", Some(15))); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Timer for Harvest 2 | [Harvest](https://www.getharvest.com/) client implemented using 3 | [GTK](https://www.gtk.org/) and [Rust](https://www.rust-lang.org/) for Linux 4 | and the various BSD's. 5 | 6 | ## Screenshots 7 | 8 | ![Main window](/assets/main-window.png?raw=true "The main window") 9 | ![Popup](/assets/popup.png?raw=true "The time entry popup") 10 | 11 | ## Installation 12 | On the Timer for Harvest github page, click on the 13 | [releases link](https://github.com/frenkel/timer-for-harvest/releases). The 14 | newest release is listed at the top of the page. It contains pre-build 15 | binaries for Ubuntu 20.04+ and Fedora 34+, as well as other distributions. 16 | You will be notified of new releases by a message in the main window. 17 | 18 | ## Usage 19 | After installation your GNOME shell should be able to find the 20 | application when you type "Timer for Harvest" in the activity searcher. 21 | 22 | ### First usage 23 | Upon first launch you will first see a web browser start. This will open an 24 | authorization flow from Harvest. When you've completed these steps you will 25 | end up on a white page with the text "Authorized successfully." The actual 26 | application will start now. 27 | 28 | ### Daily usage 29 | Just like the Harvest web interface there are some handy keyboard shortcuts: 30 | - **F5** in the main window will refresh the time entries list. This can be 31 | usefull when you updated the entries using a different interface and 32 | Timer for Harvest still shows the old state. 33 | - **N** in the main window opens the new time entry popup. 34 | - **Esc** closes the time entry popup. 35 | - **Enter** activates the "Save Timer" button in the time entry popup. 36 | 37 | ## Security 38 | Username and password details are never seen by Timer for Harvest. A web 39 | browser is used to authorize Timer for Harvest access to your account. This 40 | authorization is stored in the form of an authorization token, which Harvest 41 | lets expire in 14 days. Leaking this token would thus give somebody access to 42 | your account for a maximum of 14 days. 43 | 44 | The authorization token is currently stored on the file system, namely in 45 | $XDG\_CONFIG\_HOME/timer-for-harvest.json. In the future we hope to move this 46 | token to a more secure location, such as 47 | [libsecret](https://wiki.gnome.org/Projects/Libsecret). 48 | 49 | ## Wishlist 50 | - Idle detection to ask user whether idle time should be subtracted or booked 51 | as a new time entry. For example, using this dbus call when using GNOME: 52 | `dbus-send --print-reply --dest=org.gnome.Mutter.IdleMonitor /org/gnome/Mutter/IdleMonitor/Core org.gnome.Mutter.IdleMonitor.GetIdletime` 53 | - Authentication token storage in [libsecret](https://wiki.gnome.org/Projects/Libsecret). 54 | - [Improve UI](https://github.com/frenkel/timer-for-harvest/issues/34) to speed up the new entry process. 55 | 56 | ## Building 57 | If you want to build the application yourself you'll need rust, cargo, bindgen, clang and the gtk3 58 | development libraries installed. You can then run `cargo build --release` to generate 59 | the binary in `target/release/`. 60 | 61 | ## Uninstall 62 | 63 | To ininstall, simply run the following command in a terminal based on your distro: 64 | 65 | - Fedora / RHEL or other RPM-based distros : `sudo dnf remove timer-for-harvest` 66 | - Debian / Ubuntu / Linux Mint or other apt-based distros: `sudo apt remove timer-for-harvest` 67 | 68 | ## Donations 69 | If you like the software I create, please consider donating through 70 | [Paypal](https://paypal.me/frankgroeneveld) or 71 | [Github Sponsors](https://github.com/sponsors/frenkel). 72 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [UNRELEASED] - YYYY-MM-DD 4 | 5 | ## [0.3.10] - 2022-08-01 6 | 7 | - Build against OpenSSL 1.1 for Ubuntu 20.04. 8 | 9 | ## [0.3.10] - 2022-07-29 10 | 11 | - Build against OpenSSL 3 for Ubuntu 22.04. 12 | 13 | ## [0.3.9] - 2022-06-11 14 | 15 | - Updates to dependencies. These dependencies needed to be updated because of various security issues. 16 | - Clear selected task when switching project. Previously an invalid entry was still selected, which stopped the save button from working. 17 | 18 | ## [0.3.8] - 2021-09-21 19 | 20 | - Re-enable the ability to resize the main window, implemented by @mlunax. 21 | - The license of Timer for Harvest will no longer show up as proprietary in GNOME Software. 22 | 23 | ## [0.3.7] - 2021-08-26 24 | 25 | - Show correct button label when adding hours instead of starting a new timer, implemented by @mlunax. 26 | - Fixed a crash on resume-from-suspend because the time entries could not load while network was not up yet. 27 | 28 | ## [0.3.6] - 2021-03-12 29 | 30 | This release includes escaping for special characters such as & in task, project en client names. Thanks @wvengen for reporting this bug. 31 | Furthermore the update link is now clickable thanks to @iatanas0v. 32 | 33 | ## [0.3.5] - 2021-02-26 34 | 35 | Thanks to @KillerCodeMonkey we have another release! This time he contributed changes to the main window that keep it a static size with a scrollbar. Thanks again @KillerCodeMonkey! 36 | 37 | ## [0.3.4] - 2021-02-05 38 | 39 | Thanks to @KillerCodeMonkey we now have multi-line description support! 40 | We also had to switch to a different client\_id for the authorization at Harvest. You shouldn't notice this but we're reporting this so that you know it is not a security incident. 41 | 42 | ## [0.3.3] - 2020-10-23 43 | 44 | This release includes the following new features and bugfixes: 45 | 46 | - Auto refresh when receiving window focus, idea by @Flimm 47 | - Add client name to project chooser, requested by @jarnalyrkar 48 | - New entry's are now added on the chosen date instead of the current date 49 | - Edit button is now re-enabled when cancelling the edit popup 50 | 51 | 52 | ## [0.3.2] - 2020-08-21 53 | 54 | This release consist of a major architectural rewrite. As an end user this shouldn't be noticeable, except for some interface alignment changes. However one user noticeable feature was added: the application will now check whether a new version is available and show a notification when one is available. This works by checking a DNS TXT record, so we cannot misuse this to track usage of the application. 55 | 56 | ## [0.3.1] - 2020-06-26 57 | 58 | This release adds two new features contributed by @jaspervandenberg: 59 | 60 | - Add a button to go to the current date 61 | - Ask for confirmation before deleting an entry 62 | 63 | ## [0.3.0] - 2020-06-09 64 | 65 | This release includes various interface improvements and one big new feature: the ability to switch to different days. 66 | 67 | ## [0.2.1] - 2020-04-14 68 | 69 | This is a very small release that fixes one important bug: the project code can be null, which would result in a crash. 70 | 71 | ## [0.2.0] - 2020-04-08 72 | 73 | This release improves the user experience a lot. All API communication has been moved to a background thread, resulting in a more response user interface. Furthermore, a number of error messages have been approved, which should make debugging problems easier. 74 | 75 | ## [0.1.12] - 2020-02-19 76 | 77 | The main window acquired a lot of white spacing after enabling truncate on the labels. This release reduces it drastically. 78 | 79 | ## [0.1.11] - 2020-02-18 80 | 81 | This release mostly documents the application, but it also fixes a line wrapping problem for long project names 82 | 83 | ## [0.1.10] - 2020-02-11 84 | 85 | This release loads project data on startup instead of everytime you click a button. This makes using it on a daily basis a lot faster, at the expense of a slower startup time. 86 | 87 | ## [0.1.9] - 2020-01-14 88 | 89 | This release is the first release with packages for Fedora 31 and Ubuntu 19.10. A small bug that would occur on Ubuntu when authorizing was fixed in this release as well. 90 | 91 | ## [0.1.8] - 2019-12-31 92 | 93 | This release adds the ability to save and load the previously obtained OAuth tokens to disk. This means that the authorization flow needs to be run only once every two weeks, making it a lot more user-friendly. 94 | 95 | ## [0.1.7] - 2019-12-29 96 | 97 | This is the first user-friendly release because it replaces the usage of developer tokens with an OAuth authorization flow. Currently the obtained authorization is not remembered between different application launches, but support for that will follow in a later release. 98 | 99 | ## [0.1.6] - 2019-12-27 100 | 101 | Improvements in this release: 102 | 103 | - Re-order time entries of the day correctly, newest at the bottom 104 | - Don't require a terminal when running, by removing println!() statements 105 | 106 | ## [0.1.5] - 2019-12-26 107 | 108 | This release includes some important refactoring, which allowed us to add the ability for running timers to update their labels. 109 | 110 | ## [0.1.4] - 2019-12-24 111 | 112 | This release makes usage a lot faster by adding the following features: 113 | 114 | - Auto completion in the project chooser combobox 115 | - Auto completion in the task chooser combobox 116 | - Close timer popup without saving by pressing escape 117 | - Open timer popup by pressing n 118 | 119 | ## [0.1.3] - 2019-12-10 120 | 121 | This release adds some more polish to the whole application. Notable changes include: 122 | 123 | - Wrap notes in main window. 124 | - Don't crash when time entry has no notes. 125 | - Pressing enter in popup now defaults to saving the timer. 126 | - Task loading has been modified to don't rely on admin-permission API calls. This change also allowed for an extra API call to be removed, resulting in a nicer workflow. 127 | 128 | ## [0.1.2] - 2019-12-03 129 | 130 | This release adds some small but important improvements. 131 | 132 | - Implement workaround for updating running timers without overwriting hours 133 | - Show more time entry info, add some style improvements 134 | - Order projects case-insensitive 135 | - Don't crash after switching project where task is missing 136 | 137 | ## [0.1.1] - 2019-11-28 138 | 139 | This release fixes various bugs: 140 | 141 | - Fix bug in time formatting where 1 minute would show as 10 142 | - Don't crash when editing timers without notes 143 | - Refresh time entries list after edits 144 | 145 | ## [0.1.0] - 2019-11-26 146 | 147 | This is the first public release. Basic functionality works, including: 148 | 149 | - Show entry's of today 150 | - Start new timer or 151 | - Restart earlier timer 152 | - Stop running timer 153 | - Add new entry with duration 154 | - Refresh time entries list on F5 press 155 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::ui; 2 | use std::sync::mpsc; 3 | use std::thread; 4 | use timer_for_harvest::*; 5 | 6 | pub enum Signal { 7 | RetrieveTimeEntries, 8 | NewTimeEntry, 9 | EditTimeEntry(u32), 10 | RestartTimeEntry(u32), 11 | StopTimeEntry(u32), 12 | DeleteTimeEntry(u32), 13 | PrevDate, 14 | NextDate, 15 | TodayDate, 16 | LoadTasksForProject(u32), 17 | StartTimer(u32, u32, String, f32), 18 | MinutePassed, 19 | UpdateTimer(u32, u32, u32, String, f32), 20 | CheckVersion, 21 | } 22 | 23 | pub struct App { 24 | to_ui: glib::Sender, 25 | shown_date: chrono::NaiveDate, 26 | api: Harvest, 27 | user: User, 28 | project_assignments: Vec, 29 | time_entries: Vec, 30 | } 31 | 32 | impl App { 33 | pub fn new(to_ui: glib::Sender) -> App { 34 | let now = chrono::Local::today().naive_local(); 35 | let api = Harvest::new(); 36 | let user = api.current_user(); 37 | let mut project_assignments = api.active_project_assignments(); 38 | project_assignments.sort_by(|a, b| { 39 | a.project 40 | .name 41 | .to_lowercase() 42 | .cmp(&b.project.name.to_lowercase()) 43 | }); 44 | 45 | App { 46 | to_ui: to_ui, 47 | shown_date: now, 48 | api: api, 49 | user: user, 50 | project_assignments: project_assignments, 51 | time_entries: vec![], 52 | } 53 | } 54 | 55 | pub fn handle_ui_signals(mut app: App, from_ui: mpsc::Receiver) { 56 | thread::spawn(move || { 57 | for signal in from_ui { 58 | match signal { 59 | Signal::RetrieveTimeEntries => { 60 | app.retrieve_time_entries(); 61 | } 62 | Signal::NewTimeEntry => { 63 | app.to_ui 64 | .send(ui::Signal::OpenPopup(app.project_assignments.to_vec())) 65 | .expect("Sending message to ui thread"); 66 | } 67 | Signal::EditTimeEntry(id) => { 68 | app.edit_time_entry(id); 69 | } 70 | Signal::RestartTimeEntry(id) => { 71 | app.restart_timer(id); 72 | app.retrieve_time_entries(); 73 | } 74 | Signal::StopTimeEntry(id) => { 75 | app.stop_timer(id); 76 | app.retrieve_time_entries(); 77 | } 78 | Signal::DeleteTimeEntry(id) => { 79 | app.api.delete_timer(id); 80 | app.retrieve_time_entries(); 81 | } 82 | Signal::PrevDate => { 83 | app.shown_date = app.shown_date.pred(); 84 | app.retrieve_time_entries(); 85 | } 86 | Signal::NextDate => { 87 | app.shown_date = app.shown_date.succ(); 88 | app.retrieve_time_entries(); 89 | } 90 | Signal::TodayDate => { 91 | app.shown_date = chrono::Local::today().naive_local(); 92 | app.retrieve_time_entries(); 93 | } 94 | Signal::LoadTasksForProject(id) => { 95 | app.retrieve_tasks_for_project(id); 96 | } 97 | Signal::StartTimer(project_id, task_id, notes, hours) => { 98 | app.start_timer(project_id, task_id, notes, hours); 99 | app.retrieve_time_entries(); 100 | } 101 | Signal::MinutePassed => { 102 | app.increment_running_timer(); 103 | } 104 | Signal::UpdateTimer(id, project_id, task_id, notes, hours) => { 105 | app.update_timer(id, project_id, task_id, notes, hours); 106 | app.retrieve_time_entries(); 107 | } 108 | Signal::CheckVersion => { 109 | app.check_version(); 110 | } 111 | } 112 | } 113 | }); 114 | } 115 | 116 | fn format_and_send_title(&self) { 117 | let title = format!("Harvest - {}", self.shown_date.format("%a %-d %b")); 118 | self.to_ui 119 | .send(ui::Signal::SetTitle(title)) 120 | .expect("Sending message to ui thread"); 121 | } 122 | 123 | fn retrieve_time_entries(&mut self) { 124 | self.to_ui 125 | .send(ui::Signal::SetTitle("Loading...".to_string())) 126 | .expect("Sending message to ui thread"); 127 | self.time_entries = self.api.time_entries_for( 128 | &self.user, 129 | self.shown_date.to_string(), 130 | self.shown_date.to_string(), 131 | ); 132 | 133 | self.to_ui 134 | .send(ui::Signal::SetTimeEntries(self.time_entries.clone())) 135 | .expect("Sending message to ui thread"); 136 | self.format_and_send_title(); 137 | } 138 | 139 | fn increment_running_timer(&mut self) { 140 | for mut time_entry in &mut self.time_entries { 141 | if time_entry.is_running { 142 | time_entry.hours += 1.0 / 60.0; 143 | } 144 | } 145 | 146 | self.to_ui 147 | .send(ui::Signal::SetTimeEntries(self.time_entries.clone())) 148 | .expect("Sending message to ui thread"); 149 | self.format_and_send_title(); 150 | } 151 | 152 | fn restart_timer(&self, id: u32) { 153 | self.to_ui 154 | .send(ui::Signal::SetTitle("Loading...".to_string())) 155 | .expect("Sending message to ui thread"); 156 | self.api.restart_timer(id); 157 | } 158 | 159 | fn stop_timer(&self, id: u32) { 160 | self.to_ui 161 | .send(ui::Signal::SetTitle("Loading...".to_string())) 162 | .expect("Sending message to ui thread"); 163 | self.api.stop_timer(id); 164 | } 165 | 166 | fn retrieve_tasks_for_project(&self, id: u32) { 167 | for project_assignment in &self.project_assignments { 168 | if id == project_assignment.project.id { 169 | self.to_ui 170 | .send(ui::Signal::TaskAssignments( 171 | project_assignment.task_assignments.clone(), 172 | )) 173 | .expect("Sending message to ui thread"); 174 | break; 175 | } 176 | } 177 | } 178 | 179 | fn start_timer(&self, project_id: u32, task_id: u32, notes: String, hours: f32) { 180 | self.api 181 | .start_timer(project_id, task_id, notes, hours, &self.shown_date); 182 | } 183 | 184 | fn update_timer(&self, id: u32, project_id: u32, task_id: u32, notes: String, hours: f32) { 185 | for time_entry in &self.time_entries { 186 | if time_entry.id == id { 187 | self.api.update_timer( 188 | id, 189 | project_id, 190 | task_id, 191 | notes, 192 | hours, 193 | time_entry.is_running, 194 | time_entry.spent_date.clone(), 195 | ); 196 | break; 197 | } 198 | } 199 | } 200 | 201 | fn edit_time_entry(&self, id: u32) { 202 | for time_entry in self.time_entries.clone() { 203 | if time_entry.id == id { 204 | self.to_ui 205 | .send(ui::Signal::OpenPopupWithTimeEntry( 206 | self.project_assignments.to_vec(), 207 | time_entry, 208 | )) 209 | .expect("Sending message to ui thread"); 210 | } 211 | } 212 | } 213 | 214 | fn check_version(&self) { 215 | let version_string = format!( 216 | "{}.{}.{}{}", 217 | env!("CARGO_PKG_VERSION_MAJOR"), 218 | env!("CARGO_PKG_VERSION_MINOR"), 219 | env!("CARGO_PKG_VERSION_PATCH"), 220 | option_env!("CARGO_PKG_VERSION_PRE").unwrap_or("") 221 | ); 222 | let current_version = version_compare::Version::from(&version_string).unwrap(); 223 | 224 | let mut resolver = resolv::Resolver::new().unwrap(); 225 | 226 | let response = resolver.query( 227 | b"current-version.timer-for-harvest.frankgroeneveld.nl", 228 | resolv::Class::IN, 229 | resolv::RecordType::TXT, 230 | ); 231 | match response { 232 | Err(_) => {} 233 | Ok(mut response) => { 234 | for i in 0..response.get_section_count(resolv::Section::Answer) { 235 | let txt: resolv::Record = 236 | response.get_record(resolv::Section::Answer, i).unwrap(); 237 | 238 | let latest_version = version_compare::Version::from(&txt.data.dname).unwrap(); 239 | if current_version < latest_version { 240 | self.to_ui 241 | .send(ui::Signal::ShowNotice(format!( 242 | "New version available ({}), download it from {}", 243 | txt.data.dname, 244 | env!("CARGO_PKG_HOMEPAGE"), 245 | env!("CARGO_PKG_HOMEPAGE") 246 | ))) 247 | .expect("Sending message to ui thread"); 248 | break; 249 | } 250 | } 251 | } 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/popup.rs: -------------------------------------------------------------------------------- 1 | use crate::app; 2 | use gtk::prelude::*; 3 | use std::sync::mpsc; 4 | use timer_for_harvest::*; 5 | 6 | /* handy gtk callback clone macro taken from https://gtk-rs.org/docs-src/tutorial/closures */ 7 | macro_rules! clone { 8 | (@param _) => ( _ ); 9 | (@param $x:ident) => ( $x ); 10 | ($($n:ident),+ => move || $body:expr) => ( 11 | { 12 | $( let $n = $n.clone(); )+ 13 | move || $body 14 | } 15 | ); 16 | ($($n:ident),+ => move |$($p:tt),+| $body:expr) => ( 17 | { 18 | $( let $n = $n.clone(); )+ 19 | move |$(clone!(@param $p),)+| $body 20 | } 21 | ); 22 | } 23 | 24 | pub struct Popup { 25 | window: gtk::Window, 26 | project_chooser: gtk::ComboBox, 27 | task_chooser: gtk::ComboBox, 28 | to_app: mpsc::Sender, 29 | delete_button: gtk::Button, 30 | save_button: gtk::Button, 31 | notes_input: gtk::TextView, 32 | hours_input: gtk::Entry, 33 | time_entry_id: Option, 34 | } 35 | 36 | impl Popup { 37 | pub fn new( 38 | application: >k::Application, 39 | project_assignments: Vec, 40 | to_app: mpsc::Sender, 41 | ) -> Popup { 42 | let window = gtk::Window::new(gtk::WindowType::Toplevel); 43 | 44 | window.set_title("Add time entry"); 45 | window.set_default_size(400, 300); 46 | window.set_modal(true); 47 | window.set_type_hint(gdk::WindowTypeHint::Dialog); 48 | window.set_border_width(18); 49 | 50 | window.set_resizable(false); 51 | 52 | window.connect_delete_event(|_, _| Inhibit(false)); 53 | window.add_events(gdk::EventMask::KEY_PRESS_MASK); 54 | window.connect_key_press_event(|window, event| { 55 | if event.get_keyval() == gdk::enums::key::Escape { 56 | window.close(); 57 | Inhibit(true) 58 | } else { 59 | Inhibit(false) 60 | } 61 | }); 62 | 63 | window.set_transient_for(Some(&application.get_active_window().unwrap())); 64 | application.add_window(&window); 65 | 66 | window.show_all(); 67 | 68 | let delete_button = gtk::Button::new_with_label("Delete"); 69 | delete_button 70 | .get_style_context() 71 | .add_class(>k::STYLE_CLASS_DESTRUCTIVE_ACTION); 72 | let save_button = gtk::Button::new_with_label("Start Timer"); 73 | save_button.set_can_default(true); 74 | let notes_input = gtk::TextView::new(); 75 | notes_input.set_border_width(4); 76 | notes_input.set_wrap_mode(gtk::WrapMode::WordChar); 77 | notes_input.set_accepts_tab(false); 78 | let hours_input = gtk::Entry::new(); 79 | hours_input 80 | .set_property("activates-default", &true) 81 | .expect("could not allow default activation"); 82 | hours_input.set_placeholder_text(Some("00:00")); 83 | 84 | hours_input.connect_changed(clone!(save_button => move |hours_input| { 85 | if &hours_input.get_text().unwrap() != "" { 86 | save_button.set_label("Save Timer"); 87 | } else { 88 | save_button.set_label("Start Timer"); 89 | } 90 | })); 91 | 92 | let popup = Popup { 93 | window: window, 94 | project_chooser: Popup::project_chooser(project_assignments), 95 | task_chooser: Popup::task_chooser(), 96 | to_app: to_app, 97 | delete_button: delete_button, 98 | save_button: save_button, 99 | notes_input: notes_input, 100 | hours_input: hours_input, 101 | time_entry_id: None, 102 | }; 103 | popup.add_widgets(); 104 | popup 105 | } 106 | 107 | fn project_chooser(project_assignments: Vec) -> gtk::ComboBox { 108 | let project_store = gtk::ListStore::new(&[gtk::Type::String, gtk::Type::U32]); 109 | for project_assignment in project_assignments { 110 | project_store.set( 111 | &project_store.append(), 112 | &[0, 1], 113 | &[ 114 | &format!( 115 | "{} ({})", 116 | project_assignment.project.name_and_code(), 117 | project_assignment.client.name 118 | ), 119 | &project_assignment.project.id, 120 | ], 121 | ); 122 | } 123 | let project_chooser = gtk::ComboBox::new_with_model_and_entry(&project_store); 124 | project_chooser.set_entry_text_column(0); 125 | 126 | let project_completer = gtk::EntryCompletion::new(); 127 | project_completer.set_model(Some(&project_store)); 128 | project_completer.set_text_column(0); 129 | project_completer.set_match_func(Popup::fuzzy_matching); 130 | project_completer.connect_match_selected( 131 | clone!(project_chooser => move |_completion, _model, iter| { 132 | project_chooser.set_active_iter(Some(&iter)); 133 | Inhibit(false) 134 | }), 135 | ); 136 | project_chooser 137 | .get_child() 138 | .unwrap() 139 | .downcast::() 140 | .unwrap() 141 | .set_completion(Some(&project_completer)); 142 | project_chooser.set_hexpand(true); 143 | project_chooser 144 | } 145 | 146 | fn task_chooser() -> gtk::ComboBox { 147 | let task_store = gtk::ListStore::new(&[gtk::Type::String, gtk::Type::U32]); 148 | let task_chooser = gtk::ComboBox::new_with_model_and_entry(&task_store); 149 | task_chooser.set_entry_text_column(0); 150 | 151 | let task_completer = gtk::EntryCompletion::new(); 152 | task_completer.set_model(Some(&task_store)); 153 | task_completer.set_text_column(0); 154 | task_completer.set_match_func(Popup::fuzzy_matching); 155 | task_completer.connect_match_selected( 156 | clone!(task_chooser => move |_completion, _model, iter| { 157 | task_chooser.set_active_iter(Some(&iter)); 158 | Inhibit(false) 159 | }), 160 | ); 161 | 162 | task_chooser 163 | .get_child() 164 | .unwrap() 165 | .downcast::() 166 | .unwrap() 167 | .set_completion(Some(&task_completer)); 168 | task_chooser 169 | } 170 | 171 | fn add_widgets(&self) { 172 | let grid = gtk::Grid::new(); 173 | grid.set_column_spacing(4); 174 | grid.set_row_spacing(18); 175 | 176 | self.window.add(&grid); 177 | 178 | let scrollable_window = 179 | gtk::ScrolledWindow::new(gtk::NONE_ADJUSTMENT, gtk::NONE_ADJUSTMENT); 180 | scrollable_window.set_policy(gtk::PolicyType::Automatic, gtk::PolicyType::Automatic); 181 | scrollable_window.add(&self.notes_input); 182 | scrollable_window.set_shadow_type(gtk::ShadowType::Out); 183 | 184 | grid.attach(&self.project_chooser, 0, 0, 2, 1); 185 | grid.attach(&self.task_chooser, 0, 1, 2, 1); 186 | grid.attach(&scrollable_window, 0, 2, 2, 6); 187 | grid.attach(&self.hours_input, 1, 8, 1, 1); 188 | 189 | self.delete_button.set_sensitive(false); 190 | grid.attach(&self.delete_button, 0, 9, 1, 2); 191 | 192 | grid.attach(&self.save_button, 1, 9, 1, 2); 193 | self.save_button.grab_default(); 194 | 195 | grid.set_column_homogeneous(true); 196 | 197 | grid.show_all(); 198 | } 199 | 200 | pub fn connect_signals(&self) { 201 | let to_app = self.to_app.clone(); 202 | let project_chooser = self.project_chooser.clone(); 203 | let task_chooser = self.task_chooser.clone(); 204 | let window = self.window.clone(); 205 | let notes_input = self.notes_input.clone(); 206 | let hours_input = self.hours_input.clone(); 207 | let time_entry_id = self.time_entry_id; 208 | self.save_button.connect_clicked(move |button| { 209 | button.set_sensitive(false); 210 | let project_id = match project_chooser.get_active() { 211 | Some(index) => Popup::id_from_combo_box(&project_chooser, index), 212 | None => 0, 213 | }; 214 | let task_id = match task_chooser.get_active() { 215 | Some(index) => Popup::id_from_combo_box(&task_chooser, index), 216 | None => 0, 217 | }; 218 | if project_id > 0 && task_id > 0 { 219 | match time_entry_id { 220 | None => { 221 | let notes_buffer = notes_input.get_buffer().unwrap(); 222 | to_app 223 | .send(app::Signal::StartTimer( 224 | project_id, 225 | task_id, 226 | notes_buffer 227 | .get_text( 228 | ¬es_buffer.get_start_iter(), 229 | ¬es_buffer.get_end_iter(), 230 | false, 231 | ) 232 | .unwrap() 233 | .to_string(), 234 | duration_str_to_f32(&hours_input.get_text().unwrap()), 235 | )) 236 | .expect("Sending message to background thread"); 237 | } 238 | Some(id) => { 239 | let notes_buffer = notes_input.get_buffer().unwrap(); 240 | to_app 241 | .send(app::Signal::UpdateTimer( 242 | id, 243 | project_id, 244 | task_id, 245 | notes_buffer 246 | .get_text( 247 | ¬es_buffer.get_start_iter(), 248 | ¬es_buffer.get_end_iter(), 249 | false, 250 | ) 251 | .unwrap() 252 | .to_string(), 253 | duration_str_to_f32(&hours_input.get_text().unwrap()), 254 | )) 255 | .expect("Sending message to background thread"); 256 | } 257 | } 258 | window.close(); 259 | } else { 260 | button.set_sensitive(true); 261 | } 262 | }); 263 | 264 | let to_app = self.to_app.clone(); 265 | self.project_chooser 266 | .connect_changed(move |project_chooser| match project_chooser.get_active() { 267 | Some(index) => { 268 | let project_id = Popup::id_from_combo_box(&project_chooser, index); 269 | to_app 270 | .send(app::Signal::LoadTasksForProject(project_id)) 271 | .expect("Sending message to application thread"); 272 | } 273 | None => {} 274 | }); 275 | } 276 | 277 | pub fn populate(&mut self, time_entry: TimeEntry) { 278 | self.window.set_title("Edit time entry"); 279 | self.time_entry_id = Some(time_entry.id); 280 | self.save_button.set_label("Save Timer"); 281 | self.hours_input.set_editable(!time_entry.is_running); 282 | self.project_chooser.set_active_iter(Some( 283 | &Popup::iter_from_id(&self.project_chooser, time_entry.project.id).unwrap(), 284 | )); 285 | 286 | match &time_entry.notes { 287 | Some(n) => self.notes_input.get_buffer().unwrap().set_text(&n), 288 | None => {} 289 | } 290 | self.hours_input 291 | .set_text(&f32_to_duration_str(time_entry.hours)); 292 | 293 | self.task_chooser.set_active_iter(Some( 294 | &Popup::iter_from_id(&self.task_chooser, time_entry.task.id).unwrap(), 295 | )); 296 | 297 | let to_app = self.to_app.clone(); 298 | let window = self.window.clone(); 299 | self.delete_button.set_sensitive(true); 300 | self.delete_button.connect_clicked(move |button| { 301 | button.set_sensitive(false); 302 | 303 | let confirmation_box = gtk::MessageDialog::new( 304 | None::<>k::Window>, 305 | gtk::DialogFlags::empty(), 306 | gtk::MessageType::Warning, 307 | gtk::ButtonsType::YesNo, 308 | "Are you sure you want to delete this entry?", 309 | ); 310 | 311 | let confirmation_response = confirmation_box.run(); 312 | confirmation_box.destroy(); 313 | 314 | if confirmation_response == gtk::ResponseType::Yes { 315 | to_app 316 | .send(app::Signal::DeleteTimeEntry(time_entry.id)) 317 | .expect("Sending message to application thread"); 318 | window.close(); 319 | } else { 320 | button.set_sensitive(true); 321 | } 322 | }); 323 | } 324 | 325 | fn fuzzy_matching(completion: >k::EntryCompletion, key: &str, iter: >k::TreeIter) -> bool { 326 | let store = completion.get_model().unwrap(); 327 | let column_number = completion.get_text_column(); 328 | let row = store 329 | .get_value(iter, column_number) 330 | .get::() 331 | .unwrap(); 332 | 333 | /* key is already lower case */ 334 | if row.to_lowercase().contains(key) { 335 | true 336 | } else { 337 | false 338 | } 339 | } 340 | 341 | fn id_from_combo_box(combo_box: >k::ComboBox, index: u32) -> u32 { 342 | let model = combo_box.get_model().unwrap(); 343 | 344 | let iter = model.get_iter_from_string(&format!("{}", index)).unwrap(); 345 | model.get_value(&iter, 1).get::().unwrap() 346 | } 347 | 348 | fn iter_from_id(combo_box: >k::ComboBox, id: u32) -> Option { 349 | let store = combo_box 350 | .get_model() 351 | .unwrap() 352 | .downcast::() 353 | .unwrap(); 354 | let iter = store.get_iter_first().unwrap(); 355 | loop { 356 | if store.get_value(&iter, 1).get::().unwrap() == id { 357 | return Some(iter); 358 | } 359 | if !store.iter_next(&iter) { 360 | break; 361 | } 362 | } 363 | None 364 | } 365 | 366 | pub fn load_tasks(&self, task_assignments: Vec) { 367 | let store = self 368 | .task_chooser 369 | .get_model() 370 | .unwrap() 371 | .downcast::() 372 | .unwrap(); 373 | store.clear(); 374 | self.task_chooser 375 | .get_child() 376 | .unwrap() 377 | .downcast::() 378 | .unwrap() 379 | .set_text(""); 380 | for task_assignment in task_assignments { 381 | store.set( 382 | &store.append(), 383 | &[0, 1], 384 | &[&task_assignment.task.name, &task_assignment.task.id], 385 | ); 386 | } 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | use crate::app; 2 | use crate::popup::Popup; 3 | use gio::prelude::*; 4 | use gtk::prelude::*; 5 | use std::env; 6 | use std::sync::atomic::{AtomicI32, Ordering}; 7 | use std::sync::mpsc; 8 | use std::sync::Arc; 9 | use timer_for_harvest::*; 10 | 11 | /* handy gtk callback clone macro taken from https://gtk-rs.org/docs-src/tutorial/closures */ 12 | macro_rules! clone { 13 | (@param _) => ( _ ); 14 | (@param $x:ident) => ( $x ); 15 | ($($n:ident),+ => move || $body:expr) => ( 16 | { 17 | $( let $n = $n.clone(); )+ 18 | move || $body 19 | } 20 | ); 21 | ($($n:ident),+ => move |$($p:tt),+| $body:expr) => ( 22 | { 23 | $( let $n = $n.clone(); )+ 24 | move |$(clone!(@param $p),)+| $body 25 | } 26 | ); 27 | } 28 | 29 | pub enum Signal { 30 | SetTitle(String), 31 | SetTimeEntries(Vec), 32 | OpenPopup(Vec), 33 | OpenPopupWithTimeEntry(Vec, TimeEntry), 34 | TaskAssignments(Vec), 35 | ShowNotice(String), 36 | } 37 | 38 | pub struct Ui { 39 | application: gtk::Application, 40 | header_bar: gtk::HeaderBar, 41 | grid: gtk::Grid, 42 | total_amount_label: gtk::Label, 43 | no_time_entries_label: gtk::Label, 44 | to_app: mpsc::Sender, 45 | popup: Option, 46 | } 47 | 48 | impl Ui { 49 | pub fn new(to_app: mpsc::Sender) -> Ui { 50 | let application = gtk::Application::new( 51 | Some("nl.frankgroeneveld.timer-for-harvest"), 52 | Default::default(), 53 | ) 54 | .unwrap(); 55 | let header_bar = gtk::HeaderBar::new(); 56 | 57 | let grid = gtk::Grid::new(); 58 | grid.set_column_spacing(12); 59 | grid.set_row_spacing(18); 60 | let no_time_entries_label = gtk::Label::new(Some(&"No entries found")); 61 | no_time_entries_label.set_use_markup(true); 62 | no_time_entries_label.set_hexpand(true); 63 | 64 | let total_amount_label = gtk::Label::new(None); 65 | total_amount_label.set_use_markup(true); 66 | total_amount_label.set_label(&"0:00"); 67 | 68 | let total_grid = gtk::Grid::new(); 69 | total_grid.set_border_width(18); 70 | total_grid.set_column_spacing(12); 71 | 72 | let total_label = gtk::Label::new(Some(&"Total")); 73 | total_label.set_use_markup(true); 74 | total_label.set_hexpand(true); 75 | total_label.set_xalign(0.0); 76 | total_grid.attach(&total_label, 0, 0, 1, 1); 77 | 78 | total_grid.attach_next_to( 79 | &total_amount_label, 80 | Some(&total_label), 81 | gtk::PositionType::Right, 82 | 1, 83 | 1, 84 | ); 85 | 86 | application.connect_activate(clone!(to_app, header_bar, grid => move |app| { 87 | gtk::timeout_add_seconds(60, clone!(to_app => move || { 88 | to_app.send(app::Signal::MinutePassed) 89 | .expect("Sending message to application thread"); 90 | glib::Continue(true) 91 | })); 92 | 93 | Ui::main_window(app, &to_app, &header_bar, &grid, &total_grid); 94 | })); 95 | 96 | to_app 97 | .send(app::Signal::CheckVersion) 98 | .expect("Sending message to application thread"); 99 | 100 | Ui { 101 | application: application, 102 | header_bar: header_bar, 103 | grid: grid, 104 | total_amount_label: total_amount_label, 105 | no_time_entries_label: no_time_entries_label, 106 | to_app: to_app, 107 | popup: None, 108 | } 109 | } 110 | 111 | pub fn handle_app_signals(mut ui: Ui, from_app: glib::Receiver) { 112 | let application = ui.application.clone(); 113 | from_app.attach(None, move |signal| { 114 | match signal { 115 | Signal::SetTitle(value) => { 116 | ui.header_bar.set_title(Some(&value)); 117 | } 118 | Signal::SetTimeEntries(time_entries) => { 119 | ui.set_time_entries(time_entries); 120 | } 121 | Signal::OpenPopup(project_assignments) => { 122 | ui.open_popup(project_assignments, vec![], None); 123 | } 124 | Signal::OpenPopupWithTimeEntry(project_assignments, time_entry) => { 125 | let mut task_assignments = vec![]; 126 | for project_assignment in &project_assignments { 127 | if project_assignment.project.id == time_entry.project.id { 128 | task_assignments = project_assignment.task_assignments.clone(); 129 | break; 130 | } 131 | } 132 | ui.open_popup(project_assignments, task_assignments, Some(time_entry)); 133 | } 134 | Signal::TaskAssignments(task_assignments) => match &ui.popup { 135 | Some(popup) => { 136 | popup.load_tasks(task_assignments); 137 | } 138 | None => {} 139 | }, 140 | Signal::ShowNotice(message) => { 141 | let bar = gtk::InfoBar::new(); 142 | let content_area = bar.get_content_area().unwrap(); 143 | let label = gtk::Label::new(None); 144 | label.set_markup(&message); 145 | 146 | content_area 147 | .downcast::() 148 | .unwrap() 149 | .add(&label); 150 | bar.show_all(); 151 | ui.grid.attach(&bar, 0, 0, 4, 1); 152 | } 153 | } 154 | glib::Continue(true) 155 | }); 156 | application.run(&[]); 157 | } 158 | 159 | pub fn main_window( 160 | application: >k::Application, 161 | to_app: &mpsc::Sender, 162 | header_bar: >k::HeaderBar, 163 | grid: >k::Grid, 164 | total_grid: >k::Grid, 165 | ) -> gtk::ApplicationWindow { 166 | let window = gtk::ApplicationWindow::new(application); 167 | 168 | header_bar.set_title(Some("Harvest")); 169 | header_bar.set_show_close_button(true); 170 | 171 | window.set_title("Harvest"); 172 | window.set_titlebar(Some(header_bar)); 173 | window.set_position(gtk::WindowPosition::Center); 174 | 175 | // set default window size by env 176 | let a_w: i32 = env::var("TFH_SIZE_W") 177 | .unwrap_or(String::new()) 178 | .parse() 179 | .unwrap_or(500); 180 | let a_h: i32 = env::var("TFH_SIZE_H") 181 | .unwrap_or(String::new()) 182 | .parse() 183 | .unwrap_or(500); 184 | window.set_default_size(a_w, a_h); 185 | window.set_size_request(a_w, a_h); 186 | 187 | window.add_events(gdk::EventMask::KEY_PRESS_MASK); 188 | window.connect_key_press_event(clone!(to_app => move |_window, event| { 189 | if event.get_keyval() == gdk::enums::key::F5 { 190 | to_app.send(app::Signal::RetrieveTimeEntries) 191 | .expect("Sending message to application thread"); 192 | Inhibit(true) 193 | } else if event.get_keyval() == gdk::enums::key::n { 194 | to_app.send(app::Signal::NewTimeEntry) 195 | .expect("Sending message to application thread"); 196 | Inhibit(true) 197 | } else { 198 | Inhibit(false) 199 | } 200 | })); 201 | 202 | window.connect_property_has_toplevel_focus_notify(clone!(to_app => move |window| { 203 | if window.has_toplevel_focus() { 204 | to_app.send(app::Signal::RetrieveTimeEntries) 205 | .expect("Sending message to application thread"); 206 | } 207 | })); 208 | 209 | let button = 210 | gtk::Button::new_from_icon_name(Some("list-add-symbolic"), gtk::IconSize::Button); 211 | header_bar.pack_start(&button); 212 | button.connect_clicked(clone!(to_app => move |_button| { 213 | to_app.send(app::Signal::NewTimeEntry) 214 | .expect("Sending message to application thread"); 215 | })); 216 | 217 | let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 2); 218 | hbox.set_spacing(0); 219 | hbox.get_style_context().add_class(>k::STYLE_CLASS_LINKED); 220 | let prev_button = 221 | gtk::Button::new_from_icon_name(Some("go-previous-symbolic"), gtk::IconSize::Button); 222 | hbox.pack_start(&prev_button, false, false, 0); 223 | prev_button.connect_clicked(clone!(to_app => move |_button| { 224 | to_app.send(app::Signal::PrevDate) 225 | .expect("Sending message to application thread"); 226 | })); 227 | 228 | let today_button = 229 | gtk::Button::new_from_icon_name(Some("go-home-symbolic"), gtk::IconSize::Button); 230 | hbox.pack_start(&today_button, false, false, 0); 231 | today_button.connect_clicked(clone!(to_app => move |_button| { 232 | to_app.send(app::Signal::TodayDate) 233 | .expect("Sending message to application thread"); 234 | })); 235 | 236 | let next_button = 237 | gtk::Button::new_from_icon_name(Some("go-next-symbolic"), gtk::IconSize::Button); 238 | hbox.pack_start(&next_button, false, false, 0); 239 | header_bar.pack_start(&hbox); 240 | next_button.connect_clicked(clone!(to_app => move |_button| { 241 | to_app.send(app::Signal::NextDate) 242 | .expect("Sending message to application thread"); 243 | })); 244 | 245 | let scroll_view = gtk::ScrolledWindow::new(gtk::NONE_ADJUSTMENT, gtk::NONE_ADJUSTMENT); 246 | scroll_view.set_min_content_height(400); 247 | scroll_view.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Always); 248 | 249 | grid.set_border_width(18); 250 | scroll_view.add(grid); 251 | 252 | let content_box = gtk::Box::new(gtk::Orientation::Vertical, 0); 253 | content_box.pack_start(&scroll_view, true, true, 0); 254 | content_box.pack_start(total_grid, false, false, 0); 255 | 256 | window.add(&content_box); 257 | //window.set_resizable(false); 258 | 259 | let current_height = Arc::new(AtomicI32::new(0)); 260 | grid.connect_size_allocate( 261 | clone!(scroll_view, current_height => move |_grid, allocation| { 262 | if allocation.height > current_height.load(Ordering::SeqCst) { 263 | let adj = scroll_view.get_vadjustment().unwrap(); 264 | adj.set_value(adj.get_upper() - adj.get_page_size()); 265 | current_height.store(allocation.height, Ordering::SeqCst); 266 | } else if allocation.height < current_height.load(Ordering::SeqCst) { 267 | current_height.store(allocation.height, Ordering::SeqCst); 268 | } 269 | }), 270 | ); 271 | 272 | window.show_all(); 273 | 274 | window 275 | } 276 | 277 | pub fn set_total(&self, total_hours: f32) { 278 | let formatted_label = format!("{}", f32_to_duration_str(total_hours)); 279 | self.total_amount_label.set_label(&formatted_label); 280 | } 281 | 282 | pub fn set_time_entries(&mut self, time_entries: Vec) { 283 | let total_entries = time_entries.len() as i32; 284 | let mut total_hours = 0.0; 285 | let mut row_number = total_entries + 1; /* info bar is row 0 */ 286 | 287 | for child in self.grid.get_children() { 288 | if !child.is::() { 289 | self.grid.remove(&child); 290 | } 291 | } 292 | 293 | for time_entry in time_entries { 294 | total_hours += time_entry.hours; 295 | 296 | let notes = match time_entry.notes.as_ref() { 297 | Some(n) => format_timeentry_notes_for_list(n, None), 298 | None => "".to_string(), 299 | }; 300 | 301 | let project_client = format!( 302 | "{} ({})\n{} - {}", 303 | &escape_html(&time_entry.project.name_and_code()), 304 | &escape_html(&time_entry.client.name), 305 | &escape_html(&time_entry.task.name), 306 | ¬es 307 | ); 308 | let project_label = gtk::Label::new(Some(&project_client)); 309 | project_label.set_xalign(0.0); 310 | project_label.set_line_wrap(true); 311 | project_label.set_use_markup(true); 312 | project_label.set_hexpand(true); 313 | self.grid.attach(&project_label, 0, row_number, 1, 1); 314 | 315 | let hours_label = gtk::Label::new(Some(&f32_to_duration_str(time_entry.hours))); 316 | hours_label.set_xalign(0.0); 317 | self.grid.attach(&hours_label, 1, row_number, 1, 1); 318 | 319 | let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 2); 320 | hbox.set_spacing(0); 321 | hbox.get_style_context().add_class(>k::STYLE_CLASS_LINKED); 322 | 323 | let button: gtk::Button; 324 | let to_app = self.to_app.clone(); 325 | let id = time_entry.id; 326 | if time_entry.is_running { 327 | button = gtk::Button::new_from_icon_name( 328 | Some("media-playback-stop-symbolic"), 329 | gtk::IconSize::Button, 330 | ); 331 | button 332 | .get_style_context() 333 | .add_class(>k::STYLE_CLASS_SUGGESTED_ACTION); 334 | button.connect_clicked(move |button| { 335 | button.set_sensitive(false); 336 | to_app 337 | .send(app::Signal::StopTimeEntry(id)) 338 | .expect("Sending message to application thread"); 339 | }); 340 | } else { 341 | button = gtk::Button::new_from_icon_name( 342 | Some("media-playback-start-symbolic"), 343 | gtk::IconSize::Button, 344 | ); 345 | button.connect_clicked(move |button| { 346 | button.set_sensitive(false); 347 | to_app 348 | .send(app::Signal::RestartTimeEntry(id)) 349 | .expect("Sending message to application thread"); 350 | }); 351 | }; 352 | button.set_valign(gtk::Align::Center); 353 | hbox.pack_start(&button, false, false, 0); 354 | 355 | let edit_button = gtk::Button::new_from_icon_name( 356 | Some("document-edit-symbolic"), 357 | gtk::IconSize::Button, 358 | ); 359 | edit_button.set_valign(gtk::Align::Center); 360 | let to_app = self.to_app.clone(); 361 | let id = time_entry.id; 362 | edit_button.connect_clicked(move |button| { 363 | button.set_sensitive(false); 364 | to_app 365 | .send(app::Signal::EditTimeEntry(id)) 366 | .expect("Sending message to application thread"); 367 | }); 368 | hbox.pack_start(&edit_button, false, false, 0); 369 | 370 | self.grid.attach(&hbox, 2, row_number, 1, 1); 371 | 372 | row_number -= 1; 373 | } 374 | 375 | if total_entries == 0 { 376 | self.grid.attach(&self.no_time_entries_label, 0, 0, 1, 1) 377 | } 378 | 379 | self.set_total(total_hours); 380 | 381 | self.grid.show_all(); 382 | } 383 | 384 | fn open_popup( 385 | &mut self, 386 | project_assignments: Vec, 387 | task_assignments: Vec, 388 | time_entry: Option, 389 | ) { 390 | let mut popup = Popup::new(&self.application, project_assignments, self.to_app.clone()); 391 | 392 | popup.load_tasks(task_assignments); 393 | match time_entry { 394 | Some(time_entry) => popup.populate(time_entry), 395 | None => {} 396 | } 397 | /* call after populate, otherwise task active iter is reset */ 398 | popup.connect_signals(); 399 | 400 | self.popup = Some(popup); 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use dirs; 2 | use hyper; 3 | use serde; 4 | use serde_json::json; 5 | use std::fs::write; 6 | use std::fs::File; 7 | use std::io::Read; 8 | use std::io::Write; 9 | use std::net::TcpListener; 10 | use std::net::TcpStream; 11 | use std::path::PathBuf; 12 | use std::process::Command; 13 | use std::time::{SystemTime, UNIX_EPOCH}; 14 | 15 | #[derive(serde::Serialize, serde::Deserialize)] 16 | pub struct Harvest { 17 | token: String, 18 | account_id: u32, 19 | expires_at: u64, 20 | } 21 | 22 | #[derive(serde::Serialize, serde::Deserialize, Clone)] 23 | pub struct Project { 24 | pub id: u32, 25 | pub name: String, 26 | pub code: Option, 27 | pub client: Option, 28 | } 29 | 30 | #[derive(serde::Serialize, serde::Deserialize, Clone)] 31 | pub struct ProjectAssignment { 32 | pub id: u32, 33 | pub project: Project, 34 | pub task_assignments: Vec, 35 | pub client: Client, 36 | } 37 | 38 | #[derive(serde::Serialize, serde::Deserialize, Clone)] 39 | pub struct Client { 40 | pub id: u32, 41 | pub name: String, 42 | } 43 | 44 | #[derive(serde::Serialize, serde::Deserialize, Clone)] 45 | pub struct Task { 46 | pub id: u32, 47 | pub name: String, 48 | } 49 | 50 | #[derive(serde::Serialize, serde::Deserialize, Clone)] 51 | pub struct TaskAssignment { 52 | pub id: u32, 53 | pub task: Task, 54 | } 55 | 56 | #[derive(serde::Serialize, serde::Deserialize, Clone)] 57 | pub struct TimeEntry { 58 | pub id: u32, 59 | pub project: Project, 60 | pub client: Client, 61 | pub hours: f32, 62 | pub user: User, 63 | pub spent_date: String, 64 | pub task: Task, 65 | pub notes: Option, 66 | pub is_running: bool, 67 | } 68 | 69 | /* a partially filled TimeEntry with id's instead of objects (Project etc) */ 70 | #[derive(serde::Serialize, serde::Deserialize)] 71 | pub struct Timer { 72 | pub id: Option, 73 | pub project_id: u32, 74 | pub task_id: u32, 75 | pub spent_date: Option, 76 | pub notes: Option, 77 | pub hours: Option, 78 | pub is_running: bool, 79 | } 80 | 81 | /* a partially filled TimeEntry with id's instead of objects (Project etc) */ 82 | #[derive(serde::Serialize, serde::Deserialize)] 83 | pub struct TimerWithoutHours { 84 | pub id: Option, 85 | pub project_id: u32, 86 | pub task_id: u32, 87 | pub spent_date: Option, 88 | pub notes: Option, 89 | pub is_running: bool, 90 | } 91 | 92 | #[derive(serde::Serialize, serde::Deserialize)] 93 | pub struct ProjectPage { 94 | pub projects: Vec, 95 | pub per_page: u32, 96 | pub total_pages: u32, 97 | pub total_entries: u32, 98 | pub page: u32, 99 | } 100 | 101 | #[derive(serde::Serialize, serde::Deserialize)] 102 | pub struct ProjectAssignmentPage { 103 | pub project_assignments: Vec, 104 | pub per_page: u32, 105 | pub total_pages: u32, 106 | pub total_entries: u32, 107 | pub page: u32, 108 | } 109 | 110 | #[derive(serde::Serialize, serde::Deserialize)] 111 | pub struct TimeEntryPage { 112 | pub time_entries: Vec, 113 | pub per_page: u32, 114 | pub total_pages: u32, 115 | pub total_entries: u32, 116 | pub page: u32, 117 | } 118 | 119 | #[derive(serde::Serialize, serde::Deserialize)] 120 | pub struct TaskAssignmentPage { 121 | pub task_assignments: Vec, 122 | pub per_page: u32, 123 | pub total_pages: u32, 124 | pub total_entries: u32, 125 | pub page: u32, 126 | } 127 | 128 | #[derive(serde::Serialize, serde::Deserialize, Clone)] 129 | pub struct User { 130 | pub id: u32, 131 | } 132 | 133 | impl Project { 134 | pub fn name_and_code(&self) -> String { 135 | if self.code == None || self.code.as_ref().unwrap() == "" { 136 | self.name.clone() 137 | } else { 138 | format!("[{}] {}", self.code.as_ref().unwrap(), self.name) 139 | } 140 | } 141 | } 142 | 143 | impl Harvest { 144 | const CLIENT_ID: &'static str = "ew1-8t73wKHsqmhRNtxwkBaO"; 145 | const CONFIG_FILE_NAME: &'static str = "timer-for-harvest.json"; 146 | 147 | pub fn new() -> Harvest { 148 | match Harvest::read_authorization_from_file() { 149 | Some(harvest) => { 150 | let unix_timestamp = SystemTime::now() 151 | .duration_since(UNIX_EPOCH) 152 | .unwrap() 153 | .as_secs(); 154 | let one_day = 60 * 60 * 24; 155 | 156 | if harvest.expires_at < unix_timestamp + one_day { 157 | Harvest::obtain_new_authorization() 158 | } else { 159 | return harvest; 160 | } 161 | } 162 | None => Harvest::obtain_new_authorization(), 163 | } 164 | } 165 | 166 | fn obtain_new_authorization() -> Harvest { 167 | let listener = TcpListener::bind("127.0.0.1:12345").expect("Port 12345 is already in use"); 168 | 169 | Command::new("xdg-open") 170 | .arg(format!( 171 | "https://id.getharvest.com/oauth2/authorize?client_id={}&response_type=token", 172 | Harvest::CLIENT_ID 173 | )) 174 | .spawn() 175 | .expect("Unable to open browser"); 176 | 177 | for stream in listener.incoming() { 178 | let stream = stream.unwrap(); 179 | let result = Harvest::authorize_callback(stream); 180 | 181 | let unix_timestamp = SystemTime::now() 182 | .duration_since(UNIX_EPOCH) 183 | .unwrap() 184 | .as_secs(); 185 | let expires_in: u64 = result.2.parse().unwrap(); 186 | 187 | let harvest = Harvest { 188 | token: result.0, 189 | account_id: result.1.parse().unwrap(), 190 | expires_at: unix_timestamp + expires_in, 191 | }; 192 | harvest.write_authorization_to_file(); 193 | return harvest; 194 | } 195 | 196 | panic!("unable to authorize"); 197 | } 198 | 199 | fn read_authorization_from_file() -> Option { 200 | match File::open(Harvest::config_file_path()) { 201 | Ok(mut file) => { 202 | let mut content = String::new(); 203 | file.read_to_string(&mut content).unwrap(); 204 | Some( 205 | serde_json::from_str(&content) 206 | .expect(&format!("Invalid configuration file: {}", content).to_string()), 207 | ) 208 | } 209 | Err(_) => None, 210 | } 211 | } 212 | 213 | fn write_authorization_to_file(&self) { 214 | write(Harvest::config_file_path(), json!(self).to_string()) 215 | .expect("unable to save config file"); 216 | } 217 | 218 | fn config_file_path() -> PathBuf { 219 | let mut path = dirs::config_dir().expect("Unable to find XDG config dir path"); 220 | path.push(Harvest::CONFIG_FILE_NAME); 221 | path 222 | } 223 | 224 | fn authorize_callback(mut stream: TcpStream) -> (String, String, String) { 225 | let mut buffer = [0; 512]; 226 | let mut first_line = "".to_string(); 227 | 228 | loop { 229 | match stream.read(&mut buffer) { 230 | Ok(n) => { 231 | if first_line == "" { 232 | let request = String::from_utf8_lossy(&buffer[..]).to_string(); 233 | first_line = request.lines().next().unwrap().to_string(); 234 | } 235 | if n < buffer.len() { 236 | break; 237 | } 238 | } 239 | Err(_) => { 240 | panic!("unable to read request"); 241 | } 242 | } 243 | } 244 | 245 | let result = parse_account_details(&first_line); 246 | 247 | let response = "HTTP/1.1 200 OK\r\n\r\n 248 | 249 | 250 | Authorized successfully 251 | 252 | \r\n"; 253 | 254 | stream.write(response.as_bytes()).unwrap(); 255 | stream.flush().unwrap(); 256 | 257 | result 258 | } 259 | 260 | pub fn user_agent() -> String { 261 | format!( 262 | "{} {}.{}.{}{} ({})", 263 | env!("CARGO_PKG_DESCRIPTION"), 264 | env!("CARGO_PKG_VERSION_MAJOR"), 265 | env!("CARGO_PKG_VERSION_MINOR"), 266 | env!("CARGO_PKG_VERSION_PATCH"), 267 | option_env!("CARGO_PKG_VERSION_PRE").unwrap_or(""), 268 | env!("CARGO_PKG_HOMEPAGE") 269 | ) 270 | } 271 | 272 | pub fn active_project_assignments(&self) -> Vec { 273 | let mut project_assignments: Vec = vec![]; 274 | let mut current_page = 1; 275 | 276 | loop { 277 | let url = format!( 278 | "https://api.harvestapp.com/v2/users/me/project_assignments?page={}", 279 | current_page 280 | ); 281 | let res = self.api_get_request(&url); 282 | let body = &res.unwrap().text().unwrap(); 283 | let page: ProjectAssignmentPage = serde_json::from_str(body).expect( 284 | &format!("Unexpected project assignment page structure: {}", body).to_string(), 285 | ); 286 | 287 | for project_assignment in page.project_assignments { 288 | project_assignments.push(project_assignment); 289 | } 290 | 291 | if current_page == page.total_pages { 292 | break; 293 | } else { 294 | current_page += 1; 295 | } 296 | } 297 | 298 | project_assignments 299 | } 300 | 301 | pub fn time_entries_for(&self, user: &User, from: String, till: String) -> Vec { 302 | let url = format!( 303 | "https://api.harvestapp.com/v2/time_entries?user_id={}&from={}&to={}", 304 | user.id, from, till 305 | ); 306 | match self.api_get_request(&url) { 307 | Ok(res) => { 308 | let body = &res.text().unwrap(); 309 | let page: TimeEntryPage = serde_json::from_str(body) 310 | .expect(&format!("Unexpected time entry page structure: {}", body).to_string()); 311 | 312 | page.time_entries 313 | } 314 | Err(_e) => Vec::new(), 315 | } 316 | } 317 | 318 | pub fn current_user(&self) -> User { 319 | let url = "https://api.harvestapp.com/v2/users/me"; 320 | let res = self.api_get_request(&url); 321 | let body = &res.unwrap().text().unwrap(); 322 | serde_json::from_str(body) 323 | .expect(&format!("Unexpected user structure: {}", body).to_string()) 324 | } 325 | 326 | pub fn start_timer( 327 | &self, 328 | project_id: u32, 329 | task_id: u32, 330 | notes: String, 331 | hours: f32, 332 | now: &chrono::NaiveDate, 333 | ) -> TimeEntry { 334 | let url = "https://api.harvestapp.com/v2/time_entries"; 335 | let mut timer = Timer { 336 | id: None, 337 | project_id: project_id, 338 | task_id: task_id, 339 | spent_date: Some(now.format("%Y-%m-%d").to_string()), 340 | notes: None, 341 | hours: None, 342 | is_running: true, 343 | }; 344 | if notes.len() > 0 { 345 | timer.notes = Some(notes); 346 | } 347 | if hours > 0.0 { 348 | timer.hours = Some(hours); 349 | } 350 | 351 | let res = self.api_post_request(&url, &timer); 352 | let body = &res.text().unwrap(); 353 | serde_json::from_str(body) 354 | .expect(&format!("Unexpected timer structure: {}", body).to_string()) 355 | } 356 | 357 | pub fn restart_timer(&self, time_entry_id: u32) -> TimeEntry { 358 | let url = format!( 359 | "https://api.harvestapp.com/v2/time_entries/{}/restart", 360 | time_entry_id 361 | ); 362 | 363 | let res = self.api_patch_request(&url, &()); 364 | let body = &res.text().unwrap(); 365 | serde_json::from_str(body) 366 | .expect(&format!("Unexpected timer structure: {}", body).to_string()) 367 | } 368 | 369 | pub fn stop_timer(&self, time_entry_id: u32) -> TimeEntry { 370 | let url = format!( 371 | "https://api.harvestapp.com/v2/time_entries/{}/stop", 372 | time_entry_id 373 | ); 374 | 375 | let res = self.api_patch_request(&url, &()); 376 | let body = &res.text().unwrap(); 377 | serde_json::from_str(body) 378 | .expect(&format!("Unexpected timer structure: {}", body).to_string()) 379 | } 380 | 381 | pub fn update_timer( 382 | &self, 383 | id: u32, 384 | project_id: u32, 385 | task_id: u32, 386 | notes: String, 387 | hours: f32, 388 | is_running: bool, 389 | spent_date: String, 390 | ) -> TimeEntry { 391 | let url = format!("https://api.harvestapp.com/v2/time_entries/{}", id); 392 | 393 | /* TODO how not to sent hours when is_running in a better way? */ 394 | if is_running { 395 | let t2 = TimerWithoutHours { 396 | id: Some(id), 397 | project_id: project_id, 398 | task_id: task_id, 399 | notes: Some(notes), 400 | is_running: is_running, 401 | spent_date: Some(spent_date), 402 | }; 403 | 404 | let res = self.api_patch_request(&url, &t2); 405 | let body = &res.text().unwrap(); 406 | serde_json::from_str(body) 407 | .expect(&format!("Unexpected time entry structure: {}", body).to_string()) 408 | } else { 409 | let timer = Timer { 410 | id: Some(id), 411 | project_id: project_id, 412 | task_id: task_id, 413 | notes: Some(notes), 414 | is_running: is_running, 415 | hours: Some(hours), 416 | spent_date: Some(spent_date), 417 | }; 418 | let res = self.api_patch_request(&url, &timer); 419 | let body = &res.text().unwrap(); 420 | serde_json::from_str(body) 421 | .expect(&format!("Unexpected time entry structure: {}", body).to_string()) 422 | } 423 | } 424 | 425 | pub fn delete_timer(&self, timer_id: u32) -> TimeEntry { 426 | let url = format!("https://api.harvestapp.com/v2/time_entries/{}", timer_id); 427 | 428 | let res = self.api_delete_request(&url); 429 | let body = &res.text().unwrap(); 430 | serde_json::from_str(body) 431 | .expect(&format!("Unexpected timer structure: {}", body).to_string()) 432 | } 433 | 434 | fn api_get_request(&self, url: &str) -> Result { 435 | let client = reqwest::blocking::Client::new(); 436 | 437 | client 438 | .get(url) 439 | .header("Authorization", format!("Bearer {}", self.token)) 440 | .header("Harvest-Account-Id", format!("{}", self.account_id)) 441 | .header("User-Agent", Harvest::user_agent()) 442 | .send() 443 | } 444 | 445 | fn api_post_request( 446 | &self, 447 | url: &str, 448 | json: &T, 449 | ) -> reqwest::blocking::Response { 450 | let client = reqwest::blocking::Client::new(); 451 | 452 | client 453 | .post(url) 454 | .json(&json) 455 | .header("Authorization", format!("Bearer {}", self.token)) 456 | .header("Harvest-Account-Id", format!("{}", self.account_id)) 457 | .header("User-Agent", Harvest::user_agent()) 458 | .send() 459 | .unwrap() 460 | } 461 | 462 | fn api_delete_request(&self, url: &str) -> reqwest::blocking::Response { 463 | let client = reqwest::blocking::Client::new(); 464 | 465 | client 466 | .delete(url) 467 | .header("Authorization", format!("Bearer {}", self.token)) 468 | .header("Harvest-Account-Id", format!("{}", self.account_id)) 469 | .header("User-Agent", Harvest::user_agent()) 470 | .send() 471 | .unwrap() 472 | } 473 | 474 | fn api_patch_request( 475 | &self, 476 | url: &str, 477 | json: &T, 478 | ) -> reqwest::blocking::Response { 479 | let client = reqwest::blocking::Client::new(); 480 | 481 | client 482 | .patch(url) 483 | .json(&json) 484 | .header("Authorization", format!("Bearer {}", self.token)) 485 | .header("Harvest-Account-Id", format!("{}", self.account_id)) 486 | .header("User-Agent", Harvest::user_agent()) 487 | .send() 488 | .unwrap() 489 | } 490 | } 491 | 492 | /* TODO move to TimeEntry */ 493 | pub fn duration_str_to_f32(duration: &str) -> f32 { 494 | if duration.len() > 0 { 495 | let mut parts = duration.split(":"); 496 | let hours: f32 = match parts.next() { 497 | None => 0.0, 498 | Some(h) => match h.parse() { 499 | Ok(p) => p, 500 | Err(_) => 0.0, 501 | }, 502 | }; 503 | let minutes: f32 = match parts.next() { 504 | None => 0.0, 505 | Some(m) => match m.parse() { 506 | Ok(p) => p, 507 | Err(_) => 0.0, 508 | }, 509 | }; 510 | hours + minutes / 60.0 511 | } else { 512 | 0.0 513 | } 514 | } 515 | 516 | /* TODO move to TimeEntry */ 517 | pub fn f32_to_duration_str(duration: f32) -> String { 518 | let mut minutes = duration % 1.0; 519 | let mut hours = duration - minutes; 520 | 521 | if format!("{:0>2.0}", minutes * 60.0) == "60" { 522 | minutes = 0.0; 523 | hours += 1.0; 524 | } 525 | format!("{:.0}:{:0>2.0}", hours, minutes * 60.0) 526 | } 527 | 528 | /* TODO improve this messy parse function */ 529 | pub fn parse_account_details(request: &str) -> (String, String, String) { 530 | let mut parts = request.split(" "); 531 | parts.next(); /* GET */ 532 | let uri = parts.next().unwrap().parse::().unwrap(); 533 | let parts = uri.query().unwrap().split("&"); 534 | let mut access_token = ""; 535 | let mut account_id = ""; 536 | let mut expires_in = ""; 537 | 538 | for part in parts { 539 | if part.starts_with("access_token") { 540 | let mut parts = part.split("="); 541 | parts.next(); 542 | access_token = parts.next().unwrap(); 543 | } else if part.starts_with("scope") { 544 | let mut parts = part.split("="); 545 | parts.next(); 546 | account_id = parts.next().unwrap(); 547 | let mut parts = account_id.split("%3A"); 548 | parts.next(); 549 | account_id = parts.next().unwrap(); 550 | } else if part.starts_with("expires_in") { 551 | let mut parts = part.split("="); 552 | parts.next(); 553 | expires_in = parts.next().unwrap(); 554 | } 555 | } 556 | ( 557 | access_token.to_string(), 558 | account_id.to_string(), 559 | expires_in.to_string(), 560 | ) 561 | } 562 | 563 | pub fn escape_html(subject: &str) -> String { 564 | subject 565 | .replace("&", "&") 566 | .replace("<", "<") 567 | .replace(">", ">") 568 | .replace("\"", """) 569 | .replace("'", "'") 570 | .replace("`", "`") 571 | } 572 | 573 | pub fn format_timeentry_notes_for_list(n: &str, length: Option) -> std::string::String { 574 | let take: usize = match length { 575 | Some(value) => value, 576 | None => 80, 577 | }; 578 | 579 | let formatted: String = escape_html(n) 580 | .replace("\n\n", "\n") 581 | .replace("\n", " - ") 582 | .chars() 583 | .take(take) 584 | .collect(); 585 | 586 | if n.chars().count() > take { 587 | formatted.clone() + "..." 588 | } else { 589 | formatted 590 | } 591 | } 592 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "arrayref" 7 | version = "0.3.5" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee" 10 | 11 | [[package]] 12 | name = "arrayvec" 13 | version = "0.5.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" 16 | 17 | [[package]] 18 | name = "atk" 19 | version = "0.7.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "86b7499272acf036bb5820c6e346bbfb5acc5dceb104bc2c4fd7e6e33dfcde6a" 22 | dependencies = [ 23 | "atk-sys", 24 | "bitflags", 25 | "glib", 26 | "glib-sys", 27 | "gobject-sys", 28 | "libc", 29 | ] 30 | 31 | [[package]] 32 | name = "atk-sys" 33 | version = "0.9.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "5067531f752c01027f004032bb676e715aba74b75e904a7340a61ce3fb0b61b0" 36 | dependencies = [ 37 | "glib-sys", 38 | "gobject-sys", 39 | "libc", 40 | "pkg-config", 41 | ] 42 | 43 | [[package]] 44 | name = "autocfg" 45 | version = "0.1.4" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "0e49efa51329a5fd37e7c79db4621af617cd4e3e5bc224939808d076077077bf" 48 | 49 | [[package]] 50 | name = "autocfg" 51 | version = "1.1.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 54 | 55 | [[package]] 56 | name = "backtrace" 57 | version = "0.3.30" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "ada4c783bb7e7443c14e0480f429ae2cc99da95065aeab7ee1b81ada0419404f" 60 | dependencies = [ 61 | "autocfg 0.1.4", 62 | "backtrace-sys", 63 | "cfg-if 0.1.9", 64 | "libc", 65 | "rustc-demangle", 66 | ] 67 | 68 | [[package]] 69 | name = "backtrace-sys" 70 | version = "0.1.28" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6" 73 | dependencies = [ 74 | "cc", 75 | "libc", 76 | ] 77 | 78 | [[package]] 79 | name = "base64" 80 | version = "0.10.1" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" 83 | dependencies = [ 84 | "byteorder", 85 | ] 86 | 87 | [[package]] 88 | name = "base64" 89 | version = "0.13.0" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 92 | 93 | [[package]] 94 | name = "bitflags" 95 | version = "1.3.2" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 98 | 99 | [[package]] 100 | name = "blake2b_simd" 101 | version = "0.5.9" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "b83b7baab1e671718d78204225800d6b170e648188ac7dc992e9d6bddf87d0c0" 104 | dependencies = [ 105 | "arrayref", 106 | "arrayvec", 107 | "constant_time_eq", 108 | ] 109 | 110 | [[package]] 111 | name = "bumpalo" 112 | version = "3.10.0" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" 115 | 116 | [[package]] 117 | name = "byteorder" 118 | version = "1.3.2" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" 121 | 122 | [[package]] 123 | name = "bytes" 124 | version = "1.1.0" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" 127 | 128 | [[package]] 129 | name = "cairo-rs" 130 | version = "0.7.1" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "e05db47de3b0f09a222fa4bba2eab957d920d4243962a86b2d77ab401e4a359c" 133 | dependencies = [ 134 | "bitflags", 135 | "cairo-sys-rs", 136 | "glib", 137 | "glib-sys", 138 | "gobject-sys", 139 | "libc", 140 | ] 141 | 142 | [[package]] 143 | name = "cairo-sys-rs" 144 | version = "0.9.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "90a1ec04603a78c111886a385edcec396dbfbc57ea26b9e74aeea6a1fe55dcca" 147 | dependencies = [ 148 | "glib-sys", 149 | "libc", 150 | "pkg-config", 151 | "winapi", 152 | ] 153 | 154 | [[package]] 155 | name = "cc" 156 | version = "1.0.37" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "39f75544d7bbaf57560d2168f28fd649ff9c76153874db88bdbdfd839b1a7e7d" 159 | 160 | [[package]] 161 | name = "cfg-if" 162 | version = "0.1.9" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" 165 | 166 | [[package]] 167 | name = "cfg-if" 168 | version = "1.0.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 171 | 172 | [[package]] 173 | name = "chrono" 174 | version = "0.4.9" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "e8493056968583b0193c1bb04d6f7684586f3726992d6c573261941a895dbd68" 177 | dependencies = [ 178 | "libc", 179 | "num-integer", 180 | "num-traits", 181 | "time", 182 | ] 183 | 184 | [[package]] 185 | name = "cloudabi" 186 | version = "0.0.3" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" 189 | dependencies = [ 190 | "bitflags", 191 | ] 192 | 193 | [[package]] 194 | name = "constant_time_eq" 195 | version = "0.1.4" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "995a44c877f9212528ccc74b21a232f66ad69001e40ede5bcee2ac9ef2657120" 198 | 199 | [[package]] 200 | name = "core-foundation" 201 | version = "0.9.3" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 204 | dependencies = [ 205 | "core-foundation-sys", 206 | "libc", 207 | ] 208 | 209 | [[package]] 210 | name = "core-foundation-sys" 211 | version = "0.8.3" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 214 | 215 | [[package]] 216 | name = "crossbeam-utils" 217 | version = "0.6.5" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "f8306fcef4a7b563b76b7dd949ca48f52bc1141aa067d2ea09565f3e2652aa5c" 220 | dependencies = [ 221 | "cfg-if 0.1.9", 222 | "lazy_static", 223 | ] 224 | 225 | [[package]] 226 | name = "dirs" 227 | version = "2.0.2" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" 230 | dependencies = [ 231 | "cfg-if 0.1.9", 232 | "dirs-sys", 233 | ] 234 | 235 | [[package]] 236 | name = "dirs-sys" 237 | version = "0.3.4" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" 240 | dependencies = [ 241 | "cfg-if 0.1.9", 242 | "libc", 243 | "redox_users", 244 | "winapi", 245 | ] 246 | 247 | [[package]] 248 | name = "encoding_rs" 249 | version = "0.8.17" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "4155785c79f2f6701f185eb2e6b4caf0555ec03477cb4c70db67b465311620ed" 252 | dependencies = [ 253 | "cfg-if 0.1.9", 254 | ] 255 | 256 | [[package]] 257 | name = "failure" 258 | version = "0.1.8" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" 261 | dependencies = [ 262 | "backtrace", 263 | "failure_derive", 264 | ] 265 | 266 | [[package]] 267 | name = "failure_derive" 268 | version = "0.1.8" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" 271 | dependencies = [ 272 | "proc-macro2", 273 | "quote", 274 | "syn", 275 | "synstructure", 276 | ] 277 | 278 | [[package]] 279 | name = "fastrand" 280 | version = "1.7.0" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" 283 | dependencies = [ 284 | "instant", 285 | ] 286 | 287 | [[package]] 288 | name = "fnv" 289 | version = "1.0.6" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" 292 | 293 | [[package]] 294 | name = "foreign-types" 295 | version = "0.3.2" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 298 | dependencies = [ 299 | "foreign-types-shared", 300 | ] 301 | 302 | [[package]] 303 | name = "foreign-types-shared" 304 | version = "0.1.1" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 307 | 308 | [[package]] 309 | name = "form_urlencoded" 310 | version = "1.0.1" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 313 | dependencies = [ 314 | "matches", 315 | "percent-encoding", 316 | ] 317 | 318 | [[package]] 319 | name = "fragile" 320 | version = "0.3.0" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "05f8140122fa0d5dcb9fc8627cfce2b37cc1500f752636d46ea28bc26785c2f9" 323 | 324 | [[package]] 325 | name = "fuchsia-cprng" 326 | version = "0.1.1" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 329 | 330 | [[package]] 331 | name = "futures-channel" 332 | version = "0.3.21" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" 335 | dependencies = [ 336 | "futures-core", 337 | ] 338 | 339 | [[package]] 340 | name = "futures-core" 341 | version = "0.3.21" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" 344 | 345 | [[package]] 346 | name = "futures-io" 347 | version = "0.3.21" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" 350 | 351 | [[package]] 352 | name = "futures-sink" 353 | version = "0.3.21" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" 356 | 357 | [[package]] 358 | name = "futures-task" 359 | version = "0.3.21" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" 362 | 363 | [[package]] 364 | name = "futures-util" 365 | version = "0.3.21" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" 368 | dependencies = [ 369 | "futures-core", 370 | "futures-io", 371 | "futures-task", 372 | "memchr", 373 | "pin-project-lite", 374 | "pin-utils", 375 | "slab", 376 | ] 377 | 378 | [[package]] 379 | name = "gdk" 380 | version = "0.11.0" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "6243e995f41f3a61a31847e54cc719edce93dd9140c89dca3b9919be1cfe22d5" 383 | dependencies = [ 384 | "bitflags", 385 | "cairo-rs", 386 | "cairo-sys-rs", 387 | "gdk-pixbuf", 388 | "gdk-sys", 389 | "gio", 390 | "gio-sys", 391 | "glib", 392 | "glib-sys", 393 | "gobject-sys", 394 | "libc", 395 | "pango", 396 | ] 397 | 398 | [[package]] 399 | name = "gdk-pixbuf" 400 | version = "0.7.0" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "9726408ee1bbada83094326a99b9c68fea275f9dbb515de242a69e72051f4fcc" 403 | dependencies = [ 404 | "gdk-pixbuf-sys", 405 | "gio", 406 | "gio-sys", 407 | "glib", 408 | "glib-sys", 409 | "gobject-sys", 410 | "libc", 411 | ] 412 | 413 | [[package]] 414 | name = "gdk-pixbuf-sys" 415 | version = "0.9.0" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "b1d6778abf5764b9080a9345a16c5d16289426a3b3edd808a29a9061d431c465" 418 | dependencies = [ 419 | "gio-sys", 420 | "glib-sys", 421 | "gobject-sys", 422 | "libc", 423 | "pkg-config", 424 | ] 425 | 426 | [[package]] 427 | name = "gdk-sys" 428 | version = "0.9.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "3ebe06357212127f50575b535bdb04638f5d375bb41062287abc6c94e5b8067b" 431 | dependencies = [ 432 | "cairo-sys-rs", 433 | "gdk-pixbuf-sys", 434 | "gio-sys", 435 | "glib-sys", 436 | "gobject-sys", 437 | "libc", 438 | "pango-sys", 439 | "pkg-config", 440 | ] 441 | 442 | [[package]] 443 | name = "gio" 444 | version = "0.7.0" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "6261b5d34c30c2d59f879e643704cf54cb44731f3a2038000b68790c03e360e3" 447 | dependencies = [ 448 | "bitflags", 449 | "fragile", 450 | "gio-sys", 451 | "glib", 452 | "glib-sys", 453 | "gobject-sys", 454 | "lazy_static", 455 | "libc", 456 | ] 457 | 458 | [[package]] 459 | name = "gio-sys" 460 | version = "0.9.0" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "778b856a70a32e2cc5dd5cc7fa1b0c4b6df924fdf5c82984bc28f30565657cfe" 463 | dependencies = [ 464 | "glib-sys", 465 | "gobject-sys", 466 | "libc", 467 | "pkg-config", 468 | ] 469 | 470 | [[package]] 471 | name = "glib" 472 | version = "0.8.1" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "91a70db179515473b57aaff8b879167f1f8460bc5523e97beacf6d1026a8b99d" 475 | dependencies = [ 476 | "bitflags", 477 | "glib-sys", 478 | "gobject-sys", 479 | "lazy_static", 480 | "libc", 481 | ] 482 | 483 | [[package]] 484 | name = "glib-sys" 485 | version = "0.9.1" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "95856f3802f446c05feffa5e24859fe6a183a7cb849c8449afc35c86b1e316e2" 488 | dependencies = [ 489 | "libc", 490 | "pkg-config", 491 | ] 492 | 493 | [[package]] 494 | name = "gobject-sys" 495 | version = "0.9.0" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "61d55bc9202447ca776f6ad0048c36e3312010f66f82ab478e97513e93f3604b" 498 | dependencies = [ 499 | "glib-sys", 500 | "libc", 501 | "pkg-config", 502 | ] 503 | 504 | [[package]] 505 | name = "gtk" 506 | version = "0.7.0" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "709f1074259d4685b96133f92b75c7f35b504715b0fcdc96ec95de2607296a60" 509 | dependencies = [ 510 | "atk", 511 | "bitflags", 512 | "cairo-rs", 513 | "cairo-sys-rs", 514 | "cc", 515 | "gdk", 516 | "gdk-pixbuf", 517 | "gdk-pixbuf-sys", 518 | "gdk-sys", 519 | "gio", 520 | "gio-sys", 521 | "glib", 522 | "glib-sys", 523 | "gobject-sys", 524 | "gtk-sys", 525 | "lazy_static", 526 | "libc", 527 | "pango", 528 | "pango-sys", 529 | ] 530 | 531 | [[package]] 532 | name = "gtk-sys" 533 | version = "0.9.0" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "bbd9395497ae1d1915d1d6e522d51ae8745bf613906c34ac191c411250fc4025" 536 | dependencies = [ 537 | "atk-sys", 538 | "cairo-sys-rs", 539 | "gdk-pixbuf-sys", 540 | "gdk-sys", 541 | "gio-sys", 542 | "glib-sys", 543 | "gobject-sys", 544 | "libc", 545 | "pango-sys", 546 | "pkg-config", 547 | ] 548 | 549 | [[package]] 550 | name = "h2" 551 | version = "0.3.13" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" 554 | dependencies = [ 555 | "bytes", 556 | "fnv", 557 | "futures-core", 558 | "futures-sink", 559 | "futures-util", 560 | "http", 561 | "indexmap", 562 | "slab", 563 | "tokio", 564 | "tokio-util", 565 | "tracing", 566 | ] 567 | 568 | [[package]] 569 | name = "hashbrown" 570 | version = "0.11.2" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 573 | 574 | [[package]] 575 | name = "hermit-abi" 576 | version = "0.1.19" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 579 | dependencies = [ 580 | "libc", 581 | ] 582 | 583 | [[package]] 584 | name = "http" 585 | version = "0.2.8" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" 588 | dependencies = [ 589 | "bytes", 590 | "fnv", 591 | "itoa 1.0.2", 592 | ] 593 | 594 | [[package]] 595 | name = "http-body" 596 | version = "0.4.5" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 599 | dependencies = [ 600 | "bytes", 601 | "http", 602 | "pin-project-lite", 603 | ] 604 | 605 | [[package]] 606 | name = "httparse" 607 | version = "1.7.1" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" 610 | 611 | [[package]] 612 | name = "httpdate" 613 | version = "1.0.2" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 616 | 617 | [[package]] 618 | name = "hyper" 619 | version = "0.14.12" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "13f67199e765030fa08fe0bd581af683f0d5bc04ea09c2b1102012c5fb90e7fd" 622 | dependencies = [ 623 | "bytes", 624 | "futures-channel", 625 | "futures-core", 626 | "futures-util", 627 | "h2", 628 | "http", 629 | "http-body", 630 | "httparse", 631 | "httpdate", 632 | "itoa 0.4.4", 633 | "pin-project-lite", 634 | "socket2", 635 | "tokio", 636 | "tower-service", 637 | "tracing", 638 | "want", 639 | ] 640 | 641 | [[package]] 642 | name = "hyper-tls" 643 | version = "0.5.0" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 646 | dependencies = [ 647 | "bytes", 648 | "hyper", 649 | "native-tls", 650 | "tokio", 651 | "tokio-native-tls", 652 | ] 653 | 654 | [[package]] 655 | name = "idna" 656 | version = "0.2.3" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 659 | dependencies = [ 660 | "matches", 661 | "unicode-bidi", 662 | "unicode-normalization", 663 | ] 664 | 665 | [[package]] 666 | name = "indexmap" 667 | version = "1.8.2" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" 670 | dependencies = [ 671 | "autocfg 1.1.0", 672 | "hashbrown", 673 | ] 674 | 675 | [[package]] 676 | name = "instant" 677 | version = "0.1.12" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 680 | dependencies = [ 681 | "cfg-if 1.0.0", 682 | ] 683 | 684 | [[package]] 685 | name = "ipnet" 686 | version = "2.5.0" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" 689 | 690 | [[package]] 691 | name = "itoa" 692 | version = "0.4.4" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" 695 | 696 | [[package]] 697 | name = "itoa" 698 | version = "1.0.2" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" 701 | 702 | [[package]] 703 | name = "js-sys" 704 | version = "0.3.57" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" 707 | dependencies = [ 708 | "wasm-bindgen", 709 | ] 710 | 711 | [[package]] 712 | name = "lazy_static" 713 | version = "1.4.0" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 716 | 717 | [[package]] 718 | name = "libc" 719 | version = "0.2.126" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 722 | 723 | [[package]] 724 | name = "libresolv-sys" 725 | version = "0.2.0" 726 | source = "git+https://github.com/mikedilger/resolv-rs?rev=63fce7c9c9b88a7c2c453bcf90c1eabb67500449#63fce7c9c9b88a7c2c453bcf90c1eabb67500449" 727 | dependencies = [ 728 | "libc", 729 | ] 730 | 731 | [[package]] 732 | name = "log" 733 | version = "0.4.17" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 736 | dependencies = [ 737 | "cfg-if 1.0.0", 738 | ] 739 | 740 | [[package]] 741 | name = "matches" 742 | version = "0.1.8" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 745 | 746 | [[package]] 747 | name = "memchr" 748 | version = "2.5.0" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 751 | 752 | [[package]] 753 | name = "mime" 754 | version = "0.3.16" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 757 | 758 | [[package]] 759 | name = "mio" 760 | version = "0.8.3" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" 763 | dependencies = [ 764 | "libc", 765 | "log", 766 | "wasi", 767 | "windows-sys", 768 | ] 769 | 770 | [[package]] 771 | name = "native-tls" 772 | version = "0.2.10" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" 775 | dependencies = [ 776 | "lazy_static", 777 | "libc", 778 | "log", 779 | "openssl", 780 | "openssl-probe", 781 | "openssl-sys", 782 | "schannel", 783 | "security-framework", 784 | "security-framework-sys", 785 | "tempfile", 786 | ] 787 | 788 | [[package]] 789 | name = "num-integer" 790 | version = "0.1.41" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" 793 | dependencies = [ 794 | "autocfg 0.1.4", 795 | "num-traits", 796 | ] 797 | 798 | [[package]] 799 | name = "num-traits" 800 | version = "0.2.8" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" 803 | dependencies = [ 804 | "autocfg 0.1.4", 805 | ] 806 | 807 | [[package]] 808 | name = "num_cpus" 809 | version = "1.13.1" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 812 | dependencies = [ 813 | "hermit-abi", 814 | "libc", 815 | ] 816 | 817 | [[package]] 818 | name = "once_cell" 819 | version = "1.12.0" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" 822 | 823 | [[package]] 824 | name = "openssl" 825 | version = "0.10.40" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e" 828 | dependencies = [ 829 | "bitflags", 830 | "cfg-if 1.0.0", 831 | "foreign-types", 832 | "libc", 833 | "once_cell", 834 | "openssl-macros", 835 | "openssl-sys", 836 | ] 837 | 838 | [[package]] 839 | name = "openssl-macros" 840 | version = "0.1.0" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" 843 | dependencies = [ 844 | "proc-macro2", 845 | "quote", 846 | "syn", 847 | ] 848 | 849 | [[package]] 850 | name = "openssl-probe" 851 | version = "0.1.2" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" 854 | 855 | [[package]] 856 | name = "openssl-sys" 857 | version = "0.9.74" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1" 860 | dependencies = [ 861 | "autocfg 1.1.0", 862 | "cc", 863 | "libc", 864 | "pkg-config", 865 | "vcpkg", 866 | ] 867 | 868 | [[package]] 869 | name = "pango" 870 | version = "0.7.0" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "393fa071b144f8ffb83ede273758983cf414ca3c0b1d2a5a9ce325b3ba3dd786" 873 | dependencies = [ 874 | "bitflags", 875 | "glib", 876 | "glib-sys", 877 | "gobject-sys", 878 | "lazy_static", 879 | "libc", 880 | "pango-sys", 881 | ] 882 | 883 | [[package]] 884 | name = "pango-sys" 885 | version = "0.9.0" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "1ee97abcad820f9875e032656257ad1c790e7b11a0e6ce2516a8f5b0d8f8213f" 888 | dependencies = [ 889 | "glib-sys", 890 | "gobject-sys", 891 | "libc", 892 | "pkg-config", 893 | ] 894 | 895 | [[package]] 896 | name = "percent-encoding" 897 | version = "2.1.0" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 900 | 901 | [[package]] 902 | name = "pin-project-lite" 903 | version = "0.2.9" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 906 | 907 | [[package]] 908 | name = "pin-utils" 909 | version = "0.1.0" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 912 | 913 | [[package]] 914 | name = "pkg-config" 915 | version = "0.3.14" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" 918 | 919 | [[package]] 920 | name = "proc-macro2" 921 | version = "1.0.39" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" 924 | dependencies = [ 925 | "unicode-ident", 926 | ] 927 | 928 | [[package]] 929 | name = "quote" 930 | version = "1.0.18" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" 933 | dependencies = [ 934 | "proc-macro2", 935 | ] 936 | 937 | [[package]] 938 | name = "rand_core" 939 | version = "0.3.1" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 942 | dependencies = [ 943 | "rand_core 0.4.0", 944 | ] 945 | 946 | [[package]] 947 | name = "rand_core" 948 | version = "0.4.0" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "d0e7a549d590831370895ab7ba4ea0c1b6b011d106b5ff2da6eee112615e6dc0" 951 | 952 | [[package]] 953 | name = "rand_os" 954 | version = "0.1.3" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" 957 | dependencies = [ 958 | "cloudabi", 959 | "fuchsia-cprng", 960 | "libc", 961 | "rand_core 0.4.0", 962 | "rdrand", 963 | "winapi", 964 | ] 965 | 966 | [[package]] 967 | name = "rdrand" 968 | version = "0.4.0" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 971 | dependencies = [ 972 | "rand_core 0.3.1", 973 | ] 974 | 975 | [[package]] 976 | name = "redox_syscall" 977 | version = "0.1.54" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "12229c14a0f65c4f1cb046a3b52047cdd9da1f4b30f8a39c5063c8bae515e252" 980 | 981 | [[package]] 982 | name = "redox_syscall" 983 | version = "0.2.13" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" 986 | dependencies = [ 987 | "bitflags", 988 | ] 989 | 990 | [[package]] 991 | name = "redox_users" 992 | version = "0.3.1" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "4ecedbca3bf205f8d8f5c2b44d83cd0690e39ee84b951ed649e9f1841132b66d" 995 | dependencies = [ 996 | "failure", 997 | "rand_os", 998 | "redox_syscall 0.1.54", 999 | "rust-argon2", 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "remove_dir_all" 1004 | version = "0.5.2" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" 1007 | dependencies = [ 1008 | "winapi", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "reqwest" 1013 | version = "0.11.10" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" 1016 | dependencies = [ 1017 | "base64 0.13.0", 1018 | "bytes", 1019 | "encoding_rs", 1020 | "futures-core", 1021 | "futures-util", 1022 | "h2", 1023 | "http", 1024 | "http-body", 1025 | "hyper", 1026 | "hyper-tls", 1027 | "ipnet", 1028 | "js-sys", 1029 | "lazy_static", 1030 | "log", 1031 | "mime", 1032 | "native-tls", 1033 | "percent-encoding", 1034 | "pin-project-lite", 1035 | "serde", 1036 | "serde_json", 1037 | "serde_urlencoded", 1038 | "tokio", 1039 | "tokio-native-tls", 1040 | "url", 1041 | "wasm-bindgen", 1042 | "wasm-bindgen-futures", 1043 | "web-sys", 1044 | "winreg", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "resolv" 1049 | version = "0.2.0" 1050 | source = "git+https://github.com/mikedilger/resolv-rs?rev=63fce7c9c9b88a7c2c453bcf90c1eabb67500449#63fce7c9c9b88a7c2c453bcf90c1eabb67500449" 1051 | dependencies = [ 1052 | "byteorder", 1053 | "libc", 1054 | "libresolv-sys", 1055 | ] 1056 | 1057 | [[package]] 1058 | name = "rust-argon2" 1059 | version = "0.5.1" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "4ca4eaef519b494d1f2848fc602d18816fed808a981aedf4f1f00ceb7c9d32cf" 1062 | dependencies = [ 1063 | "base64 0.10.1", 1064 | "blake2b_simd", 1065 | "crossbeam-utils", 1066 | ] 1067 | 1068 | [[package]] 1069 | name = "rustc-demangle" 1070 | version = "0.1.15" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "a7f4dccf6f4891ebcc0c39f9b6eb1a83b9bf5d747cb439ec6fba4f3b977038af" 1073 | 1074 | [[package]] 1075 | name = "ryu" 1076 | version = "1.0.0" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997" 1079 | 1080 | [[package]] 1081 | name = "schannel" 1082 | version = "0.1.20" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" 1085 | dependencies = [ 1086 | "lazy_static", 1087 | "windows-sys", 1088 | ] 1089 | 1090 | [[package]] 1091 | name = "security-framework" 1092 | version = "2.6.1" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" 1095 | dependencies = [ 1096 | "bitflags", 1097 | "core-foundation", 1098 | "core-foundation-sys", 1099 | "libc", 1100 | "security-framework-sys", 1101 | ] 1102 | 1103 | [[package]] 1104 | name = "security-framework-sys" 1105 | version = "2.6.1" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" 1108 | dependencies = [ 1109 | "core-foundation-sys", 1110 | "libc", 1111 | ] 1112 | 1113 | [[package]] 1114 | name = "serde" 1115 | version = "1.0.137" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" 1118 | dependencies = [ 1119 | "serde_derive", 1120 | ] 1121 | 1122 | [[package]] 1123 | name = "serde_derive" 1124 | version = "1.0.137" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" 1127 | dependencies = [ 1128 | "proc-macro2", 1129 | "quote", 1130 | "syn", 1131 | ] 1132 | 1133 | [[package]] 1134 | name = "serde_json" 1135 | version = "1.0.81" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" 1138 | dependencies = [ 1139 | "itoa 1.0.2", 1140 | "ryu", 1141 | "serde", 1142 | ] 1143 | 1144 | [[package]] 1145 | name = "serde_urlencoded" 1146 | version = "0.7.1" 1147 | source = "registry+https://github.com/rust-lang/crates.io-index" 1148 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1149 | dependencies = [ 1150 | "form_urlencoded", 1151 | "itoa 1.0.2", 1152 | "ryu", 1153 | "serde", 1154 | ] 1155 | 1156 | [[package]] 1157 | name = "slab" 1158 | version = "0.4.2" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 1161 | 1162 | [[package]] 1163 | name = "socket2" 1164 | version = "0.4.4" 1165 | source = "registry+https://github.com/rust-lang/crates.io-index" 1166 | checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" 1167 | dependencies = [ 1168 | "libc", 1169 | "winapi", 1170 | ] 1171 | 1172 | [[package]] 1173 | name = "syn" 1174 | version = "1.0.96" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" 1177 | dependencies = [ 1178 | "proc-macro2", 1179 | "quote", 1180 | "unicode-ident", 1181 | ] 1182 | 1183 | [[package]] 1184 | name = "synstructure" 1185 | version = "0.12.6" 1186 | source = "registry+https://github.com/rust-lang/crates.io-index" 1187 | checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" 1188 | dependencies = [ 1189 | "proc-macro2", 1190 | "quote", 1191 | "syn", 1192 | "unicode-xid", 1193 | ] 1194 | 1195 | [[package]] 1196 | name = "tempfile" 1197 | version = "3.3.0" 1198 | source = "registry+https://github.com/rust-lang/crates.io-index" 1199 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 1200 | dependencies = [ 1201 | "cfg-if 1.0.0", 1202 | "fastrand", 1203 | "libc", 1204 | "redox_syscall 0.2.13", 1205 | "remove_dir_all", 1206 | "winapi", 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "time" 1211 | version = "0.1.42" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" 1214 | dependencies = [ 1215 | "libc", 1216 | "redox_syscall 0.1.54", 1217 | "winapi", 1218 | ] 1219 | 1220 | [[package]] 1221 | name = "timer-for-harvest" 1222 | version = "0.3.11" 1223 | dependencies = [ 1224 | "chrono", 1225 | "dirs", 1226 | "gdk", 1227 | "gio", 1228 | "glib", 1229 | "glib-sys", 1230 | "gtk", 1231 | "hyper", 1232 | "reqwest", 1233 | "resolv", 1234 | "serde", 1235 | "serde_json", 1236 | "version-compare", 1237 | ] 1238 | 1239 | [[package]] 1240 | name = "tinyvec" 1241 | version = "1.6.0" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1244 | dependencies = [ 1245 | "tinyvec_macros", 1246 | ] 1247 | 1248 | [[package]] 1249 | name = "tinyvec_macros" 1250 | version = "0.1.0" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1253 | 1254 | [[package]] 1255 | name = "tokio" 1256 | version = "1.19.2" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" 1259 | dependencies = [ 1260 | "bytes", 1261 | "libc", 1262 | "memchr", 1263 | "mio", 1264 | "num_cpus", 1265 | "once_cell", 1266 | "pin-project-lite", 1267 | "socket2", 1268 | "winapi", 1269 | ] 1270 | 1271 | [[package]] 1272 | name = "tokio-native-tls" 1273 | version = "0.3.0" 1274 | source = "registry+https://github.com/rust-lang/crates.io-index" 1275 | checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" 1276 | dependencies = [ 1277 | "native-tls", 1278 | "tokio", 1279 | ] 1280 | 1281 | [[package]] 1282 | name = "tokio-util" 1283 | version = "0.7.3" 1284 | source = "registry+https://github.com/rust-lang/crates.io-index" 1285 | checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" 1286 | dependencies = [ 1287 | "bytes", 1288 | "futures-core", 1289 | "futures-sink", 1290 | "pin-project-lite", 1291 | "tokio", 1292 | "tracing", 1293 | ] 1294 | 1295 | [[package]] 1296 | name = "tower-service" 1297 | version = "0.3.1" 1298 | source = "registry+https://github.com/rust-lang/crates.io-index" 1299 | checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" 1300 | 1301 | [[package]] 1302 | name = "tracing" 1303 | version = "0.1.35" 1304 | source = "registry+https://github.com/rust-lang/crates.io-index" 1305 | checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" 1306 | dependencies = [ 1307 | "cfg-if 1.0.0", 1308 | "pin-project-lite", 1309 | "tracing-core", 1310 | ] 1311 | 1312 | [[package]] 1313 | name = "tracing-core" 1314 | version = "0.1.27" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "7709595b8878a4965ce5e87ebf880a7d39c9afc6837721b21a5a816a8117d921" 1317 | dependencies = [ 1318 | "once_cell", 1319 | ] 1320 | 1321 | [[package]] 1322 | name = "try-lock" 1323 | version = "0.2.2" 1324 | source = "registry+https://github.com/rust-lang/crates.io-index" 1325 | checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" 1326 | 1327 | [[package]] 1328 | name = "unicode-bidi" 1329 | version = "0.3.4" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" 1332 | dependencies = [ 1333 | "matches", 1334 | ] 1335 | 1336 | [[package]] 1337 | name = "unicode-ident" 1338 | version = "1.0.0" 1339 | source = "registry+https://github.com/rust-lang/crates.io-index" 1340 | checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" 1341 | 1342 | [[package]] 1343 | name = "unicode-normalization" 1344 | version = "0.1.19" 1345 | source = "registry+https://github.com/rust-lang/crates.io-index" 1346 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" 1347 | dependencies = [ 1348 | "tinyvec", 1349 | ] 1350 | 1351 | [[package]] 1352 | name = "unicode-xid" 1353 | version = "0.2.3" 1354 | source = "registry+https://github.com/rust-lang/crates.io-index" 1355 | checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" 1356 | 1357 | [[package]] 1358 | name = "url" 1359 | version = "2.2.2" 1360 | source = "registry+https://github.com/rust-lang/crates.io-index" 1361 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 1362 | dependencies = [ 1363 | "form_urlencoded", 1364 | "idna", 1365 | "matches", 1366 | "percent-encoding", 1367 | ] 1368 | 1369 | [[package]] 1370 | name = "vcpkg" 1371 | version = "0.2.15" 1372 | source = "registry+https://github.com/rust-lang/crates.io-index" 1373 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1374 | 1375 | [[package]] 1376 | name = "version-compare" 1377 | version = "0.0.10" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" 1380 | 1381 | [[package]] 1382 | name = "want" 1383 | version = "0.3.0" 1384 | source = "registry+https://github.com/rust-lang/crates.io-index" 1385 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1386 | dependencies = [ 1387 | "log", 1388 | "try-lock", 1389 | ] 1390 | 1391 | [[package]] 1392 | name = "wasi" 1393 | version = "0.11.0+wasi-snapshot-preview1" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1396 | 1397 | [[package]] 1398 | name = "wasm-bindgen" 1399 | version = "0.2.80" 1400 | source = "registry+https://github.com/rust-lang/crates.io-index" 1401 | checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" 1402 | dependencies = [ 1403 | "cfg-if 1.0.0", 1404 | "wasm-bindgen-macro", 1405 | ] 1406 | 1407 | [[package]] 1408 | name = "wasm-bindgen-backend" 1409 | version = "0.2.80" 1410 | source = "registry+https://github.com/rust-lang/crates.io-index" 1411 | checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" 1412 | dependencies = [ 1413 | "bumpalo", 1414 | "lazy_static", 1415 | "log", 1416 | "proc-macro2", 1417 | "quote", 1418 | "syn", 1419 | "wasm-bindgen-shared", 1420 | ] 1421 | 1422 | [[package]] 1423 | name = "wasm-bindgen-futures" 1424 | version = "0.4.30" 1425 | source = "registry+https://github.com/rust-lang/crates.io-index" 1426 | checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" 1427 | dependencies = [ 1428 | "cfg-if 1.0.0", 1429 | "js-sys", 1430 | "wasm-bindgen", 1431 | "web-sys", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "wasm-bindgen-macro" 1436 | version = "0.2.80" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" 1439 | dependencies = [ 1440 | "quote", 1441 | "wasm-bindgen-macro-support", 1442 | ] 1443 | 1444 | [[package]] 1445 | name = "wasm-bindgen-macro-support" 1446 | version = "0.2.80" 1447 | source = "registry+https://github.com/rust-lang/crates.io-index" 1448 | checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" 1449 | dependencies = [ 1450 | "proc-macro2", 1451 | "quote", 1452 | "syn", 1453 | "wasm-bindgen-backend", 1454 | "wasm-bindgen-shared", 1455 | ] 1456 | 1457 | [[package]] 1458 | name = "wasm-bindgen-shared" 1459 | version = "0.2.80" 1460 | source = "registry+https://github.com/rust-lang/crates.io-index" 1461 | checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" 1462 | 1463 | [[package]] 1464 | name = "web-sys" 1465 | version = "0.3.57" 1466 | source = "registry+https://github.com/rust-lang/crates.io-index" 1467 | checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" 1468 | dependencies = [ 1469 | "js-sys", 1470 | "wasm-bindgen", 1471 | ] 1472 | 1473 | [[package]] 1474 | name = "winapi" 1475 | version = "0.3.9" 1476 | source = "registry+https://github.com/rust-lang/crates.io-index" 1477 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1478 | dependencies = [ 1479 | "winapi-i686-pc-windows-gnu", 1480 | "winapi-x86_64-pc-windows-gnu", 1481 | ] 1482 | 1483 | [[package]] 1484 | name = "winapi-i686-pc-windows-gnu" 1485 | version = "0.4.0" 1486 | source = "registry+https://github.com/rust-lang/crates.io-index" 1487 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1488 | 1489 | [[package]] 1490 | name = "winapi-x86_64-pc-windows-gnu" 1491 | version = "0.4.0" 1492 | source = "registry+https://github.com/rust-lang/crates.io-index" 1493 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1494 | 1495 | [[package]] 1496 | name = "windows-sys" 1497 | version = "0.36.1" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 1500 | dependencies = [ 1501 | "windows_aarch64_msvc", 1502 | "windows_i686_gnu", 1503 | "windows_i686_msvc", 1504 | "windows_x86_64_gnu", 1505 | "windows_x86_64_msvc", 1506 | ] 1507 | 1508 | [[package]] 1509 | name = "windows_aarch64_msvc" 1510 | version = "0.36.1" 1511 | source = "registry+https://github.com/rust-lang/crates.io-index" 1512 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 1513 | 1514 | [[package]] 1515 | name = "windows_i686_gnu" 1516 | version = "0.36.1" 1517 | source = "registry+https://github.com/rust-lang/crates.io-index" 1518 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 1519 | 1520 | [[package]] 1521 | name = "windows_i686_msvc" 1522 | version = "0.36.1" 1523 | source = "registry+https://github.com/rust-lang/crates.io-index" 1524 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 1525 | 1526 | [[package]] 1527 | name = "windows_x86_64_gnu" 1528 | version = "0.36.1" 1529 | source = "registry+https://github.com/rust-lang/crates.io-index" 1530 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 1531 | 1532 | [[package]] 1533 | name = "windows_x86_64_msvc" 1534 | version = "0.36.1" 1535 | source = "registry+https://github.com/rust-lang/crates.io-index" 1536 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 1537 | 1538 | [[package]] 1539 | name = "winreg" 1540 | version = "0.10.1" 1541 | source = "registry+https://github.com/rust-lang/crates.io-index" 1542 | checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" 1543 | dependencies = [ 1544 | "winapi", 1545 | ] 1546 | --------------------------------------------------------------------------------