Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 10 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,21 @@ test = false

[workspace]
members = [
"autocomplete",
"common",
"edit",
"load",
"new",
"retry_test",
"snapshot"
"snapshot",
]

[dependencies]
common = { path = "./common" }
docopt = "1.1.0"
edit = { path = "./edit" }
load = { path = "./load" }
new = { path = "./new" }
snapshot = { path = "./snapshot" }
list = { path = "./list" }
autocomplete = { path = "./autocomplete" }
common = { path = "./common" }
docopt = "1.1.0"
edit = { path = "./edit" }
load = { path = "./load" }
new = { path = "./new" }
snapshot = { path = "./snapshot" }
list = { path = "./list" }
13 changes: 13 additions & 0 deletions autocomplete/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "autocomplete"
version = "0.8.2"
authors = ["Brian Pearce"]
publish = false
edition = "2024"

[lib]
doctest = false
test = false

[dependencies]
common = { path = "../common" }
84 changes: 84 additions & 0 deletions autocomplete/src/bash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use std::env::VarError;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::{env, fmt, fs, io};

const BASH_COMPLETION: &str = r#"_muxed() {
local cur prev opts projects
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"

opts="list ls edit load new snapshot autocomplete"
projects="$(muxed list -1 2>/dev/null)"

# First argument after 'muxed' — offer commands + project names
if [[ ${COMP_CWORD} -eq 1 ]]; then
COMPREPLY=( $(compgen -W "${opts} ${projects}" -- "${cur}") )
return 0
fi

# If the previous word is a command that expects a project name
case "${prev}" in
edit|load|snapshot)
COMPREPLY=( $(compgen -W "${projects}" -- "${cur}") )
return 0
;;
esac
}
complete -F _muxed muxed
"#;

#[derive(Debug)]
pub enum BashError {
Var(VarError),
Io(io::Error),
}

impl fmt::Display for BashError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
BashError::Var(err) => write!(f, "{}", err),
BashError::Io(err) => write!(f, "{}", err),
}
}
}

impl std::error::Error for BashError {}

impl From<io::Error> for BashError {
fn from(err: io::Error) -> Self {
BashError::Io(err)
}
}

impl From<VarError> for BashError {
fn from(err: VarError) -> Self {
BashError::Var(err)
}
}

pub fn install() -> Result<(), BashError> {
let home = env::var("HOME")?;
let completion_dir = PathBuf::from(&home).join(".bash_completion.d");
fs::create_dir_all(&completion_dir)?;

let completion_file = completion_dir.join("muxed");
fs::write(&completion_file, BASH_COMPLETION)?;

let bashrc = PathBuf::from(&home).join(".bashrc");
let source_line = format!("source {}\n", completion_file.display());

if bashrc.exists() {
let content = fs::read_to_string(&bashrc)?;
if !content.contains(source_line.trim()) {
let mut file = OpenOptions::new().append(true).open(&bashrc)?;
writeln!(file, "\n# muxed completion")?;
write!(file, "{}", source_line)?;
}
}

println!("✓ Bash completion installed. Run: source ~/.bashrc");
Ok(())
}
54 changes: 54 additions & 0 deletions autocomplete/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use crate::bash::BashError;
use crate::fish::FishError;
use crate::zsh::ZshError;
use std::env::VarError;
use std::fmt;

#[derive(Debug)]
pub enum AutocompleteError {
ShellNotSupported,
Var(VarError),
Bash(BashError),
Zsh(ZshError),
Fish(FishError),
}

impl fmt::Display for AutocompleteError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AutocompleteError::ShellNotSupported => {
write!(f, "Sorry, that shell is not supported at this time")
}
AutocompleteError::Var(err) => write!(f, "{}", err),
AutocompleteError::Bash(err) => write!(f, "Bash error: {}", err),
AutocompleteError::Zsh(err) => write!(f, "Zsh error: {}", err),
AutocompleteError::Fish(err) => write!(f, "Fish error: {}", err),
}
}
}

impl std::error::Error for AutocompleteError {}

impl From<VarError> for AutocompleteError {
fn from(err: VarError) -> Self {
AutocompleteError::Var(err)
}
}

impl From<BashError> for AutocompleteError {
fn from(err: BashError) -> Self {
AutocompleteError::Bash(err)
}
}

impl From<ZshError> for AutocompleteError {
fn from(err: ZshError) -> Self {
AutocompleteError::Zsh(err)
}
}

impl From<FishError> for AutocompleteError {
fn from(err: FishError) -> Self {
AutocompleteError::Fish(err)
}
}
68 changes: 68 additions & 0 deletions autocomplete/src/fish.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::env::VarError;
use std::path::PathBuf;
use std::{env, fmt, fs, io};

const FISH_COMPLETION: &str = r#"function __fish_muxed_projects
muxed list -1 2>/dev/null
end

function __fish_muxed_needs_command
set cmd (commandline -opc)
test (count $cmd) -eq 1
end

function __fish_muxed_needs_project
set cmd (commandline -opc)
set sub (string split ' ' -- $cmd)[2]
contains -- $sub edit load snapshot
end

# Subcommands
complete -c muxed -n '__fish_muxed_needs_command' -a "list ls edit load new snapshot autocomplete"

# Project completions for commands that expect a project
complete -c muxed -n '__fish_muxed_needs_project' -a '(__fish_muxed_projects)'
"#;

#[derive(Debug)]
pub enum FishError {
Var(VarError),
Io(io::Error),
}

impl fmt::Display for FishError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
FishError::Var(err) => write!(f, "{}", err),
FishError::Io(err) => write!(f, "{}", err),
}
}
}

impl std::error::Error for FishError {}

impl From<io::Error> for FishError {
fn from(err: io::Error) -> Self {
FishError::Io(err)
}
}

impl From<VarError> for FishError {
fn from(err: VarError) -> Self {
FishError::Var(err)
}
}

pub fn install() -> Result<(), FishError> {
let home = env::var("HOME")?;
let completion_dir = PathBuf::from(&home)
.join(".config")
.join("fish")
.join("completions");
fs::create_dir_all(&completion_dir)?;

fs::write(completion_dir.join("muxed.fish"), FISH_COMPLETION)?;

println!("✓ Fish completion installed. Start new session to use.");
Ok(())
}
56 changes: 56 additions & 0 deletions autocomplete/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use common::args::Args;
use std::env;

mod bash;
mod error;
mod fish;
mod zsh;

use crate::error::AutocompleteError;

type Result<T> = std::result::Result<T, AutocompleteError>;

/// Detects the user's current shell and installs shell autocompletion support.
///
/// Supports installing autocomplete for "bash", "zsh", and "fish" shells.
/// Returns an error if the shell is not supported, or if installation fails for any reason.
///
/// Caveat: It doesn't detect the running shell right now. It detects the
/// configured user login shell.
///
/// # Arguments
/// * `_args` - Command-line arguments relevant to the autocomplete process.
///
/// # Returns
/// * `Result<()>` - Ok on successful installation, or `AutocompleteError` if an error occurs.
pub fn autocomplete(_args: Args) -> Result<()> {
let shell = detect_shell()?;
println!("Detected shell: {}", shell);

match shell.as_str() {
"bash" => bash::install()?,
"zsh" => zsh::install()?,
"fish" => fish::install()?,
_ => return Err(AutocompleteError::ShellNotSupported),
}

Ok(())
}

/// Detects the user's configured login shell by inspecting the SHELL environment variable.
///
/// This function does NOT detect the currently running shell process. Instead, it retrieves the shell path
/// specified by the SHELL environment variable, which typically points to the user's default login shell as set
/// in their system configuration (e.g., `/bin/zsh`, `/bin/bash`). The shell name is extracted from the end of this path.
///
/// # Returns
/// * `Result<String>` - The name of the configured login shell on success, or an `AutocompleteError` if the SHELL variable
/// is missing or invalid.
///
/// # Example
/// If SHELL is `/bin/zsh`, this will return `"zsh"`.fn detect_shell() -> Result<String> {
fn detect_shell() -> Result<String> {
let shell_path = env::var("SHELL")?;
let shell_name = shell_path.split('/').next_back().unwrap_or("unknown");
Ok(shell_name.to_string())
}
Loading
Loading