use std::ffi::{OsStr, OsString};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
#[cfg(feature = "control-mode")]
use crate::SessionId;
use crate::formats::TmuxText;
use crate::internal::core::Core;
#[cfg(test)]
use crate::internal::executor::Executor;
use crate::internal::listing;
#[cfg(feature = "control-mode")]
use crate::internal::process::PersistentChild;
use crate::internal::scoped;
use crate::pane::Pane;
use crate::session::Session;
use crate::{
Command, CommandChain, CommandResult, EngineCapabilities, Error, ReleaseSuffix, ReleaseVersion,
ServerConfigurationErrorKind, ServerGeneration, ServerIdentity,
};
mod builder;
mod channels;
mod discovery;
mod interactive;
mod settings;
pub use builder::ServerBuilder;
pub use discovery::{SessionTree, WindowTree};
#[cfg(feature = "query")]
pub use discovery::{SessionTreeFields, WindowTreeFields};
use crate::version::since::SERVER_ACCESS as SERVER_ACCESS_SINCE;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AccessMode {
ReadOnly,
Write,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AccessRule {
user: String,
mode: AccessMode,
}
impl AccessRule {
#[must_use]
pub fn user(&self) -> &str {
&self.user
}
#[must_use]
pub const fn mode(&self) -> AccessMode {
self.mode
}
}
use crate::version::since::PROMPT_HISTORY as PROMPT_HISTORY_SINCE;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PromptKind {
Command,
Search,
Target,
WindowTarget,
}
impl PromptKind {
const fn name(self) -> &'static str {
match self {
Self::Command => "command",
Self::Search => "search",
Self::Target => "target",
Self::WindowTarget => "window-target",
}
}
}
#[derive(Clone)]
pub struct Server {
core: Arc<Core>,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum ChannelWait {
Signalled,
TimedOut,
}
impl Server {
pub(crate) const fn from_core(core: Arc<Core>) -> Self {
Self { core }
}
#[cfg(feature = "control-mode")]
pub(crate) async fn spawn_control(
&self,
session: &SessionId,
) -> Result<PersistentChild, Error> {
self.core.spawn_control(session).await
}
pub fn new() -> Result<Self, Error> {
Self::builder().build()
}
pub fn builder() -> ServerBuilder {
ServerBuilder::new()
}
#[must_use]
pub fn identity(&self) -> &ServerIdentity {
self.core.configuration().identity()
}
#[must_use]
pub fn socket_path(&self) -> &Path {
self.identity().socket_path()
}
#[must_use]
pub fn socket_name(&self) -> Option<&OsStr> {
self.core.configuration().socket_name()
}
#[must_use]
pub fn config_file(&self) -> Option<&Path> {
self.core.configuration().config_file()
}
#[must_use]
pub fn colors(&self) -> Option<u16> {
self.core.configuration().colors()
}
#[must_use]
pub fn tmux_executable(&self) -> &OsStr {
self.core.configuration().executable()
}
#[must_use]
pub fn resolved_tmux_executable(&self) -> Option<PathBuf> {
self.core.configuration().resolved_executable()
}
#[must_use]
pub fn default_timeout(&self) -> Duration {
self.core.configuration().timeout()
}
pub async fn capabilities(&self) -> Result<&EngineCapabilities, Error> {
self.core.capabilities().await
}
pub async fn cmd(&self, command: Command) -> Result<CommandResult, Error> {
self.core.execute(command).await
}
pub async fn chain(&self, chain: CommandChain) -> Result<CommandResult, Error> {
self.core.execute_chain(chain).await
}
pub async fn shutdown(&self) -> Result<(), Error> {
self.core.shutdown().await
}
pub fn from_env() -> Result<Self, Error> {
Self::from_env_value(std::env::var_os("TMUX"))
}
pub fn from_env_value(value: Option<impl Into<OsString>>) -> Result<Self, Error> {
use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
let value: OsString = value.map(Into::into).ok_or_else(|| {
Error::invalid_server_configuration(ServerConfigurationErrorKind::NotInsideTmux)
})?;
let bytes = value.as_bytes();
let socket = bytes
.iter()
.position(|byte| *byte == b',')
.map(|index| &bytes[..index])
.filter(|socket| !socket.is_empty())
.ok_or_else(|| {
Error::invalid_server_configuration(
ServerConfigurationErrorKind::MalformedTmuxVariable,
)
})?;
Self::builder()
.socket_path(PathBuf::from(OsString::from_vec(socket.to_vec())))
.build()
}
pub async fn attached_sessions_or_empty(&self) -> Vec<Session> {
self.attached_sessions().await.unwrap_or_else(|error| {
listing::trace_discarded("list-sessions", &error);
Vec::new()
})
}
pub async fn attached_sessions(&self) -> Result<Vec<Session>, Error> {
let mut sessions = self.sessions().await?;
sessions.retain(Session::is_attached);
Ok(sessions)
}
pub async fn new_session(
&self,
options: impl Into<NewSessionOptions>,
) -> Result<Session, Error> {
let options = options.into();
let info =
listing::create_session(&self.core, |format| options.into_command(format)).await?;
Ok(Session::new(Arc::clone(&self.core), info))
}
pub async fn lock_all(&self) -> Result<(), Error> {
listing::mutate(&self.core, "lock-server", Command::new("lock-server")).await
}
pub async fn kill(&self) -> Result<(), Error> {
let result = self.cmd(Command::new("kill-server")).await?;
if result.success() {
return Ok(());
}
if self.is_alive().await {
return Err(Error::CommandFailed {
command: "kill-server",
exit_code: result.exit_code(),
stderr: result.stderr_lossy().into_owned(),
});
}
Ok(())
}
pub async fn set_buffer(
&self,
name: Option<&str>,
data: impl Into<OsString>,
) -> Result<(), Error> {
let mut command = Command::new("set-buffer");
if let Some(name) = name {
command = command.arg("-b").arg(OsString::from(name));
}
command = command.arg("--");
listing::mutate(&self.core, "set-buffer", command.sensitive_arg(data.into())).await
}
pub async fn buffer(&self, name: &str) -> Result<Option<Vec<u8>>, Error> {
let result = self
.cmd(
Command::new("show-buffer")
.arg("-b")
.arg(OsString::from(name)),
)
.await?;
if result.success() {
Ok(Some(result.stdout().to_vec()))
} else {
Ok(None)
}
}
pub async fn buffer_names(&self) -> Result<Vec<String>, Error> {
let result = self
.cmd(Command::new("list-buffers").arg("-F").arg("#{buffer_name}"))
.await?;
if !result.success() {
return Err(Error::CommandFailed {
command: "list-buffers",
exit_code: result.exit_code(),
stderr: result.stderr_lossy().into_owned(),
});
}
Ok(result
.stdout_lossy()
.lines()
.map(ToOwned::to_owned)
.collect())
}
pub async fn delete_buffer(&self, name: &str) -> Result<(), Error> {
listing::mutate(
&self.core,
"delete-buffer",
Command::new("delete-buffer")
.arg("-b")
.arg(OsString::from(name)),
)
.await
}
pub async fn bind_key(
&self,
table: &str,
key: &str,
command: impl Into<OsString>,
) -> Result<(), Error> {
listing::mutate(
&self.core,
"bind-key",
Command::new("bind-key")
.arg("-T")
.arg(OsString::from(table))
.arg(OsString::from(key))
.arg(command.into()),
)
.await
}
pub async fn unbind_key(&self, table: &str, key: &str) -> Result<(), Error> {
listing::mutate(
&self.core,
"unbind-key",
Command::new("unbind-key")
.arg("-T")
.arg(OsString::from(table))
.arg(OsString::from(key)),
)
.await
}
pub async fn key_bindings(&self, table: Option<&str>) -> Result<Vec<String>, Error> {
let mut command = Command::new("list-keys");
if let Some(table) = table {
command = command.arg("-T").arg(OsString::from(table));
}
let result = self.cmd(command).await?;
if !result.success() {
return Err(Error::CommandFailed {
command: "list-keys",
exit_code: result.exit_code(),
stderr: result.stderr_lossy().into_owned(),
});
}
Ok(result
.stdout_lossy()
.lines()
.map(ToOwned::to_owned)
.collect())
}
pub async fn format(&self, pane: Option<&Pane>, format: &str) -> Result<TmuxText, Error> {
let mut command = Command::new("display-message").arg("-p");
if let Some(pane) = pane {
self.core
.require_same_server(pane.server_identity(), "display-message")?;
command = command.arg("-t").arg(pane.id().to_string());
}
let result = self.cmd(command.arg(OsString::from(format))).await?;
if !result.success() {
return Err(Error::from_refused_result("display-message", &result, None));
}
let stdout = result.stdout();
let value = stdout.strip_suffix(b"\n").unwrap_or(stdout);
Ok(TmuxText::from(value.to_vec()))
}
pub(crate) async fn require(
&self,
capability: &'static str,
needs: ReleaseVersion,
) -> Result<(), Error> {
self.capabilities()
.await?
.tmux_version()
.require(capability, needs)
}
async fn refuse_if_defective(
&self,
capability: &'static str,
broken_in: ReleaseVersion,
fixed_in: ReleaseVersion,
) -> Result<(), Error> {
let found = self.capabilities().await?.tmux_version();
if found
.behavior_release()
.is_some_and(|release| release >= broken_in && release < fixed_in)
{
return Err(Error::CapabilityDefective {
capability,
found: found.clone(),
broken_in,
fixed_in,
});
}
Ok(())
}
pub async fn prompt_history(&self, kind: PromptKind) -> Result<Vec<TmuxText>, Error> {
self.require("prompt history", PROMPT_HISTORY_SINCE).await?;
let result = self
.cmd(
Command::new("show-prompt-history")
.arg("-T")
.arg(kind.name()),
)
.await?;
if !result.success() {
return Err(Error::from_refused_result(
"show-prompt-history",
&result,
None,
));
}
Ok(result
.stdout_lossy()
.lines()
.skip_while(|line| line.starts_with("History for "))
.filter(|line| !line.is_empty())
.map(|line| TmuxText::from(line.as_bytes().to_vec()))
.collect())
}
pub async fn clear_prompt_history(&self) -> Result<(), Error> {
self.require("prompt history", PROMPT_HISTORY_SINCE).await?;
listing::mutate(
&self.core,
"clear-prompt-history",
Command::new("clear-prompt-history"),
)
.await
}
pub async fn access_rules(&self) -> Result<Vec<AccessRule>, Error> {
self.require("the server access list", SERVER_ACCESS_SINCE)
.await?;
let result = self.cmd(Command::new("server-access").arg("-l")).await?;
if !result.success() {
return Err(Error::from_refused_result("server-access", &result, None));
}
Ok(result
.stdout_lossy()
.lines()
.filter_map(|line| {
let (user, flag) = line.rsplit_once(' ')?;
let mode = match flag {
"(R)" => AccessMode::ReadOnly,
"(W)" => AccessMode::Write,
_ => return None,
};
Some(AccessRule {
user: user.to_owned(),
mode,
})
})
.collect())
}
pub async fn grant_access(&self, user: &str, mode: AccessMode) -> Result<(), Error> {
self.require("the server access list", SERVER_ACCESS_SINCE)
.await?;
listing::mutate(
&self.core,
"server-access",
Command::new("server-access")
.arg("-a")
.arg(match mode {
AccessMode::ReadOnly => "-r",
AccessMode::Write => "-w",
})
.arg(OsString::from(user)),
)
.await
}
pub async fn revoke_access(&self, user: &str) -> Result<(), Error> {
self.require("the server access list", SERVER_ACCESS_SINCE)
.await?;
listing::mutate(
&self.core,
"server-access",
Command::new("server-access")
.arg("-d")
.arg(OsString::from(user)),
)
.await
}
pub async fn source_file(&self, path: impl Into<PathBuf>) -> Result<(), Error> {
listing::mutate(
&self.core,
"source-file",
Command::new("source-file")
.arg("--")
.arg(path.into().into_os_string()),
)
.await
}
pub async fn with_session<T, E>(
&self,
options: impl Into<NewSessionOptions>,
operation: impl AsyncFnOnce(&Session) -> Result<T, E>,
) -> Result<T, E>
where
E: From<Error>,
{
let server = self.clone();
let options = options.into();
scoped::run(
"with-session",
async move { server.new_session(options).await },
Session::kill,
operation,
)
.await
}
pub async fn run_shell(&self, command: impl Into<OsString>) -> Result<Vec<TmuxText>, Error> {
self.refuse_if_defective(
"run-shell output",
ReleaseVersion::new(3, 3, ReleaseSuffix::FINAL),
ReleaseVersion::new(3, 5, ReleaseSuffix::FINAL),
)
.await?;
let result = self
.cmd(Command::new("run-shell").sensitive_arg(command.into()))
.await?;
if !result.success() {
return Err(Error::from_refused_result("run-shell", &result, None));
}
let stdout = result.stdout();
let stdout = stdout.strip_suffix(b"\n").unwrap_or(stdout);
if stdout.is_empty() {
return Ok(Vec::new());
}
Ok(stdout
.split(|byte| *byte == b'\n')
.map(|line| TmuxText::from(line.to_vec()))
.collect())
}
pub async fn spawn_shell(&self, command: impl Into<OsString>) -> Result<(), Error> {
listing::mutate(
&self.core,
"run-shell",
Command::new("run-shell")
.arg("-b")
.sensitive_arg(command.into()),
)
.await
}
pub async fn start(&self) -> Result<(), Error> {
listing::mutate(&self.core, "start-server", Command::new("start-server")).await
}
pub async fn generation(&self) -> Result<ServerGeneration, Error> {
let answer = self.format(None, "#{pid} #{start_time}").await?;
let text = answer.to_string_lossy();
let mut parts = text.split_whitespace();
let parsed = parts
.next()
.and_then(|pid| pid.parse::<u32>().ok())
.zip(parts.next().and_then(|start| start.parse::<i64>().ok()));
let Some((pid, start_time)) = parsed else {
return Err(Error::UnreadableFormatValue {
format: "#{pid} #{start_time}",
detail: crate::IdParseError::new('#'),
});
};
Ok(ServerGeneration { pid, start_time })
}
pub async fn require_generation(&self, expected: ServerGeneration) -> Result<(), Error> {
let found = self.generation().await?;
if found == expected {
return Ok(());
}
Err(Error::ServerGenerationChanged { expected, found })
}
pub async fn is_alive(&self) -> bool {
self.check_alive().await.is_ok()
}
pub async fn check_alive(&self) -> Result<(), Error> {
let result = self.cmd(Command::new("list-sessions")).await?;
if result.success() {
return Ok(());
}
Err(Error::from_refused_result("list-sessions", &result, None))
}
#[cfg(test)]
pub(crate) fn from_executor_for_test(executor: Arc<dyn Executor>) -> Self {
Self {
core: Arc::new(Core::from_executor_for_test(executor)),
}
}
}
impl PartialEq for Server {
fn eq(&self, other: &Self) -> bool {
self.identity() == other.identity()
}
}
impl Eq for Server {}
impl Hash for Server {
fn hash<H: Hasher>(&self, state: &mut H) {
self.identity().hash(state);
}
}
impl fmt::Debug for Server {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Server")
.field("identity", self.identity())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use std::os::unix::process::ExitStatusExt as _;
use std::process::ExitStatus;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use tokio::sync::{Notify, watch};
use super::{NewSessionOptions, Server};
use crate::command::{CommandRequest, CommandResult, ProcessStatus};
use crate::formats::{DecoderKind, FormatDescriptor, FormatPlan, ListProfile};
use crate::internal::executor::{DispatchFuture, Executor, ShutdownFuture};
use crate::{Error, ErrorKind, TmuxVersion};
struct RefusingExecutor {
calls: AtomicUsize,
}
#[derive(Clone, Copy)]
enum SessionFollowup {
DispatchError,
EmptyListing,
}
struct ComposedSessionExecutor {
calls: AtomicUsize,
sessions_stdout: Vec<u8>,
followup: SessionFollowup,
}
impl Executor for ComposedSessionExecutor {
fn execute(&self, request: CommandRequest) -> DispatchFuture {
let call = self.calls.fetch_add(1, Ordering::SeqCst);
let sessions_stdout = self.sessions_stdout.clone();
let followup = self.followup;
DispatchFuture::new(async move {
if call == 3 && matches!(followup, SessionFollowup::DispatchError) {
return Err(Error::Overloaded {
request_id: request.request_id().get(),
command: request.summary().clone(),
in_flight: 1,
});
}
let stdout = match call {
0 => b"tmux 3.7b\n".to_vec(),
1 => sessions_stdout,
2 | 3 => Vec::new(),
_ => panic!("one probe, one listing, one mutation, and one refresh"),
};
Ok(CommandResult::new(
request.request_id(),
request.summary().clone(),
ProcessStatus::from_exit_status(ExitStatus::from_raw(0)),
stdout,
Vec::new(),
))
})
}
fn shutdown(&self) -> ShutdownFuture {
ShutdownFuture::new(async { Ok(()) })
}
}
fn default_format_value(descriptor: &FormatDescriptor) -> &'static [u8] {
match descriptor.name() {
"session_id" => b"$1",
"window_id" => b"@1",
"pane_id" => b"%1",
"client_name" => b"client",
_ => match descriptor.decoder() {
DecoderKind::Ascii => b"ascii",
DecoderKind::Text => b"text",
DecoderKind::Bool
| DecoderKind::U8
| DecoderKind::U32
| DecoderKind::U64
| DecoderKind::I32
| DecoderKind::Timestamp
| DecoderKind::PaneProgress => b"0",
DecoderKind::SessionId => b"$1",
DecoderKind::WindowId => b"@1",
DecoderKind::PaneId => b"%1",
DecoderKind::PaneProgressState => b"normal",
},
}
}
fn session_listing_stdout() -> Vec<u8> {
let version = TmuxVersion::parse_output(b"tmux 3.7b\n").expect("fixture version");
let plan = FormatPlan::for_profile(ListProfile::Sessions, &version);
let mut stdout = Vec::new();
for descriptor in plan.descriptors_for_test() {
for byte in default_format_value(descriptor) {
if matches!(*byte, b'\\' | b'%' | b'=') {
stdout.push(b'\\');
}
stdout.push(*byte);
}
stdout.push(b'=');
}
stdout.push(b'\n');
stdout
}
impl Executor for RefusingExecutor {
fn execute(&self, request: CommandRequest) -> DispatchFuture {
let call = self.calls.fetch_add(1, Ordering::SeqCst);
DispatchFuture::new(async move {
let (status, stdout, stderr) = if call == 0 {
(0, b"tmux 3.7b\n".to_vec(), Vec::new())
} else {
assert_eq!(call, 1, "one probe and one run-shell dispatch");
assert_eq!(request.summary().sensitive_argument_count(), 1);
(1 << 8, Vec::new(), b"sentinel-run-shell-output\n".to_vec())
};
Ok(CommandResult::new(
request.request_id(),
request.summary().clone(),
ProcessStatus::from_exit_status(ExitStatus::from_raw(status)),
stdout,
stderr,
))
})
}
fn shutdown(&self) -> ShutdownFuture {
ShutdownFuture::new(async { Ok(()) })
}
}
struct BlockingShutdownExecutor {
closed: AtomicBool,
shutdown_started: Notify,
release: watch::Receiver<bool>,
}
impl Executor for BlockingShutdownExecutor {
fn execute(&self, request: CommandRequest) -> DispatchFuture {
let closed = self.closed.load(Ordering::SeqCst);
DispatchFuture::new(async move {
assert!(
closed,
"test dispatch occurs only after shutdown closes admission"
);
Err(Error::executor_shutdown(
request.request_id().get(),
request.summary().clone(),
))
})
}
fn shutdown(&self) -> ShutdownFuture {
self.closed.store(true, Ordering::SeqCst);
self.shutdown_started.notify_one();
let mut release = self.release.clone();
ShutdownFuture::new(async move {
while !*release.borrow() {
release
.changed()
.await
.expect("test release sender remains alive");
}
Ok(())
})
}
}
#[tokio::test]
async fn aborting_one_server_shutdown_keeps_all_clones_closed_until_later_completion() {
let (release_sender, release_receiver) = watch::channel(false);
let executor = Arc::new(BlockingShutdownExecutor {
closed: AtomicBool::new(false),
shutdown_started: Notify::new(),
release: release_receiver,
});
let server = Server::from_executor_for_test(executor.clone());
let started = executor.shutdown_started.notified();
let shutdown_server = server.clone();
let shutdown = tokio::spawn(async move { shutdown_server.shutdown().await });
started.await;
shutdown.abort();
assert!(
shutdown
.await
.expect_err("shutdown task was aborted")
.is_cancelled()
);
assert!(matches!(
server.cmd(crate::Command::new("display-message")).await,
Err(Error::ExecutorShutdown { .. })
));
release_sender
.send(true)
.expect("test release receiver remains alive");
server
.clone()
.shutdown()
.await
.expect("later clone completes shutdown");
}
#[tokio::test]
async fn run_shell_failure_withholds_sensitive_output() {
let server = Server::from_executor_for_test(Arc::new(RefusingExecutor {
calls: AtomicUsize::new(0),
}));
let error = server
.run_shell("sentinel-run-shell-command")
.await
.expect_err("the command is refused");
let diagnostic = format!("{error:?} {error}");
for secret in ["sentinel-run-shell-command", "sentinel-run-shell-output"] {
assert!(!diagnostic.contains(secret), "{diagnostic}");
}
}
#[tokio::test]
async fn session_refresh_failure_after_rename_marks_the_completed_effect() {
let executor = Arc::new(ComposedSessionExecutor {
calls: AtomicUsize::new(0),
sessions_stdout: session_listing_stdout(),
followup: SessionFollowup::DispatchError,
});
let server = Server::from_executor_for_test(executor.clone());
let mut session = server
.sessions()
.await
.expect("fixture session listing")
.pop()
.expect("one fixture session");
let error = session
.rename("renamed")
.await
.expect_err("refresh dispatch fails after rename succeeds");
assert_eq!(executor.calls.load(Ordering::SeqCst), 4);
assert_eq!(error.kind(), ErrorKind::PartialEffect);
assert!(
matches!(
error,
Error::AfterEffect { operation: "rename-session", source }
if source.kind() == ErrorKind::Refused && source.is_transient()
),
"the refresh error remains available as the source",
);
}
#[tokio::test]
async fn successful_session_step_requires_an_active_window_postcondition() {
let executor = Arc::new(ComposedSessionExecutor {
calls: AtomicUsize::new(0),
sessions_stdout: session_listing_stdout(),
followup: SessionFollowup::EmptyListing,
});
let server = Server::from_executor_for_test(executor.clone());
let session = server
.sessions()
.await
.expect("fixture session listing")
.pop()
.expect("one fixture session");
let error = session
.next_window()
.await
.expect_err("an accepted step must leave an active window");
assert_eq!(executor.calls.load(Ordering::SeqCst), 4);
assert!(matches!(
error,
Error::AfterEffect { operation: "next-window", source }
if matches!(*source, Error::ObjectGone { .. })
));
}
#[test]
fn new_session_options_redact_the_shell_command() {
let secret = "sentinel-session-command";
let options = NewSessionOptions::new("work").command(secret);
assert!(!format!("{options:?}").contains(secret));
let summary = options.into_command("#{session_id}").summary();
assert_eq!(summary.sensitive_argument_count(), 1);
assert!(!summary.to_string().contains(secret));
}
}
#[must_use = "options describe a session but do not create one"]
#[derive(Clone)]
pub struct NewSessionOptions {
name: OsString,
start_directory: Option<PathBuf>,
window_name: Option<OsString>,
command: Option<OsString>,
width: Option<u32>,
height: Option<u32>,
}
impl fmt::Debug for NewSessionOptions {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("NewSessionOptions")
.field("has_start_directory", &self.start_directory.is_some())
.field("has_window_name", &self.window_name.is_some())
.field("has_command", &self.command.is_some())
.field("width", &self.width)
.field("height", &self.height)
.finish_non_exhaustive()
}
}
impl NewSessionOptions {
pub fn new(name: impl Into<OsString>) -> Self {
Self {
name: name.into(),
start_directory: None,
window_name: None,
command: None,
width: None,
height: None,
}
}
pub fn start_directory(mut self, directory: impl Into<PathBuf>) -> Self {
self.start_directory = Some(directory.into());
self
}
pub fn window_name(mut self, name: impl Into<OsString>) -> Self {
self.window_name = Some(name.into());
self
}
pub fn command(mut self, command: impl Into<OsString>) -> Self {
self.command = Some(command.into());
self
}
pub fn size(mut self, width: u32, height: u32) -> Self {
self.width = Some(width);
self.height = Some(height);
self
}
fn into_command(self, print_format: &str) -> Command {
let mut command = Command::new("new-session")
.arg("-d")
.arg("-P")
.arg("-F")
.arg(print_format)
.arg("-s")
.arg(self.name);
if let Some(directory) = self.start_directory {
command = command.arg("-c").arg(directory.into_os_string());
}
if let Some(name) = self.window_name {
command = command.arg("-n").arg(name);
}
if let (Some(width), Some(height)) = (self.width, self.height) {
command = command
.arg("-x")
.arg(width.to_string())
.arg("-y")
.arg(height.to_string());
}
if let Some(shell_command) = self.command {
command = command.sensitive_arg(shell_command);
}
command
}
}
impl<T: Into<OsString>> From<T> for NewSessionOptions {
fn from(name: T) -> Self {
Self::new(name)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Chooser {
Tree,
Client,
Buffer,
Customize,
}
impl Chooser {
const fn command(self) -> &'static str {
match self {
Self::Tree => "choose-tree",
Self::Client => "choose-client",
Self::Buffer => "choose-buffer",
Self::Customize => "customize-mode",
}
}
}