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
27 changes: 26 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,35 @@ jobs:
run: cargo xtask test

# The app bundle, signed ad hoc: catches a broken Info.plist, icon or
# signature before a release does.
# signature before a release does. This runner's Xcode has no actool
# 26, so this is the bundle without the Icon Composer icon.
- name: Bundle
run: cargo xtask bundle

bundle-macos-26:
name: Bundle (macOS 26)
# The bundle with the Icon Composer icon: Xcode 26's actool compiles it
# into Assets.car, and the plist names it, which the job checks.
runs-on: macos-26
steps:
- uses: actions/checkout@v5

- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.97"

- uses: Swatinem/rust-cache@v2

- name: Bundle
run: cargo xtask bundle

- name: Icon catalog
run: |
app=target/bundle/disktree.app
test -f "$app/Contents/Resources/Assets.car"
test -f "$app/Contents/Resources/AppIcon.icns"
test "$(plutil -extract CFBundleIconName raw "$app/Contents/Info.plist")" = Icon

windows:
name: Lint and test (Windows)
runs-on: windows-2025
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,10 @@ jobs:
matrix:
include:
# `xtask bundle` names the zip after the machine it runs on, so
# each architecture builds on its own.
- { runner: macos-15, arch: aarch64 }
# each architecture builds on its own. macOS 26's Xcode compiles
# the Icon Composer icon into the bundle; there is no macOS 26
# Intel runner, so that build ships the legacy icon alone.
- { runner: macos-26, arch: aarch64 }
- { runner: macos-15-intel, arch: x86_64 }
runs-on: ${{ matrix.runner }}
env:
Expand Down
17 changes: 17 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ make install # ~/.local: binary, desktop entry, icon
make install PREFIX=/usr/local # system-wide (needs root)
make uninstall
make bundle # macOS: target/bundle/disktree.app and its zip
make dock-icons # macOS 26: redraw the Dock icon of an unbundled run
make lint # rustfmt --check, then clippy --all-targets -D warnings
make test # core and window-harness tests
make ci # lint, then test
Expand All @@ -37,6 +38,21 @@ On macOS `make install` installs `target/bundle/disktree.app` into
`packaging/macos/` (the `Info.plist` template and the icon's SVG). The bundle
identifier there keys every macOS permission grant; do not change it lightly.

The macOS icon has two sources in `packaging/macos/`, and each macOS reads
the one made for it. `AppIcon.svg` is the mark on Apple's legacy 824-in-1024
tile; the bundle draws `AppIcon.icns` from it, which macOS 15 and earlier
draw literally. `AppIcon.icon` with `AppGlyph.svg` is the Icon Composer
package: when Xcode 26's actool is at hand `cargo xtask bundle` compiles it
into `Assets.car` and names it with `CFBundleIconName`, and macOS 26 draws
that with its own shape, glass edge and Dark, Clear and Tinted looks. Handed
only the `.icns`, macOS 26 normalizes it instead, and 26.6.2 has drawn such a
padded icns on a light plate, so a bundle without the catalog says so. A run
outside the bundle (`make run`, `cargo run`) has no icon for macOS to draw,
so `dock_icon.rs` paints one: `DockIcon.png`, or on macOS 26
`DockIcon-glass.png`, the shipped icon as that macOS itself draws it, since
Liquid Glass is never applied to an image set at run time. `make dock-icons`
redraws both on a macOS 26 with Xcode 26; run it after either SVG changes.

`cargo xtask lint` is the gate. It must be green before anything is called
done, and it must not fix anything: a red local run is the same signal CI gives.
On Windows, where there is no make, run `cargo xtask lint`, `cargo xtask test`
Expand Down Expand Up @@ -138,6 +154,7 @@ and `cargo build --release` directly; CI runs the gate on both systems.
| the mosaic's painting or labels | `crates/disktree-app/src/treemap_view.rs` |
| layout of a screen | `crates/disktree-app/src/views.rs` |
| colours derived from the theme | `crates/disktree-app/src/palette.rs` |
| the macOS icon, in any of its looks | `packaging/macos/` — the SVGs and `AppIcon.icon`, then `make dock-icons` |

## Verification expectations

Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

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

9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ define check_install_path
fi
endef

.PHONY: help build run install install-scan uninstall bundle lint test test-ssh ci fmt clean
.PHONY: help build run install install-scan uninstall bundle dock-icons lint test test-ssh ci fmt clean

help:
@echo "disktree"
Expand All @@ -58,6 +58,7 @@ help:
@echo " make install install to $(PREFIX): binary, desktop entry, icon"
@echo " (macOS: disktree.app into $(APPS))"
@echo " make bundle macOS: build target/bundle/disktree.app and its zip"
@echo " make dock-icons macOS 26: redraw the Dock icon of an unbundled run"
@echo " make uninstall remove what install put there"
@echo " make lint rustfmt --check and clippy -D warnings"
@echo " make test core and window-harness tests"
Expand Down Expand Up @@ -153,5 +154,11 @@ endif
bundle:
$(CARGO) xtask bundle

# The Dock icon a run outside the bundle shows, in the flat look of macOS 15
# and the Liquid Glass of macOS 26, which only that macOS can draw: needs
# macOS 26 and Xcode 26. Run after either SVG in packaging/macos changes.
dock-icons:
swift packaging/macos/render-dock-icons.swift

clean:
$(CARGO) clean
21 changes: 21 additions & 0 deletions crates/disktree-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,27 @@ dunce = "1.0.5"
rustc-hash = { workspace = true }
tempfile = "3"

[target.'cfg(target_os = "macos")'.dependencies]
# AppKit for the Dock icon of a run outside the app bundle (dock_icon.rs):
# the crates and versions GPUI already builds, with only the classes used.
objc2 = "0.6"
# The kernel release, which says which macOS this is whatever SDK linked it.
rustix = { version = "1.1", features = ["system"] }
objc2-app-kit = { version = "0.3", default-features = false, features = [
"std",
"NSApplication",
"NSImage",
"NSResponder",
] }
objc2-foundation = { version = "0.3", default-features = false, features = [
"std",
"NSBundle",
"NSData",
"NSGeometry",
"NSProcessInfo",
"NSString",
] }

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_System_Console"] }

Expand Down
113 changes: 113 additions & 0 deletions crates/disktree-app/src/dock_icon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//! The Dock icon of a run outside the app bundle.
//!
//! `make run`, `cargo run` and the bare release binary all start disktree
//! with no bundle around it, so macOS has no icon to draw and the Dock shows
//! the generic executable. `AppKit` lets a running app replace its Dock icon
//! with an image, so the app carries its own and paints it whenever the
//! bundle it runs in, if any, names none.
//!
//! The image is drawn literally. macOS 26 gives Liquid Glass only to a
//! bundle's Icon Composer icon and has no API to ask for it at run time, so
//! the glass is baked into the image instead: `DockIcon-glass.png` is the
//! shipped icon as macOS 26 itself draws it, rendered by
//! `packaging/macos/render-dock-icons.swift`. macOS 15 and earlier draw a
//! flat icon on Apple's legacy tile, and get `DockIcon.png`, which is that.

#![allow(
unsafe_code,
reason = "one AppKit setter objc2 marks unsafe; the call says why it is \
sound"
)]

use objc2::{AllocAnyThread as _, MainThreadMarker};
use objc2_app_kit::{NSApplication, NSImage};
use objc2_foundation::{NSBundle, NSData, NSString};

const FLAT: &[u8] = include_bytes!("../../../packaging/macos/DockIcon.png");
const GLASS: &[u8] =
include_bytes!("../../../packaging/macos/DockIcon-glass.png");

/// Give the Dock an icon unless the bundle the app runs in has one.
///
/// For the main thread, once `AppKit` is up: the closure `Application::run`
/// calls back is that, and by then the Dock tile exists to paint. Anywhere
/// else this does nothing.
pub fn install() {
let Some(mtm) = MainThreadMarker::new() else {
return;
};
if bundle_names_an_icon() {
return;
}
let data = NSData::with_bytes(embedded(draws_liquid_glass()));
let Some(image) = NSImage::initWithData(NSImage::alloc(), &data) else {
return;
};
let app = NSApplication::sharedApplication(mtm);
// SAFETY: the setter is main-thread only, which `mtm` proves, and takes
// an image AppKit retains for itself.
unsafe { app.setApplicationIconImage(Some(&image)) };
}

/// Whether the main bundle names an icon for macOS to draw. Outside a bundle
/// `NSBundle.mainBundle` is the executable's directory and names nothing.
fn bundle_names_an_icon() -> bool {
let bundle = NSBundle::mainBundle();
["CFBundleIconName", "CFBundleIconFile"].iter().any(|key| {
bundle
.objectForInfoDictionaryKey(&NSString::from_str(key))
.is_some()
})
}

/// Whether this Mac draws app icons in Liquid Glass: macOS 26 and later,
/// which is Darwin 25.
///
/// The kernel release, not `NSProcessInfo`: macOS 26 reports itself as 16.0
/// to a binary linked against an older SDK, which is what a Mac with the
/// Command Line Tools or Xcode 16 builds.
fn draws_liquid_glass() -> bool {
darwin_major(&rustix::system::uname().release().to_string_lossy())
.is_some_and(|major| major >= 25)
}

/// The major version of a Darwin release such as `25.6.0`.
fn darwin_major(release: &str) -> Option<u32> {
release.split('.').next()?.parse().ok()
}

/// The icon for the look this Mac gives its icons.
const fn embedded(liquid_glass: bool) -> &'static [u8] {
if liquid_glass { GLASS } else { FLAT }
}

#[cfg(test)]
mod tests {
use super::*;

/// Both files decode, and to the size the renderer promises: the Dock
/// draws at most 128 pt, twice that on a Retina display.
#[test]
fn both_dock_icons_are_512_pixel_pngs() {
for liquid_glass in [false, true] {
let bytes = embedded(liquid_glass);
assert!(bytes.starts_with(b"\x89PNG\r\n\x1a\n"));
let image = NSImage::initWithData(
NSImage::alloc(),
&NSData::with_bytes(bytes),
)
.expect("a decodable PNG");
let size = image.size();
assert_eq!((size.width, size.height), (512., 512.));
}
// The glass one is a render of the flat one, never the same file.
assert_ne!(embedded(true), embedded(false));
}

#[test]
fn darwin_25_is_the_first_to_draw_liquid_glass() {
assert_eq!(darwin_major("25.6.0"), Some(25));
assert_eq!(darwin_major("24.6.0"), Some(24));
assert_eq!(darwin_major(""), None);
}
}
4 changes: 4 additions & 0 deletions crates/disktree-app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

mod app_menu;
mod appearance;
#[cfg(target_os = "macos")]
mod dock_icon;
mod git;
mod marks;
mod palette;
Expand Down Expand Up @@ -183,6 +185,8 @@ fn run() -> Result<()> {
.run(move |cx| {
gpui_omarchy::init(cx);
app_menu::install(cx);
#[cfg(target_os = "macos")]
dock_icon::install();
let home = std::env::home_dir();
let native_look = appearance::follows_system(home.as_deref());
if native_look {
Expand Down
31 changes: 31 additions & 0 deletions packaging/macos/AppGlyph.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions packaging/macos/AppIcon.icon/icon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"fill" : {
"solid" : "srgb:0.10196,0.10588,0.14902,1.00000"
},
"groups" : [
{
"name" : "Mark",
"layers" : [
{
"name" : "glyph",
"image-name" : "glyph.png",
"fill" : "automatic",
"glass" : false,
"hidden" : false,
"blend-mode" : "normal"
}
],
"lighting" : "individual",
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : false,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}
Binary file added packaging/macos/DockIcon-glass.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packaging/macos/DockIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions packaging/macos/Info.plist.in
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@
<string>disktree</string>
<key>CFBundleExecutable</key>
<string>disktree</string>
<!-- AppIcon.icns, for macOS 15 and earlier. The marker becomes
CFBundleIconName when the bundle also carries Assets.car, the Icon
Composer icon macOS 26 draws instead; without Xcode 26's actool to
compile it the marker is dropped. -->
<key>CFBundleIconFile</key>
<string>AppIcon</string>
@ICON_CATALOG@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key>
Expand Down
Loading
Loading