├── .gitignore ├── assets └── demo.gif ├── LICENSE ├── .travis.yml ├── src ├── logger.rs ├── meta.rs ├── main.rs ├── view.rs ├── player.rs ├── config.rs ├── draw.rs ├── key.rs ├── input.rs ├── providers.rs └── app.rs ├── Cargo.toml ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/l4l/rum/HEAD/assets/demo.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Kitsu 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | cache: cargo 2 | before_cache: 3 | - rm -rf "$TRAVIS_HOME/.cargo/registry/src" 4 | os: 5 | - linux 6 | language: rust 7 | rust: 8 | - stable 9 | - beta 10 | - nightly 11 | before_script: 12 | - rustup component add rustfmt clippy 13 | - sudo apt-get update 14 | - sudo apt-get -y install libmpv-dev 15 | script: 16 | - cargo fmt -- --check 17 | - cargo clippy --all-targets --all-features -- -D clippy::all 18 | - cargo build --all-targets --all-features 19 | - cargo test --all-targets --all-features 20 | -------------------------------------------------------------------------------- /src/logger.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use log::Level; 4 | 5 | const MAX_TTL: usize = 4; 6 | 7 | #[derive(Default)] 8 | pub struct Logger { 9 | line: Option<(Level, String)>, 10 | ticks_lived: usize, 11 | } 12 | 13 | impl Logger { 14 | pub fn log(&mut self, level: Level, context: &str, line: impl Display) { 15 | self.ticks_lived = 0; 16 | log::log!(level, "{}: {}", context, line); 17 | self.line = Some((level, format!("{}", line))); 18 | } 19 | 20 | pub fn log_lines(&mut self) -> impl Iterator { 21 | self.ticks_lived += 1; 22 | if self.ticks_lived > MAX_TTL { 23 | self.line.take(); 24 | } 25 | 26 | self.line.as_ref().into_iter() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/meta.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct Artist { 5 | pub url: String, 6 | pub name: String, 7 | } 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct Artists { 11 | pub artists: Vec, 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct Album { 16 | pub url: String, 17 | pub title: String, 18 | pub artists: Vec, 19 | pub year: u16, 20 | pub version: Option, 21 | } 22 | 23 | impl Album { 24 | #[allow(unused)] 25 | pub fn id(&self) -> u32 { 26 | self.url.split('/').nth(1).unwrap().parse().unwrap() 27 | } 28 | } 29 | 30 | #[derive(Debug)] 31 | pub struct Albums { 32 | pub albums: Vec, 33 | } 34 | 35 | #[derive(Debug, Clone)] 36 | pub struct Track { 37 | pub album_id: u32, 38 | pub track_id: u32, 39 | pub name: String, 40 | pub artists: Arc>, 41 | } 42 | 43 | #[derive(Debug)] 44 | pub struct Tracks { 45 | pub tracks: Vec, 46 | } 47 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rum-player" 3 | description = "TUI-based music player" 4 | version = "0.1.6" 5 | authors = ["Kitsu "] 6 | edition = "2018" 7 | 8 | license = "WTFPL" 9 | readme = "README.md" 10 | repository = "https://github.com/l4l/rum" 11 | keywords = ["audio", "tui", "music", "player"] 12 | categories = ["command-line-utilities", "multimedia::audio"] 13 | 14 | [dependencies] 15 | tokio = {version = "0.2", features = ["stream", "rt-threaded", "macros", "io-std", "time"] } 16 | futures = "0.3" 17 | reqwest = { version = "0.10", features = ["json"] } 18 | async-stream = "0.2" 19 | 20 | unhtml = { version = "0.7.5", features = ["derive"] } 21 | serde = { version = "1.0.101", features = ["derive"] } 22 | serde-xml-rs = "0.3.1" 23 | toml = "0.5.5" 24 | 25 | mpv = "0.2.3" 26 | 27 | tui = "0.8.0" 28 | termion = "1.5.5" 29 | 30 | log = "0.4.8" 31 | flexi_logger = "0.14.4" 32 | snafu = "0.6.2" 33 | strum = "0.16.0" 34 | strum_macros = "0.16.0" 35 | itertools = "0.8.1" 36 | dirs = "2.0.2" 37 | derive_more = "0.99.3" 38 | 39 | [dev-dependencies] 40 | quickcheck = "0.9.0" 41 | quickcheck_macros = "0.8.0" 42 | 43 | [badges] 44 | travis-ci = { repository = "l4l/rum" } 45 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fs::File; 3 | 4 | use flexi_logger::Logger; 5 | 6 | mod app; 7 | mod config; 8 | mod draw; 9 | mod input; 10 | mod key; 11 | mod logger; 12 | mod meta; 13 | mod player; 14 | mod providers; 15 | mod view; 16 | 17 | use crate::config::Config; 18 | 19 | #[tokio::main] 20 | async fn main() -> Result<(), Box> { 21 | Logger::with_env_or_str("warn, rum_player = debug") 22 | .log_to_file() 23 | .directory("/tmp") 24 | .start()?; 25 | log::info!("Logging initialized"); 26 | 27 | let config = dirs::config_dir() 28 | .and_then(|mut config_path| { 29 | config_path.push("rum-player"); 30 | config_path.push("config"); 31 | File::open(config_path) 32 | .and_then(|mut file| { 33 | let mut s = String::new(); 34 | use std::io::Read; 35 | file.read_to_string(&mut s)?; 36 | Ok(s) 37 | }) 38 | .ok() 39 | }) 40 | .map(|x| x.parse()) 41 | .transpose()? 42 | .unwrap_or_else(Config::default); 43 | 44 | let provider = providers::Provider::new(); 45 | 46 | let (player, chan) = player::Player::new(); 47 | let (state, _) = player.start_worker(); 48 | 49 | let app = app::App::create(config, provider, chan, state)?; 50 | log::info!("Spinning up a fancy UI"); 51 | app.run().await?; 52 | 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RUM Player 2 | 3 | [![Build Status](https://travis-ci.org/l4l/rum.svg?branch=master)](https://travis-ci.org/l4l/rum) 4 | [![Crates.io](https://img.shields.io/crates/v/rum-player.svg)](https://crates.io/crates/rum-player) 5 | 6 | RUM is a terminal music player powered by [tui-rs](https://github.com/fdehau/tui-rs). It is able to play remote audio from Ya.Music. **Note:** Haven't been tested on MacOS yet, feel free to file a report/issues. 7 | 8 | ![](assets/demo.gif) 9 | 10 | # Usage 11 | 12 | Playing media is performed via _mpv_ player, thus it need to be installed (e.g. libmpv-dev on ubuntu, or mpv on arch linux). 13 | 14 | ```bash 15 | cargo install rum-player 16 | # by default installed at ~/.cargo/bin, you may add it to path: 17 | export PATH=$PATH:~/.cargo/bin 18 | rum-player 19 | ``` 20 | 21 | Currently, the tool has several main views: album/artist search panel, track list view and a playlist. 22 | 23 | ## Hotkeys 24 | 25 | Hotkeys can be set via toml config, it should be placed at `$XDG_CONFIG_HOME` for Linux, or at `$HOME/Library/Preferences` for macOS. All bindings must be specified at `[hotkey]` table and should be in form (note quotes): `"Event" = "Action"`. Hotkeys might also be specified for a particular view or context (currently only for one at a time) via sub-table. If no context specified then hotkey considered as global and will be used with a lower priority. Here is a config example: 26 | 27 | ```toml 28 | [hotkey] 29 | "PointerUp" = "ArrowUp" 30 | "PointerDown" = "ArrowDown" 31 | "NextTrack" = "+" 32 | "PrevTrack" = "-" 33 | "Forward5" = "Ctrl++" 34 | "Backward5" = "Ctrl+-" 35 | 36 | [hotkey.search] 37 | "PointerUp" = "ArrowDown" 38 | "PointerDown" = "ArrowUp" 39 | 40 | [hotkey.tracklist] 41 | "Select" = "Alt+0" 42 | ``` 43 | 44 | Default hotkeys are the following: 45 | 46 | - Arrow Up/Down - scroll up/down displayed list; 47 | - Arrow Left/Right - switch to previous/next track; 48 | - Alt+Esc - display back to previous view; 49 | - Tab - switch between search types (currently track & album search are available); 50 | - Ctrl+a (at track list view) - add all tracks to playlist; 51 | - Ctrl+s - stop playback and clear playlist; 52 | - Ctrl+p - pause/unpause playback; 53 | - Alt+a (at artist search) - switch to artist albums; 54 | - Alt+t (at artist search) - switch to artist tracks; 55 | - Alt+s - switch to related artist(s); 56 | - Alt+p - switch to playlist view; 57 | - ] - skip 5 seconds forward of currently played track; 58 | - [ - skip 5 seconds backward of currently played track; 59 | - Alt+Enter - search item in buffer; 60 | - Enter - select item at list view; 61 | - Ctrl+c/Delete - quit the program. 62 | 63 | # Development 64 | 65 | For development you need any rust compiler: https://rustup.rs/. Afterwards you may build sources via `cargo build` and start hacking. Please also use rustfmt & clippy at development process: `rustup component add rustfmt clippy`. 66 | -------------------------------------------------------------------------------- /src/view.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | use derive_more::From; 4 | 5 | use crate::meta::{Album, Artist, Track}; 6 | 7 | #[derive(Debug, Clone, Default)] 8 | pub struct MainView { 9 | insert_buffer: String, 10 | view: View, 11 | } 12 | 13 | impl MainView { 14 | pub fn replace_view(&mut self, view: View) -> View { 15 | std::mem::replace(&mut self.view, view) 16 | } 17 | 18 | pub fn insert_buffer(&self) -> &str { 19 | &self.insert_buffer 20 | } 21 | 22 | pub fn insert_buffer_mut(&mut self) -> &mut String { 23 | &mut self.insert_buffer 24 | } 25 | 26 | pub fn view_and_buffer_mut(&mut self) -> (&mut View, &mut String) { 27 | (&mut self.view, &mut self.insert_buffer) 28 | } 29 | 30 | pub fn view(&self) -> &View { 31 | &self.view 32 | } 33 | } 34 | 35 | impl Deref for MainView { 36 | type Target = View; 37 | 38 | fn deref(&self) -> &Self::Target { 39 | self.view() 40 | } 41 | } 42 | 43 | impl DerefMut for MainView { 44 | fn deref_mut(&mut self) -> &mut Self::Target { 45 | &mut self.view 46 | } 47 | } 48 | 49 | #[derive(Debug, Clone, Default)] 50 | pub struct ArtistSearch { 51 | pub cached_artists: Vec, 52 | pub cursor: usize, 53 | } 54 | 55 | impl From> for ArtistSearch { 56 | fn from(artists: Vec) -> Self { 57 | Self { 58 | cached_artists: artists, 59 | cursor: 0, 60 | } 61 | } 62 | } 63 | 64 | #[derive(Debug, Clone, Default)] 65 | pub struct AlbumSearch { 66 | pub cached_albums: Vec, 67 | pub cursor: usize, 68 | } 69 | 70 | impl From> for AlbumSearch { 71 | fn from(albums: Vec) -> Self { 72 | Self { 73 | cached_albums: albums, 74 | cursor: 0, 75 | } 76 | } 77 | } 78 | 79 | #[derive(Debug, Clone, Default)] 80 | pub struct TrackList { 81 | pub cached_tracks: Vec, 82 | pub cursor: usize, 83 | } 84 | 85 | impl From> for TrackList { 86 | fn from(tracks: Vec) -> Self { 87 | Self { 88 | cached_tracks: tracks, 89 | cursor: 0, 90 | } 91 | } 92 | } 93 | 94 | #[derive(Debug, Clone)] 95 | pub struct Playlist { 96 | pub tracks: Vec, 97 | pub current: usize, 98 | } 99 | 100 | impl Playlist { 101 | pub fn create(tracks: Vec, current: usize) -> Self { 102 | Self { tracks, current } 103 | } 104 | } 105 | 106 | #[derive(Debug, Clone, From)] 107 | pub enum View { 108 | ArtistSearch(ArtistSearch), 109 | AlbumSearch(AlbumSearch), 110 | TrackList(TrackList), 111 | Playlist(Playlist), 112 | } 113 | 114 | impl Default for View { 115 | fn default() -> Self { 116 | View::AlbumSearch(AlbumSearch::from(vec![])) 117 | } 118 | } 119 | 120 | pub struct CursorMut<'a> { 121 | cursor: &'a mut usize, 122 | max_cursor: usize, 123 | } 124 | 125 | impl Drop for CursorMut<'_> { 126 | fn drop(&mut self) { 127 | *self.cursor = self.max_cursor.min(*self.cursor); 128 | } 129 | } 130 | 131 | impl Deref for CursorMut<'_> { 132 | type Target = usize; 133 | 134 | fn deref(&self) -> &Self::Target { 135 | &self.cursor 136 | } 137 | } 138 | 139 | impl DerefMut for CursorMut<'_> { 140 | fn deref_mut(&mut self) -> &mut Self::Target { 141 | &mut self.cursor 142 | } 143 | } 144 | 145 | #[allow(unused)] 146 | impl View { 147 | pub fn name(&self) -> &'static str { 148 | match self { 149 | View::ArtistSearch(_) => "ArtistSearch", 150 | View::AlbumSearch(_) => "AlbumSearch", 151 | View::TrackList(_) => "TrackList", 152 | View::Playlist(_) => "Playlist", 153 | } 154 | } 155 | 156 | pub fn cursor(&self) -> Option { 157 | match self { 158 | View::ArtistSearch(search) => Some(search.cursor), 159 | View::AlbumSearch(search) => Some(search.cursor), 160 | View::TrackList(search) => Some(search.cursor), 161 | View::Playlist(_) => None, 162 | } 163 | } 164 | 165 | pub fn cursor_mut(&mut self) -> Option> { 166 | let max_cursor = self.len().saturating_sub(1); 167 | 168 | match self { 169 | View::ArtistSearch(search) => Some(CursorMut { 170 | cursor: &mut search.cursor, 171 | max_cursor, 172 | }), 173 | View::AlbumSearch(search) => Some(CursorMut { 174 | cursor: &mut search.cursor, 175 | max_cursor, 176 | }), 177 | View::TrackList(search) => Some(CursorMut { 178 | cursor: &mut search.cursor, 179 | max_cursor, 180 | }), 181 | View::Playlist(_) => None, 182 | } 183 | } 184 | 185 | pub fn reset_cursor(&mut self) { 186 | if let Some(mut cursor) = self.cursor_mut() { 187 | *cursor = 0; 188 | } 189 | } 190 | 191 | pub fn len(&self) -> usize { 192 | match self { 193 | View::ArtistSearch(search) => search.cached_artists.len(), 194 | View::AlbumSearch(search) => search.cached_albums.len(), 195 | View::TrackList(search) => search.cached_tracks.len(), 196 | View::Playlist(_) => 0, 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/player.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::{self, TryRecvError}; 2 | use std::sync::{Arc, Mutex}; 3 | 4 | use mpv::{MpvHandler, MpvHandlerBuilder, Result}; 5 | 6 | use crate::meta::Track; 7 | 8 | struct MediaWorker { 9 | handler: MpvHandler, 10 | is_paused: bool, 11 | } 12 | 13 | impl MediaWorker { 14 | fn new() -> Result { 15 | let handler = MpvHandlerBuilder::new()?.build()?; 16 | Ok(Self { 17 | handler, 18 | is_paused: false, 19 | }) 20 | } 21 | 22 | fn loadfile(&mut self, url: &str) -> Result<()> { 23 | self.handler.command(&["loadfile", &url, "append-play"])?; 24 | Ok(()) 25 | } 26 | 27 | fn stop(&mut self) -> Result<()> { 28 | self.handler.command(&["stop"])?; 29 | Ok(()) 30 | } 31 | 32 | fn next(&mut self) -> Result<()> { 33 | self.handler.command(&["playlist-next"])?; 34 | Ok(()) 35 | } 36 | 37 | fn prev(&mut self) -> Result<()> { 38 | self.handler.command(&["playlist-prev"])?; 39 | Ok(()) 40 | } 41 | 42 | fn flip_pause(&mut self) -> Result<()> { 43 | self.is_paused ^= true; 44 | self.handler.set_property("pause", self.is_paused)?; 45 | Ok(()) 46 | } 47 | 48 | fn time_seek(&mut self, f: impl FnOnce(i64) -> i64) -> Result<()> { 49 | let pos: i64 = self.handler.get_property("time-pos")?; 50 | self.handler.set_property("time-pos", f(pos))?; 51 | Ok(()) 52 | } 53 | 54 | fn playlist_pos(&self) -> Result { 55 | let pos: i64 = self.handler.get_property("playlist-pos")?; 56 | Ok(pos as usize) 57 | } 58 | 59 | fn poll_events(&mut self) -> Result { 60 | while let Some(ev) = self.handler.wait_event(0.1) { 61 | match ev { 62 | mpv::Event::Shutdown | mpv::Event::Idle => { 63 | return Ok(false); 64 | } 65 | _ => log::debug!("mpv: {:?}", ev), 66 | } 67 | } 68 | Ok(true) 69 | } 70 | } 71 | 72 | pub enum Command { 73 | Enqueue { track: Track, url: String }, 74 | Stop, 75 | NextTrack, 76 | PrevTrack, 77 | FlipPause, 78 | Seek(i64), 79 | } 80 | 81 | pub struct PlayerState { 82 | playlist: Vec, 83 | current_position: usize, 84 | } 85 | 86 | impl PlayerState { 87 | fn new() -> Self { 88 | Self { 89 | playlist: vec![], 90 | current_position: 0, 91 | } 92 | } 93 | 94 | pub fn playlist(&self) -> impl Iterator { 95 | self.playlist.iter() 96 | } 97 | 98 | pub fn current(&self) -> usize { 99 | self.current_position 100 | } 101 | } 102 | 103 | pub type State = Arc>; 104 | 105 | pub struct Player { 106 | rx: mpsc::Receiver, 107 | state: State, 108 | } 109 | 110 | impl Player { 111 | pub fn new() -> (Self, mpsc::Sender) { 112 | let (tx, rx) = mpsc::channel(); 113 | let state = Arc::new(Mutex::new(PlayerState::new())); 114 | (Self { rx, state }, tx) 115 | } 116 | 117 | pub fn start_worker(self) -> (State, std::thread::JoinHandle>) { 118 | let state = self.state.clone(); 119 | 120 | let handle = std::thread::spawn(move || { 121 | let mut worker = MediaWorker::new()?; 122 | loop { 123 | worker.poll_events()?; 124 | match self.rx.try_recv() { 125 | Ok(Command::Enqueue { track, url }) => { 126 | if let Err(err) = worker.loadfile(&url) { 127 | log::error!("cannot load {}: {}, url: {}", track.name, err, url); 128 | } else { 129 | self.state.lock().unwrap().playlist.push(track); 130 | } 131 | } 132 | Ok(Command::Stop) => { 133 | if let Err(err) = worker.stop() { 134 | log::error!("cannot stop the track: {}", err); 135 | } else { 136 | let mut state = self.state.lock().unwrap(); 137 | state.playlist.clear(); 138 | state.current_position = 0; 139 | } 140 | } 141 | Ok(Command::NextTrack) => { 142 | if let Err(err) = worker.next() { 143 | log::error!("cannot switch to next track: {}", err); 144 | } else { 145 | self.state.lock().unwrap().current_position += 1; 146 | } 147 | } 148 | Ok(Command::PrevTrack) => { 149 | if let Err(err) = worker.prev() { 150 | log::error!("cannot switch to previous track: {}", err); 151 | } else { 152 | self.state.lock().unwrap().current_position -= 1; 153 | } 154 | } 155 | Ok(Command::FlipPause) => { 156 | if let Err(err) = worker.flip_pause() { 157 | log::error!("cannot pause/unpause track: {}", err); 158 | } 159 | } 160 | Ok(Command::Seek(x)) => { 161 | if let Err(err) = worker.time_seek(|pos| pos + x) { 162 | log::error!("cannot seek time ({} secs): {}", x, err); 163 | } 164 | } 165 | Err(TryRecvError::Empty) => {} 166 | Err(TryRecvError::Disconnected) => { 167 | log::warn!("player command stream disconnected, finishing"); 168 | return Ok(()); 169 | } 170 | } 171 | 172 | if let Ok(pos) = worker.playlist_pos() { 173 | let mut state = self.state.lock().unwrap(); 174 | state.current_position = pos; 175 | } else { 176 | let mut state = self.state.lock().unwrap(); 177 | state.playlist.clear(); 178 | state.current_position = 0; 179 | } 180 | } 181 | }); 182 | 183 | (state, handle) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt; 3 | use std::str::FromStr; 4 | 5 | use snafu::ResultExt; 6 | use termion::event::{Event as InnerEvent, Key}; 7 | 8 | use crate::key::BindingConfig; 9 | use crate::key::{Action, Context, ContextedAction}; 10 | 11 | struct Event(InnerEvent); 12 | 13 | #[derive(Debug)] 14 | pub struct UnknownEvent; 15 | 16 | impl fmt::Display for UnknownEvent { 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | write!(f, "UnknownEvent") 19 | } 20 | } 21 | 22 | impl std::error::Error for UnknownEvent {} 23 | 24 | impl FromStr for Event { 25 | type Err = UnknownEvent; 26 | 27 | fn from_str(s: &str) -> Result { 28 | match s { 29 | "ArrowUp" => Ok(Event(InnerEvent::Key(Key::Up))), 30 | "ArrowDown" => Ok(Event(InnerEvent::Key(Key::Down))), 31 | "ArrowRight" => Ok(Event(InnerEvent::Key(Key::Right))), 32 | "ArrowLeft" => Ok(Event(InnerEvent::Key(Key::Left))), 33 | "Del" => Ok(Event(InnerEvent::Key(Key::Delete))), 34 | "Backspace" => Ok(Event(InnerEvent::Key(Key::Backspace))), 35 | "Home" => Ok(Event(InnerEvent::Key(Key::Home))), 36 | "End" => Ok(Event(InnerEvent::Key(Key::End))), 37 | "PageUp" => Ok(Event(InnerEvent::Key(Key::PageUp))), 38 | "PageDown" => Ok(Event(InnerEvent::Key(Key::PageDown))), 39 | "Insert" => Ok(Event(InnerEvent::Key(Key::Insert))), 40 | "Esc" => Ok(Event(InnerEvent::Key(Key::Esc))), 41 | s => { 42 | const CTRL_PREFIX: &str = "Ctrl+"; 43 | const ALT_PREFIX: &str = "Alt+"; 44 | const FN_PREFIX: &str = "Fn+"; 45 | 46 | fn parse_prefixed( 47 | haystack: &str, 48 | prefix: &str, 49 | ) -> Option> { 50 | if !haystack.starts_with(prefix) { 51 | return None; 52 | } 53 | 54 | let suffix = haystack.split_at(prefix.as_bytes().len()).1; 55 | 56 | if suffix.len() != 1 { 57 | Some(Err(UnknownEvent)) 58 | } else { 59 | let c = suffix.chars().next().unwrap(); 60 | Some(Ok(Event(InnerEvent::Key(Key::Char(c))))) 61 | } 62 | } 63 | 64 | if let Some(ev) = parse_prefixed(s, CTRL_PREFIX) { 65 | ev 66 | } else if let Some(ev) = parse_prefixed(s, ALT_PREFIX) { 67 | ev 68 | } else if let Some(ev) = parse_prefixed(s, FN_PREFIX) { 69 | ev 70 | } else if s.chars().count() == 1 { 71 | let c = s.chars().next().unwrap(); 72 | Ok(Event(InnerEvent::Key(Key::Char(c)))) 73 | } else { 74 | Err(UnknownEvent) 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | #[derive(Debug, snafu::Snafu)] 82 | pub enum Error { 83 | #[snafu(display("incorrect toml config: {}", source))] 84 | IncorrectToml { source: toml::de::Error }, 85 | #[snafu(display("incorrect action value: {}", value))] 86 | IncorrectAction { 87 | value: String, 88 | source: toml::de::Error, 89 | }, 90 | #[snafu(display("incorrect event value: {}", value))] 91 | IncorrectEvent { value: String, source: UnknownEvent }, 92 | #[snafu(display("unsupported config key {}", key))] 93 | UnsupportedKey { key: String }, 94 | #[snafu(display("unsupported toml item"))] 95 | UnsupportedTomlItem, 96 | } 97 | 98 | #[derive(Default, Debug)] 99 | pub struct Config { 100 | pub binding: BindingConfig, 101 | } 102 | 103 | const HOTKEY_TABLE: &str = "hotkey"; 104 | 105 | macro_rules! try_toml { 106 | ($val:expr; $t:ident) => {{ 107 | if let toml::Value::$t(value) = $val { 108 | value 109 | } else { 110 | return Err(Error::UnsupportedTomlItem); 111 | } 112 | }}; 113 | } 114 | 115 | impl FromStr for Config { 116 | type Err = Error; 117 | 118 | fn from_str(s: &str) -> Result { 119 | let mut config = Config { 120 | binding: BindingConfig::default(), 121 | }; 122 | 123 | for (key, value) in try_toml!(s.parse().context(IncorrectToml {})?; Table).into_iter() { 124 | match key.as_str() { 125 | HOTKEY_TABLE => { 126 | let value = try_toml!(value; Table); 127 | config.binding = parse_binding_config(value)?; 128 | } 129 | _ => return Err(Error::UnsupportedKey { key }), 130 | } 131 | } 132 | 133 | Ok(config) 134 | } 135 | } 136 | 137 | fn parse_binding_config(table: toml::value::Table) -> Result { 138 | const SEARCH_TABLE: &str = "search"; 139 | const TRACKLIST_TABLE: &str = "tracklist"; 140 | const PLAYLIST_TABLE: &str = "playlist"; 141 | 142 | let mut event_actions: HashMap<_, Vec<_>> = HashMap::new(); 143 | for (key, value) in table.into_iter() { 144 | let (context, map): (Context, toml::map::Map<_, _>) = match key.as_str() { 145 | SEARCH_TABLE => { 146 | let map = try_toml!(value; Table); 147 | (Context::search(), map) 148 | } 149 | TRACKLIST_TABLE => { 150 | let map = try_toml!(value; Table); 151 | (Context::tracklist(), map) 152 | } 153 | PLAYLIST_TABLE => { 154 | let map = try_toml!(value; Table); 155 | (Context::playlist(), map) 156 | } 157 | _ => { 158 | let mut map = toml::map::Map::new(); 159 | map.insert(key, value); 160 | (Context::all(), map) 161 | } 162 | }; 163 | 164 | for (key, value) in map { 165 | let action: Action = toml::Value::String(key.clone()) 166 | .try_into() 167 | .context(IncorrectAction { value: key })?; 168 | let action = ContextedAction { action, context }; 169 | 170 | let value: String = try_toml!(value; String); 171 | let event = value 172 | .as_str() 173 | .parse::() 174 | .context(IncorrectEvent { value })? 175 | .0; 176 | 177 | event_actions.entry(event).or_default().push(action); 178 | } 179 | } 180 | 181 | Ok(event_actions.into()) 182 | } 183 | 184 | #[cfg(test)] 185 | mod tests { 186 | use super::*; 187 | 188 | #[test] 189 | fn parse_toml() { 190 | let sample_toml = r#" 191 | [hotkey] 192 | "PointerUp" = "ArrowUp" 193 | "PointerDown" = "ArrowDown" 194 | "NextTrack" = "+" 195 | "PrevTrack" = "-" 196 | "Forward5" = "Ctrl++" 197 | "Backward5" = "Ctrl+-" 198 | 199 | [hotkey.search] 200 | "PointerUp" = "ArrowDown" 201 | "PointerDown" = "ArrowUp" 202 | 203 | [hotkey.tracklist] 204 | "Select" = "Alt+0" 205 | "# 206 | .to_string(); 207 | 208 | let config = sample_toml.parse::().unwrap(); 209 | println!("{:?}", config); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/draw.rs: -------------------------------------------------------------------------------- 1 | use std::io::{stdout, Error, Stdout}; 2 | 3 | use log::Level; 4 | use termion::raw::{IntoRawMode, RawTerminal}; 5 | use tui::backend::TermionBackend; 6 | use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; 7 | use tui::style::{Color, Modifier, Style}; 8 | use tui::terminal::Frame; 9 | use tui::widgets::{Block, Borders, List, Paragraph, Text, Widget}; 10 | use tui::Terminal; 11 | 12 | use crate::view; 13 | 14 | type Backend = TermionBackend>; 15 | 16 | pub struct Drawer { 17 | terminal: Terminal, 18 | } 19 | 20 | impl Drawer { 21 | pub fn new() -> Result { 22 | let stdout = stdout().into_raw_mode()?; 23 | let backend = TermionBackend::new(stdout); 24 | let mut terminal = Terminal::new(backend)?; 25 | 26 | terminal.clear()?; 27 | terminal.hide_cursor()?; 28 | 29 | Ok(Self { terminal }) 30 | } 31 | 32 | pub fn redraw<'a>( 33 | &mut self, 34 | main_view: &view::MainView, 35 | log_lines: impl Iterator, 36 | ) -> Result<(), Error> { 37 | self.terminal.draw(|mut frame| { 38 | let constraints = if frame.size().height < 20 { 39 | [Constraint::Length(3), Constraint::Min(0)].as_ref() 40 | } else { 41 | &[ 42 | Constraint::Length(3), 43 | Constraint::Min(0), 44 | Constraint::Length(6), 45 | ] 46 | }; 47 | let chunks = Layout::default() 48 | .direction(Direction::Vertical) 49 | .margin(1) 50 | .constraints(constraints) 51 | .split(frame.size()); 52 | let texts = [Text::styled( 53 | main_view.insert_buffer(), 54 | Style::default().fg(Color::Gray).modifier(Modifier::BOLD), 55 | )]; 56 | Paragraph::new(texts.iter()) 57 | .block( 58 | Block::default() 59 | .title(main_view.view().name()) 60 | .title_style(Style::default().fg(Color::Magenta).modifier(Modifier::BOLD)) 61 | .borders(Borders::ALL), 62 | ) 63 | .alignment(Alignment::Center) 64 | .wrap(true) 65 | .render(&mut frame, chunks[0]); 66 | 67 | main_view.view().draw_at(&mut frame, chunks[1]); 68 | 69 | if chunks.len() >= 3 { 70 | let line = log_lines 71 | .last() 72 | .map(|(level, s)| { 73 | Text::styled( 74 | s.as_str(), 75 | Style::default().modifier(Modifier::BOLD).fg(match level { 76 | Level::Info => Color::Green, 77 | Level::Warn => Color::Yellow, 78 | Level::Error => Color::Red, 79 | _ => Color::Gray, 80 | }), 81 | ) 82 | }) 83 | .unwrap_or_else(|| Text::raw("")); 84 | 85 | Paragraph::new([line].iter()) 86 | .wrap(true) 87 | .block( 88 | Block::default().title("Log").title_style( 89 | Style::default().fg(Color::Magenta).modifier(Modifier::BOLD), 90 | ), 91 | ) 92 | .render(&mut frame, chunks[2]); 93 | } 94 | }) 95 | } 96 | } 97 | 98 | impl view::View { 99 | fn draw_at(&self, frame: &mut Frame, chunk: Rect) { 100 | match self { 101 | view::View::ArtistSearch(search) => search.draw_at(frame, chunk), 102 | view::View::AlbumSearch(search) => search.draw_at(frame, chunk), 103 | view::View::TrackList(list) => list.draw_at(frame, chunk), 104 | view::View::Playlist(playlist) => playlist.draw_at(frame, chunk), 105 | } 106 | } 107 | } 108 | 109 | impl view::ArtistSearch { 110 | fn draw_at(&self, mut frame: &mut Frame, chunk: Rect) { 111 | List::new(cursored_line( 112 | self.cached_artists.iter().map(|album| &album.name), 113 | self.cursor, 114 | chunk, 115 | )) 116 | .block(Block::default().title("Artists").borders(Borders::ALL)) 117 | .render(&mut frame, chunk); 118 | } 119 | } 120 | 121 | impl view::AlbumSearch { 122 | fn draw_at(&self, mut frame: &mut Frame, chunk: Rect) { 123 | List::new(cursored_line( 124 | self.cached_albums.iter().map(|album| { 125 | if let Some(ref version) = album.version { 126 | format!( 127 | "{}: {} (year: {}, {})", 128 | album 129 | .artists 130 | .get(0) 131 | .map(|a| a.name.as_str()) 132 | .unwrap_or("unknown"), 133 | album.title, 134 | album.year, 135 | version 136 | ) 137 | } else { 138 | format!( 139 | "{}: {} (year: {})", 140 | album 141 | .artists 142 | .get(0) 143 | .map(|a| a.name.as_str()) 144 | .unwrap_or("unknown"), 145 | album.title, 146 | album.year 147 | ) 148 | } 149 | }), 150 | self.cursor, 151 | chunk, 152 | )) 153 | .block(Block::default().title("Albums").borders(Borders::ALL)) 154 | .render(&mut frame, chunk); 155 | } 156 | } 157 | 158 | impl view::TrackList { 159 | fn draw_at(&self, mut frame: &mut Frame, chunk: Rect) { 160 | List::new(cursored_line( 161 | self.cached_tracks.iter().map(|track| { 162 | format!( 163 | "{} ({})", 164 | track.name, 165 | itertools::join(track.artists.iter().map(|a| a.name.as_str()), ", ") 166 | ) 167 | }), 168 | self.cursor, 169 | chunk, 170 | )) 171 | .block(Block::default().title("Found Tracks").borders(Borders::ALL)) 172 | .render(&mut frame, chunk); 173 | } 174 | } 175 | 176 | impl view::Playlist { 177 | fn draw_at(&self, mut frame: &mut Frame, chunk: Rect) { 178 | List::new(cursored_line( 179 | self.tracks.iter().map(|track| { 180 | format!( 181 | "{} ({})", 182 | track.name, 183 | itertools::join(track.artists.iter().map(|a| a.name.as_str()), ", ") 184 | ) 185 | }), 186 | self.current, 187 | chunk, 188 | )) 189 | .block(Block::default().title("Playlist").borders(Borders::ALL)) 190 | .render(&mut frame, chunk); 191 | } 192 | } 193 | 194 | fn cursored_line<'a>( 195 | iter: impl IntoIterator>, 196 | cursor_pos: usize, 197 | chunk: Rect, 198 | ) -> impl Iterator> { 199 | let half = usize::from(chunk.height) / 2; 200 | let skip = cursor_pos.saturating_sub(half); 201 | iter.into_iter() 202 | .enumerate() 203 | .skip(skip) 204 | .map(move |(i, line)| { 205 | let style = if i == cursor_pos { 206 | Style::default() 207 | .bg(Color::Gray) 208 | .fg(Color::Black) 209 | .modifier(Modifier::BOLD) 210 | } else { 211 | Default::default() 212 | }; 213 | Text::styled(line.into(), style) 214 | }) 215 | } 216 | -------------------------------------------------------------------------------- /src/key.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::ops::BitOr; 3 | use std::sync::{Arc, Mutex}; 4 | 5 | use futures::channel::mpsc; 6 | use futures::prelude::*; 7 | use serde::{Deserialize, Serialize}; 8 | use termion::event::{Event, Key}; 9 | 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 11 | pub struct Context { 12 | is_search: bool, 13 | is_tracklist: bool, 14 | is_playlist: bool, 15 | } 16 | 17 | impl Context { 18 | fn is_valid(self) -> bool { 19 | self.is_search | self.is_tracklist | self.is_playlist 20 | } 21 | 22 | fn is_sub(self, other: Context) -> bool { 23 | macro_rules! implies { 24 | ($a:expr => $b:expr) => {{ 25 | !$a || $b 26 | }}; 27 | } 28 | 29 | implies!(self.is_search => other.is_search) 30 | && implies!(self.is_tracklist => other.is_tracklist) 31 | && implies!(self.is_playlist => other.is_playlist) 32 | } 33 | 34 | pub fn search() -> Self { 35 | Context { 36 | is_search: true, 37 | is_tracklist: false, 38 | is_playlist: false, 39 | } 40 | } 41 | 42 | pub fn tracklist() -> Self { 43 | Context { 44 | is_search: false, 45 | is_tracklist: true, 46 | is_playlist: false, 47 | } 48 | } 49 | 50 | pub fn playlist() -> Self { 51 | Context { 52 | is_search: false, 53 | is_tracklist: false, 54 | is_playlist: true, 55 | } 56 | } 57 | 58 | pub fn all() -> Self { 59 | Context { 60 | is_search: true, 61 | is_tracklist: true, 62 | is_playlist: true, 63 | } 64 | } 65 | } 66 | 67 | impl BitOr for Context { 68 | type Output = Self; 69 | 70 | fn bitor(self, rhs: Self) -> Self::Output { 71 | Self { 72 | is_search: self.is_search | rhs.is_search, 73 | is_tracklist: self.is_tracklist | rhs.is_tracklist, 74 | is_playlist: self.is_playlist | rhs.is_playlist, 75 | } 76 | } 77 | } 78 | 79 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 80 | pub enum Action { 81 | Quit, 82 | PointerUp, 83 | PointerDown, 84 | NextTrack, 85 | PrevTrack, 86 | FlipPause, 87 | Stop, 88 | Forward5, 89 | Backward5, 90 | Refresh, 91 | AddAll, 92 | ShowPlaylist, 93 | SwitchToAlbums, 94 | SwitchToTracks, 95 | SwitchToArtists, 96 | Search, 97 | Select, 98 | SwitchView, 99 | PrevView, 100 | #[serde(skip)] 101 | Char(char), 102 | Backspace, 103 | } 104 | 105 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 106 | pub struct ContextedAction { 107 | pub context: Context, 108 | pub action: Action, 109 | } 110 | 111 | #[derive(Default, Debug)] 112 | pub struct BindingConfig { 113 | bindings: HashMap>, 114 | } 115 | 116 | impl From>> for BindingConfig { 117 | fn from(event_actions: HashMap>) -> Self { 118 | Self { 119 | bindings: event_actions 120 | .into_iter() 121 | .filter_map(|(key, mut actions)| { 122 | actions.sort_by_key(|v| v.context); 123 | actions.retain(|action| action.context.is_valid()); 124 | actions.dedup(); 125 | 126 | if actions.is_empty() { 127 | None 128 | } else { 129 | Some((key, actions)) 130 | } 131 | }) 132 | .collect(), 133 | } 134 | } 135 | } 136 | 137 | impl BindingConfig { 138 | fn action(&self, context: Context, event: &Event) -> Option { 139 | self.bindings 140 | .get(event) 141 | .and_then(|actions| { 142 | actions 143 | .iter() 144 | .find(|contexed| context.is_sub(contexed.context)) 145 | .map(|contexed| contexed.action) 146 | }) 147 | .or_else(|| BindingConfig::default_action(&event)) 148 | } 149 | 150 | // TODO: use context here 151 | fn default_action(event: &Event) -> Option { 152 | let event = if let Event::Key(event) = event { 153 | event 154 | } else { 155 | return None; 156 | }; 157 | 158 | match event { 159 | Key::Up => Some(Action::PointerUp), 160 | Key::Down => Some(Action::PointerDown), 161 | Key::Right => Some(Action::NextTrack), 162 | Key::Left => Some(Action::PrevTrack), 163 | Key::Delete | Key::Ctrl('c') => Some(Action::Quit), 164 | Key::Ctrl('p') => Some(Action::FlipPause), 165 | Key::Char(']') => Some(Action::Forward5), 166 | Key::Char('[') => Some(Action::Backward5), 167 | Key::Ctrl('r') => Some(Action::Refresh), 168 | Key::Ctrl('s') => Some(Action::Stop), 169 | Key::Ctrl('a') => Some(Action::AddAll), 170 | Key::Alt('p') => Some(Action::ShowPlaylist), 171 | Key::Alt('a') => Some(Action::SwitchToAlbums), 172 | Key::Alt('t') => Some(Action::SwitchToTracks), 173 | Key::Alt('s') => Some(Action::SwitchToArtists), 174 | Key::Alt('\n') | Key::Alt('\r') => Some(Action::Search), 175 | Key::Char('\n') | Key::Char('\r') => Some(Action::Select), 176 | Key::Char('\t') => Some(Action::SwitchView), 177 | Key::Char(c) => Some(Action::Char(*c)), 178 | Key::Backspace => Some(Action::Backspace), 179 | Key::Alt('\x1b') => Some(Action::PrevView), 180 | _ => None, 181 | } 182 | } 183 | 184 | pub fn actions(self) -> (mpsc::UnboundedReceiver, Arc>) { 185 | let (mut action_tx, action_rx) = mpsc::unbounded(); 186 | let context = Arc::new(Mutex::new(Context::search())); 187 | 188 | let current_context = context.clone(); 189 | 190 | tokio::spawn(async move { 191 | let mut stdin = tokio::io::stdin(); 192 | let stream = crate::input::events_stream(&mut stdin); 193 | futures::pin_mut!(stream); 194 | 195 | while let Some(event) = stream.next().await { 196 | match event { 197 | Ok(event) => { 198 | let current_context = *current_context.lock().unwrap(); 199 | if let Some(action) = self.action(current_context, &event) { 200 | if let Err(err) = action_tx.send(action).await { 201 | log::warn!("events ended due to closed rx channel {}", err); 202 | break; 203 | } 204 | } 205 | } 206 | Err(err) => { 207 | log::error!("stdint event stream issue: {}", err); 208 | } 209 | }; 210 | } 211 | }); 212 | (action_rx, context) 213 | } 214 | } 215 | 216 | #[cfg(test)] 217 | mod tests { 218 | use super::*; 219 | use std::collections::BinaryHeap; 220 | 221 | use quickcheck::{Arbitrary, Gen, TestResult}; 222 | use quickcheck_macros::quickcheck; 223 | 224 | impl From for Context { 225 | fn from(bits: u8) -> Self { 226 | Self { 227 | is_search: bits & 0b001 > 0, 228 | is_tracklist: bits & 0b010 > 0, 229 | is_playlist: bits & 0b100 > 0, 230 | } 231 | } 232 | } 233 | 234 | #[test] 235 | fn test_context_order() { 236 | let contexts = (1u8..=7) 237 | .map(Context::from) 238 | .collect::>() 239 | .into_sorted_vec(); 240 | assert_eq!( 241 | contexts[0], 242 | Context { 243 | is_search: false, 244 | is_tracklist: false, 245 | is_playlist: true, 246 | } 247 | ); 248 | assert_eq!( 249 | *contexts.last().unwrap(), 250 | Context { 251 | is_search: true, 252 | is_tracklist: true, 253 | is_playlist: true, 254 | } 255 | ); 256 | } 257 | 258 | impl Arbitrary for Context { 259 | fn arbitrary(g: &mut G) -> Self { 260 | let mut val = [0]; 261 | loop { 262 | g.fill_bytes(&mut val); 263 | let this: Context = val[0].into(); 264 | if this.is_valid() { 265 | return this; 266 | } 267 | } 268 | } 269 | } 270 | 271 | #[quickcheck] 272 | fn test_context_search(search: Context, contexts: Vec) -> TestResult { 273 | if !contexts.iter().any(|x| x == &search) { 274 | return TestResult::discard(); 275 | } 276 | 277 | let event = Event::Key(Key::Up); 278 | let contexts = contexts 279 | .into_iter() 280 | .map(|context| { 281 | if context == search { 282 | ContextedAction { 283 | context, 284 | action: Action::Search, 285 | } 286 | } else { 287 | ContextedAction { 288 | context, 289 | action: Action::Quit, 290 | } 291 | } 292 | }) 293 | .collect::>(); 294 | let config: BindingConfig = vec![(event.clone(), contexts)] 295 | .into_iter() 296 | .collect::>() 297 | .into(); 298 | 299 | if let Some(found) = config.action(search, &event) { 300 | TestResult::from_bool(found == Action::Search) 301 | } else { 302 | TestResult::error("item not found") 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error, ErrorKind}; 2 | use std::marker::Unpin; 3 | 4 | use async_stream::stream; 5 | use futures::Stream; 6 | use termion::event::{Event, Key, MouseButton, MouseEvent}; 7 | use tokio::io::AsyncReadExt; 8 | use tokio::prelude::*; 9 | 10 | // This file contains tty event handling rewritten in async 11 | // src: https://github.com/redox-os/termion/blob/master/src/event.rs 12 | 13 | async fn fetch_byte(rdr: &mut (impl AsyncRead + Unpin)) -> Result { 14 | let mut buf = [0u8]; 15 | rdr.read_exact(&mut buf[..]).await?; 16 | Ok(buf[0]) 17 | } 18 | 19 | async fn parse_csi(mut rdr: &mut (impl AsyncRead + Unpin)) -> Option { 20 | let ev = match fetch_byte(&mut rdr).await.ok()? { 21 | b'[' => { 22 | let val = fetch_byte(&mut rdr).await.ok()?; 23 | Event::Key(Key::F(1 + val - b'A')) 24 | } 25 | b'D' => Event::Key(Key::Left), 26 | b'C' => Event::Key(Key::Right), 27 | b'A' => Event::Key(Key::Up), 28 | b'B' => Event::Key(Key::Down), 29 | b'H' => Event::Key(Key::Home), 30 | b'F' => Event::Key(Key::End), 31 | b'M' => { 32 | // X10 emulation mouse encoding: ESC [ CB Cx Cy (6 characters only). 33 | let cb = fetch_byte(&mut rdr).await.ok()? as i8 - 32; 34 | // (1, 1) are the coords for upper left. 35 | let cx = fetch_byte(&mut rdr).await.ok()?.saturating_sub(32) as u16; 36 | let cy = fetch_byte(&mut rdr).await.ok()?.saturating_sub(32) as u16; 37 | Event::Mouse(match cb & 0b11 { 38 | 0 => { 39 | if cb & 0x40 != 0 { 40 | MouseEvent::Press(MouseButton::WheelUp, cx, cy) 41 | } else { 42 | MouseEvent::Press(MouseButton::Left, cx, cy) 43 | } 44 | } 45 | 1 => { 46 | if cb & 0x40 != 0 { 47 | MouseEvent::Press(MouseButton::WheelDown, cx, cy) 48 | } else { 49 | MouseEvent::Press(MouseButton::Middle, cx, cy) 50 | } 51 | } 52 | 2 => MouseEvent::Press(MouseButton::Right, cx, cy), 53 | 3 => MouseEvent::Release(cx, cy), 54 | _ => return None, 55 | }) 56 | } 57 | b'<' => { 58 | // xterm mouse encoding: 59 | // ESC [ < Cb ; Cx ; Cy (;) (M or m) 60 | let mut buf = Vec::new(); 61 | let mut c = fetch_byte(&mut rdr).await.unwrap(); 62 | while match c { 63 | b'm' | b'M' => false, 64 | _ => true, 65 | } { 66 | buf.push(c); 67 | c = fetch_byte(&mut rdr).await.unwrap(); 68 | } 69 | let str_buf = String::from_utf8(buf).unwrap(); 70 | let nums = &mut str_buf.split(';'); 71 | 72 | let cb = nums.next().unwrap().parse::().unwrap(); 73 | let cx = nums.next().unwrap().parse::().unwrap(); 74 | let cy = nums.next().unwrap().parse::().unwrap(); 75 | 76 | let event = match cb { 77 | 0..=2 | 64..=65 => { 78 | let button = match cb { 79 | 0 => MouseButton::Left, 80 | 1 => MouseButton::Middle, 81 | 2 => MouseButton::Right, 82 | 64 => MouseButton::WheelUp, 83 | 65 => MouseButton::WheelDown, 84 | _ => unreachable!(), 85 | }; 86 | match c { 87 | b'M' => MouseEvent::Press(button, cx, cy), 88 | b'm' => MouseEvent::Release(cx, cy), 89 | _ => return None, 90 | } 91 | } 92 | 32 => MouseEvent::Hold(cx, cy), 93 | 3 => MouseEvent::Release(cx, cy), 94 | _ => return None, 95 | }; 96 | 97 | Event::Mouse(event) 98 | } 99 | c @ b'0'..=b'9' => { 100 | // Numbered escape code. 101 | let mut buf = Vec::new(); 102 | buf.push(c); 103 | let mut c = fetch_byte(&mut rdr).await.unwrap(); 104 | // The final byte of a CSI sequence can be in the range 64-126, so 105 | // let's keep reading anything else. 106 | while c < 64 || c > 126 { 107 | buf.push(c); 108 | c = fetch_byte(&mut rdr).await.unwrap(); 109 | } 110 | 111 | match c { 112 | // rxvt mouse encoding: 113 | // ESC [ Cb ; Cx ; Cy ; M 114 | b'M' => { 115 | let str_buf = String::from_utf8(buf).unwrap(); 116 | 117 | let nums: Vec = str_buf.split(';').map(|n| n.parse().unwrap()).collect(); 118 | 119 | let cb = nums[0]; 120 | let cx = nums[1]; 121 | let cy = nums[2]; 122 | 123 | let event = match cb { 124 | 32 => MouseEvent::Press(MouseButton::Left, cx, cy), 125 | 33 => MouseEvent::Press(MouseButton::Middle, cx, cy), 126 | 34 => MouseEvent::Press(MouseButton::Right, cx, cy), 127 | 35 => MouseEvent::Release(cx, cy), 128 | 64 => MouseEvent::Hold(cx, cy), 129 | 96 | 97 => MouseEvent::Press(MouseButton::WheelUp, cx, cy), 130 | _ => return None, 131 | }; 132 | 133 | Event::Mouse(event) 134 | } 135 | // Special key code. 136 | b'~' => { 137 | let str_buf = String::from_utf8(buf).unwrap(); 138 | 139 | // This CSI sequence can be a list of semicolon-separated 140 | // numbers. 141 | let nums: Vec = str_buf.split(';').map(|n| n.parse().unwrap()).collect(); 142 | 143 | if nums.is_empty() { 144 | return None; 145 | } 146 | 147 | // TODO: handle multiple values for key modififiers (ex: values 148 | // [3, 2] means Shift+Delete) 149 | if nums.len() > 1 { 150 | return None; 151 | } 152 | 153 | match nums[0] { 154 | 1 | 7 => Event::Key(Key::Home), 155 | 2 => Event::Key(Key::Insert), 156 | 3 => Event::Key(Key::Delete), 157 | 4 | 8 => Event::Key(Key::End), 158 | 5 => Event::Key(Key::PageUp), 159 | 6 => Event::Key(Key::PageDown), 160 | v @ 11..=15 => Event::Key(Key::F(v - 10)), 161 | v @ 17..=21 => Event::Key(Key::F(v - 11)), 162 | v @ 23..=24 => Event::Key(Key::F(v - 12)), 163 | _ => return None, 164 | } 165 | } 166 | _ => return None, 167 | } 168 | } 169 | _ => return None, 170 | }; 171 | Some(ev) 172 | } 173 | 174 | /// Parse `c` as either a single byte ASCII char or a variable size UTF-8 char. 175 | async fn parse_utf8_char(c: u8, mut rdr: &mut (impl AsyncRead + Unpin)) -> Result { 176 | if c.is_ascii() { 177 | return Ok(c as char); 178 | } 179 | let mut buf = Vec::with_capacity(5); 180 | buf.push(c); 181 | 182 | loop { 183 | buf.push(fetch_byte(&mut rdr).await?); 184 | match std::str::from_utf8(&buf) { 185 | Ok(st) => return Ok(st.chars().next().unwrap()), 186 | Err(err) if buf.len() >= 4 => { 187 | return Err(Error::new( 188 | ErrorKind::Other, 189 | format!("Input character is not valid UTF-8: {}", err), 190 | )); 191 | } 192 | _ => {} 193 | } 194 | } 195 | } 196 | 197 | pub async fn parse_event(mut rdr: &mut (impl AsyncRead + Unpin)) -> Result { 198 | let item = match fetch_byte(&mut rdr).await { 199 | Ok(item) => item, 200 | Err(err) => return Err(err), 201 | }; 202 | match item { 203 | b'\x1B' => { 204 | // This is an escape character, leading a control sequence. 205 | let c = match fetch_byte(&mut rdr).await? { 206 | b'O' => { 207 | match fetch_byte(&mut rdr).await? { 208 | // F1-F4 209 | val @ b'P'..=b'S' => Event::Key(Key::F(1 + val - b'P')), 210 | _ => { 211 | return Err(Error::new( 212 | ErrorKind::Other, 213 | "Could not parse a function key event", 214 | )) 215 | } 216 | } 217 | } 218 | b'[' => { 219 | // This is a CSI sequence. 220 | parse_csi(&mut rdr).await.ok_or_else(|| { 221 | Error::new(ErrorKind::Other, "Could not parse a csi sequence key event") 222 | })? 223 | } 224 | c => Event::Key(Key::Alt(parse_utf8_char(c, rdr).await?)), 225 | }; 226 | Ok(c) 227 | } 228 | b'\n' | b'\r' => Ok(Event::Key(Key::Char('\n'))), 229 | b'\t' => Ok(Event::Key(Key::Char('\t'))), 230 | b'\x7F' => Ok(Event::Key(Key::Backspace)), 231 | c @ b'\x01'..=b'\x19' => Ok(Event::Key(Key::Ctrl((c as u8 - 0x1 + b'a') as char))), 232 | c @ b'\x1C'..=b'\x1F' => Ok(Event::Key(Key::Ctrl((c as u8 - 0x1C + b'4') as char))), 233 | b'\0' => Ok(Event::Key(Key::Null)), 234 | c => Ok({ Event::Key(Key::Char(parse_utf8_char(c, rdr).await?)) }), 235 | } 236 | } 237 | 238 | pub fn events_stream(mut rdr: impl AsyncRead + Unpin) -> impl Stream> { 239 | stream! { 240 | loop { 241 | match parse_event(&mut rdr).await { 242 | Ok(event) => yield Ok(event), 243 | Err(err) if err.kind() == ErrorKind::UnexpectedEof => break, 244 | Err(err) => yield Err(err), 245 | } 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/providers.rs: -------------------------------------------------------------------------------- 1 | use std::convert::{TryFrom, TryInto}; 2 | use std::result::Result as StdResult; 3 | use std::time::Duration; 4 | 5 | use futures::future::TryFutureExt; 6 | use reqwest::Client; 7 | use snafu::ResultExt; 8 | use strum_macros::Display; 9 | use unhtml::FromHtml; 10 | 11 | use crate::meta; 12 | 13 | const REQUEST_TIMEOUT: Duration = Duration::from_secs(2); 14 | 15 | #[derive(FromHtml)] 16 | struct ArtistRaw { 17 | #[html(attr = "href")] 18 | url: Option, 19 | #[html(attr = "inner")] 20 | name: Option, 21 | } 22 | 23 | impl TryFrom for meta::Artist { 24 | type Error = (); 25 | 26 | fn try_from(raw: ArtistRaw) -> StdResult { 27 | Ok(Self { 28 | url: raw.url.ok_or(())?, 29 | name: raw.name.ok_or(())?, 30 | }) 31 | } 32 | } 33 | 34 | #[derive(FromHtml)] 35 | #[html(selector = "div.serp-snippet__artists")] 36 | struct ArtistsRaw { 37 | #[html(selector = "div.artist__content div.artist__name a.d-link")] 38 | artists: Vec, 39 | } 40 | 41 | impl From for meta::Artists { 42 | fn from(raw: ArtistsRaw) -> Self { 43 | Self { 44 | artists: raw 45 | .artists 46 | .into_iter() 47 | .filter_map(|artist| artist.try_into().ok()) 48 | .collect(), 49 | } 50 | } 51 | } 52 | 53 | #[derive(FromHtml)] 54 | struct AlbumRaw { 55 | #[html(selector = "div.album__title a.deco-link", attr = "href")] 56 | url: String, 57 | #[html(selector = "div.album__title", attr = "inner")] 58 | title: String, 59 | #[html(selector = "div.album__artist a.d-link")] 60 | artists: Vec, 61 | #[html(selector = "div.album__year", attr = "inner")] 62 | year_with_version: String, 63 | #[html(selector = "div.album__year span.album__version", attr = "inner")] 64 | version: Option, 65 | } 66 | 67 | impl TryFrom for meta::Album { 68 | type Error = (); 69 | 70 | fn try_from(raw: AlbumRaw) -> StdResult { 71 | Ok(Self { 72 | url: raw.url, 73 | title: raw.title, 74 | artists: raw 75 | .artists 76 | .into_iter() 77 | .filter_map(|raw| raw.try_into().ok()) 78 | .collect(), 79 | year: raw 80 | .year_with_version 81 | .replace(raw.version.as_deref().unwrap_or(""), "") 82 | .parse() 83 | .map_err(|_| ())?, 84 | version: raw.version, 85 | }) 86 | } 87 | } 88 | 89 | #[derive(FromHtml)] 90 | #[html(selector = "div.centerblock")] 91 | struct AlbumsRaw { 92 | #[html(selector = "div.album_selectable")] 93 | albums: Vec, 94 | } 95 | 96 | impl From for meta::Albums { 97 | fn from(raws: AlbumsRaw) -> Self { 98 | Self { 99 | albums: raws 100 | .albums 101 | .into_iter() 102 | .filter_map(|raws| raws.try_into().ok()) 103 | .collect(), 104 | } 105 | } 106 | } 107 | 108 | #[derive(FromHtml)] 109 | struct TrackRaw { 110 | #[html(selector = "div.d-track__name a.d-track__title", attr = "href")] 111 | url: Option, 112 | #[html(selector = "div.d-track__name a.d-track__title", attr = "inner")] 113 | name: Option, 114 | #[html(selector = "div.d-track__meta span.d-track__artists a")] 115 | artists: Vec, 116 | } 117 | 118 | impl TryFrom for meta::Track { 119 | type Error = (); 120 | 121 | fn try_from(raw: TrackRaw) -> StdResult { 122 | // `/album/4766/track/57703` 123 | let url = raw.url.ok_or(())?; 124 | let name = raw.name.ok_or(())?; 125 | let mut iter = url.split('/'); 126 | iter.next(); 127 | let mut parse_int = move || { 128 | iter.next(); 129 | iter.next().and_then(|val| val.parse().ok()).ok_or(()) 130 | }; 131 | let album_id = parse_int()?; 132 | let track_id = parse_int()?; 133 | 134 | let artists = raw 135 | .artists 136 | .into_iter() 137 | .filter_map(|raw| raw.try_into().ok()) 138 | .collect(); 139 | 140 | Ok(Self { 141 | album_id, 142 | track_id, 143 | name, 144 | artists: std::sync::Arc::new(artists), 145 | }) 146 | } 147 | } 148 | 149 | #[derive(FromHtml)] 150 | struct TracksRaw { 151 | #[html(selector = "div.d-track")] 152 | tracks: Vec, 153 | } 154 | 155 | impl From for meta::Tracks { 156 | fn from(raws: TracksRaw) -> Self { 157 | Self { 158 | tracks: raws 159 | .tracks 160 | .into_iter() 161 | .filter_map(|track| track.try_into().ok()) 162 | .collect(), 163 | } 164 | } 165 | } 166 | 167 | const BASE_URL: &str = "https://music.yandex.ru"; 168 | 169 | /* 170 | {"codec":"mp3" 171 | "bitrate":192, 172 | "src":"https://storage.mds.yandex.net/file-download-info/53090_49160231.49166739.1.57703/2?sign=1172df07524abd16c528c85adacf6e3716cb13aec818822d7fcf32c48d1a5fd3&ts=5db42aa5", 173 | "gain":false, 174 | "preview":false} 175 | */ 176 | #[derive(serde::Deserialize, Debug)] 177 | struct BalancerResponse { 178 | codec: String, 179 | bitrate: u32, 180 | src: String, 181 | //.. 182 | } 183 | 184 | /* 185 | 186 | 187 | s96vla.storage.yandex.net 188 | /rmusic/U2FsdGVkX18HQugf-LCm69vdpBvuPSCgSPq64xSmb0Ld-WB0mwjeDmmEuxE9cVjT_LlO25BG46S_igZvDqh_AuafEynGp4qFyVMGb5iI5ZE/be2821fda525ebd020996360f6f394dee09af26c3623aabd1d62ac2dff7ec2e6 189 | 000595cf7fdf9b99 190 | -1 191 | be2821fda525ebd020996360f6f394dee09af26c3623aabd1d62ac2dff7ec2e6 192 | 193 | */ 194 | #[derive(serde::Deserialize, Debug)] 195 | struct DownloadInfo { 196 | host: String, 197 | path: String, 198 | ts: String, 199 | s: String, 200 | } 201 | 202 | #[derive(Debug, snafu::Snafu)] 203 | pub enum Error { 204 | #[snafu(display("http err: {}", source))] 205 | HttpError { url: String, source: reqwest::Error }, 206 | #[snafu(display("html error: {}", source))] 207 | HtmlError { source: unhtml::Error }, 208 | #[snafu(display("xml error: {}", source))] 209 | XmlError { 210 | body: String, 211 | source: serde_xml_rs::Error, 212 | }, 213 | #[snafu(display("timeouted"))] 214 | Timeout { source: tokio::time::Elapsed }, 215 | } 216 | 217 | pub type Result = StdResult; 218 | 219 | /// Yandex Music info/media provider 220 | pub struct Provider { 221 | client: Client, 222 | } 223 | 224 | #[derive(Display, Clone, Copy)] 225 | #[strum(serialize_all = "snake_case")] 226 | enum SearchType { 227 | Albums, 228 | Tracks, 229 | Artists, 230 | } 231 | 232 | impl SearchType { 233 | fn search_url(self, search_text: &str) -> String { 234 | format!( 235 | "{}/search?text={}&type={}", 236 | BASE_URL, 237 | search_text, // TODO: url encode 238 | self.to_string() 239 | ) 240 | } 241 | } 242 | 243 | impl Provider { 244 | pub fn new() -> Self { 245 | Self { 246 | client: Client::new(), 247 | } 248 | } 249 | 250 | pub async fn artists_search(&self, text: &str) -> Result { 251 | let url = SearchType::Artists.search_url(text); 252 | 253 | tokio::time::timeout( 254 | REQUEST_TIMEOUT, 255 | self.client.get(&url).send().and_then(|r| r.text()), 256 | ) 257 | .await 258 | .context(Timeout {})? 259 | .context(HttpError { url }) 260 | .and_then(|body| { 261 | ArtistsRaw::from_html(&body) 262 | .map(Into::into) 263 | .context(HtmlError {}) 264 | }) 265 | } 266 | 267 | pub async fn artist_albums(&self, artist: &meta::Artist) -> Result { 268 | let url = format!("{}{}/albums", BASE_URL, artist.url); 269 | 270 | tokio::time::timeout( 271 | REQUEST_TIMEOUT, 272 | self.client.get(&url).send().and_then(|r| r.text()), 273 | ) 274 | .await 275 | .context(Timeout {})? 276 | .context(HttpError { url }) 277 | .and_then(|body| { 278 | AlbumsRaw::from_html(&body) 279 | .map(Into::into) 280 | .context(HtmlError {}) 281 | }) 282 | } 283 | 284 | pub async fn artist_tracks(&self, artist: &meta::Artist) -> Result { 285 | let url = format!("{}{}/tracks", BASE_URL, artist.url); 286 | 287 | tokio::time::timeout( 288 | REQUEST_TIMEOUT, 289 | self.client.get(&url).send().and_then(|r| r.text()), 290 | ) 291 | .await 292 | .context(Timeout {})? 293 | .context(HttpError { url }) 294 | .and_then(|body| { 295 | TracksRaw::from_html(&body) 296 | .map(Into::into) 297 | .context(HtmlError {}) 298 | }) 299 | } 300 | 301 | pub async fn album_search(&self, text: &str) -> Result { 302 | let url = SearchType::Albums.search_url(text); 303 | 304 | tokio::time::timeout( 305 | REQUEST_TIMEOUT, 306 | self.client.get(&url).send().and_then(|r| r.text()), 307 | ) 308 | .await 309 | .context(Timeout {})? 310 | .context(HttpError { url }) 311 | .and_then(|body| { 312 | AlbumsRaw::from_html(&body) 313 | .map(Into::into) 314 | .context(HtmlError {}) 315 | }) 316 | } 317 | 318 | pub async fn track_search(&self, text: &str) -> Result { 319 | let url = SearchType::Tracks.search_url(text); 320 | 321 | tokio::time::timeout( 322 | REQUEST_TIMEOUT, 323 | self.client.get(&url).send().and_then(|r| r.text()), 324 | ) 325 | .await 326 | .context(Timeout {})? 327 | .context(HttpError { url }) 328 | .and_then(|body| { 329 | TracksRaw::from_html(&body) 330 | .map(Into::into) 331 | .context(HtmlError {}) 332 | }) 333 | } 334 | 335 | pub async fn album_tracks(&self, album: &meta::Album) -> Result { 336 | let url = format!("{}{}", BASE_URL, album.url); 337 | 338 | tokio::time::timeout( 339 | REQUEST_TIMEOUT, 340 | self.client.get(&url).send().and_then(|r| r.text()), 341 | ) 342 | .await 343 | .context(Timeout {})? 344 | .context(HttpError { url }) 345 | .and_then(|body| { 346 | TracksRaw::from_html(&body) 347 | .map(Into::into) 348 | .context(HtmlError {}) 349 | }) 350 | } 351 | 352 | pub async fn get_track_url(&self, track: &meta::Track) -> Result { 353 | let url = format!("https://music.yandex.ru/api/v2.1/handlers/track/{}:{}/web-album-track-track-saved/download/m", track.track_id, track.album_id); 354 | 355 | let url = tokio::time::timeout( 356 | REQUEST_TIMEOUT, 357 | self.client 358 | .get(&url) 359 | .header( 360 | "X-Retpath-Y", 361 | format!("https%3A%2F%2Fmusic.yandex.ru%2Falbum%2F{}", track.album_id), 362 | ) 363 | .send() 364 | .and_then(|r| r.json::()), 365 | ) 366 | .await 367 | .context(Timeout {})? 368 | .context(HttpError { url })? 369 | .src; 370 | 371 | let info = tokio::time::timeout( 372 | REQUEST_TIMEOUT, 373 | self.client.get(&url).send().and_then(|r| r.text()), 374 | ) 375 | .await 376 | .context(Timeout {})? 377 | .context(HttpError { url }) 378 | .and_then(|response| { 379 | serde_xml_rs::from_str::(&response).context(XmlError { body: response }) 380 | })?; 381 | 382 | Ok(format!( 383 | "https://{}/get-mp3/11111111111111111111111111111111/{}{}?track-id={}&play=false", 384 | info.host, info.ts, info.path, track.track_id 385 | )) 386 | } 387 | } 388 | 389 | #[cfg(test)] 390 | mod tests { 391 | use super::*; 392 | 393 | #[test] 394 | fn test_search_type_string() { 395 | assert_eq!(SearchType::Albums.to_string(), "albums"); 396 | assert_eq!(SearchType::Tracks.to_string(), "tracks"); 397 | assert_eq!(SearchType::Artists.to_string(), "artists"); 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{mpsc, Arc}; 2 | 3 | use log::Level; 4 | use snafu::ResultExt; 5 | use tokio::stream::StreamExt; 6 | 7 | use crate::config::Config; 8 | use crate::draw; 9 | use crate::key::{Action, Context as KeyContext}; 10 | use crate::logger::Logger; 11 | use crate::player::{self, Command}; 12 | use crate::providers::Provider; 13 | use crate::view::{AlbumSearch, ArtistSearch, MainView, Playlist, TrackList, View}; 14 | 15 | struct State { 16 | provider: Provider, 17 | player_state: player::State, 18 | prev_view: Option, 19 | main_view: MainView, 20 | } 21 | 22 | impl State { 23 | fn new(provider: Provider, player_state: player::State) -> Self { 24 | Self { 25 | provider, 26 | player_state, 27 | prev_view: None, 28 | main_view: MainView::default(), 29 | } 30 | } 31 | 32 | fn pointer_down(&mut self) { 33 | if let Some(mut cursor) = self.main_view.cursor_mut() { 34 | *cursor += 1; 35 | } 36 | } 37 | 38 | fn pointer_up(&mut self) { 39 | if let Some(mut cursor) = self.main_view.cursor_mut() { 40 | *cursor = cursor.saturating_sub(1); 41 | } 42 | } 43 | 44 | fn push_char(&mut self, c: char) { 45 | self.main_view.insert_buffer_mut().push(c); 46 | } 47 | 48 | fn backspace(&mut self) { 49 | self.main_view.insert_buffer_mut().pop(); 50 | } 51 | 52 | fn restore_view(&mut self) { 53 | if let Some(view) = self.prev_view.take() { 54 | self.main_view.replace_view(view); 55 | } 56 | } 57 | 58 | fn update_view(&mut self, new_view: impl Into) { 59 | self.prev_view = self.main_view.replace_view(new_view.into()).into(); 60 | } 61 | 62 | #[allow(clippy::single_match)] 63 | async fn switch_to_album_search(&mut self) -> Result<(), crate::providers::Error> { 64 | match &mut *self.main_view { 65 | View::ArtistSearch(search) => { 66 | if let Some(artist) = search.cached_artists.get(search.cursor) { 67 | let albums = self.provider.artist_albums(&artist).await?.albums; 68 | 69 | self.update_view(AlbumSearch::from(albums)); 70 | } else { 71 | search.cursor = 0; 72 | } 73 | } 74 | _ => {} 75 | } 76 | Ok(()) 77 | } 78 | 79 | #[allow(clippy::single_match)] 80 | async fn switch_to_track_search(&mut self) -> Result<(), crate::providers::Error> { 81 | match &mut *self.main_view { 82 | View::ArtistSearch(search) => { 83 | if let Some(artist) = search.cached_artists.get(search.cursor) { 84 | let tracks = self 85 | .provider 86 | .artist_tracks(&artist) 87 | .await? 88 | .tracks 89 | .into_iter() 90 | .map(|mut track| { 91 | Arc::get_mut(&mut track.artists) 92 | .unwrap() 93 | .insert(0, artist.clone()); 94 | track 95 | }) 96 | .collect::>(); 97 | 98 | self.update_view(TrackList::from(tracks)); 99 | } else { 100 | search.cursor = 0; 101 | } 102 | } 103 | _ => {} 104 | } 105 | Ok(()) 106 | } 107 | 108 | async fn switch_to_artist(&mut self) -> Result<(), crate::providers::Error> { 109 | match &mut *self.main_view { 110 | View::AlbumSearch(search) => { 111 | if let Some(album) = search.cached_albums.get(search.cursor) { 112 | let artists = album.artists.clone(); 113 | 114 | self.update_view(ArtistSearch::from(artists)); 115 | } else { 116 | search.cursor = 0; 117 | } 118 | } 119 | View::TrackList(list) => { 120 | if let Some(track) = list.cached_tracks.get(list.cursor) { 121 | let artists = track.artists.to_vec(); 122 | 123 | self.update_view(ArtistSearch::from(artists)); 124 | } else { 125 | list.cursor = 0; 126 | } 127 | } 128 | _ => {} 129 | } 130 | Ok(()) 131 | } 132 | 133 | async fn search(&mut self) -> Result<(), crate::providers::Error> { 134 | match self.main_view.view_and_buffer_mut() { 135 | (View::ArtistSearch(search), insert_buffer) if !insert_buffer.is_empty() => { 136 | search.cached_artists = self.provider.artists_search(&insert_buffer).await?.artists; 137 | insert_buffer.clear(); 138 | } 139 | (View::AlbumSearch(search), insert_buffer) if !insert_buffer.is_empty() => { 140 | search.cached_albums = self.provider.album_search(&insert_buffer).await?.albums; 141 | insert_buffer.clear(); 142 | } 143 | (View::TrackList(_), insert_buffer) if !insert_buffer.is_empty() => { 144 | let tracks = self.provider.track_search(insert_buffer).await?.tracks; 145 | self.update_view(TrackList::from(tracks)); 146 | } 147 | _ => {} 148 | } 149 | Ok(()) 150 | } 151 | 152 | async fn select(&mut self) -> Result, crate::providers::Error> { 153 | match &mut *self.main_view { 154 | View::AlbumSearch(search) if search.cursor < search.cached_albums.len() => { 155 | let album = &search.cached_albums[search.cursor]; 156 | let tracks = self 157 | .provider 158 | .album_tracks(&album) 159 | .await? 160 | .tracks 161 | .into_iter() 162 | .map(|mut track| { 163 | let track_artists = Arc::get_mut(&mut track.artists).unwrap(); 164 | // XXX: quadratic complexity here, though maybe ok due to small sizes 165 | for album_artist in album.artists.iter() { 166 | if !track_artists.iter().any(|x| x.name == album_artist.name) { 167 | track_artists.push(album_artist.clone()); 168 | } 169 | } 170 | track 171 | }) 172 | .collect::>(); 173 | 174 | self.update_view(TrackList::from(tracks)); 175 | } 176 | View::TrackList(search) if search.cursor < search.cached_tracks.len() => { 177 | let track = search.cached_tracks[search.cursor].clone(); 178 | let url = self.provider.get_track_url(&track).await?; 179 | log::info!("{:?}; {:?}", track, url); 180 | return Ok(Some(Command::Enqueue { track, url })); 181 | } 182 | _ => {} 183 | } 184 | Ok(None) 185 | } 186 | } 187 | 188 | #[derive(Debug, snafu::Snafu)] 189 | pub enum Error { 190 | #[snafu(display("player error at {:?}: {}", action, source))] 191 | PlayerCommandError { 192 | action: Action, 193 | source: mpsc::SendError, 194 | }, 195 | #[snafu(display("draw error at {}: {}", case, source))] 196 | Drawer { 197 | case: &'static str, 198 | source: std::io::Error, 199 | }, 200 | } 201 | 202 | pub struct App { 203 | config: Config, 204 | provider: Provider, 205 | player_commands: mpsc::Sender, 206 | player_state: player::State, 207 | } 208 | 209 | impl App { 210 | pub fn create( 211 | config: Config, 212 | provider: Provider, 213 | player_commands: mpsc::Sender, 214 | player_state: player::State, 215 | ) -> Result { 216 | Ok(Self { 217 | config, 218 | provider, 219 | player_commands, 220 | player_state, 221 | }) 222 | } 223 | 224 | pub async fn run(self) -> Result<(), Error> { 225 | let App { 226 | config, 227 | provider, 228 | player_commands, 229 | player_state, 230 | } = self; 231 | 232 | let mut state = State::new(provider, player_state); 233 | let mut logger = Logger::default(); 234 | let mut drawer = draw::Drawer::new().context(Drawer { 235 | case: "create context", 236 | })?; 237 | 238 | drawer 239 | .redraw(&state.main_view, logger.log_lines()) 240 | .context(Drawer { 241 | case: "initial draw", 242 | })?; 243 | 244 | let (mut events, current_context) = config.binding.actions(); 245 | 246 | while let Some(action) = events.next().await { 247 | match action { 248 | Action::PointerUp => state.pointer_up(), 249 | Action::PointerDown => state.pointer_down(), 250 | Action::NextTrack => player_commands 251 | .send(Command::NextTrack) 252 | .context(PlayerCommandError { action })?, 253 | Action::PrevTrack => player_commands 254 | .send(Command::PrevTrack) 255 | .context(PlayerCommandError { action })?, 256 | Action::Quit => return Ok(()), 257 | Action::FlipPause => player_commands 258 | .send(Command::FlipPause) 259 | .context(PlayerCommandError { action })?, 260 | Action::Forward5 => player_commands 261 | .send(Command::Seek(5)) 262 | .context(PlayerCommandError { action })?, 263 | Action::Backward5 => player_commands 264 | .send(Command::Seek(-5)) 265 | .context(PlayerCommandError { action })?, 266 | Action::Stop => player_commands 267 | .send(Command::Stop) 268 | .context(PlayerCommandError { action })?, 269 | Action::AddAll => { 270 | if let View::TrackList(ref search) = *state.main_view { 271 | for track in search.cached_tracks.iter() { 272 | match state.provider.get_track_url(&track).await { 273 | Ok(url) => { 274 | let track = track.clone(); 275 | player_commands 276 | .send(Command::Enqueue { track, url }) 277 | .context(PlayerCommandError { action })?; 278 | } 279 | Err(err) => logger.log( 280 | Level::Error, 281 | format!("cannot get track: {:?}", track).as_str(), 282 | err, 283 | ), 284 | } 285 | } 286 | logger.log(Level::Info, "ok", "all tracks are added to queue"); 287 | } 288 | } 289 | Action::ShowPlaylist => { 290 | if let View::Playlist(_) = *state.main_view { 291 | state.restore_view(); 292 | } else { 293 | let player_state = state.player_state.lock().unwrap(); 294 | let tracks = player_state.playlist().cloned().collect(); 295 | let current = player_state.current(); 296 | drop(player_state); 297 | 298 | state.update_view(Playlist::create(tracks, current)); 299 | } 300 | } 301 | Action::SwitchToAlbums => { 302 | if let Err(err) = state.switch_to_album_search().await { 303 | logger.log(Level::Error, "cannot switch to album search", err); 304 | } 305 | } 306 | Action::SwitchToTracks => { 307 | if let Err(err) = state.switch_to_track_search().await { 308 | logger.log(Level::Error, "cannot switch to track search", err); 309 | } 310 | } 311 | Action::SwitchToArtists => { 312 | if let Err(err) = state.switch_to_artist().await { 313 | logger.log(Level::Error, "cannot switch to artist search", err); 314 | } 315 | } 316 | Action::Search => match state.search().await { 317 | Ok(()) => logger.log(Level::Info, "ok", "completed"), 318 | Err(err) => { 319 | logger.log(Level::Error, "search failed", err); 320 | } 321 | }, 322 | Action::Select => match state.select().await { 323 | Ok(cmd) => { 324 | if let Some(cmd) = cmd { 325 | player_commands 326 | .send(cmd) 327 | .context(PlayerCommandError { action })?; 328 | } 329 | logger.log(Level::Info, "ok", "completed"); 330 | } 331 | Err(err) => logger.log(Level::Error, "cannot perform action", err), 332 | }, 333 | Action::SwitchView => match state.main_view.view().clone() { 334 | View::AlbumSearch(_) => state.update_view(TrackList::default()), 335 | View::TrackList(_) => state.update_view(ArtistSearch::default()), 336 | View::ArtistSearch(_) => state.update_view(AlbumSearch::default()), 337 | _ => continue, 338 | }, 339 | Action::PrevView => { 340 | if let Some(view) = state.prev_view.take() { 341 | state.main_view.replace_view(view); 342 | } 343 | } 344 | Action::Char(c) => state.push_char(c), 345 | Action::Backspace => state.backspace(), 346 | Action::Refresh => continue, 347 | } 348 | 349 | *current_context.lock().unwrap() = match state.main_view.view() { 350 | View::AlbumSearch(_) | View::ArtistSearch(_) => KeyContext::search(), 351 | View::TrackList(_) => KeyContext::search() | KeyContext::tracklist(), 352 | View::Playlist(_) => KeyContext::playlist(), 353 | }; 354 | 355 | drawer 356 | .redraw(&state.main_view, logger.log_lines()) 357 | .context(Drawer { 358 | case: "loop update state", 359 | })?; 360 | } 361 | Ok(()) 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aho-corasick" 5 | version = "0.7.8" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "743ad5a418686aad3b87fd14c43badd828cf26e214a00f92a384291cf22e1811" 8 | dependencies = [ 9 | "memchr", 10 | ] 11 | 12 | [[package]] 13 | name = "anyhow" 14 | version = "1.0.26" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "7825f6833612eb2414095684fcf6c635becf3ce97fe48cf6421321e93bfbd53c" 17 | 18 | [[package]] 19 | name = "arrayref" 20 | version = "0.3.6" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" 23 | 24 | [[package]] 25 | name = "arrayvec" 26 | version = "0.5.1" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" 29 | 30 | [[package]] 31 | name = "async-stream" 32 | version = "0.2.1" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "22068c0c19514942eefcfd4daf8976ef1aad84e61539f95cd200c35202f80af5" 35 | dependencies = [ 36 | "async-stream-impl", 37 | "futures-core", 38 | ] 39 | 40 | [[package]] 41 | name = "async-stream-impl" 42 | version = "0.2.1" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "25f9db3b38af870bf7e5cc649167533b493928e50744e2c30ae350230b414670" 45 | dependencies = [ 46 | "proc-macro2 1.0.8", 47 | "quote 1.0.2", 48 | "syn 1.0.15", 49 | ] 50 | 51 | [[package]] 52 | name = "autocfg" 53 | version = "0.1.7" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" 56 | 57 | [[package]] 58 | name = "autocfg" 59 | version = "1.0.0" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 62 | 63 | [[package]] 64 | name = "backtrace" 65 | version = "0.3.44" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "e4036b9bf40f3cf16aba72a3d65e8a520fc4bafcdc7079aea8f848c58c5b5536" 68 | dependencies = [ 69 | "backtrace-sys", 70 | "cfg-if", 71 | "libc", 72 | "rustc-demangle", 73 | ] 74 | 75 | [[package]] 76 | name = "backtrace-sys" 77 | version = "0.1.32" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491" 80 | dependencies = [ 81 | "cc", 82 | "libc", 83 | ] 84 | 85 | [[package]] 86 | name = "base64" 87 | version = "0.11.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" 90 | 91 | [[package]] 92 | name = "bitflags" 93 | version = "1.2.1" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 96 | 97 | [[package]] 98 | name = "blake2b_simd" 99 | version = "0.5.10" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" 102 | dependencies = [ 103 | "arrayref", 104 | "arrayvec", 105 | "constant_time_eq", 106 | ] 107 | 108 | [[package]] 109 | name = "bumpalo" 110 | version = "3.2.0" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "1f359dc14ff8911330a51ef78022d376f25ed00248912803b58f00cb1c27f742" 113 | 114 | [[package]] 115 | name = "byteorder" 116 | version = "1.3.4" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 119 | 120 | [[package]] 121 | name = "bytes" 122 | version = "0.5.4" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" 125 | 126 | [[package]] 127 | name = "c2-chacha" 128 | version = "0.2.3" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" 131 | dependencies = [ 132 | "ppv-lite86", 133 | ] 134 | 135 | [[package]] 136 | name = "cassowary" 137 | version = "0.3.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 140 | 141 | [[package]] 142 | name = "cc" 143 | version = "1.0.50" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" 146 | 147 | [[package]] 148 | name = "cfg-if" 149 | version = "0.1.10" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 152 | 153 | [[package]] 154 | name = "chrono" 155 | version = "0.4.10" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01" 158 | dependencies = [ 159 | "num-integer", 160 | "num-traits 0.2.11", 161 | "time", 162 | ] 163 | 164 | [[package]] 165 | name = "cloudabi" 166 | version = "0.0.3" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" 169 | dependencies = [ 170 | "bitflags", 171 | ] 172 | 173 | [[package]] 174 | name = "constant_time_eq" 175 | version = "0.1.5" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 178 | 179 | [[package]] 180 | name = "core-foundation" 181 | version = "0.6.4" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d" 184 | dependencies = [ 185 | "core-foundation-sys", 186 | "libc", 187 | ] 188 | 189 | [[package]] 190 | name = "core-foundation-sys" 191 | version = "0.6.2" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" 194 | 195 | [[package]] 196 | name = "crossbeam-utils" 197 | version = "0.7.2" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" 200 | dependencies = [ 201 | "autocfg 1.0.0", 202 | "cfg-if", 203 | "lazy_static", 204 | ] 205 | 206 | [[package]] 207 | name = "cssparser" 208 | version = "0.25.9" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "fbe18ca4efb9ba3716c6da66cc3d7e673bf59fa576353011f48c4cfddbdd740e" 211 | dependencies = [ 212 | "autocfg 0.1.7", 213 | "cssparser-macros", 214 | "dtoa-short", 215 | "itoa", 216 | "matches", 217 | "phf", 218 | "proc-macro2 1.0.8", 219 | "procedural-masquerade", 220 | "quote 1.0.2", 221 | "smallvec 0.6.13", 222 | "syn 1.0.15", 223 | ] 224 | 225 | [[package]] 226 | name = "cssparser-macros" 227 | version = "0.3.6" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "5bb1c84e87c717666564ec056105052331431803d606bd45529b28547b611eef" 230 | dependencies = [ 231 | "phf_codegen", 232 | "proc-macro2 1.0.8", 233 | "procedural-masquerade", 234 | "quote 1.0.2", 235 | "syn 1.0.15", 236 | ] 237 | 238 | [[package]] 239 | name = "derive_more" 240 | version = "0.99.3" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "a806e96c59a76a5ba6e18735b6cf833344671e61e7863f2edb5c518ea2cac95c" 243 | dependencies = [ 244 | "proc-macro2 1.0.8", 245 | "quote 1.0.2", 246 | "syn 1.0.15", 247 | ] 248 | 249 | [[package]] 250 | name = "dirs" 251 | version = "2.0.2" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" 254 | dependencies = [ 255 | "cfg-if", 256 | "dirs-sys", 257 | ] 258 | 259 | [[package]] 260 | name = "dirs-sys" 261 | version = "0.3.4" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" 264 | dependencies = [ 265 | "cfg-if", 266 | "libc", 267 | "redox_users", 268 | "winapi 0.3.8", 269 | ] 270 | 271 | [[package]] 272 | name = "doc-comment" 273 | version = "0.3.1" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "923dea538cea0aa3025e8685b20d6ee21ef99c4f77e954a30febbaac5ec73a97" 276 | 277 | [[package]] 278 | name = "dtoa" 279 | version = "0.4.5" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" 282 | 283 | [[package]] 284 | name = "dtoa-short" 285 | version = "0.3.2" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "59020b8513b76630c49d918c33db9f4c91638e7d3404a28084083b87e33f76f2" 288 | dependencies = [ 289 | "dtoa", 290 | ] 291 | 292 | [[package]] 293 | name = "ego-tree" 294 | version = "0.6.2" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" 297 | 298 | [[package]] 299 | name = "either" 300 | version = "1.5.3" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" 303 | 304 | [[package]] 305 | name = "encoding_rs" 306 | version = "0.8.22" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "cd8d03faa7fe0c1431609dfad7bbe827af30f82e1e2ae6f7ee4fca6bd764bc28" 309 | dependencies = [ 310 | "cfg-if", 311 | ] 312 | 313 | [[package]] 314 | name = "enum_primitive" 315 | version = "0.1.1" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180" 318 | dependencies = [ 319 | "num-traits 0.1.43", 320 | ] 321 | 322 | [[package]] 323 | name = "env_logger" 324 | version = "0.7.1" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" 327 | dependencies = [ 328 | "log 0.4.8", 329 | "regex", 330 | ] 331 | 332 | [[package]] 333 | name = "error-chain" 334 | version = "0.10.0" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "d9435d864e017c3c6afeac1654189b06cdb491cf2ff73dbf0d73b0f292f42ff8" 337 | dependencies = [ 338 | "backtrace", 339 | ] 340 | 341 | [[package]] 342 | name = "flexi_logger" 343 | version = "0.14.8" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "515fb7f6541dafe542c87c12a7ab6a52190cccb6c348b5951ef62d9978189ae8" 346 | dependencies = [ 347 | "chrono", 348 | "glob", 349 | "log 0.4.8", 350 | "regex", 351 | "yansi", 352 | ] 353 | 354 | [[package]] 355 | name = "fnv" 356 | version = "1.0.6" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" 359 | 360 | [[package]] 361 | name = "foreign-types" 362 | version = "0.3.2" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 365 | dependencies = [ 366 | "foreign-types-shared", 367 | ] 368 | 369 | [[package]] 370 | name = "foreign-types-shared" 371 | version = "0.1.1" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 374 | 375 | [[package]] 376 | name = "fuchsia-cprng" 377 | version = "0.1.1" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 380 | 381 | [[package]] 382 | name = "fuchsia-zircon" 383 | version = "0.3.3" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" 386 | dependencies = [ 387 | "bitflags", 388 | "fuchsia-zircon-sys", 389 | ] 390 | 391 | [[package]] 392 | name = "fuchsia-zircon-sys" 393 | version = "0.3.3" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" 396 | 397 | [[package]] 398 | name = "futf" 399 | version = "0.1.4" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" 402 | dependencies = [ 403 | "mac", 404 | "new_debug_unreachable", 405 | ] 406 | 407 | [[package]] 408 | name = "futures" 409 | version = "0.3.4" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "5c329ae8753502fb44ae4fc2b622fa2a94652c41e795143765ba0927f92ab780" 412 | dependencies = [ 413 | "futures-channel", 414 | "futures-core", 415 | "futures-executor", 416 | "futures-io", 417 | "futures-sink", 418 | "futures-task", 419 | "futures-util", 420 | ] 421 | 422 | [[package]] 423 | name = "futures-channel" 424 | version = "0.3.4" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "f0c77d04ce8edd9cb903932b608268b3fffec4163dc053b3b402bf47eac1f1a8" 427 | dependencies = [ 428 | "futures-core", 429 | "futures-sink", 430 | ] 431 | 432 | [[package]] 433 | name = "futures-core" 434 | version = "0.3.4" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "f25592f769825e89b92358db00d26f965761e094951ac44d3663ef25b7ac464a" 437 | 438 | [[package]] 439 | name = "futures-executor" 440 | version = "0.3.4" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "f674f3e1bcb15b37284a90cedf55afdba482ab061c407a9c0ebbd0f3109741ba" 443 | dependencies = [ 444 | "futures-core", 445 | "futures-task", 446 | "futures-util", 447 | ] 448 | 449 | [[package]] 450 | name = "futures-io" 451 | version = "0.3.4" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "a638959aa96152c7a4cddf50fcb1e3fede0583b27157c26e67d6f99904090dc6" 454 | 455 | [[package]] 456 | name = "futures-macro" 457 | version = "0.3.4" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "9a5081aa3de1f7542a794a397cde100ed903b0630152d0973479018fd85423a7" 460 | dependencies = [ 461 | "proc-macro-hack", 462 | "proc-macro2 1.0.8", 463 | "quote 1.0.2", 464 | "syn 1.0.15", 465 | ] 466 | 467 | [[package]] 468 | name = "futures-sink" 469 | version = "0.3.4" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "3466821b4bc114d95b087b850a724c6f83115e929bc88f1fa98a3304a944c8a6" 472 | 473 | [[package]] 474 | name = "futures-task" 475 | version = "0.3.4" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "7b0a34e53cf6cdcd0178aa573aed466b646eb3db769570841fda0c7ede375a27" 478 | 479 | [[package]] 480 | name = "futures-util" 481 | version = "0.3.4" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "22766cf25d64306bedf0384da004d05c9974ab104fcc4528f1236181c18004c5" 484 | dependencies = [ 485 | "futures-channel", 486 | "futures-core", 487 | "futures-io", 488 | "futures-macro", 489 | "futures-sink", 490 | "futures-task", 491 | "memchr", 492 | "pin-utils", 493 | "proc-macro-hack", 494 | "proc-macro-nested", 495 | "slab", 496 | ] 497 | 498 | [[package]] 499 | name = "fxhash" 500 | version = "0.2.1" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 503 | dependencies = [ 504 | "byteorder", 505 | ] 506 | 507 | [[package]] 508 | name = "getopts" 509 | version = "0.2.21" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" 512 | dependencies = [ 513 | "unicode-width", 514 | ] 515 | 516 | [[package]] 517 | name = "getrandom" 518 | version = "0.1.14" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 521 | dependencies = [ 522 | "cfg-if", 523 | "libc", 524 | "wasi", 525 | ] 526 | 527 | [[package]] 528 | name = "glob" 529 | version = "0.3.0" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 532 | 533 | [[package]] 534 | name = "h2" 535 | version = "0.2.1" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "b9433d71e471c1736fd5a61b671fc0b148d7a2992f666c958d03cd8feb3b88d1" 538 | dependencies = [ 539 | "bytes", 540 | "fnv", 541 | "futures-core", 542 | "futures-sink", 543 | "futures-util", 544 | "http", 545 | "indexmap", 546 | "log 0.4.8", 547 | "slab", 548 | "tokio", 549 | "tokio-util", 550 | ] 551 | 552 | [[package]] 553 | name = "heck" 554 | version = "0.3.1" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 557 | dependencies = [ 558 | "unicode-segmentation", 559 | ] 560 | 561 | [[package]] 562 | name = "hermit-abi" 563 | version = "0.1.7" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "e2c55f143919fbc0bc77e427fe2d74cf23786d7c1875666f2fde3ac3c659bb67" 566 | dependencies = [ 567 | "libc", 568 | ] 569 | 570 | [[package]] 571 | name = "html5ever" 572 | version = "0.24.1" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "025483b0a1e4577bb28578318c886ee5f817dda6eb62473269349044406644cb" 575 | dependencies = [ 576 | "log 0.4.8", 577 | "mac", 578 | "markup5ever", 579 | "proc-macro2 1.0.8", 580 | "quote 1.0.2", 581 | "syn 1.0.15", 582 | ] 583 | 584 | [[package]] 585 | name = "http" 586 | version = "0.2.0" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "b708cc7f06493459026f53b9a61a7a121a5d1ec6238dee58ea4941132b30156b" 589 | dependencies = [ 590 | "bytes", 591 | "fnv", 592 | "itoa", 593 | ] 594 | 595 | [[package]] 596 | name = "http-body" 597 | version = "0.3.1" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" 600 | dependencies = [ 601 | "bytes", 602 | "http", 603 | ] 604 | 605 | [[package]] 606 | name = "httparse" 607 | version = "1.3.4" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" 610 | 611 | [[package]] 612 | name = "hyper" 613 | version = "0.13.2" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "fa1c527bbc634be72aa7ba31e4e4def9bbb020f5416916279b7c705cd838893e" 616 | dependencies = [ 617 | "bytes", 618 | "futures-channel", 619 | "futures-core", 620 | "futures-util", 621 | "h2", 622 | "http", 623 | "http-body", 624 | "httparse", 625 | "itoa", 626 | "log 0.4.8", 627 | "net2", 628 | "pin-project", 629 | "time", 630 | "tokio", 631 | "tower-service", 632 | "want", 633 | ] 634 | 635 | [[package]] 636 | name = "hyper-tls" 637 | version = "0.4.1" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "3adcd308402b9553630734e9c36b77a7e48b3821251ca2493e8cd596763aafaa" 640 | dependencies = [ 641 | "bytes", 642 | "hyper", 643 | "native-tls", 644 | "tokio", 645 | "tokio-tls", 646 | ] 647 | 648 | [[package]] 649 | name = "idna" 650 | version = "0.2.0" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" 653 | dependencies = [ 654 | "matches", 655 | "unicode-bidi", 656 | "unicode-normalization", 657 | ] 658 | 659 | [[package]] 660 | name = "indexmap" 661 | version = "1.3.2" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" 664 | dependencies = [ 665 | "autocfg 1.0.0", 666 | ] 667 | 668 | [[package]] 669 | name = "iovec" 670 | version = "0.1.4" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" 673 | dependencies = [ 674 | "libc", 675 | ] 676 | 677 | [[package]] 678 | name = "itertools" 679 | version = "0.8.2" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" 682 | dependencies = [ 683 | "either", 684 | ] 685 | 686 | [[package]] 687 | name = "itoa" 688 | version = "0.4.5" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" 691 | 692 | [[package]] 693 | name = "js-sys" 694 | version = "0.3.35" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "7889c7c36282151f6bf465be4700359318aef36baa951462382eae49e9577cf9" 697 | dependencies = [ 698 | "wasm-bindgen", 699 | ] 700 | 701 | [[package]] 702 | name = "kernel32-sys" 703 | version = "0.2.2" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 706 | dependencies = [ 707 | "winapi 0.2.8", 708 | "winapi-build", 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.67" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" 722 | 723 | [[package]] 724 | name = "log" 725 | version = "0.3.9" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" 728 | dependencies = [ 729 | "log 0.4.8", 730 | ] 731 | 732 | [[package]] 733 | name = "log" 734 | version = "0.4.8" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 737 | dependencies = [ 738 | "cfg-if", 739 | ] 740 | 741 | [[package]] 742 | name = "mac" 743 | version = "0.1.1" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 746 | 747 | [[package]] 748 | name = "markup5ever" 749 | version = "0.9.0" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "65381d9d47506b8592b97c4efd936afcf673b09b059f2bef39c7211ee78b9d03" 752 | dependencies = [ 753 | "log 0.4.8", 754 | "phf", 755 | "phf_codegen", 756 | "serde", 757 | "serde_derive", 758 | "serde_json", 759 | "string_cache", 760 | "string_cache_codegen", 761 | "tendril", 762 | ] 763 | 764 | [[package]] 765 | name = "matches" 766 | version = "0.1.8" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 769 | 770 | [[package]] 771 | name = "maybe-uninit" 772 | version = "2.0.0" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 775 | 776 | [[package]] 777 | name = "memchr" 778 | version = "2.3.3" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 781 | 782 | [[package]] 783 | name = "mime" 784 | version = "0.3.16" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 787 | 788 | [[package]] 789 | name = "mime_guess" 790 | version = "2.0.1" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "1a0ed03949aef72dbdf3116a383d7b38b4768e6f960528cd6a6044aa9ed68599" 793 | dependencies = [ 794 | "mime", 795 | "unicase", 796 | ] 797 | 798 | [[package]] 799 | name = "mio" 800 | version = "0.6.21" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f" 803 | dependencies = [ 804 | "cfg-if", 805 | "fuchsia-zircon", 806 | "fuchsia-zircon-sys", 807 | "iovec", 808 | "kernel32-sys", 809 | "libc", 810 | "log 0.4.8", 811 | "miow", 812 | "net2", 813 | "slab", 814 | "winapi 0.2.8", 815 | ] 816 | 817 | [[package]] 818 | name = "miow" 819 | version = "0.2.1" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" 822 | dependencies = [ 823 | "kernel32-sys", 824 | "net2", 825 | "winapi 0.2.8", 826 | "ws2_32-sys", 827 | ] 828 | 829 | [[package]] 830 | name = "mpv" 831 | version = "0.2.3" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "9e57fd944655bbef6aaab8a154b0f78ed55aaaaf211edf956330c56309236f82" 834 | dependencies = [ 835 | "enum_primitive", 836 | "log 0.3.9", 837 | "num", 838 | ] 839 | 840 | [[package]] 841 | name = "native-tls" 842 | version = "0.2.3" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "4b2df1a4c22fd44a62147fd8f13dd0f95c9d8ca7b2610299b2a2f9cf8964274e" 845 | dependencies = [ 846 | "lazy_static", 847 | "libc", 848 | "log 0.4.8", 849 | "openssl", 850 | "openssl-probe", 851 | "openssl-sys", 852 | "schannel", 853 | "security-framework", 854 | "security-framework-sys", 855 | "tempfile", 856 | ] 857 | 858 | [[package]] 859 | name = "net2" 860 | version = "0.2.33" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" 863 | dependencies = [ 864 | "cfg-if", 865 | "libc", 866 | "winapi 0.3.8", 867 | ] 868 | 869 | [[package]] 870 | name = "new_debug_unreachable" 871 | version = "1.0.4" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" 874 | 875 | [[package]] 876 | name = "nodrop" 877 | version = "0.1.14" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" 880 | 881 | [[package]] 882 | name = "nom" 883 | version = "4.2.3" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" 886 | dependencies = [ 887 | "memchr", 888 | "version_check 0.1.5", 889 | ] 890 | 891 | [[package]] 892 | name = "num" 893 | version = "0.1.42" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" 896 | dependencies = [ 897 | "num-bigint", 898 | "num-complex", 899 | "num-integer", 900 | "num-iter", 901 | "num-rational", 902 | "num-traits 0.2.11", 903 | ] 904 | 905 | [[package]] 906 | name = "num-bigint" 907 | version = "0.1.44" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1" 910 | dependencies = [ 911 | "num-integer", 912 | "num-traits 0.2.11", 913 | "rand 0.4.6", 914 | "rustc-serialize", 915 | ] 916 | 917 | [[package]] 918 | name = "num-complex" 919 | version = "0.1.43" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656" 922 | dependencies = [ 923 | "num-traits 0.2.11", 924 | "rustc-serialize", 925 | ] 926 | 927 | [[package]] 928 | name = "num-integer" 929 | version = "0.1.42" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" 932 | dependencies = [ 933 | "autocfg 1.0.0", 934 | "num-traits 0.2.11", 935 | ] 936 | 937 | [[package]] 938 | name = "num-iter" 939 | version = "0.1.40" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00" 942 | dependencies = [ 943 | "autocfg 1.0.0", 944 | "num-integer", 945 | "num-traits 0.2.11", 946 | ] 947 | 948 | [[package]] 949 | name = "num-rational" 950 | version = "0.1.42" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" 953 | dependencies = [ 954 | "num-bigint", 955 | "num-integer", 956 | "num-traits 0.2.11", 957 | "rustc-serialize", 958 | ] 959 | 960 | [[package]] 961 | name = "num-traits" 962 | version = "0.1.43" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" 965 | dependencies = [ 966 | "num-traits 0.2.11", 967 | ] 968 | 969 | [[package]] 970 | name = "num-traits" 971 | version = "0.2.11" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" 974 | dependencies = [ 975 | "autocfg 1.0.0", 976 | ] 977 | 978 | [[package]] 979 | name = "num_cpus" 980 | version = "1.12.0" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6" 983 | dependencies = [ 984 | "hermit-abi", 985 | "libc", 986 | ] 987 | 988 | [[package]] 989 | name = "numtoa" 990 | version = "0.1.0" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" 993 | 994 | [[package]] 995 | name = "openssl" 996 | version = "0.10.28" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "973293749822d7dd6370d6da1e523b0d1db19f06c459134c658b2a4261378b52" 999 | dependencies = [ 1000 | "bitflags", 1001 | "cfg-if", 1002 | "foreign-types", 1003 | "lazy_static", 1004 | "libc", 1005 | "openssl-sys", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "openssl-probe" 1010 | version = "0.1.2" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" 1013 | 1014 | [[package]] 1015 | name = "openssl-sys" 1016 | version = "0.9.54" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "1024c0a59774200a555087a6da3f253a9095a5f344e353b212ac4c8b8e450986" 1019 | dependencies = [ 1020 | "autocfg 1.0.0", 1021 | "cc", 1022 | "libc", 1023 | "pkg-config", 1024 | "vcpkg", 1025 | ] 1026 | 1027 | [[package]] 1028 | name = "percent-encoding" 1029 | version = "2.1.0" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 1032 | 1033 | [[package]] 1034 | name = "phf" 1035 | version = "0.7.24" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" 1038 | dependencies = [ 1039 | "phf_shared", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "phf_codegen" 1044 | version = "0.7.24" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" 1047 | dependencies = [ 1048 | "phf_generator", 1049 | "phf_shared", 1050 | ] 1051 | 1052 | [[package]] 1053 | name = "phf_generator" 1054 | version = "0.7.24" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" 1057 | dependencies = [ 1058 | "phf_shared", 1059 | "rand 0.6.5", 1060 | ] 1061 | 1062 | [[package]] 1063 | name = "phf_shared" 1064 | version = "0.7.24" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" 1067 | dependencies = [ 1068 | "siphasher", 1069 | ] 1070 | 1071 | [[package]] 1072 | name = "pin-project" 1073 | version = "0.4.8" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "7804a463a8d9572f13453c516a5faea534a2403d7ced2f0c7e100eeff072772c" 1076 | dependencies = [ 1077 | "pin-project-internal", 1078 | ] 1079 | 1080 | [[package]] 1081 | name = "pin-project-internal" 1082 | version = "0.4.8" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "385322a45f2ecf3410c68d2a549a4a2685e8051d0f278e39743ff4e451cb9b3f" 1085 | dependencies = [ 1086 | "proc-macro2 1.0.8", 1087 | "quote 1.0.2", 1088 | "syn 1.0.15", 1089 | ] 1090 | 1091 | [[package]] 1092 | name = "pin-project-lite" 1093 | version = "0.1.4" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae" 1096 | 1097 | [[package]] 1098 | name = "pin-utils" 1099 | version = "0.1.0-alpha.4" 1100 | source = "registry+https://github.com/rust-lang/crates.io-index" 1101 | checksum = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" 1102 | 1103 | [[package]] 1104 | name = "pkg-config" 1105 | version = "0.3.17" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" 1108 | 1109 | [[package]] 1110 | name = "ppv-lite86" 1111 | version = "0.2.6" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" 1114 | 1115 | [[package]] 1116 | name = "precomputed-hash" 1117 | version = "0.1.1" 1118 | source = "registry+https://github.com/rust-lang/crates.io-index" 1119 | checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 1120 | 1121 | [[package]] 1122 | name = "proc-macro-hack" 1123 | version = "0.5.11" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5" 1126 | dependencies = [ 1127 | "proc-macro2 1.0.8", 1128 | "quote 1.0.2", 1129 | "syn 1.0.15", 1130 | ] 1131 | 1132 | [[package]] 1133 | name = "proc-macro-nested" 1134 | version = "0.1.3" 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" 1136 | checksum = "369a6ed065f249a159e06c45752c780bda2fb53c995718f9e484d08daa9eb42e" 1137 | 1138 | [[package]] 1139 | name = "proc-macro2" 1140 | version = "0.4.30" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" 1143 | dependencies = [ 1144 | "unicode-xid 0.1.0", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "proc-macro2" 1149 | version = "1.0.8" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "3acb317c6ff86a4e579dfa00fc5e6cca91ecbb4e7eb2df0468805b674eb88548" 1152 | dependencies = [ 1153 | "unicode-xid 0.2.0", 1154 | ] 1155 | 1156 | [[package]] 1157 | name = "procedural-masquerade" 1158 | version = "0.1.6" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "9a1574a51c3fd37b26d2c0032b649d08a7d51d4cca9c41bbc5bf7118fa4509d0" 1161 | 1162 | [[package]] 1163 | name = "quickcheck" 1164 | version = "0.9.2" 1165 | source = "registry+https://github.com/rust-lang/crates.io-index" 1166 | checksum = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" 1167 | dependencies = [ 1168 | "env_logger", 1169 | "log 0.4.8", 1170 | "rand 0.7.3", 1171 | "rand_core 0.5.1", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "quickcheck_macros" 1176 | version = "0.8.0" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "d7dfc1c4a1e048f5cc7d36a4c4118dfcf31d217c79f4b9a61bad65d68185752c" 1179 | dependencies = [ 1180 | "proc-macro2 0.4.30", 1181 | "quote 0.6.13", 1182 | "syn 0.15.44", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "quote" 1187 | version = "0.6.13" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" 1190 | dependencies = [ 1191 | "proc-macro2 0.4.30", 1192 | ] 1193 | 1194 | [[package]] 1195 | name = "quote" 1196 | version = "1.0.2" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" 1199 | dependencies = [ 1200 | "proc-macro2 1.0.8", 1201 | ] 1202 | 1203 | [[package]] 1204 | name = "rand" 1205 | version = "0.4.6" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 1208 | dependencies = [ 1209 | "fuchsia-cprng", 1210 | "libc", 1211 | "rand_core 0.3.1", 1212 | "rdrand", 1213 | "winapi 0.3.8", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "rand" 1218 | version = "0.6.5" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" 1221 | dependencies = [ 1222 | "autocfg 0.1.7", 1223 | "libc", 1224 | "rand_chacha 0.1.1", 1225 | "rand_core 0.4.2", 1226 | "rand_hc 0.1.0", 1227 | "rand_isaac", 1228 | "rand_jitter", 1229 | "rand_os", 1230 | "rand_pcg", 1231 | "rand_xorshift", 1232 | "winapi 0.3.8", 1233 | ] 1234 | 1235 | [[package]] 1236 | name = "rand" 1237 | version = "0.7.3" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 1240 | dependencies = [ 1241 | "getrandom", 1242 | "libc", 1243 | "rand_chacha 0.2.1", 1244 | "rand_core 0.5.1", 1245 | "rand_hc 0.2.0", 1246 | ] 1247 | 1248 | [[package]] 1249 | name = "rand_chacha" 1250 | version = "0.1.1" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" 1253 | dependencies = [ 1254 | "autocfg 0.1.7", 1255 | "rand_core 0.3.1", 1256 | ] 1257 | 1258 | [[package]] 1259 | name = "rand_chacha" 1260 | version = "0.2.1" 1261 | source = "registry+https://github.com/rust-lang/crates.io-index" 1262 | checksum = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" 1263 | dependencies = [ 1264 | "c2-chacha", 1265 | "rand_core 0.5.1", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "rand_core" 1270 | version = "0.3.1" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 1273 | dependencies = [ 1274 | "rand_core 0.4.2", 1275 | ] 1276 | 1277 | [[package]] 1278 | name = "rand_core" 1279 | version = "0.4.2" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 1282 | 1283 | [[package]] 1284 | name = "rand_core" 1285 | version = "0.5.1" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 1288 | dependencies = [ 1289 | "getrandom", 1290 | ] 1291 | 1292 | [[package]] 1293 | name = "rand_hc" 1294 | version = "0.1.0" 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" 1296 | checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" 1297 | dependencies = [ 1298 | "rand_core 0.3.1", 1299 | ] 1300 | 1301 | [[package]] 1302 | name = "rand_hc" 1303 | version = "0.2.0" 1304 | source = "registry+https://github.com/rust-lang/crates.io-index" 1305 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 1306 | dependencies = [ 1307 | "rand_core 0.5.1", 1308 | ] 1309 | 1310 | [[package]] 1311 | name = "rand_isaac" 1312 | version = "0.1.1" 1313 | source = "registry+https://github.com/rust-lang/crates.io-index" 1314 | checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" 1315 | dependencies = [ 1316 | "rand_core 0.3.1", 1317 | ] 1318 | 1319 | [[package]] 1320 | name = "rand_jitter" 1321 | version = "0.1.4" 1322 | source = "registry+https://github.com/rust-lang/crates.io-index" 1323 | checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" 1324 | dependencies = [ 1325 | "libc", 1326 | "rand_core 0.4.2", 1327 | "winapi 0.3.8", 1328 | ] 1329 | 1330 | [[package]] 1331 | name = "rand_os" 1332 | version = "0.1.3" 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" 1334 | checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" 1335 | dependencies = [ 1336 | "cloudabi", 1337 | "fuchsia-cprng", 1338 | "libc", 1339 | "rand_core 0.4.2", 1340 | "rdrand", 1341 | "winapi 0.3.8", 1342 | ] 1343 | 1344 | [[package]] 1345 | name = "rand_pcg" 1346 | version = "0.1.2" 1347 | source = "registry+https://github.com/rust-lang/crates.io-index" 1348 | checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" 1349 | dependencies = [ 1350 | "autocfg 0.1.7", 1351 | "rand_core 0.4.2", 1352 | ] 1353 | 1354 | [[package]] 1355 | name = "rand_xorshift" 1356 | version = "0.1.1" 1357 | source = "registry+https://github.com/rust-lang/crates.io-index" 1358 | checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" 1359 | dependencies = [ 1360 | "rand_core 0.3.1", 1361 | ] 1362 | 1363 | [[package]] 1364 | name = "rdrand" 1365 | version = "0.4.0" 1366 | source = "registry+https://github.com/rust-lang/crates.io-index" 1367 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 1368 | dependencies = [ 1369 | "rand_core 0.3.1", 1370 | ] 1371 | 1372 | [[package]] 1373 | name = "redox_syscall" 1374 | version = "0.1.56" 1375 | source = "registry+https://github.com/rust-lang/crates.io-index" 1376 | checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 1377 | 1378 | [[package]] 1379 | name = "redox_termios" 1380 | version = "0.1.1" 1381 | source = "registry+https://github.com/rust-lang/crates.io-index" 1382 | checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 1383 | dependencies = [ 1384 | "redox_syscall", 1385 | ] 1386 | 1387 | [[package]] 1388 | name = "redox_users" 1389 | version = "0.3.4" 1390 | source = "registry+https://github.com/rust-lang/crates.io-index" 1391 | checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" 1392 | dependencies = [ 1393 | "getrandom", 1394 | "redox_syscall", 1395 | "rust-argon2", 1396 | ] 1397 | 1398 | [[package]] 1399 | name = "regex" 1400 | version = "1.3.4" 1401 | source = "registry+https://github.com/rust-lang/crates.io-index" 1402 | checksum = "322cf97724bea3ee221b78fe25ac9c46114ebb51747ad5babd51a2fc6a8235a8" 1403 | dependencies = [ 1404 | "aho-corasick", 1405 | "memchr", 1406 | "regex-syntax", 1407 | "thread_local", 1408 | ] 1409 | 1410 | [[package]] 1411 | name = "regex-syntax" 1412 | version = "0.6.14" 1413 | source = "registry+https://github.com/rust-lang/crates.io-index" 1414 | checksum = "b28dfe3fe9badec5dbf0a79a9cccad2cfc2ab5484bdb3e44cbd1ae8b3ba2be06" 1415 | 1416 | [[package]] 1417 | name = "remove_dir_all" 1418 | version = "0.5.2" 1419 | source = "registry+https://github.com/rust-lang/crates.io-index" 1420 | checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" 1421 | dependencies = [ 1422 | "winapi 0.3.8", 1423 | ] 1424 | 1425 | [[package]] 1426 | name = "reqwest" 1427 | version = "0.10.2" 1428 | source = "registry+https://github.com/rust-lang/crates.io-index" 1429 | checksum = "bae3fc32eacd4a5200c6b34bd6c057b07fb64f5a1e55bb67d624cc1393354621" 1430 | dependencies = [ 1431 | "base64", 1432 | "bytes", 1433 | "encoding_rs", 1434 | "futures-core", 1435 | "futures-util", 1436 | "http", 1437 | "http-body", 1438 | "hyper", 1439 | "hyper-tls", 1440 | "js-sys", 1441 | "lazy_static", 1442 | "log 0.4.8", 1443 | "mime", 1444 | "mime_guess", 1445 | "native-tls", 1446 | "percent-encoding", 1447 | "pin-project-lite", 1448 | "serde", 1449 | "serde_json", 1450 | "serde_urlencoded", 1451 | "time", 1452 | "tokio", 1453 | "tokio-tls", 1454 | "url", 1455 | "wasm-bindgen", 1456 | "wasm-bindgen-futures", 1457 | "web-sys", 1458 | "winreg", 1459 | ] 1460 | 1461 | [[package]] 1462 | name = "rum-player" 1463 | version = "0.1.6" 1464 | dependencies = [ 1465 | "async-stream", 1466 | "derive_more", 1467 | "dirs", 1468 | "flexi_logger", 1469 | "futures", 1470 | "itertools", 1471 | "log 0.4.8", 1472 | "mpv", 1473 | "quickcheck", 1474 | "quickcheck_macros", 1475 | "reqwest", 1476 | "serde", 1477 | "serde-xml-rs", 1478 | "snafu", 1479 | "strum", 1480 | "strum_macros", 1481 | "termion", 1482 | "tokio", 1483 | "toml", 1484 | "tui", 1485 | "unhtml", 1486 | ] 1487 | 1488 | [[package]] 1489 | name = "rust-argon2" 1490 | version = "0.7.0" 1491 | source = "registry+https://github.com/rust-lang/crates.io-index" 1492 | checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" 1493 | dependencies = [ 1494 | "base64", 1495 | "blake2b_simd", 1496 | "constant_time_eq", 1497 | "crossbeam-utils", 1498 | ] 1499 | 1500 | [[package]] 1501 | name = "rustc-demangle" 1502 | version = "0.1.16" 1503 | source = "registry+https://github.com/rust-lang/crates.io-index" 1504 | checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" 1505 | 1506 | [[package]] 1507 | name = "rustc-serialize" 1508 | version = "0.3.24" 1509 | source = "registry+https://github.com/rust-lang/crates.io-index" 1510 | checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" 1511 | 1512 | [[package]] 1513 | name = "ryu" 1514 | version = "1.0.2" 1515 | source = "registry+https://github.com/rust-lang/crates.io-index" 1516 | checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" 1517 | 1518 | [[package]] 1519 | name = "schannel" 1520 | version = "0.1.17" 1521 | source = "registry+https://github.com/rust-lang/crates.io-index" 1522 | checksum = "507a9e6e8ffe0a4e0ebb9a10293e62fdf7657c06f1b8bb07a8fcf697d2abf295" 1523 | dependencies = [ 1524 | "lazy_static", 1525 | "winapi 0.3.8", 1526 | ] 1527 | 1528 | [[package]] 1529 | name = "scraper" 1530 | version = "0.11.0" 1531 | source = "registry+https://github.com/rust-lang/crates.io-index" 1532 | checksum = "77e013440e2ab1c25065551adda5a9773698bb5be3dbab06ff2deac3d5c1a793" 1533 | dependencies = [ 1534 | "cssparser", 1535 | "ego-tree", 1536 | "getopts", 1537 | "html5ever", 1538 | "matches", 1539 | "selectors", 1540 | "smallvec 0.6.13", 1541 | "tendril", 1542 | ] 1543 | 1544 | [[package]] 1545 | name = "security-framework" 1546 | version = "0.3.4" 1547 | source = "registry+https://github.com/rust-lang/crates.io-index" 1548 | checksum = "8ef2429d7cefe5fd28bd1d2ed41c944547d4ff84776f5935b456da44593a16df" 1549 | dependencies = [ 1550 | "core-foundation", 1551 | "core-foundation-sys", 1552 | "libc", 1553 | "security-framework-sys", 1554 | ] 1555 | 1556 | [[package]] 1557 | name = "security-framework-sys" 1558 | version = "0.3.3" 1559 | source = "registry+https://github.com/rust-lang/crates.io-index" 1560 | checksum = "e31493fc37615debb8c5090a7aeb4a9730bc61e77ab10b9af59f1a202284f895" 1561 | dependencies = [ 1562 | "core-foundation-sys", 1563 | ] 1564 | 1565 | [[package]] 1566 | name = "selectors" 1567 | version = "0.21.0" 1568 | source = "registry+https://github.com/rust-lang/crates.io-index" 1569 | checksum = "1b86b100bede4f651059740afc3b6cb83458d7401cb7c1ad96d8a11e91742c86" 1570 | dependencies = [ 1571 | "bitflags", 1572 | "cssparser", 1573 | "fxhash", 1574 | "log 0.4.8", 1575 | "matches", 1576 | "phf", 1577 | "phf_codegen", 1578 | "precomputed-hash", 1579 | "servo_arc", 1580 | "smallvec 0.6.13", 1581 | "thin-slice", 1582 | ] 1583 | 1584 | [[package]] 1585 | name = "serde" 1586 | version = "1.0.104" 1587 | source = "registry+https://github.com/rust-lang/crates.io-index" 1588 | checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" 1589 | dependencies = [ 1590 | "serde_derive", 1591 | ] 1592 | 1593 | [[package]] 1594 | name = "serde-xml-rs" 1595 | version = "0.3.1" 1596 | source = "registry+https://github.com/rust-lang/crates.io-index" 1597 | checksum = "27d98dfc234faa8532d66c837de56bf4276a259a43dd10ef96feb2fb7ab333b1" 1598 | dependencies = [ 1599 | "error-chain", 1600 | "log 0.4.8", 1601 | "serde", 1602 | "xml-rs", 1603 | ] 1604 | 1605 | [[package]] 1606 | name = "serde_derive" 1607 | version = "1.0.104" 1608 | source = "registry+https://github.com/rust-lang/crates.io-index" 1609 | checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" 1610 | dependencies = [ 1611 | "proc-macro2 1.0.8", 1612 | "quote 1.0.2", 1613 | "syn 1.0.15", 1614 | ] 1615 | 1616 | [[package]] 1617 | name = "serde_json" 1618 | version = "1.0.48" 1619 | source = "registry+https://github.com/rust-lang/crates.io-index" 1620 | checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25" 1621 | dependencies = [ 1622 | "itoa", 1623 | "ryu", 1624 | "serde", 1625 | ] 1626 | 1627 | [[package]] 1628 | name = "serde_urlencoded" 1629 | version = "0.6.1" 1630 | source = "registry+https://github.com/rust-lang/crates.io-index" 1631 | checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" 1632 | dependencies = [ 1633 | "dtoa", 1634 | "itoa", 1635 | "serde", 1636 | "url", 1637 | ] 1638 | 1639 | [[package]] 1640 | name = "servo_arc" 1641 | version = "0.1.1" 1642 | source = "registry+https://github.com/rust-lang/crates.io-index" 1643 | checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" 1644 | dependencies = [ 1645 | "nodrop", 1646 | "stable_deref_trait", 1647 | ] 1648 | 1649 | [[package]] 1650 | name = "siphasher" 1651 | version = "0.2.3" 1652 | source = "registry+https://github.com/rust-lang/crates.io-index" 1653 | checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" 1654 | 1655 | [[package]] 1656 | name = "slab" 1657 | version = "0.4.2" 1658 | source = "registry+https://github.com/rust-lang/crates.io-index" 1659 | checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 1660 | 1661 | [[package]] 1662 | name = "smallvec" 1663 | version = "0.6.13" 1664 | source = "registry+https://github.com/rust-lang/crates.io-index" 1665 | checksum = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6" 1666 | dependencies = [ 1667 | "maybe-uninit", 1668 | ] 1669 | 1670 | [[package]] 1671 | name = "smallvec" 1672 | version = "1.2.0" 1673 | source = "registry+https://github.com/rust-lang/crates.io-index" 1674 | checksum = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" 1675 | 1676 | [[package]] 1677 | name = "snafu" 1678 | version = "0.6.2" 1679 | source = "registry+https://github.com/rust-lang/crates.io-index" 1680 | checksum = "546db9181bce2aa22ed883c33d65603b76335b4c2533a98289f54265043de7a1" 1681 | dependencies = [ 1682 | "doc-comment", 1683 | "snafu-derive", 1684 | ] 1685 | 1686 | [[package]] 1687 | name = "snafu-derive" 1688 | version = "0.6.2" 1689 | source = "registry+https://github.com/rust-lang/crates.io-index" 1690 | checksum = "bdc75da2e0323f297402fd9c8fdba709bb04e4c627cbe31d19a2c91fc8d9f0e2" 1691 | dependencies = [ 1692 | "proc-macro2 1.0.8", 1693 | "quote 1.0.2", 1694 | "syn 1.0.15", 1695 | ] 1696 | 1697 | [[package]] 1698 | name = "sourcefile" 1699 | version = "0.1.4" 1700 | source = "registry+https://github.com/rust-lang/crates.io-index" 1701 | checksum = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3" 1702 | 1703 | [[package]] 1704 | name = "stable_deref_trait" 1705 | version = "1.1.1" 1706 | source = "registry+https://github.com/rust-lang/crates.io-index" 1707 | checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" 1708 | 1709 | [[package]] 1710 | name = "string_cache" 1711 | version = "0.7.5" 1712 | source = "registry+https://github.com/rust-lang/crates.io-index" 1713 | checksum = "89c058a82f9fd69b1becf8c274f412281038877c553182f1d02eb027045a2d67" 1714 | dependencies = [ 1715 | "lazy_static", 1716 | "new_debug_unreachable", 1717 | "phf_shared", 1718 | "precomputed-hash", 1719 | "serde", 1720 | "string_cache_codegen", 1721 | "string_cache_shared", 1722 | ] 1723 | 1724 | [[package]] 1725 | name = "string_cache_codegen" 1726 | version = "0.4.4" 1727 | source = "registry+https://github.com/rust-lang/crates.io-index" 1728 | checksum = "f0f45ed1b65bf9a4bf2f7b7dc59212d1926e9eaf00fa998988e420fd124467c6" 1729 | dependencies = [ 1730 | "phf_generator", 1731 | "phf_shared", 1732 | "proc-macro2 1.0.8", 1733 | "quote 1.0.2", 1734 | "string_cache_shared", 1735 | ] 1736 | 1737 | [[package]] 1738 | name = "string_cache_shared" 1739 | version = "0.3.0" 1740 | source = "registry+https://github.com/rust-lang/crates.io-index" 1741 | checksum = "b1884d1bc09741d466d9b14e6d37ac89d6909cbcac41dd9ae982d4d063bbedfc" 1742 | 1743 | [[package]] 1744 | name = "strum" 1745 | version = "0.16.0" 1746 | source = "registry+https://github.com/rust-lang/crates.io-index" 1747 | checksum = "6138f8f88a16d90134763314e3fc76fa3ed6a7db4725d6acf9a3ef95a3188d22" 1748 | 1749 | [[package]] 1750 | name = "strum_macros" 1751 | version = "0.16.0" 1752 | source = "registry+https://github.com/rust-lang/crates.io-index" 1753 | checksum = "0054a7df764039a6cd8592b9de84be4bec368ff081d203a7d5371cbfa8e65c81" 1754 | dependencies = [ 1755 | "heck", 1756 | "proc-macro2 1.0.8", 1757 | "quote 1.0.2", 1758 | "syn 1.0.15", 1759 | ] 1760 | 1761 | [[package]] 1762 | name = "syn" 1763 | version = "0.15.44" 1764 | source = "registry+https://github.com/rust-lang/crates.io-index" 1765 | checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" 1766 | dependencies = [ 1767 | "proc-macro2 0.4.30", 1768 | "quote 0.6.13", 1769 | "unicode-xid 0.1.0", 1770 | ] 1771 | 1772 | [[package]] 1773 | name = "syn" 1774 | version = "1.0.15" 1775 | source = "registry+https://github.com/rust-lang/crates.io-index" 1776 | checksum = "7a0294dc449adc58bb6592fff1a23d3e5e6e235afc6a0ffca2657d19e7bbffe5" 1777 | dependencies = [ 1778 | "proc-macro2 1.0.8", 1779 | "quote 1.0.2", 1780 | "unicode-xid 0.2.0", 1781 | ] 1782 | 1783 | [[package]] 1784 | name = "tempfile" 1785 | version = "3.1.0" 1786 | source = "registry+https://github.com/rust-lang/crates.io-index" 1787 | checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" 1788 | dependencies = [ 1789 | "cfg-if", 1790 | "libc", 1791 | "rand 0.7.3", 1792 | "redox_syscall", 1793 | "remove_dir_all", 1794 | "winapi 0.3.8", 1795 | ] 1796 | 1797 | [[package]] 1798 | name = "tendril" 1799 | version = "0.4.1" 1800 | source = "registry+https://github.com/rust-lang/crates.io-index" 1801 | checksum = "707feda9f2582d5d680d733e38755547a3e8fb471e7ba11452ecfd9ce93a5d3b" 1802 | dependencies = [ 1803 | "futf", 1804 | "mac", 1805 | "utf-8", 1806 | ] 1807 | 1808 | [[package]] 1809 | name = "termion" 1810 | version = "1.5.5" 1811 | source = "registry+https://github.com/rust-lang/crates.io-index" 1812 | checksum = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905" 1813 | dependencies = [ 1814 | "libc", 1815 | "numtoa", 1816 | "redox_syscall", 1817 | "redox_termios", 1818 | ] 1819 | 1820 | [[package]] 1821 | name = "thin-slice" 1822 | version = "0.1.1" 1823 | source = "registry+https://github.com/rust-lang/crates.io-index" 1824 | checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" 1825 | 1826 | [[package]] 1827 | name = "thread_local" 1828 | version = "1.0.1" 1829 | source = "registry+https://github.com/rust-lang/crates.io-index" 1830 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 1831 | dependencies = [ 1832 | "lazy_static", 1833 | ] 1834 | 1835 | [[package]] 1836 | name = "time" 1837 | version = "0.1.42" 1838 | source = "registry+https://github.com/rust-lang/crates.io-index" 1839 | checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" 1840 | dependencies = [ 1841 | "libc", 1842 | "redox_syscall", 1843 | "winapi 0.3.8", 1844 | ] 1845 | 1846 | [[package]] 1847 | name = "tokio" 1848 | version = "0.2.11" 1849 | source = "registry+https://github.com/rust-lang/crates.io-index" 1850 | checksum = "8fdd17989496f49cdc57978c96f0c9fe5e4a58a8bddc6813c449a4624f6a030b" 1851 | dependencies = [ 1852 | "bytes", 1853 | "fnv", 1854 | "futures-core", 1855 | "iovec", 1856 | "lazy_static", 1857 | "memchr", 1858 | "mio", 1859 | "num_cpus", 1860 | "pin-project-lite", 1861 | "slab", 1862 | "tokio-macros", 1863 | ] 1864 | 1865 | [[package]] 1866 | name = "tokio-macros" 1867 | version = "0.2.4" 1868 | source = "registry+https://github.com/rust-lang/crates.io-index" 1869 | checksum = "f4b1e7ed7d5d4c2af3d999904b0eebe76544897cdbfb2b9684bed2174ab20f7c" 1870 | dependencies = [ 1871 | "proc-macro2 1.0.8", 1872 | "quote 1.0.2", 1873 | "syn 1.0.15", 1874 | ] 1875 | 1876 | [[package]] 1877 | name = "tokio-tls" 1878 | version = "0.3.0" 1879 | source = "registry+https://github.com/rust-lang/crates.io-index" 1880 | checksum = "7bde02a3a5291395f59b06ec6945a3077602fac2b07eeeaf0dee2122f3619828" 1881 | dependencies = [ 1882 | "native-tls", 1883 | "tokio", 1884 | ] 1885 | 1886 | [[package]] 1887 | name = "tokio-util" 1888 | version = "0.2.0" 1889 | source = "registry+https://github.com/rust-lang/crates.io-index" 1890 | checksum = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930" 1891 | dependencies = [ 1892 | "bytes", 1893 | "futures-core", 1894 | "futures-sink", 1895 | "log 0.4.8", 1896 | "pin-project-lite", 1897 | "tokio", 1898 | ] 1899 | 1900 | [[package]] 1901 | name = "toml" 1902 | version = "0.5.6" 1903 | source = "registry+https://github.com/rust-lang/crates.io-index" 1904 | checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" 1905 | dependencies = [ 1906 | "serde", 1907 | ] 1908 | 1909 | [[package]] 1910 | name = "tower-service" 1911 | version = "0.3.0" 1912 | source = "registry+https://github.com/rust-lang/crates.io-index" 1913 | checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" 1914 | 1915 | [[package]] 1916 | name = "try-lock" 1917 | version = "0.2.2" 1918 | source = "registry+https://github.com/rust-lang/crates.io-index" 1919 | checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" 1920 | 1921 | [[package]] 1922 | name = "tui" 1923 | version = "0.8.0" 1924 | source = "registry+https://github.com/rust-lang/crates.io-index" 1925 | checksum = "6b871b61f4c4b81e630215cd12e0ec29953d4545898e21a9e023b7520a74a9f9" 1926 | dependencies = [ 1927 | "bitflags", 1928 | "cassowary", 1929 | "either", 1930 | "itertools", 1931 | "log 0.4.8", 1932 | "termion", 1933 | "unicode-segmentation", 1934 | "unicode-width", 1935 | ] 1936 | 1937 | [[package]] 1938 | name = "unhtml" 1939 | version = "0.7.5" 1940 | source = "registry+https://github.com/rust-lang/crates.io-index" 1941 | checksum = "b83979ddfa44f5acffa870121fa52fd92e18a1716d5f5ca82658ea776af0b5f0" 1942 | dependencies = [ 1943 | "derive_more", 1944 | "scraper", 1945 | "unhtml_derive", 1946 | ] 1947 | 1948 | [[package]] 1949 | name = "unhtml_derive" 1950 | version = "0.7.5" 1951 | source = "registry+https://github.com/rust-lang/crates.io-index" 1952 | checksum = "dfee30025292ae12895a65dcabc2e449a1980af744d27e2d49b711e36ba427f0" 1953 | dependencies = [ 1954 | "proc-macro2 1.0.8", 1955 | "quote 1.0.2", 1956 | "scraper", 1957 | "syn 1.0.15", 1958 | ] 1959 | 1960 | [[package]] 1961 | name = "unicase" 1962 | version = "2.6.0" 1963 | source = "registry+https://github.com/rust-lang/crates.io-index" 1964 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 1965 | dependencies = [ 1966 | "version_check 0.9.1", 1967 | ] 1968 | 1969 | [[package]] 1970 | name = "unicode-bidi" 1971 | version = "0.3.4" 1972 | source = "registry+https://github.com/rust-lang/crates.io-index" 1973 | checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" 1974 | dependencies = [ 1975 | "matches", 1976 | ] 1977 | 1978 | [[package]] 1979 | name = "unicode-normalization" 1980 | version = "0.1.12" 1981 | source = "registry+https://github.com/rust-lang/crates.io-index" 1982 | checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" 1983 | dependencies = [ 1984 | "smallvec 1.2.0", 1985 | ] 1986 | 1987 | [[package]] 1988 | name = "unicode-segmentation" 1989 | version = "1.6.0" 1990 | source = "registry+https://github.com/rust-lang/crates.io-index" 1991 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 1992 | 1993 | [[package]] 1994 | name = "unicode-width" 1995 | version = "0.1.7" 1996 | source = "registry+https://github.com/rust-lang/crates.io-index" 1997 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 1998 | 1999 | [[package]] 2000 | name = "unicode-xid" 2001 | version = "0.1.0" 2002 | source = "registry+https://github.com/rust-lang/crates.io-index" 2003 | checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 2004 | 2005 | [[package]] 2006 | name = "unicode-xid" 2007 | version = "0.2.0" 2008 | source = "registry+https://github.com/rust-lang/crates.io-index" 2009 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 2010 | 2011 | [[package]] 2012 | name = "url" 2013 | version = "2.1.1" 2014 | source = "registry+https://github.com/rust-lang/crates.io-index" 2015 | checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" 2016 | dependencies = [ 2017 | "idna", 2018 | "matches", 2019 | "percent-encoding", 2020 | ] 2021 | 2022 | [[package]] 2023 | name = "utf-8" 2024 | version = "0.7.5" 2025 | source = "registry+https://github.com/rust-lang/crates.io-index" 2026 | checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" 2027 | 2028 | [[package]] 2029 | name = "vcpkg" 2030 | version = "0.2.8" 2031 | source = "registry+https://github.com/rust-lang/crates.io-index" 2032 | checksum = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" 2033 | 2034 | [[package]] 2035 | name = "version_check" 2036 | version = "0.1.5" 2037 | source = "registry+https://github.com/rust-lang/crates.io-index" 2038 | checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 2039 | 2040 | [[package]] 2041 | name = "version_check" 2042 | version = "0.9.1" 2043 | source = "registry+https://github.com/rust-lang/crates.io-index" 2044 | checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" 2045 | 2046 | [[package]] 2047 | name = "want" 2048 | version = "0.3.0" 2049 | source = "registry+https://github.com/rust-lang/crates.io-index" 2050 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 2051 | dependencies = [ 2052 | "log 0.4.8", 2053 | "try-lock", 2054 | ] 2055 | 2056 | [[package]] 2057 | name = "wasi" 2058 | version = "0.9.0+wasi-snapshot-preview1" 2059 | source = "registry+https://github.com/rust-lang/crates.io-index" 2060 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 2061 | 2062 | [[package]] 2063 | name = "wasm-bindgen" 2064 | version = "0.2.58" 2065 | source = "registry+https://github.com/rust-lang/crates.io-index" 2066 | checksum = "5205e9afdf42282b192e2310a5b463a6d1c1d774e30dc3c791ac37ab42d2616c" 2067 | dependencies = [ 2068 | "cfg-if", 2069 | "serde", 2070 | "serde_json", 2071 | "wasm-bindgen-macro", 2072 | ] 2073 | 2074 | [[package]] 2075 | name = "wasm-bindgen-backend" 2076 | version = "0.2.58" 2077 | source = "registry+https://github.com/rust-lang/crates.io-index" 2078 | checksum = "11cdb95816290b525b32587d76419facd99662a07e59d3cdb560488a819d9a45" 2079 | dependencies = [ 2080 | "bumpalo", 2081 | "lazy_static", 2082 | "log 0.4.8", 2083 | "proc-macro2 1.0.8", 2084 | "quote 1.0.2", 2085 | "syn 1.0.15", 2086 | "wasm-bindgen-shared", 2087 | ] 2088 | 2089 | [[package]] 2090 | name = "wasm-bindgen-futures" 2091 | version = "0.4.8" 2092 | source = "registry+https://github.com/rust-lang/crates.io-index" 2093 | checksum = "8bbdd49e3e28b40dec6a9ba8d17798245ce32b019513a845369c641b275135d9" 2094 | dependencies = [ 2095 | "cfg-if", 2096 | "js-sys", 2097 | "wasm-bindgen", 2098 | "web-sys", 2099 | ] 2100 | 2101 | [[package]] 2102 | name = "wasm-bindgen-macro" 2103 | version = "0.2.58" 2104 | source = "registry+https://github.com/rust-lang/crates.io-index" 2105 | checksum = "574094772ce6921576fb6f2e3f7497b8a76273b6db092be18fc48a082de09dc3" 2106 | dependencies = [ 2107 | "quote 1.0.2", 2108 | "wasm-bindgen-macro-support", 2109 | ] 2110 | 2111 | [[package]] 2112 | name = "wasm-bindgen-macro-support" 2113 | version = "0.2.58" 2114 | source = "registry+https://github.com/rust-lang/crates.io-index" 2115 | checksum = "e85031354f25eaebe78bb7db1c3d86140312a911a106b2e29f9cc440ce3e7668" 2116 | dependencies = [ 2117 | "proc-macro2 1.0.8", 2118 | "quote 1.0.2", 2119 | "syn 1.0.15", 2120 | "wasm-bindgen-backend", 2121 | "wasm-bindgen-shared", 2122 | ] 2123 | 2124 | [[package]] 2125 | name = "wasm-bindgen-shared" 2126 | version = "0.2.58" 2127 | source = "registry+https://github.com/rust-lang/crates.io-index" 2128 | checksum = "f5e7e61fc929f4c0dddb748b102ebf9f632e2b8d739f2016542b4de2965a9601" 2129 | 2130 | [[package]] 2131 | name = "wasm-bindgen-webidl" 2132 | version = "0.2.58" 2133 | source = "registry+https://github.com/rust-lang/crates.io-index" 2134 | checksum = "ef012a0d93fc0432df126a8eaf547b2dce25a8ce9212e1d3cbeef5c11157975d" 2135 | dependencies = [ 2136 | "anyhow", 2137 | "heck", 2138 | "log 0.4.8", 2139 | "proc-macro2 1.0.8", 2140 | "quote 1.0.2", 2141 | "syn 1.0.15", 2142 | "wasm-bindgen-backend", 2143 | "weedle", 2144 | ] 2145 | 2146 | [[package]] 2147 | name = "web-sys" 2148 | version = "0.3.35" 2149 | source = "registry+https://github.com/rust-lang/crates.io-index" 2150 | checksum = "aaf97caf6aa8c2b1dac90faf0db529d9d63c93846cca4911856f78a83cebf53b" 2151 | dependencies = [ 2152 | "anyhow", 2153 | "js-sys", 2154 | "sourcefile", 2155 | "wasm-bindgen", 2156 | "wasm-bindgen-webidl", 2157 | ] 2158 | 2159 | [[package]] 2160 | name = "weedle" 2161 | version = "0.10.0" 2162 | source = "registry+https://github.com/rust-lang/crates.io-index" 2163 | checksum = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164" 2164 | dependencies = [ 2165 | "nom", 2166 | ] 2167 | 2168 | [[package]] 2169 | name = "winapi" 2170 | version = "0.2.8" 2171 | source = "registry+https://github.com/rust-lang/crates.io-index" 2172 | checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 2173 | 2174 | [[package]] 2175 | name = "winapi" 2176 | version = "0.3.8" 2177 | source = "registry+https://github.com/rust-lang/crates.io-index" 2178 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 2179 | dependencies = [ 2180 | "winapi-i686-pc-windows-gnu", 2181 | "winapi-x86_64-pc-windows-gnu", 2182 | ] 2183 | 2184 | [[package]] 2185 | name = "winapi-build" 2186 | version = "0.1.1" 2187 | source = "registry+https://github.com/rust-lang/crates.io-index" 2188 | checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 2189 | 2190 | [[package]] 2191 | name = "winapi-i686-pc-windows-gnu" 2192 | version = "0.4.0" 2193 | source = "registry+https://github.com/rust-lang/crates.io-index" 2194 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2195 | 2196 | [[package]] 2197 | name = "winapi-x86_64-pc-windows-gnu" 2198 | version = "0.4.0" 2199 | source = "registry+https://github.com/rust-lang/crates.io-index" 2200 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2201 | 2202 | [[package]] 2203 | name = "winreg" 2204 | version = "0.6.2" 2205 | source = "registry+https://github.com/rust-lang/crates.io-index" 2206 | checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" 2207 | dependencies = [ 2208 | "winapi 0.3.8", 2209 | ] 2210 | 2211 | [[package]] 2212 | name = "ws2_32-sys" 2213 | version = "0.2.1" 2214 | source = "registry+https://github.com/rust-lang/crates.io-index" 2215 | checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" 2216 | dependencies = [ 2217 | "winapi 0.2.8", 2218 | "winapi-build", 2219 | ] 2220 | 2221 | [[package]] 2222 | name = "xml-rs" 2223 | version = "0.8.0" 2224 | source = "registry+https://github.com/rust-lang/crates.io-index" 2225 | checksum = "541b12c998c5b56aa2b4e6f18f03664eef9a4fd0a246a55594efae6cc2d964b5" 2226 | 2227 | [[package]] 2228 | name = "yansi" 2229 | version = "0.5.0" 2230 | source = "registry+https://github.com/rust-lang/crates.io-index" 2231 | checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" 2232 | --------------------------------------------------------------------------------