Skip to content

Commit 266abd3

Browse files
committed
Add support for Linux.
smithay
1 parent 6935ca5 commit 266abd3

9 files changed

Lines changed: 3877 additions & 1760 deletions

File tree

time_tracker/Cargo.lock

Lines changed: 3150 additions & 1232 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

time_tracker/Cargo.toml

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,35 @@
22
name = "time_tracker"
33
version = "0.1.0"
44
authors = ["Michael Kaltschmid <kaltschmidmichael@gmail.com>"]
5-
edition = "2018"
5+
edition = "2024"
66

77
[dependencies]
88
libc = "0.2.66"
99
reqwest = { version = "0.9.24", optional = true }
1010
serde_json = "1.0.44"
1111
serde = "1.0.104"
12-
crossbeam-channel = "0.4.0"
13-
chrono = "0.4.10"
12+
crossbeam-channel = "0.5.0"
13+
chrono = "0.4.41"
1414
log = "0.4.8"
1515
env_logger = "0.7.1"
1616
jsonrpc-core = "14.0.5"
1717
jsonrpc-http-server = "14.0.5"
18-
lazy_static = "1.4.0"
18+
lazy_static = "1.5.0"
1919
postgres = { version = "0.15.2", features = ["with-chrono"], optional = true }
2020
inputbot = "0.4.0"
21-
rodio = { version = "0.10.0", default-features = false, features = ["vorbis"] }
22-
rust-embed = "5.2.0"
21+
rodio = { version = "0.21.1", default-features = false, features = ["playback"] }
2322
barrel = { version = "0.6.5", features = ["pg"], optional = true }
2423
toml = "0.5.6"
25-
serde_derive = "1.0.106"
24+
serde_derive = "1.0.219"
25+
rust-embed = "8.7.2"
26+
27+
[target.'cfg(not(windows))'.dependencies]
28+
zbus = { version = "3.14", default-features = false, features = ["tokio"] }
29+
tokio = { version = "1.47.1", features = ["rt", "rt-multi-thread"] }
30+
procfs = "0.15"
31+
dirs = "5.0"
32+
sysinfo = "0.37.0"
33+
niri-ipc = "25.5.1"
2634

2735
[target.'cfg(windows)'.dependencies]
2836
winapi = { version = "0.3.8", features = ["tlhelp32", "handleapi", "winver", "winuser", "wincon", "shellapi"] }

time_tracker/src/hook.rs

Lines changed: 53 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,67 @@
11
use std::{
2-
thread,
3-
sync::{Arc, RwLock},
4-
io::{Cursor, BufReader}
2+
io::{BufReader, Cursor},
3+
sync::{Arc, RwLock},
4+
thread,
55
};
66

77
use inputbot::{KeybdKey::*, *};
8-
use rodio::{default_output_device, play_once};
98

9+
use crate::Asset;
1010
use crate::native::get_foreground_meta;
1111
use crate::restable::Restable;
1212
use crate::time_tracking::{add_process, pause};
13-
use crate::Asset;
1413

15-
pub fn init_hook<T>(client: T) where T : Restable + Sync + Send + 'static {
16-
let client_arc = Arc::new(RwLock::new(client));
17-
18-
thread::spawn(move || {
19-
let device = Arc::new(default_output_device().unwrap());
20-
let cursor = Arc::new(Cursor::new(Asset::get("when.ogg").unwrap()));
21-
22-
let add_device_ref = device.clone();
23-
let add_cursor_ref = cursor.clone();
24-
25-
HomeKey.bind(move || {
26-
if LShiftKey.is_pressed() && LControlKey.is_pressed() {
27-
if let (Some(path), Some(file_name)) = get_foreground_meta() {
28-
if let Err(e) = add_process(&file_name, &path, &client_arc) {
29-
error!("{}", e);
30-
} else {
31-
let when = play_once(&(*add_device_ref), BufReader::new((*add_cursor_ref).clone())).unwrap();
32-
when.set_volume(0.1);
33-
when.detach();
34-
}
35-
} else {
36-
error!("Cannot add process.");
37-
}
38-
}
39-
});
14+
pub fn init_hook<T>(client: T)
15+
where
16+
T: Restable + Sync + Send + 'static,
17+
{
18+
let client_arc = Arc::new(RwLock::new(client));
4019

41-
let pause_device_ref = device.clone();
42-
let pause_cursor_ref = cursor.clone();
20+
thread::spawn(move || {
21+
let stream_handle = Arc::new(
22+
rodio::OutputStreamBuilder::open_default_stream().expect("open default audio stream"),
23+
);
4324

44-
PKey.bind(move || {
45-
if LShiftKey.is_pressed() && LControlKey.is_pressed() {
46-
let when = play_once(&(*pause_device_ref), BufReader::new((*pause_cursor_ref).clone())).unwrap();
47-
when.set_volume(0.1);
48-
when.detach();
25+
let embedded = Asset::get("when.ogg").expect("missing asset");
4926

50-
pause();
51-
}
52-
});
27+
let cursor = Arc::new(Cursor::new(embedded.data));
28+
29+
let add_device_ref = stream_handle.clone();
30+
let add_cursor_ref = cursor.clone();
31+
32+
HomeKey.bind(move || {
33+
if LShiftKey.is_pressed() && LControlKey.is_pressed() {
34+
if let (Some(path), Some(file_name)) = get_foreground_meta() {
35+
if let Err(e) = add_process(&file_name, &path, &client_arc) {
36+
error!("{}", e);
37+
} else {
38+
let file = BufReader::new((*add_cursor_ref).clone());
5339

54-
handle_input_events();
55-
});
40+
rodio::play((add_device_ref).clone().mixer(), file)
41+
.map(|op| op.set_volume(0.1))
42+
.expect("Cannot play audio");
43+
}
44+
} else {
45+
error!("Cannot add process.");
46+
}
47+
}
48+
});
49+
50+
let pause_device_ref = stream_handle.clone();
51+
let pause_cursor_ref = cursor.clone();
52+
53+
PKey.bind(move || {
54+
if LShiftKey.is_pressed() && LControlKey.is_pressed() {
55+
let file = BufReader::new((*pause_cursor_ref).clone());
56+
57+
rodio::play((pause_device_ref).clone().mixer(), file)
58+
.map(|op| op.set_volume(0.1))
59+
.expect("Cannot play audio.");
60+
61+
pause();
62+
}
63+
});
64+
65+
handle_input_events();
66+
});
5667
}

time_tracker/src/linux.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
use std::{
2+
collections::HashMap,
3+
env,
4+
error::Error as Std_Error,
5+
fs,
6+
io::{Error, ErrorKind},
7+
process::Command,
8+
thread,
9+
};
10+
11+
use procfs::process::Process;
12+
use sysinfo::{Pid, System};
13+
14+
use niri_ipc::socket::Socket;
15+
use niri_ipc::{Request, Response};
16+
17+
fn is_x11_session() -> bool {
18+
env::var("XDG_SESSION_TYPE").map_or(false, |session_type| session_type == "x11")
19+
}
20+
21+
fn is_niri_session() -> bool {
22+
env::var("NIRI_SOCKET").is_ok()
23+
}
24+
25+
fn get_active_window_x11() -> Result<(String, String), Box<dyn Std_Error>> {
26+
// Get active window ID using xprop
27+
let active_output = Command::new("xprop")
28+
.args(["-root", "_NET_ACTIVE_WINDOW"])
29+
.output()?;
30+
31+
if !active_output.status.success() {
32+
return Err("xprop failed to get active window".into());
33+
}
34+
35+
let active_line = String::from_utf8_lossy(&active_output.stdout);
36+
let window_id = active_line
37+
.split_whitespace()
38+
.last()
39+
.ok_or("Could not parse window ID")?;
40+
41+
// Get window list with PIDs using wmctrl
42+
let output = Command::new("wmctrl").args(["-l", "-p"]).output()?;
43+
44+
if !output.status.success() {
45+
return Err("wmctrl failed".into());
46+
}
47+
48+
let window_list = String::from_utf8_lossy(&output.stdout);
49+
50+
// Find the active window in the list
51+
for line in window_list.lines() {
52+
if line.starts_with(window_id) {
53+
let parts: Vec<&str> = line.split_whitespace().collect();
54+
if parts.len() >= 3 {
55+
if let Ok(pid) = parts[2].parse::<i32>() {
56+
if let Ok(process) = Process::new(pid) {
57+
if let Ok(exe) = process.exe() {
58+
let path = exe.to_string_lossy().into_owned();
59+
let name = exe
60+
.file_name()
61+
.map(|n| n.to_string_lossy().into_owned())
62+
.unwrap_or_else(|| "unknown".to_string());
63+
return Ok((path, name));
64+
}
65+
}
66+
}
67+
}
68+
}
69+
}
70+
71+
Err("Could not determine active window".into())
72+
}
73+
74+
fn get_active_window_niri() -> Result<(String, String), Box<dyn Std_Error>> {
75+
let mut socket = Socket::connect()?;
76+
77+
socket
78+
.send(Request::Windows)?
79+
.ok()
80+
.and_then(|resp| {
81+
if let Response::Windows(ws) = resp {
82+
Some(ws)
83+
} else {
84+
None
85+
}
86+
})
87+
.and_then(|windows| windows.into_iter().find(|w| w.is_focused))
88+
.and_then(|window| window.pid)
89+
.and_then(|pid| {
90+
let sys = System::new_all();
91+
let sys_pid = Pid::from(pid as usize);
92+
93+
sys.process(sys_pid).map(|proc| {
94+
let exe = proc
95+
.exe()
96+
.map(|exe| exe.to_string_lossy().into_owned())
97+
.unwrap_or_default();
98+
99+
let cmd = proc
100+
.cmd()
101+
.iter()
102+
.map(|os| os.to_string_lossy())
103+
.collect::<Vec<_>>()
104+
.join(" ");
105+
106+
(exe, cmd)
107+
})
108+
})
109+
.ok_or("Could not determine active window".into())
110+
}
111+
112+
pub fn ux_are_processes_running<'a>(
113+
processes: &'a [String],
114+
) -> Result<HashMap<&'a String, bool>, Error> {
115+
let mut map = HashMap::new();
116+
let sys = System::new_all();
117+
118+
for (_, process) in sys.processes() {
119+
let process_name = process.name().to_str().unwrap_or("");
120+
for target_process in processes {
121+
if target_process == &format!("{}.exe", process_name) {
122+
map.insert(target_process, true);
123+
}
124+
}
125+
}
126+
127+
Ok(map)
128+
}
129+
130+
pub fn ux_get_foreground_meta() -> (Option<String>, Option<String>) {
131+
if is_x11_session() {
132+
match get_active_window_x11() {
133+
Ok((path, name)) => (Some(path), Some(name)),
134+
Err(_) => (None, None),
135+
}
136+
} else {
137+
if is_niri_session() {
138+
match get_active_window_niri() {
139+
Ok((path, name)) => (Some(path), Some(name)),
140+
Err(_) => (None, None),
141+
}
142+
} else {
143+
(None, None)
144+
}
145+
}
146+
}
147+
148+
pub fn init_tray() {
149+
thread::spawn(move || {});
150+
}

0 commit comments

Comments
 (0)