random commit (please ignore)
This commit is contained in:
parent
65d2207a5f
commit
85c82e7504
|
@ -15,8 +15,14 @@
|
|||
|
||||
#![allow(dead_code, clippy::module_name_repetitions)]
|
||||
|
||||
use crate::msg_listener::Action;
|
||||
use crate::Result;
|
||||
use serde::Deserialize;
|
||||
use breadx::auto::xproto::KeyButMask;
|
||||
use serde::{
|
||||
de::{Deserializer, Visitor},
|
||||
Deserialize,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
|
@ -27,6 +33,9 @@ pub struct XcrabConfig {
|
|||
gap_size: Option<u16>,
|
||||
outer_gap_size: Option<u16>,
|
||||
pub msg: Option<XcrabMsgConfig>,
|
||||
#[allow(clippy::zero_sized_map_values)] // TODO: Action will be expanded in the future
|
||||
#[serde(default)]
|
||||
pub binds: HashMap<Keybind, Action>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
|
@ -48,6 +57,7 @@ impl Default for XcrabConfig {
|
|||
gap_size: Some(DEFAULT_GAP_SIZE),
|
||||
outer_gap_size: None,
|
||||
msg: Some(XcrabMsgConfig::default()),
|
||||
binds: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,3 +106,79 @@ pub fn load_file() -> Result<XcrabConfig> {
|
|||
fn get_home() -> Result<String> {
|
||||
Ok(std::env::var("HOME")?)
|
||||
}
|
||||
|
||||
struct ActionVisitor;
|
||||
impl<'de> Visitor<'de> for ActionVisitor {
|
||||
type Value = Action;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a valid WM action")
|
||||
}
|
||||
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> std::result::Result<Self::Value, E> {
|
||||
value.parse().map_err(|s| E::custom(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Action {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
|
||||
deserializer.deserialize_str(ActionVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct Keybind {
|
||||
pub key: char,
|
||||
pub mods: KeyButMask,
|
||||
}
|
||||
|
||||
struct KeybindVisitor;
|
||||
impl<'de> Visitor<'de> for KeybindVisitor {
|
||||
type Value = Keybind;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a keybind in the form of 'M-x'")
|
||||
}
|
||||
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> std::result::Result<Self::Value, E> {
|
||||
let mut mask = KeyButMask::default();
|
||||
|
||||
for part in value.split('-') {
|
||||
if part.len() > 1 {
|
||||
return Err(E::custom("parts may only contain one character"));
|
||||
}
|
||||
|
||||
let c = part
|
||||
.chars()
|
||||
.next()
|
||||
.ok_or_else(|| E::custom("parts must contain at least one character"))?;
|
||||
|
||||
if c.is_ascii_uppercase() {
|
||||
// FIXME: add more as required
|
||||
match c {
|
||||
'C' => mask.set_control(true),
|
||||
'S' => mask.set_shift(true),
|
||||
'A' => mask.set_mod1(true), // alt key
|
||||
'W' => mask.set_mod4(true), // super key, 'w' for windows because S is taken
|
||||
_ => return Err(E::custom(format!("no such modifier: {}", c))),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ignores extraneous keys
|
||||
let c = value
|
||||
.split('-')
|
||||
.flat_map(str::chars)
|
||||
.find(char::is_ascii_lowercase)
|
||||
.ok_or_else(|| E::custom("must specify one normal key"))?
|
||||
.to_ascii_uppercase();
|
||||
|
||||
Ok(Keybind { key: c, mods: mask })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Keybind {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
|
||||
deserializer.deserialize_str(KeybindVisitor)
|
||||
}
|
||||
}
|
||||
|
|
2107
src/keysymdef.rs
Normal file
2107
src/keysymdef.rs
Normal file
File diff suppressed because it is too large
Load diff
27
src/main.rs
27
src/main.rs
|
@ -28,12 +28,16 @@ use breadx::{
|
|||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
mod config;
|
||||
mod msg_listener;
|
||||
mod x11;
|
||||
|
||||
mod keysymdef;
|
||||
|
||||
use x11::client::{may_not_exist, XcrabWindowManager};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
@ -140,19 +144,20 @@ async fn main() -> Result<()> {
|
|||
conn.ungrab_server_async().await?;
|
||||
|
||||
let (send, mut recv) = unbounded_channel();
|
||||
let (result_send, result_recv) = mpsc::unbounded_channel();
|
||||
|
||||
tokio::spawn(msg_listener::listener_task(
|
||||
CONFIG.msg.clone().unwrap_or_default().socket_path,
|
||||
send,
|
||||
result_recv,
|
||||
));
|
||||
|
||||
let mask = ModMask {
|
||||
inner: 20
|
||||
};
|
||||
let mask = ModMask { inner: 20 };
|
||||
let keymap = keymap(&mut conn).await.unwrap_or_default();
|
||||
let key_u32 = u32::from_str_radix("0078", 16).unwrap_or_default();
|
||||
let key: &u8 = keymap
|
||||
.get(&key_u32).ok_or(XcrabError::Custom("the keysym requested was not found in the keymap".to_string()))?;
|
||||
let key: &u8 = keymap.get(&key_u32).ok_or_else(|| {
|
||||
XcrabError::Custom("the keysym requested was not found in the keymap".to_string())
|
||||
})?;
|
||||
|
||||
conn.exchange_request_async(GrabKeyRequest {
|
||||
req_type: 33,
|
||||
|
@ -166,13 +171,15 @@ async fn main() -> Result<()> {
|
|||
})
|
||||
.await?;
|
||||
|
||||
// let mut keyboard_state = KeyboardState::new_async(&mut conn).await?;
|
||||
|
||||
loop {
|
||||
// biased mode makes select! poll the channel first in order to keep xcrab-msg from being
|
||||
// starved by x11 events. Probably unnecessary, but better safe than sorry.
|
||||
tokio::select! {
|
||||
biased;
|
||||
Some(s) = recv.recv() => msg_listener::on_recv(s, &mut manager, &mut conn).await?,
|
||||
Ok(ev) = conn.wait_for_event_async() => process_event(ev, &mut manager, &mut conn, root, &keymap).await?,
|
||||
Some(s) = recv.recv() => msg_listener::on_recv(s, &mut manager, &mut conn, &result_send).await?,
|
||||
Ok(ev) = conn.wait_for_event_async() => process_event(ev, &mut manager, &mut conn, root, &keymap,/* &mut keyboard_state */).await?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -183,6 +190,7 @@ async fn process_event<Dpy: AsyncDisplay + ?Sized>(
|
|||
conn: &mut Dpy,
|
||||
root: Window,
|
||||
keymap: &HashMap<Keysym, Keycode>,
|
||||
// keyboard_state: &mut KeyboardState,
|
||||
) -> Result<()> {
|
||||
match ev {
|
||||
Event::MapRequest(ev) => {
|
||||
|
@ -225,8 +233,9 @@ async fn process_event<Dpy: AsyncDisplay + ?Sized>(
|
|||
}
|
||||
}
|
||||
Event::KeyPress(ev) => {
|
||||
let key_u32 = u32::from_str_radix("0078", 16).unwrap_or_default();
|
||||
let key = keymap.get(&key_u32).unwrap_or(&u8::MIN);
|
||||
let keysym = u32::from_str_radix("0078", 16).unwrap_or_default();
|
||||
let key = keymap.get(&keysym).unwrap_or(&u8::MIN);
|
||||
|
||||
if ev.detail == *key {
|
||||
manager.destroy_focused_client(conn).await?;
|
||||
}
|
||||
|
|
|
@ -1,33 +1,28 @@
|
|||
// Copyright (C) 2022 Infoshock Tech
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::Result;
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
// Dummy struct for deserializing the message config - we're using the same file for both binaries
|
||||
pub struct XcrabConfig {
|
||||
pub msg: XcrabMsgConfig,
|
||||
}
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct XcrabMsgConfig {
|
||||
pub socket_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for XcrabMsgConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
@ -35,21 +30,15 @@ impl Default for XcrabMsgConfig {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_file_inner() -> Result<XcrabConfig> {
|
||||
let home_dir = get_home();
|
||||
|
||||
let contents = std::fs::read_to_string(format!("{}/.config/xcrab/config.toml", home_dir))?;
|
||||
|
||||
let config: XcrabConfig = toml::from_str(&contents)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn load_file() -> XcrabConfig {
|
||||
load_file_inner().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_home() -> String {
|
||||
std::env::var("HOME").expect("Error: $HOME variable was not set")
|
||||
}
|
||||
|
|
|
@ -1,36 +1,47 @@
|
|||
// Copyright (C) 2022 Infoshock Tech
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#![warn(clippy::pedantic)]
|
||||
mod config;
|
||||
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
struct CustomError(String);
|
||||
impl Debug for CustomError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
impl Display for CustomError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
impl Error for CustomError {}
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let msg = std::env::args().skip(1).collect::<Vec<String>>().join(" ");
|
||||
|
||||
let conf = config::load_file();
|
||||
|
||||
let path = conf.msg.socket_path;
|
||||
|
||||
let mut stream = UnixStream::connect(path).await?;
|
||||
|
||||
stream.write_all(msg.as_bytes()).await?;
|
||||
|
||||
let stream = UnixStream::connect(path).await?;
|
||||
let (mut read, mut write) = stream.into_split();
|
||||
write.write_all(msg.as_bytes()).await?;
|
||||
drop(write); // Shutdown the writer half so that the write actually goes through
|
||||
// "Don't cross the streams!""
|
||||
let mut buf = String::new();
|
||||
read.read_to_string(&mut buf).await?;
|
||||
if !buf.is_empty() {
|
||||
return Err(CustomError(buf).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -17,9 +17,10 @@ use crate::x11::client::XcrabWindowManager;
|
|||
use crate::Result;
|
||||
use breadx::AsyncDisplay;
|
||||
use std::path::Path;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use std::str::FromStr;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::UnixListener;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
|
||||
macro_rules! unwrap_or_continue {
|
||||
($e:expr) => {
|
||||
|
@ -34,6 +35,7 @@ macro_rules! unwrap_or_continue {
|
|||
pub async fn listener_task<P: AsRef<Path>>(
|
||||
socket_path: P,
|
||||
sender: UnboundedSender<String>,
|
||||
mut result_recv: UnboundedReceiver<Result<()>>,
|
||||
) -> Result<()> {
|
||||
let socket_path = socket_path.as_ref();
|
||||
if socket_path.exists() {
|
||||
|
@ -47,6 +49,13 @@ pub async fn listener_task<P: AsRef<Path>>(
|
|||
stream.read_to_string(&mut buf).await?;
|
||||
|
||||
drop(sender.send(buf)); // go back to ms word clippy
|
||||
|
||||
// we can unwrap here because if the channel is closed then something's not right
|
||||
if let Err(e) = result_recv.recv().await.unwrap() {
|
||||
stream.write_all(format!("{}", e).as_bytes()).await?;
|
||||
} else {
|
||||
stream.write_all(&[]).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,10 +63,76 @@ pub async fn on_recv<Dpy: AsyncDisplay + ?Sized>(
|
|||
data: String,
|
||||
manager: &mut XcrabWindowManager,
|
||||
conn: &mut Dpy,
|
||||
result_sender: &UnboundedSender<Result<()>>,
|
||||
) -> Result<()> {
|
||||
match &*data {
|
||||
"close" => manager.destroy_focused_client(conn).await?,
|
||||
_ => println!("{}", data),
|
||||
let res = { data.parse::<Action>() };
|
||||
|
||||
if let Ok(ref a) = res {
|
||||
a.eval(manager, conn).await?; // Don't send these errors over the channel, because they're
|
||||
// xcrab errors, not msg errors
|
||||
}
|
||||
|
||||
drop(result_sender.send(res.map(|_| ())));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum Action {
|
||||
Close,
|
||||
}
|
||||
|
||||
impl FromStr for Action {
|
||||
type Err = crate::XcrabError;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use Action::*;
|
||||
let parts: Vec<String> = s
|
||||
.split(' ')
|
||||
.map(str::to_ascii_lowercase)
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
if parts.is_empty() {
|
||||
return Err(String::from("No action provided").into());
|
||||
}
|
||||
|
||||
macro_rules! eq_ignore_ascii_case_match {
|
||||
(($scrutinee:expr) { $($s:literal => $v:expr,)+ else => $else:expr $(,)? }) => {
|
||||
$(
|
||||
if $scrutinee.eq_ignore_ascii_case($s) {
|
||||
$v
|
||||
} else
|
||||
)+ {
|
||||
$else
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: When more actions are added (such as focus etc), they will take arguments. In that
|
||||
// case, they will get passed the rest of `parts`.
|
||||
eq_ignore_ascii_case_match!((parts[0]) {
|
||||
"close" => Ok(Close),
|
||||
else => Err(format!("Unknown action: {}", s).into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub async fn eval<Dpy: AsyncDisplay + ?Sized>(
|
||||
&self,
|
||||
manager: &mut XcrabWindowManager,
|
||||
conn: &mut Dpy,
|
||||
) -> Result<()> {
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use Action::*;
|
||||
|
||||
match self {
|
||||
Close => manager.destroy_focused_client(conn).await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -137,14 +137,12 @@ enum RectangleContents {
|
|||
Client(Client),
|
||||
}
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
#[derive(Debug, Clone)]
|
||||
struct Pane {
|
||||
children: Vec<XcrabKey>,
|
||||
directionality: Directionality,
|
||||
}
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Client {
|
||||
frame: FramedWindow,
|
||||
|
@ -244,7 +242,10 @@ impl XcrabWindowManager {
|
|||
};
|
||||
|
||||
if let Some(focus) = self.focused {
|
||||
req.focus = focus;
|
||||
let focused_key = self.clients.get(&focus).unwrap();
|
||||
let focused = self.rects.get(*focused_key).unwrap();
|
||||
let focused_frame = focused.unwrap_client().frame;
|
||||
req.focus = focused_frame.input;
|
||||
}
|
||||
|
||||
conn.exchange_request_async(req).await?;
|
||||
|
@ -610,6 +611,17 @@ impl XcrabWindowManager {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_focused(&self) -> Option<Window> {
|
||||
self.focused
|
||||
}
|
||||
|
||||
pub async fn get_framed_window(&self, window: Window) -> FramedWindow {
|
||||
let focused_key = self.clients.get(&window).unwrap();
|
||||
let focused = self.rects.get(*focused_key).unwrap();
|
||||
let focused_frame = focused.unwrap_client().frame;
|
||||
focused_frame
|
||||
}
|
||||
}
|
||||
|
||||
pub fn may_not_exist(res: breadx::Result) -> breadx::Result {
|
||||
|
@ -625,9 +637,10 @@ pub fn may_not_exist(res: breadx::Result) -> breadx::Result {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct FramedWindow {
|
||||
frame: Window,
|
||||
win: Window,
|
||||
pub struct FramedWindow {
|
||||
pub frame: Window,
|
||||
pub win: Window,
|
||||
pub input: Window,
|
||||
}
|
||||
|
||||
impl FramedWindow {
|
||||
|
@ -687,12 +700,27 @@ impl FramedWindow {
|
|||
.await,
|
||||
)?;
|
||||
|
||||
self.input
|
||||
.configure_async(
|
||||
conn,
|
||||
ConfigureWindowParameters {
|
||||
x: Some(0),
|
||||
y: Some(0),
|
||||
width,
|
||||
height,
|
||||
border_width: None,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn map<Dpy: AsyncDisplay + ?Sized>(self, conn: &mut Dpy) -> Result<()> {
|
||||
may_not_exist(self.win.map_async(conn).await)?;
|
||||
self.frame.map_async(conn).await?;
|
||||
self.input.map_async(conn).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -708,6 +736,10 @@ impl FramedWindow {
|
|||
// no longer related to us, remove from save set
|
||||
may_not_exist(self.win.change_save_set_async(conn, SetMode::Delete).await)?;
|
||||
|
||||
self.input.unmap_async(conn).await?;
|
||||
|
||||
self.input.free_async(conn).await?;
|
||||
|
||||
self.frame.free_async(conn).await?;
|
||||
|
||||
Ok(())
|
||||
|
@ -819,6 +851,21 @@ async fn frame<Dpy: AsyncDisplay + ?Sized>(conn: &mut Dpy, win: Window) -> Resul
|
|||
)
|
||||
.await?;
|
||||
|
||||
let input = conn
|
||||
.create_window_async(
|
||||
frame,
|
||||
breadx::WindowClass::InputOnly,
|
||||
None,
|
||||
Some(conn.default_visual_id()),
|
||||
0,
|
||||
0,
|
||||
geometry.width,
|
||||
geometry.height,
|
||||
0,
|
||||
WindowParameters::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
frame
|
||||
.set_event_mask_async(
|
||||
conn,
|
||||
|
@ -829,9 +876,21 @@ async fn frame<Dpy: AsyncDisplay + ?Sized>(conn: &mut Dpy, win: Window) -> Resul
|
|||
win.set_event_mask_async(conn, EventMask::BUTTON_PRESS)
|
||||
.await?;
|
||||
|
||||
input
|
||||
.set_event_mask_async(
|
||||
conn,
|
||||
EventMask::BUTTON_PRESS
|
||||
| EventMask::BUTTON_RELEASE
|
||||
| EventMask::KEY_PRESS
|
||||
| EventMask::KEY_RELEASE
|
||||
| EventMask::ENTER_WINDOW
|
||||
| EventMask::LEAVE_WINDOW,
|
||||
)
|
||||
.await?;
|
||||
|
||||
may_not_exist(win.change_save_set_async(conn, SetMode::Insert).await)?;
|
||||
|
||||
may_not_exist(win.reparent_async(conn, frame, 0, 0).await)?;
|
||||
|
||||
Ok(FramedWindow { frame, win })
|
||||
Ok(FramedWindow { frame, win, input })
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue