add hyprspace workspace manager
This commit is contained in:
parent
893b3772d4
commit
01facc5866
4 changed files with 634 additions and 0 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2898,6 +2898,7 @@ dependencies = [
|
|||
"env_logger",
|
||||
"hyprland",
|
||||
"log",
|
||||
"nix",
|
||||
"regex",
|
||||
"serde",
|
||||
"strum",
|
||||
|
|
17
examples/worf-hyprspace/Cargo.toml
Normal file
17
examples/worf-hyprspace/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "worf-hyprspace"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
worf = {path = "../../worf"}
|
||||
env_logger = "0.11.8"
|
||||
hyprland = "0.4.0-beta.2"
|
||||
clap = "4.5.40"
|
||||
serde = "1.0.219"
|
||||
strum = "0.27.1"
|
||||
strum_macros = "0.27.1"
|
||||
Inflector = "0.11"
|
||||
regex = "1.11.1"
|
||||
log = "0.4.27"
|
||||
nix = "0.30.1"
|
1
examples/worf-hyprspace/Readme.md
Normal file
1
examples/worf-hyprspace/Readme.md
Normal file
|
@ -0,0 +1 @@
|
|||
# Worf Hyprspace
|
615
examples/worf-hyprspace/src/main.rs
Normal file
615
examples/worf-hyprspace/src/main.rs
Normal file
|
@ -0,0 +1,615 @@
|
|||
use std::{
|
||||
env,
|
||||
fmt::{Display, Formatter},
|
||||
str::FromStr,
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
thread::sleep,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use hyprland::data::Client;
|
||||
use hyprland::dispatch::WindowIdentifier;
|
||||
use hyprland::{
|
||||
data::{Workspace, Workspaces},
|
||||
dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial},
|
||||
prelude::HyprData,
|
||||
shared::HyprDataActive,
|
||||
};
|
||||
use nix::libc::{SIGTERM, kill};
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use strum::IntoEnumIterator;
|
||||
use strum_macros::EnumIter;
|
||||
use worf::gui::{
|
||||
self, ArcFactory, ArcProvider, ExpandMode, ItemFactory, ItemProvider, MenuItem, ProviderData,
|
||||
Selection,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Action {
|
||||
workspace: Option<Workspace>,
|
||||
mode: Mode,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct HyprspaceProvider {
|
||||
cfg: HyprSpaceConfig,
|
||||
search_ignored_words: Vec<Regex>,
|
||||
detected_mode: Option<Mode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, EnumIter, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
Auto,
|
||||
Rename,
|
||||
SwitchToWorkspace,
|
||||
MoveCurrentWindowToOtherWorkspace,
|
||||
MoveCurrentWindowToOtherWorkspaceSilent,
|
||||
MoveAllWindowsToOtherWorkSpace,
|
||||
DeleteWorkspace,
|
||||
}
|
||||
|
||||
impl FromStr for Mode {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"auto" => Ok(Mode::Auto),
|
||||
"rename" => Ok(Mode::Rename),
|
||||
"switchtoworkspace" => Ok(Mode::SwitchToWorkspace),
|
||||
"movecurrentwindowtootherworkspace" => Ok(Mode::MoveCurrentWindowToOtherWorkspace),
|
||||
"movecurrentwindowtootherworkspacesilent" => {
|
||||
Ok(Mode::MoveCurrentWindowToOtherWorkspaceSilent)
|
||||
}
|
||||
"moveallwindowstootherworkspace" => Ok(Mode::MoveCurrentWindowToOtherWorkspace),
|
||||
"deleteworkspace" => Ok(Mode::DeleteWorkspace),
|
||||
_ => Err(format!("Invalid mode: {s}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Mode {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let variant = format!("{self:?}");
|
||||
// Convert PascalCase to Title Case with spaces
|
||||
let spaced = inflector::cases::titlecase::to_title_case(&variant);
|
||||
write!(f, "{spaced}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Parser, Deserialize)]
|
||||
#[clap(about = "Worf-Hyprspace is a Hyprland workspace manager built on top of Worf")]
|
||||
struct HyprSpaceConfig {
|
||||
#[command(flatten)]
|
||||
worf: worf::config::Config,
|
||||
|
||||
#[arg(long)]
|
||||
hypr_space_mode: Option<Mode>,
|
||||
|
||||
#[arg(long)]
|
||||
add_id_prefix: Option<bool>,
|
||||
|
||||
#[arg(long)]
|
||||
max_workspace_id: Option<i32>,
|
||||
}
|
||||
|
||||
impl HyprSpaceConfig {
|
||||
fn hypr_space_mode(&self) -> Mode {
|
||||
self.hypr_space_mode.clone().unwrap_or(Mode::Auto)
|
||||
}
|
||||
|
||||
fn add_id_prefix(&self) -> bool {
|
||||
self.add_id_prefix.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn max_workspace_id(&self) -> i32 {
|
||||
self.max_workspace_id.unwrap_or(10)
|
||||
}
|
||||
}
|
||||
|
||||
impl HyprspaceProvider {
|
||||
fn new(cfg: &HyprSpaceConfig, search_ignored_words: Vec<Regex>) -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
cfg: cfg.clone(),
|
||||
search_ignored_words,
|
||||
detected_mode: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemProvider<Action> for HyprspaceProvider {
|
||||
fn get_elements(&mut self, query: Option<&str>) -> ProviderData<Action> {
|
||||
let auto = if self.cfg.hypr_space_mode() == Mode::Auto {
|
||||
query.and_then(|q| {
|
||||
Mode::iter()
|
||||
.find(|m| m.to_string().to_lowercase().trim() == q.to_lowercase())
|
||||
.map(|m| {
|
||||
self.detected_mode = Some(m.clone());
|
||||
ProviderData {
|
||||
items: Some(get_modes_actions(
|
||||
&m,
|
||||
query,
|
||||
self.search_ignored_words.as_ref(),
|
||||
)),
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
self.detected_mode = None;
|
||||
None
|
||||
};
|
||||
auto.unwrap_or(ProviderData {
|
||||
items: Some(get_modes_actions(
|
||||
&self.cfg.hypr_space_mode(),
|
||||
query,
|
||||
self.search_ignored_words.as_ref(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_sub_elements(&mut self, item: &MenuItem<Action>) -> ProviderData<Action> {
|
||||
if let Some(mode) = Mode::iter()
|
||||
.find(|m| {
|
||||
m.to_string()
|
||||
.to_lowercase()
|
||||
.trim()
|
||||
.contains(&item.label.to_lowercase())
|
||||
})
|
||||
.map(|m| {
|
||||
self.detected_mode = Some(m.clone());
|
||||
ProviderData {
|
||||
items: Some(get_modes_actions(
|
||||
&m,
|
||||
Some(&item.label),
|
||||
self.search_ignored_words.as_ref(),
|
||||
)),
|
||||
}
|
||||
})
|
||||
{
|
||||
mode
|
||||
} else {
|
||||
ProviderData { items: None }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemFactory<Action> for HyprspaceProvider {
|
||||
fn new_menu_item(&self, label: String) -> Option<MenuItem<Action>> {
|
||||
Some(MenuItem::new(
|
||||
label,
|
||||
None,
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
0.0,
|
||||
Some(Action {
|
||||
workspace: None,
|
||||
mode: if self.cfg.hypr_space_mode() == Mode::Auto {
|
||||
self.detected_mode.clone().unwrap_or(Mode::Auto)
|
||||
} else {
|
||||
self.cfg.hypr_space_mode()
|
||||
},
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn build_menu_items<'a, F>(
|
||||
mode: &Mode,
|
||||
aws: &'a Workspace,
|
||||
workspaces: &'a Workspaces,
|
||||
query: Option<&'a str>,
|
||||
search_ignored_words: &Vec<Regex>,
|
||||
filter_fn: F,
|
||||
) -> Vec<MenuItem<Action>>
|
||||
where
|
||||
F: for<'b> Fn(&'b Workspace) -> bool + Copy,
|
||||
{
|
||||
workspaces
|
||||
.iter()
|
||||
.filter(|ws| filter_fn(ws))
|
||||
.map(|ws| workspace_to_menu_item(mode, aws, ws))
|
||||
.chain(query.map(|q| {
|
||||
MenuItem::new(
|
||||
gui::filtered_query(Some(search_ignored_words), q),
|
||||
None,
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
0.0,
|
||||
Some(Action {
|
||||
workspace: None,
|
||||
mode: mode.clone(),
|
||||
}),
|
||||
)
|
||||
}))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_modes_actions(
|
||||
mode: &Mode,
|
||||
query: Option<&str>,
|
||||
search_ignored_words: &Vec<Regex>,
|
||||
) -> Vec<MenuItem<Action>> {
|
||||
let workspaces = match hyprland::data::Workspaces::get() {
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
log::error!("Failed to get workspaces {e}");
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
|
||||
let aws = if let Ok(ws) = hyprland::data::Workspace::get_active() {
|
||||
ws
|
||||
} else {
|
||||
log::error!("No active workspace found");
|
||||
return Vec::<MenuItem<Action>>::new();
|
||||
};
|
||||
|
||||
match mode {
|
||||
Mode::Auto => Mode::iter()
|
||||
.filter(|m| m != &Mode::Auto)
|
||||
.map(|mode| {
|
||||
MenuItem::new(
|
||||
mode.to_string(),
|
||||
None,
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
0.0,
|
||||
Some(Action {
|
||||
workspace: None,
|
||||
mode,
|
||||
}),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
|
||||
Mode::Rename | Mode::DeleteWorkspace => {
|
||||
build_menu_items(mode, &aws, &workspaces, query, search_ignored_words, |_| {
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
Mode::SwitchToWorkspace
|
||||
| Mode::MoveAllWindowsToOtherWorkSpace
|
||||
| Mode::MoveCurrentWindowToOtherWorkspace
|
||||
| Mode::MoveCurrentWindowToOtherWorkspaceSilent => {
|
||||
build_menu_items(mode, &aws, &workspaces, query, search_ignored_words, |ws| {
|
||||
ws.id != aws.id
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn workspace_to_menu_item(mode: &Mode, aws: &Workspace, ws: &Workspace) -> MenuItem<Action> {
|
||||
MenuItem::new(
|
||||
ws.name.clone(),
|
||||
None,
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
if aws.id == ws.id { 1.0 } else { 0.0 },
|
||||
Some(Action {
|
||||
workspace: Some(ws.clone()),
|
||||
mode: mode.clone(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn handle_sub_selection(
|
||||
item: &MenuItem<Action>,
|
||||
query: Option<&str>,
|
||||
search_ignored_words: &Vec<Regex>,
|
||||
) -> ProviderData<Action> {
|
||||
if let Some(mode) = Mode::iter()
|
||||
.find(|m| {
|
||||
m.to_string()
|
||||
.to_lowercase()
|
||||
.contains(&item.label.to_lowercase())
|
||||
})
|
||||
.map(|m| ProviderData {
|
||||
items: Some(get_modes_actions(&m, query, search_ignored_words)),
|
||||
})
|
||||
{
|
||||
mode
|
||||
} else {
|
||||
ProviderData { items: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct EmptyProvider {}
|
||||
|
||||
impl ItemProvider<Action> for EmptyProvider {
|
||||
fn get_elements(&mut self, search: Option<&str>) -> ProviderData<Action> {
|
||||
ProviderData {
|
||||
items: Some(vec![MenuItem::new(
|
||||
search.unwrap_or_default().to_owned(),
|
||||
None,
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
0.0,
|
||||
Some(Action {
|
||||
workspace: None,
|
||||
mode: Mode::Auto,
|
||||
}),
|
||||
)]),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_sub_elements(&mut self, _: &MenuItem<Action>) -> ProviderData<Action> {
|
||||
ProviderData { items: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemFactory<Action> for EmptyProvider {
|
||||
fn new_menu_item(&self, label: String) -> Option<MenuItem<Action>> {
|
||||
Some(MenuItem::new(
|
||||
label,
|
||||
None,
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
0.0,
|
||||
Some(Action {
|
||||
workspace: None,
|
||||
mode: Mode::Auto,
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn find_first_free_workspace_id(max_id: i32) -> Option<i32> {
|
||||
let ws = Workspaces::get().ok()?;
|
||||
(1..=max_id).find(|&i| !ws.iter().any(|w| w.id == i))
|
||||
}
|
||||
|
||||
fn show_gui<T: ItemProvider<Action> + ItemFactory<Action> + Send + Clone + 'static>(
|
||||
cfg: &HyprSpaceConfig,
|
||||
pattern: &Regex,
|
||||
provider: Arc<Mutex<T>>,
|
||||
) -> Result<Selection<Action>, String> {
|
||||
gui::show(
|
||||
&Arc::new(RwLock::new(cfg.worf.clone())),
|
||||
Arc::clone(&provider) as ArcProvider<Action>,
|
||||
Some(provider as ArcFactory<Action>),
|
||||
Some(vec![pattern.clone()]),
|
||||
ExpandMode::WithSpace,
|
||||
None,
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn workspace_from_selection<'a>(
|
||||
action: Option<Action>,
|
||||
max_id: i32,
|
||||
) -> Result<(WorkspaceIdentifierWithSpecial<'a>, i32, bool), String> {
|
||||
if let Some(action) = action {
|
||||
if let Some(ws) = action.workspace {
|
||||
return Ok((WorkspaceIdentifierWithSpecial::Id(ws.id), ws.id, false));
|
||||
}
|
||||
}
|
||||
find_first_free_workspace_id(max_id)
|
||||
.map(|id| (WorkspaceIdentifierWithSpecial::Id(id), id, true))
|
||||
.ok_or_else(|| "Failed to get workspace id".to_string())
|
||||
}
|
||||
|
||||
fn set_workspace_name(label: &str, id: i32, add_id_prefix: bool) -> Result<(), String> {
|
||||
// todo maybe there is a better way to poll if a workspace has been created
|
||||
let start = Instant::now();
|
||||
let ws = loop {
|
||||
// same as above might break at some point but waiting at the tail
|
||||
// end of the loop sometimes leads to timing issues
|
||||
// where the workspace exists in some weird state
|
||||
sleep(Duration::from_millis(10));
|
||||
if start.elapsed().as_millis() >= 1500 {
|
||||
break None;
|
||||
}
|
||||
|
||||
if let Some(workspace) = get_workspace(id)? {
|
||||
break Some(workspace);
|
||||
}
|
||||
};
|
||||
|
||||
ws.map(|ws| {
|
||||
let ws_id = ws.id.to_string();
|
||||
let id_prefix = format!("{ws_id}: ");
|
||||
let new_name = if add_id_prefix && !ws.name.starts_with(&id_prefix) {
|
||||
&format!("{id_prefix}{label}")
|
||||
} else {
|
||||
label
|
||||
};
|
||||
|
||||
Dispatch::call(DispatchType::RenameWorkspace(ws.id, Some(new_name)))
|
||||
})
|
||||
.transpose()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_workspace(id: i32) -> Result<Option<Workspace>, String> {
|
||||
let ws = Workspaces::get()
|
||||
.map_err(|e| e.to_string())?
|
||||
.into_iter()
|
||||
.find(|ws| ws.id == id);
|
||||
Ok(ws)
|
||||
}
|
||||
|
||||
fn process_clients_on_workspace<F>(ws_id: i32, proc: F) -> Result<(), String>
|
||||
where
|
||||
F: for<'a> Fn(&'a Client),
|
||||
{
|
||||
hyprland::data::Clients::get()
|
||||
.map_err(|e| format!("failed to get clients for ws {ws_id}, err {e}"))?
|
||||
.iter()
|
||||
.filter(|client| client.workspace.id == ws_id)
|
||||
.for_each(proc);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_workspace_action<F>(
|
||||
cfg: &HyprSpaceConfig,
|
||||
label: &str,
|
||||
action: Option<Action>,
|
||||
dispatch_builder: F,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: FnOnce(WorkspaceIdentifierWithSpecial) -> DispatchType,
|
||||
{
|
||||
let (workspace, id, _new) = workspace_from_selection(action, cfg.max_workspace_id())?;
|
||||
Dispatch::call(dispatch_builder(workspace)).map_err(|e| e.to_string())?;
|
||||
set_workspace_name(label, id, cfg.add_id_prefix())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
env_logger::Builder::new()
|
||||
.parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "error".to_owned()))
|
||||
.format_timestamp_micros()
|
||||
.init();
|
||||
|
||||
let mut cfg = HyprSpaceConfig::parse();
|
||||
cfg.worf = worf::config::load_config(Some(&cfg.worf)).unwrap_or(cfg.worf);
|
||||
if cfg.worf.prompt().is_none() {
|
||||
cfg.worf.set_prompt(cfg.hypr_space_mode().to_string());
|
||||
}
|
||||
|
||||
let pattern = Mode::iter()
|
||||
.map(|m| regex::escape(&m.to_string().to_lowercase()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("|");
|
||||
|
||||
let pattern = Regex::new(&format!("(?i){pattern}")).map_err(|e| e.to_string())?;
|
||||
|
||||
let provider = Arc::new(Mutex::new(HyprspaceProvider::new(
|
||||
&cfg,
|
||||
vec![pattern.clone()],
|
||||
)?));
|
||||
|
||||
process_inputs(&mut cfg, &pattern, provider)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_inputs(
|
||||
cfg: &mut HyprSpaceConfig,
|
||||
pattern: &Regex,
|
||||
provider: Arc<Mutex<HyprspaceProvider>>,
|
||||
) -> Result<(), String> {
|
||||
let result = show_gui(cfg, pattern, Arc::clone(&provider))?;
|
||||
|
||||
let result_items = handle_sub_selection(&result.menu, None, vec![pattern.clone()].as_ref());
|
||||
let result = if result_items.items.is_some() {
|
||||
if let Some(menu) = result.menu.data {
|
||||
cfg.hypr_space_mode = Some(menu.mode.clone());
|
||||
cfg.worf.set_prompt(cfg.hypr_space_mode().to_string());
|
||||
|
||||
let provider = Arc::new(Mutex::new(HyprspaceProvider::new(
|
||||
&cfg.clone(),
|
||||
vec![pattern.clone()],
|
||||
)?));
|
||||
show_gui(cfg, pattern, provider)?
|
||||
} else {
|
||||
result
|
||||
}
|
||||
} else {
|
||||
result
|
||||
};
|
||||
|
||||
let action = result.menu.data;
|
||||
let mode = action
|
||||
.as_ref()
|
||||
.map(|m| m.mode.clone())
|
||||
.unwrap_or(cfg.hypr_space_mode());
|
||||
match mode {
|
||||
Mode::Auto => {
|
||||
return process_inputs(cfg, pattern, provider);
|
||||
}
|
||||
Mode::Rename => {
|
||||
if let Some(action) = action {
|
||||
cfg.worf
|
||||
.set_prompt(format!("Rename {} to ", result.menu.label));
|
||||
let provider = Arc::new(Mutex::new(EmptyProvider {}));
|
||||
let rename_result = show_gui(cfg, pattern, provider)?;
|
||||
|
||||
let new_name = if cfg.add_id_prefix() {
|
||||
let ws_id = action
|
||||
.workspace
|
||||
.as_ref()
|
||||
.map(|ws| ws.id.to_string())
|
||||
.unwrap_or_default();
|
||||
format!("{}: {}", ws_id, rename_result.menu.label)
|
||||
} else {
|
||||
rename_result.menu.label.to_string()
|
||||
};
|
||||
|
||||
Dispatch::call(DispatchType::RenameWorkspace(
|
||||
action.workspace.as_ref().unwrap().id,
|
||||
Some(&new_name),
|
||||
))
|
||||
.map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
Err("Action is not set, cannot rename workspace".to_owned())?;
|
||||
}
|
||||
}
|
||||
Mode::SwitchToWorkspace => {
|
||||
// Clippy suggests removing this closure as redundant,
|
||||
// but doing so causes lifetime inference issues with `DispatchType::Workspace`.
|
||||
// Keeping the closure avoids `'static` lifetime assumptions.
|
||||
#[allow(clippy::redundant_closure)]
|
||||
handle_workspace_action(cfg, &result.menu.label, action, |ws| {
|
||||
DispatchType::Workspace(ws)
|
||||
})?;
|
||||
}
|
||||
Mode::MoveCurrentWindowToOtherWorkspace => {
|
||||
handle_workspace_action(cfg, &result.menu.label, action, |ws| {
|
||||
DispatchType::MoveToWorkspace(ws, None)
|
||||
})?;
|
||||
}
|
||||
Mode::MoveCurrentWindowToOtherWorkspaceSilent => {
|
||||
handle_workspace_action(cfg, &result.menu.label, action, |ws| {
|
||||
DispatchType::MoveToWorkspaceSilent(ws, None)
|
||||
})?;
|
||||
}
|
||||
Mode::DeleteWorkspace => {
|
||||
let (_ws, selected_id, _new) =
|
||||
workspace_from_selection(action, cfg.max_workspace_id())?;
|
||||
|
||||
process_clients_on_workspace(selected_id, |client| unsafe {
|
||||
kill(client.pid, SIGTERM);
|
||||
})?;
|
||||
|
||||
let active_ws = Workspace::get_active()
|
||||
.map_err(|e| format!("failed to get active workspace {e}"))?;
|
||||
if active_ws.id == selected_id {
|
||||
Dispatch::call(DispatchType::Workspace(
|
||||
WorkspaceIdentifierWithSpecial::Previous,
|
||||
))
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
Mode::MoveAllWindowsToOtherWorkSpace => {
|
||||
let active_ws = Workspace::get_active()
|
||||
.map_err(|e| format!("failed to get active workspace {e}"))?;
|
||||
|
||||
let (ws, target_id, new) = workspace_from_selection(action, cfg.max_workspace_id())?;
|
||||
process_clients_on_workspace(active_ws.id, |client| {
|
||||
if let Err(e) = Dispatch::call(DispatchType::MoveToWorkspace(
|
||||
ws,
|
||||
Some(WindowIdentifier::Address(client.address.clone())),
|
||||
)) {
|
||||
log::warn!("cannot move client to new workspace, ignoring it, err={e}")
|
||||
}
|
||||
})?;
|
||||
|
||||
if new {
|
||||
set_workspace_name(&result.menu.label, target_id, cfg.add_id_prefix())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
Loading…
Add table
Reference in a new issue