├── .clang-format ├── .editorconfig ├── .github ├── FUNDING.yml ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .markdownlint.json ├── .prettierrc.json ├── .swift-version ├── .swiftlint.yml ├── .vscode ├── .gitignore └── settings.json ├── LICENSE.md ├── Makefile ├── NEWS.md ├── README.md ├── SECURITY.md ├── docs └── screenshot.png ├── make-package.sh ├── resources ├── truewidget-logo-rev1.svg ├── truewidget-logo-rev2.svg ├── truewidget-logo-rev3.svg ├── truewidget-logo.icns └── truewidget-menu.svg ├── scripts ├── codesign.sh ├── get-codesign-identity.sh └── update_version.py ├── src ├── .gitignore ├── Helper │ ├── Extensions │ │ └── String+Extensions.swift │ ├── Helper.entitlements │ ├── HelperProtocol.swift │ ├── Info.plist │ ├── TopCommand.swift │ └── main.swift ├── Makefile ├── TrueWidget │ ├── .gitignore │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── clear.imageset │ │ │ ├── Contents.json │ │ │ └── clear.svg │ │ └── menu.imageset │ │ │ ├── Contents.json │ │ │ └── menu.svg │ ├── Info.plist.in │ ├── TrueWidget.entitlements │ ├── TrueWidget.provisionprofile │ ├── app.icns │ └── swift │ │ ├── CodableAppStorage.swift │ │ ├── DisplayMonitor.swift │ │ ├── Extensions │ │ ├── Color+Extensions.swift │ │ ├── String+Extensions.swift │ │ ├── Toggle+Extensions.swift │ │ └── View+Extensions.swift │ │ ├── HelperClient.swift │ │ ├── Notifications.swift │ │ ├── OpenAtLogin.swift │ │ ├── Relauncher.swift │ │ ├── TrueWidgetApp.swift │ │ ├── Updater.swift │ │ ├── UserSettings.swift │ │ ├── Views │ │ ├── BundlePickerView.swift │ │ ├── ConditionalModifier.swift │ │ ├── ContentView.swift │ │ ├── DateStylePicker.swift │ │ ├── DoubleTextField.swift │ │ ├── IntTextField.swift │ │ ├── Main │ │ │ ├── CompactCPUUsageView.swift │ │ │ ├── CompactTimeView.swift │ │ │ ├── CompactView.swift │ │ │ ├── MainBundleView.swift │ │ │ ├── MainCPUUsageView.swift │ │ │ ├── MainOperatingSystemView.swift │ │ │ ├── MainTimeView.swift │ │ │ └── MainXcodeView.swift │ │ ├── MouseInsideModifier.swift │ │ ├── Settings │ │ │ ├── SettingsActionView.swift │ │ │ ├── SettingsBundleView.swift │ │ │ ├── SettingsCPUUsageView.swift │ │ │ ├── SettingsCompactView.swift │ │ │ ├── SettingsMainView.swift │ │ │ ├── SettingsOperatingSystemView.swift │ │ │ ├── SettingsTimeView.swift │ │ │ ├── SettingsUpdateView.swift │ │ │ └── SettingsXcodeView.swift │ │ ├── SettingsView.swift │ │ └── TimeZonePickerView.swift │ │ ├── WidgetSource │ │ ├── Bundle.swift │ │ ├── CPUUsage.swift │ │ ├── OperatingSystem.swift │ │ ├── Time.swift │ │ ├── WidgetSource.swift │ │ └── Xcode.swift │ │ └── WindowPositionManager.swift ├── org.sparkle-project.Downloader.entitlements ├── project-base.yml ├── project-with-codesign.yml ├── project-without-codesign.yml └── run-xcodegen.sh ├── tools └── clean-launch-services-database │ ├── .gitignore │ ├── Makefile │ ├── Package.swift │ └── Sources │ └── clean-launch-services-database │ ├── config.swift │ └── main.swift └── version /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | ColumnLimit: 0 3 | DerivePointerBinding: true 4 | AllowShortIfStatementsOnASingleLine: true 5 | AllowShortBlocksOnASingleLine: true 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | 11 | [Makefile] 12 | indent_style = tab 13 | 14 | [*.json] 15 | indent_size = 4 16 | 17 | [*.jsonc] 18 | indent_size = 4 19 | 20 | [*.md] 21 | indent_size = 4 22 | 23 | [*.sh] 24 | indent_size = 4 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [tekezo] 2 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 60 5 | 6 | # Number of days of inactivity before a stale Issue or Pull Request is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: 7 9 | 10 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 11 | exemptLabels: 12 | - pinned 13 | - security 14 | - regression 15 | 16 | # Label to use when marking an issue as stale 17 | staleLabel: stale 18 | 19 | # Comment to post when marking as stale. Set to `false` to disable 20 | markComment: > 21 | This issue has been automatically marked as stale because it has not had 22 | recent activity. It will be closed if no further activity occurs. Thank you 23 | for your contributions. 24 | 25 | # Limit to only `issues` or `pulls` 26 | only: issues 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: TrueWidget CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: macos-15 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 1 17 | submodules: recursive 18 | - name: brew install 19 | run: brew install xcodegen 20 | - name: package 21 | run: make clean build 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | build_xcode/ 4 | *.dmg 5 | /tmp/ 6 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD007": { 3 | "indent": 4 4 | }, 5 | "MD013": false, 6 | "MD026": { 7 | "punctuation": ".,;:!。,;:!" 8 | }, 9 | "MD030": { 10 | "ul_single": 3, 11 | "ul_multi": 3, 12 | "ol_single": 2, 13 | "ol_multi": 2 14 | }, 15 | "MD033": { 16 | "allowed_elements": ["br", "img", "table", "tbody", "tr", "td"] 17 | }, 18 | "MD041": false 19 | } 20 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | 6 | "overrides": [ 7 | { 8 | "files": "*.yml", 9 | "options": { 10 | "tabWidth": 2 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.5 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - opening_brace 3 | - trailing_comma 4 | 5 | excluded: 6 | - '**/build' 7 | - '**/vendor' 8 | 9 | # 10 | # parameters 11 | # 12 | 13 | cyclomatic_complexity: 14 | warning: 100 15 | error: 150 16 | 17 | file_length: 18 | warning: 2000 19 | error: 3000 20 | 21 | function_body_length: 22 | warning: 1200 23 | error: 1500 24 | 25 | function_parameter_count: 26 | warning: 10 27 | error: 20 28 | 29 | identifier_name: 30 | max_length: 31 | warning: 100 32 | min_length: 33 | warning: 1 34 | 35 | large_tuple: 36 | warning: 5 37 | 38 | line_length: 39 | warning: 1000 40 | error: 2000 41 | 42 | nesting: 43 | type_level: 44 | warning: 2 45 | 46 | type_body_length: 47 | warning: 1200 48 | error: 1500 49 | -------------------------------------------------------------------------------- /.vscode/.gitignore: -------------------------------------------------------------------------------- 1 | /browse.vc.db* -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | ".vscode/browse.*": true, 4 | "**/.DS_Store": true, 5 | "**/.git": true, 6 | "**/*.dmg": true, 7 | "**/build_xcode/": true, 8 | "**/build/": true, 9 | "tmp/": true 10 | }, 11 | "files.watcherExclude": { 12 | "**/build_xcode/**": true, 13 | "**/build/**": true, 14 | "**/tmp/**": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = `head -n 1 version` 2 | 3 | all: 4 | $(MAKE) gitclean 5 | $(MAKE) clean 6 | ./make-package.sh 7 | $(MAKE) clean-launch-services-database 8 | 9 | build: 10 | $(MAKE) -C src 11 | 12 | clean: 13 | $(MAKE) -C src clean 14 | rm -f *.dmg 15 | 16 | clean-launch-services-database: 17 | $(MAKE) -C tools/clean-launch-services-database 18 | 19 | gitclean: 20 | git clean -f -x -d 21 | 22 | notarize: 23 | xcrun notarytool \ 24 | submit TrueWidget-$(VERSION).dmg \ 25 | --keychain-profile "pqrs.org notarization" \ 26 | --wait 27 | $(MAKE) staple 28 | say "notarization completed" 29 | 30 | staple: 31 | xcrun stapler staple TrueWidget-$(VERSION).dmg 32 | 33 | check-staple: 34 | @xcrun stapler validate TrueWidget-$(VERSION).dmg 35 | 36 | swift-format: 37 | $(MAKE) -C src swift-format 38 | 39 | swiftlint: 40 | swiftlint 41 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## TrueWidget 2.3.0 4 | 5 | - [📦 Download](https://github.com/pqrs-org/TrueWidget/releases/download/v2.3.0/TrueWidget-2.3.0.dmg) 6 | - 📅 Release date 7 | - Apr 29, 2025 8 | - ✨ New Features 9 | - The date display format can now be changed. You can choose between RFC 3339 and the current locale's formats. 10 | 11 | ## TrueWidget 2.2.0 12 | 13 | - [📦 Download](https://github.com/pqrs-org/TrueWidget/releases/download/v2.2.0/TrueWidget-2.2.0.dmg) 14 | - 📅 Release date 15 | - Mar 31, 2025 16 | - ✨ New Features 17 | - Added a feature to show system uptime. 18 | - Added a feature to show awake time. 19 | - Added `Auto compact mode`, which automatically switches to compact mode when there is only one display. 20 | - Added the following settings to the widget position configuration: 21 | - Allow overlapping with Dock 22 | - Window level (z-order) 23 | - Offset X 24 | - Offset Y 25 | - ⚡️ Improvements 26 | - Added the ability to specify the font size for seconds in the time display. 27 | - Added local date to compact mode. 28 | 29 | ## TrueWidget 2.1.0 30 | 31 | - [📦 Download](https://github.com/pqrs-org/TrueWidget/releases/download/v2.1.0/TrueWidget-2.1.0.dmg) 32 | - 📅 Release date 33 | - Jan 17, 2025 34 | - ✨ New Features 35 | - Added a feature to display the Apple Account. 36 | 37 | ## TrueWidget 2.0.0 38 | 39 | - [📦 Download](https://github.com/pqrs-org/TrueWidget/releases/download/v2.0.0/TrueWidget-2.0.0.dmg) 40 | - 📅 Release date 41 | - Jan 15, 2025 42 | - 💥 Breaking changes 43 | - macOS 11 and macOS 12 are no longer supported. 44 | - ✨ New Features 45 | - Added a compact display mode. 46 |
47 | compact 48 |
49 |

50 |
51 | menu 52 |
53 |

54 |
55 | settings compact 56 |
57 | - Added a feature to display the other application's version. 58 |
59 | application versions 60 |
61 |

62 |
63 | settings app 64 |
65 | - ⚡️ Improvements 66 | - Migrated to the SwiftUI life cycle. 67 | - Sparkle Framework has been updated. 68 | 69 | ## TrueWidget 1.6.0 70 | 71 | - [📦 Download](https://github.com/pqrs-org/TrueWidget/releases/download/v1.6.0/TrueWidget-1.6.0.dmg) 72 | - 📅 Release date 73 | - Nov 15, 2023 74 | - ⚡️ Improvements 75 | - Updated the app icon. 76 | - Reduced memory usage of TrueWidget Helper. 77 | - Sparkle Framework has been updated. 78 | 79 | ## TrueWidget 1.4.0 80 | 81 | - [📦 Download](https://github.com/pqrs-org/TrueWidget/releases/download/v1.4.0/TrueWidget-1.4.0.dmg) 82 | - 📅 Release date 83 | - May 14, 2023 84 | - ✨ New Features 85 | - Changed the widget to fade out on mouseover instead of hiding immediately. 86 | - ⚡️ Improvements 87 | - Enabled the setting of `Open at login` by default. 88 | - Changed to open TrueWidget directly instead of via the helper app when opening at login. 89 | 90 | ## TrueWidget 1.3.0 91 | 92 | - [📦 Download](https://github.com/pqrs-org/TrueWidget/releases/download/v1.3.0/TrueWidget-1.3.0.dmg) 93 | - 📅 Release date 94 | - May 2, 2023 95 | - ✨ New Features 96 | - Added option to show user name. 97 | - ⚡️ Improvements 98 | - Supported the security update version in macOS version display. 99 | - Sparkle Framework has been updated. 100 | 101 | ## TrueWidget 1.2.0 102 | 103 | - [📦 Download](https://github.com/pqrs-org/TrueWidget/releases/download/v1.2.0/TrueWidget-1.2.0.dmg) 104 | - 📅 Release date 105 | - Feb 4, 2023 106 | - 🐛 Bug Fixes 107 | - Fixed an issue that `Open at login` option does not work properly. 108 | - ✨ New Features 109 | - Added option to show time of other time zones. 110 | - Added option to show the root volume name. 111 | 112 | ## TrueWidget 1.1.0 113 | 114 | - [📦 Download](https://github.com/pqrs-org/TrueWidget/releases/download/v1.1.0/TrueWidget-1.1.0.dmg) 115 | - 📅 Release date 116 | - Jan 11, 2023 117 | - ✨ New Features 118 | - Added option to show local date. 119 | - Added option to show processes using CPU. 120 | - Added option to show Xcode bundle path. 121 | 122 | ## TrueWidget 1.0.0 123 | 124 | - [📦 Download](https://github.com/pqrs-org/TrueWidget/releases/download/v1.0.0/TrueWidget-1.0.0.dmg) 125 | - 📅 Release date 126 | - Dec 4, 2022 127 | - ✨ New Features 128 | - First release. 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/pqrs-org/TrueWidget/workflows/TrueWidget%20CI/badge.svg)](https://github.com/pqrs-org/TrueWidget/actions) 2 | [![License](https://img.shields.io/badge/license-Public%20Domain-blue.svg)](https://github.com/pqrs-org/TrueWidget/blob/main/LICENSE.md) 3 | 4 | # TrueWidget 5 | 6 | TrueWidget displays macOS version, CPU usage and local time on screen at all times. 7 | 8 | ![screenshot](docs/screenshot.png) 9 | 10 | The advantages of this application are as follows: 11 | 12 | - Check macOS version and host name at a glance when using multiple versions of macOS on your Mac. 13 | - The CPU usage can be monitored not only by instantaneous usage, which can vary widely, but also by a moving average, which is less likely to be blurred, to determine recent trends. 14 | - The local time can be displayed in a size that is easy to read unlike the time on the menu bar, which is not legible when using high resolution. 15 | 16 | ## Web pages 17 | 18 | 19 | 20 | ## System requirements 21 | 22 | macOS 13 Ventura or later 23 | 24 | ## How to build 25 | 26 | System Requirements: 27 | 28 | - macOS 15.0+ 29 | - Xcode 16.2+ 30 | - Command Line Tools for Xcode 31 | - [XcodeGen](https://github.com/yonaskolb/XcodeGen) 32 | - [create-dmg](https://github.com/sindresorhus/create-dmg) 33 | 34 | ### Steps 35 | 36 | 1. Get source code by executing a following command in Terminal.app. 37 | 38 | ```shell 39 | git clone --depth 1 https://github.com/pqrs-org/TrueWidget.git 40 | cd TrueWidget 41 | git submodule update --init --recursive --depth 1 42 | ``` 43 | 44 | 2. Find your codesign identity if you have one.
45 | (Skip this step if you don't have your codesign identity.) 46 | 47 | ```shell 48 | security find-identity -p codesigning -v | grep 'Developer ID Application' 49 | ``` 50 | 51 | The result is as follows. 52 | 53 | ```text 54 | 1) 8D660191481C98F5C56630847A6C39D95C166F22 "Developer ID Application: Fumihiko Takayama (G43BCU2T37)" 55 | ``` 56 | 57 | Your codesign identity is `8D660191481C98F5C56630847A6C39D95C166F22` in the above case. 58 | 59 | 3. Set environment variable to use your codesign identity.
60 | (Skip this step if you don't have your codesign identity.) 61 | 62 | ```shell 63 | export PQRS_ORG_CODE_SIGN_IDENTITY=8D660191481C98F5C56630847A6C39D95C166F22 64 | ``` 65 | 66 | 4. Build a package by executing a following command in Terminal.app. 67 | 68 | ```shell 69 | cd TrueWidget 70 | make clean all 71 | ``` 72 | 73 | Then, TrueWidget-VERSION.dmg has been created in the current directory. 74 | It's a distributable package. 75 | 76 | Note: If you don't have codesign identity, the dmg works only on your machine. 77 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please send an email to 6 | 7 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pqrs-org/TrueWidget/939c5b8e5730b499896f4f8fe8ef8603c73b37ca/docs/screenshot.png -------------------------------------------------------------------------------- /make-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -u # forbid undefined variables 4 | set -e # forbid command failure 5 | 6 | # 7 | # Execute make command 8 | # 9 | 10 | cd $(dirname $0) 11 | 12 | version=$(cat version) 13 | 14 | echo "make build" 15 | make build 16 | if [ ${PIPESTATUS[0]} -ne 0 ]; then 17 | exit 99 18 | fi 19 | 20 | # 21 | # Create dmg 22 | # 23 | 24 | dmg=TrueWidget-$version.dmg 25 | 26 | rm -f $dmg 27 | 28 | # create-dmg 29 | if [[ -n "${PQRS_ORG_CODE_SIGN_IDENTITY:-}" ]]; then 30 | # find identity for create-dmg 31 | identity=$(security find-identity -v -p codesigning | grep "${PQRS_ORG_CODE_SIGN_IDENTITY}" | grep -oE '"[^"]+"$' | sed 's|^"||' | sed 's|"$||') 32 | create-dmg --overwrite --identity="$identity" src/build/Release/TrueWidget.app 33 | else 34 | # create-dmg is always failed if codesign identity is not found. 35 | set +e # allow command failure 36 | create-dmg --overwrite src/build/Release/TrueWidget.app 37 | set -e # forbid command failure 38 | fi 39 | 40 | mv "TrueWidget $version.dmg" TrueWidget-$version.dmg 41 | -------------------------------------------------------------------------------- /resources/truewidget-logo-rev1.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 28 | 34 | 35 | 60 | 65 | 66 | 68 | 69 | 71 | image/svg+xml 72 | 74 | 75 | 77 | 78 | 80 | 82 | 84 | 86 | 87 | 88 | 89 | 95 | 109 | 110 | 114 | 118 | 121 | 􀫥 132 | 􀐫 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /resources/truewidget-logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pqrs-org/TrueWidget/939c5b8e5730b499896f4f8fe8ef8603c73b37ca/resources/truewidget-logo.icns -------------------------------------------------------------------------------- /resources/truewidget-menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 60 | 􀫥 71 | 72 | 73 | -------------------------------------------------------------------------------- /scripts/codesign.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -u # forbid undefined variables 4 | set -e # forbid command failure 5 | 6 | readonly PATH=/bin:/sbin:/usr/bin:/usr/sbin 7 | export PATH 8 | 9 | readonly CODE_SIGN_IDENTITY=$(bash $(dirname $0)/get-codesign-identity.sh) 10 | 11 | if [[ -z $CODE_SIGN_IDENTITY ]]; then 12 | echo "Skip codesign" 13 | exit 0 14 | fi 15 | 16 | # 17 | # Define do_codesign 18 | # 19 | 20 | do_codesign() { 21 | echo -ne '\033[31;40m' 22 | 23 | set +e # allow command failure 24 | 25 | local entitlements="" 26 | if [[ -n "$2" ]]; then 27 | entitlements="--entitlements $2" 28 | fi 29 | 30 | codesign \ 31 | --force \ 32 | --options runtime \ 33 | --sign "$CODE_SIGN_IDENTITY" \ 34 | $entitlements \ 35 | "$1" 2>&1 | 36 | grep -v ': replacing existing signature' 37 | 38 | set -e # forbid command failure 39 | 40 | echo -ne '\033[0m' 41 | } 42 | 43 | # 44 | # Run 45 | # 46 | 47 | set +u # allow undefined variables 48 | target_path="$1" 49 | entitlements_path="$2" 50 | set -u # forbid undefined variables 51 | 52 | do_codesign "$target_path" "$entitlements_path" 53 | -------------------------------------------------------------------------------- /scripts/get-codesign-identity.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -u # forbid undefined variables 4 | set -e # forbid command failure 5 | 6 | readonly PATH=/bin:/sbin:/usr/bin:/usr/sbin 7 | export PATH 8 | 9 | if [[ -n "${PQRS_ORG_CODE_SIGN_IDENTITY:-}" ]]; then 10 | if security find-identity -p codesigning -v | grep -q ") $PQRS_ORG_CODE_SIGN_IDENTITY \""; then 11 | echo $PQRS_ORG_CODE_SIGN_IDENTITY 12 | exit 0 13 | fi 14 | fi 15 | 16 | echo 17 | -------------------------------------------------------------------------------- /scripts/update_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | '''Replace @VERSION@''' 4 | 5 | import re 6 | from pathlib import Path 7 | from itertools import chain 8 | 9 | top_directory = Path(__file__).resolve(True).parents[1] 10 | 11 | 12 | def update_version(): 13 | '''Replace @VERSION@''' 14 | 15 | with top_directory.joinpath('version').open(encoding='utf-8') as version_file: 16 | version = version_file.readline().strip() 17 | 18 | for template_file_path in chain(top_directory.rglob('*.hpp.in'), 19 | top_directory.rglob('*.plist.in'), 20 | top_directory.rglob('*.xml.in')): 21 | replaced_file_path = Path( 22 | re.sub(r'\.in$', '', str(template_file_path))) 23 | needs_update = False 24 | 25 | with template_file_path.open('r') as template_file: 26 | template_lines = template_file.readlines() 27 | replaced_lines = [] 28 | 29 | if replaced_file_path.exists(): 30 | with replaced_file_path.open(encoding='utf-8') as replaced_file: 31 | replaced_lines = replaced_file.readlines() 32 | while len(replaced_lines) < len(template_lines): 33 | replaced_lines.append('') 34 | else: 35 | replaced_lines = template_lines 36 | 37 | for index, template_line in enumerate(template_lines): 38 | line = template_line 39 | line = line.replace('@VERSION@', version) 40 | 41 | if replaced_lines[index] != line: 42 | needs_update = True 43 | replaced_lines[index] = line 44 | 45 | if needs_update: 46 | with replaced_file_path.open('w', encoding='utf-8') as replaced_file: 47 | print("Update " + str(replaced_file_path)) 48 | replaced_file.write(''.join(replaced_lines)) 49 | 50 | 51 | if __name__ == "__main__": 52 | update_version() 53 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | /TrueWidget.xcodeproj/ 2 | -------------------------------------------------------------------------------- /src/Helper/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | public func capturedGroups(withRegex regex: NSRegularExpression) -> [String] { 5 | var results = [String]() 6 | 7 | if let match = regex.firstMatch( 8 | in: self, 9 | range: NSRange(self.startIndex..., in: self)) 10 | { 11 | let lastRangeIndex = match.numberOfRanges - 1 12 | guard lastRangeIndex >= 1 else { return results } 13 | 14 | for i in 1...lastRangeIndex { 15 | let capturedGroupIndex = match.range(at: i) 16 | let matchedString = (self as NSString).substring(with: capturedGroupIndex) 17 | results.append(matchedString) 18 | } 19 | } 20 | 21 | return results 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Helper/Helper.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | com.apple.security.app-sandbox 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Helper/HelperProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let helperServiceName = "org.pqrs.TrueWidget.Helper" 4 | 5 | @objc 6 | protocol HelperProtocol { 7 | // 8 | // AppleAccount 9 | // 10 | 11 | func appleAccount(reply: @escaping (String) -> Void) 12 | 13 | // 14 | // BundleVersions 15 | // 16 | 17 | func bundleVersions(paths: [String], reply: @escaping ([String: [String: String]]) -> Void) 18 | 19 | // 20 | // TopCommand 21 | // 22 | 23 | func topCommand(reply: @escaping (Double, [[String: String]]) -> Void) 24 | func stopTopCommand() 25 | } 26 | -------------------------------------------------------------------------------- /src/Helper/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | $(PRODUCT_BUNDLE_IDENTIFIER) 7 | CFBundlePackageType 8 | XPC! 9 | XPCService 10 | 11 | ServiceType 12 | Application 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Helper/TopCommand.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct TopCommandData { 4 | var cpuUsage: Double = 0.0 5 | var processes: [[String: String]] = [] 6 | } 7 | 8 | enum TopCommandError: Error { 9 | case invalidRegexp 10 | } 11 | 12 | func topCommandStream() -> AsyncThrowingStream { 13 | return AsyncThrowingStream { continuation in 14 | guard 15 | // CPU usage: 10.84% user, 8.27% sys, 80.88% idle 16 | let topCPUUsageRegex = try? NSRegularExpression( 17 | pattern: "^CPU usage: ([\\d\\.]+)% user, ([\\d\\.]+)% sys, "), 18 | // %CPU COMMAND 19 | let topProcessesStartRegex = try? NSRegularExpression(pattern: "^%CPU\\s+COMMAND"), 20 | // 21.5 Google Chrome He 21 | let topProcessRegex = try? NSRegularExpression(pattern: "^([\\d\\.]+)\\s+(.+)") 22 | else { 23 | continuation.finish(throwing: TopCommandError.invalidRegexp) 24 | return 25 | } 26 | 27 | let process = Process() 28 | let pipe = Pipe() 29 | 30 | process.launchPath = "/usr/bin/top" 31 | process.arguments = [ 32 | "-stats", "cpu,command", 33 | // infinity loop 34 | "-l", "0", 35 | // 3 processes 36 | "-n", "3", 37 | // 3 second interval 38 | "-s", "3", 39 | ] 40 | process.environment = [ 41 | "LC_ALL": "C" 42 | ] 43 | process.standardOutput = pipe 44 | 45 | let task = Task { 46 | do { 47 | var inProcessesLine = false 48 | var data = TopCommandData() 49 | 50 | for try await line in pipe.fileHandleForReading.bytes.lines { 51 | if Task.isCancelled { 52 | process.terminate() 53 | continuation.finish() 54 | break 55 | } 56 | 57 | // 58 | // Parse CPU usage 59 | // 60 | 61 | let cpuUsages = line.capturedGroups(withRegex: topCPUUsageRegex) 62 | if cpuUsages.count > 0, 63 | let user = Double(cpuUsages[0]), 64 | let sys = Double(cpuUsages[1]) 65 | { 66 | data.cpuUsage = user + sys 67 | } 68 | 69 | // 70 | // Parse processes 71 | // 72 | 73 | if inProcessesLine { 74 | let process = line.capturedGroups(withRegex: topProcessRegex) 75 | if process.count > 0 { 76 | data.processes.append( 77 | [ 78 | "cpu": process[0], 79 | "name": process[1].trimmingCharacters(in: .whitespacesAndNewlines), 80 | ] 81 | ) 82 | 83 | if data.processes.count >= 3 { 84 | continuation.yield(data) 85 | 86 | inProcessesLine = false 87 | data = TopCommandData() 88 | } 89 | } 90 | } 91 | 92 | if topProcessesStartRegex.numberOfMatches( 93 | in: line, range: NSRange(line.startIndex..., in: line)) > 0 94 | { 95 | inProcessesLine = true 96 | } 97 | } 98 | } catch { 99 | continuation.finish(throwing: error) 100 | } 101 | } 102 | 103 | process.terminationHandler = { _ in 104 | continuation.finish() 105 | } 106 | 107 | do { 108 | try process.run() 109 | } catch { 110 | continuation.finish(throwing: error) 111 | } 112 | 113 | continuation.onTermination = { _ in 114 | process.terminate() 115 | task.cancel() 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Helper/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class HelperService: NSObject, NSXPCListenerDelegate, HelperProtocol { 4 | let listener = NSXPCListener.service() 5 | 6 | override init() { 7 | super.init() 8 | listener.delegate = self 9 | } 10 | 11 | func startListening() { 12 | listener.resume() 13 | } 14 | 15 | func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) 16 | -> Bool 17 | { 18 | newConnection.exportedInterface = NSXPCInterface(with: HelperProtocol.self) 19 | newConnection.exportedObject = self 20 | newConnection.resume() 21 | return true 22 | } 23 | 24 | // 25 | // AppleAccount 26 | // 27 | 28 | func appleAccount(reply: @escaping (String) -> Void) { 29 | let account = 30 | (UserDefaults.standard.persistentDomain(forName: "MobileMeAccounts")?["Accounts"] 31 | as? [[String: Any]])?.first?["AccountID"] as? String ?? "" 32 | reply(account) 33 | } 34 | 35 | // 36 | // BundleVersions 37 | // 38 | 39 | @objc func bundleVersions( 40 | paths: [String], reply: @escaping ([String: [String: String]]) -> Void 41 | ) { 42 | var versions: [String: [String: String]] = [:] 43 | 44 | for path in paths { 45 | // Once a Bundle instance is created and associated with a url (or more precisely, a path), that instance continues to be reused. 46 | // As a result, even if the version or other information is updated later, outdated information will still be returned. 47 | // Therefore, instead of using Bundle, retrieve the information directly from Info.plist. 48 | let url = URL(fileURLWithPath: path) 49 | let plistPath = 50 | url 51 | .appending(component: "Contents", directoryHint: .isDirectory) 52 | .appending(component: "Info.plist", directoryHint: .notDirectory) 53 | .path 54 | 55 | guard let plistData = FileManager.default.contents(atPath: plistPath), 56 | let plistDict = try? PropertyListSerialization.propertyList( 57 | from: plistData, options: [], format: nil) as? [String: Any], 58 | let version = plistDict["CFBundleShortVersionString"] as? String 59 | ?? plistDict["CFBundleVersion"] as? String 60 | else { 61 | continue 62 | } 63 | 64 | versions[path] = [ 65 | "name": plistDict["CFBundleDisplayName"] as? String 66 | ?? plistDict["CFBundleName"] as? String 67 | ?? url.lastPathComponent, 68 | "version": version, 69 | ] 70 | } 71 | 72 | reply(versions) 73 | } 74 | 75 | // 76 | // TopCommand 77 | // 78 | 79 | private var topCommandTask: Task? 80 | private var topCommandData: TopCommandData = TopCommandData() 81 | 82 | @objc func topCommand(reply: @escaping (Double, [[String: String]]) -> Void) { 83 | if topCommandTask == nil { 84 | topCommandTask = Task { 85 | do { 86 | for try await data in topCommandStream() { 87 | Task { @MainActor in 88 | topCommandData = data 89 | } 90 | } 91 | } catch { 92 | print("error in topCommandStream: \(error)") 93 | stopTopCommand() 94 | } 95 | } 96 | } 97 | 98 | Task { @MainActor in 99 | reply(topCommandData.cpuUsage, topCommandData.processes) 100 | } 101 | } 102 | 103 | @objc func stopTopCommand() { 104 | topCommandTask?.cancel() 105 | topCommandTask = nil 106 | topCommandData = TopCommandData() 107 | } 108 | } 109 | 110 | let service = HelperService() 111 | service.startListening() 112 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | /usr/bin/python3 ../scripts/update_version.py 3 | bash run-xcodegen.sh 4 | xcodebuild -configuration Release -alltargets SYMROOT="$(CURDIR)/build" 5 | # Copy embedded.provisionprofile 6 | cp TrueWidget/TrueWidget.provisionprofile build/Release/TrueWidget.app/Contents/embedded.provisionprofile 7 | $(MAKE) codesign 8 | $(MAKE) -C .. clean-launch-services-database 9 | 10 | clean: purge-swift-package-manager-cache 11 | rm -rf TrueWidget.xcodeproj 12 | rm -rf build 13 | 14 | codesign: 15 | # Helper 16 | bash ../scripts/codesign.sh 'build/Release/TrueWidget.app/Contents/XPCServices/TrueWidget Helper.xpc' $(CURDIR)/Helper/Helper.entitlements 17 | # Sparkle 18 | bash ../scripts/codesign.sh build/Release/TrueWidget.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc 19 | bash ../scripts/codesign.sh build/Release/TrueWidget.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc $(CURDIR)/org.sparkle-project.Downloader.entitlements 20 | bash ../scripts/codesign.sh build/Release/TrueWidget.app/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate 21 | bash ../scripts/codesign.sh build/Release/TrueWidget.app/Contents/Frameworks/Sparkle.framework/Versions/B/Updater.app 22 | bash ../scripts/codesign.sh build/Release/TrueWidget.app/Contents/Frameworks/Sparkle.framework 23 | # TrueWidget 24 | bash ../scripts/codesign.sh build/Release/TrueWidget.app $(CURDIR)/TrueWidget/TrueWidget.entitlements 25 | 26 | purge-swift-package-manager-cache: 27 | rm -rf ~/Library/Developer/Xcode/DerivedData/TrueWidget-* 28 | rm -rf ~/Library/Caches/org.swift.swiftpm/repositories/Sparkle-* 29 | 30 | xcode: 31 | open TrueWidget.xcodeproj 32 | 33 | run: 34 | open build/Release/TrueWidget.app 35 | 36 | swift-format: 37 | find . -name '*.swift' -print0 | xargs -0 swift-format -i 38 | 39 | install: 40 | rsync -a --delete build/Release/TrueWidget.app /Applications 41 | -------------------------------------------------------------------------------- /src/TrueWidget/.gitignore: -------------------------------------------------------------------------------- 1 | /Info.plist 2 | -------------------------------------------------------------------------------- /src/TrueWidget/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/TrueWidget/Assets.xcassets/clear.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "clear.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/TrueWidget/Assets.xcassets/clear.imageset/clear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/TrueWidget/Assets.xcassets/menu.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "menu.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/TrueWidget/Assets.xcassets/menu.imageset/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/TrueWidget/Info.plist.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | app.icns 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | @VERSION@ 21 | CFBundleVersion 22 | @VERSION@ 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSSupportsAutomaticTermination 26 | 27 | NSSupportsSuddenTermination 28 | 29 | LSUIElement 30 | 31 | SUPublicEDKey 32 | WdJ29ySfnKxiloyN7d2q/Q9NRijT1kC7q1b87fRcW8c= 33 | SUEnableAutomaticChecks 34 | 35 | SUScheduledCheckInterval 36 | 0 37 | SUFeedURL 38 | https://appcast.pqrs.org/truewidget-appcast.xml 39 | SUEnableInstallerLauncherService 40 | 41 | SUEnableDownloaderService 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/TrueWidget/TrueWidget.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | com.apple.security.files.user-selected.read-only 10 | 11 | 12 | 13 | com.apple.security.temporary-exception.mach-lookup.global-name 14 | 15 | org.pqrs.TrueWidget-spks 16 | org.pqrs.TrueWidget-spki 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/TrueWidget/TrueWidget.provisionprofile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pqrs-org/TrueWidget/939c5b8e5730b499896f4f8fe8ef8603c73b37ca/src/TrueWidget/TrueWidget.provisionprofile -------------------------------------------------------------------------------- /src/TrueWidget/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pqrs-org/TrueWidget/939c5b8e5730b499896f4f8fe8ef8603c73b37ca/src/TrueWidget/app.icns -------------------------------------------------------------------------------- /src/TrueWidget/swift/CodableAppStorage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @propertyWrapper 4 | struct CodableAppStorage { 5 | private let key: String 6 | private let defaultValue: T 7 | 8 | init(wrappedValue: T, _ key: String) { 9 | self.key = key 10 | self.defaultValue = wrappedValue 11 | } 12 | 13 | var wrappedValue: T { 14 | get { 15 | if let data = UserDefaults.standard.data(forKey: key), 16 | let decoded = try? JSONDecoder().decode(T.self, from: data) 17 | { 18 | return decoded 19 | } 20 | return defaultValue 21 | } 22 | set { 23 | if let data = try? JSONEncoder().encode(newValue) { 24 | UserDefaults.standard.set(data, forKey: key) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/DisplayMonitor.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import SwiftUI 3 | 4 | public class DisplayMonitor: ObservableObject { 5 | @Published var displayCount = 0 6 | 7 | init() { 8 | updateDisplayCount() 9 | CGDisplayRegisterReconfigurationCallback(callback, Unmanaged.passUnretained(self).toOpaque()) 10 | } 11 | 12 | deinit { 13 | CGDisplayRemoveReconfigurationCallback(callback, Unmanaged.passUnretained(self).toOpaque()) 14 | } 15 | 16 | private func updateDisplayCount() { 17 | Task { @MainActor in 18 | var displayCount: UInt32 = 0 19 | var onlineDisplays = [CGDirectDisplayID](repeating: 0, count: 16) 20 | CGGetOnlineDisplayList(UInt32(onlineDisplays.count), &onlineDisplays, &displayCount) 21 | 22 | self.displayCount = Int(displayCount) 23 | } 24 | } 25 | 26 | private let callback: CGDisplayReconfigurationCallBack = { _, flags, userInfo in 27 | if flags.isDisjoint(with: [.addFlag, .removeFlag]) { 28 | guard let opaque = userInfo else { 29 | return 30 | } 31 | 32 | let monitor = 33 | Unmanaged.fromOpaque(opaque).takeUnretainedValue() as DisplayMonitor 34 | monitor.updateDisplayCount() 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Extensions/Color+Extensions.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | 4 | extension Color { 5 | public init(colorString: String) { 6 | var red: Double = 0 7 | var green: Double = 0 8 | var blue: Double = 0 9 | var opacity: Double = 0 10 | 11 | if colorString.hasPrefix("#"), colorString.count == 9 { 12 | // #RRGGBBAA 13 | 14 | if let r = UInt8( 15 | colorString[ 16 | colorString.index( 17 | colorString.startIndex, offsetBy: 1)...colorString.index( 18 | colorString.startIndex, offsetBy: 2)], radix: 16), 19 | let g = UInt8( 20 | colorString[ 21 | colorString.index( 22 | colorString.startIndex, offsetBy: 3)...colorString.index( 23 | colorString.startIndex, offsetBy: 4)], radix: 16), 24 | let b = UInt8( 25 | colorString[ 26 | colorString.index( 27 | colorString.startIndex, offsetBy: 5)...colorString.index( 28 | colorString.startIndex, offsetBy: 6)], radix: 16), 29 | let a = UInt8( 30 | colorString[ 31 | colorString.index( 32 | colorString.startIndex, offsetBy: 7)...colorString.index( 33 | colorString.startIndex, offsetBy: 8)], radix: 16) 34 | { 35 | red = Double(r) / 255 36 | green = Double(g) / 255 37 | blue = Double(b) / 255 38 | opacity = Double(a) / 255 39 | } 40 | 41 | } else { 42 | switch colorString { 43 | // Colors without opacity 44 | case "black": 45 | // Adjust color 46 | red = 0.5 47 | green = 0.5 48 | blue = 0.5 49 | opacity = 1.0 50 | case "blue": 51 | red = 0.0 52 | green = 0.0 53 | blue = 1.0 54 | opacity = 1.0 55 | case "brown": 56 | red = 0.6 57 | green = 0.4 58 | blue = 0.2 59 | opacity = 1.0 60 | case "clear": 61 | red = 0.0 62 | green = 0.0 63 | blue = 0.0 64 | opacity = 0.0 65 | case "cyan": 66 | red = 0.0 67 | green = 1.0 68 | blue = 1.0 69 | opacity = 1.0 70 | case "green": 71 | red = 0.0 72 | green = 1.0 73 | blue = 0.0 74 | opacity = 1.0 75 | case "magenta": 76 | red = 1.0 77 | green = 0.0 78 | blue = 1.0 79 | opacity = 1.0 80 | case "orange": 81 | red = 1.0 82 | green = 0.5 83 | blue = 0.0 84 | opacity = 1.0 85 | case "purple": 86 | red = 0.5 87 | green = 0.0 88 | blue = 0.5 89 | opacity = 1.0 90 | case "red": 91 | red = 1.0 92 | green = 0.0 93 | blue = 0.0 94 | opacity = 1.0 95 | case "white": 96 | red = 1.0 97 | green = 1.0 98 | blue = 1.0 99 | opacity = 1.0 100 | case "yellow": 101 | red = 1.0 102 | green = 1.0 103 | blue = 0.0 104 | opacity = 1.0 105 | 106 | // Colors with opacity 107 | 108 | // black 0.0, 0.0, 0.0 109 | case "black1.0": 110 | red = 0.0 111 | green = 0.0 112 | blue = 0.0 113 | opacity = 1.0 114 | case "black0.8": 115 | red = 0.0 116 | green = 0.0 117 | blue = 0.0 118 | opacity = 0.8 119 | case "black0.6": 120 | red = 0.0 121 | green = 0.0 122 | blue = 0.0 123 | opacity = 0.6 124 | case "black0.4": 125 | red = 0.0 126 | green = 0.0 127 | blue = 0.0 128 | opacity = 0.4 129 | case "black0.2": 130 | red = 0.0 131 | green = 0.0 132 | blue = 0.0 133 | opacity = 0.2 134 | 135 | // gray 0.5, 0.5, 0.5 136 | case "gray1.0": 137 | red = 0.5 138 | green = 0.5 139 | blue = 0.5 140 | opacity = 1.0 141 | case "gray0.8": 142 | red = 0.5 143 | green = 0.5 144 | blue = 0.5 145 | opacity = 0.8 146 | case "gray0.6": 147 | red = 0.5 148 | green = 0.5 149 | blue = 0.5 150 | opacity = 0.6 151 | case "gray0.4": 152 | red = 0.5 153 | green = 0.5 154 | blue = 0.5 155 | opacity = 0.4 156 | case "gray0.2": 157 | red = 0.5 158 | green = 0.5 159 | blue = 0.5 160 | opacity = 0.2 161 | 162 | // silver 0.75, 0.75, 0.75 163 | case "silver1.0": 164 | red = 0.75 165 | green = 0.75 166 | blue = 0.75 167 | opacity = 1.0 168 | case "silver0.8": 169 | red = 0.75 170 | green = 0.75 171 | blue = 0.75 172 | opacity = 0.8 173 | case "silver0.6": 174 | red = 0.75 175 | green = 0.75 176 | blue = 0.75 177 | opacity = 0.6 178 | case "silver0.4": 179 | red = 0.75 180 | green = 0.75 181 | blue = 0.75 182 | opacity = 0.4 183 | case "silver0.2": 184 | red = 0.75 185 | green = 0.75 186 | blue = 0.75 187 | opacity = 0.2 188 | 189 | // white 1.0f, 1.0f, 1.0f 190 | case "white1.0": 191 | red = 1.0 192 | green = 1.0 193 | blue = 1.0 194 | opacity = 1.0 195 | case "white0.8": 196 | red = 1.0 197 | green = 1.0 198 | blue = 1.0 199 | opacity = 0.8 200 | case "white0.6": 201 | red = 1.0 202 | green = 1.0 203 | blue = 1.0 204 | opacity = 0.6 205 | case "white0.4": 206 | red = 1.0 207 | green = 1.0 208 | blue = 1.0 209 | opacity = 0.4 210 | case "white0.2": 211 | red = 1.0 212 | green = 1.0 213 | blue = 1.0 214 | opacity = 0.2 215 | 216 | // maroon 0.5f, 0.0f, 0.0f 217 | case "maroon1.0": 218 | red = 0.5 219 | green = 0.0 220 | blue = 0.0 221 | opacity = 1.0 222 | case "maroon0.8": 223 | red = 0.5 224 | green = 0.0 225 | blue = 0.0 226 | opacity = 0.8 227 | case "maroon0.6": 228 | red = 0.5 229 | green = 0.0 230 | blue = 0.0 231 | opacity = 0.6 232 | case "maroon0.4": 233 | red = 0.5 234 | green = 0.0 235 | blue = 0.0 236 | opacity = 0.4 237 | case "maroon0.2": 238 | red = 0.5 239 | green = 0.0 240 | blue = 0.0 241 | opacity = 0.2 242 | 243 | // red 1.0f, 0.0f, 0.0f 244 | case "red1.0": 245 | red = 1.0 246 | green = 0.0 247 | blue = 0.0 248 | opacity = 1.0 249 | case "red0.8": 250 | red = 1.0 251 | green = 0.0 252 | blue = 0.0 253 | opacity = 0.8 254 | case "red0.6": 255 | red = 1.0 256 | green = 0.0 257 | blue = 0.0 258 | opacity = 0.6 259 | case "red0.4": 260 | red = 1.0 261 | green = 0.0 262 | blue = 0.0 263 | opacity = 0.4 264 | case "red0.2": 265 | red = 1.0 266 | green = 0.0 267 | blue = 0.0 268 | opacity = 0.2 269 | 270 | // olive 0.5f, 0.5f, 0.0f 271 | case "olive1.0": 272 | red = 0.5 273 | green = 0.5 274 | blue = 0.0 275 | opacity = 1.0 276 | case "olive0.8": 277 | red = 0.5 278 | green = 0.5 279 | blue = 0.0 280 | opacity = 0.8 281 | case "olive0.6": 282 | red = 0.5 283 | green = 0.5 284 | blue = 0.0 285 | opacity = 0.6 286 | case "olive0.4": 287 | red = 0.5 288 | green = 0.5 289 | blue = 0.0 290 | opacity = 0.4 291 | case "olive0.2": 292 | red = 0.5 293 | green = 0.5 294 | blue = 0.0 295 | opacity = 0.2 296 | 297 | // yellow 1.0f, 1.0f, 0.0f 298 | case "yellow1.0": 299 | red = 1.0 300 | green = 1.0 301 | blue = 0.0 302 | opacity = 1.0 303 | case "yellow0.8": 304 | red = 1.0 305 | green = 1.0 306 | blue = 0.0 307 | opacity = 0.8 308 | case "yellow0.6": 309 | red = 1.0 310 | green = 1.0 311 | blue = 0.0 312 | opacity = 0.6 313 | case "yellow0.4": 314 | red = 1.0 315 | green = 1.0 316 | blue = 0.0 317 | opacity = 0.4 318 | case "yellow0.2": 319 | red = 1.0 320 | green = 1.0 321 | blue = 0.0 322 | opacity = 0.2 323 | 324 | // green 0.0f, 0.5f, 0.0f 325 | case "green1.0": 326 | red = 0.0 327 | green = 0.5 328 | blue = 0.0 329 | opacity = 1.0 330 | case "green0.8": 331 | red = 0.0 332 | green = 0.5 333 | blue = 0.0 334 | opacity = 0.8 335 | case "green0.6": 336 | red = 0.0 337 | green = 0.5 338 | blue = 0.0 339 | opacity = 0.6 340 | case "green0.4": 341 | red = 0.0 342 | green = 0.5 343 | blue = 0.0 344 | opacity = 0.4 345 | case "green0.2": 346 | red = 0.0 347 | green = 0.5 348 | blue = 0.0 349 | opacity = 0.2 350 | 351 | // lime 0.0f, 1.0f, 0.0f 352 | case "lime1.0": 353 | red = 0.0 354 | green = 1.0 355 | blue = 0.0 356 | opacity = 1.0 357 | case "lime0.8": 358 | red = 0.0 359 | green = 1.0 360 | blue = 0.0 361 | opacity = 0.8 362 | case "lime0.6": 363 | red = 0.0 364 | green = 1.0 365 | blue = 0.0 366 | opacity = 0.6 367 | case "lime0.4": 368 | red = 0.0 369 | green = 1.0 370 | blue = 0.0 371 | opacity = 0.4 372 | case "lime0.2": 373 | red = 0.0 374 | green = 1.0 375 | blue = 0.0 376 | opacity = 0.2 377 | 378 | // teal 0.0f, 0.5f, 0.5f 379 | case "teal1.0": 380 | red = 0.0 381 | green = 0.5 382 | blue = 0.5 383 | opacity = 1.0 384 | case "teal0.8": 385 | red = 0.0 386 | green = 0.5 387 | blue = 0.5 388 | opacity = 0.8 389 | case "teal0.6": 390 | red = 0.0 391 | green = 0.5 392 | blue = 0.5 393 | opacity = 0.6 394 | case "teal0.4": 395 | red = 0.0 396 | green = 0.5 397 | blue = 0.5 398 | opacity = 0.4 399 | case "teal0.2": 400 | red = 0.0 401 | green = 0.5 402 | blue = 0.5 403 | opacity = 0.2 404 | 405 | // aqua 0.0f, 1.0f, 1.0f 406 | case "aqua1.0": 407 | red = 0.0 408 | green = 1.0 409 | blue = 1.0 410 | opacity = 1.0 411 | case "aqua0.8": 412 | red = 0.0 413 | green = 1.0 414 | blue = 1.0 415 | opacity = 0.8 416 | case "aqua0.6": 417 | red = 0.0 418 | green = 1.0 419 | blue = 1.0 420 | opacity = 0.6 421 | case "aqua0.4": 422 | red = 0.0 423 | green = 1.0 424 | blue = 1.0 425 | opacity = 0.4 426 | case "aqua0.2": 427 | red = 0.0 428 | green = 1.0 429 | blue = 1.0 430 | opacity = 0.2 431 | 432 | // navy 0.0f, 0.0f, 0.5f 433 | case "navy1.0": 434 | red = 0.0 435 | green = 0.0 436 | blue = 0.5 437 | opacity = 1.0 438 | case "navy0.8": 439 | red = 0.0 440 | green = 0.0 441 | blue = 0.5 442 | opacity = 0.8 443 | case "navy0.6": 444 | red = 0.0 445 | green = 0.0 446 | blue = 0.5 447 | opacity = 0.6 448 | case "navy0.4": 449 | red = 0.0 450 | green = 0.0 451 | blue = 0.5 452 | opacity = 0.4 453 | case "navy0.2": 454 | red = 0.0 455 | green = 0.0 456 | blue = 0.5 457 | opacity = 0.2 458 | 459 | // blue 0.0f, 0.0f, 1.0f 460 | case "blue1.0": 461 | red = 0.0 462 | green = 0.0 463 | blue = 1.0 464 | opacity = 1.0 465 | case "blue0.8": 466 | red = 0.0 467 | green = 0.0 468 | blue = 1.0 469 | opacity = 0.8 470 | case "blue0.6": 471 | red = 0.0 472 | green = 0.0 473 | blue = 1.0 474 | opacity = 0.6 475 | case "blue0.4": 476 | red = 0.0 477 | green = 0.0 478 | blue = 1.0 479 | opacity = 0.4 480 | case "blue0.2": 481 | red = 0.0 482 | green = 0.0 483 | blue = 1.0 484 | opacity = 0.2 485 | 486 | // purple 0.5f, 0.0f, 0.5f 487 | case "purple1.0": 488 | red = 0.5 489 | green = 0.0 490 | blue = 0.5 491 | opacity = 1.0 492 | case "purple0.8": 493 | red = 0.5 494 | green = 0.0 495 | blue = 0.5 496 | opacity = 0.8 497 | case "purple0.6": 498 | red = 0.5 499 | green = 0.0 500 | blue = 0.5 501 | opacity = 0.6 502 | case "purple0.4": 503 | red = 0.5 504 | green = 0.0 505 | blue = 0.5 506 | opacity = 0.4 507 | case "purple0.2": 508 | red = 0.5 509 | green = 0.0 510 | blue = 0.5 511 | opacity = 0.2 512 | 513 | // fuchsia 1.0f, 0.0f, 1.0f 514 | case "fuchsia1.0": 515 | red = 1.0 516 | green = 0.0 517 | blue = 1.0 518 | opacity = 1.0 519 | case "fuchsia0.8": 520 | red = 1.0 521 | green = 0.0 522 | blue = 1.0 523 | opacity = 0.8 524 | case "fuchsia0.6": 525 | red = 1.0 526 | green = 0.0 527 | blue = 1.0 528 | opacity = 0.6 529 | case "fuchsia0.4": 530 | red = 1.0 531 | green = 0.0 532 | blue = 1.0 533 | opacity = 0.4 534 | case "fuchsia0.2": 535 | red = 1.0 536 | green = 0.0 537 | blue = 1.0 538 | opacity = 0.2 539 | 540 | default: 541 | red = 0.0 542 | green = 0.0 543 | blue = 0.0 544 | opacity = 0.0 545 | } 546 | } 547 | 548 | self.init(.sRGB, red: red, green: green, blue: blue, opacity: opacity) 549 | } 550 | 551 | public var components: (red: CGFloat, green: CGFloat, blue: CGFloat, opacity: CGFloat) { 552 | var r: CGFloat = 0 553 | var g: CGFloat = 0 554 | var b: CGFloat = 0 555 | var o: CGFloat = 0 556 | 557 | NSColor(self).getRed(&r, green: &g, blue: &b, alpha: &o) 558 | 559 | return (r, g, b, o) 560 | } 561 | 562 | public var hexString: String { 563 | let components = components 564 | return String( 565 | format: "#%02x%02x%02x%02x", 566 | UInt8(components.red * 255), 567 | UInt8(components.green * 255), 568 | UInt8(components.blue * 255), 569 | UInt8(components.opacity * 255)) 570 | } 571 | 572 | public static var infoBackground: Color = Color(colorString: "#cff4fcff") 573 | public static var infoForeground: Color = Color(colorString: "#055160ff") 574 | public static var errorBackground: Color = Color(colorString: "#f2dedeff") 575 | public static var errorForeground: Color = Color(colorString: "#a94442ff") 576 | public static var warningBackground: Color = Color(colorString: "#fcf8e3ff") 577 | public static var warningForeground: Color = Color(colorString: "#8a6d3bff") 578 | } 579 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | func leftPadding(toLength: Int, withPad character: Character) -> String { 5 | let stringLength = self.count 6 | if stringLength < toLength { 7 | return String(repeatElement(character, count: toLength - stringLength)) + self 8 | } else { 9 | return String(self.suffix(toLength)) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Extensions/Toggle+Extensions.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Toggle { 4 | func switchToggleStyle() -> some View { 5 | self 6 | .toggleStyle(.switch) 7 | .controlSize(.small) 8 | .font(.body) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Extensions/View+Extensions.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | // https://gist.github.com/importRyan/c668904b0c5442b80b6f38a980595031 5 | func whenHovered(_ mouseIsInside: @escaping (Bool) -> Void) -> some View { 6 | modifier(MouseInsideModifier(mouseIsInside)) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/HelperClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @MainActor 4 | public class HelperClient { 5 | static let shared = HelperClient() 6 | 7 | private var helperConnection: NSXPCConnection? 8 | private var helperProxy: HelperProtocol? 9 | 10 | var proxy: HelperProtocol? { 11 | if helperConnection == nil { 12 | helperConnection = NSXPCConnection(serviceName: helperServiceName) 13 | helperConnection?.remoteObjectInterface = NSXPCInterface(with: HelperProtocol.self) 14 | helperConnection?.resume() 15 | } 16 | 17 | if helperProxy == nil { 18 | helperProxy = helperConnection?.remoteObjectProxy as? HelperProtocol 19 | } 20 | 21 | return helperProxy 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Notifications.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let windowPositionUpdateNeededNotification = NSNotification.Name("windowPositionUpdateNeeded") 4 | let openSettingsNotification = NSNotification.Name("openSettings") 5 | 6 | func postWindowPositionUpdateNeededNotification() { 7 | NotificationCenter.default.post( 8 | name: windowPositionUpdateNeededNotification, 9 | object: nil, 10 | userInfo: nil) 11 | } 12 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/OpenAtLogin.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ServiceManagement 3 | 4 | final class OpenAtLogin: ObservableObject { 5 | static let shared = OpenAtLogin() 6 | 7 | @Published var registered = false 8 | 9 | var error = "" 10 | 11 | init() { 12 | registered = SMAppService.mainApp.status == .enabled 13 | } 14 | 15 | var developmentBinary: Bool { 16 | let bundlePath = Bundle.main.bundlePath 17 | 18 | // Xcode builds 19 | // - /Build/Products/Debug/*.app 20 | // - /Build/Products/Release/*.app 21 | if bundlePath.contains("/Build/") { 22 | return true 23 | } 24 | 25 | // Command line builds 26 | // - /build/Release/*.app 27 | if bundlePath.contains("/build/") { 28 | return true 29 | } 30 | 31 | return false 32 | } 33 | 34 | @MainActor 35 | func update(register: Bool) { 36 | error = "" 37 | 38 | do { 39 | if register { 40 | try SMAppService.mainApp.register() 41 | } else { 42 | // `unregister` throws `Operation not permitted` error in the following cases. 43 | // 44 | // 1. `unregister` is called. 45 | // 2. macOS is restarted to clean up login items entries. 46 | // 3. `unregister` is called again. 47 | // 48 | // So, we ignore the error of `unregister`. 49 | 50 | try? SMAppService.mainApp.unregister() 51 | } 52 | 53 | registered = register 54 | } catch { 55 | self.error = error.localizedDescription 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Relauncher.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Foundation 3 | 4 | struct Relauncher { 5 | static func relaunch() { 6 | print("relaunch") 7 | 8 | let configuration = NSWorkspace.OpenConfiguration() 9 | configuration.createsNewApplicationInstance = true 10 | 11 | NSWorkspace.shared.openApplication( 12 | at: Bundle.main.bundleURL, 13 | configuration: configuration 14 | ) { _, error in 15 | if error == nil { 16 | Task { @MainActor in 17 | NSApplication.shared.terminate(self) 18 | } 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/TrueWidgetApp.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import SettingsAccess 3 | import SwiftUI 4 | 5 | @main 6 | struct TrueWidgetApp: App { 7 | @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 8 | 9 | @StateObject private var userSettings: UserSettings 10 | // Since passing a property of an ObservableObject to MenuBarExtra.isInserted causes a notification loop, the flag must be an independent variable. 11 | @AppStorage("showMenu") var showMenuBarExtra: Bool = true 12 | 13 | private var cancellables = Set() 14 | private let version = 15 | Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "" 16 | 17 | init() { 18 | // 19 | // Initialize properties 20 | // 21 | 22 | let userSettings = UserSettings() 23 | 24 | _userSettings = StateObject(wrappedValue: userSettings) 25 | 26 | appDelegate.userSettings = userSettings 27 | 28 | // 29 | // Register OpenAtLogin 30 | // 31 | 32 | if !OpenAtLogin.shared.developmentBinary { 33 | if !userSettings.initialOpenAtLoginRegistered { 34 | OpenAtLogin.shared.update(register: true) 35 | userSettings.initialOpenAtLoginRegistered = true 36 | } 37 | } 38 | 39 | // 40 | // Additional setups 41 | // 42 | 43 | NSApplication.shared.disableRelaunchOnLogin() 44 | 45 | NotificationCenter.default.addObserver( 46 | forName: NSApplication.didChangeScreenParametersNotification, 47 | object: nil, 48 | queue: .main 49 | ) { _ in 50 | postWindowPositionUpdateNeededNotification() 51 | } 52 | 53 | userSettings.objectWillChange.sink { _ in 54 | postWindowPositionUpdateNeededNotification() 55 | }.store(in: &cancellables) 56 | 57 | Updater.shared.checkForUpdatesInBackground() 58 | } 59 | 60 | @State var selectedOption = "Normal" 61 | 62 | var body: some Scene { 63 | // The main window is manually managed by MainWindowController. 64 | 65 | MenuBarExtra( 66 | isInserted: $showMenuBarExtra, 67 | content: { 68 | Text("TrueWidget \(version)") 69 | 70 | Divider() 71 | 72 | Label("Appearance", systemImage: "rectangle.3.group") 73 | .labelStyle(.titleAndIcon) 74 | 75 | Button( 76 | action: { 77 | userSettings.widgetAppearance = WidgetAppearance.normal.rawValue 78 | }, 79 | label: { 80 | checkmarkLabel( 81 | title: "Normal", 82 | checked: userSettings.widgetAppearance == WidgetAppearance.normal.rawValue) 83 | } 84 | ) 85 | 86 | Button( 87 | action: { 88 | userSettings.widgetAppearance = WidgetAppearance.compact.rawValue 89 | }, 90 | label: { 91 | checkmarkLabel( 92 | title: "Compact", 93 | checked: userSettings.widgetAppearance == WidgetAppearance.compact.rawValue) 94 | } 95 | ) 96 | 97 | Button( 98 | action: { 99 | userSettings.widgetAppearance = WidgetAppearance.autoCompact.rawValue 100 | }, 101 | label: { 102 | checkmarkLabel( 103 | title: "Auto compact", 104 | checked: userSettings.widgetAppearance == WidgetAppearance.autoCompact.rawValue) 105 | } 106 | ) 107 | 108 | Button( 109 | action: { 110 | userSettings.widgetAppearance = WidgetAppearance.hidden.rawValue 111 | }, 112 | label: { 113 | checkmarkLabel( 114 | title: "Hidden", 115 | checked: userSettings.widgetAppearance == WidgetAppearance.hidden.rawValue) 116 | } 117 | ) 118 | 119 | Divider() 120 | 121 | SettingsLink { 122 | Label("Settings...", systemImage: "gearshape") 123 | .labelStyle(.titleAndIcon) 124 | } preAction: { 125 | NSApp.activate(ignoringOtherApps: true) 126 | } postAction: { 127 | } 128 | 129 | Button( 130 | action: { 131 | Updater.shared.checkForUpdatesStableOnly() 132 | }, 133 | label: { 134 | Label("Check for updates...", systemImage: "network") 135 | .labelStyle(.titleAndIcon) 136 | } 137 | ) 138 | 139 | Divider() 140 | 141 | Button( 142 | action: { 143 | NSApp.terminate(nil) 144 | }, 145 | label: { 146 | Label("Quit TrueWidget", systemImage: "xmark") 147 | .labelStyle(.titleAndIcon) 148 | } 149 | ) 150 | }, 151 | label: { 152 | Label( 153 | title: { Text("TrueWidget") }, 154 | icon: { 155 | // To prevent the menu icon from appearing blurry, it is necessary to explicitly set the displayScale. 156 | Image("menu") 157 | .environment(\.displayScale, 2.0) 158 | } 159 | ) 160 | } 161 | ) 162 | 163 | Settings { 164 | SettingsView(showMenuBarExtra: $showMenuBarExtra) 165 | .environmentObject(userSettings) 166 | } 167 | } 168 | 169 | private func checkmarkLabel(title: String, checked: Bool) -> some View { 170 | if checked { 171 | return Label(title, systemImage: "checkmark") 172 | .labelStyle(.titleAndIcon) 173 | } else { 174 | return Label(title, image: "clear") 175 | .labelStyle(.titleAndIcon) 176 | } 177 | } 178 | } 179 | 180 | class AppDelegate: NSObject, NSApplicationDelegate { 181 | var mainWindowController: MainWindowController? 182 | var userSettings: UserSettings? 183 | 184 | func applicationDidFinishLaunching(_ notification: Notification) { 185 | guard let userSettings = userSettings else { return } 186 | 187 | mainWindowController = MainWindowController(userSettings: userSettings) 188 | mainWindowController?.showWindow(nil) 189 | } 190 | 191 | func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool 192 | { 193 | NotificationCenter.default.post( 194 | name: openSettingsNotification, 195 | object: nil, 196 | userInfo: nil) 197 | return true 198 | } 199 | } 200 | 201 | class MainWindowController: NSWindowController, NSWindowDelegate { 202 | private var cancellables = Set() 203 | 204 | init(userSettings: UserSettings) { 205 | // Note: 206 | // On macOS 13, the only way to remove the title bar is to manually create an NSWindow like this. 207 | // 208 | // The following methods do not work properly: 209 | // - .windowStyle(.hiddenTitleBar) does not remove the window frame. 210 | // - NSApp.windows.first.styleMask = [.borderless] causes the app to crash. 211 | 212 | let window = NSWindow( 213 | contentRect: .zero, 214 | styleMask: [ 215 | .borderless, 216 | .fullSizeContentView, 217 | ], 218 | backing: .buffered, 219 | defer: false 220 | ) 221 | 222 | // Note: Do not set alpha value for window. 223 | // Window with alpha value causes glitch at switching a space (Mission Control). 224 | 225 | window.backgroundColor = .clear 226 | window.isOpaque = false 227 | window.hasShadow = false 228 | window.ignoresMouseEvents = true 229 | window.level = NSWindow.Level(userSettings.widgetWindowLevel) 230 | window.collectionBehavior.insert(.canJoinAllSpaces) 231 | window.collectionBehavior.insert(.ignoresCycle) 232 | window.collectionBehavior.insert(.stationary) 233 | window.contentView = NSHostingView( 234 | rootView: ContentView(window: window, userSettings: userSettings) 235 | .openSettingsAccess() 236 | ) 237 | 238 | super.init(window: window) 239 | 240 | window.delegate = self 241 | 242 | userSettings.objectWillChange.sink { _ in 243 | window.level = NSWindow.Level(userSettings.widgetWindowLevel) 244 | }.store(in: &cancellables) 245 | } 246 | 247 | required init?(coder: NSCoder) { 248 | fatalError("init(coder:) has not been implemented") 249 | } 250 | 251 | func windowDidResize(_ notification: Notification) { 252 | // Since GeometryReader.onChange in View is called before the window resizing is completed, 253 | // it may move the window based on the old size, leading to an incorrect position. 254 | // To ensure that the window position is updated only after resizing is fully completed, 255 | // windowDidResize should be used. Therefore, implementing NSWindowDelegate is necessary. 256 | postWindowPositionUpdateNeededNotification() 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Updater.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if USE_SPARKLE 4 | import Sparkle 5 | #endif 6 | 7 | final class Updater: ObservableObject { 8 | static let shared = Updater() 9 | 10 | #if USE_SPARKLE 11 | private let updaterController: SPUStandardUpdaterController 12 | private let delegate = SparkleDelegate() 13 | #endif 14 | 15 | @Published var canCheckForUpdates = false 16 | 17 | init() { 18 | #if USE_SPARKLE 19 | updaterController = SPUStandardUpdaterController( 20 | startingUpdater: true, 21 | updaterDelegate: delegate, 22 | userDriverDelegate: nil 23 | ) 24 | 25 | updaterController.updater.clearFeedURLFromUserDefaults() 26 | 27 | updaterController.updater.publisher(for: \.canCheckForUpdates) 28 | .assign(to: &$canCheckForUpdates) 29 | #endif 30 | } 31 | 32 | func checkForUpdatesInBackground() { 33 | #if USE_SPARKLE 34 | delegate.includingBetaVersions = false 35 | updaterController.updater.checkForUpdatesInBackground() 36 | #endif 37 | } 38 | 39 | func checkForUpdatesStableOnly() { 40 | #if USE_SPARKLE 41 | delegate.includingBetaVersions = false 42 | updaterController.checkForUpdates(nil) 43 | #endif 44 | } 45 | 46 | func checkForUpdatesWithBetaVersion() { 47 | #if USE_SPARKLE 48 | delegate.includingBetaVersions = true 49 | updaterController.checkForUpdates(nil) 50 | #endif 51 | } 52 | 53 | #if USE_SPARKLE 54 | private class SparkleDelegate: NSObject, SPUUpdaterDelegate, 55 | SPUStandardUserDriverDelegate 56 | { 57 | var includingBetaVersions = false 58 | 59 | func feedURLString(for updater: SPUUpdater) -> String? { 60 | var url = "https://appcast.pqrs.org/truewidget-appcast.xml" 61 | if includingBetaVersions { 62 | url = "https://appcast.pqrs.org/truewidget-appcast-devel.xml" 63 | } 64 | 65 | print("feedURLString \(url)") 66 | 67 | return url 68 | } 69 | } 70 | #endif 71 | } 72 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/UserSettings.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import SwiftUI 4 | 5 | enum WidgetPosition: String { 6 | case bottomLeft 7 | case bottomRight 8 | case topLeft 9 | case topRight 10 | } 11 | 12 | enum WidgetScreen: String { 13 | case primary 14 | case bottomLeft 15 | case bottomRight 16 | case topLeft 17 | case topRight 18 | case leftTop 19 | case leftBottom 20 | case rightTop 21 | case rightBottom 22 | } 23 | 24 | enum WidgetAppearance: String { 25 | case normal 26 | case compact 27 | case autoCompact 28 | case hidden 29 | } 30 | 31 | enum CPUUsageType: String { 32 | case movingAverage 33 | case latest 34 | } 35 | 36 | enum DateStyle: String { 37 | case rfc3339 38 | case rfc3339WithDayName 39 | case short 40 | case shortWithDayName 41 | case medium 42 | case mediumWithDayName 43 | case long 44 | case longWithDayName 45 | case full 46 | } 47 | 48 | struct TimeZoneTimeSetting: Identifiable, Codable { 49 | var id = UUID().uuidString 50 | var show = false 51 | var abbreviation: String = "UTC" 52 | } 53 | 54 | struct BundleSetting: Identifiable, Codable { 55 | var id = UUID().uuidString 56 | var show = false 57 | var path = "" 58 | } 59 | 60 | final class UserSettings: ObservableObject { 61 | init() { 62 | initializeTimeZoneTimeSettings() 63 | initializeBundleSettings() 64 | } 65 | 66 | @AppStorage("initialOpenAtLoginRegistered") var initialOpenAtLoginRegistered: Bool = false 67 | 68 | // 69 | // Layout 70 | // 71 | 72 | @AppStorage("widgetPosition") var widgetPosition: String = WidgetPosition.bottomRight.rawValue 73 | @AppStorage("widgetAllowOverlappingWithDock") var widgetAllowOverlappingWithDock: Bool = false 74 | @AppStorage("widgetWindowLevel") var widgetWindowLevel: Int = NSWindow.Level.statusBar.rawValue 75 | @AppStorage("widgetOffsetX") var widgetOffsetX: Double = 10.0 76 | @AppStorage("widgetOffsetY") var widgetOffsetY: Double = 10.0 77 | @AppStorage("widgetWidth") var widgetWidth: Double = 250.0 78 | @AppStorage("widgetOpacity") var widgetOpacity: Double = 0.8 79 | @AppStorage("widgetScreen") var widgetScreen: String = WidgetScreen.primary.rawValue 80 | @AppStorage("widgetFadeOutDuration") var widgetFadeOutDuration: Double = 500.0 81 | @AppStorage("widgetAppearance") var widgetAppearance: String = WidgetAppearance.normal.rawValue 82 | 83 | // 84 | // Operating system 85 | // 86 | 87 | @AppStorage("showOperatingSystem") var showOperatingSystem: Bool = true 88 | @AppStorage("operatingSystemFontSize") var operatingSystemFontSize: Double = 14.0 89 | @AppStorage("showUptime") var showUptime: Bool = false 90 | @AppStorage("showAwakeTime") var showAwakeTime: Bool = false 91 | @AppStorage("showHostName") var showHostName: Bool = false 92 | @AppStorage("showRootVolumeName") var showRootVolumeName: Bool = false 93 | @AppStorage("showUserName") var showUserName: Bool = false 94 | @AppStorage("showAppleAccount") var showAppleAccount: Bool = false 95 | 96 | // 97 | // Xcode 98 | // 99 | 100 | @AppStorage("showXcode") var showXcode: Bool = false 101 | @AppStorage("xcodeFontSize") var xcodeFontSize: Double = 12.0 102 | 103 | // 104 | // CPU usage 105 | // 106 | 107 | @AppStorage("showCPUUsage") var showCPUUsage: Bool = true 108 | @AppStorage("cpuUsageFontSize") var cpuUsageFontSize: Double = 36.0 109 | @AppStorage("cpuUsageType") var cpuUsageType: String = CPUUsageType.movingAverage.rawValue 110 | @AppStorage("cpuUsageMovingAverageRange") var cpuUsageMovingAverageRange: Int = 30 111 | @AppStorage("showProcesses") var showProcesses: Bool = true 112 | @AppStorage("processesFontSize") var processesFontSize: Double = 12.0 113 | 114 | // 115 | // Local time 116 | // 117 | 118 | @AppStorage("showLocalTime") var showLocalTime: Bool = true 119 | @AppStorage("localTimeFontSize") var localTimeFontSize: Double = 36.0 120 | @AppStorage("localTimeSecondsFontSize") var localTimeSecondsFontSize: Double = 18.0 121 | @AppStorage("dateStyle") var dateStyle: String = DateStyle.rfc3339WithDayName.rawValue 122 | @AppStorage("showLocalDate") var showLocalDate: Bool = true 123 | @AppStorage("localDateFontSize") var localDateFontSize: Double = 12.0 124 | 125 | // 126 | // Another time zone time 127 | // 128 | 129 | @CodableAppStorage("timeZoneTimeSettings") var timeZoneTimeSettings: [TimeZoneTimeSetting] = [] { 130 | willSet { 131 | objectWillChange.send() 132 | } 133 | } 134 | 135 | func initializeTimeZoneTimeSettings() { 136 | let maxCount = 5 137 | while timeZoneTimeSettings.count < maxCount { 138 | timeZoneTimeSettings.append(TimeZoneTimeSetting()) 139 | } 140 | } 141 | 142 | @AppStorage("timeZoneDateFontSize") var timeZoneDateFontSize: Double = 10.0 143 | @AppStorage("timeZoneTimeFontSize") var timeZoneTimeFontSize: Double = 12.0 144 | 145 | // 146 | // Bundle 147 | // 148 | 149 | @CodableAppStorage("bundleSettings") var bundleSettings: [BundleSetting] = [] { 150 | willSet { 151 | objectWillChange.send() 152 | } 153 | } 154 | 155 | func initializeBundleSettings() { 156 | let maxCount = 10 157 | while bundleSettings.count < maxCount { 158 | if bundleSettings.isEmpty { 159 | bundleSettings.append( 160 | BundleSetting( 161 | path: "/Applications/TrueWidget.app" 162 | )) 163 | } else { 164 | bundleSettings.append(BundleSetting()) 165 | } 166 | } 167 | } 168 | 169 | @AppStorage("bundleFontSize") var bundleFontSize: Double = 12.0 170 | 171 | // 172 | // Compact 173 | // 174 | 175 | @AppStorage("compactShowLocalTime") var compactShowLocalTime: Bool = true 176 | @AppStorage("compactLocalTimeFontSize") var compactLocalTimeFontSize: Double = 24.0 177 | @AppStorage("compactLocalTimeSecondsFontSize") var compactLocalTimeSecondsFontSize: Double = 12.0 178 | @AppStorage("compactShowLocalDate") var compactShowLocalDate: Bool = true 179 | @AppStorage("compactLocalDateFontSize") var compactLocalDateFontSize: Double = 10.0 180 | @AppStorage("compactShowCPUUsage") var compactShowCPUUsage: Bool = true 181 | @AppStorage("compactCPUUsageFontSize") var compactCPUUsageFontSize: Double = 12.0 182 | 183 | // 184 | // Auto compact 185 | // 186 | 187 | @AppStorage("autoCompactDisplayCount") var autoCompactDisplayCount: Int = 1 188 | } 189 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/BundlePickerView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UniformTypeIdentifiers 3 | 4 | struct BundlePickerView: View { 5 | @Binding private(set) var path: String 6 | 7 | @State private var isPickingFile = false 8 | @State private var errorMessage: String? 9 | 10 | init(path: Binding) { 11 | self._path = path 12 | } 13 | 14 | var body: some View { 15 | HStack { 16 | VStack(alignment: .leading) { 17 | Text("\(path.isEmpty ? "---" : path)") 18 | .fixedSize(horizontal: false, vertical: true) 19 | 20 | if let error = errorMessage { 21 | Text(error) 22 | .foregroundColor(Color.errorForeground) 23 | .background(Color.errorBackground) 24 | } 25 | } 26 | 27 | Spacer() 28 | 29 | Button("Select") { 30 | isPickingFile = true 31 | } 32 | .fileImporter( 33 | isPresented: $isPickingFile, 34 | allowedContentTypes: [.item], 35 | allowsMultipleSelection: false 36 | ) { result in 37 | if let url = try? result.get().first { 38 | HelperClient.shared.proxy?.bundleVersions(paths: [url.path]) { versions in 39 | // Update path on the main thread, as it could be an object observed by the view. 40 | Task { @MainActor in 41 | if versions[url.path] != nil { 42 | path = url.path 43 | errorMessage = nil 44 | } else { 45 | path = "" 46 | errorMessage = "Could not get the version of the selected file" 47 | } 48 | } 49 | } 50 | return 51 | } 52 | 53 | path = "" 54 | errorMessage = "File selection failed" 55 | } 56 | 57 | Button( 58 | role: .destructive, 59 | action: { 60 | path = "" 61 | errorMessage = nil 62 | }, 63 | label: { 64 | Label("Reset", systemImage: "trash") 65 | .labelStyle(.iconOnly) 66 | .foregroundColor(.red) 67 | } 68 | ) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/ConditionalModifier.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) 5 | -> some View 6 | { 7 | if condition { 8 | transform(self) 9 | } else { 10 | self 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SettingsAccess 2 | import SwiftUI 3 | 4 | struct ContentView: View { 5 | @Environment(\.openSettingsLegacy) var openSettingsLegacy 6 | private var window: NSWindow 7 | @ObservedObject private var userSettings: UserSettings 8 | @StateObject private var displayMonitor = DisplayMonitor() 9 | 10 | @State private var hidden = false 11 | private let windowPositionManager: WindowPositionManager 12 | 13 | init(window: NSWindow, userSettings: UserSettings) { 14 | self.window = window 15 | self.userSettings = userSettings 16 | windowPositionManager = WindowPositionManager(window: window, userSettings: userSettings) 17 | } 18 | 19 | var body: some View { 20 | VStack { 21 | if isCompactView() { 22 | CompactView(userSettings: userSettings) 23 | } else { 24 | VStack(alignment: .leading, spacing: 10.0) { 25 | if userSettings.showOperatingSystem { 26 | MainOperatingSystemView(userSettings: userSettings) 27 | } 28 | 29 | if userSettings.showXcode { 30 | MainXcodeView(userSettings: userSettings) 31 | } 32 | 33 | if !userSettings.bundleSettings.filter({ $0.show }).isEmpty { 34 | MainBundleView(userSettings: userSettings) 35 | } 36 | 37 | if userSettings.showCPUUsage { 38 | MainCPUUsageView(userSettings: userSettings) 39 | } 40 | 41 | if userSettings.showLocalTime 42 | || userSettings.showLocalDate 43 | || !userSettings.timeZoneTimeSettings.filter({ $0.show }).isEmpty 44 | { 45 | MainTimeView(userSettings: userSettings) 46 | } 47 | } 48 | } 49 | } 50 | .padding() 51 | .if(!isCompactView()) { 52 | $0.frame(width: userSettings.widgetWidth) 53 | } 54 | .fixedSize() 55 | .background( 56 | RoundedRectangle(cornerRadius: 12) 57 | .fill(.black) 58 | ) 59 | .foregroundColor(.white) 60 | .opacity( 61 | userSettings.widgetAppearance == WidgetAppearance.hidden.rawValue 62 | ? 0.0 63 | : (hidden ? 0.0 : userSettings.widgetOpacity) 64 | ) 65 | .whenHovered { hover in 66 | if hover { 67 | withAnimation(.easeInOut(duration: userSettings.widgetFadeOutDuration / 1000.0)) { 68 | hidden = true 69 | } 70 | } else { 71 | hidden = false 72 | } 73 | } 74 | .overlay( 75 | RoundedRectangle(cornerRadius: 12) 76 | .stroke(.black, lineWidth: 4) 77 | ) 78 | .clipShape(RoundedRectangle(cornerRadius: 12)) 79 | .onAppear { 80 | windowPositionManager.updateWindowPosition() 81 | } 82 | .onReceive( 83 | NotificationCenter.default.publisher(for: windowPositionUpdateNeededNotification) 84 | ) { _ in 85 | Task { @MainActor in 86 | windowPositionManager.updateWindowPosition() 87 | } 88 | } 89 | .onReceive(NotificationCenter.default.publisher(for: openSettingsNotification)) { _ in 90 | Task { @MainActor in 91 | try? openSettingsLegacy() 92 | } 93 | } 94 | } 95 | 96 | private func isCompactView() -> Bool { 97 | switch userSettings.widgetAppearance { 98 | case WidgetAppearance.compact.rawValue: 99 | return true 100 | case WidgetAppearance.autoCompact.rawValue: 101 | if displayMonitor.displayCount <= userSettings.autoCompactDisplayCount { 102 | return true 103 | } 104 | return false 105 | default: 106 | return false 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/DateStylePicker.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct DateStylePicker: View { 4 | @EnvironmentObject private var userSettings: UserSettings 5 | 6 | var body: some View { 7 | Picker( 8 | selection: $userSettings.dateStyle, 9 | label: Text("Date style:") 10 | ) { 11 | Text("RFC 3339").tag(DateStyle.rfc3339.rawValue) 12 | Text("RFC 3339 with the day of the week (Default)").tag( 13 | DateStyle.rfc3339WithDayName.rawValue) 14 | Text("Short").tag(DateStyle.short.rawValue) 15 | Text("Short with the day of the week").tag(DateStyle.shortWithDayName.rawValue) 16 | Text("Medium").tag(DateStyle.medium.rawValue) 17 | Text("Medium with the day of the week").tag(DateStyle.mediumWithDayName.rawValue) 18 | Text("Long").tag(DateStyle.long.rawValue) 19 | Text("Long with the day of the week").tag(DateStyle.longWithDayName.rawValue) 20 | Text("Full").tag(DateStyle.full.rawValue) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/DoubleTextField.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct DoubleTextField: View { 4 | @Binding var value: Double 5 | // Specifying a formatter directly in a TextField makes it difficult to enter numbers less than 1.0. 6 | // Specifically, if a user wants to enter "0.2" and enters a value of "0.", the value becomes "0.0". 7 | // To avoid this, do not specify the formatter directly in the TextField, but use onChange to format the value. 8 | @State private var text = "" 9 | @State private var error = false 10 | 11 | private let step: Double 12 | private let range: ClosedRange 13 | private let width: CGFloat 14 | private let maximumFractionDigits: Int 15 | private let formatter: NumberFormatter 16 | 17 | init( 18 | value: Binding, 19 | range: ClosedRange, 20 | step: Double, 21 | maximumFractionDigits: Int, 22 | width: CGFloat 23 | ) { 24 | _value = value 25 | text = String(value.wrappedValue) 26 | 27 | self.step = step 28 | self.range = range 29 | self.width = width 30 | self.maximumFractionDigits = maximumFractionDigits 31 | 32 | formatter = NumberFormatter() 33 | formatter.numberStyle = .decimal // Use .decimal number style for double values 34 | formatter.minimum = NSNumber(value: range.lowerBound) 35 | formatter.maximum = NSNumber(value: range.upperBound) 36 | formatter.maximumFractionDigits = maximumFractionDigits 37 | } 38 | 39 | var body: some View { 40 | HStack(spacing: 0) { 41 | TextField("", text: $text).frame(width: width) 42 | 43 | Stepper( 44 | value: $value, 45 | in: range, 46 | step: step 47 | ) { 48 | Text("") 49 | }.whenHovered { hover in 50 | if hover { 51 | // In macOS 13.0.1, if the corresponding TextField has the focus, changing the value by Stepper will not be reflected in the TextField. 52 | // Therefore, we should remove the focus before Stepper will be clicked. 53 | Task { @MainActor in 54 | NSApp.keyWindow?.makeFirstResponder(nil) 55 | } 56 | } 57 | } 58 | 59 | if error { 60 | Text( 61 | String( 62 | format: "must be between %.\(maximumFractionDigits)f and %.\(maximumFractionDigits)f", 63 | range.lowerBound, 64 | range.upperBound) 65 | ) 66 | .foregroundColor(Color.errorForeground) 67 | .background(Color.errorBackground) 68 | } 69 | } 70 | .onChange(of: text) { newText in 71 | update(byText: newText) 72 | } 73 | .onChange(of: value) { newValue in 74 | update(byValue: newValue) 75 | } 76 | } 77 | 78 | private func update(byValue newValue: Double) { 79 | if let newText = formatter.string(for: newValue) { 80 | error = false 81 | 82 | Task { @MainActor in 83 | if value != newValue { 84 | value = newValue 85 | } 86 | if text != newText { 87 | text = newText 88 | } 89 | } 90 | } else { 91 | error = true 92 | } 93 | } 94 | 95 | private func update(byText newText: String) { 96 | if let number = formatter.number(from: newText) { 97 | error = false 98 | 99 | let newValue = number.doubleValue 100 | Task { @MainActor in 101 | if value != newValue { 102 | value = newValue 103 | } 104 | if text != newText { 105 | text = newText 106 | } 107 | } 108 | } else { 109 | error = true 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/IntTextField.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct IntTextField: View { 4 | @Binding var value: Int 5 | // When a formatter is applied directly to a TextField, unintended changes to the content may occur during input. 6 | // Specifically, the moment the input content is cleared, the minimum value is automatically entered. 7 | // To avoid this, do not apply the formatter directly to the TextField; instead, apply the formatting in the onChange event. 8 | @State private var text = "" 9 | @State private var error = false 10 | 11 | private let step: Int 12 | private let range: ClosedRange 13 | private let width: CGFloat 14 | private let formatter: NumberFormatter 15 | 16 | init( 17 | value: Binding, 18 | range: ClosedRange, 19 | step: Int, 20 | width: CGFloat 21 | ) { 22 | _value = value 23 | text = String(value.wrappedValue) 24 | 25 | self.step = step 26 | self.range = range 27 | self.width = width 28 | 29 | formatter = NumberFormatter() 30 | formatter.numberStyle = .none 31 | formatter.minimum = NSNumber(value: range.lowerBound) 32 | formatter.maximum = NSNumber(value: range.upperBound) 33 | } 34 | 35 | var body: some View { 36 | HStack(spacing: 0) { 37 | TextField("", text: $text).frame(width: width) 38 | 39 | Stepper( 40 | value: $value, 41 | in: range, 42 | step: step 43 | ) { 44 | Text("") 45 | }.whenHovered { hover in 46 | if hover { 47 | // In macOS 13.0.1, if the corresponding TextField has the focus, changing the value by Stepper will not be reflected in the TextField. 48 | // Therefore, we should remove the focus before Stepper will be clicked. 49 | Task { @MainActor in 50 | NSApp.keyWindow?.makeFirstResponder(nil) 51 | } 52 | } 53 | } 54 | 55 | if error { 56 | Text("must be between \(range.lowerBound) and \(range.upperBound)") 57 | .foregroundColor(Color.errorForeground) 58 | .background(Color.errorBackground) 59 | } 60 | } 61 | .onChange(of: text) { newText in 62 | update(byText: newText) 63 | } 64 | .onChange(of: value) { newValue in 65 | update(byValue: newValue) 66 | } 67 | } 68 | 69 | private func update(byValue newValue: Int) { 70 | if let newText = formatter.string(for: newValue) { 71 | error = false 72 | 73 | Task { @MainActor in 74 | if value != newValue { 75 | value = newValue 76 | } 77 | if text != newText { 78 | text = newText 79 | } 80 | } 81 | } else { 82 | error = true 83 | } 84 | } 85 | 86 | private func update(byText newText: String) { 87 | if let number = formatter.number(from: newText) { 88 | error = false 89 | 90 | let newValue = number.intValue 91 | Task { @MainActor in 92 | if value != newValue { 93 | value = newValue 94 | } 95 | if text != newText { 96 | text = newText 97 | } 98 | } 99 | } else { 100 | error = true 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Main/CompactCPUUsageView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct CompactCPUUsageView: View { 4 | @ObservedObject private var userSettings: UserSettings 5 | @StateObject private var cpuUsage: WidgetSource.CPUUsage 6 | 7 | init(userSettings: UserSettings) { 8 | self.userSettings = userSettings 9 | _cpuUsage = StateObject(wrappedValue: WidgetSource.CPUUsage(userSettings: userSettings)) 10 | } 11 | 12 | var body: some View { 13 | // 14 | // CPU usage 15 | // 16 | 17 | HStack(alignment: .firstTextBaseline, spacing: 0) { 18 | Text("CPU") 19 | 20 | if userSettings.cpuUsageType == CPUUsageType.latest.rawValue { 21 | Text( 22 | String( 23 | format: "% 3d.%02d%%", 24 | cpuUsage.usageInteger, 25 | cpuUsage.usageDecimal)) 26 | } else { 27 | // Moving average 28 | Text( 29 | String( 30 | format: "% 3d.%02d%%", 31 | cpuUsage.usageAverageInteger, 32 | cpuUsage.usageAverageDecimal)) 33 | } 34 | } 35 | .font(.custom("Menlo", size: userSettings.compactCPUUsageFontSize)) 36 | .frame(maxWidth: .infinity, alignment: .trailing) 37 | .onDisappear { 38 | cpuUsage.cancelTimer() 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Main/CompactTimeView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct CompactTimeView: View { 4 | @ObservedObject private var userSettings: UserSettings 5 | @StateObject private var time: WidgetSource.Time 6 | 7 | init(userSettings: UserSettings) { 8 | self.userSettings = userSettings 9 | _time = StateObject(wrappedValue: WidgetSource.Time(userSettings: userSettings)) 10 | } 11 | 12 | var body: some View { 13 | VStack(alignment: .trailing, spacing: 0) { 14 | if userSettings.compactShowLocalDate { 15 | Text(time.localTime?.date ?? "---") 16 | .font(.custom("Menlo", size: userSettings.compactLocalDateFontSize)) 17 | .padding(.bottom, 4.0) 18 | } 19 | 20 | if userSettings.compactShowLocalTime { 21 | HStack(alignment: .firstTextBaseline, spacing: 0) { 22 | if userSettings.compactLocalTimeFontSize > 0 { 23 | Text( 24 | time.localTime == nil 25 | ? "---" 26 | : String( 27 | format: " %02d:%02d", 28 | time.localTime?.hour ?? 0, 29 | time.localTime?.minute ?? 0 30 | ) 31 | ) 32 | .font(.custom("Menlo", size: userSettings.compactLocalTimeFontSize)) 33 | } 34 | 35 | if userSettings.compactLocalTimeSecondsFontSize > 0 { 36 | Text( 37 | time.localTime == nil 38 | ? "---" 39 | : String( 40 | format: " %02d", 41 | time.localTime?.second ?? 0 42 | ) 43 | ) 44 | .font(.custom("Menlo", size: userSettings.compactLocalTimeSecondsFontSize)) 45 | } 46 | } 47 | } 48 | } 49 | .frame(maxWidth: .infinity, alignment: .trailing) 50 | .onDisappear { 51 | time.cancelTimer() 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Main/CompactView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct CompactView: View { 4 | @ObservedObject private var userSettings: UserSettings 5 | 6 | init(userSettings: UserSettings) { 7 | self.userSettings = userSettings 8 | } 9 | 10 | var body: some View { 11 | VStack(alignment: .leading, spacing: 4) { 12 | if userSettings.compactShowLocalTime || userSettings.compactShowLocalDate { 13 | CompactTimeView(userSettings: userSettings) 14 | } 15 | if userSettings.compactShowCPUUsage { 16 | CompactCPUUsageView(userSettings: userSettings) 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Main/MainBundleView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MainBundleView: View { 4 | @ObservedObject private var userSettings: UserSettings 5 | @StateObject private var bundle: WidgetSource.Bundle 6 | 7 | init(userSettings: UserSettings) { 8 | self.userSettings = userSettings 9 | _bundle = StateObject(wrappedValue: WidgetSource.Bundle(userSettings: userSettings)) 10 | } 11 | 12 | var body: some View { 13 | VStack(alignment: .trailing, spacing: 0) { 14 | ForEach(userSettings.bundleSettings) { setting in 15 | if setting.show { 16 | if let version = bundle.bundleVersions[setting.path] { 17 | Text("\(version["name"] ?? "---"): \(version["version"] ?? "---")") 18 | .fixedSize(horizontal: false, vertical: true) 19 | .multilineTextAlignment(.trailing) 20 | } else { 21 | Text("---") 22 | } 23 | } 24 | } 25 | } 26 | .font(.system(size: userSettings.bundleFontSize)) 27 | .frame(maxWidth: .infinity, alignment: .trailing) 28 | .onDisappear { 29 | bundle.cancelTimer() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Main/MainCPUUsageView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MainCPUUsageView: View { 4 | @ObservedObject private var userSettings: UserSettings 5 | @StateObject private var cpuUsage: WidgetSource.CPUUsage 6 | 7 | init(userSettings: UserSettings) { 8 | self.userSettings = userSettings 9 | _cpuUsage = StateObject(wrappedValue: WidgetSource.CPUUsage(userSettings: userSettings)) 10 | } 11 | 12 | var body: some View { 13 | // 14 | // CPU usage 15 | // 16 | 17 | VStack(alignment: .trailing, spacing: 0.0) { 18 | HStack(alignment: .firstTextBaseline, spacing: 0) { 19 | Text("CPU") 20 | .font(.system(size: userSettings.cpuUsageFontSize / 2)) 21 | 22 | if userSettings.cpuUsageType == CPUUsageType.latest.rawValue { 23 | Text(String(format: "% 3d", cpuUsage.usageInteger)) 24 | 25 | Text(String(format: ".%02d%%", cpuUsage.usageDecimal)) 26 | .font(.custom("Menlo", size: userSettings.cpuUsageFontSize / 2)) 27 | } else { 28 | // Moving average 29 | Text(String(format: "% 3d", cpuUsage.usageAverageInteger)) 30 | 31 | Text(String(format: ".%02d%%", cpuUsage.usageAverageDecimal)) 32 | .font(.custom("Menlo", size: userSettings.cpuUsageFontSize / 2)) 33 | } 34 | } 35 | .if(!userSettings.showProcesses) { 36 | $0.overlay( 37 | Rectangle() 38 | .frame(height: 1.0), 39 | alignment: .bottom 40 | ) 41 | } 42 | .font(.custom("Menlo", size: userSettings.cpuUsageFontSize)) 43 | 44 | if userSettings.showProcesses { 45 | VStack(alignment: .trailing, spacing: 0) { 46 | ForEach(Array(cpuUsage.processes.enumerated()), id: \.0) { _, process in 47 | Text( 48 | "\(process["name"] ?? "---") \((process["cpu"] ?? "---").leftPadding(toLength: 6, withPad: " "))%" 49 | ) 50 | } 51 | } 52 | .padding(.vertical, 4.0) 53 | .padding(.horizontal, 10.0) 54 | .font(.custom("Menlo", size: userSettings.processesFontSize)) 55 | .border(.gray) 56 | } 57 | } 58 | .frame(maxWidth: .infinity, alignment: .trailing) 59 | .onDisappear { 60 | cpuUsage.cancelTimer() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Main/MainOperatingSystemView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MainOperatingSystemView: View { 4 | @ObservedObject private var userSettings: UserSettings 5 | @StateObject private var operatingSystem: WidgetSource.OperatingSystem 6 | 7 | init(userSettings: UserSettings) { 8 | self.userSettings = userSettings 9 | _operatingSystem = StateObject( 10 | wrappedValue: WidgetSource.OperatingSystem(userSettings: userSettings)) 11 | } 12 | 13 | var body: some View { 14 | // 15 | // Operating system 16 | // 17 | 18 | VStack(spacing: 0) { 19 | HStack(alignment: .center, spacing: 0) { 20 | Text("macOS ") 21 | 22 | Text(operatingSystem.version) 23 | 24 | Spacer() 25 | 26 | if userSettings.showUptime { 27 | Text("up \(operatingSystem.uptime)") 28 | } 29 | } 30 | 31 | VStack(alignment: .trailing, spacing: 0) { 32 | if userSettings.showAwakeTime { 33 | Text("awake \(operatingSystem.awakeTime)") 34 | } 35 | 36 | if userSettings.showHostName { 37 | Text(operatingSystem.hostName) 38 | } 39 | 40 | if userSettings.showRootVolumeName { 41 | Text("/Volumes/\(operatingSystem.rootVolumeName)") 42 | } 43 | 44 | if userSettings.showUserName { 45 | Text(operatingSystem.userName) 46 | } 47 | 48 | if userSettings.showAppleAccount { 49 | // The spacing between the icon and text is too wide when using a Label, so managing it manually with an HStack. 50 | HStack(alignment: .center, spacing: 4) { 51 | Image(systemName: "apple.logo") 52 | Text( 53 | operatingSystem.appleAccount.isEmpty ? "---" : operatingSystem.appleAccount 54 | ) 55 | } 56 | } 57 | } 58 | .frame(maxWidth: .infinity, alignment: .trailing) 59 | } 60 | .font(.system(size: userSettings.operatingSystemFontSize)) 61 | .onDisappear { 62 | operatingSystem.cancelTimer() 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Main/MainTimeView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MainTimeView: View { 4 | @ObservedObject private var userSettings: UserSettings 5 | @StateObject private var time: WidgetSource.Time 6 | 7 | init(userSettings: UserSettings) { 8 | self.userSettings = userSettings 9 | _time = StateObject(wrappedValue: WidgetSource.Time(userSettings: userSettings)) 10 | } 11 | 12 | var body: some View { 13 | VStack(alignment: .trailing, spacing: 0) { 14 | if userSettings.showLocalDate { 15 | Text(time.localTime?.date ?? "---") 16 | .font(.custom("Menlo", size: userSettings.localDateFontSize)) 17 | .padding(.bottom, 4.0) 18 | } 19 | 20 | if userSettings.showLocalTime { 21 | HStack(alignment: .firstTextBaseline, spacing: 0) { 22 | if userSettings.localTimeFontSize > 0 { 23 | Text( 24 | time.localTime == nil 25 | ? "---" 26 | : String( 27 | format: " %02d:%02d", 28 | time.localTime?.hour ?? 0, 29 | time.localTime?.minute ?? 0 30 | ) 31 | ) 32 | .font(.custom("Menlo", size: userSettings.localTimeFontSize)) 33 | } 34 | 35 | if userSettings.localTimeSecondsFontSize > 0 { 36 | Text( 37 | time.localTime == nil 38 | ? "---" 39 | : String( 40 | format: " %02d", 41 | time.localTime?.second ?? 0 42 | ) 43 | ) 44 | .font(.custom("Menlo", size: userSettings.localTimeSecondsFontSize)) 45 | } 46 | } 47 | } 48 | 49 | ForEach(userSettings.timeZoneTimeSettings) { setting in 50 | if setting.show { 51 | HStack(alignment: .firstTextBaseline, spacing: 0) { 52 | let dateTime = time.timeZoneTimes[setting.abbreviation] 53 | 54 | Text(String(format: "%@: ", setting.abbreviation)) 55 | .font(.custom("Menlo", size: userSettings.timeZoneTimeFontSize)) 56 | 57 | if userSettings.timeZoneDateFontSize > 0 { 58 | Text(String(format: "%@ ", dateTime?.date ?? "---")) 59 | .font(.custom("Menlo", size: userSettings.timeZoneDateFontSize)) 60 | } 61 | 62 | if userSettings.timeZoneTimeFontSize > 0 { 63 | Text( 64 | dateTime == nil 65 | ? "---" 66 | : String( 67 | format: "%02d:%02d:%02d", 68 | dateTime?.hour ?? 0, 69 | dateTime?.minute ?? 0, 70 | dateTime?.second ?? 0) 71 | ) 72 | .font(.custom("Menlo", size: userSettings.timeZoneTimeFontSize)) 73 | } 74 | } 75 | } 76 | } 77 | } 78 | .frame(maxWidth: .infinity, alignment: .trailing) 79 | .onDisappear { 80 | time.cancelTimer() 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Main/MainXcodeView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct MainXcodeView: View { 4 | @ObservedObject private var userSettings: UserSettings 5 | @StateObject private var xcode: WidgetSource.Xcode 6 | 7 | init(userSettings: UserSettings) { 8 | self.userSettings = userSettings 9 | _xcode = StateObject(wrappedValue: WidgetSource.Xcode()) 10 | } 11 | 12 | var body: some View { 13 | VStack(alignment: .trailing, spacing: 0) { 14 | Text(xcode.path) 15 | .foregroundColor(pathColor(xcode.pathState)) 16 | } 17 | .font(.system(size: userSettings.xcodeFontSize)) 18 | .frame(maxWidth: .infinity, alignment: .trailing) 19 | .onDisappear { 20 | xcode.cancelTimer() 21 | } 22 | } 23 | 24 | private func pathColor(_ pathState: WidgetSource.Xcode.PathState) -> Color { 25 | switch pathState { 26 | case .notInstalled: 27 | return .gray 28 | case .defaultPath: 29 | return .white 30 | case .nonDefaultPath: 31 | return .green 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/MouseInsideModifier.swift: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/importRyan/c668904b0c5442b80b6f38a980595031 2 | 3 | import SwiftUI 4 | 5 | struct MouseInsideModifier: ViewModifier { 6 | let mouseIsInside: (Bool) -> Void 7 | 8 | init(_ mouseIsInside: @escaping (Bool) -> Void) { 9 | self.mouseIsInside = mouseIsInside 10 | } 11 | 12 | func body(content: Content) -> some View { 13 | content.background( 14 | GeometryReader { proxy in 15 | Representable( 16 | mouseIsInside: mouseIsInside, 17 | frame: proxy.frame(in: .global)) 18 | } 19 | ) 20 | } 21 | 22 | private struct Representable: NSViewRepresentable { 23 | let mouseIsInside: (Bool) -> Void 24 | let frame: NSRect 25 | 26 | func makeCoordinator() -> Coordinator { 27 | let coordinator = Coordinator() 28 | coordinator.mouseIsInside = mouseIsInside 29 | return coordinator 30 | } 31 | 32 | class Coordinator: NSResponder { 33 | var mouseIsInside: ((Bool) -> Void)? 34 | 35 | override func mouseEntered(with _: NSEvent) { 36 | mouseIsInside?(true) 37 | } 38 | 39 | override func mouseExited(with _: NSEvent) { 40 | mouseIsInside?(false) 41 | } 42 | } 43 | 44 | func makeNSView(context: Context) -> NSView { 45 | let view = NSView(frame: frame) 46 | 47 | let options: NSTrackingArea.Options = [ 48 | .mouseEnteredAndExited, 49 | .inVisibleRect, 50 | .activeAlways, 51 | ] 52 | 53 | let trackingArea = NSTrackingArea( 54 | rect: frame, 55 | options: options, 56 | owner: context.coordinator, 57 | userInfo: nil) 58 | 59 | view.addTrackingArea(trackingArea) 60 | 61 | return view 62 | } 63 | 64 | func updateNSView(_: NSView, context _: Context) {} 65 | 66 | static func dismantleNSView(_ nsView: NSView, coordinator _: Coordinator) { 67 | nsView.trackingAreas.forEach { nsView.removeTrackingArea($0) } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Settings/SettingsActionView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsActionView: View { 4 | var body: some View { 5 | VStack(alignment: .leading, spacing: 25.0) { 6 | GroupBox(label: Text("Action")) { 7 | VStack(alignment: .leading, spacing: 16) { 8 | Button( 9 | action: { 10 | Relauncher.relaunch() 11 | }, 12 | label: { 13 | Label("Restart TrueWidget", systemImage: "arrow.clockwise") 14 | }) 15 | 16 | Button( 17 | action: { 18 | NSApplication.shared.terminate(self) 19 | }, 20 | label: { 21 | Label("Quit TrueWidget", systemImage: "xmark") 22 | }) 23 | } 24 | .padding() 25 | .frame(maxWidth: .infinity, alignment: .leading) 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Settings/SettingsBundleView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsBundleView: View { 4 | @EnvironmentObject private var userSettings: UserSettings 5 | 6 | @State var selectedFileURL: URL? 7 | 8 | var body: some View { 9 | GroupBox(label: Text("Show app versions")) { 10 | VStack(alignment: .leading, spacing: 12.0) { 11 | ForEach($userSettings.bundleSettings) { setting in 12 | HStack { 13 | Toggle(isOn: setting.show) { 14 | Text("Show") 15 | } 16 | .switchToggleStyle() 17 | 18 | BundlePickerView(path: setting.path) 19 | } 20 | } 21 | 22 | Divider() 23 | 24 | HStack { 25 | Text("Font size:") 26 | 27 | DoubleTextField( 28 | value: $userSettings.bundleFontSize, 29 | range: 0...1000, 30 | step: 2, 31 | maximumFractionDigits: 1, 32 | width: 40) 33 | 34 | Text("pt") 35 | 36 | Text("(Default: 12 pt)") 37 | } 38 | } 39 | .padding() 40 | .frame(maxWidth: .infinity, alignment: .leading) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Settings/SettingsCPUUsageView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsCPUUsageView: View { 4 | @EnvironmentObject private var userSettings: UserSettings 5 | 6 | var body: some View { 7 | VStack(alignment: .leading, spacing: 25.0) { 8 | GroupBox(label: Text("CPU usage")) { 9 | VStack(alignment: .leading) { 10 | Toggle(isOn: $userSettings.showCPUUsage) { 11 | Text("Show CPU usage") 12 | } 13 | .switchToggleStyle() 14 | 15 | HStack { 16 | Text("Font size:") 17 | 18 | DoubleTextField( 19 | value: $userSettings.cpuUsageFontSize, 20 | range: 0...1000, 21 | step: 2, 22 | maximumFractionDigits: 1, 23 | width: 40) 24 | 25 | Text("pt") 26 | 27 | Text("(Default: 36 pt)") 28 | } 29 | } 30 | .padding() 31 | .frame(maxWidth: .infinity, alignment: .leading) 32 | } 33 | 34 | if userSettings.showCPUUsage { 35 | GroupBox(label: Text("Advanced")) { 36 | VStack(alignment: .leading) { 37 | Picker(selection: $userSettings.cpuUsageType, label: Text("Value:")) { 38 | Text("Moving Average (Default)").tag(CPUUsageType.movingAverage.rawValue) 39 | Text("Latest").tag(CPUUsageType.latest.rawValue) 40 | } 41 | 42 | HStack { 43 | Text("Moving Average periods:") 44 | 45 | IntTextField( 46 | value: $userSettings.cpuUsageMovingAverageRange, 47 | range: 0...1000, 48 | step: 5, 49 | width: 40) 50 | 51 | Text("seconds") 52 | 53 | Text("(Default: 30 seconds)") 54 | } 55 | 56 | Toggle(isOn: $userSettings.showProcesses) { 57 | Text("Show processes") 58 | } 59 | .switchToggleStyle() 60 | .padding(.top, 20.0) 61 | 62 | HStack { 63 | Text("Processes font size:") 64 | 65 | DoubleTextField( 66 | value: $userSettings.processesFontSize, 67 | range: 0...1000, 68 | step: 2, 69 | maximumFractionDigits: 1, 70 | width: 40) 71 | 72 | Text("pt") 73 | 74 | Text("(Default: 12 pt)") 75 | } 76 | } 77 | .padding() 78 | .frame(maxWidth: .infinity, alignment: .leading) 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Settings/SettingsCompactView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsCompactView: View { 4 | @EnvironmentObject private var userSettings: UserSettings 5 | 6 | var body: some View { 7 | VStack(alignment: .leading, spacing: 25.0) { 8 | GroupBox(label: Text("Local time")) { 9 | VStack(alignment: .leading) { 10 | Toggle(isOn: $userSettings.compactShowLocalTime) { 11 | Text("Show local time") 12 | } 13 | .switchToggleStyle() 14 | 15 | HStack { 16 | Text("Local time font size:") 17 | 18 | DoubleTextField( 19 | value: $userSettings.compactLocalTimeFontSize, 20 | range: 0...1000, 21 | step: 2, 22 | maximumFractionDigits: 1, 23 | width: 40) 24 | 25 | Text("pt") 26 | 27 | Text("(Default: 24 pt)") 28 | } 29 | 30 | HStack { 31 | Text("Local time font size for seconds:") 32 | 33 | DoubleTextField( 34 | value: $userSettings.compactLocalTimeSecondsFontSize, 35 | range: 0...1000, 36 | step: 2, 37 | maximumFractionDigits: 1, 38 | width: 40) 39 | 40 | Text("pt") 41 | 42 | Text("(Default: 12 pt)") 43 | } 44 | } 45 | .padding() 46 | .frame(maxWidth: .infinity, alignment: .leading) 47 | } 48 | 49 | GroupBox(label: Text("Local date")) { 50 | VStack(alignment: .leading) { 51 | Toggle(isOn: $userSettings.compactShowLocalDate) { 52 | Text("Show local date") 53 | } 54 | .switchToggleStyle() 55 | 56 | HStack { 57 | Text("Local date font size:") 58 | 59 | DoubleTextField( 60 | value: $userSettings.compactLocalDateFontSize, 61 | range: 0...1000, 62 | step: 2, 63 | maximumFractionDigits: 1, 64 | width: 40) 65 | 66 | Text("pt") 67 | 68 | Text("(Default: 10 pt)") 69 | } 70 | 71 | DateStylePicker() 72 | } 73 | .padding() 74 | .frame(maxWidth: .infinity, alignment: .leading) 75 | } 76 | 77 | GroupBox(label: Text("CPU Usage")) { 78 | VStack(alignment: .leading) { 79 | Toggle(isOn: $userSettings.compactShowCPUUsage) { 80 | Text("Show CPU usage") 81 | } 82 | .switchToggleStyle() 83 | .padding(.top, 20.0) 84 | 85 | HStack { 86 | Text("CPU usage font size:") 87 | 88 | DoubleTextField( 89 | value: $userSettings.compactCPUUsageFontSize, 90 | range: 0...1000, 91 | step: 2, 92 | maximumFractionDigits: 1, 93 | width: 40) 94 | 95 | Text("pt") 96 | 97 | Text("(Default: 12 pt)") 98 | } 99 | } 100 | .padding() 101 | .frame(maxWidth: .infinity, alignment: .leading) 102 | } 103 | 104 | GroupBox(label: Text("Auto compact")) { 105 | VStack(alignment: .leading) { 106 | HStack { 107 | Text("Automatically switch to compact mode if the display count is ") 108 | 109 | Picker("", selection: $userSettings.autoCompactDisplayCount) { 110 | ForEach(1...16, id: \.self) { number in 111 | Text("\(number)").tag(number) 112 | } 113 | } 114 | .frame(width: 60) 115 | 116 | Text("or less") 117 | } 118 | } 119 | .padding() 120 | .frame(maxWidth: .infinity, alignment: .leading) 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Settings/SettingsMainView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsMainView: View { 4 | @Binding var showMenuBarExtra: Bool 5 | 6 | @EnvironmentObject private var userSettings: UserSettings 7 | @ObservedObject private var openAtLogin = OpenAtLogin.shared 8 | 9 | private let windowLevels: [(String, Int)] = [ 10 | ("normal", NSWindow.Level.normal.rawValue), // 0 11 | ("floating", NSWindow.Level.floating.rawValue), // 3 12 | ("modalPanel", NSWindow.Level.modalPanel.rawValue), // 8 13 | ("mainMenu", NSWindow.Level.mainMenu.rawValue), // 24 14 | ("statusBar (Default)", NSWindow.Level.statusBar.rawValue), // 25 15 | ("popUpMenu", NSWindow.Level.popUpMenu.rawValue), // 101 16 | ("screenSaver", NSWindow.Level.screenSaver.rawValue), // 1000 17 | // ("submenu", NSWindow.Level.submenu.rawValue), // == floating 18 | // ("tornOffMenu", NSWindow.Level.tornOffMenu.rawValue), // == floating 19 | ] 20 | 21 | var body: some View { 22 | VStack(alignment: .leading, spacing: 25.0) { 23 | GroupBox(label: Text("Basic")) { 24 | VStack(alignment: .leading) { 25 | Toggle(isOn: $openAtLogin.registered) { 26 | Text("Open at login") 27 | } 28 | .switchToggleStyle() 29 | .disabled(openAtLogin.developmentBinary) 30 | .onChange(of: openAtLogin.registered) { value in 31 | OpenAtLogin.shared.update(register: value) 32 | } 33 | 34 | if openAtLogin.error.count > 0 { 35 | VStack { 36 | Label( 37 | openAtLogin.error, 38 | systemImage: "exclamationmark.circle.fill" 39 | ) 40 | .padding() 41 | } 42 | .foregroundColor(Color.errorForeground) 43 | .background(Color.errorBackground) 44 | } 45 | 46 | Toggle(isOn: $showMenuBarExtra) { 47 | Text("Show icon in menu bar") 48 | } 49 | .switchToggleStyle() 50 | 51 | Picker(selection: $userSettings.widgetAppearance, label: Text("Appearance:")) { 52 | Text("Normal").tag(WidgetAppearance.normal.rawValue) 53 | Text("Compact").tag(WidgetAppearance.compact.rawValue) 54 | Text("Auto compact").tag(WidgetAppearance.autoCompact.rawValue) 55 | Text("Hidden").tag(WidgetAppearance.hidden.rawValue) 56 | } 57 | } 58 | .padding() 59 | .frame(maxWidth: .infinity, alignment: .leading) 60 | } 61 | 62 | GroupBox(label: Text("Widget")) { 63 | VStack(alignment: .leading) { 64 | HStack(alignment: .top) { 65 | Text("Widget position:") 66 | 67 | VStack(alignment: .leading) { 68 | Picker(selection: $userSettings.widgetPosition, label: Text("Widget position:")) { 69 | Text("Bottom Left").tag(WidgetPosition.bottomLeft.rawValue) 70 | Text("Bottom Right (Default)").tag(WidgetPosition.bottomRight.rawValue) 71 | Text("Top Left").tag(WidgetPosition.topLeft.rawValue) 72 | Text("Top Right").tag(WidgetPosition.topRight.rawValue) 73 | } 74 | .labelsHidden() 75 | 76 | Toggle(isOn: $userSettings.widgetAllowOverlappingWithDock) { 77 | Text("Allow overlapping with Dock") 78 | } 79 | .switchToggleStyle() 80 | 81 | Picker(selection: $userSettings.widgetWindowLevel, label: Text("Window level:")) { 82 | ForEach(windowLevels, id: \.0) { level in 83 | Text("\(level.0): \(level.1)").tag(level.1) 84 | } 85 | } 86 | Text( 87 | "The higher the window level number, the more frontmost the window will appear" 88 | ) 89 | .font(.caption) 90 | 91 | Grid(alignment: .leadingFirstTextBaseline) { 92 | GridRow { 93 | Text("Offset X:") 94 | 95 | DoubleTextField( 96 | value: $userSettings.widgetOffsetX, 97 | range: -10000...10000, 98 | step: 10, 99 | maximumFractionDigits: 1, 100 | width: 50) 101 | 102 | Text("pt") 103 | 104 | Text("(Default: 10 pt)") 105 | } 106 | 107 | GridRow { 108 | Text("Offset Y:") 109 | 110 | DoubleTextField( 111 | value: $userSettings.widgetOffsetY, 112 | range: -10000...10000, 113 | step: 10, 114 | maximumFractionDigits: 1, 115 | width: 50) 116 | 117 | Text("pt") 118 | 119 | Text("(Default: 10 pt)") 120 | } 121 | } 122 | } 123 | } 124 | 125 | HStack { 126 | Text("Widget width:") 127 | 128 | DoubleTextField( 129 | value: $userSettings.widgetWidth, 130 | range: 0...10000, 131 | step: 10, 132 | maximumFractionDigits: 1, 133 | width: 50) 134 | 135 | Text("pt") 136 | 137 | Text("(Default: 250 pt)") 138 | } 139 | 140 | HStack { 141 | Text("Widget opacity:") 142 | 143 | Slider( 144 | value: $userSettings.widgetOpacity, 145 | in: 0.0...1.0, 146 | step: 0.1, 147 | minimumValueLabel: Text("Clear"), 148 | maximumValueLabel: Text("Colored"), 149 | label: { 150 | Text("") 151 | } 152 | ) 153 | } 154 | 155 | Picker( 156 | selection: $userSettings.widgetScreen, 157 | label: Text("Widget screen when using multiple displays:") 158 | ) { 159 | Text("Primary screen (Default)").tag(WidgetScreen.primary.rawValue) 160 | Text("Bottom Left screen").tag(WidgetScreen.bottomLeft.rawValue) 161 | Text("Bottom Right screen").tag(WidgetScreen.bottomRight.rawValue) 162 | Text("Top Left screen").tag(WidgetScreen.topLeft.rawValue) 163 | Text("Top Right screen").tag(WidgetScreen.topRight.rawValue) 164 | Text("Leftmost (top)").tag(WidgetScreen.leftTop.rawValue) 165 | Text("Leftmost (bottom)").tag(WidgetScreen.leftBottom.rawValue) 166 | Text("Rightmost (top)").tag(WidgetScreen.rightTop.rawValue) 167 | Text("Rightmost (bottom)").tag(WidgetScreen.rightBottom.rawValue) 168 | } 169 | 170 | HStack { 171 | Text("Widget fade-out duration:") 172 | 173 | DoubleTextField( 174 | value: $userSettings.widgetFadeOutDuration, 175 | range: 0...10000, 176 | step: 100, 177 | maximumFractionDigits: 1, 178 | width: 50) 179 | 180 | Text("milliseconds") 181 | 182 | Text("(Default: 500 ms)") 183 | } 184 | } 185 | .padding() 186 | .frame(maxWidth: .infinity, alignment: .leading) 187 | } 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Settings/SettingsOperatingSystemView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsOperatingSystemView: View { 4 | @EnvironmentObject private var userSettings: UserSettings 5 | 6 | var body: some View { 7 | VStack(alignment: .leading, spacing: 25.0) { 8 | GroupBox(label: Text("Operating system")) { 9 | VStack(alignment: .leading) { 10 | Toggle(isOn: $userSettings.showOperatingSystem) { 11 | Text("Show macOS version") 12 | } 13 | .switchToggleStyle() 14 | 15 | HStack { 16 | Text("Font size:") 17 | 18 | DoubleTextField( 19 | value: $userSettings.operatingSystemFontSize, 20 | range: 0...1000, 21 | step: 2, 22 | maximumFractionDigits: 1, 23 | width: 40) 24 | 25 | Text("pt") 26 | 27 | Text("(Default: 14 pt)") 28 | } 29 | } 30 | .padding() 31 | .frame(maxWidth: .infinity, alignment: .leading) 32 | } 33 | 34 | if userSettings.showOperatingSystem { 35 | GroupBox(label: Text("Advanced")) { 36 | VStack(alignment: .leading) { 37 | Toggle(isOn: $userSettings.showUptime) { 38 | Text("Show uptime") 39 | } 40 | .switchToggleStyle() 41 | 42 | Toggle(isOn: $userSettings.showAwakeTime) { 43 | Text("Show awake time") 44 | } 45 | .switchToggleStyle() 46 | 47 | Toggle(isOn: $userSettings.showHostName) { 48 | Text("Show host name") 49 | } 50 | .switchToggleStyle() 51 | 52 | Toggle(isOn: $userSettings.showRootVolumeName) { 53 | Text("Show root volume name") 54 | } 55 | .switchToggleStyle() 56 | 57 | Toggle(isOn: $userSettings.showUserName) { 58 | Text("Show user name") 59 | } 60 | .switchToggleStyle() 61 | 62 | Toggle(isOn: $userSettings.showAppleAccount) { 63 | Text("Show Apple Account") 64 | } 65 | .switchToggleStyle() 66 | } 67 | .padding() 68 | .frame(maxWidth: .infinity, alignment: .leading) 69 | } 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Settings/SettingsTimeView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsTimeView: View { 4 | @EnvironmentObject private var userSettings: UserSettings 5 | 6 | var body: some View { 7 | VStack(alignment: .leading, spacing: 25.0) { 8 | GroupBox(label: Text("Local time")) { 9 | VStack(alignment: .leading) { 10 | Toggle(isOn: $userSettings.showLocalTime) { 11 | Text("Show local time") 12 | } 13 | .switchToggleStyle() 14 | 15 | HStack { 16 | Text("Font size:") 17 | 18 | DoubleTextField( 19 | value: $userSettings.localTimeFontSize, 20 | range: 0...1000, 21 | step: 2, 22 | maximumFractionDigits: 1, 23 | width: 40) 24 | 25 | Text("pt") 26 | 27 | Text("(Default: 36 pt)") 28 | } 29 | 30 | HStack { 31 | Text("Font size for seconds:") 32 | 33 | DoubleTextField( 34 | value: $userSettings.localTimeSecondsFontSize, 35 | range: 0...1000, 36 | step: 2, 37 | maximumFractionDigits: 1, 38 | width: 40) 39 | 40 | Text("pt") 41 | 42 | Text("(Default: 18 pt)") 43 | } 44 | } 45 | .padding() 46 | .frame(maxWidth: .infinity, alignment: .leading) 47 | } 48 | 49 | GroupBox(label: Text("Local date")) { 50 | VStack(alignment: .leading) { 51 | Toggle(isOn: $userSettings.showLocalDate) { 52 | Text("Show local date") 53 | } 54 | .switchToggleStyle() 55 | 56 | HStack { 57 | Text("Local date font size:") 58 | 59 | DoubleTextField( 60 | value: $userSettings.localDateFontSize, 61 | range: 0...1000, 62 | step: 2, 63 | maximumFractionDigits: 1, 64 | width: 40) 65 | 66 | Text("pt") 67 | 68 | Text("(Default: 12 pt)") 69 | } 70 | 71 | DateStylePicker() 72 | } 73 | .padding() 74 | .frame(maxWidth: .infinity, alignment: .leading) 75 | } 76 | 77 | GroupBox(label: Text("Other time zones")) { 78 | VStack(alignment: .leading) { 79 | ForEach($userSettings.timeZoneTimeSettings) { timeZoneTimeSetting in 80 | HStack { 81 | Toggle(isOn: timeZoneTimeSetting.show) { 82 | Text("Show") 83 | } 84 | .switchToggleStyle() 85 | 86 | TimeZonePickerView(abbreviation: timeZoneTimeSetting.abbreviation) 87 | } 88 | } 89 | 90 | Divider() 91 | 92 | Grid(alignment: .leadingFirstTextBaseline) { 93 | GridRow { 94 | Text("Date font size:") 95 | .gridColumnAlignment(.trailing) 96 | 97 | DoubleTextField( 98 | value: $userSettings.timeZoneDateFontSize, 99 | range: 0...1000, 100 | step: 2, 101 | maximumFractionDigits: 1, 102 | width: 40) 103 | 104 | Text("pt") 105 | 106 | Text("(Default: 10 pt)") 107 | } 108 | 109 | GridRow { 110 | Text("Time font size:") 111 | 112 | DoubleTextField( 113 | value: $userSettings.timeZoneTimeFontSize, 114 | range: 0...1000, 115 | step: 2, 116 | maximumFractionDigits: 1, 117 | width: 40) 118 | 119 | Text("pt") 120 | 121 | Text("(Default: 12 pt)") 122 | } 123 | } 124 | } 125 | .padding() 126 | .frame(maxWidth: .infinity, alignment: .leading) 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Settings/SettingsUpdateView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsUpdateView: View { 4 | let version = 5 | Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "" 6 | 7 | var body: some View { 8 | VStack(alignment: .leading, spacing: 25.0) { 9 | GroupBox(label: Text("Updates")) { 10 | VStack(alignment: .leading) { 11 | Text("TrueWidget version \(version)") 12 | 13 | HStack { 14 | CheckForUpdatesView() 15 | 16 | Spacer() 17 | 18 | CheckForBetaUpdatesView() 19 | } 20 | } 21 | .padding() 22 | .frame(maxWidth: .infinity, alignment: .leading) 23 | } 24 | 25 | GroupBox(label: Text("Websites")) { 26 | HStack(spacing: 20.0) { 27 | Button( 28 | action: { NSWorkspace.shared.open(URL(string: "https://truewidget.pqrs.org")!) }, 29 | label: { 30 | Label("Open official website", systemImage: "house") 31 | }) 32 | 33 | Button( 34 | action: { 35 | NSWorkspace.shared.open(URL(string: "https://github.com/pqrs-org/TrueWidget")!) 36 | }, 37 | label: { 38 | Label("Open GitHub (source code)", systemImage: "hammer") 39 | }) 40 | } 41 | .padding() 42 | .frame(maxWidth: .infinity, alignment: .leading) 43 | } 44 | } 45 | } 46 | 47 | // This additional view is needed for the disabled state on the menu item to work properly before Monterey. 48 | // See https://stackoverflow.com/questions/68553092/menu-not-updating-swiftui-bug for more information 49 | struct CheckForUpdatesView: View { 50 | @ObservedObject var updater = Updater.shared 51 | 52 | var body: some View { 53 | Button( 54 | action: { updater.checkForUpdatesStableOnly() }, 55 | label: { 56 | Label("Check for updates...", systemImage: "network") 57 | } 58 | ) 59 | .disabled(!updater.canCheckForUpdates) 60 | } 61 | } 62 | 63 | // This additional view is needed for the disabled state on the menu item to work properly before Monterey. 64 | // See https://stackoverflow.com/questions/68553092/menu-not-updating-swiftui-bug for more information 65 | struct CheckForBetaUpdatesView: View { 66 | @ObservedObject var updater = Updater.shared 67 | 68 | var body: some View { 69 | Button( 70 | action: { updater.checkForUpdatesWithBetaVersion() }, 71 | label: { 72 | Label("Check for beta updates...", systemImage: "sparkles") 73 | } 74 | ) 75 | .disabled(!updater.canCheckForUpdates) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/Settings/SettingsXcodeView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsXcodeView: View { 4 | @EnvironmentObject private var userSettings: UserSettings 5 | 6 | var body: some View { 7 | VStack(alignment: .leading, spacing: 25.0) { 8 | GroupBox(label: Text("Xcode")) { 9 | VStack(alignment: .leading) { 10 | Toggle(isOn: $userSettings.showXcode) { 11 | Text("Show Xcode bundle path") 12 | } 13 | .switchToggleStyle() 14 | 15 | HStack { 16 | Text("Font size:") 17 | 18 | DoubleTextField( 19 | value: $userSettings.xcodeFontSize, 20 | range: 0...1000, 21 | step: 2, 22 | maximumFractionDigits: 1, 23 | width: 40) 24 | 25 | Text("pt") 26 | 27 | Text("(Default: 12 pt)") 28 | } 29 | } 30 | .padding() 31 | .frame(maxWidth: .infinity, alignment: .leading) 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/SettingsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | enum TabTag: String { 4 | case main 5 | case cpuUsage 6 | case time 7 | case operatingSystem 8 | case xcode 9 | case bundle 10 | case compact 11 | case update 12 | case action 13 | } 14 | 15 | struct SettingsView: View { 16 | @Binding var showMenuBarExtra: Bool 17 | 18 | @State private var selection: TabTag = .main 19 | 20 | var body: some View { 21 | TabView(selection: $selection) { 22 | SettingsMainView(showMenuBarExtra: $showMenuBarExtra) 23 | .tabItem { 24 | Label("Main", systemImage: "gearshape") 25 | } 26 | .tag(TabTag.main) 27 | 28 | SettingsCPUUsageView() 29 | .tabItem { 30 | Label("CPU", systemImage: "cube") 31 | } 32 | .tag(TabTag.cpuUsage) 33 | 34 | SettingsTimeView() 35 | .tabItem { 36 | Label("Time", systemImage: "cube") 37 | } 38 | .tag(TabTag.time) 39 | 40 | SettingsOperatingSystemView() 41 | .tabItem { 42 | Label("System", systemImage: "cube") 43 | } 44 | .tag(TabTag.operatingSystem) 45 | 46 | SettingsXcodeView() 47 | .tabItem { 48 | Label("Xcode", systemImage: "cube") 49 | } 50 | .tag(TabTag.xcode) 51 | 52 | SettingsBundleView() 53 | .tabItem { 54 | Label("App", systemImage: "cube") 55 | } 56 | .tag(TabTag.bundle) 57 | 58 | SettingsCompactView() 59 | .tabItem { 60 | Label("Compact", systemImage: "cube") 61 | } 62 | .tag(TabTag.compact) 63 | 64 | SettingsUpdateView() 65 | .tabItem { 66 | Label("Update", systemImage: "network") 67 | } 68 | .tag(TabTag.update) 69 | 70 | SettingsActionView() 71 | .tabItem { 72 | Label("Quit, Restart", systemImage: "xmark") 73 | } 74 | .tag(TabTag.action) 75 | } 76 | .scenePadding() 77 | .frame(width: 600) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/Views/TimeZonePickerView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct TimeZonePickerView: View { 4 | class Source { 5 | struct TimeZoneEntry: Identifiable { 6 | let id = UUID() 7 | let abbreviation: String // JST 8 | let identifier: String // Asia/Tokyo 9 | let secondsFromGMT: Int 10 | let label: String 11 | 12 | init(abbreviation: String, identifier: String) { 13 | self.abbreviation = abbreviation 14 | self.identifier = identifier 15 | 16 | let timeZone = TimeZone(identifier: identifier) 17 | secondsFromGMT = timeZone?.secondsFromGMT() ?? 0 18 | let minutesFromGMT = abs(secondsFromGMT) / 60 19 | 20 | var label = String( 21 | format: "GMT%@%02d:%02d\t", 22 | secondsFromGMT < 0 ? "-" : "+", 23 | minutesFromGMT / 60, 24 | minutesFromGMT % 60 25 | ) 26 | 27 | if abbreviation == identifier { 28 | label = "\(label)\(abbreviation)" 29 | } else { 30 | label = 31 | "\(label)\(abbreviation.padding(toLength: 7, withPad: " ", startingAt: 0))\t(\(identifier))" 32 | } 33 | 34 | self.label = label 35 | } 36 | } 37 | 38 | static let shared = Source() 39 | 40 | var timeZones: [TimeZoneEntry] = [] 41 | 42 | init() { 43 | TimeZone.abbreviationDictionary.forEach { d in 44 | let abbreviation = d.key 45 | let identifier = d.value 46 | timeZones.append(TimeZoneEntry(abbreviation: abbreviation, identifier: identifier)) 47 | } 48 | 49 | timeZones.sort { 50 | if $0.secondsFromGMT != $1.secondsFromGMT { 51 | return $0.secondsFromGMT < $1.secondsFromGMT 52 | } 53 | return $0.abbreviation < $1.abbreviation 54 | } 55 | } 56 | } 57 | 58 | @Binding private(set) var abbreviation: String 59 | // If passing $abbreviation directly to the Picker's selection, changes may not be reflected in the Picker because $abbreviation might not be an ObservableObject. 60 | // Instead, pass $value to the Picker and manually update the changes. 61 | @State private(set) var value: String 62 | 63 | init(abbreviation: Binding) { 64 | self._abbreviation = abbreviation 65 | self.value = abbreviation.wrappedValue 66 | } 67 | 68 | var body: some View { 69 | Picker("", selection: $value) { 70 | ForEach(Source.shared.timeZones) { timeZone in 71 | Text(timeZone.label) 72 | .tag(timeZone.abbreviation) 73 | } 74 | } 75 | .pickerStyle(.menu) 76 | .onChange(of: value) { _ in 77 | abbreviation = value 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/WidgetSource/Bundle.swift: -------------------------------------------------------------------------------- 1 | import AsyncAlgorithms 2 | import Combine 3 | import Foundation 4 | 5 | extension WidgetSource { 6 | public class Bundle: ObservableObject { 7 | private var userSettings: UserSettings 8 | 9 | private var helperConnection: NSXPCConnection? 10 | private var helperProxy: HelperProtocol? 11 | 12 | @Published public var bundleVersions: [String: [String: String]] = [:] 13 | private let timer: AsyncTimerSequence 14 | private var timerTask: Task? 15 | 16 | typealias ProxyResponse = [String: [String: String]] 17 | 18 | private let proxyResponseStream: AsyncStream 19 | private let proxyResponseContinuation: AsyncStream.Continuation 20 | private var proxyResponseTask: Task? 21 | 22 | init(userSettings: UserSettings) { 23 | self.userSettings = userSettings 24 | 25 | timer = AsyncTimerSequence( 26 | interval: .seconds(3), 27 | clock: .continuous 28 | ) 29 | 30 | var continuation: AsyncStream.Continuation! 31 | proxyResponseStream = AsyncStream { continuation = $0 } 32 | proxyResponseContinuation = continuation 33 | 34 | timerTask = Task { @MainActor in 35 | update() 36 | 37 | for await _ in timer { 38 | update() 39 | } 40 | } 41 | 42 | proxyResponseTask = Task { @MainActor in 43 | // When resuming from sleep or in similar situations, 44 | // responses from the proxy may be called consecutively within a short period. 45 | // To avoid frequent UI updates in such cases, throttle is used to control the update frequency. 46 | for await versions in proxyResponseStream._throttle( 47 | for: .seconds(1), latest: true) 48 | { 49 | bundleVersions = versions 50 | } 51 | } 52 | } 53 | 54 | // Since timerTask strongly references self, make sure to call cancelTimer when Bundle is no longer used. 55 | func cancelTimer() { 56 | timerTask?.cancel() 57 | proxyResponseTask?.cancel() 58 | } 59 | 60 | @MainActor 61 | private func update() { 62 | HelperClient.shared.proxy?.bundleVersions( 63 | paths: userSettings.bundleSettings.filter({ $0.show && !$0.path.isEmpty }).map({ $0.path }) 64 | ) { versions in 65 | self.proxyResponseContinuation.yield(versions) 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/WidgetSource/CPUUsage.swift: -------------------------------------------------------------------------------- 1 | import AsyncAlgorithms 2 | import Combine 3 | import Foundation 4 | 5 | extension WidgetSource { 6 | public class CPUUsage: ObservableObject { 7 | private var userSettings: UserSettings 8 | 9 | @Published public var usageInteger: Int = 0 10 | @Published public var usageDecimal: Int = 0 11 | 12 | @Published public var usageAverageInteger: Int = 0 13 | @Published public var usageAverageDecimal: Int = 0 14 | 15 | private var usageHistory: [Double] = [] 16 | 17 | @Published public var processes: [[String: String]] = [[:], [:], [:]] 18 | 19 | private let timer: AsyncTimerSequence 20 | private var timerTask: Task? 21 | 22 | typealias ProxyResponse = (Double, [[String: String]]) 23 | 24 | private let proxyResponseStream: AsyncStream 25 | private let proxyResponseContinuation: AsyncStream.Continuation 26 | private var proxyResponseTask: Task? 27 | 28 | init(userSettings: UserSettings) { 29 | self.userSettings = userSettings 30 | 31 | timer = AsyncTimerSequence( 32 | interval: .seconds(3), 33 | clock: .continuous 34 | ) 35 | 36 | var continuation: AsyncStream.Continuation! 37 | proxyResponseStream = AsyncStream { continuation = $0 } 38 | proxyResponseContinuation = continuation 39 | 40 | timerTask = Task { @MainActor in 41 | update() 42 | 43 | for await _ in timer { 44 | update() 45 | } 46 | } 47 | 48 | proxyResponseTask = Task { @MainActor in 49 | // When resuming from sleep or in similar situations, 50 | // responses from the proxy may be called consecutively within a short period. 51 | // To avoid frequent UI updates in such cases, throttle is used to control the update frequency. 52 | for await (responseCPUUsage, responseProcesses) in proxyResponseStream._throttle( 53 | for: .seconds(1), latest: true) 54 | { 55 | usageInteger = Int(floor(responseCPUUsage)) 56 | usageDecimal = Int(floor((responseCPUUsage) * 100)) % 100 57 | 58 | // 59 | // Calculate average 60 | // 61 | 62 | let averageRange = max(userSettings.cpuUsageMovingAverageRange, 1) 63 | usageHistory.append(responseCPUUsage) 64 | while usageHistory.count > averageRange { 65 | usageHistory.remove(at: 0) 66 | } 67 | 68 | let usageAverage = usageHistory.reduce(0.0, +) / Double(usageHistory.count) 69 | usageAverageInteger = Int(floor(usageAverage)) 70 | usageAverageDecimal = Int(floor((usageAverage) * 100)) % 100 71 | 72 | // 73 | // Processes 74 | // 75 | 76 | var newProcesses = responseProcesses 77 | while newProcesses.count < 3 { 78 | newProcesses.append([:]) 79 | } 80 | processes = newProcesses 81 | } 82 | } 83 | } 84 | 85 | // Since timerTask strongly references self, make sure to call cancelTimer when CPUUsage is no longer used. 86 | func cancelTimer() { 87 | timerTask?.cancel() 88 | proxyResponseTask?.cancel() 89 | 90 | Task { @MainActor in 91 | HelperClient.shared.proxy?.stopTopCommand() 92 | } 93 | } 94 | 95 | @MainActor 96 | private func update() { 97 | // To get the CPU utilization of a process (especially kernel_task information), 98 | // as far as I've been able to find out, we need to use the results of the top command or need administrator privileges. 99 | // Since the top command has a setuid bit and can be used without privilege, we run top command in a helper process and use the result. 100 | 101 | HelperClient.shared.proxy?.topCommand { cpuUsage, processes in 102 | self.proxyResponseContinuation.yield((cpuUsage, processes)) 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/WidgetSource/OperatingSystem.swift: -------------------------------------------------------------------------------- 1 | import AsyncAlgorithms 2 | import Combine 3 | import Foundation 4 | 5 | extension WidgetSource { 6 | public class OperatingSystem: ObservableObject { 7 | private var userSettings: UserSettings 8 | 9 | @Published public var version = "" 10 | @Published public var uptime = "" 11 | @Published public var awakeTime = "" 12 | @Published public var hostName = "" 13 | @Published public var rootVolumeName = "" 14 | @Published public var userName = "" 15 | @Published public var appleAccount = "" 16 | 17 | private let timer: AsyncTimerSequence 18 | private var timerTask: Task? 19 | 20 | typealias ProxyResponse = String 21 | 22 | private let proxyResponseStream: AsyncStream 23 | private let proxyResponseContinuation: AsyncStream.Continuation 24 | private var proxyResponseTask: Task? 25 | 26 | init(userSettings: UserSettings) { 27 | self.userSettings = userSettings 28 | 29 | timer = AsyncTimerSequence( 30 | interval: .seconds(3), 31 | clock: .continuous 32 | ) 33 | 34 | var continuation: AsyncStream.Continuation! 35 | proxyResponseStream = AsyncStream { continuation = $0 } 36 | proxyResponseContinuation = continuation 37 | 38 | timerTask = Task { @MainActor in 39 | update() 40 | 41 | for await _ in timer { 42 | update() 43 | } 44 | } 45 | 46 | proxyResponseTask = Task { @MainActor in 47 | // When resuming from sleep or in similar situations, 48 | // responses from the proxy may be called consecutively within a short period. 49 | // To avoid frequent UI updates in such cases, throttle is used to control the update frequency. 50 | for await account in proxyResponseStream._throttle( 51 | for: .seconds(1), latest: true) where appleAccount != account 52 | { 53 | appleAccount = account 54 | } 55 | } 56 | 57 | // We have to use `operatingSystemVersionString` instead of `operatingSystemVersion` because 58 | // `operatingSystemVersion` does not have a security update version, such as "(a)" in "13.3.1 (a)". 59 | // 60 | // Note: operatingSystemVersionString returns "Version 13.3.1 (a) (Build 22E772610a)" 61 | version = ProcessInfo.processInfo.operatingSystemVersionString.replacingOccurrences( 62 | of: "Version ", with: "") 63 | if let index = version.range(of: "(Build ")?.lowerBound { 64 | version = String(version[.. String { 79 | let rootURL = NSURL.fileURL(withPath: "/", isDirectory: true) 80 | if let resourceValues = try? rootURL.resourceValues(forKeys: [.volumeNameKey]) { 81 | return resourceValues.volumeName ?? "" 82 | } 83 | 84 | return "" 85 | } 86 | 87 | @MainActor 88 | private func update() { 89 | if userSettings.showUptime || userSettings.showAwakeTime { 90 | let uptimeSeconds = getSecondsFromBoot() 91 | let awakeTimeSeconds = Int(ProcessInfo.processInfo.systemUptime) 92 | 93 | uptime = formatUptime(seconds: uptimeSeconds) 94 | awakeTime = formatUptime(seconds: awakeTimeSeconds) 95 | 96 | if let uptimeSeconds = uptimeSeconds { 97 | if uptimeSeconds > 0 { 98 | let ratio = min(100.0, Double(awakeTimeSeconds) / Double(uptimeSeconds) * 100.0) 99 | awakeTime += " (\(String(format: "%.02f", ratio))%)" 100 | } 101 | } 102 | } 103 | 104 | if userSettings.showHostName { 105 | // `ProcessInfo.processInfo.hostName` is not reflected the host name changes after the application is launched. 106 | // So, we have to use `gethostname`.` 107 | let length = 128 108 | var buffer = [CChar](repeating: 0, count: length) 109 | let error = gethostname(&buffer, length) 110 | if error == 0 { 111 | if let name = String(utf8String: buffer) { 112 | var h = name 113 | if let index = name.firstIndex(of: ".") { 114 | h = String(name[...index].dropLast()) 115 | } 116 | 117 | if hostName != h { 118 | hostName = h 119 | } 120 | } 121 | } 122 | } 123 | 124 | if userSettings.showAppleAccount { 125 | HelperClient.shared.proxy?.appleAccount { account in 126 | self.proxyResponseContinuation.yield(account) 127 | } 128 | } 129 | } 130 | 131 | // ProcessInfo.processInfo.systemUptime does not return seconds from boot. 132 | // It returns how long has the CPU been running. 133 | // Therefore, we need to use sysctl to get the boot time and calculate it. 134 | // https://forums.developer.apple.com/forums/thread/98682 135 | private func getSecondsFromBoot() -> Int? { 136 | var bootTime = timeval() 137 | var size = MemoryLayout.size 138 | 139 | let result = sysctlbyname("kern.boottime", &bootTime, &size, nil, 0) 140 | 141 | if result != 0 { 142 | return nil 143 | } 144 | 145 | let bootDate = Date(timeIntervalSince1970: TimeInterval(bootTime.tv_sec)) 146 | let uptime = Date().timeIntervalSince(bootDate) 147 | return Int(uptime) 148 | } 149 | 150 | private func formatUptime(seconds: Int?) -> String { 151 | if let seconds = seconds { 152 | let days = seconds / (24 * 3600) 153 | let hours = (seconds % (24 * 3600)) / 3600 154 | let minutes = (seconds % 3600) / 60 155 | 156 | var daysString = "" 157 | if days > 1 { 158 | daysString = "\(days) days, " 159 | } else if days == 1 { 160 | daysString = "1 day, " 161 | } 162 | 163 | return String( 164 | format: "%@%02d:%02d", 165 | daysString, 166 | hours, 167 | minutes 168 | ) 169 | } else { 170 | return "---" 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/WidgetSource/Time.swift: -------------------------------------------------------------------------------- 1 | import AsyncAlgorithms 2 | import Combine 3 | import SwiftUI 4 | 5 | extension WidgetSource { 6 | public class Time: ObservableObject { 7 | public struct DateTime: Identifiable { 8 | public let id = UUID() 9 | public let date: String 10 | public let hour: Int 11 | public let minute: Int 12 | public let second: Int 13 | 14 | init(now: Date, timeZone: TimeZone, dateStyle: DateStyle) { 15 | let formatter1 = DateFormatter() 16 | formatter1.timeZone = timeZone 17 | 18 | let formatter2 = DateFormatter() 19 | formatter2.timeZone = timeZone 20 | 21 | if let languageCode = Locale.preferredLanguages.first { 22 | formatter1.locale = Locale(identifier: languageCode) 23 | formatter2.locale = formatter1.locale 24 | } else { 25 | formatter1.locale = Locale.current 26 | formatter2.locale = formatter1.locale 27 | } 28 | 29 | switch dateStyle { 30 | case .rfc3339: 31 | formatter1.locale = Locale(identifier: "en_US_POSIX") 32 | formatter1.dateFormat = "yyyy-MM-dd" 33 | date = formatter1.string(from: now) 34 | 35 | case .rfc3339WithDayName: 36 | formatter1.locale = Locale(identifier: "en_US_POSIX") 37 | formatter1.dateFormat = "yyyy-MM-dd (EEE)" 38 | date = formatter1.string(from: now) 39 | 40 | case .short: 41 | formatter1.dateStyle = .short 42 | formatter1.timeStyle = .none 43 | date = formatter1.string(from: now) 44 | 45 | case .shortWithDayName: 46 | formatter1.dateStyle = .short 47 | formatter1.timeStyle = .none 48 | formatter2.setLocalizedDateFormatFromTemplate("EEE") 49 | date = String( 50 | format: "%@ (%@)", formatter1.string(from: now), formatter2.string(from: now)) 51 | 52 | case .medium: 53 | formatter1.dateStyle = .medium 54 | formatter1.timeStyle = .none 55 | date = formatter1.string(from: now) 56 | 57 | case .mediumWithDayName: 58 | formatter1.dateStyle = .medium 59 | formatter1.timeStyle = .none 60 | formatter2.setLocalizedDateFormatFromTemplate("EEE") 61 | date = String( 62 | format: "%@ (%@)", formatter1.string(from: now), formatter2.string(from: now)) 63 | 64 | case .long: 65 | formatter1.dateStyle = .long 66 | formatter1.timeStyle = .none 67 | date = formatter1.string(from: now) 68 | 69 | case .longWithDayName: 70 | formatter1.dateStyle = .long 71 | formatter1.timeStyle = .none 72 | formatter2.setLocalizedDateFormatFromTemplate("EEE") 73 | date = String( 74 | format: "%@ (%@)", formatter1.string(from: now), formatter2.string(from: now)) 75 | 76 | case .full: 77 | formatter1.dateStyle = .full 78 | formatter1.timeStyle = .none 79 | date = formatter1.string(from: now) 80 | } 81 | 82 | var calendar = Calendar.current 83 | calendar.timeZone = timeZone 84 | 85 | hour = calendar.component(.hour, from: now) 86 | minute = calendar.component(.minute, from: now) 87 | second = calendar.component(.second, from: now) 88 | } 89 | } 90 | 91 | private var userSettings: UserSettings 92 | 93 | @Published public var localTime: DateTime? 94 | @Published public var timeZoneTimes: [String: DateTime] = [:] 95 | 96 | private var cancellables = Set() 97 | private let timer: AsyncTimerSequence 98 | private var timerTask: Task? 99 | 100 | init(userSettings: UserSettings) { 101 | self.userSettings = userSettings 102 | 103 | timer = AsyncTimerSequence( 104 | interval: .seconds(1), 105 | clock: .continuous 106 | ) 107 | 108 | timerTask = Task { @MainActor in 109 | update() 110 | 111 | for await _ in timer { 112 | update() 113 | } 114 | } 115 | 116 | userSettings.objectWillChange.sink { [weak self] _ in 117 | Task { @MainActor in 118 | guard let self = self else { return } 119 | self.update() 120 | } 121 | }.store(in: &cancellables) 122 | } 123 | 124 | // Since timerTask strongly references self, make sure to call cancelTimer when Time is no longer used. 125 | func cancelTimer() { 126 | timerTask?.cancel() 127 | } 128 | 129 | private func update() { 130 | let now = Date() 131 | self.updateLocalTime(now) 132 | self.updateTimeZoneTimes(now) 133 | } 134 | 135 | private func updateLocalTime(_ now: Date) { 136 | if !userSettings.showLocalTime && !userSettings.showLocalDate { 137 | return 138 | } 139 | 140 | if let dateStyle = DateStyle(rawValue: userSettings.dateStyle) { 141 | localTime = DateTime(now: now, timeZone: Calendar.current.timeZone, dateStyle: dateStyle) 142 | } 143 | } 144 | 145 | private func updateTimeZoneTimes(_ now: Date) { 146 | var times: [String: DateTime] = [:] 147 | 148 | userSettings.timeZoneTimeSettings.forEach { setting in 149 | if setting.show && times[setting.abbreviation] == nil { 150 | if let identifier = TimeZone.abbreviationDictionary[setting.abbreviation] { 151 | if let timeZone = TimeZone(identifier: identifier) { 152 | if let dateStyle = DateStyle(rawValue: userSettings.dateStyle) { 153 | times[setting.abbreviation] = DateTime( 154 | now: now, timeZone: timeZone, dateStyle: dateStyle) 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | timeZoneTimes = times 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/WidgetSource/WidgetSource.swift: -------------------------------------------------------------------------------- 1 | public struct WidgetSource { 2 | } 3 | -------------------------------------------------------------------------------- /src/TrueWidget/swift/WidgetSource/Xcode.swift: -------------------------------------------------------------------------------- 1 | import AsyncAlgorithms 2 | import Combine 3 | import Foundation 4 | 5 | extension WidgetSource { 6 | public class Xcode: ObservableObject { 7 | public enum PathState { 8 | case notInstalled 9 | case defaultPath 10 | case nonDefaultPath 11 | } 12 | 13 | @Published public var path = "" 14 | @Published public var pathState = PathState.notInstalled 15 | 16 | private let timer: AsyncTimerSequence 17 | private var timerTask: Task? 18 | 19 | init() { 20 | timer = AsyncTimerSequence( 21 | interval: .seconds(3), 22 | clock: .continuous 23 | ) 24 | 25 | timerTask = Task { @MainActor in 26 | update() 27 | 28 | for await _ in timer { 29 | update() 30 | } 31 | } 32 | } 33 | 34 | // Since timerTask strongly references self, make sure to call cancelTimer when Xcode is no longer used. 35 | func cancelTimer() { 36 | timerTask?.cancel() 37 | } 38 | 39 | private func update() { 40 | let (bundlePath, pathState) = self.xcodePath() 41 | 42 | if self.path != bundlePath { 43 | self.path = bundlePath 44 | } 45 | 46 | if self.pathState != pathState { 47 | self.pathState = pathState 48 | } 49 | } 50 | 51 | private func xcodePath() -> (String, PathState) { 52 | let command = "/usr/bin/xcode-select" 53 | 54 | if FileManager.default.fileExists(atPath: command) { 55 | let xcodeSelectCommand = Process() 56 | xcodeSelectCommand.launchPath = command 57 | xcodeSelectCommand.arguments = [ 58 | "--print-path" 59 | ] 60 | 61 | xcodeSelectCommand.environment = [ 62 | "LC_ALL": "C" 63 | ] 64 | 65 | let pipe = Pipe() 66 | xcodeSelectCommand.standardOutput = pipe 67 | 68 | xcodeSelectCommand.launch() 69 | xcodeSelectCommand.waitUntilExit() 70 | 71 | if let data = try? pipe.fileHandleForReading.readToEnd(), 72 | let fullPath = String(data: data, encoding: .utf8) 73 | { 74 | if fullPath.count > 0 { 75 | var bundlePath = "" 76 | 77 | if let range = fullPath.range(of: ".app/") { 78 | let startIndex = fullPath.startIndex 79 | let endIndex = fullPath.index(before: range.upperBound) 80 | bundlePath = String(fullPath[startIndex.. CGPoint { 20 | var origin = NSPoint.zero 21 | 22 | if let mainScreen = NSScreen.main { 23 | // 24 | // Determine screen 25 | // 26 | 27 | var screen = mainScreen 28 | if let widgetScreen = WidgetScreen(rawValue: userSettings.widgetScreen) { 29 | NSScreen.screens.forEach { s in 30 | // 31 | // +--------------------+--------------------+--------------------+ 32 | // | | | | 33 | // | | | | 34 | // | | | | 35 | // |(-100,100) |(0,100) |(100,100) | 36 | // +--------------------+--------------------+--------------------+ 37 | // | | | | 38 | // | | main | | 39 | // | | | | 40 | // |(-100,0) |(0,0) |(100,0) | 41 | // +--------------------+--------------------+--------------------+ 42 | // | | | | 43 | // | | | | 44 | // | | | | 45 | // |(-100,-100) |(0,-100) |(100,-100) | 46 | // +--------------------+--------------------+--------------------+ 47 | // 48 | 49 | switch widgetScreen { 50 | case WidgetScreen.primary: 51 | if s.frame.origin == NSPoint.zero { 52 | screen = s 53 | } 54 | case WidgetScreen.bottomLeft: 55 | if s.frame.origin.x <= screen.frame.origin.x, 56 | s.frame.origin.y <= screen.frame.origin.y 57 | { 58 | screen = s 59 | } 60 | case WidgetScreen.bottomRight: 61 | if s.frame.origin.x >= screen.frame.origin.x, 62 | s.frame.origin.y <= screen.frame.origin.y 63 | { 64 | screen = s 65 | } 66 | case WidgetScreen.topLeft: 67 | if s.frame.origin.x <= screen.frame.origin.x, 68 | s.frame.origin.y >= screen.frame.origin.y 69 | { 70 | screen = s 71 | } 72 | case WidgetScreen.topRight: 73 | if s.frame.origin.x >= screen.frame.origin.x, 74 | s.frame.origin.y >= screen.frame.origin.y 75 | { 76 | screen = s 77 | } 78 | case WidgetScreen.leftTop: 79 | // Leftmost screen (top) 80 | if s.frame.origin.x < screen.frame.origin.x { 81 | screen = s 82 | } else if s.frame.origin.x == screen.frame.origin.x { 83 | if s.frame.origin.y > screen.frame.origin.y { 84 | screen = s 85 | } 86 | } 87 | case WidgetScreen.leftBottom: 88 | // Leftmost screen (bottom) 89 | if s.frame.origin.x < screen.frame.origin.x { 90 | screen = s 91 | } else if s.frame.origin.x == screen.frame.origin.x { 92 | if s.frame.origin.y < screen.frame.origin.y { 93 | screen = s 94 | } 95 | } 96 | case WidgetScreen.rightTop: 97 | // Rightmost screen (top) 98 | if s.frame.origin.x > screen.frame.origin.x { 99 | screen = s 100 | } else if s.frame.origin.x == screen.frame.origin.x { 101 | if s.frame.origin.y > screen.frame.origin.y { 102 | screen = s 103 | } 104 | } 105 | case WidgetScreen.rightBottom: 106 | // Rightmost screen (bottom) 107 | if s.frame.origin.x > screen.frame.origin.x { 108 | screen = s 109 | } else if s.frame.origin.x == screen.frame.origin.x { 110 | if s.frame.origin.y < screen.frame.origin.y { 111 | screen = s 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | // 119 | // Determine origin 120 | // 121 | 122 | let screenFrame = 123 | userSettings.widgetAllowOverlappingWithDock ? screen.frame : screen.visibleFrame 124 | if let widgetPosition = WidgetPosition(rawValue: userSettings.widgetPosition) { 125 | switch widgetPosition { 126 | case WidgetPosition.bottomLeft: 127 | origin.x = screenFrame.origin.x + userSettings.widgetOffsetX 128 | origin.y = screenFrame.origin.y + userSettings.widgetOffsetY 129 | 130 | case WidgetPosition.topLeft: 131 | origin.x = screenFrame.origin.x + userSettings.widgetOffsetX 132 | origin.y = 133 | screenFrame.origin.y + screenFrame.size.height - window.frame.height 134 | - userSettings.widgetOffsetY 135 | 136 | case WidgetPosition.topRight: 137 | origin.x = 138 | screenFrame.origin.x + screenFrame.size.width - window.frame.width 139 | - userSettings.widgetOffsetX 140 | origin.y = 141 | screenFrame.origin.y + screenFrame.size.height - window.frame.height 142 | - userSettings.widgetOffsetY 143 | 144 | default: 145 | // WidgetPosition.bottomRight 146 | origin.x = 147 | screenFrame.origin.x + screenFrame.size.width - window.frame.width 148 | - userSettings.widgetOffsetX 149 | origin.y = screenFrame.origin.y + userSettings.widgetOffsetY 150 | } 151 | } 152 | } 153 | 154 | return origin 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/org.sparkle-project.Downloader.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/project-base.yml: -------------------------------------------------------------------------------- 1 | name: TrueWidget 2 | 3 | options: 4 | bundleIdPrefix: org.pqrs 5 | packages: 6 | # We have to declare all packages in project-base.yml instead of separated in project-base.yml and project-with-codesign.yml to avoid `Could not resolve package dependencies` error, 7 | # when the codesign requirement is changed between builds. 8 | # (For example, the first build is with codesign, then the second build is without codesign.) 9 | AsyncAlgorithms: 10 | url: https://github.com/apple/swift-async-algorithms 11 | from: 1.0.0 12 | SettingsAccess: 13 | url: https://github.com/orchetect/SettingsAccess 14 | from: 2.1.0 15 | Sparkle: 16 | url: https://github.com/sparkle-project/Sparkle 17 | from: 2.7.0 18 | 19 | targets: 20 | TrueWidget: 21 | type: application 22 | platform: macOS 23 | deploymentTarget: '13.0' 24 | sources: 25 | - path: TrueWidget 26 | excludes: 27 | - 'Info.plist.in' 28 | - 'embedded.provisionprofile' 29 | - path: Helper/HelperProtocol.swift 30 | settings: 31 | base: 32 | ASSETCATALOG_COMPILER_APPICON_NAME: '' 33 | OTHER_SWIFT_FLAGS: '-warnings-as-errors' 34 | dependencies: 35 | - package: AsyncAlgorithms 36 | - package: SettingsAccess 37 | - target: TrueWidget Helper 38 | 39 | TrueWidget Helper: 40 | type: xpc-service 41 | platform: macOS 42 | deploymentTarget: '13.0' 43 | sources: 44 | - path: Helper 45 | settings: 46 | base: 47 | PRODUCT_BUNDLE_IDENTIFIER: org.pqrs.TrueWidget.Helper 48 | LD_RUNPATH_SEARCH_PATHS: 49 | - '$(inherited)' 50 | - '@loader_path/../../../../Frameworks' 51 | OTHER_SWIFT_FLAGS: '-warnings-as-errors' 52 | -------------------------------------------------------------------------------- /src/project-with-codesign.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - project-base.yml 3 | 4 | targets: 5 | TrueWidget: 6 | settings: 7 | base: 8 | CODE_SIGN_ENTITLEMENTS: 'TrueWidget/TrueWidget.entitlements' 9 | configs: 10 | debug: 11 | SWIFT_ACTIVE_COMPILATION_CONDITIONS: 'USE_SPARKLE DEBUG' 12 | release: 13 | SWIFT_ACTIVE_COMPILATION_CONDITIONS: 'USE_SPARKLE' 14 | dependencies: 15 | - package: Sparkle 16 | -------------------------------------------------------------------------------- /src/project-without-codesign.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - project-base.yml 3 | -------------------------------------------------------------------------------- /src/run-xcodegen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | spec="project-with-codesign.yml" 4 | 5 | if [[ -z $(bash ../scripts/get-codesign-identity.sh) ]]; then 6 | spec="project-without-codesign.yml" 7 | fi 8 | 9 | echo "Use $spec" 10 | xcodegen generate --spec $spec 11 | -------------------------------------------------------------------------------- /tools/clean-launch-services-database/.gitignore: -------------------------------------------------------------------------------- 1 | /.build 2 | -------------------------------------------------------------------------------- /tools/clean-launch-services-database/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | swift run 3 | -------------------------------------------------------------------------------- /tools/clean-launch-services-database/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "clean-launch-services-database", 8 | platforms: [ 9 | .macOS(.v13) 10 | ], 11 | targets: [ 12 | // Targets are the basic building blocks of a package, defining a module or a test suite. 13 | // Targets can depend on other targets in this package and products from dependencies. 14 | .executableTarget( 15 | name: "clean-launch-services-database") 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /tools/clean-launch-services-database/Sources/clean-launch-services-database/config.swift: -------------------------------------------------------------------------------- 1 | let bundleIdentifiers = [ 2 | "org.pqrs.TrueWidget", 3 | "org.pqrs.TrueWidget.Helper", 4 | ] 5 | -------------------------------------------------------------------------------- /tools/clean-launch-services-database/Sources/clean-launch-services-database/main.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | // 4 | // The built application is automatically registered in the Launch Services database. 5 | // This means that unwanted entries such as */build/Release/*.app will be registered. 6 | // 7 | // Then, the names displayed in Background items of Login Items & Extensions System Settings refer to the launch services database. 8 | // If the path of the built application is referenced, the application name may not be taken properly and the developer name may appear in Login Items. 9 | // 10 | // To avoid this problem, unintended application entries should be removed from the database. 11 | // This script removes such entries which includes /Build/ or /build/ in the file path. 12 | // 13 | 14 | let lsregisterCommandPath = 15 | "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister" 16 | let deleteTargetPathRegex = #/ 17 | /build/ | 18 | /Build/ 19 | /# 20 | 21 | for bundleIdentifier in bundleIdentifiers { 22 | print("clean-launch-services-database \(bundleIdentifier)...") 23 | 24 | let urls = NSWorkspace.shared.urlsForApplications(withBundleIdentifier: bundleIdentifier) 25 | 26 | for url in urls { 27 | let path = url.path 28 | if path.matches(of: deleteTargetPathRegex).count > 0 { 29 | print("unregister from the Launch Services database: \(path)") 30 | 31 | let process = Process() 32 | process.launchPath = lsregisterCommandPath 33 | process.arguments = [ 34 | "-u", 35 | path, 36 | ] 37 | 38 | process.launch() 39 | process.waitUntilExit() 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | 2.3.0 2 | --------------------------------------------------------------------------------