Add switch-workspace subcommand

timeout_old
Tassilo Horn 3 years ago
parent 8530b78d97
commit 6b872e2237
  1. 8
      README.md
  2. 3
      src/bin/swayr.rs
  3. 8
      src/bin/swayrd.rs
  4. 56
      src/client.rs
  5. 177
      src/con.rs
  6. 94
      src/demon.rs
  7. 35
      src/ipc.rs

@ -5,9 +5,11 @@ creations, deletions, and focus changes using sway's JSON IPC interface. The
client `swayr` offers subcommands, see `swayr --help`. client `swayr` offers subcommands, see `swayr --help`.
Right now, there are these subcommands: Right now, there are these subcommands:
* `switch-window`: a wofi-based window switcher sorting the windows in the * `switch-window` displays all windows in the order urgent first, then LRU,
order urgent first, then LRU, focused last. focused last and focuses the selected.
* `quit-window`: displays all windows using wofi and quits the selected one. * `quit-window` displays all windows and quits the selected one.
* `switch-workspace` displays all workspaces in LRU order and switches to the
selected.
Swayr is licensed under the Swayr is licensed under the
[GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html) (or later). [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html) (or later).

@ -22,6 +22,8 @@ enum SwayrCommand {
SwitchWindow, SwitchWindow,
/// Quit a window with display order focused first, then reverse-LRU order, urgent last /// Quit a window with display order focused first, then reverse-LRU order, urgent last
QuitWindow, QuitWindow,
/// Switch workspace with LRU display order
SwitchWorkspace,
} }
fn main() { fn main() {
@ -29,5 +31,6 @@ fn main() {
match opts.command { match opts.command {
SwayrCommand::SwitchWindow => client::switch_window(), SwayrCommand::SwitchWindow => client::switch_window(),
SwayrCommand::QuitWindow => client::quit_window(), SwayrCommand::QuitWindow => client::quit_window(),
SwayrCommand::SwitchWorkspace => client::switch_workspace(),
} }
} }

@ -9,15 +9,15 @@ use swayr::demon;
use swayr::ipc; use swayr::ipc;
fn main() { fn main() {
let win_props: Arc<RwLock<HashMap<ipc::Id, ipc::WindowProps>>> = let con_props: Arc<RwLock<HashMap<ipc::Id, ipc::ConProps>>> =
Arc::new(RwLock::new(HashMap::new())); Arc::new(RwLock::new(HashMap::new()));
let win_props_for_ev_handler = win_props.clone(); let con_props_for_ev_handler = con_props.clone();
let subscriber_handle = thread::spawn(move || { let subscriber_handle = thread::spawn(move || {
demon::monitor_window_events(win_props_for_ev_handler) demon::monitor_window_events(con_props_for_ev_handler)
}); });
match demon::serve_client_requests(win_props) { match demon::serve_client_requests(con_props) {
Ok(()) => { Ok(()) => {
let subscriber_result = subscriber_handle.join(); let subscriber_result = subscriber_handle.join();
match subscriber_result { match subscriber_result {

@ -1,10 +1,9 @@
use crate::con; use crate::con;
use crate::ipc;
use crate::util; use crate::util;
pub fn switch_window() { pub fn switch_window() {
let root_node = get_tree(); let root = con::get_tree();
let mut windows = con::get_windows(&root_node); let mut windows = con::get_windows(&root);
windows.sort(); windows.sort();
if let Some(window) = con::select_window("Switch to window", &windows) { if let Some(window) = con::select_window("Switch to window", &windows) {
@ -15,9 +14,21 @@ pub fn switch_window() {
} }
} }
pub fn switch_workspace() {
let root = con::get_tree();
let mut workspaces = con::get_workspaces(&root, false);
workspaces.sort();
if let Some(workspace) =
con::select_workspace("Switch to workspace", &workspaces)
{
util::swaymsg(vec!["workspace", "number", workspace.get_name()]);
}
}
pub fn quit_window() { pub fn quit_window() {
let root_node = get_tree(); let root = con::get_tree();
let mut windows = con::get_windows(&root_node); let mut windows = con::get_windows(&root);
windows.sort_by(|a, b| a.cmp(b).reverse()); windows.sort_by(|a, b| a.cmp(b).reverse());
if let Some(window) = con::select_window("Quit window", &windows) { if let Some(window) = con::select_window("Quit window", &windows) {
@ -27,38 +38,3 @@ pub fn quit_window() {
]); ]);
} }
} }
fn get_tree() -> ipc::Node {
let output = util::swaymsg(vec!["-t", "get_tree"]);
let result = serde_json::from_str(output.as_str());
match result {
Ok(node) => node,
Err(e) => {
eprintln!("Error: {}", e);
panic!()
}
}
}
#[test]
fn test_get_tree() {
let tree = get_tree();
println!("Those IDs are in get_tree():");
for n in tree.iter() {
println!(" id: {}, type: {:?}", n.id, n.r#type);
}
}
#[test]
fn test_get_windows() {
let tree = get_tree();
let cons = con::get_windows(&tree);
println!("There are {} cons.", cons.len());
for c in cons {
println!(" {}", c);
}
}

@ -5,16 +5,39 @@ use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
pub fn get_tree() -> ipc::Node {
let output = util::swaymsg(vec!["-t", "get_tree"]);
let result = serde_json::from_str(output.as_str());
match result {
Ok(node) => node,
Err(e) => {
eprintln!("Error: {}", e);
panic!()
}
}
}
#[test]
fn test_get_tree() {
let tree = get_tree();
println!("Those IDs are in get_tree():");
for n in tree.iter() {
println!(" id: {}, type: {:?}", n.id, n.r#type);
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct Window<'a> { pub struct Window<'a> {
node: &'a ipc::Node, node: &'a ipc::Node,
workspace: &'a ipc::Node, workspace: &'a ipc::Node,
win_props: Option<ipc::WindowProps>, con_props: Option<ipc::ConProps>,
} }
impl Window<'_> { impl Window<'_> {
pub fn get_id(&self) -> ipc::Id { pub fn get_id(&self) -> &ipc::Id {
self.node.id &self.node.id
} }
pub fn get_app_name(&self) -> &str { pub fn get_app_name(&self) -> &str {
@ -38,7 +61,7 @@ impl Window<'_> {
} }
impl PartialEq for Window<'_> { impl PartialEq for Window<'_> {
fn eq(&self, other: &Window) -> bool { fn eq(&self, other: &Self) -> bool {
self.get_id() == other.get_id() self.get_id() == other.get_id()
} }
} }
@ -59,9 +82,9 @@ impl Ord for Window<'_> {
std::cmp::Ordering::Greater std::cmp::Ordering::Greater
} else { } else {
let lru_a = let lru_a =
self.win_props.as_ref().map_or(0, |wp| wp.last_focus_time); self.con_props.as_ref().map_or(0, |wp| wp.last_focus_time);
let lru_b = let lru_b =
other.win_props.as_ref().map_or(0, |wp| wp.last_focus_time); other.con_props.as_ref().map_or(0, |wp| wp.last_focus_time);
lru_a.cmp(&lru_b).reverse() lru_a.cmp(&lru_b).reverse()
} }
} }
@ -95,15 +118,15 @@ impl<'a> fmt::Display for Window<'a> {
} }
fn build_windows( fn build_windows(
tree: &ipc::Node, root: &ipc::Node,
mut win_props: HashMap<ipc::Id, ipc::WindowProps>, mut con_props: HashMap<ipc::Id, ipc::ConProps>,
) -> Vec<Window> { ) -> Vec<Window> {
let mut v = vec![]; let mut v = vec![];
for workspace in tree.workspaces() { for workspace in root.workspaces() {
for n in workspace.windows() { for n in workspace.windows() {
v.push(Window { v.push(Window {
node: &n, node: &n,
win_props: win_props.remove(&n.id), con_props: con_props.remove(&n.id),
workspace: &workspace, workspace: &workspace,
}) })
} }
@ -111,8 +134,37 @@ fn build_windows(
v v
} }
fn get_window_props( fn build_workspaces(
) -> Result<HashMap<ipc::Id, ipc::WindowProps>, serde_json::Error> { root: &ipc::Node,
mut con_props: HashMap<ipc::Id, ipc::ConProps>,
include_scratchpad: bool,
) -> Vec<Workspace> {
let mut v = vec![];
for workspace in root.workspaces() {
if !include_scratchpad
&& workspace.name.as_ref().unwrap().eq("__i3_scratch")
{
continue;
}
v.push(Workspace {
node: &workspace,
con_props: con_props.remove(&workspace.id),
windows: workspace
.windows()
.iter()
.map(|w| Window {
node: &w,
con_props: con_props.remove(&w.id),
workspace: &workspace,
})
.collect(),
})
}
v
}
fn get_con_props() -> Result<HashMap<ipc::Id, ipc::ConProps>, serde_json::Error>
{
if let Ok(sock) = UnixStream::connect(util::get_swayr_socket_path()) { if let Ok(sock) = UnixStream::connect(util::get_swayr_socket_path()) {
serde_json::from_reader(sock) serde_json::from_reader(sock)
} else { } else {
@ -121,16 +173,44 @@ fn get_window_props(
} }
/// Gets all application windows of the tree. /// Gets all application windows of the tree.
pub fn get_windows(root_node: &ipc::Node) -> Vec<Window> { pub fn get_windows(root: &ipc::Node) -> Vec<Window> {
let win_props = match get_window_props() { let con_props = match get_con_props() {
Ok(win_props) => Some(win_props), Ok(con_props) => Some(con_props),
Err(e) => {
eprintln!("Got no con_props: {:?}", e);
None
}
};
build_windows(root, con_props.unwrap_or_default())
}
/// Gets all application windows of the tree.
pub fn get_workspaces(
root: &ipc::Node,
include_scratchpad: bool,
) -> Vec<Workspace> {
let con_props = match get_con_props() {
Ok(con_props) => Some(con_props),
Err(e) => { Err(e) => {
eprintln!("Got no win_props: {:?}", e); eprintln!("Got no con_props: {:?}", e);
None None
} }
}; };
build_windows(&root_node, win_props.unwrap_or_default()) build_workspaces(root, con_props.unwrap_or_default(), include_scratchpad)
}
#[test]
fn test_get_windows() {
let root = get_tree();
let cons = get_windows(&root);
println!("There are {} cons.", cons.len());
for c in cons {
println!(" {}", c);
}
} }
pub fn select_window<'a>( pub fn select_window<'a>(
@ -139,3 +219,66 @@ pub fn select_window<'a>(
) -> Option<&'a Window<'a>> { ) -> Option<&'a Window<'a>> {
util::wofi_select(prompt, windows) util::wofi_select(prompt, windows)
} }
pub fn select_workspace<'a>(
prompt: &'a str,
workspaces: &'a [Workspace],
) -> Option<&'a Workspace<'a>> {
util::wofi_select(prompt, workspaces)
}
pub struct Workspace<'a> {
node: &'a ipc::Node,
con_props: Option<ipc::ConProps>,
windows: Vec<Window<'a>>,
}
impl Workspace<'_> {
pub fn get_name(&self) -> &str {
self.node.name.as_ref().unwrap()
}
pub fn get_id(&self) -> &ipc::Id {
&self.node.id
}
}
impl PartialEq for Workspace<'_> {
fn eq(&self, other: &Self) -> bool {
self.get_id() == other.get_id()
}
}
impl Eq for Workspace<'_> {}
impl Ord for Workspace<'_> {
fn cmp(&self, other: &Self) -> cmp::Ordering {
if self == other {
cmp::Ordering::Equal
} else {
let lru_a =
self.con_props.as_ref().map_or(0, |wp| wp.last_focus_time);
let lru_b =
other.con_props.as_ref().map_or(0, |wp| wp.last_focus_time);
lru_a.cmp(&lru_b).reverse()
}
}
}
impl PartialOrd for Workspace<'_> {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<'a> fmt::Display for Workspace<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(
f,
"<span font_weight=\"bold\">Workspace {}</span> \
<span alpha=\"20000\">id {}</span>", // Almost hide ID!
self.get_name(),
self.get_id()
)
}
}

@ -11,50 +11,86 @@ use std::thread;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
pub fn monitor_window_events( pub fn monitor_window_events(
win_props: Arc<RwLock<HashMap<ipc::Id, ipc::WindowProps>>>, con_props: Arc<RwLock<HashMap<ipc::Id, ipc::ConProps>>>,
) { ) {
let child = proc::Command::new("swaymsg") let child = proc::Command::new("swaymsg")
.arg("--monitor") .arg("--monitor")
.arg("--raw") .arg("--raw")
.arg("-t") .arg("-t")
.arg("subscribe") .arg("subscribe")
.arg("[\"window\"]") .arg("[\"window\", \"workspace\"]")
.stdout(proc::Stdio::piped()) .stdout(proc::Stdio::piped())
.spawn() .spawn()
.expect("Failed to subscribe to window events"); .expect("Failed to subscribe to window events");
let stdout: std::process::ChildStdout = child.stdout.unwrap(); let stdout: std::process::ChildStdout = child.stdout.unwrap();
let stream = let stream = Deserializer::from_reader(stdout).into_iter::<ipc::ConEvent>();
Deserializer::from_reader(stdout).into_iter::<ipc::WindowEvent>();
for res in stream { for res in stream {
match res { match res {
Ok(win_ev) => handle_window_event(win_ev, win_props.clone()), Ok(win_ev) => handle_con_event(win_ev, con_props.clone()),
Err(err) => eprintln!("Error handling window event:\n{:?}", err), Err(err) => eprintln!("Error handling window event:\n{:?}", err),
} }
} }
} }
fn handle_window_event( fn update_last_focus_time(
ev: ipc::WindowEvent, id: ipc::Id,
win_props: Arc<RwLock<HashMap<ipc::Id, ipc::WindowProps>>>, con_props: Arc<RwLock<HashMap<ipc::Id, ipc::ConProps>>>,
) { ) {
match ev.change { let mut write_lock = con_props.write().unwrap();
ipc::WindowEventType::Focus => { if let Some(mut wp) = write_lock.get_mut(&id) {
let mut write_lock = win_props.write().unwrap(); wp.last_focus_time = get_epoch_time_as_millis();
if let Some(mut wp) = write_lock.get_mut(&ev.container.id) { } else {
wp.last_focus_time = get_epoch_time_as_millis(); write_lock.insert(
} else { id,
write_lock.insert( ipc::ConProps {
ev.container.id, last_focus_time: get_epoch_time_as_millis(),
ipc::WindowProps { },
last_focus_time: get_epoch_time_as_millis(), );
}, }
); }
fn remove_winprops(
id: &ipc::Id,
con_props: Arc<RwLock<HashMap<ipc::Id, ipc::ConProps>>>,
) {
con_props.write().unwrap().remove(id);
}
fn handle_con_event(
ev: ipc::ConEvent,
con_props: Arc<RwLock<HashMap<ipc::Id, ipc::ConProps>>>,
) {
let mut handled = true;
let con_props2 = con_props.clone();
match ev {
ipc::ConEvent::WindowEvent { change, container } => match change {
ipc::WindowEventType::New | ipc::WindowEventType::Focus => {
update_last_focus_time(container.id, con_props)
} }
} ipc::WindowEventType::Close => {
ipc::WindowEventType::Close => { remove_winprops(&container.id, con_props)
win_props.write().unwrap().remove(&ev.container.id); }
} _ => handled = false,
_ => (), },
ipc::ConEvent::WorkspaceEvent {
change,
current,
old: _,
} => match change {
ipc::WorkspaceEventType::Init | ipc::WorkspaceEventType::Focus => {
println!("WsEv");
update_last_focus_time(current.id, con_props)
}
ipc::WorkspaceEventType::Empty => {
remove_winprops(&current.id, con_props)
}
_ => handled = false,
},
}
if handled {
println!("New con_props state:\n{:#?}", *con_props2.read().unwrap());
} }
} }
@ -66,7 +102,7 @@ fn get_epoch_time_as_millis() -> u128 {
} }
pub fn serve_client_requests( pub fn serve_client_requests(
win_props: Arc<RwLock<HashMap<ipc::Id, ipc::WindowProps>>>, con_props: Arc<RwLock<HashMap<ipc::Id, ipc::ConProps>>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
match std::fs::remove_file(util::get_swayr_socket_path()) { match std::fs::remove_file(util::get_swayr_socket_path()) {
Ok(()) => println!("Deleted stale socket from previous run."), Ok(()) => println!("Deleted stale socket from previous run."),
@ -77,7 +113,7 @@ pub fn serve_client_requests(
for stream in listener.incoming() { for stream in listener.incoming() {
match stream { match stream {
Ok(stream) => { Ok(stream) => {
let wp_clone = win_props.clone(); let wp_clone = con_props.clone();
thread::spawn(move || handle_client_request(stream, wp_clone)); thread::spawn(move || handle_client_request(stream, wp_clone));
} }
Err(err) => return Err(err), Err(err) => return Err(err),
@ -88,9 +124,9 @@ pub fn serve_client_requests(
fn handle_client_request( fn handle_client_request(
mut stream: UnixStream, mut stream: UnixStream,
win_props: Arc<RwLock<HashMap<ipc::Id, ipc::WindowProps>>>, con_props: Arc<RwLock<HashMap<ipc::Id, ipc::ConProps>>>,
) { ) {
let json = serde_json::to_string(&*win_props.read().unwrap()).unwrap(); let json = serde_json::to_string(&*con_props.read().unwrap()).unwrap();
if let Err(err) = stream.write_all(json.as_bytes()) { if let Err(err) = stream.write_all(json.as_bytes()) {
eprintln!("Error writing to client: {:?}", err); eprintln!("Error writing to client: {:?}", err);
} }

@ -202,13 +202,40 @@ pub enum WindowEventType {
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[allow(dead_code)] #[allow(dead_code)]
pub struct WindowEvent { pub enum WorkspaceEventType {
pub change: WindowEventType, #[serde(rename = "init")]
pub container: Node, Init,
#[serde(rename = "empty")]
Empty,
#[serde(rename = "focus")]
Focus,
#[serde(rename = "move")]
Move,
#[serde(rename = "rename")]
Rename,
#[serde(rename = "urgent")]
Urgent,
#[serde(rename = "reload")]
Reload,
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
#[allow(dead_code)]
pub enum ConEvent {
WindowEvent {
change: WindowEventType,
container: Node,
},
WorkspaceEvent {
change: WorkspaceEventType,
current: Node,
old: Node,
},
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct WindowProps { pub struct ConProps {
/// Milliseconds since UNIX epoch. /// Milliseconds since UNIX epoch.
pub last_focus_time: u128, pub last_focus_time: u128,
} }

Loading…
Cancel
Save