revert to 33c1cc80b6 "make destroying focused client actually work"

This commit is contained in:
Akshat Deshpande 2022-07-25 19:36:34 -05:00
parent 33c1cc80b6
commit 044c492373
8 changed files with 267 additions and 30 deletions

1
Cargo.lock generated
View file

@ -604,6 +604,7 @@ name = "xcrab"
version = "0.1.0"
dependencies = [
"breadx",
"gluten-keyboard",
"lazy_static",
"serde",
"slotmap",

View file

@ -10,6 +10,7 @@ toml = "0.5.9"
serde = { version = "1.0.137", features = ["derive"]}
lazy_static = "1.4.0"
slotmap = "1.0.6"
gluten-keyboard = "0.1.2"
[[bin]]
name = "xcrab-msg"

View file

@ -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)
}
}

View file

@ -16,9 +16,9 @@
#![warn(clippy::pedantic)]
use std::fmt::{Debug, Display};
use std::ops::Deref;
use breadx::{
keyboard::KeyboardState,
prelude::{AsyncDisplay, AsyncDisplayXprotoExt, MapState},
traits::DisplayBase,
AsyncDisplayConnection, AsyncDisplayExt, BreadError, ConfigureWindowParameters, Event,
@ -27,7 +27,7 @@ use breadx::{
use lazy_static::lazy_static;
use tokio::sync::mpsc::unbounded_channel;
use tokio::sync::mpsc;
mod config;
mod msg_listener;
@ -42,6 +42,7 @@ pub enum XcrabError {
Toml(toml::de::Error),
Var(std::env::VarError),
ClientDoesntExist,
Custom(String),
}
impl From<BreadError> for XcrabError {
@ -68,19 +69,31 @@ impl From<std::env::VarError> for XcrabError {
}
}
impl From<String> for XcrabError {
fn from(v: String) -> Self {
Self::Custom(v)
}
}
lazy_static! {
pub static ref CONFIG: config::XcrabConfig = config::load_file().unwrap_or_default();
// pub static ref CONFIG: config::XcrabConfig = config::load_file().unwrap_or_default();
pub static ref CONFIG: config::XcrabConfig = config::load_file().unwrap_or_else(|e| {
println!("[CONFIG] Error parsing config: {}", e);
println!("[CONFIG] Falling back to default config");
config::XcrabConfig::default()
});
}
impl Display for XcrabError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Bread(be) => Display::fmt(&be, f)?,
Self::Io(ie) => Display::fmt(&ie, f)?,
Self::Toml(te) => Display::fmt(&te, f)?,
Self::Var(ve) => Display::fmt(&ve, f)?,
Self::Bread(be) => Display::fmt(be, f)?,
Self::Io(ie) => Display::fmt(ie, f)?,
Self::Toml(te) => Display::fmt(te, f)?,
Self::Var(ve) => Display::fmt(ve, f)?,
Self::Custom(fe) => Display::fmt(fe, f)?,
Self::ClientDoesntExist => Display::fmt("client didn't exist", f)?,
};
}
Ok(())
}
@ -124,29 +137,40 @@ async fn main() -> Result<()> {
conn.ungrab_server_async().await?;
let (send, mut recv) = unbounded_channel();
let (send, mut recv) = mpsc::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 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).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,
&mut keyboard_state,
).await?,
}
}
}
#[allow(clippy::too_many_lines)] // FIXME: missing help i have no idea how to make this shorter
async fn process_event<Dpy: AsyncDisplay + ?Sized>(
ev: Event,
manager: &mut XcrabWindowManager,
conn: &mut Dpy,
root: Window,
keyboard_state: &mut KeyboardState,
) -> Result<()> {
match ev {
Event::MapRequest(ev) => {
@ -183,11 +207,17 @@ async fn process_event<Dpy: AsyncDisplay + ?Sized>(
manager.remove_client(conn, ev.window).await?;
}
}
Event::ButtonPress(ev) => {
dbg!(&ev);
if ev.detail == 1 {
manager.set_focus(conn, ev.event).await?;
Event::KeyPress(ev) => {
if let Some(k) = keyboard_state.process_keycode(ev.detail, ev.state) {
if let Some(c) = k.as_char() {
for (&bind, action) in &CONFIG.binds {
if bind.key == c && bind.mods == ev.state {
action.eval(manager, conn).await?;
}
}
}
}
dbg!(ev);
}
_ => {}
}

View file

@ -17,12 +17,14 @@ 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,

View file

@ -13,12 +13,32 @@
// 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<()> {
@ -28,9 +48,20 @@ async fn main() -> Result<()> {
let path = conf.msg.socket_path;
let mut stream = UnixStream::connect(path).await?;
let stream = UnixStream::connect(path).await?;
let (mut read, mut write) = stream.into_split();
stream.write_all(msg.as_bytes()).await?;
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(())
}

View file

@ -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(())
}
}

View file

@ -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,
@ -235,7 +233,7 @@ impl XcrabWindowManager {
// unfortunately, i cannot find a method on `conn` to set the focus.
// https://www.x.org/releases/current/doc/xproto/x11protocol.html#Encoding::Requests
let mut req = SetInputFocusRequest {
let req = SetInputFocusRequest {
req_type: 42, // constant, specified in x protocol docs.
revert_to: InputFocus::None,
length: 3, // constant, specified in x protocol docs.
@ -244,7 +242,9 @@ 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;
}
conn.exchange_request_async(req).await?;
@ -610,6 +610,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,7 +636,7 @@ pub fn may_not_exist(res: breadx::Result) -> breadx::Result {
}
#[derive(Debug, Clone, Copy)]
struct FramedWindow {
pub struct FramedWindow {
frame: Window,
win: Window,
}