use std::path::{Path, PathBuf};
use anyhow::{Context, Result, anyhow};
const ZSH_SNIPPET: &str = include_str!("../shell/integration.zsh");
const BASH_SNIPPET: &str = include_str!("../shell/integration.bash");
const FISH_SNIPPET: &str = include_str!("../shell/integration.fish");
const RC_BLOCK_BEGIN: &str = "# >>> cmdq shell integration >>>";
const RC_BLOCK_END: &str = "# <<< cmdq shell integration <<<";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShellKind {
Zsh,
Bash,
Fish,
Sh,
}
impl ShellKind {
pub fn detect_from_path(shell: &str) -> Self {
let name = Path::new(shell)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(shell);
match name {
"zsh" => Self::Zsh,
"bash" => Self::Bash,
"fish" => Self::Fish,
_ => Self::Sh,
}
}
pub fn snippet(self) -> &'static str {
match self {
Self::Zsh => ZSH_SNIPPET,
Self::Bash | Self::Sh => BASH_SNIPPET,
Self::Fish => FISH_SNIPPET,
}
}
}
pub fn snippet_for(shell: &str) -> Result<&'static str> {
Ok(ShellKind::detect_from_path(shell).snippet())
}
pub fn current_shell() -> Option<String> {
std::env::var("SHELL").ok()
}
pub fn rc_file_for(shell: ShellKind) -> Option<PathBuf> {
let home = dirs::home_dir()?;
Some(match shell {
ShellKind::Zsh => home.join(".zshrc"),
ShellKind::Bash | ShellKind::Sh => home.join(".bashrc"),
ShellKind::Fish => home.join(".config").join("fish").join("config.fish"),
})
}
pub fn integration_script_path(shell: ShellKind) -> Result<PathBuf> {
let dir = dirs::data_dir()
.or_else(dirs::home_dir)
.ok_or_else(|| anyhow!("could not locate home/data dir"))?;
let dir = dir.join("cmdq");
std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
Ok(dir.join(match shell {
ShellKind::Zsh => "integration.zsh",
ShellKind::Bash | ShellKind::Sh => "integration.bash",
ShellKind::Fish => "integration.fish",
}))
}
pub fn write_integration_script(shell: ShellKind) -> Result<PathBuf> {
let path = integration_script_path(shell)?;
std::fs::write(&path, shell.snippet())
.with_context(|| format!("writing {}", path.display()))?;
Ok(path)
}
pub fn install_for_current_shell() -> Result<String> {
let shell_path =
current_shell().ok_or_else(|| anyhow!("$SHELL is not set; pass --shell explicitly"))?;
let kind = ShellKind::detect_from_path(&shell_path);
let script = write_integration_script(kind)?;
let rc = rc_file_for(kind).ok_or_else(|| anyhow!("could not determine rc file location"))?;
let source_line = match kind {
ShellKind::Fish => format!("source \"{}\"", script.display()),
_ => format!(
"[ -f \"{}\" ] && . \"{}\"",
script.display(),
script.display()
),
};
let block = format!(
"{RC_BLOCK_BEGIN}\n# Activated only when running under cmdq.\n{source_line}\n{RC_BLOCK_END}\n"
);
let existing = std::fs::read_to_string(&rc).unwrap_or_default();
if existing.contains(RC_BLOCK_BEGIN) {
return Ok(format!(
"cmdq integration already present in {}.\nIntegration script: {}",
rc.display(),
script.display()
));
}
let mut new = existing;
if !new.is_empty() && !new.ends_with('\n') {
new.push('\n');
}
new.push('\n');
new.push_str(&block);
if let Some(parent) = rc.parent() {
std::fs::create_dir_all(parent).ok();
}
std::fs::write(&rc, new).with_context(|| format!("writing {}", rc.display()))?;
Ok(format!(
"Installed cmdq integration into {}.\n\
Integration script: {}\n\
Restart your shell (or open a new cmdq session) to activate.",
rc.display(),
script.display()
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_shell_kind() {
assert_eq!(ShellKind::detect_from_path("/bin/zsh"), ShellKind::Zsh);
assert_eq!(
ShellKind::detect_from_path("/usr/local/bin/bash"),
ShellKind::Bash
);
assert_eq!(
ShellKind::detect_from_path("/opt/homebrew/bin/fish"),
ShellKind::Fish
);
assert_eq!(ShellKind::detect_from_path("/bin/sh"), ShellKind::Sh);
}
#[test]
fn snippets_are_non_empty_and_have_osc_133() {
for s in [ShellKind::Zsh, ShellKind::Bash, ShellKind::Fish] {
let snip = s.snippet();
assert!(!snip.is_empty(), "{:?}", s);
assert!(snip.contains("133;C"), "{:?} missing C marker", s);
assert!(snip.contains("133;D"), "{:?} missing D marker", s);
assert!(snip.contains("133;A"), "{:?} missing A marker", s);
}
}
}