├── scripts
├── smart-common-loader.sh
├── generate-standalone-scripts.sh
├── tabs.toml
├── system-setup
│ ├── dev-setup.sh
│ ├── tab_data.toml
│ ├── system-cleanup.sh
│ ├── remove-animations.sh
│ └── fix-finder.sh
├── applications-setup
│ ├── developer-tools
│ │ ├── zed.sh
│ │ ├── sublime.sh
│ │ ├── vscode.sh
│ │ ├── vscodium.sh
│ │ ├── githubdesktop.sh
│ │ ├── jetbrains-toolbox.sh
│ │ └── neovim.sh
│ ├── browsers
│ │ ├── chromium.sh
│ │ ├── zen-browser.sh
│ │ ├── brave.sh
│ │ ├── vivaldi.sh
│ │ ├── firefox.sh
│ │ ├── waterfox.sh
│ │ ├── librewolf.sh
│ │ ├── thorium.sh
│ │ └── google-chrome.sh
│ ├── communication-apps
│ │ ├── slack-setup.sh
│ │ ├── signal-setup.sh
│ │ ├── discord-setup.sh
│ │ ├── whatsapp-setup.sh
│ │ ├── telegram-setup.sh
│ │ └── thunderbird-setup.sh
│ ├── android-debloat.sh
│ ├── kitty-setup.sh
│ ├── alacritty-setup.sh
│ ├── fastfetch-setup.sh
│ ├── zsh-setup.sh
│ └── tab_data.toml
└── common-script.sh
├── run-gui.sh
├── MacUtilGUI
├── logo.png
├── MacUtilGUI.icns
├── Assets
│ └── MacUtilGUI.icns
├── Models
│ ├── ScriptCategory.fs
│ └── ScriptInfo.fs
├── MacUtilGUI.entitlements
├── ViewModels
│ ├── ViewModelBase.fs
│ └── MainWindowViewModel.fs
├── Program.fs
├── Info.plist
├── Views
│ ├── MainWindow.axaml.fs
│ └── MainWindow.axaml
├── app.manifest
├── create_icon_from_logo.sh
├── README.md
├── MacUtilGUI.fsproj
├── build_universal.sh
├── TTY_FIXES.md
├── DEPLOYMENT.md
├── App.axaml
├── sign_macos.sh
├── deploy_macos.sh
└── Services
│ └── ScriptService.fs
├── .github
├── dependabot.yml
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── feature-reqests.yml
│ └── bug-reports.yml
├── workflows
│ ├── typos.yml
│ ├── shellcheck.yml
│ ├── fsharp.yml
│ ├── bashisms.yml
│ ├── Release.yml
│ └── issue-slash-commands.yaml
├── release.yml
├── SECURITY.md
├── PULL_REQUEST_TEMPLATE.md
├── CONTRIBUTING.md
└── CODE_OF_CONDUCT.md
├── .vscode
└── tasks.json
├── LICENSE
├── README.md
└── .gitignore
/scripts/smart-common-loader.sh:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/generate-standalone-scripts.sh:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/run-gui.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$0")/MacUtilGUI" || exit 1
3 | dotnet run
4 |
--------------------------------------------------------------------------------
/MacUtilGUI/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisTitusTech/macutil/HEAD/MacUtilGUI/logo.png
--------------------------------------------------------------------------------
/scripts/tabs.toml:
--------------------------------------------------------------------------------
1 | directories = [
2 | "applications-setup",
3 | "system-setup",
4 | ]
5 |
--------------------------------------------------------------------------------
/MacUtilGUI/MacUtilGUI.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisTitusTech/macutil/HEAD/MacUtilGUI/MacUtilGUI.icns
--------------------------------------------------------------------------------
/MacUtilGUI/Assets/MacUtilGUI.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisTitusTech/macutil/HEAD/MacUtilGUI/Assets/MacUtilGUI.icns
--------------------------------------------------------------------------------
/MacUtilGUI/Models/ScriptCategory.fs:
--------------------------------------------------------------------------------
1 | namespace MacUtilGUI.Models
2 |
3 | type ScriptCategory =
4 | { Name: string
5 | Scripts: ScriptInfo list }
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: macutil documentation
4 | url: https://chris-titus-docs.github.io/macutil-docs/
5 | about: check out the docs
6 |
--------------------------------------------------------------------------------
/MacUtilGUI/Models/ScriptInfo.fs:
--------------------------------------------------------------------------------
1 | namespace MacUtilGUI.Models
2 |
3 | type ScriptInfo =
4 | { Name: string
5 | Description: string
6 | Script: string
7 | TaskList: string
8 | Category: string
9 | FullPath: string }
10 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "Build MacUtil GUI",
6 | "type": "shell",
7 | "command": "dotnet",
8 | "args": [
9 | "build"
10 | ],
11 | "group": "build",
12 | "problemMatcher": [
13 | "$tsc"
14 | ]
15 | }
16 | ]
17 | }
--------------------------------------------------------------------------------
/.github/workflows/typos.yml:
--------------------------------------------------------------------------------
1 | name: Check for typos
2 |
3 | on:
4 | [push, pull_request, workflow_dispatch]
5 |
6 | jobs:
7 | check-typos:
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v5
12 | - run: git fetch origin ${{ github.base_ref }}
13 |
14 | - name: Run spellcheck
15 | uses: crate-ci/typos@v1.34.0
16 |
--------------------------------------------------------------------------------
/MacUtilGUI/MacUtilGUI.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-jit
6 |
7 | com.apple.security.automation.apple-events
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/scripts/system-setup/dev-setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 | # shellcheck disable=SC2086
3 |
4 | . ../common-script.sh
5 |
6 | installDepend() {
7 | ## Check for dependencies.
8 | DEPENDENCIES='tree multitail tealdeer unzip cmake make jq fd ripgrep automake autoconf rustup python pipx'
9 | printf "%b\n" "${YELLOW}Installing dependencies...${RC}"
10 | brew install $DEPENDENCIES
11 | }
12 |
13 | checkEnv
14 | installDepend
--------------------------------------------------------------------------------
/MacUtilGUI/ViewModels/ViewModelBase.fs:
--------------------------------------------------------------------------------
1 | namespace MacUtilGUI.ViewModels
2 |
3 | open System.ComponentModel
4 |
5 | type ViewModelBase() =
6 | let propertyChanged = Event()
7 |
8 | interface INotifyPropertyChanged with
9 | []
10 | member _.PropertyChanged = propertyChanged.Publish
11 |
12 | member this.OnPropertyChanged(propertyName: string) =
13 | propertyChanged.Trigger(this, PropertyChangedEventArgs(propertyName))
14 |
--------------------------------------------------------------------------------
/scripts/applications-setup/developer-tools/zed.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installZed() {
6 | if ! brewprogram_exists zed; then
7 | printf "%b\n" "${CYAN}Installing Zed.${RC}"
8 | brew install --cask zed
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Zed. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Zed installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Zed is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installZed
--------------------------------------------------------------------------------
/scripts/applications-setup/browsers/chromium.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installChromium() {
6 | if ! brewprogram_exists chromium; then
7 | printf "%b\n" "${YELLOW}Installing Chromium...${RC}"
8 | brew install --cask chromium
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Chromium Browser. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Chromium Browser installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Chromium Browser is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installChromium
--------------------------------------------------------------------------------
/scripts/applications-setup/communication-apps/slack-setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installSlack() {
6 | if ! brewprogram_exists slack; then
7 | printf "%b\n" "${YELLOW}Installing Slack...${RC}"
8 | brew install --cask slack
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Slack. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Slack installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Slack is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installSlack
--------------------------------------------------------------------------------
/scripts/applications-setup/communication-apps/signal-setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installSignal() {
6 | if ! brewprogram_exists signal; then
7 | printf "%b\n" "${YELLOW}Installing Signal...${RC}"
8 | brew install --cask signal
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Signal. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Signal installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Signal is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installSignal
--------------------------------------------------------------------------------
/scripts/applications-setup/browsers/zen-browser.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installZenBrowser() {
6 | if ! brewprogram_exists zen; then
7 | printf "%b\n" "${YELLOW}Installing Zen Browser...${RC}"
8 | brew install --cask zen
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Zen Browser. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Zen Browser installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Zen Browser is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installZenBrowser
--------------------------------------------------------------------------------
/scripts/applications-setup/communication-apps/discord-setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installDiscord() {
6 | if ! brewprogram_exists discord; then
7 | printf "%b\n" "${YELLOW}Installing Discord...${RC}"
8 | brew install --cask discord
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Discord. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Discord installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Discord is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installDiscord
--------------------------------------------------------------------------------
/scripts/applications-setup/browsers/brave.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installBrave() {
6 | if ! brewprogram_exists brave-browser; then
7 | printf "%b\n" "${YELLOW}Installing Brave...${RC}"
8 | brew install --cask brave-browser
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Brave Browser. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Brave Browser installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Brave Browser is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installBrave
--------------------------------------------------------------------------------
/scripts/applications-setup/browsers/vivaldi.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installVivaldi() {
6 | if ! brewprogram_exists vivaldi; then
7 | printf "%b\n" "${YELLOW}Installing Vivaldi...${RC}"
8 | brew install --cask vivaldi
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Vivaldi Browser. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Vivaldi Browser installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Vivaldi Browser is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installVivaldi
--------------------------------------------------------------------------------
/scripts/applications-setup/communication-apps/whatsapp-setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installWhatsApp() {
6 | if ! brewprogram_exists whatsapp; then
7 | printf "%b\n" "${YELLOW}Installing WhatsApp...${RC}"
8 | brew install --cask whatsapp
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install WhatsApp. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}WhatsApp installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}WhatsApp is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installWhatsApp
--------------------------------------------------------------------------------
/scripts/applications-setup/developer-tools/sublime.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installSublime() {
6 | if ! brewprogram_exists sublime-text; then
7 | printf "%b\n" "${YELLOW}Installing Sublime...${RC}"
8 | brew install --cask sublime-text
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Sublime. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Sublime installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Sublime is already installed.${RC}"
16 | fi
17 |
18 | }
19 |
20 | checkEnv
21 | installSublime
--------------------------------------------------------------------------------
/scripts/applications-setup/developer-tools/vscode.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installVsCode() {
6 | if ! brewprogram_exists visual-studio-code; then
7 | printf "%b\n" "${YELLOW}Installing VS Code..${RC}."
8 | brew install --cask visual-studio-code
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install VS Code. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}VS Code installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}VS Code is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installVsCode
--------------------------------------------------------------------------------
/scripts/applications-setup/developer-tools/vscodium.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installVsCodium() {
6 | if ! brewprogram_exists vscodium; then
7 | printf "%b\n" "${YELLOW}Installing VS Codium...${RC}"
8 | brew install --cask vscodium
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install VS Codium. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}VS Codium installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}VS Codium is already installed.${RC}"
16 | fi
17 |
18 | }
19 |
20 | checkEnv
21 | installVsCodium
--------------------------------------------------------------------------------
/scripts/applications-setup/browsers/firefox.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installFirefox() {
6 | if ! brewprogram_exists firefox; then
7 | printf "%b\n" "${YELLOW}Installing Mozilla Firefox...${RC}"
8 | brew install --cask firefox
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Firefox Browser. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Firefox Browser installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Firefox Browser is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installFirefox
--------------------------------------------------------------------------------
/scripts/applications-setup/browsers/waterfox.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installWaterfox() {
6 | if ! brewprogram_exists waterfox; then
7 | printf "%b\n" "${YELLOW}Installing waterfox...${RC}"
8 | brew install --cask waterfox
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Waterfox Browser. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Waterfox Browser installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Waterfox is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installWaterfox
21 |
--------------------------------------------------------------------------------
/scripts/applications-setup/browsers/librewolf.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installLibreWolf() {
6 | if ! brewprogram_exists librewolf; then
7 | printf "%b\n" "${YELLOW}Installing Librewolf...${RC}"
8 | brew install --cask librewolf
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install LibreWolf Browser. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}LibreWolf Browser installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}LibreWolf Browser is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installLibreWolf
--------------------------------------------------------------------------------
/scripts/applications-setup/communication-apps/telegram-setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installTelegram() {
6 | if ! brewprogram_exists telegram-desktop; then
7 | printf "%b\n" "${YELLOW}Installing Telegram...${RC}"
8 | brew install --cask telegram-desktop
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Telegram. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Telegram installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Telegram is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installTelegram
--------------------------------------------------------------------------------
/scripts/applications-setup/browsers/thorium.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installThorium() {
6 | if ! brewprogram_exists alex313031-thorium; then
7 | printf "%b\n" "${YELLOW}Installing Thorium Browser...${RC}"
8 | brew install --cask alex313031-thorium
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Thorium Browser. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Thorium Browser installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Thorium Browser is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installThorium
--------------------------------------------------------------------------------
/scripts/applications-setup/communication-apps/thunderbird-setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installThunderBird() {
6 | if ! brewprogram_exists thunderbird; then
7 | printf "%b\n" "${YELLOW}Installing Thunderbird...${RC}"
8 | brew install --cask thunderbird
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Thunderbird. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Thunderbird installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Thunderbird is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installThunderBird
--------------------------------------------------------------------------------
/scripts/applications-setup/developer-tools/githubdesktop.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installGithubDesktop() {
6 | if ! brewprogram_exists github; then
7 | printf "%b\n" "${YELLOW}Installing Github Desktop...${RC}"
8 | brew install --cask github
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Github Desktop. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Github Desktop installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Github Desktop is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installGithubDesktop
--------------------------------------------------------------------------------
/scripts/applications-setup/browsers/google-chrome.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installChrome() {
6 | if ! brewprogram_exists google-chrome; then
7 | printf "%b\n" "${YELLOW}Installing Google Chrome...${RC}"
8 | brew install --cask google-chrome
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Google Chrome Browser. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Google Chrome Browser installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Google Chrome Browser is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installChrome
--------------------------------------------------------------------------------
/scripts/applications-setup/developer-tools/jetbrains-toolbox.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installJetBrainsToolBox() {
6 | if ! brewprogram_exists jetbrains-toolbox; then
7 | printf "%b\n" "${YELLOW}Installing Jetbrains Toolbox...${RC}"
8 | brew install --cask jetbrains-toolbox
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Jetbrains Toolbox. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Successfully installed Jetbrains Toolbox.${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Jetbrains toolbox is already installed.${RC}"
16 | fi
17 | }
18 |
19 | checkEnv
20 | installJetBrainsToolBox
--------------------------------------------------------------------------------
/.github/workflows/shellcheck.yml:
--------------------------------------------------------------------------------
1 | name: Script Checks
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - '**/*.sh'
7 | workflow_dispatch:
8 |
9 | permissions:
10 | contents: read
11 | pull-requests: write
12 |
13 | jobs:
14 | shellcheck:
15 | name: Shellcheck
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout sources
19 | uses: actions/checkout@v5
20 |
21 | - name: Run ShellCheck
22 | uses: reviewdog/action-shellcheck@v1
23 | with:
24 | shellcheck_flags: '--source-path=${{ github.workspace }}/.shellcheckrc'
25 | reviewdog_flags: '-fail-level=any'
26 |
27 | shfmt:
28 | name: Shell Fomatting
29 | runs-on: ubuntu-latest
30 | needs: shellcheck
31 | steps:
32 | - name: Checkout sources
33 | uses: actions/checkout@v5
34 |
--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
1 | changelog:
2 | categories:
3 | - title: '🚀 Features'
4 | labels:
5 | - 'enhancement'
6 | - title: '🐛 Bug Fixes'
7 | labels:
8 | - 'bug'
9 | - title: '⚙️ Refactoring'
10 | labels:
11 | - 'refactor'
12 | - title: '🧩 UI/UX'
13 | labels:
14 | - 'UI/UX'
15 | - title: '📚 Documentation'
16 | labels:
17 | - 'documentation'
18 | - title: '🔒 Security'
19 | labels:
20 | - 'security'
21 | - title: '🧰 GitHub Actions'
22 | labels:
23 | - 'github_actions'
24 | - title: '🦀 Rust'
25 | labels:
26 | - 'rust'
27 | - title: '📃 Scripting'
28 | labels:
29 | - 'script'
30 | - title: 'Other Changes'
31 | labels:
32 | - "*"
33 | exclude:
34 | labels:
35 | - 'skip-changelog'
36 |
--------------------------------------------------------------------------------
/scripts/applications-setup/android-debloat.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../common-script.sh
4 |
5 | install_adb() {
6 | brew install android-platform-tools
7 | }
8 |
9 | install_universal_android_debloater() {
10 | if ! command_exists uad; then
11 | printf "%b\n" "${YELLOW}Installing Universal Android Debloater...${RC}."
12 | curl -sSLo "${HOME}/uad" "https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/releases/latest/download/uad-ng-macos"
13 | "$ESCALATION_TOOL" chmod +x "${HOME}/uad"
14 | "$ESCALATION_TOOL" mv "${HOME}/uad" /usr/local/bin/uad
15 | else
16 | printf "%b\n" "${GREEN}Universal Android Debloater is already installed. Run 'uad' command to execute.${RC}"
17 | fi
18 | }
19 |
20 | checkEnv
21 | install_adb
22 | install_universal_android_debloater
23 |
--------------------------------------------------------------------------------
/scripts/system-setup/tab_data.toml:
--------------------------------------------------------------------------------
1 | name = "System Setup"
2 |
3 | [[data]]
4 | name = "System Tools"
5 |
6 | [[data.entries]]
7 | name = "Development Setup"
8 | description = "This script is designed to handle the installation of various development dependencies and tools across different Linux distributions"
9 | script = "dev-setup.sh"
10 |
11 | [[data.entries]]
12 | name = "Full System Cleanup"
13 | description = "This script is designed to remove unnecessary packages, clean old cache files, remove temporary files, and to empty the trash."
14 | script = "system-cleanup.sh"
15 |
16 | [[data.entries]]
17 | name = "Fix Finder"
18 | description = "Finder optimizations for people without a mental disorder."
19 | script = "fix-finder.sh"
20 |
21 | [[data.entries]]
22 | name = "Remove Animations"
23 | description = "This script is designed to remove snap packages and the snapd service from your system"
24 | script = "remove-animations.sh"
--------------------------------------------------------------------------------
/MacUtilGUI/Program.fs:
--------------------------------------------------------------------------------
1 | namespace MacUtilGUI
2 |
3 | open System
4 | open Avalonia
5 | open Avalonia.Controls.ApplicationLifetimes
6 | open Avalonia.Markup.Xaml
7 | open MacUtilGUI.Views
8 | open MacUtilGUI.ViewModels
9 |
10 | type App() =
11 | inherit Application()
12 |
13 | override this.Initialize() = AvaloniaXamlLoader.Load(this)
14 |
15 | override this.OnFrameworkInitializationCompleted() =
16 | match this.ApplicationLifetime with
17 | | :? IClassicDesktopStyleApplicationLifetime as desktop ->
18 | desktop.MainWindow <- MainWindow(DataContext = MainWindowViewModel())
19 | | _ -> ()
20 |
21 | base.OnFrameworkInitializationCompleted()
22 |
23 | module Program =
24 |
25 | []
26 | let buildAvaloniaApp () =
27 | AppBuilder.Configure().UsePlatformDetect().LogToTrace()
28 |
29 | []
30 | let main argv =
31 | buildAvaloniaApp().StartWithClassicDesktopLifetime(argv)
32 |
--------------------------------------------------------------------------------
/scripts/system-setup/system-cleanup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../common-script.sh
4 |
5 | cleanup_system() {
6 | printf "%b\n" "${YELLOW}Performing system cleanup...${RC}"
7 | # Fix Missions control to NEVER rearrange spaces
8 | printf "%b\n" "${CYAN}Fixing Mission Control to never rearrange spaces...${RC}"
9 | $ESCALATION_TOOL defaults write com.apple.dock mru-spaces -bool false
10 |
11 | # Apple Intelligence Crap
12 | $ESCALATION_TOOL defaults write com.apple.CloudSubscriptionFeatures.optIn "545129924" -bool "false"
13 |
14 | # Empty Trash
15 | printf "%b\n" "${CYAN}Emptying Trash...${RC}"
16 | $ESCALATION_TOOL rm -rf ~/.Trash/*
17 |
18 | # Remove old log files
19 | printf "%b\n" "${CYAN}Removing old log files...${RC}"
20 | find /var/log -type f -name "*.log" -mtime +30 -exec $ESCALATION_TOOL rm -f {} \;
21 | find /var/log -type f -name "*.old" -mtime +30 -exec $ESCALATION_TOOL rm -f {} \;
22 | find /var/log -type f -name "*.err" -mtime +30 -exec $ESCALATION_TOOL rm -f {} \;
23 |
24 | }
25 |
26 | checkEnv
27 | cleanup_system
--------------------------------------------------------------------------------
/scripts/applications-setup/developer-tools/neovim.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../../common-script.sh
4 |
5 | installNeovim() {
6 | print_info "Setting up Neovim..."
7 |
8 | # Install Neovim and dependencies with brew
9 | print_info "Installing Neovim and dependencies..."
10 | brew install neovim ripgrep fzf
11 |
12 | # Backup existing config if it exists
13 | if [ -d "$HOME/.config/nvim" ] && [ ! -d "$HOME/.config/nvim-backup" ]; then
14 | print_info "Backing up existing Neovim config..."
15 | cp -r "$HOME/.config/nvim" "$HOME/.config/nvim-backup"
16 | fi
17 |
18 | # Clear existing config
19 | rm -rf "$HOME/.config/nvim"
20 | mkdir -p "$HOME/.config/nvim"
21 |
22 | # Clone Titus kickstart config directly to .config/nvim
23 | print_info "Applying Titus Kickstart config..."
24 | git clone --depth 1 https://github.com/ChrisTitusTech/neovim.git /tmp/neovim
25 | cp -r /tmp/neovim/titus-kickstart/* "$HOME/.config/nvim/"
26 | rm -rf /tmp/neovim
27 | print_success "Neovim setup completed."
28 | }
29 |
30 | checkEnv
31 | installNeovim
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Chris Titus
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/scripts/applications-setup/kitty-setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../common-script.sh
4 |
5 | installKitty() {
6 | if ! brewprogram_exists kitty; then
7 | brew install --cask kitty
8 | if [ $? -ne 0 ]; then
9 | printf "%b\n" "${RED}Failed to install Kitty. Please check your Homebrew installation or try again later.${RC}"
10 | exit 1
11 | fi
12 | printf "%b\n" "${GREEN}Kitty installed successfully!${RC}"
13 | else
14 | printf "%b\n" "${GREEN}Kitty is already installed.${RC}"
15 | fi
16 | }
17 |
18 | setupKittyConfig() {
19 | printf "%b\n" "${YELLOW}Copying Kitty configuration files...${RC}"
20 | if [ -d "${HOME}/.config/kitty" ] && [ ! -d "${HOME}/.config/kitty-bak" ]; then
21 | cp -r "${HOME}/.config/kitty" "${HOME}/.config/kitty-bak"
22 | fi
23 | mkdir -p "${HOME}/.config/kitty/"
24 | curl -sSLo "${HOME}/.config/kitty/kitty.conf" https://github.com/ChrisTitusTech/dwm-titus/raw/main/config/kitty/kitty.conf
25 | curl -sSLo "${HOME}/.config/kitty/nord.conf" https://github.com/ChrisTitusTech/dwm-titus/raw/main/config/kitty/nord.conf
26 | }
27 |
28 | checkEnv
29 | installKitty
30 | setupKittyConfig
--------------------------------------------------------------------------------
/scripts/applications-setup/alacritty-setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../common-script.sh
4 |
5 | installAlacritty() {
6 | if ! brewprogram_exists alacritty; then
7 | printf "%b\n" "${YELLOW}Installing Alacritty...${RC}"
8 | brew install --cask alacritty
9 | else
10 | printf "%b\n" "${GREEN}Alacritty is already installed.${RC}"
11 | fi
12 | }
13 |
14 | setupAlacrittyConfig() {
15 | printf "%b\n" "${YELLOW}Copying alacritty config files...${RC}"
16 | if [ -d "${HOME}/.config/alacritty" ] && [ ! -d "${HOME}/.config/alacritty-bak" ]; then
17 | cp -r "${HOME}/.config/alacritty" "${HOME}/.config/alacritty-bak"
18 | fi
19 | mkdir -p "${HOME}/.config/alacritty/"
20 | curl -sSLo "${HOME}/.config/alacritty/alacritty.toml" "https://github.com/ChrisTitusTech/dwm-titus/raw/main/config/alacritty/alacritty.toml"
21 | curl -sSLo "${HOME}/.config/alacritty/keybinds.toml" "https://github.com/ChrisTitusTech/dwm-titus/raw/main/config/alacritty/keybinds.toml"
22 | curl -sSLo "${HOME}/.config/alacritty/nordic.toml" "https://github.com/ChrisTitusTech/dwm-titus/raw/main/config/alacritty/nordic.toml"
23 | printf "%b\n" "${GREEN}Alacritty configuration files copied.${RC}"
24 | }
25 |
26 | checkEnv
27 | installAlacritty
28 | setupAlacrittyConfig
--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | It is recommended that you use the stable branch as it's tested and used by most. The dev branch may contain bleeding-edge commits that are not well tested and are not meant to be used in production environments.
6 | Version tags lower than the [latest stable release](https://github.com/ChrisTitusTech/macutil/releases/latest) are **not** supported.
7 |
8 | | Branch | Supported |
9 | | ------- | ---------------------- |
10 | | Stable | :white_check_mark: YES |
11 | | Dev | :x: NO |
12 |
13 | | Version | Supported |
14 | | -------------------------------------------------- | ---------------------- |
15 | | [](https://github.com/ChrisTitusTech/macutil/releases/latest) | :white_check_mark: YES |
16 | | Below LATEST | :x: NO |
17 | | Above LATEST | :x: NO |
18 |
19 | ## Reporting a Vulnerability
20 |
21 | If you have any reason to believe there are security vulnerabilities in macutil, fill out the [report form](https://github.com/christitustech/macutil/security/advisories/new) or e-mail [contact@christitus.com](mailto:contact@christitus.com).
22 |
--------------------------------------------------------------------------------
/.github/workflows/fsharp.yml:
--------------------------------------------------------------------------------
1 | name: F# Checks
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: ["main"]
7 | paths:
8 | - '**/*.fs'
9 | - '**/*.fsproj'
10 | - 'MacUtilGUI/**'
11 | pull_request:
12 | branches: ["main"]
13 | paths:
14 | - '**/*.fs'
15 | - '**/*.fsproj'
16 | - 'MacUtilGUI/**'
17 |
18 | jobs:
19 | lints:
20 | name: F# Build and Format Check
21 | runs-on: ubuntu-latest
22 |
23 | steps:
24 | - name: Checkout sources
25 | uses: actions/checkout@v5
26 |
27 | - name: Setup .NET
28 | uses: actions/setup-dotnet@v4
29 | with:
30 | dotnet-version: '9.0.x'
31 |
32 | - name: Cache .NET packages
33 | uses: actions/cache@v4
34 | with:
35 | path: ~/.nuget/packages
36 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
37 | restore-keys: ${{ runner.os }}-nuget-
38 |
39 | - name: Restore dependencies
40 | run: dotnet restore MacUtilGUI/MacUtilGUI.fsproj
41 |
42 | - name: Install Fantomas
43 | run: |
44 | dotnet tool install -g fantomas
45 | echo "$HOME/.dotnet/tools" >> $GITHUB_PATH
46 |
47 | - name: Build project
48 | run: dotnet build MacUtilGUI/MacUtilGUI.fsproj --configuration Release --no-restore
49 |
50 | - name: Check F# formatting
51 | run: fantomas --check MacUtilGUI/
52 |
--------------------------------------------------------------------------------
/MacUtilGUI/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleIconFile
6 | MacUtilGUI.icns
7 | CFBundleIdentifier
8 | com.macutil.gui
9 | CFBundleName
10 | MacUtil
11 | CFBundleDisplayName
12 | MacUtil GUI
13 | CFBundleVersion
14 | 0.2.1
15 | CFBundleShortVersionString
16 | 0.2.1
17 | LSMinimumSystemVersion
18 | 10.15
19 | CFBundleExecutable
20 | MacUtilGUI
21 | CFBundleInfoDictionaryVersion
22 | 6.0
23 | CFBundlePackageType
24 | APPL
25 | CFBundleSignature
26 | ????
27 | NSHighResolutionCapable
28 |
29 | NSPrincipalClass
30 | NSApplication
31 | LSApplicationCategoryType
32 | public.app-category.utilities
33 | NSRequiresAquaSystemAppearance
34 |
35 | NSHumanReadableCopyright
36 | Copyright © 2025 MacUtil. All rights reserved.
37 |
38 |
39 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
5 |
6 | ## Type of Change
7 | - [ ] New feature
8 | - [ ] Bug fix
9 | - [ ] Documentation update
10 | - [ ] Refactoring
11 | - [ ] Hotfix
12 | - [ ] Security patch
13 | - [ ] UI/UX improvement
14 |
15 | ## Description
16 |
17 |
18 | ## Testing
19 |
20 |
21 | ## Impact
22 |
23 |
24 | ## Issues / other PRs related
25 |
26 | - Resolves #
27 |
28 | ## Additional Information
29 |
30 |
31 | ## Checklist
32 | - [ ] My code adheres to the coding and style guidelines of the project.
33 | - [ ] I have performed a self-review of my own code.
34 | - [ ] I have commented my code, particularly in hard-to-understand areas.
35 | - [ ] I have made corresponding changes to the documentation.
36 | - [ ] My changes generate no errors/warnings/merge conflicts.
37 |
--------------------------------------------------------------------------------
/.github/workflows/bashisms.yml:
--------------------------------------------------------------------------------
1 | name: Check for bashisms
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - 'core/tabs/**/*.sh'
7 | merge_group:
8 | workflow_dispatch:
9 |
10 | jobs:
11 | check-bashisms:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v5
16 | - run: git fetch origin ${{ github.base_ref }}
17 |
18 | - name: Install devscripts
19 | run: sudo apt-get update && sudo apt-get install -y devscripts
20 |
21 | - name: Get changed .sh files (PR only)
22 | id: changed-sh-files
23 | if: github.event_name == 'pull_request'
24 | uses: tj-actions/changed-files@v46
25 | with:
26 | files: '**/*.sh'
27 |
28 | - name: Get all .sh files (if workflow dispatched)
29 | id: sh-files
30 | if: github.event_name != 'pull_request'
31 | run: |
32 | files=$(find . -type f -name "*.sh" | tr '\n' ' ')
33 | echo "files=${files:-none}" >> $GITHUB_ENV
34 |
35 | - name: Set FILES for bashism check
36 | id: set-files
37 | run: |
38 | if [[ "${{ steps.changed-sh-files.outputs.any_changed }}" == 'true' ]]; then
39 | echo "FILES=${{ steps.changed-sh-files.outputs.all_changed_files }}" >> $GITHUB_ENV
40 | else
41 | echo "FILES=${{ env.files }}" >> $GITHUB_ENV
42 | fi
43 |
44 | - name: Check for bashisms
45 | run: |
46 | IFS=' ' read -r -a file_array <<< "$FILES"
47 | checkbashisms "${file_array[@]}"
48 |
--------------------------------------------------------------------------------
/MacUtilGUI/Views/MainWindow.axaml.fs:
--------------------------------------------------------------------------------
1 | namespace MacUtilGUI.Views
2 |
3 | open System.Windows.Input
4 | open Avalonia
5 | open Avalonia.Controls
6 | open Avalonia.Input
7 | open Avalonia.Markup.Xaml
8 | open Avalonia.Interactivity
9 | open MacUtilGUI.ViewModels
10 | open MacUtilGUI.Models
11 |
12 | type MainWindow() as this =
13 | inherit Window()
14 |
15 | do this.InitializeComponent()
16 |
17 | member private this.InitializeComponent() = AvaloniaXamlLoader.Load(this)
18 |
19 | member private this.OnCloseButtonClick(sender: obj, e: RoutedEventArgs) =
20 | this.Close()
21 |
22 | member private this.OnMinimizeButtonClick(sender: obj, e: RoutedEventArgs) =
23 | this.WindowState <- WindowState.Minimized
24 |
25 | member private this.OnMaximizeButtonClick(sender: obj, e: RoutedEventArgs) =
26 | this.WindowState <-
27 | if this.WindowState = WindowState.FullScreen then
28 | WindowState.Normal
29 | else
30 | WindowState.FullScreen
31 |
32 | member private this.OnScriptButtonClick(sender: obj, e: RoutedEventArgs) =
33 | match sender with
34 | | :? Button as button ->
35 | match button.Tag with
36 | | :? ScriptInfo as script ->
37 | match this.DataContext with
38 | | :? MainWindowViewModel as vm -> (vm.SelectScriptCommand :> ICommand).Execute(script)
39 | | _ -> ()
40 | | _ -> ()
41 | | _ -> ()
42 |
43 | member private this.TitleBar_PointerPressed(sender: obj, e: PointerPressedEventArgs) =
44 | this.BeginMoveDrag(e)
45 |
--------------------------------------------------------------------------------
/MacUtilGUI/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | true
30 | PerMonitorV2
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chris Titus Tech's MacOS Utility
2 |
3 | [](https://github.com/ChrisTitusTech/macutil/releases/latest)
4 | 
5 |
6 |
7 | > [!NOTE]
8 | > Since the project is still in active development, you may encounter some issues. Please consider [submitting feedback](https://github.com/ChrisTitusTech/macutil/issues) if you do.
9 |
10 | # HEAVY DEVELOPMENT
11 |
12 | - Look at creating or installing a package into /Applications or other methods like copying to ~/.local/bin - _Update: Waiting on Apple Developer License Approval_
13 |
14 | # Installation
15 |
16 | Currently, macutil is download only and doesn't have a developer license. You can download the latest release from the [Releases page](https://github.com/ChrisTitusTech/macutil/releases/latest).
17 |
18 | ## 💖 Support
19 |
20 | If you find macutil helpful, please consider giving it a ⭐️ to show your support!
21 |
22 | ## 🛠 Contributing
23 |
24 | We welcome contributions from the community! Before you start, please review our [Contributing Guidelines](.github/CONTRIBUTING.md) to understand how to make the most effective and efficient contributions.
25 |
26 | ## 🏅 Thanks to All Contributors
27 |
28 | Thank you to everyone who has contributed to the development of macutil. Your efforts are greatly appreciated, and you're helping make this tool better for everyone!
29 |
30 | [](https://github.com/ChrisTitusTech/macutil/graphs/contributors)
31 |
32 | ## 📜 Contributor Milestones
33 |
34 | - 2025/07/21: Claude Sonnet 4 makes boilerplate code for the project.
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-reqests.yml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description: Suggest a new feature or improvement to help us enhance this project.
3 | title: "[Feature Request]: "
4 | labels: ["enhancement"]
5 | assignees: []
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: |
10 | Thank you for suggesting a feature or enhancement! Please provide as much detail as possible to help us understand your request. Note that submitting a feature request does not guarantee implementation.
11 | - type: textarea
12 | id: related-problem
13 | attributes:
14 | label: Is your feature request related to a problem? Please describe.
15 | description: |
16 | Provide a clear and concise description of the problem, if applicable.
17 | placeholder: I'm always frustrated when ...
18 | - type: textarea
19 | id: proposed-solution
20 | attributes:
21 | label: Describe the solution you'd like
22 | description: |
23 | Provide a clear and concise description of what you want to happen.
24 | placeholder: Explain your proposed feature or enhancement in detail.
25 | validations:
26 | required: true
27 | - type: textarea
28 | id: alternatives
29 | attributes:
30 | label: Describe alternatives you've considered
31 | description: |
32 | Provide a clear and concise description of any alternative solutions or features you've considered.
33 | placeholder: Explain any other approaches or solutions you've thought about.
34 | - type: textarea
35 | id: additional-context
36 | attributes:
37 | label: Additional context
38 | description: |
39 | Add any other context or screenshots about the feature request here.
40 | placeholder: Provide additional information, screenshots, or references to explain your request further.
41 | - type: checkboxes
42 | id: checklist
43 | attributes:
44 | label: Checklist
45 | options:
46 | - label: I checked for duplicate issues.
47 | - label: I checked already existing discussions.
48 | - label: This feature is not included in the roadmap.
49 | required: true
50 |
--------------------------------------------------------------------------------
/MacUtilGUI/create_icon_from_logo.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Script to create macOS app icon from logo.png
4 | # This script creates an iconset and converts it to .icns format
5 |
6 | echo "🎨 Creating macOS app icon from logo.png..."
7 |
8 | # Check if logo.png exists
9 | if [ ! -f "logo.png" ]; then
10 | echo "❌ Error: logo.png not found in current directory"
11 | exit 1
12 | fi
13 |
14 | # Create iconset directory
15 | echo "📁 Creating iconset directory..."
16 | rm -rf MacUtilGUI.iconset
17 | mkdir MacUtilGUI.iconset
18 |
19 | # Define icon sizes needed for macOS
20 | declare -a sizes=(16 32 128 256 512)
21 | declare -a retina_sizes=(32 64 256 512 1024)
22 |
23 | echo "🔄 Generating icon sizes..."
24 |
25 | # Generate standard resolution icons
26 | for i in "${!sizes[@]}"; do
27 | size=${sizes[$i]}
28 | echo " Creating ${size}x${size} icon..."
29 | sips -z $size $size logo.png --out MacUtilGUI.iconset/icon_${size}x${size}.png >/dev/null 2>&1
30 | done
31 |
32 | # Generate retina (@2x) resolution icons
33 | for i in "${!sizes[@]}"; do
34 | size=${sizes[$i]}
35 | retina_size=${retina_sizes[$i]}
36 | echo " Creating ${size}x${size}@2x icon (${retina_size}x${retina_size})..."
37 | sips -z $retina_size $retina_size logo.png --out MacUtilGUI.iconset/icon_${size}x${size}@2x.png >/dev/null 2>&1
38 | done
39 |
40 | # Verify iconset contents
41 | echo "📋 Generated icon files:"
42 | ls -la MacUtilGUI.iconset/
43 |
44 | # Convert iconset to icns format
45 | echo "🔨 Converting iconset to .icns format..."
46 | if command -v iconutil >/dev/null 2>&1; then
47 | iconutil -c icns MacUtilGUI.iconset -o MacUtilGUI.icns
48 | if [ $? -eq 0 ]; then
49 | echo "✅ Successfully created MacUtilGUI.icns"
50 |
51 | # Show file info
52 | echo "📊 Icon file info:"
53 | ls -lh MacUtilGUI.icns
54 |
55 | # Cleanup iconset directory
56 | rm -rf MacUtilGUI.iconset
57 | echo "🧹 Cleaned up temporary iconset directory"
58 |
59 | echo "🎉 App icon creation complete!"
60 | echo "📝 The MacUtilGUI.icns file is ready to use for your macOS app bundle."
61 | else
62 | echo "❌ Error: Failed to convert iconset to icns format"
63 | exit 1
64 | fi
65 | else
66 | echo "❌ Error: iconutil command not found (required for icns conversion)"
67 | echo "Please run this script on macOS with Xcode command line tools installed"
68 | exit 1
69 | fi
70 |
--------------------------------------------------------------------------------
/scripts/common-script.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | # shellcheck disable=SC2034
4 |
5 | RC=''
6 | RED=''
7 | YELLOW=''
8 | CYAN=''
9 | GREEN=''
10 |
11 | command_exists() {
12 | for cmd in "$@"; do
13 | command -v "$cmd" >/dev/null 2>&1 || return 1
14 | done
15 | return 0
16 | }
17 |
18 | brewprogram_exists() {
19 | for cmd in "$@"; do
20 | brew list "$cmd" >/dev/null 2>&1 || return 1
21 | done
22 | return 0
23 | }
24 |
25 | setup_askpass() {
26 | # Create a temporary askpass helper script
27 | ASKPASS_SCRIPT="/tmp/macutil_askpass_$$"
28 | cat > "$ASKPASS_SCRIPT" << 'EOF'
29 | #!/bin/sh
30 | osascript -e 'display dialog "Administrator password required for MacUtil setup:" default answer "" with hidden answer' -e 'text returned of result' 2>/dev/null
31 | EOF
32 | chmod +x "$ASKPASS_SCRIPT"
33 | export SUDO_ASKPASS="$ASKPASS_SCRIPT"
34 | }
35 |
36 | cleanup_askpass() {
37 | # Clean up the temporary askpass script
38 | if [ -n "$ASKPASS_SCRIPT" ] && [ -f "$ASKPASS_SCRIPT" ]; then
39 | rm -f "$ASKPASS_SCRIPT"
40 | fi
41 | }
42 |
43 | checkPackageManager() {
44 | ## Check if brew is installed
45 | if command_exists "brew"; then
46 | printf "%b\n" "${GREEN}Homebrew is installed${RC}"
47 | else
48 | printf "%b\n" "${RED}Homebrew is not installed${RC}"
49 | printf "%b\n" "${YELLOW}Installing Homebrew...${RC}"
50 |
51 | # Setup askpass helper for automated password handling
52 | setup_askpass
53 |
54 | # Use sudo with askpass for non-interactive installation
55 | SUDO_ASKPASS="$ASKPASS_SCRIPT" sudo -A /bin/bash -c "NONINTERACTIVE=1 $(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
56 | install_result=$?
57 |
58 | # Cleanup askpass helper
59 | cleanup_askpass
60 |
61 | if [ $install_result -ne 0 ]; then
62 | printf "%b\n" "${RED}Failed to install Homebrew${RC}"
63 | exit 1
64 | fi
65 |
66 | # Add Homebrew to PATH for the current session
67 | if [ -f "/opt/homebrew/bin/brew" ]; then
68 | eval "$(/opt/homebrew/bin/brew shellenv)"
69 | elif [ -f "/usr/local/bin/brew" ]; then
70 | eval "$(/usr/local/bin/brew shellenv)"
71 | fi
72 | trap cleanup_askpass EXIT INT TERM
73 | fi
74 | }
75 |
76 | checkCurrentDirectoryWritable() {
77 | ## Check if the current directory is writable.
78 | GITPATH="$(dirname "$(realpath "$0")")"
79 | if [ ! -w "$GITPATH" ]; then
80 | printf "%b\n" "${RED}Can't write to $GITPATH${RC}"
81 | exit 1
82 | fi
83 | }
84 |
85 | checkEnv() {
86 | checkPackageManager
87 | }
88 |
--------------------------------------------------------------------------------
/scripts/applications-setup/fastfetch-setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../common-script.sh
4 |
5 | installFastfetch() {
6 | if ! command_exists fastfetch; then
7 | printf "%b\n" "${YELLOW}Installing Fastfetch...${RC}"
8 | brew install fastfetch
9 | if [ $? -ne 0 ]; then
10 | printf "%b\n" "${RED}Failed to install Fastfetch. Please check your Homebrew installation or try again later.${RC}"
11 | exit 1
12 | fi
13 | printf "%b\n" "${GREEN}Fastfetch installed successfully!${RC}"
14 | else
15 | printf "%b\n" "${GREEN}Fastfetch is already installed.${RC}"
16 | fi
17 | }
18 |
19 | setupFastfetchConfig() {
20 | printf "%b\n" "${YELLOW}Copying Fastfetch config files...${RC}"
21 | if [ -d "${HOME}/.config/fastfetch" ] && [ ! -d "${HOME}/.config/fastfetch-bak" ]; then
22 | cp -r "${HOME}/.config/fastfetch" "${HOME}/.config/fastfetch-bak"
23 | fi
24 | mkdir -p "${HOME}/.config/fastfetch/"
25 | curl -sSLo "${HOME}/.config/fastfetch/config.jsonc" https://raw.githubusercontent.com/ChrisTitusTech/mybash/main/config.jsonc
26 | }
27 |
28 | setupFastfetchShell() {
29 | printf "%b\n" "${YELLOW}Configuring shell integration...${RC}"
30 |
31 | current_shell=$(basename "$SHELL")
32 | rc_file=""
33 |
34 | case "$current_shell" in
35 | "bash")
36 | rc_file="$HOME/.bashrc"
37 | ;;
38 | "zsh")
39 | rc_file="$HOME/.zshrc"
40 | ;;
41 | "fish")
42 | rc_file="$HOME/.config/fish/config.fish"
43 | ;;
44 | "nu")
45 | rc_file="$HOME/.config/nushell/config.nu"
46 | ;;
47 | *)
48 | printf "%b\n" "${RED}$current_shell is not supported. Update your shell configuration manually.${RC}"
49 | ;;
50 | esac
51 |
52 | if [ ! -f "$rc_file" ]; then
53 | printf "%b\n" "${RED}Shell config file $rc_file not found${RC}"
54 | else
55 | if grep -q "fastfetch" "$rc_file"; then
56 | printf "%b\n" "${YELLOW}Fastfetch is already configured in $rc_file${RC}"
57 | return 0
58 | else
59 | printf "%b" "${GREEN}Would you like to add fastfetch to $rc_file? [y/N] ${RC}"
60 | read -r response
61 | if [ "$response" = "y" ] || [ "$response" = "Y" ]; then
62 | printf "\n# Run fastfetch on shell initialization\nfastfetch\n" >>"$rc_file"
63 | printf "%b\n" "${GREEN}Added fastfetch to $rc_file${RC}"
64 | else
65 | printf "%b\n" "${YELLOW}Skipped adding fastfetch to shell config${RC}"
66 | fi
67 | fi
68 | fi
69 |
70 | }
71 |
72 | checkEnv
73 | installFastfetch
74 | setupFastfetchConfig
75 | setupFastfetchShell
76 |
--------------------------------------------------------------------------------
/scripts/applications-setup/zsh-setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | . ../common-script.sh
4 |
5 | backupZshConfig() {
6 | printf "%b\n" "${YELLOW}Backing up existing Zsh configuration...${RC}"
7 |
8 | # Backup existing .zshrc if it exists
9 | if [ -f "$HOME/.zshrc" ] && [ ! -f "$HOME/.zshrc-backup" ]; then
10 | cp "$HOME/.zshrc" "$HOME/.zshrc-backup"
11 | printf "%b\n" "${GREEN}Existing .zshrc backed up to .zshrc-backup.${RC}"
12 | fi
13 |
14 | # Backup existing .config/zsh if it exists
15 | if [ -d "$HOME/.config/zsh" ] && [ ! -d "$HOME/.config/zsh-backup" ]; then
16 | cp -r "$HOME/.config/zsh" "$HOME/.config/zsh-backup"
17 | printf "%b\n" "${GREEN}Existing Zsh config backed up to .config/zsh-backup.${RC}"
18 | fi
19 | }
20 |
21 | installZshDepend() {
22 | # List of dependencies
23 | DEPENDENCIES="zsh-autocomplete bat tree multitail fastfetch wget unzip fontconfig starship fzf zoxide"
24 |
25 | printf "%b\n" "${CYAN}Installing dependencies...${RC}"
26 | for package in $DEPENDENCIES; do
27 | if brew list "$package" >/dev/null 2>&1; then
28 | printf "%b\n" "${GREEN}$package is already installed, skipping...${RC}"
29 | else
30 | printf "%b\n" "${CYAN}Installing $package...${RC}"
31 | if ! brew install "$package"; then
32 | printf "%b\n" "${RED}Failed to install $package. Please check your brew installation.${RC}"
33 | exit 1
34 | fi
35 | fi
36 | done
37 |
38 | # List of cask dependencies
39 | CASK_DEPENDENCIES="kitty ghostty font-fira-code-nerd-font"
40 |
41 | printf "%b\n" "${CYAN}Installing cask dependencies...${RC}"
42 | for cask in $CASK_DEPENDENCIES; do
43 | if brew list --cask "$cask" >/dev/null 2>&1; then
44 | printf "%b\n" "${GREEN}$cask is already installed, skipping...${RC}"
45 | else
46 | printf "%b\n" "${CYAN}Installing $cask...${RC}"
47 | if ! brew install --cask "$cask"; then
48 | printf "%b\n" "${RED}Failed to install $cask. Please check your brew installation.${RC}"
49 | exit 1
50 | fi
51 | fi
52 | done
53 |
54 | # Complete fzf installation
55 | if [ -e ~/.fzf/install ]; then
56 | ~/.fzf/install --all
57 | fi
58 | }
59 |
60 |
61 | # Function to setup zsh configuration
62 | setupZshConfig() {
63 | printf "%b\n" "${YELLOW}Setting up Zsh configuration...${RC}"
64 |
65 | wget https://raw.githubusercontent.com/ChrisTitusTech/maczsh/refs/heads/main/.zshrc -O "$HOME/.zshrc"
66 |
67 | # Ensure .zshrc is sourced
68 | if [ ! -f "$HOME/.zshrc" ]; then
69 | printf "%b\n" "${RED}Zsh configuration file not found!${RC}"
70 | exit 1
71 | fi
72 |
73 | printf "%b\n" "${GREEN}Zsh configuration has been set up successfully. Restart Shell.${RC}"
74 | }
75 |
76 | checkEnv
77 | backupZshConfig
78 | installZshDepend
79 | setupZshConfig
--------------------------------------------------------------------------------
/MacUtilGUI/README.md:
--------------------------------------------------------------------------------
1 | # MacUtil GUI
2 |
3 | A cross-platform GUI application built with F# and Avalonia UI to run the shell scripts from the MacUtil project.
4 |
5 | ## Features
6 |
7 | - **Script Browser**: Browse and categorize scripts from the `scripts/` directory
8 | - **Script Details**: View detailed information about each script including description, category, and file path
9 | - **One-Click Execution**: Run scripts with a single button click
10 | - **Cross-Platform**: Works on macOS, Windows, and Linux
11 | - **Modern UI**: Built with Avalonia UI using the Fluent theme
12 |
13 | ## Project Structure
14 |
15 | ```
16 | MacUtilGUI/
17 | ├── MacUtilGUI.fsproj # Project file
18 | ├── App.axaml # Application XAML
19 | ├── Program.fs # Application entry point
20 | ├── Models/
21 | │ ├── ScriptInfo.fs # Script information model
22 | │ └── ScriptCategory.fs # Script category model
23 | ├── Services/
24 | │ └── ScriptService.fs # Script loading and execution service
25 | ├── ViewModels/
26 | │ ├── ViewModelBase.fs # Base view model class
27 | │ └── MainWindowViewModel.fs # Main window view model
28 | └── Views/
29 | ├── MainWindow.axaml # Main window XAML
30 | └── MainWindow.axaml.fs # Main window code-behind
31 | ```
32 |
33 | ## Prerequisites
34 |
35 | - .NET 9.0 SDK or later
36 | - F# support
37 |
38 | ## Building and Running
39 |
40 | 1. Navigate to the MacUtilGUI directory:
41 | ```bash
42 | cd MacUtilGUI
43 | ```
44 |
45 | 2. Restore dependencies:
46 | ```bash
47 | dotnet restore
48 | ```
49 |
50 | 3. Build the project:
51 | ```bash
52 | dotnet build
53 | ```
54 |
55 | 4. Run the application:
56 | ```bash
57 | dotnet run
58 | ```
59 |
60 | ## How It Works
61 |
62 | 1. **Script Discovery**: The application scans the `../scripts/` directory for TOML configuration files and shell scripts
63 | 2. **TOML Parsing**: Uses the `tab_data.toml` files to organize scripts into categories with descriptions
64 | 3. **Dynamic Loading**: Scripts are organized by category and loaded dynamically from the file system
65 | 4. **Execution**: When a script is selected and "Run Script" is clicked, it executes the shell script in a new terminal process
66 |
67 | ## Configuration
68 |
69 | The application reads configuration from:
70 | - `scripts/tabs.toml` - Main configuration listing script directories
71 | - `scripts/*/tab_data.toml` - Category-specific script configurations
72 |
73 | ## Dependencies
74 |
75 | - **Avalonia**: Cross-platform .NET UI framework
76 | - **Tomlyn**: TOML parsing library for .NET
77 |
78 | ## Extending
79 |
80 | To add new script categories:
81 | 1. Create a new directory under `scripts/`
82 | 2. Add the directory name to `scripts/tabs.toml`
83 | 3. Create a `tab_data.toml` file in the new directory with script definitions
84 | 4. The GUI will automatically discover and display the new scripts
85 |
86 | ## Troubleshooting
87 |
88 | - Ensure the `scripts/` directory is one level up from the executable location
89 | - Check that shell scripts have execute permissions (`chmod +x script.sh`)
90 | - Verify TOML files are properly formatted
91 |
--------------------------------------------------------------------------------
/scripts/system-setup/remove-animations.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../common-script.sh
4 |
5 | removeAnimations() {
6 | printf "%b\n" "${YELLOW}Reducing motion and animations on macOS...${RC}"
7 |
8 | # Reduce motion in Accessibility settings (most effective)
9 | printf "%b\n" "${CYAN}Setting reduce motion preference...${RC}"
10 | $ESCALATION_TOOL defaults write com.apple.universalaccess reduceMotion -bool true
11 |
12 | # Disable window animations
13 | printf "%b\n" "${CYAN}Disabling window animations...${RC}"
14 | $ESCALATION_TOOL defaults write NSGlobalDomain NSAutomaticWindowAnimationsEnabled -bool false
15 |
16 | # Speed up window resize animations
17 | printf "%b\n" "${CYAN}Speeding up window resize animations...${RC}"
18 | $ESCALATION_TOOL defaults write NSGlobalDomain NSWindowResizeTime -float 0.001
19 |
20 | # Disable smooth scrolling
21 | printf "%b\n" "${CYAN}Disabling smooth scrolling...${RC}"
22 | $ESCALATION_TOOL defaults write NSGlobalDomain NSScrollAnimationEnabled -bool false
23 |
24 | # Disable animation when opening and closing windows
25 | printf "%b\n" "${CYAN}Disabling window open/close animations...${RC}"
26 | $ESCALATION_TOOL defaults write NSGlobalDomain NSAutomaticWindowAnimationsEnabled -bool false
27 |
28 | # Disable animation when opening a Quick Look window
29 | printf "%b\n" "${CYAN}Disabling Quick Look animations...${RC}"
30 | $ESCALATION_TOOL defaults write -g QLPanelAnimationDuration -float 0
31 |
32 | # Disable animation when opening the Info window in Finder
33 | printf "%b\n" "${CYAN}Disabling Finder Info window animations...${RC}"
34 | $ESCALATION_TOOL defaults write com.apple.finder DisableAllAnimations -bool true
35 |
36 | # Speed up Mission Control animations
37 | printf "%b\n" "${CYAN}Speeding up Mission Control animations...${RC}"
38 | $ESCALATION_TOOL defaults write com.apple.dock expose-animation-duration -float 0.1
39 | $ESCALATION_TOOL defaults write com.apple.dock expose-group-apps -bool true
40 |
41 | # Speed up Launchpad animations
42 | printf "%b\n" "${CYAN}Speeding up Launchpad animations...${RC}"
43 | $ESCALATION_TOOL defaults write com.apple.dock springboard-show-duration -float 0.1
44 | $ESCALATION_TOOL defaults write com.apple.dock springboard-hide-duration -float 0.1
45 |
46 | # Disable dock hiding animation
47 | printf "%b\n" "${CYAN}Disabling dock hiding animations...${RC}"
48 | $ESCALATION_TOOL defaults write com.apple.dock autohide-time-modifier -float 0
49 | $ESCALATION_TOOL defaults write com.apple.dock autohide-delay -float 0
50 |
51 | # Disable animations in Mail.app
52 | printf "%b\n" "${CYAN}Disabling Mail animations...${RC}"
53 | $ESCALATION_TOOL defaults write com.apple.mail DisableReplyAnimations -bool true
54 | $ESCALATION_TOOL defaults write com.apple.mail DisableSendAnimations -bool true
55 |
56 | # Disable zoom animation when focusing on text input fields
57 | printf "%b\n" "${CYAN}Disabling text field zoom animations...${RC}"
58 | $ESCALATION_TOOL defaults write NSGlobalDomain NSTextShowsControlCharacters -bool true
59 |
60 | printf "%b\n" "${GREEN}Motion and animations have been reduced.${RC}"
61 | $ESCALATION_TOOL killall Dock
62 | printf "%b\n" "${YELLOW}Dock Restarted.${RC}"
63 | }
64 |
65 | checkEnv
66 | removeAnimations
--------------------------------------------------------------------------------
/MacUtilGUI/MacUtilGUI.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | net9.0
6 | disable
7 | true
8 | app.manifest
9 | true
10 |
11 |
12 | true
13 | true
14 | true
15 | link
16 | true
17 | true
18 | true
19 | true
20 | embedded
21 |
22 |
23 | true
24 | osx-x64;osx-arm64
25 |
26 |
27 | MacUtil
28 | MacUtil GUI
29 | com.macutil.gui
30 | 0.2.1
31 | 0.2.1
32 | APPL
33 | ????
34 | MacUtilGUI
35 | MacUtilGUI.icns
36 | NSApplication
37 | true
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/.github/workflows/Release.yml:
--------------------------------------------------------------------------------
1 | name: macutil Release
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | permissions:
7 | contents: write
8 | packages: write
9 |
10 | jobs:
11 | macutil_build:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v5
16 |
17 | - name: Setup .NET
18 | uses: actions/setup-dotnet@v4
19 | with:
20 | dotnet-version: '9.0.x'
21 |
22 | - name: Cache .NET packages
23 | uses: actions/cache@v4
24 | with:
25 | path: ~/.nuget/packages
26 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
27 | restore-keys: ${{ runner.os }}-nuget-
28 |
29 | - name: Restore dependencies
30 | run: dotnet restore MacUtilGUI/MacUtilGUI.fsproj
31 |
32 | - name: Build and publish macOS x64
33 | run: |
34 | dotnet publish MacUtilGUI/MacUtilGUI.fsproj \
35 | --configuration Release \
36 | --runtime osx-x64 \
37 | --self-contained true \
38 | --output ./build/osx-x64 \
39 | /p:PublishSingleFile=true \
40 | /p:IncludeNativeLibrariesForSelfExtract=true
41 |
42 | - name: Build and publish macOS ARM64
43 | run: |
44 | dotnet publish MacUtilGUI/MacUtilGUI.fsproj \
45 | --configuration Release \
46 | --runtime osx-arm64 \
47 | --self-contained true \
48 | --output ./build/osx-arm64 \
49 | /p:PublishSingleFile=true \
50 | /p:IncludeNativeLibrariesForSelfExtract=true
51 |
52 | - name: Rename binaries
53 | run: |
54 | mv ./build/osx-x64/MacUtilGUI ./build/osx-x64/macutil-macos-x64
55 | mv ./build/osx-arm64/MacUtilGUI ./build/osx-arm64/macutil-macos-arm64
56 |
57 | - name: Extract Version
58 | id: extract_version
59 | run: |
60 | # Fetch all tags
61 | git fetch --tags
62 |
63 | # Get the latest tag, default to 0.0.0 if no tags exist
64 | latest_tag=$(git tag --sort=-version:refname | head -n1)
65 | if [ -z "$latest_tag" ]; then
66 | latest_tag="0.0.0"
67 | fi
68 |
69 | # Parse the latest tag (assuming format X.Y.Z)
70 | IFS='.' read -ra VERSION_PARTS <<< "$latest_tag"
71 | major=${VERSION_PARTS[0]:-0}
72 | minor=${VERSION_PARTS[1]:-0}
73 | patch=${VERSION_PARTS[2]:-0}
74 |
75 | # Increment minor version by 0.1 (increment minor, reset patch to 0)
76 | new_minor=$((minor + 1))
77 | version="${major}.${new_minor}.0"
78 |
79 | echo "Previous version: $latest_tag"
80 | echo "New version: $version"
81 | echo "version=$version" >> $GITHUB_ENV
82 | shell: bash
83 |
84 | - name: Create and Upload Release
85 | id: create_release
86 | uses: softprops/action-gh-release@v2
87 | with:
88 | tag_name: ${{ env.version }}
89 | name: Pre-Release ${{ env.version }}
90 | body: |
91 | 
92 | 
93 |
94 | append_body: true
95 | generate_release_notes: true
96 | files: |
97 | ./build/osx-x64/macutil-macos-x64
98 | ./build/osx-arm64/macutil-macos-arm64
99 | prerelease: true
100 | env:
101 | version: ${{ env.version }}
102 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
103 |
--------------------------------------------------------------------------------
/scripts/system-setup/fix-finder.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | . ../common-script.sh
4 |
5 | fixfinder () {
6 | printf "%b\n" "${YELLOW}Applying global theme settings for Finder...${RC}"
7 |
8 | # Set the default Finder view to list view
9 | printf "%b\n" "${CYAN}Setting default Finder view to list view...${RC}"
10 | $ESCALATION_TOOL defaults write com.apple.finder FXPreferredViewStyle -string "Nlsv"
11 |
12 | # Configure list view settings for all folders
13 | printf "%b\n" "${CYAN}Configuring list view settings for all folders...${RC}"
14 | # Set default list view settings for new folders
15 | $ESCALATION_TOOL defaults write com.apple.finder FK_StandardViewSettings -dict-add ListViewSettings '{ "columns" = ( { "ascending" = 1; "identifier" = "name"; "visible" = 1; "width" = 300; }, { "ascending" = 0; "identifier" = "dateModified"; "visible" = 1; "width" = 181; }, { "ascending" = 0; "identifier" = "size"; "visible" = 1; "width" = 97; } ); "iconSize" = 16; "showIconPreview" = 0; "sortColumn" = "name"; "textSize" = 12; "useRelativeDates" = 1; }'
16 |
17 | # Clear existing folder view settings to force use of default settings
18 | printf "%b\n" "${CYAN}Clearing existing folder view settings...${RC}"
19 | $ESCALATION_TOOL defaults delete com.apple.finder FXInfoPanesExpanded 2>/dev/null || true
20 | $ESCALATION_TOOL defaults delete com.apple.finder FXDesktopVolumePositions 2>/dev/null || true
21 |
22 | # Set list view for all view types
23 | printf "%b\n" "${CYAN}Setting list view for all folder types...${RC}"
24 | $ESCALATION_TOOL defaults write com.apple.finder FK_StandardViewSettings -dict-add ExtendedListViewSettings '{ "columns" = ( { "ascending" = 1; "identifier" = "name"; "visible" = 1; "width" = 300; }, { "ascending" = 0; "identifier" = "dateModified"; "visible" = 1; "width" = 181; }, { "ascending" = 0; "identifier" = "size"; "visible" = 1; "width" = 97; } ); "iconSize" = 16; "showIconPreview" = 0; "sortColumn" = "name"; "textSize" = 12; "useRelativeDates" = 1; }'
25 |
26 | # Sets default search scope to the current folder
27 | printf "%b\n" "${CYAN}Setting default search scope to the current folder...${RC}"
28 | $ESCALATION_TOOL defaults write com.apple.finder FXDefaultSearchScope -string "SCcf"
29 |
30 | # Remove trash items older than 30 days
31 | printf "%b\n" "${CYAN}Removing trash items older than 30 days...${RC}"
32 | $ESCALATION_TOOL defaults write com.apple.finder "FXRemoveOldTrashItems" -bool "true"
33 |
34 | # Remove .DS_Store files to reset folder view settings
35 | printf "%b\n" "${CYAN}Removing .DS_Store files to reset folder view settings...${RC}"
36 | find ~ -name ".DS_Store" -type f -delete 2>/dev/null || true
37 |
38 | # Show all filename extensions
39 | printf "%b\n" "${CYAN}Showing all filename extensions in Finder...${RC}"
40 | $ESCALATION_TOOL defaults write NSGlobalDomain AppleShowAllExtensions -bool true
41 |
42 | # Set the sidebar icon size to small
43 | printf "%b\n" "${CYAN}Setting sidebar icon size to small...${RC}"
44 | $ESCALATION_TOOL defaults write NSGlobalDomain NSTableViewDefaultSizeMode -int 1
45 |
46 | # Show status bar in Finder
47 | printf "%b\n" "${CYAN}Showing status bar in Finder...${RC}"
48 | $ESCALATION_TOOL defaults write com.apple.finder ShowStatusBar -bool true
49 |
50 | # Show path bar in Finder
51 | printf "%b\n" "${CYAN}Showing path bar in Finder...${RC}"
52 | $ESCALATION_TOOL defaults write com.apple.finder ShowPathbar -bool true
53 |
54 | # Clean up Finder's sidebar
55 | printf "%b\n" "${CYAN}Cleaning up Finder's sidebar...${RC}"
56 | $ESCALATION_TOOL defaults write com.apple.finder SidebarDevicesSectionDisclosedState -bool true
57 | $ESCALATION_TOOL defaults write com.apple.finder SidebarPlacesSectionDisclosedState -bool true
58 | $ESCALATION_TOOL defaults write com.apple.finder SidebarShowingiCloudDesktop -bool false
59 |
60 | # Restart Finder to apply changes
61 | printf "%b\n" "${GREEN}Finder has been restarted and settings have been applied.${RC}"
62 | $ESCALATION_TOOL killall Finder
63 | }
64 |
65 | checkEnv
66 | fixfinder
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines for macutil
2 |
3 | Thank you for considering contributing to macutil! We appreciate your effort in helping improve this project. To ensure that your contributions align with the goals and quality standards of macutil, please follow these guidelines:
4 |
5 | ## 1. **Install .NET**:
6 |
7 | Make sure you have .NET installed on your machine. You can install it by following the instructions at [dotnet.microsoft.com](https://dotnet.microsoft.com/en-us/download).
8 |
9 | ## 2. **Fork and Clone the repo**
10 |
11 | 1. Make a fork of the repo in GitHub
12 | 2. Clone the fork
13 | ```bash
14 | git clone https://github.com/YOUR_USERNAME_HERE/macutil.git
15 | cd macutil
16 | ```
17 |
18 | ## 3. Make your changes
19 |
20 | - **Edit the files you want to change**: Make your changes to the relevant files.
21 | - **Test your changes**: Run `dotnet run` to test your modifications in a local environment and ensure everything works as expected.
22 |
23 | ## 4. Understand the existing code
24 |
25 | - **Have a clear reason**: Don’t change the way things are done without a valid reason. If you propose an alteration, be prepared to explain why it’s necessary and how it improves the project.
26 | - **Respect existing conventions**: Changes should align with the existing code style, design patterns, and overall project philosophy. If you want to introduce a new way of doing things, justify it clearly.
27 |
28 | ## 5. Learn from Past Pull Requests (PRs)
29 |
30 | - **Check merged PRs**: Reviewing merged pull requests can give you an idea of what kind of contributions are accepted and how they are implemented.
31 | - **Study rejected PRs**: This is especially important as it helps you avoid making similar mistakes or proposing changes that have already been considered and declined.
32 |
33 | ## 6. Write Clean, Descriptive Commit Messages
34 |
35 | - **Be descriptive**: Your commit messages should clearly describe what the change does and why it was made.
36 | - **Use the imperative mood**: For example, "Add feature X" or "Fix bug in Y", rather than "Added feature X" or "Fixed bug in Y".
37 | - **Keep commits clean**: Avoid committing a change and then immediately following it with a fix for that change. Instead, amend your commit or squash it if needed.
38 |
39 | ## 7. Keep Your Pull Requests (PRs) Small and Focused
40 |
41 | - **Make small, targeted PRs**: Focus on one feature or fix per pull request. This makes it easier to review and increases the likelihood of acceptance.
42 | - **Avoid combining unrelated changes**: PRs that tackle multiple unrelated issues are harder to review and might be rejected because of a single problem.
43 |
44 | ## 8. Understand and Test the Code You Write
45 |
46 | - **Review your code**: Before submitting your changes, take the time to review your code for readability, efficiency and performance. Consider how your changes affect the project.
47 | - **Avoid using LLMs**: Don't submit AI-generated code without reviewing and testing it first. Ensure that any code you submit is thoroughly understood and meets the project's standards.
48 | - **Testing Requirements**: Failure to conduct testing after multiple requests may result in the closure of your Pull Request.
49 |
50 | ## 9. Code Review and Feedback
51 |
52 | - **Expect feedback**: PRs will undergo code review. Be open to feedback and willing to make adjustments as needed.
53 | - **Participate in reviews**: If you feel comfortable, review other contributors' PRs as well. Peer review is a great way to learn and ensure high-quality contributions.
54 |
55 | ## 10. Contributing Is More Than Just Code
56 |
57 | - **Test the tool**: Running tests and providing feedback on how the tool works in different environments is a valuable contribution.
58 | - **Write well-formed issues**: Clearly describe bugs or problems you encounter, providing as much detail as possible, including steps to reproduce the issue.
59 | - **Propose reasonable feature requests**: When suggesting new features, ensure they fit within the scope, style, and design of the project. Provide clear reasoning and use cases.
60 |
61 | ## 11. Documentation
62 |
63 | - **Update the documentation**: If your change affects the functionality, please update the relevant documentation files to reflect this.
64 |
65 | ## 12. License
66 |
67 | - **Agree to the license**: By contributing to macutil, you agree that your contributions will be licensed under the project's MIT license.
68 |
69 | We appreciate your contributions and look forward to collaborating with you to make macutil better!
70 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-reports.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Report a bug or issue to help us improve.
3 | title: "[Bug]: "
4 | labels: ["bug"]
5 | assignees: []
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: |
10 | Thank you for helping us improve! Please provide as much detail as possible to ensure we can address the issue effectively.
11 | - type: dropdown
12 | id: distribution
13 | attributes:
14 | label: Distribution
15 | multiple: false
16 | description: Select the Linux distribution you are using.
17 | options:
18 | - Arch
19 | - Ubuntu
20 | - Fedora
21 | - Debian
22 | - openSUSE
23 | - Other
24 | validations:
25 | required: true
26 | - type: input
27 | id: specify-distribution
28 | attributes:
29 | label: Specify Distribution (if "Other" selected)
30 | description: Enter the name of your Linux distribution.
31 | placeholder: e.g., Manjaro, Pop!_OS
32 | - type: dropdown
33 | id: de-wm
34 | attributes:
35 | label: Desktop Environment / Window Manager
36 | multiple: false
37 | description: Select your desktop environment or window manager.
38 | options:
39 | - GNOME
40 | - KDE Plasma
41 | - XFCE
42 | - Hyprland
43 | - i3
44 | - Other
45 | validations:
46 | required: true
47 | - type: input
48 | id: specify-de-wm
49 | attributes:
50 | label: Specify Desktop Environment/Window Manager (if "Other" selected)
51 | description: Enter the name of your desktop environment or window manager.
52 | placeholder: e.g., LXQt, Openbox
53 | - type: dropdown
54 | id: windowing-system
55 | attributes:
56 | label: Windowing System
57 | multiple: false
58 | description: Specify whether you are using X11 or Wayland.
59 | options:
60 | - X11
61 | - Wayland
62 | validations:
63 | required: true
64 | - type: input
65 | id: macutil-version
66 | attributes:
67 | label: macutil Version
68 | description: macutil version (found above the list within macutil).
69 | validations:
70 | required: true
71 | - type: dropdown
72 | id: branch
73 | attributes:
74 | label: Branch
75 | multiple: false
76 | description: Specify the branch of the project you are using.
77 | options:
78 | - main
79 | - prerelease
80 | - stable
81 | - other
82 | validations:
83 | required: true
84 | - type: input
85 | id: specify-branch
86 | attributes:
87 | label: Specify Branch (if "Other" selected)
88 | description: Enter the branch name.
89 | placeholder: e.g., feature/new-feature
90 | - type: textarea
91 | id: describe-bug
92 | attributes:
93 | label: Describe the bug
94 | description: |
95 | Provide a clear and concise description of what the bug is.
96 | placeholder: Describe the issue in detail.
97 | validations:
98 | required: true
99 | - type: textarea
100 | id: reproduce-steps
101 | attributes:
102 | label: Steps to reproduce
103 | description: Steps to reproduce the behavior.
104 | placeholder: |
105 | 1. Go to '...'
106 | 2. Click on '...'
107 | 3. Scroll down to '...'
108 | 4. See error
109 | validations:
110 | required: true
111 | - type: textarea
112 | id: expected-behavior
113 | attributes:
114 | label: Expected behavior
115 | description: |
116 | A clear and concise description of what you expected to happen.
117 | placeholder: Explain the expected outcome.
118 | validations:
119 | required: true
120 | - type: textarea
121 | id: additional-context
122 | attributes:
123 | label: Additional context
124 | description: |
125 | Add any other context or information about the problem here.
126 | placeholder: Include any related logs, error messages, or configurations.
127 | - type: textarea
128 | id: screenshots
129 | attributes:
130 | label: Screenshots
131 | description: |
132 | If applicable, add screenshots to help explain your problem. Provide links or attach images in the comments after submitting the issue.
133 | - type: checkboxes
134 | id: checklist
135 | attributes:
136 | label: Checklist
137 | options:
138 | - label: I checked for duplicate issues.
139 | - label: I checked existing discussions.
140 | - label: This issue is not included in the roadmap.
141 | - label: This issue is present on both stable and development branches.
142 | required: true
143 |
144 |
--------------------------------------------------------------------------------
/MacUtilGUI/build_universal.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Quick Universal App Builder for MacUtil GUI
4 | # This script creates only the universal app bundle without individual architecture builds
5 |
6 | echo "🚀 Building Universal MacUtil GUI App..."
7 | echo
8 |
9 | # Configuration
10 | APP_NAME="MacUtilGUI"
11 | BUNDLE_NAME="MacUtil"
12 | BUNDLE_IDENTIFIER="com.macutil.gui"
13 | VERSION="0.2.0"
14 | COPYRIGHT="Copyright © 2025 CT Tech Group LLC. All rights reserved."
15 |
16 | # Colors for output
17 | RED='\033[0;31m'
18 | GREEN='\033[0;32m'
19 | BLUE='\033[0;34m'
20 | YELLOW='\033[1;33m'
21 | NC='\033[0m' # No Color
22 |
23 | print_status() {
24 | echo -e "${BLUE}ℹ️ $1${NC}"
25 | }
26 |
27 | print_success() {
28 | echo -e "${GREEN}✅ $1${NC}"
29 | }
30 |
31 | print_error() {
32 | echo -e "${RED}❌ $1${NC}"
33 | }
34 |
35 | # Clean previous builds
36 | print_status "Cleaning previous builds..."
37 | dotnet clean -c Release > /dev/null 2>&1
38 | rm -rf ./bin/Release/net9.0/publish/
39 | rm -rf ./dist/
40 | mkdir -p ./dist/
41 |
42 | # Restore packages
43 | print_status "Restoring NuGet packages..."
44 | dotnet restore > /dev/null
45 |
46 | # Build for Intel x64
47 | print_status "Building Intel x64 binary..."
48 | dotnet publish -c Release -r osx-x64 -p:UseAppHost=true --self-contained true -o ./bin/Release/net9.0/publish/osx-x64/ > /dev/null 2>&1
49 | if [ $? -ne 0 ]; then
50 | print_error "Intel x64 build failed"
51 | exit 1
52 | fi
53 |
54 | # Build for Apple Silicon ARM64
55 | print_status "Building Apple Silicon ARM64 binary..."
56 | dotnet publish -c Release -r osx-arm64 -p:UseAppHost=true --self-contained true -o ./bin/Release/net9.0/publish/osx-arm64/ > /dev/null 2>&1
57 | if [ $? -ne 0 ]; then
58 | print_error "Apple Silicon ARM64 build failed"
59 | exit 1
60 | fi
61 |
62 | # Create Universal App Bundle
63 | print_status "Creating Universal App Bundle..."
64 | universal_app="./dist/${BUNDLE_NAME}-Universal.app"
65 | mkdir -p "$universal_app/Contents/MacOS"
66 | mkdir -p "$universal_app/Contents/Resources"
67 |
68 | # Create universal binary using lipo
69 | print_status "Merging binaries with lipo..."
70 | lipo -create \
71 | "./bin/Release/net9.0/publish/osx-x64/$APP_NAME" \
72 | "./bin/Release/net9.0/publish/osx-arm64/$APP_NAME" \
73 | -output "$universal_app/Contents/MacOS/$APP_NAME" 2>/dev/null
74 |
75 | if [ $? -ne 0 ]; then
76 | print_error "Failed to create universal binary with lipo"
77 | exit 1
78 | fi
79 |
80 | # Copy dependencies
81 | rsync -a --exclude="$APP_NAME" "./bin/Release/net9.0/publish/osx-x64/" "$universal_app/Contents/MacOS/" 2>/dev/null
82 |
83 | # Create Info.plist
84 | cat > "$universal_app/Contents/Info.plist" << EOF
85 |
86 |
87 |
88 |
89 | CFBundleIconFile
90 | MacUtilGUI.icns
91 | CFBundleIdentifier
92 | $BUNDLE_IDENTIFIER
93 | CFBundleName
94 | $BUNDLE_NAME
95 | CFBundleDisplayName
96 | MacUtil GUI
97 | CFBundleVersion
98 | $VERSION
99 | CFBundleShortVersionString
100 | $VERSION
101 | LSMinimumSystemVersion
102 | 11.0
103 | CFBundleExecutable
104 | $APP_NAME
105 | CFBundleInfoDictionaryVersion
106 | 6.0
107 | CFBundlePackageType
108 | APPL
109 | CFBundleSignature
110 | MCUT
111 | NSHighResolutionCapable
112 |
113 | NSPrincipalClass
114 | NSApplication
115 | LSApplicationCategoryType
116 | public.app-category.utilities
117 | NSRequiresAquaSystemAppearance
118 |
119 | NSHumanReadableCopyright
120 | $COPYRIGHT
121 |
122 |
123 | EOF
124 |
125 | # Copy icon
126 | if [ -f "MacUtilGUI.icns" ]; then
127 | cp "MacUtilGUI.icns" "$universal_app/Contents/Resources/"
128 | fi
129 |
130 | # Make executable
131 | chmod +x "$universal_app/Contents/MacOS/$APP_NAME"
132 |
133 | # Show results
134 | universal_size=$(du -sh "$universal_app" | cut -f1)
135 | print_success "Universal app created: $universal_app"
136 | print_status "Bundle size: $universal_size"
137 |
138 | # Verify architecture
139 | print_status "Verifying universal binary:"
140 | lipo -info "$universal_app/Contents/MacOS/$APP_NAME"
141 |
142 | echo
143 | print_success "🎉 Universal build complete!"
144 | print_status "📱 Your app will run on both Intel and Apple Silicon Macs"
145 |
--------------------------------------------------------------------------------
/MacUtilGUI/TTY_FIXES.md:
--------------------------------------------------------------------------------
1 | # TTY/Non-Interactive Execution Fixes for macOS App Bundle
2 |
3 | ## Problem
4 | When running the MacUtil GUI as a `.app` bundle in macOS, scripts were failing because:
5 |
6 | 1. **No TTY Available**: App bundles don't run in a terminal, so `stdin` is not a TTY
7 | 2. **Interactive Prompts**: Scripts expecting user input would hang or fail
8 | 3. **Homebrew Installation Issues**: Homebrew installer would try to reinstall even when already present
9 | 4. **TTY Detection Failures**: Scripts checking `tty -s` or `[ -t 0 ]` would fail
10 |
11 | ## Solution Implemented
12 |
13 | ### 1. Environment Variables
14 | Added comprehensive environment variable setup for all script execution paths:
15 |
16 | ```bash
17 | export TERM=xterm-256color
18 | export DEBIAN_FRONTEND=noninteractive
19 | export CI=true
20 | export HOMEBREW_NO_ENV_HINTS=1
21 | export HOMEBREW_NO_INSTALL_CLEANUP=1
22 | export HOMEBREW_NO_AUTO_UPDATE=1
23 | export NONINTERACTIVE=1
24 | export FORCE_NONINTERACTIVE=1
25 | ```
26 |
27 | ### 2. Script Preprocessing Function
28 | Created `preprocessScriptForNonTTY()` that:
29 |
30 | - **Disables TTY checks**: Comments out lines containing `tty -s`, `[ -t 0 ]`, etc.
31 | - **Forces non-interactive mode**: Adds `--force` flag to brew commands
32 | - **Adds safety headers**: Includes environment setup and TTY function override
33 | - **Handles package managers**: Adds `-y` flags to apt commands where needed
34 |
35 | ### 3. TTY Function Override
36 | Added a fake `tty()` function that always returns "not a TTY":
37 |
38 | ```bash
39 | function tty() {
40 | return 1 # Always return "not a TTY"
41 | }
42 | ```
43 |
44 | ### 4. ProcessStartInfo Environment Setup
45 | Updated all three script execution paths in `ScriptService.fs`:
46 | - Regular script execution
47 | - Elevated (sudo) script execution via osascript
48 | - Async script execution
49 |
50 | ## Files Modified
51 |
52 | ### `/Services/ScriptService.fs`
53 | - Added `preprocessScriptForNonTTY()` function
54 | - Updated all `ProcessStartInfo` instances with environment variables
55 | - Updated osascript command to include environment variable exports
56 | - Applied preprocessing to all script execution paths
57 |
58 | ## Testing the Fixes
59 |
60 | ### Before the Fix
61 | ```
62 | ERROR: stdin is not a TTY
63 | ERROR: Checking for sudo access...
64 | ERROR: Homebrew installation attempted when already installed
65 | ```
66 |
67 | ### After the Fix
68 | Scripts should now run smoothly in the `.app` bundle environment without TTY-related errors.
69 |
70 | ## What Each Environment Variable Does
71 |
72 | | Variable | Purpose |
73 | |----------|---------|
74 | | `TERM=xterm-256color` | Provides a valid terminal type |
75 | | `DEBIAN_FRONTEND=noninteractive` | Prevents apt from prompting for input |
76 | | `CI=true` | Signals automated environment to many tools |
77 | | `HOMEBREW_NO_ENV_HINTS=1` | Disables Homebrew environment hints |
78 | | `HOMEBREW_NO_INSTALL_CLEANUP=1` | Skips automatic cleanup |
79 | | `HOMEBREW_NO_AUTO_UPDATE=1` | Prevents automatic updates |
80 | | `NONINTERACTIVE=1` | Custom flag for scripts to detect non-interactive mode |
81 | | `FORCE_NONINTERACTIVE=1` | Additional flag for scripts |
82 |
83 | ## Script Preprocessing Examples
84 |
85 | ### TTY Check Disabling
86 | **Before:**
87 | ```bash
88 | if tty -s; then
89 | echo "Running in terminal"
90 | fi
91 | ```
92 |
93 | **After:**
94 | ```bash
95 | # TTY check disabled for .app bundle execution: if tty -s; then
96 | # echo "Running in terminal"
97 | # fi
98 | ```
99 |
100 | ### Brew Command Enhancement
101 | **Before:**
102 | ```bash
103 | brew install package
104 | ```
105 |
106 | **After:**
107 | ```bash
108 | brew install --quiet package
109 | ```
110 |
111 | **For update/upgrade commands:**
112 | ```bash
113 | brew update --quiet
114 | brew upgrade --quiet
115 | ```
116 |
117 | ## Verification Steps
118 |
119 | 1. **Build new app bundle**: `./deploy_macos.sh`
120 | 2. **Test in .app environment**: `open dist/MacUtil-Intel.app`
121 | 3. **Run scripts that previously failed**: Check for TTY-related errors
122 | 4. **Monitor script output**: Should see environment variables being set
123 |
124 | ## Future Considerations
125 |
126 | - **Add more package manager support**: Handle `dnf`, `yum`, `pacman` etc.
127 | - **Enhanced script detection**: Better detection of interactive prompts
128 | - **Logging improvements**: Add debug output for preprocessing steps
129 | - **Custom script flags**: Allow scripts to opt-out of preprocessing if needed
130 |
131 | ## Benefits
132 |
133 | ✅ **No more TTY errors** when running as .app bundle
134 | ✅ **Homebrew installs work correctly** without re-installation attempts
135 | ✅ **Scripts run non-interactively** without hanging on prompts
136 | ✅ **Backward compatibility** - still works in terminal environments
137 | ✅ **Comprehensive coverage** - all execution paths updated
138 |
--------------------------------------------------------------------------------
/.github/workflows/issue-slash-commands.yaml:
--------------------------------------------------------------------------------
1 | name: Issue slash commands
2 |
3 | on:
4 | issue_comment:
5 | types: [created, edited]
6 |
7 | jobs:
8 | issueCommands:
9 | # Skip this job if the comment was created/edited on a PR
10 | if: ${{ !github.event.issue.pull_request }}
11 | runs-on: ubuntu-latest
12 | permissions:
13 | issues: write
14 | pull-requests: none
15 | contents: read
16 |
17 | steps:
18 | - run: echo "command=false" >> $GITHUB_ENV
19 |
20 | - name: Check for /label command
21 | id: check_label_command
22 | run: |
23 | if [[ "${{ contains(github.event.comment.body, '/label') }}" == "true" ]]; then
24 | echo "command=true" >> $GITHUB_ENV
25 | LABEL_NAME=$(echo "${{ github.event.comment.body }}" | awk -F"/label" '/\/label/ { match($2, /'\''([^'\'']*)'\''/, arr); if (arr[1] != "") print arr[1] }')
26 | echo "label_command=true" >> $GITHUB_ENV
27 | echo "label_name=${LABEL_NAME}" >> $GITHUB_ENV
28 | else
29 | echo "label_command=false" >> $GITHUB_ENV
30 | fi
31 |
32 | - name: Check for /unlabel command
33 | id: check_unlabel_command
34 | run: |
35 | if [[ "${{ contains(github.event.comment.body, '/unlabel') }}" == "true" ]]; then
36 | echo "command=true" >> $GITHUB_ENV
37 | UNLABEL_NAME=$(echo "${{ github.event.comment.body }}" | awk -F"/unlabel" '/\/unlabel/ { match($2, /'\''([^'\'']*)'\''/, arr); if (arr[1] != "") print arr[1] }')
38 | echo "unlabel_command=true" >> $GITHUB_ENV
39 | echo "unlabel_name=${UNLABEL_NAME}" >> $GITHUB_ENV
40 | else
41 | echo "unlabel_command=false" >> $GITHUB_ENV
42 | fi
43 |
44 | - name: Check for /close command
45 | id: check_close_command
46 | run: |
47 | if [[ "${{ contains(github.event.comment.body, '/close') }}" == "true" ]]; then
48 | echo "command=true" >> $GITHUB_ENV
49 | echo "close_command=true" >> $GITHUB_ENV
50 | echo "reopen_command=false" >> $GITHUB_ENV
51 | else
52 | echo "close_command=false" >> $GITHUB_ENV
53 | fi
54 |
55 | - name: Check for /open or /reopen command
56 | id: check_reopen_command
57 | run: |
58 | if [[ "${{ contains(github.event.comment.body, '/open') }}" == "true" ]] || [[ "${{ contains(github.event.comment.body, '/reopen') }}" == "true" ]]; then
59 | echo "command=true" >> $GITHUB_ENV
60 | echo "reopen_command=true" >> $GITHUB_ENV
61 | echo "close_command=false" >> $GITHUB_ENV
62 | else
63 | echo "reopen_command=false" >> $GITHUB_ENV
64 | fi
65 |
66 | - name: Check if the user is allowed
67 | id: check_user
68 | if: env.command == 'true'
69 | run: |
70 | ALLOWED_USERS=("ChrisTitusTech" "afonsofrancof" "Marterich" "MyDrift-user" "Real-MullaC" "nnyyxxxx" "adamperkowski" "lj3954" "jeevithakannan2" "${{ github.event.issue.user.login }}")
71 | if [[ " ${ALLOWED_USERS[@]} " =~ " ${{ github.event.comment.user.login }} " ]]; then
72 | echo "user=true" >> $GITHUB_ENV
73 | else
74 | exit 0
75 | fi
76 |
77 | - name: Close issue
78 | if: env.close_command == 'true'
79 | env:
80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
81 | ISSUE_NUMBER: ${{ github.event.issue.number }}
82 | run: |
83 | echo Closing the issue...
84 | if [[ "${{ contains(github.event.comment.body, 'not planned') }}" == "true" ]]; then
85 | gh issue close $ISSUE_NUMBER --repo ${{ github.repository }} --reason 'not planned'
86 | else
87 | gh issue close $ISSUE_NUMBER --repo ${{ github.repository }}
88 | fi
89 |
90 | - name: Reopen issue
91 | if: env.reopen_command == 'true'
92 | env:
93 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
94 | ISSUE_NUMBER: ${{ github.event.issue.number }}
95 | run: |
96 | echo Reopening the issue...
97 | gh issue reopen $ISSUE_NUMBER --repo ${{ github.repository }}
98 |
99 | - name: Label issue
100 | if: env.label_command == 'true'
101 | env:
102 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
103 | ISSUE_NUMBER: ${{ github.event.issue.number }}
104 | run: |
105 | echo Labeling the issue...
106 | gh issue edit $ISSUE_NUMBER --repo ${{ github.repository }} --add-label "${{ env.label_name }}"
107 |
108 | - name: Remove labels
109 | if: env.unlabel_command == 'true'
110 | env:
111 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
112 | ISSUE_NUMBER: ${{ github.event.issue.number }}
113 | run: |
114 | echo Unlabeling the issue...
115 | gh issue edit $ISSUE_NUMBER --repo ${{ github.repository }} --remove-label "${{ env.unlabel_name }}"
--------------------------------------------------------------------------------
/MacUtilGUI/ViewModels/MainWindowViewModel.fs:
--------------------------------------------------------------------------------
1 | namespace MacUtilGUI.ViewModels
2 |
3 | open System.Collections.ObjectModel
4 | open System.Windows.Input
5 | open System.Threading.Tasks
6 | open Avalonia.Threading
7 | open MacUtilGUI.Models
8 | open MacUtilGUI.Services
9 |
10 | type RelayCommand(canExecute: obj -> bool, execute: obj -> unit) =
11 | let canExecuteChanged = Event()
12 |
13 | interface ICommand with
14 | []
15 | member _.CanExecuteChanged = canExecuteChanged.Publish
16 |
17 | member _.CanExecute(parameter) = canExecute parameter
18 | member _.Execute(parameter) = execute parameter
19 |
20 | new(execute: obj -> unit) = RelayCommand((fun _ -> true), execute)
21 |
22 | type MainWindowViewModel() as this =
23 | inherit ViewModelBase()
24 |
25 | let mutable selectedScript: ScriptInfo option = None
26 | let mutable scriptOutput: string = ""
27 | let mutable isScriptRunning: bool = false
28 | let categories = ObservableCollection()
29 |
30 | let selectScriptCommand =
31 | RelayCommand(fun parameter ->
32 | match parameter with
33 | | :? ScriptInfo as script ->
34 | selectedScript <- Some script
35 | scriptOutput <- "" // Clear previous output
36 | this.OnPropertyChanged("SelectedScript")
37 | this.OnPropertyChanged("ScriptOutput")
38 | this.OnPropertyChanged("CanRunScript")
39 | this.OnPropertyChanged("SelectedScriptName")
40 | this.OnPropertyChanged("SelectedScriptDescription")
41 | this.OnPropertyChanged("SelectedScriptCategory")
42 | this.OnPropertyChanged("SelectedScriptFile")
43 | | _ -> ())
44 |
45 | let runScriptCommand =
46 | RelayCommand(fun _ ->
47 | match selectedScript with
48 | | Some script when not isScriptRunning ->
49 | isScriptRunning <- true
50 | scriptOutput <- "Starting script...\n"
51 | this.OnPropertyChanged("ScriptOutput")
52 | this.OnPropertyChanged("CanRunScript")
53 | this.OnPropertyChanged("IsScriptRunning")
54 |
55 | // Define output and error handlers
56 | let onOutput (line: string) =
57 | Dispatcher.UIThread.InvokeAsync(fun () ->
58 | scriptOutput <- scriptOutput + line + "\n"
59 | this.OnPropertyChanged("ScriptOutput"))
60 | |> ignore
61 |
62 | let onError (line: string) =
63 | Dispatcher.UIThread.InvokeAsync(fun () ->
64 | scriptOutput <- scriptOutput + "[ERROR] " + line + "\n"
65 | this.OnPropertyChanged("ScriptOutput"))
66 | |> ignore
67 |
68 | // Run script asynchronously with real-time output
69 | let scriptTask = ScriptService.runScript script onOutput onError
70 |
71 | scriptTask.ContinueWith(fun (task: Task) ->
72 | Dispatcher.UIThread.InvokeAsync(fun () ->
73 | isScriptRunning <- false
74 |
75 | scriptOutput <-
76 | scriptOutput
77 | + sprintf "\n=== Script completed with exit code: %d ===" task.Result
78 |
79 | this.OnPropertyChanged("ScriptOutput")
80 | this.OnPropertyChanged("CanRunScript")
81 | this.OnPropertyChanged("IsScriptRunning"))
82 | |> ignore)
83 | |> ignore
84 | | _ -> ())
85 |
86 | do
87 | // Load scripts on initialization
88 | let loadedCategories = ScriptService.loadAllScripts ()
89 |
90 | for category in loadedCategories do
91 | categories.Add(category)
92 |
93 | member _.Categories = categories
94 |
95 | member _.SelectedScript = selectedScript
96 |
97 | member _.ScriptOutput = scriptOutput
98 |
99 | member _.SelectedScriptName =
100 | match selectedScript with
101 | | Some script -> script.Name
102 | | None -> ""
103 |
104 | member _.SelectedScriptDescription =
105 | match selectedScript with
106 | | Some script -> script.Description
107 | | None -> ""
108 |
109 | member _.SelectedScriptCategory =
110 | match selectedScript with
111 | | Some script -> script.Category
112 | | None -> ""
113 |
114 | member _.SelectedScriptFile =
115 | match selectedScript with
116 | | Some script -> script.Script
117 | | None -> ""
118 |
119 | member _.CanRunScript = selectedScript.IsSome && not isScriptRunning
120 |
121 | member _.IsScriptRunning = isScriptRunning
122 |
123 | member _.SelectScriptCommand = selectScriptCommand
124 |
125 | member _.RunScriptCommand = runScriptCommand
126 |
127 | member _.Title = "MacUtil GUI - Script Runner"
128 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | contact@christitus.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/MacUtilGUI/DEPLOYMENT.md:
--------------------------------------------------------------------------------
1 | # MacUtil GUI - macOS Deployment Guide
2 |
3 | This guide explains how to build and deploy the MacUtil GUI application for macOS.
4 |
5 | ## 🚀 Quick Start
6 |
7 | ### Building App Bundles
8 |
9 | 1. **Create the app icon** (one-time setup):
10 | ```bash
11 | ./create_icon_from_logo.sh
12 | ```
13 | This converts your `logo.png` into the required `MacUtilGUI.icns` format.
14 |
15 | 2. **Build macOS app bundles**:
16 | ```bash
17 | ./deploy_macos.sh
18 | ```
19 | This creates `.app` bundles for both Intel and Apple Silicon Macs.
20 |
21 | ## 📦 What You Get
22 |
23 | After running the deployment script, you'll find in the `./dist/` directory:
24 |
25 | - `MacUtil-Intel.app` - For Intel-based Macs (x64)
26 | - `MacUtil-AppleSilicon.app` - For Apple Silicon Macs (ARM64)
27 |
28 | Each app bundle is **self-contained** and includes all necessary dependencies.
29 |
30 | ## 🔧 Project Configuration
31 |
32 | Your project has been configured with the following macOS-specific properties:
33 |
34 | ### Bundle Information
35 | - **Bundle Name**: MacUtil
36 | - **Display Name**: MacUtil GUI
37 | - **Bundle Identifier**: com.macutil.gui
38 | - **Version**: 1.0.0
39 | - **Category**: Utilities
40 | - **Minimum macOS Version**: 10.15 (Catalina)
41 |
42 | ### Build Features
43 | - ✅ Self-contained deployment
44 | - ✅ Single-file publishing
45 | - ✅ Trimmed assemblies for smaller size
46 | - ✅ Ready-to-run images for faster startup
47 | - ✅ High-resolution display support
48 | - ✅ Custom app icon
49 |
50 | ## 🎯 Testing Your App
51 |
52 | ### Basic Testing
53 | ```bash
54 | # Test the Intel version
55 | open dist/MacUtil-Intel.app
56 |
57 | # Test the Apple Silicon version
58 | open dist/MacUtil-AppleSilicon.app
59 | ```
60 |
61 | ### On Different Machines
62 | Copy the appropriate `.app` bundle to other Macs and test:
63 | - Intel Macs: Use `MacUtil-Intel.app`
64 | - Apple Silicon Macs: Use `MacUtil-AppleSilicon.app`
65 |
66 | ## 🔐 Code Signing & Distribution
67 |
68 | ### For Testing/Development
69 | The unsigned app bundles work fine for testing and development. Users may see a warning about running unsigned software.
70 |
71 | ### For Public Distribution
72 |
73 | #### 1. Code Signing (Required for macOS 10.15+)
74 | ```bash
75 | # Update the signing identity in sign_macos.sh
76 | # Then run:
77 | ./sign_macos.sh
78 | ```
79 |
80 | **Prerequisites:**
81 | - Apple Developer Account ($99/year)
82 | - Developer ID Application certificate
83 | - Xcode Command Line Tools
84 |
85 | #### 2. Notarization (Required for macOS 10.15+)
86 | After code signing, notarize your app:
87 |
88 | ```bash
89 | # Create a zip for notarization
90 | ditto -c -k --sequesterRsrc --keepParent dist/MacUtil-Intel.app MacUtil-Intel.zip
91 |
92 | # Submit for notarization
93 | xcrun altool --notarize-app -f MacUtil-Intel.zip \
94 | --primary-bundle-id com.macutil.gui \
95 | -u your@apple.id \
96 | -p @keychain:AC_PASSWORD
97 |
98 | # Wait for approval, then staple the notarization
99 | xcrun stapler staple dist/MacUtil-Intel.app
100 | ```
101 |
102 | #### 3. Creating a DMG Installer (Optional)
103 | ```bash
104 | # Create a DMG for easy distribution
105 | hdiutil create -volname "MacUtil GUI" -srcfolder dist/MacUtil-Intel.app -ov -format UDZO MacUtil-Intel.dmg
106 | ```
107 |
108 | ## 📱 App Store Distribution
109 |
110 | For App Store distribution, additional steps are required:
111 |
112 | 1. **App Store Certificates**: Use "3rd Party Mac Developer" certificates
113 | 2. **Sandbox**: Enable App Sandbox with appropriate entitlements
114 | 3. **Provisioning Profile**: Use App Store provisioning profile
115 | 4. **Packaging**: Create a `.pkg` file for submission
116 |
117 | See `sign_macos.sh` for App Store signing templates.
118 |
119 | ## 🗂 File Structure
120 |
121 | ```
122 | MacUtilGUI/
123 | ├── MacUtilGUI.fsproj # Project file (updated with macOS config)
124 | ├── Info.plist # Bundle information template
125 | ├── MacUtilGUI.entitlements # Code signing entitlements
126 | ├── MacUtilGUI.icns # App icon
127 | ├── logo.png # Source icon image
128 | ├── create_icon_from_logo.sh # Icon generation script
129 | ├── deploy_macos.sh # Main deployment script
130 | ├── sign_macos.sh # Code signing script
131 | └── dist/ # Output directory
132 | ├── MacUtil-Intel.app/ # Intel app bundle
133 | └── MacUtil-AppleSilicon.app/ # ARM64 app bundle
134 | ```
135 |
136 | ## 🛠 Customization
137 |
138 | ### Changing App Information
139 | Edit the configuration variables in `deploy_macos.sh`:
140 | ```bash
141 | APP_NAME="MacUtilGUI"
142 | BUNDLE_NAME="MacUtil"
143 | BUNDLE_IDENTIFIER="com.macutil.gui"
144 | VERSION="1.0.0"
145 | ```
146 |
147 | ### Custom Icon
148 | Replace `logo.png` with your own image and run:
149 | ```bash
150 | ./create_icon_from_logo.sh
151 | ```
152 |
153 | ### Bundle Properties
154 | Edit `MacUtilGUI.fsproj` to modify bundle properties like:
155 | - `CFBundleName`
156 | - `CFBundleIdentifier`
157 | - `CFBundleVersion`
158 |
159 | ## ❓ Troubleshooting
160 |
161 | ### App Won't Launch
162 | - Check executable permissions: `chmod +x YourApp.app/Contents/MacOS/MacUtilGUI`
163 | - Verify all dependencies are included in the bundle
164 | - Check Console.app for error messages
165 |
166 | ### "App is damaged" Error
167 | - App needs to be code signed and notarized
168 | - Try: `xattr -cr YourApp.app` to remove quarantine attributes
169 |
170 | ### Build Errors
171 | - Ensure .NET 9 is installed
172 | - Run `dotnet restore` before building
173 | - Check that all NuGet packages are restored
174 |
175 | ## 📋 Requirements
176 |
177 | - **Development**: macOS with .NET 9 SDK
178 | - **Target**: macOS 10.15 (Catalina) or later
179 | - **Code Signing**: Xcode Command Line Tools
180 | - **Distribution**: Apple Developer Account (for signing/notarization)
181 |
182 | ## 🔗 Resources
183 |
184 | - [Avalonia macOS Deployment Docs](https://docs.avaloniaui.net/docs/deployment/macOS)
185 | - [Apple Bundle Programming Guide](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/Introduction/Introduction.html)
186 | - [macOS Code Signing Guide](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution)
187 |
188 | ---
189 |
190 | ✨ **Your Avalonia F# app is now ready for macOS deployment!** ✨
191 |
--------------------------------------------------------------------------------
/MacUtilGUI/App.axaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
21 |
24 |
25 |
28 |
29 |
32 |
33 |
34 |
43 |
44 |
54 |
55 |
56 |
59 |
60 |
66 |
67 |
68 |
73 |
74 |
77 |
78 |
81 |
82 |
87 |
88 |
93 |
94 |
99 |
100 |
104 |
105 |
108 |
109 |
112 |
113 |
116 |
117 |
120 |
121 |
124 |
125 |
130 |
131 |
134 |
135 |
136 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Rust build artifacts
2 | /target
3 | /build
4 | rust/target
5 | rust/build
6 | /build/macutil
7 |
8 | # F# and .NET build artifacts
9 | ## Ignore Visual Studio temporary files, build results, and
10 | ## files generated by popular Visual Studio add-ons.
11 |
12 | # User-specific files
13 | *.rsuser
14 | *.suo
15 | *.user
16 | *.userosscache
17 | *.sln.docstates
18 |
19 | # User-specific files (MonoDevelop/Xamarin Studio)
20 | *.userprefs
21 |
22 | # Build results
23 | [Dd]ebug/
24 | [Dd]ebugPublic/
25 | [Rr]elease/
26 | [Rr]eleases/
27 | x64/
28 | x86/
29 | [Aa][Rr][Mm]/
30 | [Aa][Rr][Mm]64/
31 | bld/
32 | [Bb]in/
33 | [Oo]bj/
34 | [Ll]og/
35 |
36 | # F# specific
37 | *.fsproj.user
38 | *.fsx.lock
39 |
40 | # .NET Core
41 | project.lock.json
42 | project.fragment.lock.json
43 | artifacts/
44 |
45 | # StyleCop
46 | StyleCopReport.xml
47 |
48 | # Files built by Visual Studio
49 | *_i.c
50 | *_p.c
51 | *_h.h
52 | *.ilk
53 | *.meta
54 | *.obj
55 | *.iobj
56 | *.pch
57 | *.pdb
58 | *.ipdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *_wpftmp.csproj
69 | *.log
70 | *.vspscc
71 | *.vssscc
72 | .builds
73 | *.pidb
74 | *.svclog
75 | *.scc
76 |
77 | # Chutzpah Test files
78 | _Chutzpah*
79 |
80 | # Visual C++ cache files
81 | ipch/
82 | *.aps
83 | *.ncb
84 | *.opendb
85 | *.opensdf
86 | *.sdf
87 | *.cachefile
88 | *.VC.db
89 | *.VC.VC.opendb
90 |
91 | # Visual Studio profiler
92 | *.psess
93 | *.vsp
94 | *.vspx
95 | *.sap
96 |
97 | # Visual Studio Trace Files
98 | *.e2e
99 |
100 | # TFS 2012 Local Workspace
101 | $tf/
102 |
103 | # Guidance Automation Toolkit
104 | *.gpState
105 |
106 | # ReSharper is a .NET coding add-in
107 | _ReSharper*/
108 | *.[Rr]e[Ss]harper
109 | *.DotSettings.user
110 |
111 | # JustCode is a .NET coding add-in
112 | .JustCode
113 |
114 | # TeamCity is a build add-in
115 | _TeamCity*
116 |
117 | # DotCover is a Code Coverage Tool
118 | *.dotCover
119 |
120 | # AxoCover is a Code Coverage Tool
121 | .axoCover/*
122 | !.axoCover/settings.json
123 |
124 | # Visual Studio code coverage results
125 | *.coverage
126 | *.coveragexml
127 |
128 | # NCrunch
129 | _NCrunch_*
130 | .*crunch*.local.xml
131 | nCrunchTemp_*
132 |
133 | # MightyMoose
134 | *.mm.*
135 | AutoTest.Net/
136 |
137 | # Web workbench (sass)
138 | .sass-cache/
139 |
140 | # Installshield output folder
141 | [Ee]xpress/
142 |
143 | # DocProject is a documentation generator add-in
144 | DocProject/buildhelp/
145 | DocProject/Help/*.HxT
146 | DocProject/Help/*.HxC
147 | DocProject/Help/Html2
148 | DocProject/Help/html
149 |
150 | # Click-Once directory
151 | publish/
152 |
153 | # Publish Web Output
154 | *.[Pp]ublish.xml
155 | *.azurePubxml
156 | # Note: Comment the next line if you want to checkin your web deploy settings,
157 | # but database connection strings (with potential passwords) will be unencrypted
158 | *.pubxml
159 | *.publishproj
160 |
161 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
162 | # checkin your Azure Web App publish settings, but sensitive information contained
163 | # in these files may be unencrypted
164 | *.azurePubxml
165 | # Note: Comment the next line if you want to checkin your web deploy settings,
166 | # but database connection strings (with potential passwords) will be unencrypted
167 | *.pubxml
168 | *.publishproj
169 |
170 | # Microsoft Azure Build Output
171 | csx/
172 | *.build.csdef
173 |
174 | # Microsoft Azure Emulator
175 | ecf/
176 | rcf/
177 |
178 | # Windows Store app package directories and files
179 | AppPackages/
180 | BundleArtifacts/
181 | Package.StoreAssociation.xml
182 | _pkginfo.txt
183 | *.appx
184 |
185 | # Visual Studio cache files
186 | # files ending in .cache can be ignored
187 | *.[Cc]ache
188 | # but keep track of directories ending in .cache
189 | !*.[Cc]ache/
190 |
191 | # Others
192 | ClientBin/
193 | ~$*
194 | *~
195 | *.dbmdl
196 | *.dbproj.schemaview
197 | *.jfm
198 | *.pfx
199 | *.publishsettings
200 | orleans.codegen.cs
201 |
202 | # Including strong name files can present a security risk
203 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
204 | #*.snk
205 |
206 | # Since there are multiple workflows, uncomment next line to ignore bower_components
207 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
208 | #bower_components/
209 |
210 | # RIA/Silverlight projects
211 | Generated_Code/
212 |
213 | # Backup & report files from converting an old project file
214 | # to a newer Visual Studio version. Backup files are not needed,
215 | # because we have git ;-)
216 | _UpgradeReport_Files/
217 | Backup*/
218 | UpgradeLog*.XML
219 | UpgradeLog*.htm
220 | CConvertLog*.txt
221 |
222 | # SQL Server files
223 | *.mdf
224 | *.ldf
225 | *.ndf
226 |
227 | # Business Intelligence projects
228 | *.rdl.data
229 | *.bim.layout
230 | *.bim_*.settings
231 | *.rptproj.rsuser
232 | *- Backup*.rdl
233 |
234 | # Microsoft Fakes
235 | FakesAssemblies/
236 |
237 | # GhostDoc plugin setting file
238 | *.GhostDoc.xml
239 |
240 | # Node.js Tools for Visual Studio
241 | .ntvs_analysis.dat
242 | node_modules/
243 |
244 | # Visual Studio 6 build log
245 | *.plg
246 |
247 | # Visual Studio 6 workspace options file
248 | *.opt
249 |
250 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
251 | *.vbw
252 |
253 | # Visual Studio LightSwitch build output
254 | **/*.HTMLClient/GeneratedArtifacts
255 | **/*.DesktopClient/GeneratedArtifacts
256 | **/*.DesktopClient/ModelManifest.xml
257 | **/*.Server/GeneratedArtifacts
258 | **/*.Server/ModelManifest.xml
259 | _Pvt_Extensions
260 |
261 | # Paket dependency manager
262 | .paket/paket.exe
263 | paket-files/
264 |
265 | # FAKE - F# Make
266 | .fake/
267 |
268 | # JetBrains Rider
269 | .idea/
270 | *.sln.iml
271 |
272 | # CodeRush personal settings
273 | .cr/personal
274 |
275 | # Python Tools for Visual Studio (PTVS)
276 | __pycache__/
277 | *.pyc
278 |
279 | # Cake - Uncomment if you are using it
280 | # tools/**
281 | # !tools/packages.config
282 |
283 | # Tabs Studio
284 | *.tss
285 |
286 | # Telerik's JustMock configuration file
287 | *.jmconfig
288 |
289 | # BizTalk build output
290 | *.btp.cs
291 | *.btm.cs
292 | *.odx.cs
293 | *.xsd.cs
294 |
295 | # OpenCover UI analysis results
296 | OpenCover/
297 |
298 | # Azure Stream Analytics local run output
299 | ASALocalRun/
300 |
301 | # MSBuild Binary and Structured Log
302 | *.binlog
303 |
304 | # NVidia Nsight GPU debugger configuration file
305 | *.nvuser
306 |
307 | # MFractors (Xamarin productivity tool) working folder
308 | .mfractor/
309 |
310 | # Local History for Visual Studio
311 | .localhistory/
312 |
313 | # BeatPulse healthcheck temp database
314 | healthchecksdb
315 |
316 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
317 | MigrationBackup/
318 |
319 | # MacUtil GUI specific ignores
320 | MacUtilGUI/bin/
321 | MacUtilGUI/obj/
322 | MacUtilGUI/dist/
323 | MacUtilGUI/publish/
324 |
325 | # macOS specific files
326 | .DS_Store
327 | .DS_Store?
328 | ._*
329 | .Spotlight-V100
330 | .Trashes
331 | ehthumbs.db
332 | Thumbs.db
--------------------------------------------------------------------------------
/scripts/applications-setup/tab_data.toml:
--------------------------------------------------------------------------------
1 | name = "Applications Setup"
2 |
3 | [[data]]
4 | name = "Communication Apps"
5 |
6 | [[data.entries]]
7 | name = "Discord"
8 | description = "Discord is a versatile communication platform for gamers and communities, offering voice, video, and text chat features."
9 | script = "communication-apps/discord-setup.sh"
10 |
11 | [[data.entries]]
12 | name = "Signal"
13 | description = "Signal is a privacy-focused messaging app that provides end-to-end encryption for secure text, voice, and video communication."
14 | script = "communication-apps/signal-setup.sh"
15 |
16 | [[data.entries]]
17 | name = "Slack"
18 | description = "Slack is a collaboration platform designed for team communication, featuring channels, direct messaging, file sharing, and integrations with various productivity tools."
19 | script = "communication-apps/slack-setup.sh"
20 |
21 | [[data.entries]]
22 | name = "Telegram"
23 | description = "Telegram is a cloud-based messaging app known for its speed and security, offering features like group chats, channels, and end-to-end encrypted calls."
24 | script = "communication-apps/telegram-setup.sh"
25 |
26 | [[data.entries]]
27 | name = "Thunderbird"
28 | description = "Thunderbird is a free, open-source email client that offers powerful features like customizable email management, a built-in calendar, and extensive add-ons for enhanced functionality."
29 | script = "communication-apps/thunderbird-setup.sh"
30 |
31 | [[data.entries]]
32 | name = "WhatsApp"
33 | description = "WhatsApp is a popular messaging application that allows users to send text messages, voice messages, make voice and video calls, and share media files."
34 | script = "communication-apps/whatsapp-setup.sh"
35 |
36 | [[data]]
37 | name = "Developer Tools"
38 |
39 | [[data.entries]]
40 | name = "Github Desktop"
41 | description = "GitHub Desktop is a user-friendly application that simplifies the process of managing Git repositories and interacting with GitHub, providing a graphical interface for tasks like committing, branching, and syncing changes."
42 | script = "developer-tools/githubdesktop.sh"
43 |
44 | [[data.entries]]
45 | name = "JetBrains Toolbox"
46 | description = "JetBrains Toolbox is a collection of tools and an app that help developers work with JetBrains products."
47 | script = "developer-tools/jetbrains-toolbox.sh"
48 |
49 | [[data.entries]]
50 | name = "Neovim"
51 | description = "Neovim is a refactor, and sometimes redactor, in the tradition of Vim.\nIt is not a rewrite but a continuation and extension of Vim.\nThis command configures neovim from CTT's neovim configuration.\nhttps://github.com/ChrisTitusTech/neovim"
52 | script = "developer-tools/neovim.sh"
53 |
54 | [[data.entries]]
55 | name = "Sublime Text"
56 | description = "Sublime Text is a fast, lightweight, and customizable text editor known for its simplicity, powerful features, and wide range of plugins for various programming languages."
57 | script = "developer-tools/sublime.sh"
58 |
59 | [[data.entries]]
60 | name = "VS Code"
61 | description = "Visual Studio Code (VS Code) is a lightweight, open-source code editor with built-in support for debugging, version control, and extensions for various programming languages and frameworks."
62 | script = "developer-tools/vscode.sh"
63 |
64 | [[data.entries]]
65 | name = "VS Codium"
66 | description = "VSCodium is a free, open-source version of Visual Studio Code (VS Code) that removes Microsoft-specific telemetry and branding."
67 | script = "developer-tools/vscodium.sh"
68 |
69 | [[data.entries]]
70 | name = "Zed"
71 | description = "Zed is a next-generation code editor written in rust, designed for high-performance collaboration with humans and AI."
72 | script = "developer-tools/zed.sh"
73 |
74 | [[data]]
75 | name = "Web Browsers"
76 |
77 | [[data.entries]]
78 | name = "Brave"
79 | description = "Brave is a free and open-source web browser developed by Brave Software, Inc. based on the Chromium web browser."
80 | script = "browsers/brave.sh"
81 |
82 | [[data.entries]]
83 | name = "Chromium"
84 | description = "Chromium is an open-source web browser project started by Google, to provide the source code for the proprietary Google Chrome browser."
85 | script = "browsers/chromium.sh"
86 |
87 | [[data.entries]]
88 | name = "Google Chrome"
89 | description = "Google Chrome is a fast, secure, and free web browser, built for the modern web."
90 | script = "browsers/google-chrome.sh"
91 |
92 | [[data.entries]]
93 | name = "LibreWolf"
94 | description = "LibreWolf is a fork of Firefox, focused on privacy, security, and freedom."
95 | script = "browsers/librewolf.sh"
96 |
97 | [[data.entries]]
98 | name = "Mozilla Firefox"
99 | description = "Mozilla Firefox is a free and open-source web browser developed by the Mozilla Foundation."
100 | script = "browsers/firefox.sh"
101 |
102 | [[data.entries]]
103 | name = "Zen Browser"
104 | description = "Zen Browser is a privacy-focused web browser designed for enhanced security and a seamless browsing experience."
105 | script = "browsers/zen-browser.sh"
106 |
107 | [[data.entries]]
108 | name = "Thorium"
109 | description = "Thorium is a Chromium-based browser focused on privacy and performance."
110 | script = "browsers/thorium.sh"
111 |
112 | [[data.entries]]
113 | name = "Vivaldi"
114 | description = "Vivaldi is a freeware, cross-platform web browser developed by Vivaldi Technologies."
115 | script = "browsers/vivaldi.sh"
116 |
117 | [[data.entries]]
118 | name = "waterfox"
119 | description = "Waterfox is the privacy-focused web browser engineered to give you speed, control, and peace of mind on the internet."
120 | script = "browsers/waterfox.sh"
121 |
122 | [[data]]
123 | name = "Alacritty"
124 | description = "Alacritty is a modern terminal emulator that comes with sensible defaults, but allows for extensive configuration. By integrating with other applications, rather than reimplementing their functionality, it manages to provide a flexible set of features with high performance. The supported platforms currently consist of BSD, Linux, macOS and Windows. This command installs and configures alacritty terminal emulator."
125 | script = "alacritty-setup.sh"
126 |
127 | [[data]]
128 | name = "Android Debloater"
129 | description = "Universal Android Debloater (UAD) is a tool designed to help users remove bloatware and unnecessary pre-installed applications from Android devices, enhancing performance and user experience."
130 | script = "android-debloat.sh"
131 |
132 | [[data]]
133 | name = "Fastfetch"
134 | description = "Fastfetch is a neofetch-like tool for fetching system information and displaying it prettily. It is written mainly in C, with performance and customizability in mind. This command installs fastfetch and configures from CTT's mybash repository. https://github.com/ChrisTitusTech/mybash"
135 | script = "fastfetch-setup.sh"
136 |
137 | [[data]]
138 | name = "Kitty"
139 | description = "kitty is a free and open-source GPU-accelerated terminal emulator for Linux, macOS, and some BSD distributions, focused on performance and features. kitty is written in a mix of C and Python programming languages. This command installs and configures kitty."
140 | script = "kitty-setup.sh"
141 |
142 | [[data]]
143 | name = "ZSH Prompt"
144 | description = "The Z shell is a Unix shell that can be used as an interactive login shell and as a command interpreter for shell scripting. Zsh is an extended Bourne shell with many improvements, including some features of Bash, ksh, and tcsh. This command installs ZSH prompt and provides basic configuration."
145 | script = "zsh-setup.sh"
146 |
--------------------------------------------------------------------------------
/MacUtilGUI/sign_macos.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # macOS Code Signing Script for MacUtil GUI
4 | # This script signs your app bundles for distribution outside the App Store
5 |
6 | # Configuration - UPDATE THESE VALUES
7 | DEVELOPER_ID="Developer ID Application: CT Tech Group LLC (8ZHX2A9ALF)"
8 | APP_BUNDLE_PATH="./dist/MacUtil-Universal.app" # Universal app bundle (recommended)
9 | ENTITLEMENTS_FILE="./MacUtilGUI.entitlements"
10 |
11 | # Colors for output
12 | RED='\033[0;31m'
13 | GREEN='\033[0;32m'
14 | BLUE='\033[0;34m'
15 | YELLOW='\033[1;33m'
16 | NC='\033[0m' # No Color
17 |
18 | print_status() {
19 | echo -e "${BLUE}ℹ️ $1${NC}"
20 | }
21 |
22 | print_success() {
23 | echo -e "${GREEN}✅ $1${NC}"
24 | }
25 |
26 | print_warning() {
27 | echo -e "${YELLOW}⚠️ $1${NC}"
28 | }
29 |
30 | print_error() {
31 | echo -e "${RED}❌ $1${NC}"
32 | }
33 |
34 | echo "🔐 MacUtil GUI Code Signing Script"
35 | echo "=================================="
36 | echo
37 |
38 | # Check if app bundle exists
39 | if [ ! -d "$APP_BUNDLE_PATH" ]; then
40 | print_error "App bundle not found at: $APP_BUNDLE_PATH"
41 | print_status "Please run ./deploy_macos.sh first to create the app bundle"
42 | exit 1
43 | fi
44 |
45 | # Check if entitlements file exists
46 | if [ ! -f "$ENTITLEMENTS_FILE" ]; then
47 | print_error "Entitlements file not found at: $ENTITLEMENTS_FILE"
48 | exit 1
49 | fi
50 |
51 | # Check if codesign is available
52 | if ! command -v codesign &> /dev/null; then
53 | print_error "codesign command not found. Please install Xcode Command Line Tools."
54 | exit 1
55 | fi
56 |
57 | print_status "App bundle: $APP_BUNDLE_PATH"
58 | print_status "Entitlements: $ENTITLEMENTS_FILE"
59 | print_status "Developer ID: $DEVELOPER_ID"
60 | echo
61 |
62 | # List available signing identities
63 | print_status "Available signing identities:"
64 | security find-identity -v -p codesigning
65 | echo
66 |
67 | print_warning "IMPORTANT: Make sure you have:"
68 | print_warning "1. A valid Developer ID Application certificate in your Keychain"
69 | print_warning "2. Updated the DEVELOPER_ID variable in this script"
70 | print_warning "3. An Apple Developer account for notarization"
71 | print_warning "4. Set up AC_PASSWORD in keychain with:"
72 | print_warning " security add-generic-password -a 'contact@christitus.com' -s 'AC_PASSWORD' -w"
73 | print_warning " (it will prompt you to enter your app-specific password securely)"
74 | echo
75 |
76 | # Check if AC_PASSWORD is accessible in keychain
77 | print_status "Checking keychain access for AC_PASSWORD..."
78 | if security find-generic-password -a 'contact@christitus.com' -s 'AC_PASSWORD' >/dev/null 2>&1; then
79 | print_success "AC_PASSWORD found in keychain"
80 | else
81 | print_error "AC_PASSWORD not found in keychain or keychain is locked"
82 | print_status "Please ensure the keychain is unlocked and AC_PASSWORD is stored"
83 | print_status "You can test with: security find-generic-password -a 'contact@christitus.com' -s 'AC_PASSWORD'"
84 | fi
85 | echo
86 |
87 | read -p "Do you want to proceed with code signing? (y/N): " -n 1 -r
88 | echo
89 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then
90 | print_status "Code signing cancelled"
91 | exit 0
92 | fi
93 |
94 | # Sign the app bundle with deep signing (signs all nested components)
95 | print_status "Signing the app bundle and all nested components..."
96 | codesign --force --deep --timestamp --options=runtime --entitlements "$ENTITLEMENTS_FILE" --sign "$DEVELOPER_ID" "$APP_BUNDLE_PATH"
97 |
98 | if [ $? -eq 0 ]; then
99 | print_success "App bundle signed successfully!"
100 |
101 | # Verify the signature
102 | print_status "Verifying signature..."
103 | codesign --verify --verbose "$APP_BUNDLE_PATH"
104 |
105 | if [ $? -eq 0 ]; then
106 | print_success "Signature verification passed!"
107 | echo
108 |
109 | # Ask if user wants to proceed with notarization
110 | read -p "Do you want to proceed with notarization? (y/N): " -n 1 -r
111 | echo
112 | if [[ $REPLY =~ ^[Yy]$ ]]; then
113 | print_status "Starting notarization process..."
114 |
115 | # Ensure keychain is unlocked
116 | print_status "Ensuring keychain is unlocked..."
117 | security unlock-keychain ~/Library/Keychains/login.keychain-db
118 |
119 | # Retrieve AC_PASSWORD from keychain (silently)
120 | print_status "Retrieving app-specific password from keychain..."
121 | AC_PASSWORD=$(security find-generic-password -a 'contact@christitus.com' -s 'AC_PASSWORD' -w 2>/dev/null)
122 | if [ $? -ne 0 ] || [ -z "$AC_PASSWORD" ]; then
123 | print_error "Failed to retrieve AC_PASSWORD from keychain"
124 | print_status "Please ensure the keychain is unlocked and AC_PASSWORD is stored correctly"
125 | exit 1
126 | fi
127 | print_success "App-specific password retrieved successfully"
128 |
129 | # Create zip for notarization
130 | ZIP_NAME="MacUtil.zip"
131 | print_status "Creating zip file for notarization: $ZIP_NAME"
132 | if [ -f "$ZIP_NAME" ]; then
133 | rm "$ZIP_NAME"
134 | fi
135 | ditto -c -k --sequesterRsrc --keepParent "$APP_BUNDLE_PATH" "$ZIP_NAME"
136 |
137 | if [ $? -eq 0 ]; then
138 | print_success "Zip file created successfully!"
139 |
140 | # Submit for notarization
141 | print_status "Submitting for notarization (this may take several minutes)..."
142 | print_status "You will see progress updates from Apple's notarization service..."
143 | echo
144 |
145 | # Submit for notarization with real-time output
146 | xcrun notarytool submit "$ZIP_NAME" --apple-id contact@christitus.com --team-id 8ZHX2A9ALF --password "$AC_PASSWORD" --wait
147 |
148 | if [ $? -eq 0 ]; then
149 |
150 | # Staple the notarization
151 | print_status "Stapling notarization to app bundle..."
152 | xcrun stapler staple "$APP_BUNDLE_PATH"
153 |
154 | if [ $? -eq 0 ]; then
155 | print_success "Notarization stapled successfully!"
156 | print_success "App is now ready for distribution! 🚀"
157 | echo
158 | print_status "Next steps:"
159 | echo "1. Test the notarized app on a different Mac"
160 | echo "2. Distribute the app bundle: $APP_BUNDLE_PATH"
161 | else
162 | print_error "Failed to staple notarization"
163 | print_status "The app is notarized but stapling failed. You can distribute it anyway."
164 | fi
165 | else
166 | print_error "Notarization failed!"
167 | print_status "Check your Apple ID credentials and app-specific password"
168 | print_status "You can still distribute the signed app, but users may see security warnings"
169 | fi
170 |
171 | # Clear the password variable for security
172 | unset AC_PASSWORD
173 |
174 | # Clean up zip file
175 | if [ -f "$ZIP_NAME" ]; then
176 | rm "$ZIP_NAME"
177 | print_status "Cleaned up temporary zip file"
178 | fi
179 | else
180 | print_error "Failed to create zip file for notarization"
181 | fi
182 | else
183 | print_status "Skipping notarization"
184 | echo
185 | print_status "🎯 Manual notarization steps (if needed later):"
186 | echo "1. Create a zip: ditto -c -k --sequesterRsrc --keepParent '$APP_BUNDLE_PATH' MacUtil.zip"
187 | echo "2. Submit for notarization: xcrun notarytool submit MacUtil.zip --apple-id contact@christitus.com --team-id 8ZHX2A9ALF --password \$(security find-generic-password -a 'contact@christitus.com' -s 'AC_PASSWORD' -w) --wait"
188 | echo "3. If successful, staple: xcrun stapler staple '$APP_BUNDLE_PATH'"
189 | fi
190 | echo
191 | print_success "Code signing complete! 🔐"
192 | else
193 | print_error "Signature verification failed!"
194 | exit 1
195 | fi
196 | else
197 | print_error "Failed to sign app bundle!"
198 | exit 1
199 | fi
200 |
--------------------------------------------------------------------------------
/MacUtilGUI/Views/MainWindow.axaml:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
62 |
63 |
64 |
65 |
66 |
67 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
94 |
95 |
96 |
97 |
98 |
99 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
117 |
118 |
119 |
120 |
125 |
126 |
127 |
128 |
129 |
130 |
135 |
136 |
137 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
--------------------------------------------------------------------------------
/MacUtilGUI/deploy_macos.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # MacUtil GUI macOS Deployment Script
4 | # Creates proper .app bundles for macOS distribution
5 |
6 | echo "🚀 Starting MacUtil GUI macOS deployment process..."
7 | echo
8 |
9 | # Configuration
10 | APP_NAME="MacUtilGUI"
11 | BUNDLE_NAME="MacUtil"
12 | BUNDLE_IDENTIFIER="com.macutil.gui"
13 | VERSION="0.2.1"
14 | COPYRIGHT="Copyright © 2025 MacUtil. All rights reserved."
15 |
16 | # Colors for output
17 | RED='\033[0;31m'
18 | GREEN='\033[0;32m'
19 | BLUE='\033[0;34m'
20 | YELLOW='\033[1;33m'
21 | NC='\033[0m' # No Color
22 |
23 | print_status() {
24 | echo -e "${BLUE}ℹ️ $1${NC}"
25 | }
26 |
27 | print_success() {
28 | echo -e "${GREEN}✅ $1${NC}"
29 | }
30 |
31 | print_warning() {
32 | echo -e "${YELLOW}⚠️ $1${NC}"
33 | }
34 |
35 | print_error() {
36 | echo -e "${RED}❌ $1${NC}"
37 | }
38 |
39 | # Clean previous builds
40 | print_status "Cleaning previous builds..."
41 | dotnet clean -c Release
42 | rm -rf ./bin/Release/net9.0/publish/
43 | rm -rf ./dist/
44 | print_success "Cleanup complete"
45 | echo
46 |
47 | # Restore packages
48 | print_status "Restoring NuGet packages..."
49 | dotnet restore
50 | print_success "Package restoration complete"
51 | echo
52 |
53 | # Create output directories
54 | mkdir -p ./dist/
55 |
56 | # Function to create app bundle
57 | create_app_bundle() {
58 | local runtime=$1
59 | local arch_name=$2
60 |
61 | print_status "Building for $arch_name ($runtime)..."
62 |
63 | # Publish the application
64 | dotnet publish -c Release -r $runtime -p:UseAppHost=true --self-contained true -o ./bin/Release/net9.0/publish/$runtime/
65 |
66 | if [ $? -ne 0 ]; then
67 | print_error "$arch_name build failed"
68 | return 1
69 | fi
70 |
71 | print_success "$arch_name build successful"
72 |
73 | # Create .app bundle structure
74 | local app_bundle="./dist/${BUNDLE_NAME}-${arch_name}.app"
75 | print_status "Creating $arch_name app bundle..."
76 |
77 | mkdir -p "$app_bundle/Contents/MacOS"
78 | mkdir -p "$app_bundle/Contents/Resources"
79 |
80 | # Copy the executable and dependencies
81 | cp -R "./bin/Release/net9.0/publish/$runtime/"* "$app_bundle/Contents/MacOS/"
82 |
83 | # Create Info.plist
84 | cat > "$app_bundle/Contents/Info.plist" << EOF
85 |
86 |
87 |
88 |
89 | CFBundleIconFile
90 | MacUtilGUI.icns
91 | CFBundleIdentifier
92 | $BUNDLE_IDENTIFIER
93 | CFBundleName
94 | $BUNDLE_NAME
95 | CFBundleDisplayName
96 | MacUtil GUI
97 | CFBundleVersion
98 | $VERSION
99 | CFBundleShortVersionString
100 | $VERSION
101 | LSMinimumSystemVersion
102 | 10.15
103 | CFBundleExecutable
104 | $APP_NAME
105 | CFBundleInfoDictionaryVersion
106 | 6.0
107 | CFBundlePackageType
108 | APPL
109 | CFBundleSignature
110 | ????
111 | NSHighResolutionCapable
112 |
113 | NSPrincipalClass
114 | NSApplication
115 | LSApplicationCategoryType
116 | public.app-category.utilities
117 | NSRequiresAquaSystemAppearance
118 |
119 | NSHumanReadableCopyright
120 | $COPYRIGHT
121 |
122 |
123 | EOF
124 |
125 | # Copy icon file
126 | if [ -f "MacUtilGUI.icns" ]; then
127 | cp "MacUtilGUI.icns" "$app_bundle/Contents/Resources/"
128 | else
129 | print_warning "Icon file MacUtilGUI.icns not found"
130 | fi
131 |
132 | # Make the executable file executable
133 | chmod +x "$app_bundle/Contents/MacOS/$APP_NAME"
134 |
135 | print_success "$arch_name app bundle created: $app_bundle"
136 |
137 | # Show bundle size
138 | local bundle_size
139 | bundle_size=$(du -sh "$app_bundle" | cut -f1)
140 | print_status "Bundle size: $bundle_size"
141 |
142 | return 0
143 | }
144 |
145 | # Build for Intel x64 Macs
146 | print_status "Building Intel x64 binary..."
147 | dotnet publish -c Release -r osx-x64 -p:UseAppHost=true --self-contained true -o ./bin/Release/net9.0/publish/osx-x64/
148 | if [ $? -ne 0 ]; then
149 | print_error "Intel x64 build failed"
150 | exit 1
151 | fi
152 | print_success "Intel x64 build successful"
153 |
154 | # Build for Apple Silicon ARM64 Macs
155 | print_status "Building Apple Silicon ARM64 binary..."
156 | dotnet publish -c Release -r osx-arm64 -p:UseAppHost=true --self-contained true -o ./bin/Release/net9.0/publish/osx-arm64/
157 | if [ $? -ne 0 ]; then
158 | print_error "Apple Silicon ARM64 build failed"
159 | exit 1
160 | fi
161 | print_success "Apple Silicon ARM64 build successful"
162 | echo
163 |
164 | # Create Universal App Bundle with lipo
165 | print_status "Creating Universal App Bundle with both architectures..."
166 |
167 | # Create the universal app bundle structure
168 | universal_app="./dist/${BUNDLE_NAME}-Universal.app"
169 | mkdir -p "$universal_app/Contents/MacOS"
170 | mkdir -p "$universal_app/Contents/Resources"
171 |
172 | # Create universal binary using lipo
173 | print_status "Creating universal binary with lipo..."
174 | lipo -create \
175 | "./bin/Release/net9.0/publish/osx-x64/$APP_NAME" \
176 | "./bin/Release/net9.0/publish/osx-arm64/$APP_NAME" \
177 | -output "$universal_app/Contents/MacOS/$APP_NAME"
178 |
179 | if [ $? -ne 0 ]; then
180 | print_error "Failed to create universal binary with lipo"
181 | exit 1
182 | fi
183 |
184 | # Copy all dependencies from one of the builds (they should be identical except for the main executable)
185 | print_status "Copying dependencies..."
186 | # Copy everything except the main executable
187 | rsync -av --exclude="$APP_NAME" "./bin/Release/net9.0/publish/osx-x64/" "$universal_app/Contents/MacOS/"
188 |
189 | # Verify universal binary
190 | print_status "Verifying universal binary..."
191 | file "$universal_app/Contents/MacOS/$APP_NAME"
192 | lipo -info "$universal_app/Contents/MacOS/$APP_NAME"
193 |
194 | # Create Info.plist for universal app
195 | cat > "$universal_app/Contents/Info.plist" << EOF
196 |
197 |
198 |
199 |
200 | CFBundleIconFile
201 | MacUtilGUI.icns
202 | CFBundleIdentifier
203 | $BUNDLE_IDENTIFIER
204 | CFBundleName
205 | $BUNDLE_NAME
206 | CFBundleDisplayName
207 | MacUtil GUI
208 | CFBundleVersion
209 | $VERSION
210 | CFBundleShortVersionString
211 | $VERSION
212 | LSMinimumSystemVersion
213 | 10.15
214 | CFBundleExecutable
215 | $APP_NAME
216 | CFBundleInfoDictionaryVersion
217 | 6.0
218 | CFBundlePackageType
219 | APPL
220 | CFBundleSignature
221 | ????
222 | NSHighResolutionCapable
223 |
224 | NSPrincipalClass
225 | NSApplication
226 | LSApplicationCategoryType
227 | public.app-category.utilities
228 | NSRequiresAquaSystemAppearance
229 |
230 | NSHumanReadableCopyright
231 | $COPYRIGHT
232 |
233 |
234 | EOF
235 |
236 | # Copy icon file
237 | if [ -f "MacUtilGUI.icns" ]; then
238 | cp "MacUtilGUI.icns" "$universal_app/Contents/Resources/"
239 | print_success "Icon copied to universal app bundle"
240 | else
241 | print_warning "Icon file MacUtilGUI.icns not found"
242 | fi
243 |
244 | # Make the executable file executable
245 | chmod +x "$universal_app/Contents/MacOS/$APP_NAME"
246 |
247 | # Show bundle size
248 | universal_size=$(du -sh "$universal_app" | cut -f1)
249 | print_success "Universal app bundle created: $universal_app"
250 | print_status "Universal bundle size: $universal_size"
251 |
252 | # Optionally create individual architecture bundles for compatibility
253 | print_status "Creating individual architecture bundles for compatibility..."
254 | if ! create_app_bundle "osx-x64" "Intel"; then
255 | print_warning "Intel-only bundle creation failed"
256 | fi
257 |
258 | if ! create_app_bundle "osx-arm64" "AppleSilicon"; then
259 | print_warning "Apple Silicon-only bundle creation failed"
260 | fi
261 |
262 | echo
263 | print_success "🎉 macOS deployment complete!"
264 | echo
265 | print_status "📦 Created app bundles:"
266 | echo "─────────────────────────────────────────────────────────"
267 | ls -la ./dist/*.app 2>/dev/null || echo "No .app bundles found in ./dist/"
268 | echo
269 | print_status "🎯 Usage Instructions:"
270 | echo "─────────────────────────────────────────────────────────"
271 | echo "• Universal (any Mac): Use MacUtil-Universal.app (RECOMMENDED)"
272 | echo "• Intel Mac users: Use MacUtil-Intel.app (if available)"
273 | echo "• Apple Silicon Mac users: Use MacUtil-AppleSilicon.app (if available)"
274 | echo
275 | print_status "📝 Next Steps for Distribution:"
276 | echo "─────────────────────────────────────────────────────────"
277 | echo "1. Test the .app bundles on target machines"
278 | echo "2. For distribution outside App Store:"
279 | echo " - Code sign the apps with a Developer ID certificate"
280 | echo " - Notarize the apps with Apple"
281 | echo " - Create a .dmg installer (optional)"
282 | echo "3. For App Store distribution:"
283 | echo " - Use App Store certificates and provisioning profiles"
284 | echo " - Package as .pkg for submission"
285 | echo
286 | print_success "All builds completed successfully! 🚀"
287 |
--------------------------------------------------------------------------------
/MacUtilGUI/Services/ScriptService.fs:
--------------------------------------------------------------------------------
1 | namespace MacUtilGUI.Services
2 |
3 | open System
4 | open System.IO
5 | open System.Diagnostics
6 | open System.Reflection
7 | open System.Threading.Tasks
8 | open System.Text
9 | open Tomlyn
10 | open MacUtilGUI.Models
11 |
12 | module ScriptService =
13 |
14 | let assembly = Assembly.GetExecutingAssembly()
15 |
16 | // Function to check if Terminal has Full Disk Access permission
17 | let checkFullDiskAccess () : bool * string =
18 | try
19 | // Test multiple protected directories that require Full Disk Access
20 | let testPaths = [
21 | // System directories that require FDA
22 | "/Library/Application Support"
23 | "/Users/Shared/.com.apple.timemachine.supported"
24 | "/System/Library/CoreServices/SystemUIServer.app/Contents"
25 | // User's own protected directories
26 | Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Mail")
27 | Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Safari")
28 | ]
29 |
30 | let mutable hasAccess = false
31 | let mutable lastError = ""
32 |
33 | // Try each test path until we find one that works or all fail
34 | for testPath in testPaths do
35 | if not hasAccess && Directory.Exists(testPath) then
36 | try
37 | // Attempt to list contents - this will fail without Full Disk Access
38 | let startInfo = ProcessStartInfo()
39 | startInfo.FileName <- "/bin/ls"
40 | startInfo.Arguments <- sprintf "-la \"%s\"" testPath
41 | startInfo.UseShellExecute <- false
42 | startInfo.RedirectStandardOutput <- true
43 | startInfo.RedirectStandardError <- true
44 | startInfo.CreateNoWindow <- true
45 |
46 | let proc = Process.Start(startInfo)
47 | if proc <> null then
48 | proc.WaitForExit()
49 | let error = proc.StandardError.ReadToEnd()
50 | let output = proc.StandardOutput.ReadToEnd()
51 |
52 | // If we get permission denied, Full Disk Access is not granted
53 | if error.Contains("Operation not permitted") || error.Contains("Permission denied") then
54 | lastError <- sprintf "Access denied to %s" testPath
55 | elif not (String.IsNullOrEmpty(output)) then
56 | // Successfully read directory contents
57 | hasAccess <- true
58 | else
59 | lastError <- sprintf "No output from listing %s" testPath
60 | with ex ->
61 | lastError <- sprintf "Error testing %s: %s" testPath ex.Message
62 |
63 | if hasAccess then
64 | true, "Terminal has Full Disk Access permission"
65 | else
66 | false, sprintf "Terminal does not have Full Disk Access permission. Last error: %s" lastError
67 |
68 | with ex ->
69 | false, sprintf "Error checking Full Disk Access: %s" ex.Message
70 |
71 | // Function to guide user through granting Full Disk Access
72 | let promptForFullDiskAccess (onOutput: string -> unit) : unit =
73 | onOutput "⚠️ IMPORTANT: Terminal needs Full Disk Access permission"
74 | onOutput ""
75 | onOutput "To grant Full Disk Access to Terminal:"
76 | onOutput "1. Open System Preferences/Settings"
77 | onOutput "2. Go to Security & Privacy (or Privacy & Security)"
78 | onOutput "3. Click on 'Full Disk Access' in the sidebar"
79 | onOutput "4. Click the lock icon and enter your password"
80 | onOutput "5. Click the '+' button to add an application"
81 | onOutput "6. Navigate to /Applications/Utilities/Terminal.app"
82 | onOutput "7. Select Terminal and click 'Open'"
83 | onOutput "8. Make sure the checkbox next to Terminal is checked"
84 | onOutput ""
85 | onOutput "📱 Alternative: You can also:"
86 | onOutput " • Open Spotlight (Cmd+Space)"
87 | onOutput " • Type 'Privacy & Security' and press Enter"
88 | onOutput " • Look for 'Full Disk Access' on the left"
89 | onOutput ""
90 | onOutput "After granting permission, restart MacUtil and try again."
91 | onOutput ""
92 | onOutput "🔍 Why is this needed?"
93 | onOutput "Many system configuration scripts need to access protected"
94 | onOutput "system files and directories that require Full Disk Access."
95 |
96 | // Function to open System Preferences to the correct pane
97 | let openPrivacySettings () : unit =
98 | let rec tryOpenSettings attempts =
99 | match attempts with
100 | | 1 ->
101 | // Try to open directly to Privacy & Security Full Disk Access (macOS 13+)
102 | try
103 | let startInfo = ProcessStartInfo()
104 | startInfo.FileName <- "/usr/bin/open"
105 | startInfo.Arguments <- "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"
106 | startInfo.UseShellExecute <- false
107 | startInfo.CreateNoWindow <- true
108 |
109 | let proc = Process.Start(startInfo)
110 | if proc <> null then
111 | proc.WaitForExit()
112 | // If exit code is 0, the URL scheme worked
113 | proc.ExitCode = 0
114 | else
115 | false
116 | with _ ->
117 | false
118 | | 2 ->
119 | // Fallback 1: Try macOS 12 and earlier URL scheme
120 | try
121 | let startInfo = ProcessStartInfo()
122 | startInfo.FileName <- "/usr/bin/open"
123 | startInfo.Arguments <- "x-apple.systempreferences:com.apple.preference.security?Privacy"
124 | startInfo.UseShellExecute <- false
125 | startInfo.CreateNoWindow <- true
126 |
127 | let proc = Process.Start(startInfo)
128 | if proc <> null then
129 | proc.WaitForExit()
130 | proc.ExitCode = 0
131 | else
132 | false
133 | with _ ->
134 | false
135 | | 3 ->
136 | // Fallback 2: Open System Preferences/Settings app directly
137 | try
138 | let startInfo = ProcessStartInfo()
139 | startInfo.FileName <- "/usr/bin/open"
140 | // Try System Settings first (macOS 13+), then System Preferences
141 | startInfo.Arguments <- "/System/Applications/System Settings.app"
142 | startInfo.UseShellExecute <- false
143 | startInfo.CreateNoWindow <- true
144 |
145 | let proc = Process.Start(startInfo)
146 | if proc <> null then
147 | proc.WaitForExit()
148 | proc.ExitCode = 0
149 | else
150 | false
151 | with _ ->
152 | false
153 | | 4 ->
154 | // Final fallback: Open legacy System Preferences (macOS 12 and earlier)
155 | try
156 | let startInfo = ProcessStartInfo()
157 | startInfo.FileName <- "/usr/bin/open"
158 | startInfo.Arguments <- "/System/Applications/System Preferences.app"
159 | startInfo.UseShellExecute <- false
160 | startInfo.CreateNoWindow <- true
161 |
162 | let proc = Process.Start(startInfo)
163 | if proc <> null then
164 | proc.WaitForExit()
165 | true // Last attempt, consider it successful regardless
166 | else
167 | false
168 | with _ ->
169 | false
170 | | _ ->
171 | false
172 |
173 | // Try each method until one succeeds
174 | let mutable currentAttempt = 1
175 | let mutable success = false
176 | while currentAttempt <= 4 && not success do
177 | success <- tryOpenSettings currentAttempt
178 | if not success then
179 | currentAttempt <- currentAttempt + 1
180 |
181 | // Modified function to preprocess scripts for non-TTY environments
182 | let preprocessScriptForNonTTY (scriptContent: string) : string =
183 | let lines = scriptContent.Split([| '\n'; '\r' |], StringSplitOptions.None)
184 | let processedLines =
185 | lines
186 | |> Array.map (fun line ->
187 | let trimmed = line.Trim()
188 |
189 | // Skip TTY checks that cause issues in .app bundles
190 | if trimmed.Contains("tty -s") ||
191 | trimmed.Contains("[ -t 0 ]") ||
192 | trimmed.Contains("[[ -t 0 ]]") ||
193 | trimmed.Contains("if tty") ||
194 | trimmed.Contains("test -t 0") ||
195 | trimmed.Contains("[ -t 1 ]") ||
196 | trimmed.Contains("[[ -t 1 ]]") ||
197 | trimmed.Contains("test -t 1") ||
198 | trimmed.Contains("isatty") ||
199 | trimmed.Contains("stdin is not a TTY") then
200 | "# TTY check disabled for .app bundle execution: " + line
201 |
202 | // Force non-interactive mode for package managers
203 | elif trimmed.StartsWith("brew install") && not (trimmed.Contains("--help")) then
204 | // Add --quiet flag to reduce output and avoid prompts
205 | if not (trimmed.Contains("--quiet")) then
206 | line.Replace("brew install", "brew install --quiet")
207 | else
208 | line
209 | elif trimmed.StartsWith("brew ") && not (trimmed.Contains("--help")) && not (trimmed.Contains("install")) then
210 | // For other brew commands, just ensure they run quietly
211 | if not (trimmed.Contains("--quiet")) && (trimmed.Contains("update") || trimmed.Contains("upgrade")) then
212 | line + " --quiet"
213 | else
214 | line
215 |
216 | // Add --yes flag to apt commands (if any)
217 | elif trimmed.Contains("apt-get ") && not (trimmed.Contains("-y")) && not (trimmed.Contains("--yes")) then
218 | line.Replace("apt-get ", "apt-get -y ")
219 |
220 | // Disable Homebrew's automatic checking
221 | elif trimmed.Contains("command -v brew") then
222 | line + " 2>/dev/null"
223 |
224 | // Handle common non-interactive mode messages
225 | elif trimmed.Contains("Running in non-interactive mode") then
226 | "# " + line + " # Suppressed for GUI execution"
227 |
228 | else
229 | line)
230 |
231 | // Add comprehensive non-interactive environment setup at the beginning
232 | let header = """#!/bin/bash
233 | # Force non-interactive mode for GUI execution
234 | export TERM=${TERM:-xterm-256color}
235 | export DEBIAN_FRONTEND=noninteractive
236 | export CI=true
237 | export NONINTERACTIVE=0
238 | export FORCE_NONINTERACTIVE=0
239 | export HOMEBREW_NO_ENV_HINTS=1
240 | export HOMEBREW_NO_INSTALL_CLEANUP=1
241 | export HOMEBREW_NO_AUTO_UPDATE=1
242 | export HOMEBREW_NO_ANALYTICS=1
243 | export HOMEBREW_NO_INSECURE_REDIRECT=1
244 |
245 | # Override TTY-related functions and commands that cause issues
246 | function tty() {
247 | return 1 # Always return "not a TTY"
248 | }
249 |
250 | function isatty() {
251 | return 1 # Always return "not a TTY"
252 | }
253 |
254 | # Redirect stdin from /dev/null to ensure non-interactive behavior
255 | exec < /dev/null
256 |
257 | # Set bash options for non-interactive execution
258 | set +o posix # Disable POSIX mode which can cause TTY issues
259 | set +m # Disable job control
260 |
261 | """
262 |
263 | // Combine header with processed script content
264 | let processedScript = String.Join("\n", processedLines)
265 |
266 | // If script already has a shebang, replace it; otherwise add header
267 | if processedScript.StartsWith("#!") then
268 | let firstNewLine = processedScript.IndexOf('\n')
269 | if firstNewLine > 0 then
270 | header + processedScript.Substring(firstNewLine + 1)
271 | else
272 | header + processedScript
273 | else
274 | header + processedScript
275 |
276 | // Function to check if a script needs elevation (contains sudo, $ESCALATION_TOOL, etc.)
277 | let needsElevation (scriptContent: string) : bool =
278 | scriptContent.Contains("$ESCALATION_TOOL")
279 | || scriptContent.Contains("${ESCALATION_TOOL}")
280 |
281 | let getEmbeddedResource (resourcePath: string) : string option =
282 | try
283 | // How F# handles embedded resources:
284 | // 1. Resource names are case-sensitive.
285 | // 2. They use the format "Namespace.Folder.FileName" with folders separated by dots.
286 | // 3. Hyphens in the directories are converted to underscores in the resource name.
287 | // 4. Hyphens in the filename are kept as-is.
288 | // For example, "script-common/common-script.sh" becomes "MacUtilGUI.scripts_common.common-script.sh".
289 | // 5. The namespace is typically the assembly name, so we prefix it with "MacUtilGUI.".
290 |
291 | let pathParts = resourcePath.Replace("\\", "/").Split('/')
292 |
293 | let convertedParts =
294 | pathParts
295 | |> Array.mapi (fun i part ->
296 | if i = pathParts.Length - 1 then
297 | // Last part is filename - keep hyphens
298 | part
299 | else
300 | // Directory parts - convert hyphens to underscores
301 | part.Replace("-", "_"))
302 |
303 | let convertedPath = String.Join(".", convertedParts)
304 | let resourceName = sprintf "MacUtilGUI.%s" convertedPath
305 |
306 | printfn "DEBUG: Resource path '%s' -> '%s'" resourcePath resourceName
307 |
308 | use stream = assembly.GetManifestResourceStream(resourceName)
309 |
310 | if stream <> null then
311 | use reader = new StreamReader(stream)
312 | Some(reader.ReadToEnd())
313 | else
314 | printfn "Resource not found: %s" resourceName
315 | None
316 | with ex ->
317 | printfn "Error reading embedded resource %s: %s" resourcePath ex.Message
318 | None
319 |
320 | let listEmbeddedResources () =
321 | let resourceNames = assembly.GetManifestResourceNames()
322 | printfn "Available embedded resources:"
323 |
324 | for name in resourceNames do
325 | printfn " %s" name
326 |
327 | let scriptsBasePath = ""
328 |
329 | let loadScriptsFromDirectory (directoryPath: string) : ScriptCategory list =
330 | let mutable categories = []
331 |
332 | try
333 | let tabDataPath = sprintf "%s/tab_data.toml" directoryPath
334 |
335 | match getEmbeddedResource tabDataPath with
336 | | Some content ->
337 | try
338 | let tomlDoc = Toml.Parse(content)
339 |
340 | // Parse the TOML as a dynamic table
341 | if tomlDoc.Diagnostics.Count > 0 then
342 | printfn "TOML parsing warnings/errors for %s:" tabDataPath
343 |
344 | for diag in tomlDoc.Diagnostics do
345 | printfn " %s" (diag.ToString())
346 |
347 | let table = tomlDoc.ToModel()
348 |
349 | // Look for 'data' array in the table
350 | if table.ContainsKey("data") then
351 | match table.["data"] with
352 | | :? Tomlyn.Model.TomlTableArray as dataArray ->
353 | for dataGroup in dataArray do
354 | if dataGroup.ContainsKey("name") && dataGroup.ContainsKey("entries") then
355 | let groupName = dataGroup.["name"].ToString()
356 |
357 | match dataGroup.["entries"] with
358 | | :? Tomlyn.Model.TomlTableArray as entriesArray ->
359 | let scripts =
360 | [ for entry in entriesArray do
361 | if
362 | entry.ContainsKey("name")
363 | && entry.ContainsKey("description")
364 | && entry.ContainsKey("script")
365 | then
366 | let name = entry.["name"].ToString()
367 | let description = entry.["description"].ToString()
368 | let script = entry.["script"].ToString()
369 | let fullPath = sprintf "%s/%s" directoryPath script
370 |
371 | yield
372 | { Name = name
373 | Description = description
374 | Script = script
375 | TaskList = "I"
376 | Category = groupName
377 | FullPath = fullPath } ]
378 |
379 | if not scripts.IsEmpty then
380 | let category = { Name = groupName; Scripts = scripts }
381 | categories <- category :: categories
382 | | _ -> ()
383 | | _ -> ()
384 | with ex ->
385 | printfn "Error parsing TOML file %s: %s" tabDataPath ex.Message
386 | | None ->
387 | printfn "ERROR: Embedded resource not found: %s" tabDataPath
388 | printfn "This should not happen if resources are properly embedded!"
389 | with ex ->
390 | printfn "Error loading scripts from %s: %s" directoryPath ex.Message
391 |
392 | categories |> List.rev
393 |
394 | let loadAllScripts () : ScriptCategory list =
395 | let mutable allCategories = []
396 |
397 | try
398 | // Quick check of embedded resources
399 | let resourceNames = assembly.GetManifestResourceNames()
400 |
401 | printfn
402 | "INFO: Found %d embedded resources including common-script.sh: %b"
403 | resourceNames.Length
404 | (resourceNames |> Array.exists (fun name -> name.EndsWith("common-script.sh")))
405 |
406 | let mainTabsPath = "tabs.toml"
407 |
408 | match getEmbeddedResource mainTabsPath with
409 | | Some content ->
410 | let tomlDoc = Toml.Parse(content)
411 | let table = tomlDoc.ToModel()
412 |
413 | if table.ContainsKey("directories") then
414 | match table.["directories"] with
415 | | :? Tomlyn.Model.TomlArray as dirArray ->
416 | for dir in dirArray do
417 | let directory = dir.ToString()
418 | let categories = loadScriptsFromDirectory directory
419 | allCategories <- allCategories @ categories
420 | | _ -> ()
421 | | None ->
422 | printfn "ERROR: Main tabs.toml not found in embedded resources!"
423 | printfn "This should not happen if resources are properly embedded!"
424 | with ex ->
425 | printfn "Error loading scripts: %s" ex.Message
426 |
427 | allCategories
428 |
429 | // Main script execution function with Full Disk Access check
430 | let runScript (scriptInfo: ScriptInfo) (onOutput: string -> unit) (onError: string -> unit) : Task =
431 | Task.Run(fun () ->
432 | try
433 | // First, check if Terminal has Full Disk Access
434 | let hasAccess, accessMessage = checkFullDiskAccess()
435 |
436 | if not hasAccess then
437 | onError "❌ Full Disk Access Required"
438 | onError ""
439 | promptForFullDiskAccess onOutput
440 | onError ""
441 | onError "Click the button below to open Privacy Settings:"
442 |
443 | // Open Privacy Settings automatically
444 | openPrivacySettings()
445 |
446 | -2 // Special exit code for permission issue
447 | else
448 | onOutput ""
449 |
450 | // Get the script content from embedded resources
451 | match getEmbeddedResource scriptInfo.FullPath with
452 | | Some scriptContent ->
453 | // Check if script sources common-script.sh
454 | let needsCommonScript =
455 | scriptContent.Contains(". ../common-script.sh")
456 | || scriptContent.Contains(". ../../common-script.sh")
457 |
458 | let finalScriptContent =
459 | if needsCommonScript then
460 | onOutput "DEBUG: Script needs common-script.sh, checking embedded resources..."
461 |
462 | // Get common script content
463 | match getEmbeddedResource "common-script.sh" with
464 | | Some commonContent ->
465 | onOutput "DEBUG: Successfully found common-script.sh in embedded resources"
466 | // Remove sourcing line and combine scripts
467 | let cleanedScript =
468 | scriptContent
469 | .Replace(". ../common-script.sh", "")
470 | .Replace(". ../../common-script.sh", "")
471 | .Trim()
472 |
473 | // Combine: shebang + common functions + original script (without sourcing)
474 | let shebang = "#!/bin/sh -e\n\n"
475 |
476 | let commonFunctions =
477 | commonContent
478 | .Replace("#!/bin/sh -e", "")
479 | .Replace("# shellcheck disable=SC2034", "")
480 | .Trim()
481 |
482 | sprintf
483 | "%s# Embedded common script functions\n%s\n\n# Original script content\n%s"
484 | shebang
485 | commonFunctions
486 | cleanedScript
487 | | None ->
488 | onError
489 | "Warning: common-script.sh not found in embedded resources, using original script"
490 |
491 | scriptContent
492 | else
493 | scriptContent
494 |
495 | // Preprocess the script for non-TTY execution
496 | let preprocessedScript = preprocessScriptForNonTTY finalScriptContent
497 |
498 | // Check if script needs elevation
499 | if needsElevation finalScriptContent then
500 | onOutput "Script requires administrator privileges..."
501 |
502 | // Replace $ESCALATION_TOOL with empty string since we'll handle elevation via osascript
503 | let cleanedScript =
504 | preprocessedScript
505 | .Replace("$ESCALATION_TOOL ", "")
506 | .Replace("${ESCALATION_TOOL} ", "")
507 | .Replace("sudo ", "")
508 |
509 | // Create a temporary script file for elevation
510 | let tempDir = Path.GetTempPath()
511 | let scriptFileName = Path.GetFileName(scriptInfo.Script)
512 |
513 | let tempFileName =
514 | sprintf "%s_%s" (Guid.NewGuid().ToString("N").Substring(0, 8)) scriptFileName
515 |
516 | let tempFilePath = Path.Combine(tempDir, tempFileName)
517 |
518 | try
519 | // Write script content to temporary file
520 | File.WriteAllText(tempFilePath, cleanedScript)
521 |
522 | // Make the temporary file executable
523 | let chmodStartInfo = ProcessStartInfo()
524 | chmodStartInfo.FileName <- "/bin/chmod"
525 | chmodStartInfo.Arguments <- sprintf "+x \"%s\"" tempFilePath
526 | chmodStartInfo.UseShellExecute <- false
527 | chmodStartInfo.CreateNoWindow <- true
528 | let chmodProc = Process.Start(chmodStartInfo)
529 |
530 | if chmodProc <> null then
531 | chmodProc.WaitForExit()
532 |
533 | onOutput "Prompting for administrator password..."
534 |
535 | // Use osascript to run the script with elevated privileges
536 | // Note: osascript doesn't provide real-time output, so we'll get all output at the end
537 |
538 | // Create a wrapper script that sets comprehensive environment and runs the main script
539 | let wrapperScript =
540 | "#!/bin/bash\n" +
541 | "export TERM=xterm-256color\n" +
542 | "export DEBIAN_FRONTEND=noninteractive\n" +
543 | "export CI=true\n" +
544 | "export NONINTERACTIVE=0\n" +
545 | "export FORCE_NONINTERACTIVE=0\n" +
546 | "export HOMEBREW_NO_ENV_HINTS=1\n" +
547 | "export HOMEBREW_NO_INSTALL_CLEANUP=1\n" +
548 | "export HOMEBREW_NO_AUTO_UPDATE=1\n" +
549 | "export HOMEBREW_NO_ANALYTICS=1\n" +
550 | "export HOMEBREW_NO_INSECURE_REDIRECT=1\n" +
551 | "\n" +
552 | "# Override TTY functions for elevated execution\n" +
553 | "function tty() { return 1; }\n" +
554 | "function isatty() { return 1; }\n" +
555 | "\n" +
556 | "# Redirect stdin from /dev/null\n" +
557 | "exec < /dev/null\n" +
558 | "\n" +
559 | sprintf "exec \"%s\"\n" tempFilePath
560 |
561 | let wrapperPath = tempFilePath + "_wrapper.sh"
562 | File.WriteAllText(wrapperPath, wrapperScript)
563 |
564 | // Make wrapper executable
565 | let chmodWrapperInfo = ProcessStartInfo()
566 | chmodWrapperInfo.FileName <- "/bin/chmod"
567 | chmodWrapperInfo.Arguments <- sprintf "+x \"%s\"" wrapperPath
568 | chmodWrapperInfo.UseShellExecute <- false
569 | chmodWrapperInfo.CreateNoWindow <- true
570 | let chmodWrapperProc = Process.Start(chmodWrapperInfo)
571 | if chmodWrapperProc <> null then
572 | chmodWrapperProc.WaitForExit()
573 |
574 | let osascriptCommand =
575 | sprintf """osascript -e 'do shell script "\"%s\"" with prompt \"MacUtil needs your permission to make changes to your computer\" with administrator privileges'""" wrapperPath
576 |
577 | let startInfo = ProcessStartInfo()
578 | startInfo.FileName <- "/bin/sh"
579 | startInfo.Arguments <- sprintf "-c \"%s\"" osascriptCommand
580 | startInfo.UseShellExecute <- false
581 | startInfo.CreateNoWindow <- true
582 | startInfo.RedirectStandardOutput <- true
583 | startInfo.RedirectStandardError <- true
584 |
585 | let proc = Process.Start(startInfo)
586 |
587 | if proc <> null then
588 | onOutput "Script is running with administrator privileges..."
589 | onOutput "Note: Output will appear when script completes (osascript limitation)"
590 |
591 | // Wait for the process to complete
592 | proc.WaitForExit()
593 |
594 | // Get all output at once (osascript limitation)
595 | let output = proc.StandardOutput.ReadToEnd()
596 | let error = proc.StandardError.ReadToEnd()
597 |
598 | // Display output line by line for better readability
599 | if not (String.IsNullOrEmpty(output)) then
600 | let lines = output.Split([| '\n'; '\r' |], StringSplitOptions.RemoveEmptyEntries)
601 |
602 | for line in lines do
603 | onOutput line
604 |
605 | if not (String.IsNullOrEmpty(error)) then
606 | let lines = error.Split([| '\n'; '\r' |], StringSplitOptions.RemoveEmptyEntries)
607 |
608 | for line in lines do
609 | onError line
610 |
611 | onOutput (sprintf "Script completed: %s (Exit Code: %d)" scriptInfo.Name proc.ExitCode)
612 | proc.ExitCode
613 | else
614 | let errorMsg = sprintf "Failed to start elevated script: %s" scriptInfo.Name
615 | onError errorMsg
616 | -1
617 | finally
618 | // Clean up temporary files
619 | if File.Exists(tempFilePath) then
620 | try
621 | File.Delete(tempFilePath)
622 | with _ ->
623 | () // Ignore cleanup errors
624 |
625 | let wrapperPath = tempFilePath + "_wrapper.sh"
626 | if File.Exists(wrapperPath) then
627 | try
628 | File.Delete(wrapperPath)
629 | with _ ->
630 | () // Ignore cleanup errors
631 | else
632 | // Script doesn't need elevation, run normally
633 | let tempDir = Path.GetTempPath()
634 | let scriptFileName = Path.GetFileName(scriptInfo.Script)
635 |
636 | let tempFileName =
637 | sprintf "%s_%s" (Guid.NewGuid().ToString("N").Substring(0, 8)) scriptFileName
638 |
639 | let tempFilePath = Path.Combine(tempDir, tempFileName)
640 |
641 | try
642 | // Write script content to temporary file
643 | File.WriteAllText(tempFilePath, preprocessedScript)
644 |
645 | // Make the temporary file executable
646 | let chmodStartInfo = ProcessStartInfo()
647 | chmodStartInfo.FileName <- "/bin/chmod"
648 | chmodStartInfo.Arguments <- sprintf "+x \"%s\"" tempFilePath
649 | chmodStartInfo.UseShellExecute <- false
650 | chmodStartInfo.CreateNoWindow <- true
651 |
652 | let chmodProc = Process.Start(chmodStartInfo)
653 |
654 | if chmodProc <> null then
655 | chmodProc.WaitForExit()
656 |
657 | // Execute the script with real-time output
658 | let startInfo = ProcessStartInfo()
659 | startInfo.FileName <- "/bin/bash"
660 | startInfo.Arguments <- sprintf "\"%s\"" tempFilePath
661 | startInfo.UseShellExecute <- false
662 | startInfo.CreateNoWindow <- true
663 | startInfo.RedirectStandardOutput <- true
664 | startInfo.RedirectStandardError <- true
665 |
666 | // Set comprehensive environment variables to handle non-TTY execution
667 | startInfo.EnvironmentVariables.["TERM"] <- "xterm-256color"
668 | startInfo.EnvironmentVariables.["DEBIAN_FRONTEND"] <- "noninteractive"
669 | startInfo.EnvironmentVariables.["CI"] <- "true"
670 | startInfo.EnvironmentVariables.["NONINTERACTIVE"] <- "1"
671 | startInfo.EnvironmentVariables.["FORCE_NONINTERACTIVE"] <- "1"
672 | startInfo.EnvironmentVariables.["HOMEBREW_NO_ENV_HINTS"] <- "1"
673 | startInfo.EnvironmentVariables.["HOMEBREW_NO_INSTALL_CLEANUP"] <- "1"
674 | startInfo.EnvironmentVariables.["HOMEBREW_NO_AUTO_UPDATE"] <- "1"
675 | startInfo.EnvironmentVariables.["HOMEBREW_NO_ANALYTICS"] <- "1"
676 | startInfo.EnvironmentVariables.["HOMEBREW_NO_INSECURE_REDIRECT"] <- "1"
677 |
678 | let proc = Process.Start(startInfo)
679 |
680 | if proc <> null then
681 | // Set up async reading of output streams
682 | let outputBuilder = StringBuilder()
683 | let errorBuilder = StringBuilder()
684 |
685 | // Handle output data received events
686 | proc.OutputDataReceived.Add(fun args ->
687 | if not (String.IsNullOrEmpty(args.Data)) then
688 | outputBuilder.AppendLine(args.Data) |> ignore
689 | onOutput args.Data)
690 |
691 | proc.ErrorDataReceived.Add(fun args ->
692 | if not (String.IsNullOrEmpty(args.Data)) then
693 | errorBuilder.AppendLine(args.Data) |> ignore
694 | onError args.Data)
695 |
696 | // Start async reading
697 | proc.BeginOutputReadLine()
698 | proc.BeginErrorReadLine()
699 |
700 | // Wait for the process to complete
701 | proc.WaitForExit()
702 |
703 | onOutput (sprintf "Script completed: %s (Exit Code: %d)" scriptInfo.Name proc.ExitCode)
704 | proc.ExitCode
705 | else
706 | let errorMsg = sprintf "Failed to start script: %s" scriptInfo.Name
707 | onError errorMsg
708 | -1
709 | finally
710 | // Clean up temporary file
711 | if File.Exists(tempFilePath) then
712 | try
713 | File.Delete(tempFilePath)
714 | with _ ->
715 | () // Ignore cleanup errors
716 | | None ->
717 | let errorMsg =
718 | sprintf "Script content not found in embedded resources: %s" scriptInfo.FullPath
719 |
720 | onError errorMsg
721 | -1
722 | with ex ->
723 | let errorMsg = sprintf "Error running script %s: %s" scriptInfo.Name ex.Message
724 | onError errorMsg
725 | -1)
726 |
727 | // Helper function to handle Full Disk Access check and notification
728 | let private handleFullDiskAccessCheck (observer: IObserver>) : bool =
729 | let hasAccess, accessMessage = checkFullDiskAccess()
730 |
731 | if not hasAccess then
732 | observer.OnNext(Choice2Of3 "❌ Full Disk Access Required")
733 | observer.OnNext(Choice2Of3 "")
734 | promptForFullDiskAccess (fun msg -> observer.OnNext(Choice2Of3 msg))
735 | observer.OnNext(Choice2Of3 "")
736 | observer.OnNext(Choice2Of3 "Click the button below to open Privacy Settings:")
737 |
738 | // Open Privacy Settings automatically
739 | openPrivacySettings()
740 |
741 | observer.OnNext(Choice3Of3 -2) // Special exit code for permission issue
742 | observer.OnCompleted()
743 | false
744 | else
745 | observer.OnNext(Choice1Of3 "✅ Terminal has Full Disk Access - proceeding with script execution")
746 | observer.OnNext(Choice1Of3 "")
747 | true
748 |
749 | // Helper function to resolve script content with common script handling
750 | let private resolveScriptContent (scriptInfo: ScriptInfo) (observer: IObserver>) : string option =
751 | match getEmbeddedResource scriptInfo.FullPath with
752 | | Some scriptContent ->
753 | // Check if script sources common-script.sh
754 | let needsCommonScript =
755 | scriptContent.Contains(". ../common-script.sh")
756 | || scriptContent.Contains(". ../../common-script.sh")
757 |
758 | if needsCommonScript then
759 | observer.OnNext(Choice1Of3 "DEBUG: Script needs common-script.sh, checking embedded resources...")
760 |
761 | // Get common script content
762 | match getEmbeddedResource "common-script.sh" with
763 | | Some commonContent ->
764 | observer.OnNext(Choice1Of3 "DEBUG: Successfully found common-script.sh in embedded resources")
765 |
766 | // Remove sourcing line and combine scripts
767 | let cleanedScript =
768 | scriptContent
769 | .Replace(". ../common-script.sh", "")
770 | .Replace(". ../../common-script.sh", "")
771 | .Trim()
772 |
773 | // Combine: shebang + common functions + original script (without sourcing)
774 | let shebang = "#!/bin/sh -e\n\n"
775 | let commonFunctions =
776 | commonContent
777 | .Replace("#!/bin/sh -e", "")
778 | .Replace("# shellcheck disable=SC2034", "")
779 | .Trim()
780 |
781 | let combinedScript =
782 | shebang + "# Embedded common script functions\n" +
783 | commonFunctions + "\n\n# Original script content\n" +
784 | cleanedScript
785 |
786 | Some combinedScript
787 | | None ->
788 | observer.OnNext(Choice2Of3 "Warning: common-script.sh not found in embedded resources, using original script")
789 | Some scriptContent
790 | else
791 | Some scriptContent
792 | | None ->
793 | let errorMsg = sprintf "Script content not found in embedded resources: %s" scriptInfo.FullPath
794 | observer.OnNext(Choice2Of3 errorMsg)
795 | observer.OnCompleted()
796 | None
797 |
798 | // Helper function to setup environment variables for non-TTY execution
799 | let private setupEnvironmentVariables (startInfo: ProcessStartInfo) : unit =
800 | startInfo.EnvironmentVariables.["TERM"] <- "xterm-256color"
801 | startInfo.EnvironmentVariables.["DEBIAN_FRONTEND"] <- "noninteractive"
802 | startInfo.EnvironmentVariables.["CI"] <- "true"
803 | startInfo.EnvironmentVariables.["HOMEBREW_NO_ENV_HINTS"] <- "1"
804 | startInfo.EnvironmentVariables.["HOMEBREW_NO_INSTALL_CLEANUP"] <- "1"
805 | startInfo.EnvironmentVariables.["HOMEBREW_NO_AUTO_UPDATE"] <- "1"
806 | startInfo.EnvironmentVariables.["HOMEBREW_NO_ANALYTICS"] <- "1"
807 | startInfo.EnvironmentVariables.["HOMEBREW_NO_INSECURE_REDIRECT"] <- "1"
808 | startInfo.EnvironmentVariables.["NONINTERACTIVE"] <- "1"
809 | startInfo.EnvironmentVariables.["FORCE_NONINTERACTIVE"] <- "1"
810 |
811 | // Helper function to execute script and handle output
812 | let private executeScript (tempFilePath: string) (scriptInfo: ScriptInfo) (observer: IObserver>) : unit =
813 | // Make the temporary file executable
814 | let chmodStartInfo = ProcessStartInfo()
815 | chmodStartInfo.FileName <- "/bin/chmod"
816 | chmodStartInfo.Arguments <- sprintf "+x \"%s\"" tempFilePath
817 | chmodStartInfo.UseShellExecute <- false
818 | chmodStartInfo.CreateNoWindow <- true
819 |
820 | let chmodProc = Process.Start(chmodStartInfo)
821 | if chmodProc <> null then
822 | chmodProc.WaitForExit()
823 |
824 | // Execute the script with real-time output
825 | let startInfo = ProcessStartInfo()
826 | startInfo.FileName <- "/bin/bash"
827 | startInfo.Arguments <- sprintf "\"%s\"" tempFilePath
828 | startInfo.UseShellExecute <- false
829 | startInfo.CreateNoWindow <- true
830 | startInfo.RedirectStandardOutput <- true
831 | startInfo.RedirectStandardError <- true
832 |
833 | // Set comprehensive environment variables to handle non-TTY execution
834 | setupEnvironmentVariables startInfo
835 |
836 | let proc = Process.Start(startInfo)
837 |
838 | match proc with
839 | | null ->
840 | let errorMsg = sprintf "Failed to start script: %s" scriptInfo.Name
841 | observer.OnNext(Choice2Of3 errorMsg)
842 | observer.OnCompleted()
843 | | _ ->
844 | // Handle output data received events
845 | proc.OutputDataReceived.Add(fun args ->
846 | if not (String.IsNullOrEmpty(args.Data)) then
847 | observer.OnNext(Choice1Of3 args.Data))
848 |
849 | proc.ErrorDataReceived.Add(fun args ->
850 | if not (String.IsNullOrEmpty(args.Data)) then
851 | observer.OnNext(Choice2Of3 args.Data))
852 |
853 | // Start async reading
854 | proc.BeginOutputReadLine()
855 | proc.BeginErrorReadLine()
856 |
857 | // Wait for the process to complete
858 | proc.WaitForExit()
859 |
860 | observer.OnNext(Choice3Of3 proc.ExitCode)
861 | observer.OnCompleted()
862 |
863 | // Helper function to clean up temporary files
864 | let private cleanupTempFile (tempFilePath: string) : unit =
865 | if File.Exists(tempFilePath) then
866 | try
867 | File.Delete(tempFilePath)
868 | with _ ->
869 | () // Ignore cleanup errors
870 |
871 | // Alternative version that returns an IObservable for reactive programming - simplified
872 | let runScriptObservable (scriptInfo: ScriptInfo) : IObservable> =
873 | { new IObservable> with
874 | member __.Subscribe(observer) =
875 | let task =
876 | Task.Run(fun () ->
877 | try
878 | // Step 1: Check Full Disk Access (early return if failed)
879 | if not (handleFullDiskAccessCheck observer) then
880 | () // Already handled in the helper function
881 | else
882 | // Step 2: Resolve script content (early return if failed)
883 | match resolveScriptContent scriptInfo observer with
884 | | None -> () // Already handled in the helper function
885 | | Some finalScriptContent ->
886 | // Step 3: Preprocess the script for non-TTY execution
887 | let preprocessedScript = preprocessScriptForNonTTY finalScriptContent
888 |
889 | // Step 4: Create temporary file and execute
890 | let tempDir = Path.GetTempPath()
891 | let scriptFileName = Path.GetFileName(scriptInfo.Script)
892 | let tempFileName = sprintf "%s_%s" (Guid.NewGuid().ToString("N").Substring(0, 8)) scriptFileName
893 | let tempFilePath = Path.Combine(tempDir, tempFileName)
894 |
895 | try
896 | // Write script content to temporary file
897 | File.WriteAllText(tempFilePath, preprocessedScript)
898 |
899 | // Execute the script
900 | executeScript tempFilePath scriptInfo observer
901 | finally
902 | // Clean up temporary file
903 | cleanupTempFile tempFilePath
904 | with ex ->
905 | let errorMsg = sprintf "Error running script %s: %s" scriptInfo.Name ex.Message
906 | observer.OnNext(Choice2Of3 errorMsg)
907 | observer.OnCompleted())
908 |
909 | { new IDisposable with
910 | member __.Dispose() = () } }
911 |
--------------------------------------------------------------------------------