Browse Source

Initial functional version.

Kestrel 3 months ago
commit
5d65320212
5 changed files with 351 additions and 0 deletions
  1. 1 0
      .gitignore
  2. 10 0
      Cargo.toml
  3. 125 0
      src/act.rs
  4. 77 0
      src/main.rs
  5. 138 0
      src/sway.rs

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+/target

+ 10 - 0
Cargo.toml

@@ -0,0 +1,10 @@
+[package]
+name = "swaykey"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"

+ 125 - 0
src/act.rs

@@ -0,0 +1,125 @@
+use std::collections::HashMap;
+use std::fs::File;
+use std::cell::RefCell;
+
+use super::sway;
+
+#[derive(Default, serde::Serialize, serde::Deserialize)]
+pub struct SwayKeyStore {
+    pub last_act: Option<String>,
+    pub act_map: HashMap<String, String>,
+}
+
+impl SwayKeyStore {
+    pub fn load() -> Self {
+        let f = File::open(format!("{}/swaykey.json", std::env::var("XDG_RUNTIME_DIR").unwrap()));
+
+        if f.is_err() { return Self::default() }
+
+        serde_json::from_reader(f.unwrap()).expect("valid JSON in swaykey temp storage")
+    }
+    pub fn save(&self) {
+        let f = File::create(format!("{}/swaykey.json", std::env::var("XDG_RUNTIME_DIR").unwrap()));
+
+        serde_json::to_writer(f.unwrap(), self).expect("Successful serialization")
+    }
+}
+
+fn update_act_map(key: &ActivityWorkspace) {
+    let mut store = SwayKeyStore::load();
+    store.act_map.insert(
+        if key.group.as_ref().is_some() {
+            key.group.as_ref().unwrap().to_string()
+        } else {
+            "default".to_string()
+        },
+        key.index.to_string()
+    );
+    store.save();
+}
+
+#[derive(Clone)]
+pub struct ActivityWorkspace {
+    pub index: i32,
+    pub group: Option<String>
+}
+
+impl ActivityWorkspace {
+    pub fn from_string(s: &str) -> Self {
+        let sep = s.find(":");
+
+        let (index, group) = match sep {
+            None => {
+                (s, None)
+            },
+            Some(pos) => {
+                let (a, b) = s.split_at(pos);
+                (a, Some(b[1..].to_string()))
+            }
+        };
+
+        Self { index: index.parse().unwrap(), group }
+    }
+
+    pub fn to_string(&self) -> String {
+        format!("{}{}", self.index,
+            match &self.group {
+                None => "".to_string(),
+                Some(g) => format!(":{}", g)
+            })
+    }
+
+    pub fn set_index(&mut self, index: i32) {
+        self.index = index;
+    }
+
+    pub fn shift_index(&mut self, by: i32) {
+        self.index = ((self.index - 1 + by + 10) % 10) + 1;
+    }
+}
+
+pub struct Activity<'a> {
+    si: &'a sway::SwayInterface,
+    active: RefCell<ActivityWorkspace>
+}
+
+impl<'a> Activity<'a> {
+    pub fn current(si: &'a sway::SwayInterface) -> Self {
+        let current = si.get_current_workspace().unwrap();
+        Self { si, active: RefCell::new(ActivityWorkspace::from_string(&current)) }
+    }
+
+    pub fn by_name(si: &'a sway::SwayInterface, name: &str) -> Self {
+        let store = SwayKeyStore::load();
+        let one = "1".to_string();
+        let index = store.act_map.get(&name.to_string()).unwrap_or(&one);
+
+        let group = if name == "default" { None } else { Some(name.to_string()) };
+
+        Self { si, active: RefCell::new(ActivityWorkspace { index: index.parse().unwrap(), group }) }
+    }
+
+    pub fn group(&self) -> Option<String> {
+        self.active.borrow().group.clone()
+    }
+
+    pub fn shift_ws(&self, by: i32) {
+        self.active.borrow_mut().shift_index(by);
+        self.si.switch_to_workspace(self.active.borrow().to_string().as_str()).unwrap();
+
+        update_act_map(&self.active.borrow());
+    }
+
+    pub fn set_ws(&self, to: i32) {
+        self.active.borrow_mut().set_index(to);
+        self.si.switch_to_workspace(self.active.borrow().to_string().as_str()).unwrap();
+
+        update_act_map(&self.active.borrow());
+    }
+
+    pub fn move_to_ws(&self, to: i32) {
+        let mut k = self.active.borrow().clone();
+        k.set_index(to);
+        self.si.move_to_workspace(k.to_string().as_str()).unwrap();
+    }
+}

+ 77 - 0
src/main.rs

@@ -0,0 +1,77 @@
+use std::collections::HashMap;
+use std::collections::HashSet;
+
+mod sway;
+mod act;
+
+fn main() {
+    let mut cmd = std::env::args().skip(1);
+
+    let verb = cmd.next();
+    let noun = cmd.next();
+
+    let si = sway::SwayInterface::new();
+
+    let act = act::Activity::current(&si);
+
+    let mut cmds : HashMap<&'static str, Box<dyn Fn(Option<String>) -> ()>> = HashMap::new();
+
+    cmds.insert("ws_left", Box::new(|_| {
+        act.shift_ws(-1)
+    }));
+
+    cmds.insert("ws_right", Box::new(|_| {
+        act.shift_ws(1)
+    }));
+
+    cmds.insert("ws_set", Box::new(|index| {
+        let ind : Option<i32> = index.unwrap().parse().ok();
+        if ind.is_some() {
+            act.set_ws(ind.unwrap())
+        }
+    }));
+
+    cmds.insert("move_to_ws", Box::new(|index| {
+        let ind : Option<i32> = index.unwrap().parse().ok();
+        if ind.is_some() {
+            act.move_to_ws(ind.unwrap())
+        }
+    }));
+
+    cmds.insert("act_list", Box::new(|_| {
+        let uniques : HashSet<Option<String>> = si.get_workspaces().unwrap().iter().map(|ws| act::ActivityWorkspace::from_string(&ws.name).group).collect();
+        for u in uniques {
+            println!("{}", if u.is_some() { u.unwrap() } else { "default".to_string() });
+        }
+    }));
+
+    cmds.insert("act_change", Box::new(|to| {
+        let nact = act::Activity::by_name(&si, &to.unwrap());
+        nact.shift_ws(0);
+    }));
+
+    cmds.insert("act_restore", Box::new(|_| {
+        let mut store = act::SwayKeyStore::load();
+        let lact = store.last_act.as_deref().unwrap_or_else(|| "default");
+
+        let nact = act::Activity::by_name(&si, lact);
+        nact.shift_ws(0);
+
+        store.last_act = act.group();
+        store.save();
+    }));
+
+    if verb == None {
+        println!("Need a verb!");
+        return
+    }
+
+    let cmd = cmds.get(verb.as_ref().unwrap().as_str());
+
+    if cmd.is_none() {
+        println!("Unknown verb {}", verb.unwrap());
+    }
+    else {
+        (cmd.unwrap())(noun)
+    }
+}

+ 138 - 0
src/sway.rs

@@ -0,0 +1,138 @@
+use std::io::prelude::*;
+use std::os::unix::net::UnixStream;
+use std::cell::RefCell;
+use std::convert::AsMut;
+use serde::Deserialize;
+
+fn into_array<A, T>(slice: &[T]) -> A
+    where A: Sized + Default + AsMut<[T]>,
+          T: Clone
+{
+    let mut a = Default::default();
+    <A as AsMut<[T]>>::as_mut(&mut a).clone_from_slice(slice);
+    a
+}
+
+#[repr(u32)]
+#[allow(unused)]
+#[derive(Clone, Copy, Debug)]
+enum PacketType {
+    RunCommand = 0,
+    GetWorkspaces,
+    Subscribe,
+    GetOutputs,
+    GetTree,
+    GetMarks,
+    GetBarConfig,
+    GetVersion,
+    GetBindingModes,
+    GetConfig,
+    SendTick,
+    SyncMsg,
+    GetBindingState,
+    GetInputs = 100,
+    GetSeats
+}
+
+const HEADER_MAGIC: [u8;6] = ['i' as u8, '3' as u8, '-' as u8, 'i' as u8, 'p' as u8, 'c' as u8];
+
+#[repr(packed)]
+#[derive(Clone, Copy, Debug)]
+struct PacketHeader {
+    _magic: [u8;6],
+    len: u32,
+    _ptype: PacketType,
+}
+
+const PACKET_HEADER_LEN : usize = 14;
+
+impl PacketHeader {
+    fn buffer_with_data(self, data: &str) -> Vec<u8> {
+        let mut ret = Vec::new();
+        let self_data = &self as *const _ as *const u8;
+
+        ret.extend_from_slice(unsafe {
+            std::slice::from_raw_parts(self_data, core::mem::size_of::<Self>())
+        });
+
+        ret.extend_from_slice(data.as_bytes());
+
+        ret
+    }
+
+    fn parse(data: [u8; 14]) -> Self {
+        Self { _magic: into_array(&data[0..6]), len: unsafe { std::mem::transmute::<[u8;4], u32>(into_array(&data[6..10])) }, _ptype: unsafe { std::mem::transmute::<[u8;4], PacketType>(into_array(&data[10..14])) } }
+    }
+}
+
+pub struct SwayInterface {
+    sock: RefCell<UnixStream>
+}
+
+impl SwayInterface {
+    pub fn new() -> Self {
+        let path = std::env::var("SWAYSOCK").expect("SWAYSOCK to be set");
+
+        let sock = UnixStream::connect(path).expect("Can connect to sway socket");
+
+        SwayInterface { sock: RefCell::new(sock) }
+    }
+
+    fn send_packet(&self, ptype: PacketType, data: &str) -> Result<(), std::io::Error> {
+        let phdr = PacketHeader { _magic: HEADER_MAGIC, len: data.len() as u32, _ptype: ptype };
+        self.sock.borrow_mut().write(&phdr.buffer_with_data(data))?;
+        Ok(())
+    }
+
+    fn read_packet(&self) -> Result<serde_json::Value, std::io::Error> {
+        let mut header_data = [0u8; PACKET_HEADER_LEN];
+        self.sock.borrow_mut().read_exact(&mut header_data)?;
+
+        let header = PacketHeader::parse(header_data);
+
+        let mut body : Vec<u8> = Vec::new();
+        body.resize(header.len as usize, 0);
+        self.sock.borrow_mut().read_exact(&mut body)?;
+
+        let body = std::str::from_utf8(body.as_slice()).expect("Valid unicode");
+
+        let v: serde_json::Value = serde_json::from_str(body).expect("to get good JSON back from sway");
+
+        Ok(v)
+    }
+
+    pub fn switch_to_workspace(&self, name: &str) -> Result<(), std::io::Error> {
+        self.send_packet(PacketType::RunCommand, format!("workspace {}", name).as_str())?;
+        self.read_packet()?;
+        Ok(())
+    }
+
+    pub fn move_to_workspace(&self, name: &str) -> Result<(), std::io::Error> {
+        self.send_packet(PacketType::RunCommand, format!("move container to workspace {}", name).as_str())?;
+        self.read_packet()?;
+        Ok(())
+    }
+
+    pub fn get_workspaces(&self) -> Result<Vec<Workspace>, std::io::Error> {
+        self.send_packet(PacketType::GetWorkspaces, "")?;
+        let response = self.read_packet()?;
+        let ws : Vec<Workspace> = serde_json::from_value(response).expect("valid schema");
+
+        Ok(ws)
+    }
+
+    pub fn get_current_workspace(&self) -> Result<String, std::io::Error> {
+        let ws = self.get_workspaces()?;
+        for w in ws {
+            if w.focused { return Ok(w.name) }
+        }
+        unreachable!("At least one workspace will be focused");
+    }
+}
+
+#[derive(Deserialize)]
+pub struct Workspace {
+    id: u32,
+    pub name: String,
+    focused: bool
+}