├── .gitignore ├── .github └── workflows │ └── rust.yml ├── src ├── clispam-main.rs ├── main.rs ├── data.rs ├── cairorender.rs ├── statefile-main.rs ├── statefile │ └── mod.rs ├── wlroots-main.rs ├── x11-main.rs ├── rpc-main.rs ├── core.rs ├── macros.rs ├── gamescope-main.rs └── cosmic-main.rs ├── README.md ├── Cargo.toml └── .vscode └── launch.json /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-22.04 16 | 17 | steps: 18 | - run: sudo apt-get update -y 19 | - run: sudo apt-get install -y libgtk-3-dev libglib2.0-dev libgraphene-1.0-dev git xvfb curl libcairo-gobject2 libcairo2-dev libxdo-dev libwebkit2gtk-4.0-dev libgtk-layer-shell-dev 20 | - uses: actions/checkout@v3 21 | - name: Build 22 | run: cargo build --verbose 23 | - name: Run tests 24 | run: cargo test --verbose 25 | -------------------------------------------------------------------------------- /src/clispam-main.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | extern crate serde_json; 3 | use futures::lock::Mutex; 4 | use futures::stream::StreamExt; 5 | use std::sync::Arc; 6 | 7 | mod core; 8 | mod data; 9 | mod macros; 10 | 11 | use crate::data::ConnState; 12 | 13 | #[tokio::main] 14 | async fn main() { 15 | // Websocket events to main thread 16 | let (event_sender, event_recv) = futures::channel::mpsc::channel::(10); 17 | let event_sender = Arc::new(Mutex::new(event_sender)); 18 | let event_recv = Arc::new(Mutex::new(event_recv)); 19 | 20 | // Main thread messages to Websocket output 21 | let (msg_sender, msg_recv) = futures::channel::mpsc::channel::(10); 22 | let _msg_sender = Arc::new(Mutex::new(msg_sender)); 23 | let msg_recv = Arc::new(Mutex::new(msg_recv)); 24 | 25 | // Start a thread for connection 26 | let connector_event_sender = event_sender.clone(); 27 | let connector_msg_recv = msg_recv.clone(); 28 | core::connector(connector_event_sender.clone(), connector_msg_recv.clone()).await; 29 | 30 | // Start our own loop - just print it 31 | let mut state = ConnState::new(); 32 | loop { 33 | while let Some(event) = event_recv.lock().await.next().await { 34 | state.replace_self(event); 35 | println!("{:?}", state); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::Path; 3 | use std::process::Command; 4 | 5 | fn run_command(working_directory: &Path, executable_name: &str) { 6 | let exec = working_directory.join(executable_name); 7 | if exec.exists() { 8 | let mut command = Command::new(exec.clone()); 9 | println!("{:?}", exec); 10 | command.output().expect("Failed to start"); 11 | } else { 12 | println!("Unable to start {:?}", exec); 13 | } 14 | } 15 | 16 | #[tokio::main] 17 | async fn main() { 18 | let wayland_env = env::var("WAYLAND_DISPLAY"); 19 | let x11_env = env::var("DISPLAY"); 20 | let gamescope_env = env::var("GAMESCOPE_WAYLAND_DISPLAY"); 21 | let statefile_env = env::var("DISCERN_STATEFILE"); 22 | 23 | match env::current_exe() { 24 | Ok(executable_path) => { 25 | println!("{:?}", executable_path); 26 | let working_directory = executable_path 27 | .parent() 28 | .expect("Unable to find working directory"); 29 | println!("{:?}", working_directory); 30 | if statefile_env.is_ok() { 31 | run_command(working_directory, "discern-statefile"); 32 | } else if gamescope_env.is_ok() { 33 | run_command(working_directory, "discern-gamescope"); 34 | } else if wayland_env.is_ok() { 35 | run_command(working_directory, "discern-wlr"); 36 | } else if x11_env.is_ok() { 37 | run_command(working_directory, "discern-x11"); 38 | } else { 39 | run_command(working_directory, "discern-clispam"); 40 | } 41 | } 42 | Err(e) => println!("Unable to find current executable location: {}", e), 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/data.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map::DefaultHasher; 2 | use std::collections::hash_map::HashMap; 3 | use std::hash::{Hash, Hasher}; 4 | 5 | #[derive(Debug, Clone, Hash)] 6 | pub struct DiscordUserData { 7 | pub avatar: Option, 8 | pub id: String, 9 | pub username: String, 10 | } 11 | 12 | #[derive(Debug, Clone, Hash)] 13 | pub struct VoiceStateData { 14 | pub mute: bool, 15 | pub self_mute: bool, 16 | pub deaf: bool, 17 | pub self_deaf: bool, 18 | pub suppress: bool, 19 | pub nick: Option, 20 | pub talking: bool, 21 | } 22 | 23 | #[derive(Debug, Clone)] 24 | pub struct ConnState { 25 | pub user_id: Option, 26 | pub voice_channel: Option, 27 | pub users: HashMap, 28 | pub voice_states: HashMap, 29 | } 30 | 31 | pub fn calculate_hash(t: &T) -> u64 { 32 | let mut s = DefaultHasher::new(); 33 | t.hash(&mut s); 34 | s.finish() 35 | } 36 | 37 | impl Hash for ConnState { 38 | fn hash(&self, state: &mut H) { 39 | self.user_id.hash(state); 40 | self.voice_channel.hash(state); 41 | for (id, user) in self.users.clone() { 42 | id.hash(state); 43 | user.hash(state); 44 | } 45 | for (id, voice_state) in self.voice_states.clone() { 46 | id.hash(state); 47 | voice_state.hash(state); 48 | } 49 | } 50 | } 51 | 52 | impl ConnState { 53 | pub fn new() -> ConnState { 54 | ConnState { 55 | user_id: None, 56 | voice_channel: None, 57 | users: HashMap::new(), 58 | voice_states: HashMap::new(), 59 | } 60 | } 61 | 62 | #[allow(dead_code)] 63 | pub fn replace_self(&mut self, new: ConnState) { 64 | self.user_id = new.user_id.clone(); 65 | self.voice_channel = new.voice_channel.clone(); 66 | self.users.clear(); 67 | for (key, val) in new.users.iter() { 68 | self.users.insert(key.clone(), val.clone()); 69 | } 70 | self.voice_states.clear(); 71 | for (key, val) in new.voice_states.iter() { 72 | self.voice_states.insert(key.clone(), val.clone()); 73 | } 74 | } 75 | 76 | pub fn clear(&mut self) { 77 | self.user_id = None; 78 | self.voice_channel = None; 79 | self.users.clear(); 80 | self.voice_states.clear(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discern 2 | 3 | Discern is, like its predecessors [DiscordOverlayLinux](https://github.com/trigg/DiscordOverlayLinux) and [Discover-overlay](https://github.com/trigg/Discover), a Discord overlay for linux. 4 | 5 | This one is written in Rust as a project to acclimatise myself to using rust for real projects but so far is a lesson in how to self induce headaches. 6 | 7 | While previous projects gave a plethora of user options and tweaks this one aims for a 'one-size-fits-all' in each module. 8 | 9 | ## Current targets 10 | 11 | 12 | | Target | Binary | Description | 13 | | ------ | ------ | ----------- | 14 | | | discern | Generic binary that will read in ENV variables and make a best-guess at what the user will want. Assumes graphical views only, no plans to best-guess terminal versions | 15 | | x11 | discern-x11 | GTK on x11. Uses combination of highest layer, undecorated window, xshape and xinputshape to draw over top of desktop. | 16 | | wlroots | discern-wlroots | GTK on wayland. Uses wlroots LayerShell to draw over top of desktop | 17 | | rpc | discern-rpc | terminal application to poll or alter discord state | 18 | | statefile | discern-statefile | terminal or daemon application to dump current state to a file or pipe. | 19 | | gamescope | discern-gamescope | Cairo on XCB. Uses X11 XAtom to mark as overlay window for use in gamescope | 20 | | clispam | discern-clispam | terminal application to output all communication raw to terminal. Useful for debugging | 21 | 22 | By default all targets are compiled at once. 23 | 24 | To choose a specific target to compile: 25 | ``` 26 | cargo clean 27 | cargo build --features "wlroots" --no-default-features 28 | ``` 29 | 30 | ## Ideas & Plans 31 | 32 | Ideally, the plan is to eventually modularise the project so we can cover a lot more area. 33 | 34 | - X11 Overlay ✓ 35 | - Wayland/wlroots Overlay ✓ 36 | - OpenGL & Vulkan injectors 37 | - Gamescope specific mode (✓) with autostart (Systemd job?) 38 | - CLI polling of Discord state ✓ 39 | 40 | ## Plans to move over? 41 | 42 | Currently there are no plans to move users of my previous projects to this one. Unless this really hits the ground running I do not expect it to reach feature parity much less improve. 43 | 44 | ## Installing 45 | 46 | ### From binaries 47 | 48 | TBC 49 | 50 | ### From Package managers 51 | 52 | TBC 53 | 54 | ### From Github source 55 | 56 | Ensure you have `rust` and `cargo` installed. 57 | 58 | ``` 59 | git clone git@github.com:trigg/discern.git 60 | cd discern 61 | cargo run 62 | ``` 63 | 64 | #### Arch linux 65 | 66 | ``` 67 | pacman -S clang rustup 68 | rustup default stable 69 | ``` 70 | 71 | ## Did you really need to make another Discord overlay? 72 | 73 | Technically it's not, yet. 74 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "discern" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [[bin]] 7 | name="discern" 8 | path="src/main.rs" 9 | 10 | [[bin]] 11 | name="discern-wlr" 12 | path="src/wlroots-main.rs" 13 | required-features=["wlroots"] 14 | 15 | [[bin]] 16 | name="discern-x11" 17 | path="src/x11-main.rs" 18 | required-features=["x11"] 19 | 20 | [[bin]] 21 | name="discern-rpc" 22 | path="src/rpc-main.rs" 23 | required-features=["rpc"] 24 | 25 | [[bin]] 26 | name="discern-clispam" 27 | path="src/clispam-main.rs" 28 | required-features=["clispam"] 29 | 30 | [[bin]] 31 | name="discern-statefile" 32 | path="src/statefile-main.rs" 33 | required-features=["statefile"] 34 | 35 | [[bin]] 36 | name="discern-gamescope" 37 | path="src/gamescope-main.rs" 38 | required-features=["gamescope"] 39 | 40 | [[bin]] 41 | name="discern-cosmic" 42 | path="src/cosmic-main.rs" 43 | required-features=["cosmic"] 44 | 45 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 46 | 47 | [dependencies] 48 | tokio-tungstenite = "*" 49 | tungstenite = "*" 50 | tokio = { version = "*", features = ["full"] } 51 | reqwest = { version = "0.11", features = ["json"] } 52 | futures-util = { version = "*" } 53 | pin-project = "1.0" 54 | futures-channel = "0.3" 55 | url = "*" 56 | http = "*" 57 | serde = { version = "1.0", features = ["derive"] } 58 | serde_json = "1.0" 59 | tui = "0.12" 60 | termion = "1.5" 61 | clap = { version = "^3.2.5", features = ["cargo"] } 62 | glib = { version ="0.15.10", optional = true } 63 | glib-sys ={ version = "0.15.10", optional = true } 64 | gdk = {version = "0.15.4", features= ["v3_22"], optional = true } 65 | gtk = { version ="0.15.4", optional = true } 66 | gtk-layer-shell = { version ="*", optional = true } 67 | gio = { version ="0.15.11", optional = true } 68 | futures = "0.3" 69 | bytes = "*" 70 | cairo-rs = {version="0.15.11", features=["png", "xcb"], optional=true} 71 | string-builder = "0.2.0" 72 | xcb = {version = "1.1.1", optional = true, features=["randr"] } 73 | xcb-sys = { version ="0.2.1", optional = true } 74 | cairo-sys-rs = {version="0.15.1", optional=true} 75 | wayland-backend = "0.3.5" 76 | 77 | [dependencies.libcosmic] 78 | git = "https://github.com/pop-os/libcosmic" 79 | default-features = false 80 | optional = true 81 | features = ["wayland", "tokio", "applet"] 82 | 83 | 84 | [dependencies.iced_sctk] 85 | git = "https://github.com/pop-os/libcosmic" 86 | optional = true 87 | 88 | [dependencies.cosmic-panel-config] 89 | git = "https://github.com/pop-os/cosmic-panel" 90 | optional = true 91 | 92 | [features] 93 | wlroots=[ "dep:gtk-layer-shell", "dep:cairo-rs", "dep:glib", "dep:gtk", "dep:gdk", "dep:gio", "avatardownloader"] 94 | x11=[ "dep:glib", "dep:gtk", "dep:gdk", "dep:gio","dep:cairo-rs", "avatardownloader"] 95 | clispam=[] 96 | statefile=[] 97 | gamescope=["dep:xcb", "dep:xcb-sys", "dep:cairo-rs", "dep:cairo-sys-rs", "avatardownloader"] 98 | rpc=[] 99 | cosmic=["dep:libcosmic","avatardownloader", "dep:iced_sctk", "dep:cosmic-panel-config"] 100 | default=['statefile', "wlroots", "gamescope", "clispam", "cosmic","x11","rpc"] 101 | 102 | avatardownloader=[] -------------------------------------------------------------------------------- /src/cairorender.rs: -------------------------------------------------------------------------------- 1 | use crate::data::ConnState; 2 | use bytes::Bytes; 3 | use futures::SinkExt; 4 | use futures_util::StreamExt; 5 | use std::collections::hash_map::HashMap; 6 | 7 | #[derive(Debug, Clone, Hash)] 8 | pub struct DiscordAvatarRaw { 9 | pub key: String, 10 | pub raw: Option, 11 | } 12 | 13 | pub async fn avatar_downloader( 14 | mut sender: futures::channel::mpsc::Sender, 15 | mut recvr: futures::channel::mpsc::Receiver, 16 | ) { 17 | tokio::spawn(async move { 18 | println!("Starting avatar thread"); 19 | let mut already_done: HashMap> = HashMap::new(); 20 | 21 | while let Some(state) = recvr.next().await { 22 | for (_key, value) in state.users.into_iter() { 23 | if value.avatar.is_some() { 24 | let avatar_key: String = format!("{}/{}", value.id, value.avatar.unwrap()); 25 | if !already_done.contains_key(&avatar_key) { 26 | println!("Requesting {}", avatar_key); 27 | let url = format!("https://cdn.discordapp.com/avatars/{}.png", avatar_key); 28 | match reqwest::Client::new() 29 | .get(url) 30 | .header("Referer", "https://streamkit.discord.com/overlay/voice") 31 | .header("User-Agent", "Mozilla/5.0") 32 | .send() 33 | .await 34 | { 35 | Ok(resp) => match resp.bytes().await { 36 | Ok(bytes) => { 37 | already_done.insert(avatar_key.clone(), Some(bytes.clone())); 38 | match sender 39 | .send(DiscordAvatarRaw { 40 | key: avatar_key.clone(), 41 | raw: Some(bytes.clone()), 42 | }) 43 | .await 44 | { 45 | Ok(_v) => {} 46 | Err(_e) => {} 47 | } 48 | } 49 | Err(_err) => { 50 | already_done.insert(avatar_key.clone(), None); 51 | match sender 52 | .send(DiscordAvatarRaw { 53 | key: avatar_key.clone(), 54 | raw: None, 55 | }) 56 | .await 57 | { 58 | Ok(_v) => {} 59 | Err(_e) => {} 60 | } 61 | } 62 | }, 63 | Err(err) => { 64 | println!("{}", err); 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | println!("Ended avatar thread"); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /src/statefile-main.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | extern crate serde_json; 3 | use data::ConnState; 4 | use futures::lock::Mutex; 5 | use futures::stream::StreamExt; 6 | use std::env; 7 | use std::fs; 8 | use std::sync::Arc; 9 | use string_builder::Builder; 10 | 11 | mod core; 12 | mod data; 13 | mod macros; 14 | 15 | #[tokio::main] 16 | async fn main() { 17 | let file_path = env::var("DISCERN_STATEFILE") 18 | .expect("No DISCERN_STATEFILE environment variable set. Quitting"); 19 | 20 | // Websocket events to main thread 21 | let (event_sender, event_recv) = futures::channel::mpsc::channel::(10); 22 | let event_sender = Arc::new(Mutex::new(event_sender)); 23 | let event_recv = Arc::new(Mutex::new(event_recv)); 24 | 25 | // Main thread messages to Websocket output 26 | let (msg_sender, msg_recv) = futures::channel::mpsc::channel::(10); 27 | let _msg_sender = Arc::new(Mutex::new(msg_sender)); 28 | let msg_recv = Arc::new(Mutex::new(msg_recv)); 29 | 30 | // Start a thread for connection 31 | let connector_event_sender = event_sender.clone(); 32 | let connector_msg_recv = msg_recv.clone(); 33 | core::connector(connector_event_sender.clone(), connector_msg_recv.clone()).await; 34 | 35 | loop { 36 | while let Some(state) = event_recv.lock().await.next().await { 37 | let file_path = file_path.clone(); 38 | if state.user_id.is_some() && state.voice_channel.is_some() { 39 | let mut builder = Builder::default(); 40 | builder.append(state.voice_channel.unwrap()); 41 | builder.append("\n"); 42 | builder.append(format!("{}", state.users.len())); 43 | builder.append("\n"); 44 | for (id, user) in state.users { 45 | match state.voice_states.get(&id) { 46 | Some(voice_state) => { 47 | match &voice_state.nick { 48 | Some(nick) => builder.append(nick.clone()), 49 | None => builder.append(user.username), 50 | } 51 | builder.append("\n"); 52 | if voice_state.mute || voice_state.self_mute { 53 | builder.append("m"); 54 | } else { 55 | builder.append("."); 56 | } 57 | if voice_state.deaf || voice_state.self_deaf { 58 | builder.append("d"); 59 | } else { 60 | builder.append("."); 61 | } 62 | if voice_state.talking { 63 | builder.append("t"); 64 | } else { 65 | builder.append("."); 66 | } 67 | builder.append("\n"); 68 | match user.avatar { 69 | Some(avatar) => { 70 | builder.append(format!( 71 | "https://cdn.discordapp.com/avatars/{}/{}.png", 72 | user.id, avatar 73 | )); 74 | } 75 | None => {} 76 | } 77 | 78 | builder.append("\n"); 79 | } 80 | None => {} 81 | } 82 | } 83 | fs::write(file_path, builder.string().unwrap()).expect("Unable to write statefile"); 84 | } else { 85 | // 0 Means no channel - and therefore no further data 86 | fs::write(file_path, "0\n").expect("Unable to write statefile"); 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/statefile/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::data::ConnState; 2 | use futures::lock::Mutex; 3 | use std::sync::Arc; 4 | use tokio::net::TcpStream; 5 | extern crate serde_json; 6 | use futures_util::stream::SplitSink; 7 | use serde_json::Value; 8 | use std::fs; 9 | use string_builder::Builder; 10 | use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; 11 | 12 | pub fn start( 13 | file_path: String, 14 | _writer: Arc< 15 | Mutex>, tungstenite::Message>>, 16 | >, 17 | _matches: &clap::ArgMatches, 18 | gui_state: Arc>, 19 | ) -> impl Fn(String) { 20 | // We're not doing a thread for output 21 | //let _gui_loop = tokio::task::spawn(async move { 22 | // loop { 23 | // sleep(Duration::from_millis(1000)).await; 24 | // } 25 | //}); 26 | 27 | move |value| { 28 | // clone all data and spawn an async task 29 | let gui_state = gui_state.clone(); 30 | let value = value.clone(); 31 | let file_path = file_path.clone(); 32 | 33 | tokio::task::spawn(async move { 34 | let data: Value = serde_json::from_str(&value).unwrap(); 35 | match data["cmd"].as_str() { 36 | Some(_) => { 37 | let state = gui_state.lock().await.clone(); 38 | if state.user_id.is_some() && state.voice_channel.is_some() { 39 | let mut builder = Builder::default(); 40 | builder.append(state.voice_channel.unwrap()); 41 | builder.append("\n"); 42 | builder.append(format!("{}", state.users.len())); 43 | builder.append("\n"); 44 | for (id, user) in state.users { 45 | match state.voice_states.get(&id) { 46 | Some(voice_state) => { 47 | match &voice_state.nick { 48 | Some(nick) => builder.append(nick.clone()), 49 | None => builder.append(user.username), 50 | } 51 | builder.append("\n"); 52 | if voice_state.mute || voice_state.self_mute { 53 | builder.append("m"); 54 | } else { 55 | builder.append("."); 56 | } 57 | if voice_state.deaf || voice_state.self_deaf { 58 | builder.append("d"); 59 | } else { 60 | builder.append("."); 61 | } 62 | if voice_state.talking { 63 | builder.append("t"); 64 | } else { 65 | builder.append("."); 66 | } 67 | builder.append("\n"); 68 | match user.avatar { 69 | Some(avatar) => { 70 | builder.append(format!( 71 | "https://cdn.discordapp.com/avatars/{}/{}.png", 72 | user.id, avatar 73 | )); 74 | } 75 | None => {} 76 | } 77 | 78 | builder.append("\n"); 79 | } 80 | None => {} 81 | } 82 | } 83 | fs::write(file_path, builder.string().unwrap()) 84 | .expect("Unable to write statefile"); 85 | } else { 86 | // 0 Means no channel - and therefore no further data 87 | fs::write(file_path, "0\n").expect("Unable to write statefile"); 88 | } 89 | } // Every message may be a change of state. Too often? 90 | None => {} 91 | } 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'discern'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=discern", 15 | "--package=discern" 16 | ], 17 | "filter": { 18 | "name": "discern", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in executable 'discern'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--bin=discern", 34 | "--package=discern" 35 | ], 36 | "filter": { 37 | "name": "discern", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | }, 44 | { 45 | "type": "lldb", 46 | "request": "launch", 47 | "name": "Debug executable 'discern-wlr'", 48 | "cargo": { 49 | "args": [ 50 | "build", 51 | "--bin=discern-wlr", 52 | "--package=discern" 53 | ], 54 | "filter": { 55 | "name": "discern-wlr", 56 | "kind": "bin" 57 | } 58 | }, 59 | "args": [], 60 | "cwd": "${workspaceFolder}" 61 | }, 62 | { 63 | "type": "lldb", 64 | "request": "launch", 65 | "name": "Debug unit tests in executable 'discern-wlr'", 66 | "cargo": { 67 | "args": [ 68 | "test", 69 | "--no-run", 70 | "--bin=discern-wlr", 71 | "--package=discern" 72 | ], 73 | "filter": { 74 | "name": "discern-wlr", 75 | "kind": "bin" 76 | } 77 | }, 78 | "args": [], 79 | "cwd": "${workspaceFolder}" 80 | }, 81 | { 82 | "type": "lldb", 83 | "request": "launch", 84 | "name": "Debug executable 'discern-x11'", 85 | "cargo": { 86 | "args": [ 87 | "build", 88 | "--bin=discern-x11", 89 | "--package=discern" 90 | ], 91 | "filter": { 92 | "name": "discern-x11", 93 | "kind": "bin" 94 | } 95 | }, 96 | "args": [], 97 | "cwd": "${workspaceFolder}" 98 | }, 99 | { 100 | "type": "lldb", 101 | "request": "launch", 102 | "name": "Debug unit tests in executable 'discern-x11'", 103 | "cargo": { 104 | "args": [ 105 | "test", 106 | "--no-run", 107 | "--bin=discern-x11", 108 | "--package=discern" 109 | ], 110 | "filter": { 111 | "name": "discern-x11", 112 | "kind": "bin" 113 | } 114 | }, 115 | "args": [], 116 | "cwd": "${workspaceFolder}" 117 | }, 118 | { 119 | "type": "lldb", 120 | "request": "launch", 121 | "name": "Debug executable 'discern-rpc'", 122 | "cargo": { 123 | "args": [ 124 | "build", 125 | "--bin=discern-rpc", 126 | "--package=discern" 127 | ], 128 | "filter": { 129 | "name": "discern-rpc", 130 | "kind": "bin" 131 | } 132 | }, 133 | "args": [], 134 | "cwd": "${workspaceFolder}" 135 | }, 136 | { 137 | "type": "lldb", 138 | "request": "launch", 139 | "name": "Debug unit tests in executable 'discern-rpc'", 140 | "cargo": { 141 | "args": [ 142 | "test", 143 | "--no-run", 144 | "--bin=discern-rpc", 145 | "--package=discern" 146 | ], 147 | "filter": { 148 | "name": "discern-rpc", 149 | "kind": "bin" 150 | } 151 | }, 152 | "args": [], 153 | "cwd": "${workspaceFolder}" 154 | }, 155 | { 156 | "type": "lldb", 157 | "request": "launch", 158 | "name": "Debug executable 'discern-clispam'", 159 | "cargo": { 160 | "args": [ 161 | "build", 162 | "--bin=discern-clispam", 163 | "--package=discern" 164 | ], 165 | "filter": { 166 | "name": "discern-clispam", 167 | "kind": "bin" 168 | } 169 | }, 170 | "args": [], 171 | "cwd": "${workspaceFolder}" 172 | }, 173 | { 174 | "type": "lldb", 175 | "request": "launch", 176 | "name": "Debug unit tests in executable 'discern-clispam'", 177 | "cargo": { 178 | "args": [ 179 | "test", 180 | "--no-run", 181 | "--bin=discern-clispam", 182 | "--package=discern" 183 | ], 184 | "filter": { 185 | "name": "discern-clispam", 186 | "kind": "bin" 187 | } 188 | }, 189 | "args": [], 190 | "cwd": "${workspaceFolder}" 191 | }, 192 | { 193 | "type": "lldb", 194 | "request": "launch", 195 | "name": "Debug executable 'discern-statefile'", 196 | "cargo": { 197 | "args": [ 198 | "build", 199 | "--bin=discern-statefile", 200 | "--package=discern" 201 | ], 202 | "filter": { 203 | "name": "discern-statefile", 204 | "kind": "bin" 205 | } 206 | }, 207 | "args": [], 208 | "cwd": "${workspaceFolder}" 209 | }, 210 | { 211 | "type": "lldb", 212 | "request": "launch", 213 | "name": "Debug unit tests in executable 'discern-statefile'", 214 | "cargo": { 215 | "args": [ 216 | "test", 217 | "--no-run", 218 | "--bin=discern-statefile", 219 | "--package=discern" 220 | ], 221 | "filter": { 222 | "name": "discern-statefile", 223 | "kind": "bin" 224 | } 225 | }, 226 | "args": [], 227 | "cwd": "${workspaceFolder}" 228 | }, 229 | { 230 | "type": "lldb", 231 | "request": "launch", 232 | "name": "Debug executable 'discern-gamescope'", 233 | "cargo": { 234 | "args": [ 235 | "build", 236 | "--bin=discern-gamescope", 237 | "--package=discern" 238 | ], 239 | "filter": { 240 | "name": "discern-gamescope", 241 | "kind": "bin" 242 | } 243 | }, 244 | "args": [], 245 | "cwd": "${workspaceFolder}" 246 | }, 247 | { 248 | "type": "lldb", 249 | "request": "launch", 250 | "name": "Debug unit tests in executable 'discern-gamescope'", 251 | "cargo": { 252 | "args": [ 253 | "test", 254 | "--no-run", 255 | "--bin=discern-gamescope", 256 | "--package=discern" 257 | ], 258 | "filter": { 259 | "name": "discern-gamescope", 260 | "kind": "bin" 261 | } 262 | }, 263 | "args": [], 264 | "cwd": "${workspaceFolder}" 265 | }, 266 | { 267 | "type": "lldb", 268 | "request": "launch", 269 | "name": "Debug executable 'discern-cosmic'", 270 | "cargo": { 271 | "args": [ 272 | "build", 273 | "--bin=discern-cosmic", 274 | "--package=discern" 275 | ], 276 | "filter": { 277 | "name": "discern-cosmic", 278 | "kind": "bin" 279 | } 280 | }, 281 | "args": [], 282 | "cwd": "${workspaceFolder}" 283 | }, 284 | { 285 | "type": "lldb", 286 | "request": "launch", 287 | "name": "Debug unit tests in executable 'discern-cosmic'", 288 | "cargo": { 289 | "args": [ 290 | "test", 291 | "--no-run", 292 | "--bin=discern-cosmic", 293 | "--package=discern" 294 | ], 295 | "filter": { 296 | "name": "discern-cosmic", 297 | "kind": "bin" 298 | } 299 | }, 300 | "args": [], 301 | "cwd": "${workspaceFolder}" 302 | } 303 | ] 304 | } -------------------------------------------------------------------------------- /src/wlroots-main.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | extern crate serde_json; 3 | use crate::data::calculate_hash; 4 | use crate::data::ConnState; 5 | use cairo::{ 6 | Antialias, Context, FillRule, FontSlant, FontWeight, ImageSurface, Operator, RectangleInt, 7 | Region, 8 | }; 9 | use cairorender::DiscordAvatarRaw; 10 | use futures::lock::Mutex; 11 | use futures::stream::StreamExt; 12 | use futures_util::SinkExt; 13 | use gio::prelude::*; 14 | use glib; 15 | use gtk::prelude::*; 16 | use gtk_layer_shell; 17 | use std::collections::hash_map::HashMap; 18 | use std::f64::consts::PI; 19 | use std::io::Cursor; 20 | use std::sync::Arc; 21 | 22 | mod cairorender; 23 | mod core; 24 | mod data; 25 | mod macros; 26 | 27 | #[tokio::main] 28 | async fn main() { 29 | // Avatar to main thread 30 | let (avatar_request_sender, avatar_request_recv) = 31 | futures::channel::mpsc::channel::(10); 32 | let avatar_request_sender = Arc::new(Mutex::new(avatar_request_sender)); 33 | 34 | // Mainthread to Avatar 35 | let (avatar_done_sender, avatar_done_recv) = 36 | futures::channel::mpsc::channel::(10); 37 | 38 | let avatar_done_recv = Arc::new(Mutex::new(avatar_done_recv)); 39 | 40 | // Websocket events to main thread 41 | let (event_sender, event_recv) = futures::channel::mpsc::channel::(10); 42 | let event_sender = Arc::new(Mutex::new(event_sender)); 43 | let event_recv = Arc::new(Mutex::new(event_recv)); 44 | 45 | // Main thread messages to Websocket output 46 | let (msg_sender, msg_recv) = futures::channel::mpsc::channel::(10); 47 | let _msg_sender = Arc::new(Mutex::new(msg_sender)); 48 | let msg_recv = Arc::new(Mutex::new(msg_recv)); 49 | 50 | // Start a thread for connection 51 | let connector_event_sender = event_sender.clone(); 52 | let connector_msg_recv = msg_recv.clone(); 53 | core::connector(connector_event_sender.clone(), connector_msg_recv.clone()).await; 54 | 55 | // Start a thread for avatars 56 | cairorender::avatar_downloader(avatar_done_sender, avatar_request_recv).await; 57 | 58 | // Avatar grabbing thread 59 | let state = Arc::new(std::sync::Mutex::new(ConnState::new())); 60 | 61 | // GTK/ Glib Main 62 | 63 | // avatar surfaces 64 | let avatar_list: HashMap> = HashMap::new(); 65 | let avatar_list = Arc::new(std::sync::Mutex::new(avatar_list)); 66 | 67 | fn draw_deaf(ctx: &Context, pos_x: f64, pos_y: f64, size: f64) { 68 | ctx.save().expect("Could not save cairo state"); 69 | ctx.translate(pos_x, pos_y); 70 | ctx.scale(size, size); 71 | ctx.set_source_rgba(1.0, 0.0, 0.0, 1.0); 72 | 73 | ctx.save().expect("Could not save cairo state"); 74 | 75 | // Clip Strike-through 76 | ctx.set_fill_rule(FillRule::EvenOdd); 77 | ctx.set_line_width(0.1); 78 | ctx.move_to(0.0, 0.0); 79 | ctx.line_to(1.0, 0.0); 80 | ctx.line_to(1.0, 1.0); 81 | ctx.line_to(0.0, 1.0); 82 | ctx.line_to(0.0, 0.0); 83 | ctx.close_path(); 84 | ctx.new_sub_path(); 85 | ctx.arc(0.9, 0.1, 0.05, 1.25 * PI, 2.25 * PI); 86 | ctx.arc(0.1, 0.9, 0.05, 0.25 * PI, 1.25 * PI); 87 | ctx.close_path(); 88 | ctx.clip(); 89 | 90 | // Top band 91 | ctx.arc(0.5, 0.5, 0.2, 1.0 * PI, 0.0); 92 | ctx.stroke().expect("Could not stroke"); 93 | 94 | // Left band 95 | ctx.arc(0.28, 0.65, 0.075, 1.5 * PI, 0.5 * PI); 96 | ctx.move_to(0.3, 0.5); 97 | ctx.line_to(0.3, 0.75); 98 | ctx.stroke().expect("Could not stroke"); 99 | 100 | // Right band 101 | ctx.arc(0.72, 0.65, 0.075, 0.5 * PI, 1.5 * PI); 102 | ctx.move_to(0.7, 0.5); 103 | ctx.line_to(0.7, 0.75); 104 | ctx.stroke().expect("Could not stroke"); 105 | 106 | ctx.restore().expect("Could not restore cairo state"); 107 | // Strike through 108 | ctx.arc(0.7, 0.3, 0.035, 1.25 * PI, 2.25 * PI); 109 | ctx.arc(0.3, 0.7, 0.035, 0.25 * PI, 1.25 * PI); 110 | ctx.close_path(); 111 | ctx.fill().expect("Could not fill"); 112 | 113 | ctx.restore().expect("Could not restore"); 114 | } 115 | 116 | fn draw_mute(ctx: &Context, pos_x: f64, pos_y: f64, size: f64) { 117 | ctx.save().expect("Could not save cairo state"); 118 | ctx.translate(pos_x, pos_y); 119 | ctx.scale(size, size); 120 | ctx.set_source_rgba(1.0, 0.0, 0.0, 1.0); 121 | ctx.save().expect("Could not save cairo state"); 122 | // Clip Strike-through 123 | ctx.set_fill_rule(FillRule::EvenOdd); 124 | ctx.set_line_width(0.1); 125 | ctx.move_to(0.0, 0.0); 126 | ctx.line_to(1.0, 0.0); 127 | ctx.line_to(1.0, 1.0); 128 | ctx.line_to(0.0, 1.0); 129 | ctx.line_to(0.0, 0.0); 130 | ctx.close_path(); 131 | ctx.new_sub_path(); 132 | ctx.arc(0.9, 0.1, 0.05, 1.25 * PI, 2.25 * PI); 133 | ctx.arc(0.1, 0.9, 0.05, 0.25 * PI, 1.25 * PI); 134 | ctx.close_path(); 135 | ctx.clip(); 136 | // Center 137 | ctx.set_line_width(0.07); 138 | ctx.arc(0.5, 0.3, 0.1, PI, 2.0 * PI); 139 | ctx.arc(0.5, 0.5, 0.1, 0.0, PI); 140 | ctx.close_path(); 141 | ctx.fill().expect("Could not fill"); 142 | ctx.set_line_width(0.05); 143 | // Stand rounded 144 | ctx.arc(0.5, 0.5, 0.15, 0.0, 1.0 * PI); 145 | ctx.stroke().expect("Could not stroke"); 146 | // Stand vertical 147 | ctx.move_to(0.5, 0.65); 148 | ctx.line_to(0.5, 0.75); 149 | ctx.stroke().expect("Could not stroke"); 150 | // Stand horizontal 151 | ctx.move_to(0.35, 0.75); 152 | ctx.line_to(0.65, 0.75); 153 | ctx.stroke().expect("Could not stroke"); 154 | ctx.restore().expect("Coult not restore cairo state"); 155 | // Strike through 156 | ctx.arc(0.7, 0.3, 0.035, 1.25 * PI, 2.25 * PI); 157 | ctx.arc(0.3, 0.7, 0.035, 0.25 * PI, 1.25 * PI); 158 | ctx.close_path(); 159 | ctx.fill().expect("Could not fill"); 160 | ctx.restore().expect("Could not restore cairo state"); 161 | } 162 | 163 | fn set_untouchable(window: >k::ApplicationWindow) { 164 | let reg = Region::create(); 165 | window.input_shape_combine_region(Some(®)); 166 | window.set_accept_focus(false); 167 | } 168 | let application = gtk::Application::new( 169 | Some("io.github.trigg.discern"), 170 | gio::ApplicationFlags::REPLACE, 171 | ); 172 | application.connect_activate(move |application: >k::Application| { 173 | // Create overlay 174 | let window = gtk::ApplicationWindow::new(application); 175 | 176 | // Customise redraw 177 | { 178 | let state = state.clone(); 179 | let avatar_list = avatar_list.clone(); 180 | window.connect_draw(move |window: >k::ApplicationWindow, ctx: &Context| { 181 | draw_overlay_gtk!(window, ctx, avatar_list, state); 182 | 183 | Inhibit(false) 184 | }); 185 | } 186 | 187 | // Set untouchable 188 | set_untouchable(&window); 189 | 190 | // Set as shell component 191 | gtk_layer_shell::init_for_window(&window); 192 | gtk_layer_shell::set_layer(&window, gtk_layer_shell::Layer::Overlay); 193 | gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Top, true); 194 | gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Bottom, true); 195 | gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Left, true); 196 | gtk_layer_shell::set_anchor(&window, gtk_layer_shell::Edge::Right, true); 197 | // Now we start! 198 | window.set_app_paintable(true); 199 | window.show_all(); 200 | let state = state.clone(); 201 | 202 | // State watcher 203 | glib::MainContext::default().spawn_local({ 204 | let window = window.clone(); 205 | let event_recv = event_recv.clone(); 206 | let avatar_request_sender = avatar_request_sender.clone(); 207 | async move { 208 | while let Some(event) = event_recv.lock().await.next().await { 209 | // We've just been alerted the state may have changed, we have a futures Mutex which can't be used in drawing, so copy data out to 'local' mutex! 210 | let update_state: ConnState = event.clone(); 211 | let last_state: ConnState = state.lock().unwrap().clone(); 212 | let _ = avatar_request_sender.lock().await.send(event.clone()).await; 213 | if calculate_hash(&update_state) != calculate_hash(&last_state) { 214 | state.lock().unwrap().replace_self(update_state); 215 | window.queue_draw(); 216 | } 217 | } 218 | } 219 | }); 220 | 221 | // Avatar watcher 222 | glib::MainContext::default().spawn_local({ 223 | let window = window.clone(); 224 | let avatar_done_recv = avatar_done_recv.clone(); 225 | let avatar_list = avatar_list.clone(); 226 | async move { 227 | while let Some(event) = avatar_done_recv.lock().await.next().await { 228 | match event.raw { 229 | Some(raw) => { 230 | let surface = ImageSurface::create_from_png(&mut Cursor::new(raw)) 231 | .expect("Error processing user avatar"); 232 | avatar_list 233 | .lock() 234 | .unwrap() 235 | .insert(event.key.clone(), Some(surface)); 236 | } 237 | None => { 238 | println!("Raw is None for user id {}", event.key); 239 | avatar_list.lock().unwrap().insert(event.key.clone(), None); 240 | } 241 | } 242 | window.queue_draw(); 243 | } 244 | } 245 | }); 246 | }); 247 | let a: [String; 0] = Default::default(); // No args 248 | application.run_with_args(&a); 249 | } 250 | -------------------------------------------------------------------------------- /src/x11-main.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | extern crate serde_json; 3 | use crate::data::calculate_hash; 4 | 5 | use crate::data::ConnState; 6 | use cairo::{ 7 | Antialias, Context, FillRule, FontSlant, FontWeight, ImageSurface, Operator, RectangleInt, 8 | Region, 9 | }; 10 | use futures::lock::Mutex; 11 | use futures::stream::StreamExt; 12 | use futures_util::SinkExt; 13 | use gio::prelude::*; 14 | use glib; 15 | use gtk::prelude::*; 16 | use std::collections::hash_map::HashMap; 17 | use std::f64::consts::PI; 18 | use std::io::Cursor; 19 | use std::sync::Arc; 20 | 21 | mod cairorender; 22 | mod core; 23 | mod data; 24 | mod macros; 25 | 26 | #[tokio::main] 27 | async fn main() { 28 | // Avatar to main thread 29 | let (avatar_request_sender, avatar_request_recv) = 30 | futures::channel::mpsc::channel::(10); 31 | let avatar_request_sender = Arc::new(Mutex::new(avatar_request_sender)); 32 | 33 | // Mainthread to Avatar 34 | let (avatar_done_sender, avatar_done_recv) = 35 | futures::channel::mpsc::channel::(10); 36 | 37 | let avatar_done_recv = Arc::new(Mutex::new(avatar_done_recv)); 38 | 39 | // Websocket events to main thread 40 | let (event_sender, event_recv) = futures::channel::mpsc::channel::(10); 41 | let event_sender = Arc::new(Mutex::new(event_sender)); 42 | let event_recv = Arc::new(Mutex::new(event_recv)); 43 | 44 | // Main thread messages to Websocket output 45 | let (msg_sender, msg_recv) = futures::channel::mpsc::channel::(10); 46 | let _msg_sender = Arc::new(Mutex::new(msg_sender)); 47 | let msg_recv = Arc::new(Mutex::new(msg_recv)); 48 | 49 | // Start a thread for connection 50 | let connector_event_sender = event_sender.clone(); 51 | let connector_msg_recv = msg_recv.clone(); 52 | core::connector(connector_event_sender.clone(), connector_msg_recv.clone()).await; 53 | 54 | cairorender::avatar_downloader(avatar_done_sender, avatar_request_recv).await; 55 | 56 | // Avatar grabbing thread 57 | let state = Arc::new(std::sync::Mutex::new(ConnState::new())); 58 | 59 | // avatar surfaces 60 | let avatar_list: HashMap> = HashMap::new(); 61 | let avatar_list = Arc::new(std::sync::Mutex::new(avatar_list)); 62 | 63 | fn draw_deaf(ctx: &Context, pos_x: f64, pos_y: f64, size: f64) { 64 | ctx.save().expect("Could not save cairo state"); 65 | ctx.translate(pos_x, pos_y); 66 | ctx.scale(size, size); 67 | ctx.set_source_rgba(1.0, 0.0, 0.0, 1.0); 68 | 69 | ctx.save().expect("Could not save cairo state"); 70 | 71 | // Clip Strike-through 72 | ctx.set_fill_rule(FillRule::EvenOdd); 73 | ctx.set_line_width(0.1); 74 | ctx.move_to(0.0, 0.0); 75 | ctx.line_to(1.0, 0.0); 76 | ctx.line_to(1.0, 1.0); 77 | ctx.line_to(0.0, 1.0); 78 | ctx.line_to(0.0, 0.0); 79 | ctx.close_path(); 80 | ctx.new_sub_path(); 81 | ctx.arc(0.9, 0.1, 0.05, 1.25 * PI, 2.25 * PI); 82 | ctx.arc(0.1, 0.9, 0.05, 0.25 * PI, 1.25 * PI); 83 | ctx.close_path(); 84 | ctx.clip(); 85 | 86 | // Top band 87 | ctx.arc(0.5, 0.5, 0.2, 1.0 * PI, 0.0); 88 | ctx.stroke().expect("Could not stroke"); 89 | 90 | // Left band 91 | ctx.arc(0.28, 0.65, 0.075, 1.5 * PI, 0.5 * PI); 92 | ctx.move_to(0.3, 0.5); 93 | ctx.line_to(0.3, 0.75); 94 | ctx.stroke().expect("Could not stroke"); 95 | 96 | // Right band 97 | ctx.arc(0.72, 0.65, 0.075, 0.5 * PI, 1.5 * PI); 98 | ctx.move_to(0.7, 0.5); 99 | ctx.line_to(0.7, 0.75); 100 | ctx.stroke().expect("Could not stroke"); 101 | 102 | ctx.restore().expect("Could not restore cairo state"); 103 | // Strike through 104 | ctx.arc(0.7, 0.3, 0.035, 1.25 * PI, 2.25 * PI); 105 | ctx.arc(0.3, 0.7, 0.035, 0.25 * PI, 1.25 * PI); 106 | ctx.close_path(); 107 | ctx.fill().expect("Could not fill"); 108 | 109 | ctx.restore().expect("Could not restore"); 110 | } 111 | 112 | fn draw_mute(ctx: &Context, pos_x: f64, pos_y: f64, size: f64) { 113 | ctx.save().expect("Could not save cairo state"); 114 | ctx.translate(pos_x, pos_y); 115 | ctx.scale(size, size); 116 | ctx.set_source_rgba(1.0, 0.0, 0.0, 1.0); 117 | ctx.save().expect("Could not save cairo state"); 118 | // Clip Strike-through 119 | ctx.set_fill_rule(FillRule::EvenOdd); 120 | ctx.set_line_width(0.1); 121 | ctx.move_to(0.0, 0.0); 122 | ctx.line_to(1.0, 0.0); 123 | ctx.line_to(1.0, 1.0); 124 | ctx.line_to(0.0, 1.0); 125 | ctx.line_to(0.0, 0.0); 126 | ctx.close_path(); 127 | ctx.new_sub_path(); 128 | ctx.arc(0.9, 0.1, 0.05, 1.25 * PI, 2.25 * PI); 129 | ctx.arc(0.1, 0.9, 0.05, 0.25 * PI, 1.25 * PI); 130 | ctx.close_path(); 131 | ctx.clip(); 132 | // Center 133 | ctx.set_line_width(0.07); 134 | ctx.arc(0.5, 0.3, 0.1, PI, 2.0 * PI); 135 | ctx.arc(0.5, 0.5, 0.1, 0.0, PI); 136 | ctx.close_path(); 137 | ctx.fill().expect("Could not fill"); 138 | ctx.set_line_width(0.05); 139 | // Stand rounded 140 | ctx.arc(0.5, 0.5, 0.15, 0.0, 1.0 * PI); 141 | ctx.stroke().expect("Could not stroke"); 142 | // Stand vertical 143 | ctx.move_to(0.5, 0.65); 144 | ctx.line_to(0.5, 0.75); 145 | ctx.stroke().expect("Could not stroke"); 146 | // Stand horizontal 147 | ctx.move_to(0.35, 0.75); 148 | ctx.line_to(0.65, 0.75); 149 | ctx.stroke().expect("Could not stroke"); 150 | ctx.restore().expect("Coult not restore cairo state"); 151 | // Strike through 152 | ctx.arc(0.7, 0.3, 0.035, 1.25 * PI, 2.25 * PI); 153 | ctx.arc(0.3, 0.7, 0.035, 0.25 * PI, 1.25 * PI); 154 | ctx.close_path(); 155 | ctx.fill().expect("Could not fill"); 156 | ctx.restore().expect("Could not restore cairo state"); 157 | } 158 | 159 | fn set_untouchable(window: >k::ApplicationWindow) { 160 | let reg = Region::create(); 161 | window.input_shape_combine_region(Some(®)); 162 | window.set_accept_focus(false); 163 | let reg = Region::create(); 164 | reg.union_rectangle(&RectangleInt { 165 | x: 0, 166 | y: 0, 167 | width: 1, 168 | height: 1, 169 | }) 170 | .expect("Failed to add rectangle"); 171 | // If region ends up as empty at this point then queue_draw is ignored and we never draw again! 172 | window.shape_combine_region(Some(®)); 173 | } 174 | let application = gtk::Application::new( 175 | Some("io.github.trigg.discern"), 176 | gio::ApplicationFlags::REPLACE, 177 | ); 178 | application.connect_activate(move |application: >k::Application| { 179 | // Create overlay 180 | let window = gtk::ApplicationWindow::new(application); 181 | 182 | // Customise redraw 183 | { 184 | let state = state.clone(); 185 | let avatar_list = avatar_list.clone(); 186 | let avatar_list = avatar_list.clone(); 187 | window.connect_draw(move |window: >k::ApplicationWindow, ctx: &Context| { 188 | draw_overlay_gtk!(window, ctx, avatar_list, state); 189 | 190 | Inhibit(false) 191 | }); 192 | } 193 | 194 | // Set untouchable 195 | set_untouchable(&window); 196 | 197 | // Set expected X11 rules 198 | window.set_skip_pager_hint(true); 199 | window.set_skip_taskbar_hint(true); 200 | window.set_keep_above(true); 201 | window.set_decorated(false); 202 | window.set_accept_focus(false); 203 | window.maximize(); 204 | // Now we start! 205 | window.set_app_paintable(true); 206 | window.show_all(); 207 | let state = state.clone(); 208 | 209 | // State watcher 210 | glib::MainContext::default().spawn_local({ 211 | let window = window.clone(); 212 | let event_recv = event_recv.clone(); 213 | let avatar_request_sender = avatar_request_sender.clone(); 214 | async move { 215 | while let Some(event) = event_recv.lock().await.next().await { 216 | // We've just been alerted the state may have changed, we have a futures Mutex which can't be used in drawing, so copy data out to 'local' mutex! 217 | let update_state: ConnState = event.clone(); 218 | let last_state: ConnState = state.lock().unwrap().clone(); 219 | let _ = avatar_request_sender.lock().await.send(event.clone()).await; 220 | if calculate_hash(&update_state) != calculate_hash(&last_state) { 221 | state.lock().unwrap().replace_self(update_state); 222 | window.queue_draw(); 223 | } 224 | } 225 | } 226 | }); 227 | 228 | // Avatar watcher 229 | glib::MainContext::default().spawn_local({ 230 | let window = window.clone(); 231 | let avatar_done_recv = avatar_done_recv.clone(); 232 | let avatar_list = avatar_list.clone(); 233 | async move { 234 | while let Some(event) = avatar_done_recv.lock().await.next().await { 235 | match event.raw { 236 | Some(raw) => { 237 | let surface = ImageSurface::create_from_png(&mut Cursor::new(raw)) 238 | .expect("Error processing user avatar"); 239 | avatar_list 240 | .lock() 241 | .unwrap() 242 | .insert(event.key.clone(), Some(surface)); 243 | } 244 | None => { 245 | println!("Raw is None for user id {}", event.key); 246 | avatar_list.lock().unwrap().insert(event.key.clone(), None); 247 | } 248 | } 249 | window.queue_draw(); 250 | } 251 | } 252 | }); 253 | }); 254 | let a: [String; 0] = Default::default(); // No args 255 | application.run_with_args(&a); 256 | } 257 | -------------------------------------------------------------------------------- /src/rpc-main.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | extern crate serde_json; 3 | use clap::{arg, command, Command}; 4 | use futures::lock::Mutex; 5 | use futures::stream::StreamExt; 6 | use serde_json::json; 7 | use std::sync::Arc; 8 | 9 | mod core; 10 | mod data; 11 | mod macros; 12 | 13 | #[tokio::main] 14 | async fn main() { 15 | // Websocket events to main thread 16 | let (event_sender, event_recv) = futures::channel::mpsc::channel::(10); 17 | let event_sender = Arc::new(Mutex::new(event_sender)); 18 | let event_recv = Arc::new(Mutex::new(event_recv)); 19 | 20 | // Main thread messages to Websocket output 21 | let (msg_sender, msg_recv) = futures::channel::mpsc::channel::(10); 22 | let msg_sender = Arc::new(Mutex::new(msg_sender)); 23 | let msg_recv = Arc::new(Mutex::new(msg_recv)); 24 | 25 | // Start a thread for connection 26 | let connector_event_sender = event_sender.clone(); 27 | let connector_msg_recv = msg_recv.clone(); 28 | core::connector(connector_event_sender.clone(), connector_msg_recv.clone()).await; 29 | 30 | // Setup Command line args 31 | let matches = command!() 32 | .subcommand_required(true) 33 | .subcommand( 34 | Command::new("channel") 35 | .about("Get current channel information") 36 | .subcommand(Command::new("id").about("Get Room ID. None is 0")) 37 | .subcommand(Command::new("name").about("Get Room Name")) 38 | .subcommand( 39 | Command::new("useridlist").about("Get List of users in room, return IDs"), 40 | ) 41 | .subcommand( 42 | Command::new("usernamelist").about("Get List of users in room, return names"), 43 | ) 44 | .subcommand( 45 | Command::new("move") 46 | .about("Switch to another room by ID") 47 | .arg(arg!([ID] "ID of room to move user to")), 48 | ), 49 | ) 50 | .subcommand( 51 | Command::new("devices") 52 | .about("Get audio device information") 53 | .subcommand( 54 | Command::new("mute").about("Check mute state of user").arg( 55 | arg!(-s --set "Alter mute state. `true` `false` or `toggle`") 56 | .required(false), 57 | ), 58 | ) 59 | .subcommand( 60 | Command::new("deaf").about("Check deaf state of user").arg( 61 | arg!(-s --set "Alter deaf state. `true` `false` or `toggle`") 62 | .required(false), 63 | ), 64 | ), 65 | ) 66 | .get_matches(); 67 | // Types to store what the user requested action was 68 | #[derive(Debug, Clone)] 69 | enum AudioAction { 70 | True, 71 | False, 72 | Toggle, 73 | Get, 74 | } 75 | #[derive(Debug, Clone)] 76 | struct Args { 77 | get_room_id: bool, 78 | get_room_name: bool, 79 | get_room_userlist: bool, 80 | get_room_idlist: bool, 81 | set_room: Option, 82 | mute: Option, 83 | deaf: Option, 84 | } 85 | let mut user_args = Args { 86 | get_room_id: false, 87 | get_room_name: false, 88 | get_room_userlist: false, 89 | get_room_idlist: false, 90 | set_room: None, 91 | mute: None, 92 | deaf: None, 93 | }; 94 | // Decant the args into the store above 95 | match matches.subcommand() { 96 | Some(("channel", sub)) => match sub.subcommand() { 97 | Some(("id", _)) => user_args.get_room_id = true, 98 | Some(("name", _)) => user_args.get_room_name = true, 99 | Some(("useridlist", _)) => user_args.get_room_idlist = true, 100 | Some(("usernamelist", _)) => user_args.get_room_userlist = true, 101 | Some(("move", sub)) => { 102 | user_args.set_room = Some(sub.value_of("ID").unwrap().to_string()); 103 | } 104 | Some((_, _)) => { 105 | println!("Unknown rpc args"); 106 | std::process::exit(0); 107 | } 108 | None => { 109 | println!("Unknown rpc args"); 110 | std::process::exit(0); 111 | } 112 | }, 113 | Some(("devices", sub)) => match sub.subcommand() { 114 | Some(("mute", args)) => match args.value_of("set") { 115 | Some("true") => user_args.mute = Some(AudioAction::True), 116 | Some("false") => user_args.mute = Some(AudioAction::False), 117 | Some("toggle") => user_args.mute = Some(AudioAction::Toggle), 118 | Some(_) => { 119 | println!("Unknown rpc args"); 120 | std::process::exit(0); 121 | } 122 | None => user_args.mute = Some(AudioAction::Get), 123 | }, 124 | Some(("deaf", args)) => match args.value_of("set") { 125 | Some("true") => user_args.deaf = Some(AudioAction::True), 126 | Some("false") => user_args.deaf = Some(AudioAction::False), 127 | Some("toggle") => user_args.deaf = Some(AudioAction::Toggle), 128 | Some(_) => { 129 | println!("Unknown rpc args"); 130 | std::process::exit(0); 131 | } 132 | None => user_args.deaf = Some(AudioAction::Get), 133 | }, 134 | Some((_, _)) => { 135 | println!("Unknown rpc args"); 136 | std::process::exit(0); 137 | } 138 | None => { 139 | println!("Unknown rpc args"); 140 | std::process::exit(0); 141 | } 142 | }, 143 | Some((_, _)) => { 144 | println!("Unknown rpc args"); 145 | std::process::exit(0); 146 | } 147 | None => { 148 | println!("Unknown rpc args"); 149 | std::process::exit(0); 150 | } 151 | } 152 | 153 | loop { 154 | while let Some(event) = event_recv.lock().await.next().await { 155 | println!("{:?}", event); 156 | //let data: Value = serde_json::from_str(&event).unwrap(); 157 | let data = json!([]); // TODO 158 | // We broke RPC as there is no direct JSON at this point. 159 | // Need to consider raw-string options again 160 | match data["cmd"].as_str() { 161 | Some("SELECT_VOICE_CHANNEL") => { 162 | // Successfully Selected a channel. Exit! 163 | std::process::exit(0); 164 | } 165 | Some("GET_SELECTED_VOICE_CHANNEL") => { 166 | if user_args.get_room_id { 167 | // Print out the channel ID and exit! 168 | match data["data"]["id"].as_str() { 169 | Some(val) => { 170 | println!("{}", val); 171 | } 172 | None => { 173 | println!("0"); 174 | } 175 | } 176 | std::process::exit(0); 177 | } 178 | if user_args.get_room_name { 179 | // Print out the channel name and exit! 180 | match data["data"]["name"].as_str() { 181 | Some(val) => { 182 | println!("{}", val); 183 | } 184 | None => { 185 | println!(""); 186 | } 187 | } 188 | std::process::exit(0); 189 | } 190 | if user_args.get_room_idlist { 191 | // Print out the user id list and exit! 192 | match data["data"]["voice_states"].as_array() { 193 | Some(array) => { 194 | for user in array { 195 | println!("{}", user["user"]["id"].as_str().unwrap()); 196 | } 197 | } 198 | None => {} 199 | } 200 | std::process::exit(0); 201 | } 202 | if user_args.get_room_userlist { 203 | // Print out the user name list and exit! 204 | match data["data"]["voice_states"].as_array() { 205 | Some(array) => { 206 | for user in array { 207 | println!("{}", user["user"]["username"].as_str().unwrap()); 208 | } 209 | } 210 | None => {} 211 | } 212 | std::process::exit(0); 213 | } 214 | } 215 | Some("SET_VOICE_SETTINGS") => { 216 | // Successfully set voice settings. Exit! 217 | std::process::exit(0); 218 | } 219 | Some("GET_VOICE_SETTINGS") => { 220 | // Got current voice settings. 221 | // if we're polling this, return and exit! 222 | // if we're toggling, pass it back inverted 223 | match user_args.mute.clone() { 224 | Some(AudioAction::True) => {} 225 | Some(AudioAction::False) => {} 226 | Some(AudioAction::Toggle) => { 227 | send_mpsc!( 228 | msg_sender, 229 | packet_set_devices!( 230 | "mute", 231 | !data["data"]["mute"].as_bool().unwrap(), 232 | "deadbeef" 233 | ) 234 | ); 235 | } 236 | Some(AudioAction::Get) => { 237 | println!("{}", data["data"]["mute"].as_bool().unwrap()); 238 | std::process::exit(0); 239 | } 240 | None => {} 241 | }; 242 | match user_args.deaf.clone() { 243 | Some(AudioAction::True) => {} 244 | Some(AudioAction::False) => {} 245 | Some(AudioAction::Toggle) => { 246 | send_mpsc!( 247 | msg_sender, 248 | packet_set_devices!( 249 | "deaf", 250 | !data["data"]["deaf"].as_bool().unwrap(), 251 | "deadbeef" 252 | ) 253 | ); 254 | } 255 | Some(AudioAction::Get) => { 256 | println!("{}", data["data"]["deaf"].as_bool().unwrap()); 257 | std::process::exit(0); 258 | } 259 | None => {} 260 | } 261 | } 262 | Some("AUTHENTICATE") => { 263 | // On connection make first call. 264 | match user_args.mute { 265 | Some(AudioAction::True) => { 266 | send_mpsc!(msg_sender, packet_set_devices!("mute", true, "deadbeef")); 267 | } 268 | Some(AudioAction::False) => { 269 | send_mpsc!(msg_sender, packet_set_devices!("mute", false, "deadbeef")); 270 | } 271 | Some(AudioAction::Toggle) => { 272 | send_mpsc!(msg_sender, packet_req_devices!()); 273 | } 274 | Some(AudioAction::Get) => { 275 | send_mpsc!(msg_sender, packet_req_devices!()); 276 | } 277 | None => {} 278 | } 279 | match user_args.deaf { 280 | Some(AudioAction::True) => { 281 | send_mpsc!(msg_sender, packet_set_devices!("deaf", true, "deadbeef")); 282 | } 283 | Some(AudioAction::False) => { 284 | send_mpsc!(msg_sender, packet_set_devices!("deaf", false, "deadbeef")); 285 | } 286 | Some(AudioAction::Toggle) => { 287 | send_mpsc!(msg_sender, packet_req_devices!()); 288 | } 289 | Some(AudioAction::Get) => { 290 | send_mpsc!(msg_sender, packet_req_devices!()); 291 | } 292 | None => {} 293 | } 294 | match user_args.set_room { 295 | Some(ref roomid) => { 296 | send_mpsc!(msg_sender, packet_set_channel!(roomid.clone())); 297 | } 298 | None => {} 299 | } 300 | } 301 | Some(_) => {} 302 | None => {} 303 | } 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/core.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | extern crate serde_json; 3 | use futures::lock::Mutex; 4 | use futures_util::{SinkExt, StreamExt}; 5 | use http::Request; 6 | use serde_json::json; 7 | use serde_json::Value; 8 | use std::sync::Arc; 9 | use tokio::time::{sleep, Duration}; 10 | use tokio_tungstenite::{connect_async, tungstenite::protocol::Message}; 11 | use tungstenite::handshake::client::generate_key; 12 | 13 | use crate::*; 14 | 15 | async fn user_left_channel(state: Arc>) { 16 | let mut current_state = state.lock().await; 17 | current_state.voice_channel = None; 18 | current_state.users.clear(); 19 | current_state.voice_states.clear(); 20 | } 21 | 22 | async fn set_user_talking(state: Arc>, user_id: String, talking: bool) { 23 | let mut unlocked = state.lock().await; 24 | let mut voice_state = unlocked.voice_states.get_mut(&user_id).unwrap().clone(); 25 | voice_state.talking = talking; 26 | unlocked.voice_states.insert(user_id.clone(), voice_state); 27 | } 28 | 29 | async fn update_state_from_voice_state(state: Arc>, voice_state: &Value) { 30 | let user_id: String = voice_state["user"]["id"].as_str().unwrap().to_string(); 31 | let mut current_state = state.lock().await; 32 | 33 | let username = voice_state["user"]["username"] 34 | .as_str() 35 | .unwrap() 36 | .to_string(); 37 | let mut avatar: Option = None; 38 | match voice_state["user"]["avatar"].as_str() { 39 | Some(in_avatar) => { 40 | avatar = Some(in_avatar.to_string()); 41 | } 42 | None => {} 43 | } 44 | 45 | let user = data::DiscordUserData { 46 | avatar: avatar, 47 | id: user_id.clone(), 48 | username: username, 49 | }; 50 | current_state.users.insert(user_id.clone(), user); 51 | let mut nick: Option = None; 52 | match voice_state["nick"].as_str() { 53 | Some(st) => { 54 | nick = Some(st.to_string()); 55 | } 56 | None => {} 57 | } 58 | match voice_state["voice_state"]["nick"].as_str() { 59 | Some(st) => { 60 | nick = Some(st.to_string()); 61 | } 62 | None => {} 63 | } 64 | let mut talking = false; 65 | if current_state.voice_states.contains_key(&user_id.clone()) { 66 | talking = current_state 67 | .voice_states 68 | .get(&user_id.clone()) 69 | .unwrap() 70 | .talking; 71 | } 72 | let vs = data::VoiceStateData { 73 | mute: voice_state["voice_state"]["mute"].as_bool().unwrap(), 74 | deaf: voice_state["voice_state"]["deaf"].as_bool().unwrap(), 75 | self_mute: voice_state["voice_state"]["self_mute"].as_bool().unwrap(), 76 | self_deaf: voice_state["voice_state"]["self_deaf"].as_bool().unwrap(), 77 | suppress: voice_state["voice_state"]["suppress"].as_bool().unwrap(), 78 | nick: nick, 79 | talking: talking, 80 | }; 81 | current_state.voice_states.insert(user_id, vs); 82 | } 83 | 84 | async fn update_state_from_voice_state_list( 85 | state: Arc>, 86 | voice_state_list: &Value, 87 | ) { 88 | for voice_state in voice_state_list.as_array().unwrap() { 89 | update_state_from_voice_state(state.clone(), voice_state).await; 90 | } 91 | } 92 | 93 | pub async fn connector( 94 | sender: Arc>>, 95 | recvr: Arc>>, 96 | ) { 97 | let state = Arc::new(Mutex::new(data::ConnState::new())); 98 | let debug_stdout = true; 99 | tokio::spawn(async move { 100 | loop { 101 | if debug_stdout { 102 | println!("Awaiting connection"); 103 | } 104 | let req = Request::builder() 105 | .uri("ws://127.0.0.1:6463/?v=1&client_id=207646673902501888") 106 | .method("GET") 107 | .header("Connection", "Upgrade") 108 | .header("Upgrade", "websocket") 109 | .header("Sec-WebSocket-Version", "13") 110 | .header("Sec-WebSocket-Key", generate_key()) 111 | .header("Host", "127.0.0.1") 112 | .header("Origin", "https://streamkit.discord.com") 113 | .body(()) 114 | .unwrap(); 115 | 116 | let ws_stream = match connect_async(req).await { 117 | Ok(a) => a.0, 118 | Err(_err) => { 119 | // Retry in 1 second. Probably make it less often? 120 | sleep(Duration::from_millis(1000)).await; 121 | continue; 122 | } 123 | }; 124 | if debug_stdout { 125 | println!("Connected to local Discord"); 126 | } 127 | let (write, read) = ws_stream.split(); 128 | let writer = Arc::new(Mutex::new(write)); 129 | 130 | // Message thread to writer 131 | { 132 | let recvr = recvr.clone(); 133 | let writer = writer.clone(); 134 | tokio::spawn(async move { 135 | while let Some(event) = recvr.clone().lock().await.next().await { 136 | //println!("{}", event); 137 | send_socket!(writer, [event]); 138 | } 139 | }); 140 | } 141 | 142 | read.for_each(|message| async { 143 | if message.is_err() { 144 | println!("Connection to Discord lost"); 145 | state.lock().await.clear(); 146 | return; 147 | } 148 | let copy_state = state.lock().await.clone(); 149 | let before_hash = data::calculate_hash(©_state); 150 | let message = message.unwrap(); 151 | let writer = writer.clone(); 152 | match message { 153 | tungstenite::Message::Text(raw_data) => { 154 | let data: Value = serde_json::from_str(&raw_data).unwrap(); 155 | // Data is a raw JSON object 156 | //println!("{}", raw_data); 157 | match data["cmd"].as_str().unwrap() { 158 | "AUTHORIZE" => { 159 | // Make HTTPS request to auth user 160 | let url = "https://streamkit.discord.com/overlay/token"; 161 | let obj = json!({"code": data["data"]["code"]}); 162 | let resp: serde_json::Value = reqwest::Client::new() 163 | .post(url) 164 | .json(&obj) 165 | .send() 166 | .await 167 | .unwrap() 168 | .json() 169 | .await 170 | .unwrap(); 171 | match resp.get("access_token") { 172 | Some(value) => { 173 | send_socket!(writer, packet_auth2!(value)); 174 | } 175 | None => { 176 | if debug_stdout { 177 | println!("No access token, failed to connect") 178 | } 179 | // TODO Reattempt connect 180 | } 181 | } 182 | } 183 | "AUTHENTICATE" => match data["data"].get("access_token") { 184 | None => { 185 | if debug_stdout { 186 | println!("Not authorized"); 187 | println!("{:?}", data); 188 | } 189 | } 190 | Some(_value) => { 191 | send_socket!(writer, packet_req_all_guilds!()); 192 | send_socket!(writer, packet_req_selected_voice!()); 193 | send_socket!(writer, packet_sub_server!()); 194 | match data["data"]["user"]["id"].as_str() { 195 | Some(value) => { 196 | state.lock().await.user_id = Some(value.to_string()); 197 | } 198 | None => { 199 | state.lock().await.user_id = None; 200 | } 201 | } 202 | } 203 | }, 204 | "GET_GUILDS" => {} 205 | "GET_SELECTED_VOICE_CHANNEL" => match data["data"].get("id") { 206 | Some(value) => { 207 | state.lock().await.voice_channel = 208 | Some(value.as_str().unwrap().parse().unwrap()); 209 | update_state_from_voice_state_list( 210 | state.clone(), 211 | &data["data"]["voice_states"], 212 | ) 213 | .await; 214 | send_socket!( 215 | writer, 216 | packet_sub_voice_channel!(value.as_str().unwrap()) 217 | ); 218 | } 219 | None => { 220 | user_left_channel(state.clone()).await; 221 | } 222 | }, 223 | "DISPATCH" => { 224 | match data["evt"].as_str().unwrap() { 225 | "READY" => { 226 | send_socket!(writer, packet_auth!("207646673902501888")); 227 | } 228 | "SPEAKING_START" => { 229 | let id = data["data"]["user_id"] 230 | .as_str() 231 | .unwrap() 232 | .to_string() 233 | .clone(); 234 | if state.lock().await.voice_channel.is_none() { 235 | send_socket!(writer, packet_req_selected_voice!()); 236 | } 237 | set_user_talking(state.clone(), id, true).await; 238 | } 239 | "SPEAKING_STOP" => { 240 | let id = data["data"]["user_id"] 241 | .as_str() 242 | .unwrap() 243 | .to_string() 244 | .clone(); 245 | set_user_talking(state.clone(), id, false).await; 246 | } 247 | "VOICE_STATE_DELETE" => { 248 | //println!("{:?}", data); 249 | let id = data["data"]["user"]["id"].clone(); 250 | let user_id = 251 | state.lock().await.user_id.as_ref().unwrap().clone(); 252 | if id == user_id { 253 | user_left_channel(state.clone()).await; 254 | } 255 | } 256 | "VOICE_STATE_CREATE" => { 257 | if state.lock().await.voice_channel.is_none() { 258 | send_socket!(writer, packet_req_selected_voice!()); 259 | } 260 | } 261 | "VOICE_STATE_UPDATE" => { 262 | let _id = data["data"]["user"]["id"].clone(); 263 | update_state_from_voice_state(state.clone(), &data["data"]) 264 | .await; 265 | } 266 | "VOICE_CHANNEL_SELECT" => { 267 | // User has manually chosen to join a room 268 | send_socket!(writer, packet_req_selected_voice!()); 269 | // Let's ask for more info 270 | } 271 | "VOICE_CONNECTION_STATUS" => { 272 | if debug_stdout { 273 | // TODO Potentially make this part of the conn state 274 | // But be aware that allowing this to change the state will 275 | // Cause the overlay to render every couple of seconds for no 276 | // effect 277 | println!("{}: {}", data["evt"], data["data"]["state"]); 278 | } 279 | } 280 | _ => { 281 | if debug_stdout { 282 | println!("{:?}", data); 283 | } 284 | } 285 | } 286 | } 287 | _ => { 288 | if debug_stdout { 289 | println!("{:?}", data); 290 | } 291 | } 292 | } 293 | let copy_state = state.lock().await.clone(); 294 | if before_hash != data::calculate_hash(©_state) { 295 | match sender.lock().await.try_send(copy_state) { 296 | Ok(_) => {} 297 | Err(_e) => {} 298 | } 299 | } 300 | } 301 | tungstenite::Message::Binary(_raw_data) => {} 302 | tungstenite::Message::Ping(_raw_data) => {} 303 | tungstenite::Message::Pong(_raw_data) => {} 304 | tungstenite::Message::Frame(_raw_data) => {} 305 | tungstenite::Message::Close(_raw_data) => { 306 | state.lock().await.clear(); 307 | } 308 | } 309 | }) 310 | .await; 311 | } 312 | }); 313 | } 314 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | // Send raw value over websocket 2 | #[macro_export] 3 | macro_rules! send_socket { 4 | ($writer: expr, $value: expr) => { 5 | for packet in $value.iter() { 6 | $writer 7 | .lock() 8 | .await 9 | .send(Message::Text(packet.to_string() + "\n")) 10 | .await 11 | .unwrap(); 12 | } 13 | }; 14 | } 15 | 16 | // Send raw value locally to socket thread 17 | #[macro_export] 18 | macro_rules! send_mpsc { 19 | ($writer: expr, $value: expr) => { 20 | for packet in $value.iter() { 21 | $writer 22 | .lock() 23 | .await 24 | .try_send(packet.to_string()) 25 | .expect("Unable to send packet"); 26 | } 27 | }; 28 | } 29 | 30 | // First packet to send. Explains what scopes the app will need 31 | #[macro_export] 32 | macro_rules! packet_auth{ 33 | ($auth_code: expr) => { 34 | [json!({ 35 | "cmd": "AUTHORIZE", 36 | "args": { 37 | "client_id": $auth_code, 38 | "scopes": ["rpc", "messages.read", "rpc.notifications.read"], 39 | "prompt": "none", 40 | }, 41 | "nonce": "deadbeef" 42 | })] 43 | } 44 | } 45 | 46 | // Second, with an access token to authenticate the app 47 | #[macro_export] 48 | macro_rules! packet_auth2{ 49 | ($token: expr) => { 50 | [json!({ 51 | "cmd": "AUTHENTICATE", 52 | "args": { 53 | "access_token": $token, 54 | }, 55 | "nonce": "deadbeef" 56 | })] 57 | } 58 | } 59 | 60 | // Request a list of all guilds the user is in 61 | #[macro_export] 62 | macro_rules! packet_req_all_guilds{ 63 | {} => { 64 | [json!({ 65 | "cmd": "GET_GUILDS", 66 | "args": { 67 | }, 68 | "nonce": "deadbeef" 69 | })] 70 | } 71 | } 72 | 73 | // Request information on the channel the user is currently in 74 | #[macro_export] 75 | macro_rules! packet_req_selected_voice{ 76 | {} => { 77 | [json!({ 78 | "cmd": "GET_SELECTED_VOICE_CHANNEL", 79 | "args": { 80 | }, 81 | "nonce": "deadbeef" 82 | })] 83 | } 84 | } 85 | 86 | // Subscribe to event callbacks 87 | #[macro_export] 88 | macro_rules! packet_sub{ 89 | {$event: expr, $args: expr, $nonce: expr} =>{ 90 | json!({ 91 | "cmd": "SUBSCRIBE", 92 | "args": $args, 93 | "evt": $event, 94 | "nonce": $nonce 95 | }) 96 | } 97 | } 98 | 99 | // Subscribe to server events 100 | #[macro_export] 101 | macro_rules! packet_sub_server{ 102 | {} => { 103 | [packet_sub!("VOICE_CHANNEL_SELECT", json!({}), "VOICE_CHANNEL_SELECT"), 104 | packet_sub!("VOICE_CONNECTION_STATUS", json!({}), "VOICE_CONNECTION_STATUS")] 105 | } 106 | } 107 | 108 | // Subscribe to a channel event 109 | #[macro_export] 110 | macro_rules! packet_sub_channel{ 111 | {$event: expr, $channel: expr} => { 112 | packet_sub!($event, json!({"channel_id":$channel}), $channel) 113 | } 114 | } 115 | 116 | // Subscribe to voice channel events 117 | #[macro_export] 118 | macro_rules! packet_sub_voice_channel{ 119 | {$channel: expr} => { 120 | [packet_sub_channel!("VOICE_STATE_CREATE", $channel), 121 | packet_sub_channel!("VOICE_STATE_UPDATE", $channel), 122 | packet_sub_channel!("VOICE_STATE_DELETE", $channel), 123 | packet_sub_channel!("SPEAKING_START", $channel), 124 | packet_sub_channel!("SPEAKING_STOP", $channel)] 125 | } 126 | } 127 | 128 | // Request information about audio devices 129 | #[macro_export] 130 | macro_rules! packet_req_devices{ 131 | {} =>{ 132 | [json!({ 133 | "cmd": "GET_VOICE_SETTINGS", 134 | "args": {}, 135 | "nonce": "deadbeef" 136 | })] 137 | } 138 | } 139 | 140 | // Request we move the user into the channel with the given ID 141 | #[macro_export] 142 | macro_rules! packet_set_channel{ 143 | {$channel: expr} => { 144 | [json!({ 145 | "cmd": "SELECT_VOICE_CHANNEL", 146 | "args": { 147 | "channel_id": $channel, 148 | "force": true 149 | }, 150 | "nonce": "deadbeef" 151 | })] 152 | } 153 | } 154 | 155 | // Request we change the users device setting (mute, deaf etc) 156 | #[macro_export] 157 | macro_rules! packet_set_devices{ 158 | {$dev: expr, $value: expr, $nonce: expr} =>{ 159 | [json!({ 160 | "cmd": "SET_VOICE_SETTINGS", 161 | "args": {$dev:$value}, 162 | "nonce": $nonce 163 | })] 164 | } 165 | } 166 | 167 | // Cairo helper 168 | #[macro_export] 169 | macro_rules! draw_overlay_gtk{ 170 | {$window: expr, $ctx: expr, $avatar_list:expr, $state: expr} => { 171 | let reg = Region::create(); 172 | reg.union_rectangle(& RectangleInt{ 173 | x: 0, 174 | y: 0, 175 | width:1, 176 | height:1 177 | }).expect("Failed to add rectangle"); 178 | // Config / Static 179 | let edge = 6.0; 180 | let line_height = 32.0; 181 | 182 | $ctx.set_antialias(Antialias::Good); 183 | $ctx.set_operator(Operator::Source); 184 | $ctx.set_source_rgba(1.0, 0.0, 0.0, 0.0); 185 | $ctx.paint().expect("Unable to paint window"); 186 | 187 | $ctx.select_font_face("Sans", FontSlant::Normal, FontWeight::Normal); 188 | $ctx.set_font_size(16.0); 189 | let state = $state.lock().unwrap().clone(); 190 | let mut y = 50.0; 191 | $ctx.set_operator(Operator::Over); 192 | 193 | if state.users.len() > 0 { 194 | for (key, user) in state.users { 195 | match state.voice_states.get(&key) { 196 | Some(voice_state) => { 197 | let mut name = user.username.clone(); 198 | match &voice_state.nick { 199 | Some(nick) => { 200 | name = nick.clone(); 201 | } 202 | None => {} 203 | } 204 | if voice_state.talking { 205 | $ctx.set_source_rgba(0.0, 0.4, 0.0, 0.6); 206 | } else { 207 | $ctx.set_source_rgba(0.0, 0.0, 0.0, 0.4); 208 | } 209 | let ext = $ctx.text_extents(&name).unwrap(); 210 | // Draw border around text 211 | $ctx.rectangle( 212 | line_height, 213 | y + (line_height / 2.0) - (ext.height / 2.0) - edge, 214 | ext.width + edge * 2.0, 215 | ext.height + edge * 2.0, 216 | ); 217 | $ctx.fill().expect("Unable to fill"); 218 | $ctx.move_to( 219 | line_height + edge, 220 | y + (line_height / 2.0) + (ext.height / 2.0), 221 | ); 222 | // Draw border into XShape 223 | reg.union_rectangle(& RectangleInt{ 224 | x:line_height as i32 , 225 | y:(y + (line_height / 2.0) - (ext.height / 2.0) - edge) as i32, 226 | width: (ext.width + edge * 2.0) as i32, 227 | height: (ext.height + edge * 2.0) as i32 228 | }).expect("Unable to add rectangle to XShape"); 229 | reg.union_rectangle(& RectangleInt{ 230 | x:0, 231 | y:y as i32 , 232 | width:line_height as i32, 233 | height:line_height as i32 234 | }).expect("Unable to add rectangle to XShape"); 235 | 236 | if voice_state.talking { 237 | $ctx.set_source_rgba(0.0, 1.0, 0.0, 1.0); 238 | } else { 239 | $ctx.set_source_rgba(1.0, 1.0, 1.0, 1.0); 240 | } 241 | $ctx.show_text(&name).expect("unable to draw text"); 242 | 243 | let avatar_list = $avatar_list.lock().unwrap(); 244 | match user.avatar{ 245 | Some(_avatar) => { 246 | match avatar_list.get(&user.id){ 247 | Some(img)=>{ 248 | match img{ 249 | Some(img) =>{ 250 | $ctx.save().expect("Unable to save cairo state"); 251 | $ctx.translate(0.0, y); 252 | $ctx.scale(line_height, line_height); 253 | $ctx.scale(1.0 / img.width() as f64, 1.0 / img.height() as f64); 254 | $ctx.set_source_surface(img,0.0,0.0).unwrap(); 255 | $ctx.rectangle(0.0,0.0,img.width() as f64, img.height() as f64); 256 | $ctx.fill().unwrap(); 257 | $ctx.restore().expect("Unable to restore cairo state"); 258 | } 259 | None => { 260 | println!("Avatar ready but None {}",user.id ); 261 | // Requested but no image (yet?) Don't draw anything more 262 | } 263 | } 264 | } 265 | None=>{ 266 | } 267 | } 268 | }, 269 | None=>{} 270 | } 271 | if voice_state.deaf || voice_state.self_deaf { 272 | draw_deaf($ctx, 0.0, y, line_height); 273 | } else if voice_state.mute || voice_state.self_mute { 274 | draw_mute($ctx, 0.0, y, line_height); 275 | } 276 | } 277 | None => {} 278 | } 279 | y += line_height; 280 | } 281 | } 282 | $window.shape_combine_region(Some(®)); 283 | } 284 | } 285 | 286 | // Cairo helper 287 | #[macro_export] 288 | macro_rules! draw_overlay{ 289 | {$ctx: expr, $avatar_list:expr, $state: expr} => { 290 | // Config / Static 291 | let edge = 6.0; 292 | let line_height = 32.0; 293 | 294 | $ctx.set_antialias(Antialias::Good); 295 | $ctx.set_operator(Operator::Source); 296 | $ctx.set_source_rgba(1.0, 0.0, 0.0, 0.0); 297 | $ctx.paint().expect("Unable to paint window"); 298 | 299 | $ctx.select_font_face("Sans", FontSlant::Normal, FontWeight::Normal); 300 | $ctx.set_font_size(16.0); 301 | let state = $state.clone(); 302 | let mut y = 50.0; 303 | $ctx.set_operator(Operator::Over); 304 | 305 | if state.users.len() > 0 { 306 | for (key, user) in state.users { 307 | match state.voice_states.get(&key) { 308 | Some(voice_state) => { 309 | let mut name = user.username.clone(); 310 | match &voice_state.nick { 311 | Some(nick) => { 312 | name = nick.clone(); 313 | } 314 | None => {} 315 | } 316 | if voice_state.talking { 317 | $ctx.set_source_rgba(0.0, 0.4, 0.0, 0.6); 318 | } else { 319 | $ctx.set_source_rgba(0.0, 0.0, 0.0, 0.4); 320 | } 321 | let ext = $ctx.text_extents(&name).unwrap(); 322 | // Draw border around text 323 | $ctx.rectangle( 324 | line_height, 325 | y + (line_height / 2.0) - (ext.height / 2.0) - edge, 326 | ext.width + edge * 2.0, 327 | ext.height + edge * 2.0, 328 | ); 329 | $ctx.fill().expect("Unable to fill"); 330 | $ctx.move_to( 331 | line_height + edge, 332 | y + (line_height / 2.0) + (ext.height / 2.0), 333 | ); 334 | 335 | if voice_state.talking { 336 | $ctx.set_source_rgba(0.0, 1.0, 0.0, 1.0); 337 | } else { 338 | $ctx.set_source_rgba(1.0, 1.0, 1.0, 1.0); 339 | } 340 | $ctx.show_text(&name).expect("unable to draw text"); 341 | 342 | let avatar_list = $avatar_list.lock().unwrap(); 343 | match user.avatar{ 344 | Some(_avatar) => { 345 | match avatar_list.get(&user.id){ 346 | Some(img)=>{ 347 | match img{ 348 | Some(img) =>{ 349 | $ctx.save().expect("Unable to save cairo state"); 350 | $ctx.translate(0.0, y); 351 | $ctx.scale(line_height, line_height); 352 | $ctx.scale(1.0 / img.width() as f64, 1.0 / img.height() as f64); 353 | $ctx.set_source_surface(img,0.0,0.0).unwrap(); 354 | $ctx.rectangle(0.0,0.0,img.width() as f64, img.height() as f64); 355 | $ctx.fill().unwrap(); 356 | $ctx.restore().expect("Unable to restore cairo state"); 357 | } 358 | None => { 359 | println!("Requested image but no data"); 360 | // Requested but no image (yet?) Don't draw anything more 361 | } 362 | } 363 | } 364 | None=>{ 365 | } 366 | } 367 | }, 368 | None=>{ 369 | println!("Error in userdata {:?}", user); 370 | } 371 | } 372 | if voice_state.deaf || voice_state.self_deaf { 373 | draw_deaf($ctx, 0.0, y, line_height); 374 | } else if voice_state.mute || voice_state.self_mute { 375 | draw_mute($ctx, 0.0, y, line_height); 376 | } 377 | } 378 | None => {} 379 | } 380 | y += line_height; 381 | } 382 | } 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /src/gamescope-main.rs: -------------------------------------------------------------------------------- 1 | extern crate cairo; 2 | extern crate cairo_sys; 3 | extern crate clap; 4 | extern crate serde_json; 5 | extern crate xcb; 6 | 7 | use cairo::{Antialias, Context, FillRule, FontSlant, FontWeight, ImageSurface, Operator}; 8 | use cairorender::DiscordAvatarRaw; 9 | use data::ConnState; 10 | use futures::lock::Mutex; 11 | use futures::stream::StreamExt; 12 | use futures_util::SinkExt; 13 | use std::collections::hash_map::HashMap; 14 | use std::f64::consts::PI; 15 | use std::io::Cursor; 16 | use std::sync::Arc; 17 | use tokio::select; 18 | use xcb::randr::Event::ScreenChangeNotify; 19 | use xcb::{x, Xid}; 20 | 21 | mod cairorender; 22 | mod core; 23 | mod data; 24 | mod macros; 25 | 26 | #[tokio::main] 27 | async fn main() { 28 | // Avatar to main thread 29 | let (mut avatar_request_sender, avatar_request_recv) = 30 | futures::channel::mpsc::channel::(10); 31 | 32 | // Mainthread to Avatar 33 | let (avatar_done_sender, mut avatar_done_recv) = 34 | futures::channel::mpsc::channel::(10); 35 | 36 | // Websocket events to main thread 37 | let (event_sender, mut event_recv) = futures::channel::mpsc::channel::(10); 38 | let event_sender: Arc>> = 39 | Arc::new(Mutex::new(event_sender)); 40 | 41 | // Main thread messages to Websocket output 42 | let (msg_sender, msg_recv) = futures::channel::mpsc::channel::(10); 43 | let _msg_sender = Arc::new(Mutex::new(msg_sender)); 44 | let msg_recv = Arc::new(Mutex::new(msg_recv)); 45 | 46 | // Start a thread for connection 47 | let connector_event_sender = event_sender.clone(); 48 | let connector_msg_recv = msg_recv.clone(); 49 | core::connector(connector_event_sender.clone(), connector_msg_recv.clone()).await; 50 | 51 | // Start a thread for avatars 52 | cairorender::avatar_downloader(avatar_done_sender, avatar_request_recv).await; 53 | 54 | // avatar surfaces 55 | let avatar_list: HashMap> = HashMap::new(); 56 | let avatar_list = Arc::new(std::sync::Mutex::new(avatar_list)); 57 | 58 | fn draw_deaf(ctx: &Context, pos_x: f64, pos_y: f64, size: f64) { 59 | ctx.save().expect("Could not save cairo state"); 60 | ctx.translate(pos_x, pos_y); 61 | ctx.scale(size, size); 62 | ctx.set_source_rgba(1.0, 0.0, 0.0, 1.0); 63 | 64 | ctx.save().expect("Could not save cairo state"); 65 | 66 | // Clip Strike-through 67 | ctx.set_fill_rule(FillRule::EvenOdd); 68 | ctx.set_line_width(0.1); 69 | ctx.move_to(0.0, 0.0); 70 | ctx.line_to(1.0, 0.0); 71 | ctx.line_to(1.0, 1.0); 72 | ctx.line_to(0.0, 1.0); 73 | ctx.line_to(0.0, 0.0); 74 | ctx.close_path(); 75 | ctx.new_sub_path(); 76 | ctx.arc(0.9, 0.1, 0.05, 1.25 * PI, 2.25 * PI); 77 | ctx.arc(0.1, 0.9, 0.05, 0.25 * PI, 1.25 * PI); 78 | ctx.close_path(); 79 | ctx.clip(); 80 | 81 | // Top band 82 | ctx.arc(0.5, 0.5, 0.2, 1.0 * PI, 0.0); 83 | ctx.stroke().expect("Could not stroke"); 84 | 85 | // Left band 86 | ctx.arc(0.28, 0.65, 0.075, 1.5 * PI, 0.5 * PI); 87 | ctx.move_to(0.3, 0.5); 88 | ctx.line_to(0.3, 0.75); 89 | ctx.stroke().expect("Could not stroke"); 90 | 91 | // Right band 92 | ctx.arc(0.72, 0.65, 0.075, 0.5 * PI, 1.5 * PI); 93 | ctx.move_to(0.7, 0.5); 94 | ctx.line_to(0.7, 0.75); 95 | ctx.stroke().expect("Could not stroke"); 96 | 97 | ctx.restore().expect("Could not restore cairo state"); 98 | // Strike through 99 | ctx.arc(0.7, 0.3, 0.035, 1.25 * PI, 2.25 * PI); 100 | ctx.arc(0.3, 0.7, 0.035, 0.25 * PI, 1.25 * PI); 101 | ctx.close_path(); 102 | ctx.fill().expect("Could not fill"); 103 | 104 | ctx.restore().expect("Could not restore"); 105 | } 106 | 107 | fn draw_mute(ctx: &Context, pos_x: f64, pos_y: f64, size: f64) { 108 | ctx.save().expect("Could not save cairo state"); 109 | ctx.translate(pos_x, pos_y); 110 | ctx.scale(size, size); 111 | ctx.set_source_rgba(1.0, 0.0, 0.0, 1.0); 112 | ctx.save().expect("Could not save cairo state"); 113 | // Clip Strike-through 114 | ctx.set_fill_rule(FillRule::EvenOdd); 115 | ctx.set_line_width(0.1); 116 | ctx.move_to(0.0, 0.0); 117 | ctx.line_to(1.0, 0.0); 118 | ctx.line_to(1.0, 1.0); 119 | ctx.line_to(0.0, 1.0); 120 | ctx.line_to(0.0, 0.0); 121 | ctx.close_path(); 122 | ctx.new_sub_path(); 123 | ctx.arc(0.9, 0.1, 0.05, 1.25 * PI, 2.25 * PI); 124 | ctx.arc(0.1, 0.9, 0.05, 0.25 * PI, 1.25 * PI); 125 | ctx.close_path(); 126 | ctx.clip(); 127 | // Center 128 | ctx.set_line_width(0.07); 129 | ctx.arc(0.5, 0.3, 0.1, PI, 2.0 * PI); 130 | ctx.arc(0.5, 0.5, 0.1, 0.0, PI); 131 | ctx.close_path(); 132 | ctx.fill().expect("Could not fill"); 133 | ctx.set_line_width(0.05); 134 | // Stand rounded 135 | ctx.arc(0.5, 0.5, 0.15, 0.0, 1.0 * PI); 136 | ctx.stroke().expect("Could not stroke"); 137 | // Stand vertical 138 | ctx.move_to(0.5, 0.65); 139 | ctx.line_to(0.5, 0.75); 140 | ctx.stroke().expect("Could not stroke"); 141 | // Stand horizontal 142 | ctx.move_to(0.35, 0.75); 143 | ctx.line_to(0.65, 0.75); 144 | ctx.stroke().expect("Could not stroke"); 145 | ctx.restore().expect("Coult not restore cairo state"); 146 | // Strike through 147 | ctx.arc(0.7, 0.3, 0.035, 1.25 * PI, 2.25 * PI); 148 | ctx.arc(0.3, 0.7, 0.035, 0.25 * PI, 1.25 * PI); 149 | ctx.close_path(); 150 | ctx.fill().expect("Could not fill"); 151 | ctx.restore().expect("Could not restore cairo state"); 152 | } 153 | 154 | // XCB Main 155 | 156 | let (conn, screen_num) = 157 | xcb::Connection::connect_with_extensions(None, &[xcb::Extension::RandR], &[]).unwrap(); 158 | let setup = conn.get_setup(); 159 | let screen = setup.roots().nth(screen_num as usize).unwrap(); 160 | 161 | let randr_data = xcb::randr::get_extension_data(&conn).expect("No RANDR"); 162 | 163 | println!("{:?}", randr_data); 164 | // Find RGBA visual 165 | let visualid = transparent_visual(screen).unwrap().visual_id(); 166 | // Create Colormap 167 | let colormap = conn.generate_id(); 168 | let cookie = conn.send_request_checked(&x::CreateColormap { 169 | alloc: x::ColormapAlloc::None, 170 | mid: colormap, 171 | window: screen.root(), 172 | visual: visualid, 173 | }); 174 | conn.check_request(cookie).expect("Error creating ColorMap"); 175 | conn.flush().expect("Error on flush"); 176 | let win = conn.generate_id(); 177 | conn.flush().expect("Error on flush"); 178 | 179 | // Create Window 180 | let cookie = conn.send_request_checked(&x::CreateWindow { 181 | depth: 32, 182 | wid: win, 183 | parent: screen.root(), 184 | x: 0, 185 | y: 0, 186 | width: 1280, 187 | height: 720, 188 | border_width: 1, 189 | class: x::WindowClass::InputOutput, 190 | visual: visualid, 191 | // this list must be in same order than `Cw` enum order 192 | value_list: &[ 193 | x::Cw::BackPixel(0), 194 | x::Cw::BorderPixel(0), 195 | x::Cw::EventMask(x::EventMask::EXPOSURE | x::EventMask::STRUCTURE_NOTIFY), 196 | x::Cw::Colormap(colormap), 197 | ], 198 | }); 199 | conn.flush().expect("Error on flush"); 200 | conn.check_request(cookie).expect("Error creating Window"); 201 | conn.flush().expect("Error on flush"); 202 | 203 | let mut window_width = 100; // Replaced on first Configure 204 | let mut window_height = 100; 205 | 206 | // Request XRandR 207 | let randr_cookie = conn.send_request(&xcb::randr::GetScreenResources { window: win }); 208 | let randr_reply = conn 209 | .wait_for_reply(randr_cookie) 210 | .expect("Unable to get displays"); 211 | 212 | for &crtc in randr_reply.crtcs() { 213 | let crtc_cookie = conn.send_request(&xcb::randr::GetCrtcInfo { 214 | crtc, 215 | config_timestamp: Default::default(), 216 | }); 217 | match conn.wait_for_reply(crtc_cookie) { 218 | Ok(display) => { 219 | if window_width < display.width() || window_height < display.height() { 220 | window_height = display.height(); 221 | window_width = display.width(); 222 | } 223 | } 224 | Err(_) => {} 225 | }; 226 | } 227 | 228 | // Request to be told of output changes 229 | let _callback_cookie = conn.send_request(&xcb::randr::SelectInput { 230 | window: win, 231 | enable: xcb::randr::NotifyMask::SCREEN_CHANGE, 232 | }); 233 | conn.flush().expect("Error on flush"); 234 | 235 | println!( 236 | "Starting window size : {} x {}", 237 | window_width, window_height 238 | ); 239 | 240 | conn.send_request(&x::ConfigureWindow { 241 | window: win, 242 | value_list: &[ 243 | x::ConfigWindow::Width(window_width as u32), 244 | x::ConfigWindow::Height(window_height as u32), 245 | ], 246 | }); 247 | 248 | // Show window 249 | conn.send_request(&x::MapWindow { window: win }); 250 | 251 | conn.flush().expect("Error on flush"); 252 | 253 | // Prepare atoms 254 | let atom_overlay = { 255 | let cookies = (conn.send_request(&x::InternAtom { 256 | only_if_exists: false, 257 | name: b"GAMESCOPE_EXTERNAL_OVERLAY", 258 | }),); 259 | conn.wait_for_reply(cookies.0).unwrap().atom() 260 | }; 261 | let mut state = ConnState::new(); 262 | loop { 263 | let xloop = async { conn.poll_for_event() }; 264 | let (xevent, threadevent, avatarevent) = select! { 265 | x = xloop => ( Some(x), None, None), 266 | x = event_recv.next() => (None,Some(x),None), 267 | x = avatar_done_recv.next() => (None, None, Some(x)), 268 | }; 269 | let mut sleep = 100; 270 | let mut redraw = false; 271 | match xevent { 272 | Some(event) => match event { 273 | Ok(Some(event)) => match event { 274 | xcb::Event::X(x::Event::Expose(_ev)) => { 275 | redraw = true; 276 | sleep = 0; 277 | } 278 | xcb::Event::X(x::Event::ClientMessage(_ev)) => {} 279 | xcb::Event::X(x::Event::ConfigureNotify(ev)) => { 280 | window_width = ev.width(); 281 | window_height = ev.height(); 282 | println!("Resized to {} x {}", window_width, window_height); 283 | redraw = true; 284 | sleep = 0; 285 | } 286 | xcb::Event::RandR(ScreenChangeNotify(ev)) => { 287 | println!("Screen change : {} {}", ev.width(), ev.height()); 288 | if ev.width() > 1 && ev.height() > 1 { 289 | conn.send_request(&x::ConfigureWindow { 290 | window: win, 291 | value_list: &[ 292 | x::ConfigWindow::Width(ev.width() as u32), 293 | x::ConfigWindow::Height(ev.height() as u32), 294 | ], 295 | }); 296 | } 297 | } 298 | _ => {} 299 | }, 300 | Ok(None) => {} 301 | Err(e) => { 302 | println!("XCB Error : {:?}", e) 303 | } 304 | }, 305 | None => {} 306 | } 307 | match threadevent { 308 | Some(event) => match event { 309 | Some(new_state) => { 310 | state.replace_self(new_state.clone()); 311 | match avatar_request_sender.send(new_state.clone()).await { 312 | Ok(_) => {} 313 | Err(e) => { 314 | println!("Could not send state to avatar thread : {:?}", e) 315 | } 316 | } 317 | redraw = true; 318 | sleep = 0; 319 | } 320 | None => {} 321 | }, 322 | None => {} 323 | } 324 | match avatarevent { 325 | Some(Some(avatardata)) => { 326 | match avatardata.raw { 327 | Some(raw) => { 328 | let surface = ImageSurface::create_from_png(&mut Cursor::new(raw)) 329 | .expect("Error processing user avatar"); 330 | avatar_list 331 | .lock() 332 | .unwrap() 333 | .insert(avatardata.key.clone(), Some(surface)); 334 | } 335 | None => { 336 | println!("Raw is None for user id {}", avatardata.key); 337 | avatar_list 338 | .lock() 339 | .unwrap() 340 | .insert(avatardata.key.clone(), None); 341 | } 342 | } 343 | 344 | redraw = true; 345 | sleep = 0; 346 | } 347 | Some(None) => {} 348 | None => {} 349 | } 350 | if redraw { 351 | let cr = create_cairo_context(&conn, &screen, &win, window_width, window_height); 352 | 353 | let should_show = state.users.len() > 0; 354 | set_as_overlay(&conn, &win, &atom_overlay, should_show); 355 | draw_overlay!(&cr, avatar_list, state); 356 | } 357 | if sleep > 0 { 358 | tokio::time::sleep(tokio::time::Duration::from_millis(sleep)).await; 359 | } 360 | conn.flush().expect("Flush error"); 361 | } 362 | } 363 | 364 | fn create_cairo_context( 365 | conn: &xcb::Connection, 366 | screen: &x::Screen, 367 | window: &x::Window, 368 | window_width: u16, 369 | window_height: u16, 370 | ) -> cairo::Context { 371 | let surface; 372 | unsafe { 373 | let cairo_conn = cairo::XCBConnection::from_raw_none( 374 | conn.get_raw_conn() as *mut cairo_sys::xcb_connection_t 375 | ); 376 | let mut visualtype = transparent_visual(screen).unwrap(); 377 | let visual_ptr: *mut cairo_sys::xcb_visualtype_t = 378 | &mut visualtype as *mut _ as *mut cairo_sys::xcb_visualtype_t; 379 | let visual = cairo::XCBVisualType::from_raw_none(visual_ptr); 380 | let cairo_screen = cairo::XCBDrawable(window.resource_id()); 381 | surface = cairo::XCBSurface::create( 382 | &cairo_conn, 383 | &cairo_screen, 384 | &visual, 385 | window_width as i32, 386 | window_height as i32, 387 | ) 388 | .unwrap(); 389 | } 390 | 391 | cairo::Context::new(&surface).unwrap() 392 | } 393 | 394 | fn transparent_visual(screen: &x::Screen) -> Option { 395 | for depth in screen.allowed_depths() { 396 | if depth.depth() == 32 { 397 | for visual in depth.visuals() { 398 | if visual.class() == xcb::x::VisualClass::TrueColor { 399 | return Some(*visual); 400 | } 401 | } 402 | } 403 | } 404 | None 405 | } 406 | 407 | fn set_as_overlay(conn: &xcb::Connection, win: &x::Window, atom: &x::Atom, enabled: bool) { 408 | let enabled: u32 = match enabled { 409 | false => 0, 410 | true => 1, 411 | }; 412 | conn.send_request(&x::ChangeProperty { 413 | mode: x::PropMode::Replace, 414 | window: *win, 415 | property: *atom, 416 | r#type: x::ATOM_CARDINAL, 417 | data: &[enabled as u32], 418 | }); 419 | conn.flush().expect("Error on flush"); 420 | } 421 | -------------------------------------------------------------------------------- /src/cosmic-main.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | extern crate serde_json; 3 | use crate::data::ConnState; 4 | use cairorender::DiscordAvatarRaw; 5 | use cosmic::iced::wayland::actions::layer_surface::SctkLayerSurfaceSettings; 6 | use cosmic::iced::wayland::actions::window::SctkWindowSettings; 7 | use cosmic::iced::widget::{column, container, image, row, text}; 8 | use cosmic::iced::window::Id; 9 | use cosmic::iced::{theme::Palette, Color, Theme}; 10 | use cosmic::iced::{Application, Element, Length}; 11 | use cosmic::{iced, iced::Subscription}; 12 | use data::calculate_hash; 13 | use futures::lock::Mutex; 14 | use futures::stream::StreamExt; 15 | use futures_channel::mpsc; 16 | use iced_sctk::commands::layer_surface::{Anchor, KeyboardInteractivity, Layer}; 17 | use iced_sctk::settings::InitialSurface; 18 | use std::cell::RefCell; 19 | use std::collections::HashMap; 20 | use std::sync::Arc; 21 | 22 | mod cairorender; 23 | mod core; 24 | mod data; 25 | mod macros; 26 | 27 | pub enum Location { 28 | Left, 29 | Right, 30 | } 31 | 32 | pub struct Preferences { 33 | location: Location, 34 | } 35 | 36 | pub struct App { 37 | height: f32, 38 | preferences: Preferences, 39 | state: ConnState, 40 | recv_state: RefCell>>, 41 | recv_avatar: RefCell>>, 42 | send_avatar: Arc>>, 43 | avatar_handler: Arc>>, 44 | } 45 | 46 | pub struct UiFlags { 47 | recv_state: mpsc::Receiver, 48 | recv_avatar: mpsc::Receiver, 49 | send_avatar: mpsc::Sender, 50 | } 51 | 52 | /// Messages that are used specifically by our [`App`]. 53 | #[derive(Clone, Debug)] 54 | pub enum Message { 55 | StateRecv(ConnState), 56 | AvatarRecv(DiscordAvatarRaw), 57 | } 58 | 59 | struct NormalStyle; 60 | struct TalkingStyle; 61 | struct MuteStyle; 62 | 63 | struct NormalImageStyle; 64 | struct TalkingImageStyle; 65 | struct MuteImageStyle; 66 | 67 | impl iced::widget::container::StyleSheet for NormalStyle { 68 | type Style = iced::Theme; 69 | 70 | fn appearance(&self, _style: &Self::Style) -> container::Appearance { 71 | let mut border = iced::Border::with_radius(5.0); 72 | border.color = iced::Color { 73 | r: 0.0, 74 | g: 0.0, 75 | b: 0.0, 76 | a: 0.0, 77 | }; 78 | container::Appearance { 79 | background: Some(iced::Background::Color(iced::Color { 80 | r: 0.0, 81 | g: 0.0, 82 | b: 0.0, 83 | a: 0.5, 84 | })), 85 | text_color: Some(Color::WHITE), 86 | border: border, 87 | ..Default::default() 88 | } 89 | } 90 | } 91 | impl iced::widget::container::StyleSheet for TalkingStyle { 92 | type Style = iced::Theme; 93 | 94 | fn appearance(&self, _style: &Self::Style) -> container::Appearance { 95 | let mut border = iced::Border::with_radius(5.0); 96 | border.color = iced::Color { 97 | r: 0.0, 98 | g: 0.3, 99 | b: 0.0, 100 | a: 0.6, 101 | }; 102 | container::Appearance { 103 | background: Some(iced::Background::Color(iced::Color { 104 | r: 0.0, 105 | g: 0.3, 106 | b: 0.0, 107 | a: 1.0, 108 | })), 109 | text_color: Some(Color::WHITE), 110 | border: border, 111 | ..Default::default() 112 | } 113 | } 114 | } 115 | impl iced::widget::container::StyleSheet for MuteStyle { 116 | type Style = iced::Theme; 117 | 118 | fn appearance(&self, _style: &Self::Style) -> container::Appearance { 119 | let mut border = iced::Border::with_radius(5.0); 120 | border.color = iced::Color { 121 | r: 0.3, 122 | g: 0.0, 123 | b: 0.0, 124 | a: 0.6, 125 | }; 126 | container::Appearance { 127 | background: Some(iced::Background::Color(iced::Color { 128 | r: 0.3, 129 | g: 0.0, 130 | b: 0.0, 131 | a: 1.0, 132 | })), 133 | text_color: Some(Color::WHITE), 134 | border: border, 135 | ..Default::default() 136 | } 137 | } 138 | } 139 | 140 | impl iced::widget::container::StyleSheet for NormalImageStyle { 141 | type Style = iced::Theme; 142 | 143 | fn appearance(&self, _style: &Self::Style) -> container::Appearance { 144 | let mut border = iced::Border::with_radius(32.0); 145 | border.color = iced::Color { 146 | r: 0.0, 147 | g: 0.0, 148 | b: 0.0, 149 | a: 0.0, 150 | }; 151 | border.width = 10.0; 152 | container::Appearance { 153 | background: Some(iced::Background::Color(iced::Color { 154 | r: 0.0, 155 | g: 0.0, 156 | b: 0.0, 157 | a: 0.0, 158 | })), 159 | text_color: Some(Color::WHITE), 160 | border: border, 161 | ..Default::default() 162 | } 163 | } 164 | } 165 | impl iced::widget::container::StyleSheet for TalkingImageStyle { 166 | type Style = iced::Theme; 167 | 168 | fn appearance(&self, _style: &Self::Style) -> container::Appearance { 169 | let mut border = iced::Border::with_radius(32.0); 170 | border.color = iced::Color { 171 | r: 0.0, 172 | g: 0.3, 173 | b: 0.0, 174 | a: 0.6, 175 | }; 176 | container::Appearance { 177 | background: Some(iced::Background::Color(iced::Color { 178 | r: 0.0, 179 | g: 0.3, 180 | b: 0.0, 181 | a: 1.0, 182 | })), 183 | text_color: Some(Color::WHITE), 184 | border: border, 185 | ..Default::default() 186 | } 187 | } 188 | } 189 | impl iced::widget::container::StyleSheet for MuteImageStyle { 190 | type Style = iced::Theme; 191 | 192 | fn appearance(&self, _style: &Self::Style) -> container::Appearance { 193 | let mut border = iced::Border::with_radius(32.0); 194 | border.color = iced::Color { 195 | r: 0.3, 196 | g: 0.0, 197 | b: 0.0, 198 | a: 0.6, 199 | }; 200 | container::Appearance { 201 | background: Some(iced::Background::Color(iced::Color { 202 | r: 0.3, 203 | g: 0.0, 204 | b: 0.0, 205 | a: 1.0, 206 | })), 207 | text_color: Some(Color::WHITE), 208 | border: border, 209 | ..Default::default() 210 | } 211 | } 212 | } 213 | 214 | impl Application for App { 215 | type Executor = iced::executor::Default; 216 | 217 | type Flags = UiFlags; 218 | 219 | type Message = Message; 220 | 221 | type Theme = Theme; 222 | 223 | fn update(&mut self, message: Message) -> iced::Command { 224 | match message { 225 | Message::StateRecv(msg) => { 226 | if calculate_hash(&msg) != calculate_hash(&self.state) { 227 | self.state = msg.clone(); 228 | match self.send_avatar.lock().unwrap().try_send(msg.clone()) { 229 | Ok(_) => {} 230 | Err(err) => { 231 | println!("Unable to send state to avatar thread: {}", err); 232 | } 233 | } 234 | } 235 | return iced::Command::none(); 236 | } 237 | Message::AvatarRecv(msg) => { 238 | match msg.raw { 239 | Some(bytes) => { 240 | let byte_image: image::Handle = image::Handle::from_memory(bytes); 241 | self.avatar_handler 242 | .lock() 243 | .unwrap() 244 | .insert(msg.key, byte_image); 245 | } 246 | None => {} 247 | }; 248 | } 249 | } 250 | let height = (self.state.users.len() as f32) * 64.0; 251 | if self.height != height { 252 | println!("Resizing {} > {}", self.height, height); 253 | self.height = height; 254 | iced::window::resize( 255 | iced::window::Id::MAIN, 256 | iced::Size { 257 | width: 200.0, 258 | height: height, 259 | }, 260 | ) 261 | } else { 262 | println!("No resize {}", height); 263 | iced::Command::none() 264 | } 265 | } 266 | 267 | /// Creates a view after each update. 268 | fn view(&self, _id: Id) -> Element { 269 | println!("Rerender"); 270 | let mut window_container = column([]); 271 | 272 | for (id, value) in self.state.users.iter() { 273 | let value = value.clone(); 274 | if let Some(voice_data) = self.state.voice_states.get(id) { 275 | let avatar_key = format!("{}/{}", id, value.avatar.unwrap()); 276 | let image_handle = match self.avatar_handler.lock().unwrap().get(&avatar_key) { 277 | Some(handle) => handle.clone(), 278 | None => { 279 | image::Handle::from_path("/home/triggerhapp/.local/share/icons/hicolor/256x256/apps/discover-overlay-tray.png") 280 | } 281 | }; 282 | 283 | let inner_image = Element::from( 284 | image::Image::::new(image_handle) 285 | .border_radius([32.0, 32.0, 32.0, 32.0]) 286 | .width(Length::Fixed(64.0)) 287 | .height(Length::Fixed(64.0)), 288 | ); 289 | let image = container(inner_image).style(if voice_data.talking { 290 | iced::theme::Container::Custom(Box::new(TalkingImageStyle)) 291 | } else { 292 | if voice_data.mute 293 | || voice_data.deaf 294 | || voice_data.self_deaf 295 | || voice_data.self_mute 296 | { 297 | iced::theme::Container::Custom(Box::new(MuteImageStyle)) 298 | } else { 299 | iced::theme::Container::Custom(Box::new(NormalImageStyle)) 300 | } 301 | }); 302 | let text = container( 303 | container(text( 304 | voice_data.nick.clone().unwrap_or(value.username.clone()), 305 | )) 306 | .padding(4) 307 | .width(Length::Shrink) 308 | .height(Length::Shrink) 309 | .style(if voice_data.talking { 310 | iced::theme::Container::Custom(Box::new(TalkingStyle)) 311 | } else { 312 | if voice_data.mute 313 | || voice_data.deaf 314 | || voice_data.self_mute 315 | || voice_data.self_deaf 316 | { 317 | iced::theme::Container::Custom(Box::new(MuteStyle)) 318 | } else { 319 | iced::theme::Container::Custom(Box::new(NormalStyle)) 320 | } 321 | }), 322 | ) 323 | .width(Length::Fill) 324 | .height(Length::Fixed(64.0)) 325 | .center_y() 326 | .align_x(match self.preferences.location { 327 | Location::Left => iced::alignment::Horizontal::Left, 328 | Location::Right => iced::alignment::Horizontal::Right, 329 | }); 330 | 331 | let row = match self.preferences.location { 332 | Location::Left => row([Element::from(image), Element::from(text)]), 333 | Location::Right => row([Element::from(text), Element::from(image)]), 334 | }; 335 | let row_cont = container(row) 336 | .height(Length::Shrink) 337 | .width(Length::Shrink) 338 | .height(Length::Shrink); 339 | window_container = window_container.push(row_cont); 340 | } 341 | } 342 | 343 | Element::from(window_container) 344 | } 345 | 346 | fn subscription(&self) -> Subscription { 347 | Subscription::batch([ 348 | iced::subscription::unfold( 349 | "connstate changes", 350 | self.recv_state.take(), 351 | move |mut receiver| async move { 352 | let new_state = receiver.as_mut().unwrap().next().await.unwrap().clone(); 353 | (Message::StateRecv(new_state), receiver) 354 | }, 355 | ), 356 | iced::subscription::unfold( 357 | "avatar changes", 358 | self.recv_avatar.take(), 359 | move |mut receiver| async move { 360 | let new_avatar_data = receiver.as_mut().unwrap().next().await.unwrap(); 361 | (Message::AvatarRecv(new_avatar_data), receiver) 362 | }, 363 | ), 364 | ]) 365 | } 366 | 367 | fn new(input: Self::Flags) -> (Self, iced::Command) { 368 | ( 369 | App { 370 | height: 0f32, 371 | preferences: Preferences { 372 | location: Location::Right, 373 | }, 374 | state: ConnState::new(), 375 | recv_state: RefCell::new(Some(input.recv_state)), 376 | recv_avatar: RefCell::new(Some(input.recv_avatar)), 377 | send_avatar: Arc::new(std::sync::Mutex::new(input.send_avatar)), 378 | avatar_handler: Arc::new(std::sync::Mutex::new(HashMap::new())), 379 | }, 380 | iced::Command::none(), 381 | ) 382 | } 383 | 384 | fn title(&self, _id: Id) -> String { 385 | "Discern".into() 386 | } 387 | 388 | fn theme(&self, _id: Id) -> Self::Theme { 389 | discern_theme() 390 | } 391 | } 392 | 393 | #[tokio::main] 394 | async fn main() { 395 | // Avatar to main thread 396 | let (avatar_request_sender, avatar_request_recv) = 397 | futures::channel::mpsc::channel::(10); 398 | 399 | // Mainthread to Avatar 400 | let (avatar_done_sender, avatar_done_recv) = 401 | futures::channel::mpsc::channel::(10); 402 | 403 | cairorender::avatar_downloader(avatar_done_sender, avatar_request_recv).await; 404 | 405 | // Websocket events to main thread 406 | let (event_sender, event_recv) = futures::channel::mpsc::channel::(10); 407 | let event_sender = Arc::new(Mutex::new(event_sender)); 408 | 409 | // Main thread messages to Websocket output 410 | let (msg_sender, msg_recv) = futures::channel::mpsc::channel::(10); 411 | let _msg_sender = Arc::new(Mutex::new(msg_sender)); 412 | let msg_recv = Arc::new(Mutex::new(msg_recv)); 413 | 414 | // Start a thread for connection 415 | let connector_event_sender = event_sender.clone(); 416 | let connector_msg_recv = msg_recv.clone(); 417 | core::connector(connector_event_sender.clone(), connector_msg_recv.clone()).await; 418 | 419 | let input = UiFlags { 420 | recv_state: event_recv, 421 | recv_avatar: avatar_done_recv, 422 | send_avatar: avatar_request_sender, 423 | }; 424 | 425 | let debug = false; 426 | 427 | let initial_window = if debug { 428 | InitialSurface::XdgWindow(SctkWindowSettings { 429 | window_id: Id::MAIN, 430 | app_id: Some("Discern".to_string()), 431 | title: Some("Discern".to_string()), 432 | parent: None, 433 | autosize: false, 434 | resizable: None, 435 | client_decorations: true, 436 | transparent: false, 437 | ..Default::default() 438 | }) 439 | } else { 440 | InitialSurface::LayerSurface(SctkLayerSurfaceSettings { 441 | id: Id::MAIN, 442 | keyboard_interactivity: KeyboardInteractivity::None, 443 | namespace: "Discern".into(), 444 | layer: Layer::Overlay, 445 | size: Some((Some(200), Some(200))), 446 | anchor: Anchor::RIGHT.union(Anchor::TOP), 447 | exclusive_zone: 0 as i32, 448 | ..Default::default() 449 | }) 450 | }; 451 | /* 452 | let settings = iced::Settings { 453 | id: None, 454 | initial_surface: InitialSurface::LayerSurface(SctkLayerSurfaceSettings { 455 | id: Id::MAIN, 456 | keyboard_interactivity: KeyboardInteractivity::None, 457 | namespace: "Discern".into(), 458 | layer: Layer::Overlay, 459 | size: Some((Some(200), Some(200))), 460 | anchor: Anchor::RIGHT.union(Anchor::TOP), 461 | exclusive_zone: 0 as i32, 462 | ..Default::default() 463 | }), 464 | flags: input, 465 | fonts: Default::default(), 466 | default_font: Default::default(), 467 | default_text_size: 14.into(), 468 | antialiasing: true, 469 | exit_on_close_request: true, 470 | }; 471 | */ 472 | let settings = iced::Settings { 473 | id: None, 474 | flags: input, 475 | fonts: Default::default(), 476 | default_font: Default::default(), 477 | default_text_size: 14.into(), 478 | antialiasing: true, 479 | exit_on_close_request: true, 480 | initial_surface: initial_window, 481 | }; 482 | match App::run(settings) { 483 | Ok(_) => {} 484 | Err(err) => { 485 | println!("Error : {:?}", err); 486 | } 487 | } 488 | } 489 | 490 | pub fn discern_theme() -> Theme { 491 | Theme::custom( 492 | "discern".into(), 493 | Palette { 494 | background: Color::from_rgba(0.0, 0.0, 0.0, 0.0), 495 | text: Color::from_rgba(0.0, 0.0, 0.0, 1.0), 496 | primary: Color::from_rgba(0.1, 0.5, 0.1, 1.0), 497 | success: Color::from_rgba(0.0, 1.0, 0.0, 1.0), 498 | danger: Color::from_rgba(1.0, 0.0, 0.0, 1.0), 499 | }, 500 | ) 501 | } 502 | --------------------------------------------------------------------------------