Skip to content

Commit 5cb0841

Browse files
committed
add support for focus switching
1 parent 0c5fa1e commit 5cb0841

18 files changed

Lines changed: 333 additions & 182 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[workspace]
2-
members = ["crates/sidecar", "crates/bridge"]
2+
members = ["crates/sidecar", "crates/bridge", "crates/core"]
33
resolver = "2"

bb.edn

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,4 @@
44
setup {:task (apply defold/run-wrapped :setup *command-line-args*)}
55
install-dependencies {:task (apply defold/run-wrapped :install-dependencies *command-line-args*)}
66
list-dependency-dirs {:task (apply defold/run-wrapped :list-dependency-dirs *command-line-args*)}
7-
focus-neovim {:task (apply defold/run-wrapped :focus-neovim *command-line-args*)}
8-
focus-game {:task (apply defold/run-wrapped :focus-game *command-line-args*)}
97
mobdap-path {:task (apply defold/run-wrapped :mobdap-path *command-line-args*)}}}

crates/bridge/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ edition = "2024"
66
[dependencies]
77
anyhow = "1.0.100"
88
clap = { version = "4.5.53", features = ["derive"] }
9+
defold-nvim-core = { path = "../core" }
910
dirs = "6.0.0"
1011
netstat2 = "0.11.2"
1112
serde = { version = "1.0.228", features = ["derive"] }
1213
serde_json = "1.0.145"
13-
sha3 = "0.10.8"
1414
tracing = "0.1.43"
1515
tracing-appender = "0.2.4"
1616
tracing-subscriber = "0.3.22"

crates/bridge/src/launcher.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ use std::{
66
};
77

88
use anyhow::{Context, Result, bail};
9+
use defold_nvim_core::{focus::focus_neovim, utils::classname};
910
use serde::Deserialize;
1011
use which::which;
1112

1213
use crate::{
1314
plugin_config::{LauncherConfig, LauncherType, PluginConfig, SocketType},
14-
utils::{self, classname, is_port_in_use},
15+
utils::{self, is_port_in_use},
1516
};
1617

1718
const ERR_NEOVIDE_NOT_FOUND: &'static str = "Could not find Neovide, have you installed it?";
@@ -430,18 +431,20 @@ pub fn run(
430431
let launcher = launcher.apply_var(VAR_REMOTE_CMD, remote_cmd.clone());
431432

432433
match config.plugin_config.launcher.and_then(|l| l.socket_type) {
433-
Some(SocketType::Fsock) => run_fsock(launcher, &nvim, root_dir, &remote_cmd)?,
434-
Some(SocketType::Netsock) => run_netsock(launcher, &nvim, root_dir, &remote_cmd)?,
434+
Some(SocketType::Fsock) => run_fsock(launcher, &nvim, root_dir.clone(), &remote_cmd)?,
435+
Some(SocketType::Netsock) => run_netsock(launcher, &nvim, root_dir.clone(), &remote_cmd)?,
435436
None => {
436437
if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
437-
run_fsock(launcher, &nvim, root_dir, &remote_cmd)?
438+
run_fsock(launcher, &nvim, root_dir.clone(), &remote_cmd)?
438439
} else {
439-
run_netsock(launcher, &nvim, root_dir, &remote_cmd)?
440+
run_netsock(launcher, &nvim, root_dir.clone(), &remote_cmd)?
440441
}
441442
}
442443
}
443444

444-
// TODO: switch focus
445+
if let Err(err) = focus_neovim(root_dir) {
446+
tracing::error!("Could not switch focus to neovim {err:?}");
447+
}
445448

446449
Ok(())
447450
}

crates/bridge/src/main.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::{env, fs, io, path::absolute};
22

33
use anyhow::{Context, Result};
44
use clap::{Parser, Subcommand, command};
5+
use defold_nvim_core::focus::{focus_game, focus_neovim};
56
use tracing::Level;
67
use tracing_appender::rolling::daily;
78
use tracing_subscriber::fmt::writer::MakeWriterExt;
@@ -19,6 +20,7 @@ struct Args {
1920

2021
#[derive(Subcommand, Debug)]
2122
enum Commands {
23+
/// Open a file in Neovim or launch a new instance
2224
LaunchNeovim {
2325
#[clap(value_name = "LAUNCH_CONFIG", index = 1)]
2426
launch_config: String,
@@ -32,6 +34,16 @@ enum Commands {
3234
#[clap(value_name = "LINE", index = 4)]
3335
line: Option<usize>,
3436
},
37+
/// Focus the currently open instance of Neovim
38+
FocusNeovim {
39+
#[clap(value_name = "GAME_ROOT_DIR", index = 1)]
40+
game_root_dir: String,
41+
},
42+
/// Focus the game
43+
FocusGame {
44+
#[clap(value_name = "GAME_ROOT_DIR", index = 1)]
45+
game_root_dir: String,
46+
},
3547
}
3648

3749
fn main() -> Result<()> {
@@ -73,6 +85,8 @@ fn main() -> Result<()> {
7385
absolute(file)?,
7486
line,
7587
)?,
88+
Commands::FocusNeovim { game_root_dir } => focus_neovim(absolute(game_root_dir)?)?,
89+
Commands::FocusGame { game_root_dir } => focus_game(absolute(game_root_dir)?)?,
7690
}
7791

7892
Ok(())

crates/bridge/src/utils.rs

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,8 @@
11
use std::{fs, net::TcpListener, path::PathBuf};
22

33
use anyhow::{Context, Result};
4+
use defold_nvim_core::utils::project_id;
45
use netstat2::{AddressFamilyFlags, ProtocolFlags, get_sockets_info};
5-
use sha3::{Digest, Sha3_256};
6-
7-
pub fn sha3(str: &str) -> String {
8-
let mut hasher = Sha3_256::new();
9-
hasher.update(str.as_bytes());
10-
let result = hasher.finalize();
11-
12-
format!("{:x}", result)
13-
}
14-
15-
pub fn project_id(root_dir: &str) -> Result<String> {
16-
Ok(sha3(root_dir)
17-
.get(0..8)
18-
.context("could not create project id")?
19-
.to_string())
20-
}
21-
22-
pub fn classname(root_dir: &str) -> Result<String> {
23-
Ok(format!("com.defold.nvim.{}", project_id(root_dir)?))
24-
}
256

267
pub fn runtime_dir(root_dir: &str) -> Result<PathBuf> {
278
let dir = dirs::cache_dir()

crates/core/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "defold-nvim-core"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
anyhow = "1.0.100"
8+
rust-ini = "0.21.3"
9+
serde = { version = "1.0.228", features = ["derive"] }
10+
sha3 = "0.10.8"
11+
strum = { version = "0.27.2", features = ["derive"] }
12+
tracing = "0.1.43"
13+
which = "8.0.0"

crates/core/src/focus.rs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
use std::path::PathBuf;
2+
3+
use anyhow::{Context, Result, bail};
4+
use std::process::Command;
5+
use which::which;
6+
7+
use strum::IntoEnumIterator;
8+
9+
use crate::{game_project::GameProject, utils::classname};
10+
11+
#[derive(Debug)]
12+
enum SwitcherType {
13+
Class(String),
14+
Title(String),
15+
AppName(String),
16+
}
17+
18+
impl SwitcherType {
19+
fn value(&self) -> String {
20+
match self {
21+
SwitcherType::Class(c) => c.clone(),
22+
SwitcherType::Title(t) => t.clone(),
23+
SwitcherType::AppName(a) => a.clone(),
24+
}
25+
}
26+
}
27+
28+
#[derive(Debug, strum::EnumIter)]
29+
enum Switcher {
30+
#[cfg(target_os = "linux")]
31+
HyprCtl,
32+
33+
#[cfg(target_os = "linux")]
34+
SwayMsg,
35+
36+
#[cfg(target_os = "linux")]
37+
WmCtrl,
38+
39+
#[cfg(target_os = "linux")]
40+
XDoTool,
41+
42+
#[cfg(target_os = "macos")]
43+
OsaScript,
44+
}
45+
46+
impl Switcher {
47+
fn path(&self) -> Option<PathBuf> {
48+
#[cfg(target_os = "linux")]
49+
match self {
50+
Switcher::HyprCtl => which("hyprctl").ok(),
51+
Switcher::SwayMsg => which("swaymsg").ok(),
52+
Switcher::WmCtrl => which("wmctrl").ok(),
53+
Switcher::XDoTool => which("xdotool").ok(),
54+
}
55+
#[cfg(target_os = "macos")]
56+
match self {
57+
Switcher::OsaScript => which("osascript").ok(),
58+
}
59+
#[cfg(target_os = "windows")]
60+
None
61+
}
62+
63+
fn from_env() -> Option<Self> {
64+
Self::iter().find(|sw| sw.path().is_some())
65+
}
66+
}
67+
68+
fn switch(switcher_type: SwitcherType) -> Result<()> {
69+
tracing::info!("Switching to {switcher_type:?}");
70+
71+
let Some(switcher) = Switcher::from_env() else {
72+
tracing::error!("No supported focus switcher found, do nothing...");
73+
return Ok(());
74+
};
75+
76+
#[cfg(target_os = "linux")]
77+
return match switcher {
78+
Switcher::HyprCtl => {
79+
Command::new(switcher.path().unwrap())
80+
.arg("dispatch")
81+
.arg("focuswindow")
82+
.arg(match switcher_type {
83+
SwitcherType::Class(class) => format!("class:{class}"),
84+
SwitcherType::Title(title) => format!("title:{title}"),
85+
_ => bail!("Unsupported switcher type {switcher_type:?} for {switcher:?}"),
86+
})
87+
.spawn()?
88+
.wait()?;
89+
90+
Ok(())
91+
}
92+
Switcher::SwayMsg => {
93+
Command::new(switcher.path().unwrap())
94+
.arg(format!(
95+
"[{}={}] focus",
96+
match switcher_type {
97+
SwitcherType::Class(_) => format!("class"),
98+
SwitcherType::Title(_) => format!("title"),
99+
_ => bail!("Unsupported switcher type {switcher_type:?} for {switcher:?}"),
100+
},
101+
switcher_type.value(),
102+
))
103+
.spawn()?
104+
.wait()?;
105+
106+
Ok(())
107+
}
108+
Switcher::WmCtrl => {
109+
let mut cmd = Command::new(switcher.path().unwrap());
110+
111+
if matches!(switcher_type, SwitcherType::Class(_)) {
112+
cmd.arg("-x");
113+
}
114+
115+
cmd.arg("-a").arg(switcher_type.value()).spawn()?.wait()?;
116+
117+
Ok(())
118+
}
119+
Switcher::XDoTool => {
120+
Command::new(switcher.path().unwrap())
121+
.arg("search")
122+
.arg(match switcher_type {
123+
SwitcherType::Class(_) => format!("--class"),
124+
SwitcherType::Title(_) => format!("--title"),
125+
_ => bail!("Unsupported switcher type {switcher_type:?} for {switcher:?}"),
126+
})
127+
.arg(switcher_type.value())
128+
.arg("windowactivate")
129+
.spawn()?
130+
.wait()?;
131+
132+
Ok(())
133+
}
134+
};
135+
136+
#[cfg(target_os = "macos")]
137+
return match switcher {
138+
Switcher::OsaScript => {
139+
Command::new(switcher.path().unwrap())
140+
.arg("-e")
141+
.arg(match switcher_type {
142+
SwitcherType::AppName(app_name) => format!("'tell application \"System Events\" to tell process \"{app_name}\" to set frontmost to true'"),
143+
_ => bail!("Unsupported switcher type {switcher_type:?} for {switcher:?}"),
144+
})
145+
.spawn()?
146+
.wait()?;
147+
148+
Ok(())
149+
}
150+
};
151+
152+
#[cfg(target_os = "windows")]
153+
return Ok(());
154+
}
155+
156+
pub fn focus_neovim(root_dir: PathBuf) -> Result<()> {
157+
if !root_dir.join("game.project").exists() {
158+
bail!("Could not find game.project file in {root_dir:?}: Not a valid Defold directory");
159+
}
160+
161+
if cfg!(target_os = "linux") {
162+
let class = classname(
163+
root_dir
164+
.to_str()
165+
.context("could not convert path to string")?,
166+
)?;
167+
168+
return switch(SwitcherType::Class(class));
169+
}
170+
171+
tracing::error!("Focus switching to Neovim is not support on current platform");
172+
173+
Ok(())
174+
}
175+
176+
pub fn focus_game(root_dir: PathBuf) -> Result<()> {
177+
if !root_dir.join("game.project").exists() {
178+
bail!("Could not find game.project file in {root_dir:?}: Not a valid Defold directory");
179+
}
180+
181+
let game_project = GameProject::load_from_path(root_dir.join("game.project"))?;
182+
183+
if cfg!(target_os = "linux") {
184+
return switch(SwitcherType::Title(game_project.title));
185+
} else if cfg!(target_os = "macos") {
186+
return switch(SwitcherType::AppName(game_project.title));
187+
}
188+
189+
tracing::error!("Focus switching to the Game is not support on current platform");
190+
191+
Ok(())
192+
}
Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use std::{fs, path::PathBuf};
22

33
use anyhow::{Context, Result, bail};
44
use ini::Ini;
5-
use mlua::UserData;
65
use serde::Serialize;
76

87
#[derive(Debug, Serialize)]
@@ -21,8 +20,6 @@ impl GameProject {
2120
}
2221
}
2322

24-
impl UserData for GameProject {}
25-
2623
impl TryFrom<String> for GameProject {
2724
type Error = anyhow::Error;
2825

crates/core/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod focus;
2+
pub mod game_project;
3+
pub mod utils;

0 commit comments

Comments
 (0)