aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-21 16:30:04 +0300
committerGitHub <noreply@github.com>2026-04-21 16:30:04 +0300
commit3397e1ce2922c4ae9b814e8110e81de7d201de81 (patch)
treec457a7cf0af271dea1a412474fad5a33c1b60be8 /src/main.rs
parent2818ff492d45d17fd4f9b15478d703c6c664a8c0 (diff)
feat: add persistent server-client sessions
Make herdr persistent by default. Launching herdr now starts or reattaches to a background session server. Clients can detach and reattach while panes and agent processes keep running. Session mode now supports multi-client attach, auto-detect startup, and a thin-client/headless-server split. This also refactors the large app, ui, input, pane, config, workspace, and persistence modules into smaller focused submodules, while preserving behavior and colocating tests with the code they exercise. Upgrade notes: - persistence mode is now the default - in-app quit detaches the current client instead of stopping the server - use `herdr server stop` to stop the background session - use `--no-session` for the old single-process behavior - default socket paths now live under the config directory
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs85
1 files changed, 49 insertions, 36 deletions
diff --git a/src/main.rs b/src/main.rs
index c990f793..2637668e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -21,19 +21,23 @@ const NESTED_HERDR_MESSAGES: [&str; 6] = [
mod api;
mod app;
mod cli;
+mod client;
mod config;
mod detect;
mod events;
mod ghostty;
mod input;
mod integration;
+mod ipc;
mod layout;
+mod logging;
mod pane;
mod persist;
mod platform;
mod raw_input;
mod release_notes;
mod selection;
+mod server;
mod sound;
mod terminal_theme;
mod ui;
@@ -41,34 +45,7 @@ mod update;
mod workspace;
fn init_logging() {
- use std::fs::{self, OpenOptions};
- use tracing_subscriber::EnvFilter;
-
- let log_dir = crate::config::config_dir();
- let _ = fs::create_dir_all(&log_dir);
- let log_path = log_dir.join("herdr.log");
-
- // Rotate: truncate if over 5MB
- if let Ok(meta) = fs::metadata(&log_path) {
- if meta.len() > 5 * 1024 * 1024 {
- let _ = fs::remove_file(&log_path);
- }
- }
-
- let file = match OpenOptions::new().create(true).append(true).open(&log_path) {
- Ok(f) => f,
- Err(_) => return, // can't open log file, proceed without logging
- };
-
- let filter =
- EnvFilter::try_from_env("HERDR_LOG").unwrap_or_else(|_| EnvFilter::new("herdr=info"));
-
- tracing_subscriber::fmt()
- .with_env_filter(filter)
- .with_writer(file)
- .with_ansi(false)
- .with_target(false)
- .init();
+ crate::logging::init_file_logging("herdr.log");
}
const DEFAULT_CONFIG: &str = r##"# herdr configuration
@@ -102,9 +79,10 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
# Navigate-mode actions
# new_workspace = "n"
# rename_workspace = "shift+n"
-# close_workspace = "d"
+# close_workspace = "shift+d"
# previous_workspace = "" # optional, unset by default
# next_workspace = "" # optional, unset by default
+# detach = "" # optional explicit detach shortcut in server/client mode
# new_tab = "c"
# rename_tab = "" # optional, unset by default
# previous_tab = "" # optional, unset by default
@@ -184,6 +162,15 @@ fn main() -> io::Result<()> {
}
// Subcommands and flags (no TUI, no logging needed)
+ if args.get(1).map(|s| s.as_str()) == Some("server") {
+ return server::headless::run_server();
+ }
+
+ // Client mode: connect to an existing server's client socket.
+ if args.get(1).map(|s| s.as_str()) == Some("client") {
+ return client::run_client();
+ }
+
if args.get(1).map(|s| s.as_str()) == Some("update") {
match update::self_update() {
Ok(_) => return Ok(()),
@@ -199,6 +186,7 @@ fn main() -> io::Result<()> {
println!();
println!("Usage: herdr [options]");
println!(" herdr update");
+ println!(" herdr server stop");
println!(" herdr workspace <subcommand> ...");
println!(" herdr tab <subcommand> ...");
println!(" herdr pane <subcommand> ...");
@@ -206,7 +194,12 @@ fn main() -> io::Result<()> {
println!(" herdr integration <subcommand> ...");
println!();
println!("Commands:");
- println!(" update Download and install the latest version");
+ println!(" server Run as headless server (no terminal, persists after client disconnect)");
+ println!(" server stop Stop the running server via the API socket");
+ println!(" client Connect to a running server as a thin client");
+ println!(
+ " update Download and install the latest version (run outside herdr)"
+ );
println!(" workspace workspace helpers over the socket api");
println!(" tab tab helpers over the socket api");
println!(" pane pane control helpers over the socket api");
@@ -214,7 +207,7 @@ fn main() -> io::Result<()> {
println!(" integration manage built-in agent integrations");
println!();
println!("Options:");
- println!(" --no-session Don't restore or save sessions");
+ println!(" --no-session Run monolithically (no server/client, escape hatch)");
println!(" --default-config Print default configuration and exit");
println!(" --version, -V Print version and exit");
println!(" --show-changelog Preview the current version's release notes");
@@ -257,7 +250,16 @@ fn main() -> io::Result<()> {
std::process::exit(1);
}
if !arg.starts_with('-')
- && !["update", "workspace", "pane", "wait", "integration"].contains(&arg.as_str())
+ && ![
+ "server",
+ "client",
+ "update",
+ "workspace",
+ "pane",
+ "wait",
+ "integration",
+ ]
+ .contains(&arg.as_str())
{
eprintln!("unknown command: {arg}");
eprintln!("run 'herdr --help' for usage");
@@ -274,6 +276,18 @@ fn main() -> io::Result<()> {
std::process::exit(1);
}
+ let no_session = args.iter().any(|a| a == "--no-session");
+
+ // Auto-detect launch: when --no-session is NOT set, use server/client mode.
+ // Check if a server is running, spawn one if needed, then attach as client.
+ if !no_session {
+ init_logging();
+ return server::autodetect::auto_detect_launch();
+ }
+
+ // --- Monolithic mode (--no-session escape hatch) ---
+ // This is the pre-mission single-process behavior.
+
init_logging();
let (api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
@@ -288,8 +302,7 @@ fn main() -> io::Result<()> {
Err(err) => return Err(err),
};
- let no_session = std::env::args().any(|a| a == "--no-session");
- let show_changelog = std::env::args().any(|a| a == "--show-changelog");
+ let show_changelog = args.iter().any(|a| a == "--show-changelog");
let in_tmux = std::env::var("TMUX").is_ok();
let original_hook = std::panic::take_hook();
@@ -321,7 +334,7 @@ fn main() -> io::Result<()> {
loaded_config.diagnostics.len() - 1
))
};
- info!("herdr starting, pid={}", std::process::id());
+ info!("herdr starting (monolithic), pid={}", std::process::id());
// Background update check (non-blocking, best-effort)
// Only checks for newer versions and notifies the TUI.
@@ -362,7 +375,7 @@ fn main() -> io::Result<()> {
let mut app = app::App::new(
config,
- no_session,
+ true, // no_session — monolithic mode never saves/restores sessions
config_diagnostic,
startup_release_notes,
api_rx,