I'm new to Rust, and this is my first successful significant Rust code. It prints a prompt(which is technically a string) to the console.
I'm struggling with concepts like ownership in Rust, so, some code review and advice might be of great help.
Current features:
- Symbol indicating the current identified shell
- Root user indicator(color)
- Error status(color)
- SSH session indicator
- Git repo indicator, branch indicator(color), repo name
pwd(abbreviated when needed)
Known Issues:
- I could'nt get the Error Status indicator to work as expected(red on err, green on success)
Here's the code:
use std::env::{current_dir,var_os,args};
use colored::{Colorize,ColoredString};
use std::process::Command;
fn get_shell_char (shell: String) -> String {
let shell_char = match shell.as_str() {
"bash"|"/bin/bash"
=> " ",
"zsh"|"/bin/zsh"|"/usr/bin/zsh"|"-zsh"
=> " ",
"fish"
=> " ",
"nushell"
=> " ",
"ion"
=> " ",
"oursh"
=> " ",
_
=> " ",
};
shell_char.to_string()
}
fn get_git_branch () -> String {
let git_status_cmd = Command::new("git")
.arg("status")
.output()
.expect("git_status_cmd_fail");
let git_status_output = String::from_utf8_lossy(&git_status_cmd.stdout);
let git_err = String::from_utf8_lossy(&git_status_cmd.stderr);
if git_err == "" {
git_status_output.split("\n").collect::<Vec<&str>>()[0]
.split(" ").collect::<Vec<&str>>()[2].to_string()
}
else {
"".to_string()
}
}
fn get_git_root () -> String {
let git_repo_root_cmd = Command::new("git")
.arg("rev-parse")
.arg("--show-toplevel")
.output()
.expect("git_repo_root_cmd_fail");
let mut git_repo_path = String::from_utf8_lossy(&git_repo_root_cmd.stdout).to_string();
let git_repo_err = String::from_utf8_lossy(&git_repo_root_cmd.stderr);
if git_repo_err == "" {
let len = git_repo_path.trim_end_matches(&['\r', '\n'][..]).len();
git_repo_path.truncate(len);
}
else {
git_repo_path = "".to_string();
}
git_repo_path
}
fn get_git_repo_name (git_repo_root: String) -> String {
let repo_path_split: Vec<&str> = git_repo_root.split("/").collect();
let last_index = repo_path_split.len() - 1;
let git_repo_name = repo_path_split[last_index];
git_repo_name.to_string()
}
fn get_git_char (git_branch: String) -> ColoredString {
match git_branch.as_str() {
"" => "".clear(),
"main" => " ".truecolor(178,98,44),
"master" => " ".truecolor(196,132,29),
_ => " ".truecolor(82,82,82),
}
}
fn abrev_path (path: String) -> String {
let mut short_dir = path.clone();
let slashes = path.matches("/").count();
if slashes > 3 {
let parts: Vec<&str> = path.split("/").collect();
let len = parts.len() - 1;
let mut ch1: String;
for part in &parts[0..len] {
if part.to_string() != "" { // to avoid the 1st "/"
ch1 = part.chars().next().expect(part).to_string(); // 1st char of each part
short_dir = short_dir.replace(part, &ch1);
}
}
}
short_dir
}
fn main() -> std::io::Result<()> {
let angle = "❯";
//Root user indicator
let user = var_os("USER").expect("UnknownUser").to_str().expect("UnknownUser").to_string();
let mut err: String = "".to_string();
let args: Vec<String> = args().collect();
let shell: String;
if args.len() > 1 {
shell = args[1].clone(); // Shell symbol
if args.len() > 2 {
err = args[2].clone(); // Error status
}
}
else {
shell = "none".to_string();
}
let root_indicator = match user.as_str() {
"root" => angle.truecolor(255, 53, 94),
_ => angle.truecolor(0, 255, 180),
};
let err_indicator = match err.as_str() {
"0" => angle.truecolor(0, 255, 180),
_ => angle.truecolor(255, 53, 94),
};
//SSH shell indicator
let ssh_char:ColoredString;
match var_os("SSH_TTY") {
Some(_val) => {
ssh_char = " ".truecolor(0,150,180);
},
None => {
ssh_char = "".clear();
}
}
//Git status
let git_branch = get_git_branch();
let git_repo_root = get_git_root();
let git_repo_name = get_git_repo_name(git_repo_root.clone()).truecolor(122, 68, 24);
let git_char = get_git_char(git_branch);
//pwd
let homedir = var_os("HOME").expect("UnknownDir").to_str().expect("UnknownDir").to_string();
let pwd = current_dir()?;
let mut cur_dir = pwd.display().to_string();
cur_dir = cur_dir.replace(&git_repo_root, ""); // Remove git repo root
cur_dir = cur_dir.replace(&homedir, "~"); // Abreviate homedir with "~"
cur_dir = abrev_path(cur_dir);
print!("{}{}{}{}{}{}{} ",
ssh_char,
get_shell_char(shell).truecolor(75,75,75),
git_repo_name,
git_char,
cur_dir.italic().truecolor(82,82,82),
root_indicator,
err_indicator,
);
Ok(())
}
It was successfully tested in bash by adding this to the bashrc:
export PS1=""
PROMPT_COMMAND="/path/to/my/binary 0ドル $?"
It can work in zsh, too, but I couldn't figure out the zsh equivalent of bash's $PROMPT_COMMAND to add to the zsh config file(precmd() didn't work).
I haven't tested in other shells, but I guess it works.
Here's as screenshot (ignore the zsh prompt):
Here's my repo
1 Answer 1
Good practices
You use Result::expect() instead of Result::unwrap() with appropriate messages.
Be specific about your types
The function get_shell_char() currently takes an owned String even though it is directly referenced to a &str. Additionally, it returns a String despite all returned values are being created from &'static str literals.
Also its name is misleading, since the function does not return a char, but a String.
Consider refactoring this function to:
fn get_shell_symbol(shell: &str) -> &'static str {
match shell {
"bash" | "/bin/bash" => " ",
"zsh" | "/bin/zsh" | "/usr/bin/zsh" | "-zsh" => " ",
"fish" => " ",
"nushell" => " ",
"ion" => " ",
"oursh" => " ",
_ => " ",
}
}
You also do some manual - and possibly unstable - path manipulation of Strings in abrev_path(). Instead of representing file system paths as Strings, consider using std::path::Path and its appropriate methods instead.
Fragile shell detection
Your shell detection in get_shell_char() is quite fragile, since it attempts to match against the shell (executable) names and possible absolute file paths of those, but in the latter case, only for a few select shells. Consider stabilizing this by passing the full executable Path (see above) and checking its file_name() against the possible binary names.
Know your libraries
You currently use std::env::var_os() but expect() it to be valid unicode.
Why not use std::env::var() directly?
Use standard formatting
Consider using rustfmt or cargo fmt to format your code according to community recommendations.
Code organization
Consider splitting up your code into multiple submodules, each dealing with a separate feature of your program (e.g. git, shell_detection, etc.).
-
\$\begingroup\$ Thanks for the suggestions. I'm still trying to figure out the difference bw
Stringandstr. I have made some changes(in the code in my repo, not here) as per your suggestions and other comments. I intend to work on it soon to improve it and add more features and customizability, when I'm done with the other stuff I'm working on \$\endgroup\$candifloss__– candifloss__2024年10月06日 13:56:01 +00:00Commented Oct 6, 2024 at 13:56
\u{1234}) - your editor may show these characters directly when pasted into code, but other don't. Now you have a CodeReview post with all characters rendered as "unknown thingy" tiny square. And please make yourself a favour of at leastcargo fmt && cargo clippy -- -W clippy::pedantic- the former will fix the indentation, the latter may point out some problems with code. Finally, since you have an external dependencycolored, please post your config file too to make sure we're on the same page. \$\endgroup\$cargo fmtandcargo clippyare really helpful! Thanks for this suggestion. I should have read the docs more. \$\endgroup\$