Skip to content
Open
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ By default, `folderify` uses your system's current light/dark mode. Use `--color
folderify --color-scheme dark mask.png
```

On macOS Tahoe, `folderify` uses your current system icon theme color by default. Use `--folder-color` to override it:

```shell
folderify --macOS 26 --folder-color auto mask.png
```

Note:

- There is currently no simple way to set an icon that will automatically switch between light and dark when you switch the entire OS. You can only assign one version of an icon to a folder.
Expand Down Expand Up @@ -168,6 +174,14 @@ Options:
[default: auto]
[possible values: auto, light, dark]

--folder-color <FOLDER_COLOR>
Tahoe folder color. `auto` matches the current Tahoe icon theme color,
`multicolor` keeps the default macOS folder look, and the tinted
variants use Tahoe's tinted folder rendering

[default: auto]
[possible values: auto, multicolor, blue, graphite, green, orange, pink, purple, red, yellow]

--no-trim
Don't trim margins from the mask.
By default (i.e. without this flag), transparent margins are trimmed from all 4 sides.
Expand Down
191 changes: 190 additions & 1 deletion src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ struct FolderifyArgs {
#[clap(long, value_enum, default_value_t = ColorSchemeOrAuto::Auto)]
color_scheme: ColorSchemeOrAuto,

/// Tahoe folder color. `auto` matches the current Tahoe icon theme color,
/// `multicolor` keeps the default macOS folder look, and the tinted
/// variants use Tahoe's tinted folder rendering.
#[clap(long, value_enum, default_value_t = FolderColorOrAuto::Auto)]
folder_color: FolderColorOrAuto,

/// Don't trim margins from the mask.
/// By default (i.e. without this flag), transparent margins are trimmed from all 4 sides.
#[clap(long, verbatim_doc_comment)]
Expand Down Expand Up @@ -94,6 +100,19 @@ pub enum ColorScheme {
Dark,
}

#[derive(ValueEnum, Clone, Debug, PartialEq, Copy)]
pub enum FolderColor {
Multicolor,
Blue,
Graphite,
Green,
Orange,
Pink,
Purple,
Red,
Yellow,
}

#[derive(ValueEnum, Clone, Debug, PartialEq, Copy)]
pub enum Badge {
Alias,
Expand All @@ -113,6 +132,42 @@ impl Display for ColorScheme {
}
}

impl FolderColor {
pub fn as_str(&self) -> &'static str {
match self {
Self::Multicolor => "multicolor",
Self::Blue => "blue",
Self::Graphite => "graphite",
Self::Green => "green",
Self::Orange => "orange",
Self::Pink => "pink",
Self::Purple => "purple",
Self::Red => "red",
Self::Yellow => "yellow",
}
}
}

impl Display for FolderColor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}

#[derive(ValueEnum, Clone, Debug, PartialEq, Copy)]
enum FolderColorOrAuto {
Auto,
Multicolor,
Blue,
Graphite,
Green,
Orange,
Pink,
Purple,
Red,
Yellow,
}

#[derive(ValueEnum, Clone, Debug, PartialEq)]
enum ColorSchemeOrAuto {
Auto,
Expand Down Expand Up @@ -141,6 +196,7 @@ enum SetIconUsingOrAuto {
pub struct Options {
pub mask_path: PathBuf,
pub color_scheme: ColorScheme,
pub folder_color: FolderColor,
pub no_trim: bool,
pub target: Option<PathBuf>,
pub folder_style: FolderStyle,
Expand Down Expand Up @@ -259,9 +315,21 @@ pub fn get_options() -> Options {
Some(SetIconUsingOrAuto::Fileicon) => SetIconUsing::Fileicon,
_ => SetIconUsing::Osascript,
};
if folder_style != FolderStyle::Tahoe
&& !matches!(
args.folder_color,
FolderColorOrAuto::Auto | FolderColorOrAuto::Multicolor
)
{
eprintln!(
"Folder tint variants are only available for Tahoe. \
Ignoring `--folder-color`."
);
}
Options {
mask_path: mask,
color_scheme: map_color_scheme_auto(args.color_scheme, folder_style),
folder_color: normalized_folder_color(folder_style, args.folder_color),
no_trim: args.no_trim,
target: args.target,
folder_style,
Expand All @@ -277,6 +345,83 @@ pub fn get_options() -> Options {
}
}

fn normalized_folder_color(
folder_style: FolderStyle,
folder_color: FolderColorOrAuto,
) -> FolderColor {
if folder_style != FolderStyle::Tahoe {
return FolderColor::Multicolor;
}

match folder_color {
FolderColorOrAuto::Auto => current_system_folder_color(),
FolderColorOrAuto::Multicolor => FolderColor::Multicolor,
FolderColorOrAuto::Blue => FolderColor::Blue,
FolderColorOrAuto::Graphite => FolderColor::Graphite,
FolderColorOrAuto::Green => FolderColor::Green,
FolderColorOrAuto::Orange => FolderColor::Orange,
FolderColorOrAuto::Pink => FolderColor::Pink,
FolderColorOrAuto::Purple => FolderColor::Purple,
FolderColorOrAuto::Red => FolderColor::Red,
FolderColorOrAuto::Yellow => FolderColor::Yellow,
}
}

fn current_system_folder_color() -> FolderColor {
let icon_appearance_theme = read_global_default("AppleIconAppearanceTheme");
if !icon_appearance_theme
.as_deref()
.is_some_and(|theme| theme.starts_with("Tinted"))
{
return FolderColor::Multicolor;
}

let Some(accent_color_string) = read_global_default("AppleAccentColor") else {
eprintln!("Could not compute auto folder color. Assuming multicolor.");
return FolderColor::Multicolor;
};

let Some(folder_color) = parse_system_accent_color(&accent_color_string) else {
eprintln!(
"Could not map the system accent color to a Tahoe folder tint. \
Assuming multicolor."
);
return FolderColor::Multicolor;
};

folder_color
}

fn read_global_default(key: &str) -> Option<String> {
let output = Command::new("/usr/bin/env")
.args(["defaults", "read", "-g", key])
.output()
.ok()?;
if !output.status.success() {
return None;
}

let stdout = from_utf8(&output.stdout).ok()?.trim().to_owned();
if stdout.is_empty() {
return None;
}
Some(stdout)
}

fn parse_system_accent_color(accent_color: &str) -> Option<FolderColor> {
match accent_color.trim().parse::<i32>().ok()? {
-1 => Some(FolderColor::Graphite),
0 => Some(FolderColor::Red),
1 => Some(FolderColor::Orange),
2 => Some(FolderColor::Yellow),
3 => Some(FolderColor::Green),
4 => Some(FolderColor::Blue),
5 => Some(FolderColor::Purple),
6 => Some(FolderColor::Pink),
_ => None,
}
}

fn map_color_scheme_auto(
color_scheme: ColorSchemeOrAuto,
folder_style: FolderStyle,
Expand Down Expand Up @@ -332,7 +477,14 @@ fn current_macOS_version() -> String {

#[cfg(test)]
mod tests {
use crate::args::FolderifyArgs;
use crate::args::{
normalized_folder_color,
parse_system_accent_color,
FolderColor,
FolderColorOrAuto,
FolderStyle,
FolderifyArgs,
};

// https://docs.rs/clap/latest/clap/_derive/_tutorial/index.html#testing
#[test]
Expand All @@ -341,4 +493,41 @@ mod tests {

FolderifyArgs::command().debug_assert();
}

#[test]
fn test_system_accent_color_mapping() {
assert_eq!(parse_system_accent_color("-1"), Some(FolderColor::Graphite));
assert_eq!(parse_system_accent_color("0"), Some(FolderColor::Red));
assert_eq!(parse_system_accent_color("1"), Some(FolderColor::Orange));
assert_eq!(parse_system_accent_color("2"), Some(FolderColor::Yellow));
assert_eq!(parse_system_accent_color("3"), Some(FolderColor::Green));
assert_eq!(parse_system_accent_color("4"), Some(FolderColor::Blue));
assert_eq!(parse_system_accent_color("5"), Some(FolderColor::Purple));
assert_eq!(parse_system_accent_color("6"), Some(FolderColor::Pink));
assert_eq!(parse_system_accent_color("99"), None);
}

#[test]
fn test_non_tahoe_folder_color_is_always_multicolor() {
assert_eq!(
normalized_folder_color(FolderStyle::BigSur, FolderColorOrAuto::Auto),
FolderColor::Multicolor
);
assert_eq!(
normalized_folder_color(FolderStyle::BigSur, FolderColorOrAuto::Purple),
FolderColor::Multicolor
);
}

#[test]
fn test_tahoe_explicit_folder_color_is_preserved() {
assert_eq!(
normalized_folder_color(FolderStyle::Tahoe, FolderColorOrAuto::Blue),
FolderColor::Blue
);
assert_eq!(
normalized_folder_color(FolderStyle::Tahoe, FolderColorOrAuto::Purple),
FolderColor::Purple
);
}
}
Loading