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 String
s in abrev_path()
. Instead of representing file system paths as String
s, 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
String
andstr
. 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 fmt
andcargo clippy
are really helpful! Thanks for this suggestion. I should have read the docs more. \$\endgroup\$