Skip to content

Commit

Permalink
feat(cli): add eager-focus command
Browse files Browse the repository at this point in the history
This commit adds a new komorebic command "eager-focus", which takes a
full case-sensitive exe identifier as an argument. When komorebi
receives this message, it will look through each monitor and workspace
for the first matching managed window and then focus it.

This allows users who have well defined workspaces and rules to bind
semantic hotkeys to commands like "komorebic eager-focus Discord.exe" to
immediately jump to applications instead of mentally looking up their
assigned workspaces or positions within container stacks.
  • Loading branch information
LGUG2Z committed Dec 29, 2024
1 parent 79eda30 commit e9bb6b4
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 1 deletion.
12 changes: 12 additions & 0 deletions komorebi/src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ impl Container {
None
}

pub fn idx_from_exe(&self, exe: &str) -> Option<usize> {
for (idx, window) in self.windows().iter().enumerate() {
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Option::from(idx);
}
}
}

None
}

pub fn contains_window(&self, hwnd: isize) -> bool {
for window in self.windows() {
if window.hwnd == hwnd {
Expand Down
1 change: 1 addition & 0 deletions komorebi/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ pub enum SocketMessage {
Promote,
PromoteFocus,
PromoteWindow(OperationDirection),
EagerFocus(String),
ToggleFloat,
ToggleMonocle,
ToggleMaximize,
Expand Down
60 changes: 60 additions & 0 deletions komorebi/src/process_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ use crate::window_manager;
use crate::window_manager::WindowManager;
use crate::windows_api::WindowsApi;
use crate::winevent_listener;
use crate::workspace::WorkspaceWindowLocation;
use crate::GlobalState;
use crate::Notification;
use crate::NotificationEvent;
Expand Down Expand Up @@ -229,6 +230,65 @@ impl WindowManager {
self.focus_container_in_direction(direction)?;
self.promote_container_to_front()?
}
SocketMessage::EagerFocus(ref exe) => {
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx = self.focused_workspace_idx()?;

let mut window_location = None;
let mut monitor_workspace_indices = None;

'search: for (monitor_idx, monitor) in self.monitors().iter().enumerate() {
for (workspace_idx, workspace) in monitor.workspaces().iter().enumerate() {
if let Some(location) = workspace.location_from_exe(exe) {
window_location = Some(location);
monitor_workspace_indices = Some((monitor_idx, workspace_idx));
break 'search;
}
}
}

if let Some((monitor_idx, workspace_idx)) = monitor_workspace_indices {
if monitor_idx != focused_monitor_idx {
self.focus_monitor(monitor_idx)?;
}

if workspace_idx != focused_workspace_idx {
self.focus_workspace(workspace_idx)?;
}
}

if let Some(location) = window_location {
match location {
WorkspaceWindowLocation::Monocle(window_idx) => {
self.focus_container_window(window_idx)?;
}
WorkspaceWindowLocation::Maximized => {
if let Some(window) =
self.focused_workspace_mut()?.maximized_window_mut()
{
window.focus(self.mouse_follows_focus)?;
}
}
WorkspaceWindowLocation::Container(container_idx, window_idx) => {
let focused_container_idx = self.focused_container_idx()?;
if container_idx != focused_container_idx {
self.focused_workspace_mut()?.focus_container(container_idx);
}

self.focus_container_window(window_idx)?;
}
WorkspaceWindowLocation::Floating(window_idx) => {
if let Some(window) = self
.focused_workspace_mut()?
.floating_windows_mut()
.get_mut(window_idx)
{
window.focus(self.mouse_follows_focus)?;
}
}
}
}
}
SocketMessage::FocusWindow(direction) => {
self.focus_container_in_direction(direction)?;
}
Expand Down
6 changes: 5 additions & 1 deletion komorebi/src/window_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2245,7 +2245,7 @@ impl WindowManager {
let len = NonZeroUsize::new(container.windows().len())
.ok_or_else(|| anyhow!("there must be at least one window in a container"))?;

if len.get() == 1 {
if len.get() == 1 && idx != 0 {
bail!("there is only one window in this container");
}

Expand Down Expand Up @@ -3270,6 +3270,10 @@ impl WindowManager {
.ok_or_else(|| anyhow!("there is no container"))
}

pub fn focused_container_idx(&self) -> Result<usize> {
Ok(self.focused_workspace()?.focused_container_idx())
}

pub fn focused_container_mut(&mut self) -> Result<&mut Container> {
self.focused_workspace_mut()?
.focused_container_mut()
Expand Down
43 changes: 43 additions & 0 deletions komorebi/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ impl Default for Workspace {
}
}

#[derive(Debug)]
pub enum WorkspaceWindowLocation {
Monocle(usize), // window_idx
Maximized,
Container(usize, usize), // container_idx, window_idx
Floating(usize), // idx in floating_windows
}

impl Workspace {
pub fn load_static_config(&mut self, config: &WorkspaceConfig) -> Result<()> {
self.name = Option::from(config.name.clone());
Expand Down Expand Up @@ -579,6 +587,41 @@ impl Workspace {
None
}

pub fn location_from_exe(&self, exe: &str) -> Option<WorkspaceWindowLocation> {
for (container_idx, container) in self.containers().iter().enumerate() {
if let Some(window_idx) = container.idx_from_exe(exe) {
return Some(WorkspaceWindowLocation::Container(
container_idx,
window_idx,
));
}
}

if let Some(window) = self.maximized_window() {
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Some(WorkspaceWindowLocation::Maximized);
}
}
}

if let Some(container) = self.monocle_container() {
if let Some(window_idx) = container.idx_from_exe(exe) {
return Some(WorkspaceWindowLocation::Monocle(window_idx));
}
}

for (window_idx, window) in self.floating_windows().iter().enumerate() {
if let Ok(window_exe) = window.exe() {
if exe == window_exe {
return Some(WorkspaceWindowLocation::Floating(window_idx));
}
}
}

None
}

pub fn contains_managed_window(&self, hwnd: isize) -> bool {
for container in self.containers() {
if container.contains_window(hwnd) {
Expand Down
12 changes: 12 additions & 0 deletions komorebic/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,12 @@ struct ReplaceConfiguration {
path: PathBuf,
}

#[derive(Parser)]
struct EagerFocus {
/// Case-sensitive exe identifier
exe: String,
}

#[derive(Parser)]
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
struct Opts {
Expand Down Expand Up @@ -1020,6 +1026,9 @@ enum SubCommand {
/// Move the focused window in the specified cycle direction
#[clap(arg_required_else_help = true)]
CycleMove(CycleMove),
/// Focus the first managed window matching the given exe
#[clap(arg_required_else_help = true)]
EagerFocus(EagerFocus),
/// Stack the focused window in the specified direction
#[clap(arg_required_else_help = true)]
Stack(Stack),
Expand Down Expand Up @@ -1726,6 +1735,9 @@ fn main() -> Result<()> {
SubCommand::CycleMove(arg) => {
send_message(&SocketMessage::CycleMoveWindow(arg.cycle_direction))?;
}
SubCommand::EagerFocus(arg) => {
send_message(&SocketMessage::EagerFocus(arg.exe))?;
}
SubCommand::MoveToMonitor(arg) => {
send_message(&SocketMessage::MoveContainerToMonitorNumber(arg.target))?;
}
Expand Down

0 comments on commit e9bb6b4

Please sign in to comment.