Skip to content

Commit 367a904

Browse files
committed
Port IPC to Rust, delete the app folder finally! There's still one weird quirk with the window restoration behavior.
1 parent 27c6b9b commit 367a904

File tree

7 files changed

+162
-175
lines changed

7 files changed

+162
-175
lines changed

app/app.cpp

Lines changed: 0 additions & 142 deletions
This file was deleted.

app/app.hpp

Lines changed: 0 additions & 19 deletions
This file was deleted.

po/paperback.pot

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ msgid ""
88
msgstr ""
99
"Project-Id-Version: paperback 0.8.0\n"
1010
"Report-Msgid-Bugs-To: https://github.com/trypsynth/paperback/issues\n"
11-
"POT-Creation-Date: 2026-01-31 11:48-0700\n"
11+
"POT-Creation-Date: 2026-02-05 17:28-0700\n"
1212
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
1313
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1414
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,6 +17,14 @@ msgstr ""
1717
"Content-Type: text/plain; charset=CHARSET\n"
1818
"Content-Transfer-Encoding: 8bit\n"
1919

20+
#: C:\Users\Quin\git\paperback\src\ui\app.rs
21+
msgid "Failed to create IPC server"
22+
msgstr ""
23+
24+
#: C:\Users\Quin\git\paperback\src\ui\app.rs
25+
msgid "Warning"
26+
msgstr ""
27+
2028
#: C:\Users\Quin\git\paperback\src\ui\dialogs.rs
2129
#: C:\Users\Quin\git\paperback\src\ui\find.rs
2230
msgid "Options"

src/ipc.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use std::{env, path::{Path, PathBuf}};
2+
3+
pub const IPC_SERVICE: &str = "4242";
4+
pub const IPC_TOPIC_OPEN_FILE: &str = "open_file";
5+
pub const IPC_COMMAND_ACTIVATE: &str = "ACTIVATE";
6+
pub const IPC_HOST_LOCALHOST: &str = "localhost";
7+
pub const SINGLE_INSTANCE_NAME: &str = "paperback_running";
8+
9+
#[derive(Debug, Clone)]
10+
pub enum IpcCommand {
11+
Activate,
12+
OpenFile(PathBuf),
13+
}
14+
15+
pub fn decode_execute_payload(data: &[u8]) -> Option<IpcCommand> {
16+
if data.is_empty() {
17+
return None;
18+
}
19+
let payload = String::from_utf8_lossy(data);
20+
let payload = payload.trim_end_matches('\0');
21+
if payload.is_empty() {
22+
return None;
23+
}
24+
if payload == IPC_COMMAND_ACTIVATE {
25+
return Some(IpcCommand::Activate);
26+
}
27+
Some(IpcCommand::OpenFile(PathBuf::from(payload)))
28+
}
29+
30+
pub fn normalize_cli_path(path: &Path) -> PathBuf {
31+
if let Ok(normalized) = path.canonicalize() {
32+
return normalized;
33+
}
34+
if path.is_absolute() {
35+
return path.to_path_buf();
36+
}
37+
env::current_dir().map_or_else(|_| path.to_path_buf(), |cwd| cwd.join(path))
38+
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
mod config;
55
mod document;
66
mod html_to_text;
7+
mod ipc;
78
mod parser;
89
mod reader_core;
910
mod session;

src/ui/app.rs

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,35 @@
11
use std::{
22
env,
3-
path::{Path, PathBuf},
3+
path::Path,
4+
process,
45
rc::Rc,
5-
sync::Mutex,
6+
sync::{
7+
Mutex,
8+
atomic::{AtomicUsize, Ordering},
9+
},
610
};
711

8-
use wxdragon::prelude::*;
12+
use wxdragon::{prelude::*, translations::translate as t};
913

1014
use super::MainWindow;
11-
use crate::{config::ConfigManager, translation_manager::TranslationManager};
15+
use crate::{
16+
config::ConfigManager,
17+
ipc::{
18+
IPC_COMMAND_ACTIVATE, IPC_HOST_LOCALHOST, IPC_SERVICE, IPC_TOPIC_OPEN_FILE, IpcCommand, SINGLE_INSTANCE_NAME,
19+
decode_execute_payload, normalize_cli_path,
20+
},
21+
translation_manager::TranslationManager,
22+
};
1223

1324
pub struct PaperbackApp {
1425
_config: Rc<Mutex<ConfigManager>>,
15-
_main_window: MainWindow,
26+
_main_window: Rc<MainWindow>,
27+
_ipc_server: Option<IPCServer>,
28+
_single_instance_checker: Option<SingleInstanceChecker>,
1629
}
1730

31+
static MAIN_WINDOW_PTR: AtomicUsize = AtomicUsize::new(0);
32+
1833
impl PaperbackApp {
1934
pub fn new(_app: App) -> Self {
2035
let mut config = ConfigManager::new();
@@ -28,14 +43,28 @@ impl PaperbackApp {
2843
}
2944
}
3045
let config = Rc::new(Mutex::new(config));
31-
let main_window = MainWindow::new(Rc::clone(&config));
46+
let single_instance_checker = SingleInstanceChecker::new(SINGLE_INSTANCE_NAME, None);
47+
if let Some(checker) = single_instance_checker.as_ref() {
48+
if checker.is_another_running() {
49+
send_ipc_command(ipc_command_from_cli());
50+
process::exit(0);
51+
}
52+
}
53+
let main_window = Rc::new(MainWindow::new(Rc::clone(&config)));
54+
MAIN_WINDOW_PTR.store(Rc::as_ptr(&main_window) as usize, Ordering::SeqCst);
3255
wxdragon::app::set_top_window(main_window.frame());
56+
let ipc_server = start_ipc_server(Rc::clone(&main_window));
3357
main_window.show();
3458
open_from_command_line(&main_window);
3559
if config.lock().unwrap().get_app_bool("check_for_updates_on_startup", true) {
3660
MainWindow::check_for_updates(true);
3761
}
38-
Self { _config: config, _main_window: main_window }
62+
Self {
63+
_config: config,
64+
_main_window: main_window,
65+
_ipc_server: ipc_server,
66+
_single_instance_checker: single_instance_checker,
67+
}
3968
}
4069
}
4170

@@ -46,12 +75,63 @@ fn open_from_command_line(main_window: &MainWindow) {
4675
}
4776
}
4877

49-
fn normalize_cli_path(path: &Path) -> PathBuf {
50-
if let Ok(normalized) = path.canonicalize() {
51-
return normalized;
78+
fn main_window_from_ptr() -> Option<&'static MainWindow> {
79+
let ptr = MAIN_WINDOW_PTR.load(Ordering::SeqCst);
80+
if ptr == 0 {
81+
return None;
5282
}
53-
if path.is_absolute() {
54-
return path.to_path_buf();
83+
unsafe { (ptr as *const MainWindow).as_ref() }
84+
}
85+
86+
fn ipc_command_from_cli() -> IpcCommand {
87+
if let Some(path) = env::args().nth(1) {
88+
let normalized = normalize_cli_path(Path::new(&path));
89+
return IpcCommand::OpenFile(normalized);
90+
}
91+
IpcCommand::Activate
92+
}
93+
94+
fn send_ipc_command(command: IpcCommand) {
95+
let client = IPCClient::new();
96+
let Some(conn) = client.make_connection(IPC_HOST_LOCALHOST, IPC_SERVICE, IPC_TOPIC_OPEN_FILE) else {
97+
return;
98+
};
99+
let payload = match command {
100+
IpcCommand::Activate => IPC_COMMAND_ACTIVATE.to_string(),
101+
IpcCommand::OpenFile(path) => path.to_string_lossy().to_string(),
102+
};
103+
let _ = conn.execute_string(&payload);
104+
let _ = conn.disconnect();
105+
}
106+
107+
fn start_ipc_server(main_window: Rc<MainWindow>) -> Option<IPCServer> {
108+
let server = IPCServer::new(move |topic| {
109+
if topic != IPC_TOPIC_OPEN_FILE {
110+
return None;
111+
}
112+
Some(
113+
IPCConnection::builder()
114+
.on_execute({
115+
move |_topic, data, _format| {
116+
let Some(command) = decode_execute_payload(data) else {
117+
return false;
118+
};
119+
wxdragon::call_after(Box::new(move || {
120+
if let Some(window) = main_window_from_ptr() {
121+
window.handle_ipc_command(command);
122+
}
123+
}));
124+
true
125+
}
126+
})
127+
.build(),
128+
)
129+
});
130+
if !server.create(IPC_SERVICE) {
131+
let dialog = MessageDialog::builder(main_window.frame(), &t("Failed to create IPC server"), &t("Warning"))
132+
.with_style(MessageDialogStyle::OK | MessageDialogStyle::IconWarning | MessageDialogStyle::Centre)
133+
.build();
134+
dialog.show_modal();
55135
}
56-
env::current_dir().map_or_else(|_| path.to_path_buf(), |cwd| cwd.join(path))
136+
Some(server)
57137
}

0 commit comments

Comments
 (0)