Repository: exelban/stats
Branch: master
Commit: c457e0fafb58
Files: 283
Total size: 17.7 MB
Directory structure:
gitextract_0i6vpw9i/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ └── bug_report.md
│ └── workflows/
│ ├── build.yaml
│ ├── i18n.yaml
│ └── linter.yaml
├── .gitignore
├── .swiftlint.yml
├── Kit/
│ ├── Supporting Files/
│ │ ├── Assets.xcassets/
│ │ │ ├── Contents.json
│ │ │ ├── calendar.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── cancel.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── chart.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── close.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── refresh.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── settings.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── tune.imageset/
│ │ │ └── Contents.json
│ │ ├── Info.plist
│ │ └── Kit.h
│ ├── Widgets/
│ │ ├── BarChart.swift
│ │ ├── Battery.swift
│ │ ├── Label.swift
│ │ ├── LineChart.swift
│ │ ├── Memory.swift
│ │ ├── Mini.swift
│ │ ├── NetworkChart.swift
│ │ ├── PieChart.swift
│ │ ├── Speed.swift
│ │ ├── Stack.swift
│ │ ├── State.swift
│ │ ├── Tachometer.swift
│ │ └── Text.swift
│ ├── constants.swift
│ ├── extensions.swift
│ ├── helpers.swift
│ ├── lldb/
│ │ ├── LICENSE.txt
│ │ ├── include/
│ │ │ ├── c.h
│ │ │ ├── cache.h
│ │ │ ├── comparator.h
│ │ │ ├── db.h
│ │ │ ├── dumpfile.h
│ │ │ ├── env.h
│ │ │ ├── export.h
│ │ │ ├── filter_policy.h
│ │ │ ├── iterator.h
│ │ │ ├── options.h
│ │ │ ├── slice.h
│ │ │ ├── status.h
│ │ │ ├── table.h
│ │ │ ├── table_builder.h
│ │ │ └── write_batch.h
│ │ ├── libleveldb.a
│ │ ├── lldb.h
│ │ └── lldb.m
│ ├── module/
│ │ ├── module.swift
│ │ ├── notifications.swift
│ │ ├── popup.swift
│ │ ├── portal.swift
│ │ ├── reader.swift
│ │ ├── widget.swift
│ │ └── window.swift
│ ├── plugins/
│ │ ├── Charts.swift
│ │ ├── DB.swift
│ │ ├── Logger.swift
│ │ ├── Reachability.swift
│ │ ├── Remote.swift
│ │ ├── Repeater.swift
│ │ ├── Store.swift
│ │ ├── SystemKit.swift
│ │ └── Updater.swift
│ ├── process.swift
│ ├── scripts/
│ │ ├── SMJobBlessUtil.py
│ │ ├── changelog.py
│ │ ├── i18n.py
│ │ ├── uninstall.sh
│ │ └── updater.sh
│ └── types.swift
├── LICENSE
├── LaunchAtLogin/
│ ├── Info.plist
│ ├── LaunchAtLogin.entitlements
│ └── main.swift
├── Makefile
├── Modules/
│ ├── Battery/
│ │ ├── Info.plist
│ │ ├── config.plist
│ │ ├── main.swift
│ │ ├── notifications.swift
│ │ ├── popup.swift
│ │ ├── portal.swift
│ │ ├── readers.swift
│ │ └── settings.swift
│ ├── Bluetooth/
│ │ ├── Info.plist
│ │ ├── config.plist
│ │ ├── main.swift
│ │ ├── notifications.swift
│ │ ├── popup.swift
│ │ ├── readers.swift
│ │ └── settings.swift
│ ├── CPU/
│ │ ├── Info.plist
│ │ ├── bridge.h
│ │ ├── config.plist
│ │ ├── main.swift
│ │ ├── notifications.swift
│ │ ├── popup.swift
│ │ ├── portal.swift
│ │ ├── readers.swift
│ │ ├── settings.swift
│ │ └── widget.swift
│ ├── Clock/
│ │ ├── config.plist
│ │ ├── main.swift
│ │ ├── popup.swift
│ │ ├── portal.swift
│ │ ├── reader.swift
│ │ └── settings.swift
│ ├── Disk/
│ │ ├── Info.plist
│ │ ├── config.plist
│ │ ├── header.h
│ │ ├── main.swift
│ │ ├── notifications.swift
│ │ ├── popup.swift
│ │ ├── portal.swift
│ │ ├── readers.swift
│ │ ├── settings.swift
│ │ └── widget.swift
│ ├── GPU/
│ │ ├── Info.plist
│ │ ├── config.plist
│ │ ├── main.swift
│ │ ├── notifications.swift
│ │ ├── popup.swift
│ │ ├── portal.swift
│ │ ├── reader.swift
│ │ ├── settings.swift
│ │ └── widget.swift
│ ├── Net/
│ │ ├── Info.plist
│ │ ├── config.plist
│ │ ├── main.swift
│ │ ├── notifications.swift
│ │ ├── popup.swift
│ │ ├── portal.swift
│ │ ├── readers.swift
│ │ ├── settings.swift
│ │ └── widget.swift
│ ├── RAM/
│ │ ├── Info.plist
│ │ ├── config.plist
│ │ ├── main.swift
│ │ ├── notifications.swift
│ │ ├── popup.swift
│ │ ├── portal.swift
│ │ ├── readers.swift
│ │ ├── settings.swift
│ │ └── widget.swift
│ └── Sensors/
│ ├── Info.plist
│ ├── bridge.h
│ ├── config.plist
│ ├── main.swift
│ ├── notifications.swift
│ ├── popup.swift
│ ├── portal.swift
│ ├── reader.m
│ ├── readers.swift
│ ├── settings.swift
│ └── values.swift
├── README.md
├── SMC/
│ ├── Helper/
│ │ ├── Info.plist
│ │ ├── Launchd.plist
│ │ ├── main.swift
│ │ └── protocol.swift
│ ├── Makefile
│ ├── main.swift
│ └── smc.swift
├── Stats/
│ ├── AppDelegate.swift
│ ├── Supporting Files/
│ │ ├── Assets.xcassets/
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ ├── ac_unit.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── apps.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── bug.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── devices/
│ │ │ │ ├── Contents.json
│ │ │ │ ├── imac.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── imacPro.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── macMini.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── macMini2020.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── macMini2024.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── macPro.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── macPro2019.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── macStudio.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── macbookAir.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── macbookAir4thGen.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── macbookNeo.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── macbookPro.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── macbookPro5thGen.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── donate.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── high-battery.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── low-battery.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── pause.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── power.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── record.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── resume.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── settings.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── stop.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── support/
│ │ │ ├── Contents.json
│ │ │ ├── github.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── ko-fi.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── patreon.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── paypal.imageset/
│ │ │ └── Contents.json
│ │ ├── Info.plist
│ │ ├── Stats/
│ │ │ └── en.xcloc/
│ │ │ ├── Localized Contents/
│ │ │ │ └── en.xliff
│ │ │ ├── Source Contents/
│ │ │ │ ├── LaunchAtLogin/
│ │ │ │ │ └── en.lproj/
│ │ │ │ │ └── InfoPlist.strings
│ │ │ │ ├── ModuleKit/
│ │ │ │ │ └── Supporting Files/
│ │ │ │ │ └── en.lproj/
│ │ │ │ │ └── InfoPlist.strings
│ │ │ │ ├── Modules/
│ │ │ │ │ ├── Battery/
│ │ │ │ │ │ └── en.lproj/
│ │ │ │ │ │ └── InfoPlist.strings
│ │ │ │ │ ├── CPU/
│ │ │ │ │ │ └── en.lproj/
│ │ │ │ │ │ └── InfoPlist.strings
│ │ │ │ │ ├── Disk/
│ │ │ │ │ │ └── en.lproj/
│ │ │ │ │ │ └── InfoPlist.strings
│ │ │ │ │ ├── GPU/
│ │ │ │ │ │ └── en.lproj/
│ │ │ │ │ │ └── InfoPlist.strings
│ │ │ │ │ ├── Memory/
│ │ │ │ │ │ └── en.lproj/
│ │ │ │ │ │ └── InfoPlist.strings
│ │ │ │ │ ├── Net/
│ │ │ │ │ │ └── en.lproj/
│ │ │ │ │ │ └── InfoPlist.strings
│ │ │ │ │ └── Sensors/
│ │ │ │ │ └── en.lproj/
│ │ │ │ │ └── InfoPlist.strings
│ │ │ │ ├── Stats/
│ │ │ │ │ └── Supporting Files/
│ │ │ │ │ └── en.lproj/
│ │ │ │ │ ├── InfoPlist.strings
│ │ │ │ │ └── Localizable.strings
│ │ │ │ └── StatsKit/
│ │ │ │ └── en.lproj/
│ │ │ │ └── InfoPlist.strings
│ │ │ └── contents.json
│ │ ├── Stats.entitlements
│ │ ├── ar.lproj/
│ │ │ └── Localizable.strings
│ │ ├── bg.lproj/
│ │ │ └── Localizable.strings
│ │ ├── ca.lproj/
│ │ │ └── Localizable.strings
│ │ ├── cs.lproj/
│ │ │ └── Localizable.strings
│ │ ├── da.lproj/
│ │ │ └── Localizable.strings
│ │ ├── de.lproj/
│ │ │ └── Localizable.strings
│ │ ├── el.lproj/
│ │ │ └── Localizable.strings
│ │ ├── en-AU.lproj/
│ │ │ └── Localizable.strings
│ │ ├── en-GB.lproj/
│ │ │ └── Localizable.strings
│ │ ├── en.lproj/
│ │ │ └── Localizable.strings
│ │ ├── es.lproj/
│ │ │ └── Localizable.strings
│ │ ├── et.lproj/
│ │ │ └── Localizable.strings
│ │ ├── fa.lproj/
│ │ │ └── Localizable.strings
│ │ ├── fi.lproj/
│ │ │ └── Localizable.strings
│ │ ├── fr.lproj/
│ │ │ └── Localizable.strings
│ │ ├── he.lproj/
│ │ │ └── Localizable.strings
│ │ ├── hi.lproj/
│ │ │ └── Localizable.strings
│ │ ├── hr.lproj/
│ │ │ └── Localizable.strings
│ │ ├── hu.lproj/
│ │ │ └── Localizable.strings
│ │ ├── id.lproj/
│ │ │ └── Localizable.strings
│ │ ├── it.lproj/
│ │ │ └── Localizable.strings
│ │ ├── ja.lproj/
│ │ │ └── Localizable.strings
│ │ ├── ko.lproj/
│ │ │ └── Localizable.strings
│ │ ├── menus.psd
│ │ ├── nb.lproj/
│ │ │ └── Localizable.strings
│ │ ├── nl.lproj/
│ │ │ └── Localizable.strings
│ │ ├── pl.lproj/
│ │ │ └── Localizable.strings
│ │ ├── popups.psd
│ │ ├── pt-BR.lproj/
│ │ │ └── Localizable.strings
│ │ ├── pt-PT.lproj/
│ │ │ └── Localizable.strings
│ │ ├── ro.lproj/
│ │ │ └── Localizable.strings
│ │ ├── ru.lproj/
│ │ │ └── Localizable.strings
│ │ ├── sk.lproj/
│ │ │ └── Localizable.strings
│ │ ├── sl.lproj/
│ │ │ └── Localizable.strings
│ │ ├── sv.lproj/
│ │ │ └── Localizable.strings
│ │ ├── th.lproj/
│ │ │ └── Localizable.strings
│ │ ├── tr.lproj/
│ │ │ └── Localizable.strings
│ │ ├── uk.lproj/
│ │ │ └── Localizable.strings
│ │ ├── vi.lproj/
│ │ │ └── Localizable.strings
│ │ ├── zh-Hans.lproj/
│ │ │ └── Localizable.strings
│ │ └── zh-Hant.lproj/
│ │ └── Localizable.strings
│ ├── Views/
│ │ ├── AppSettings.swift
│ │ ├── CombinedView.swift
│ │ ├── Dashboard.swift
│ │ ├── Settings.swift
│ │ ├── Setup.swift
│ │ ├── Support.swift
│ │ └── Update.swift
│ └── helpers.swift
├── Stats.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata/
│ └── xcschemes/
│ ├── SMC.xcscheme
│ ├── Stats.xcscheme
│ └── WidgetsExtension.xcscheme
├── Tests/
│ ├── Info.plist
│ └── RAM.swift
├── Widgets/
│ ├── Supporting Files/
│ │ ├── Assets.xcassets/
│ │ │ ├── AccentColor.colorset/
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ └── WidgetBackground.colorset/
│ │ │ └── Contents.json
│ │ ├── Info.plist
│ │ └── Widgets.entitlements
│ ├── UnitedWidget.swift
│ └── widgets.swift
└── exportOptions.plist
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
github: [exelban]
patreon: exelban
ko_fi: exelban
custom: ["https://www.paypal.com/donate?hosted_button_id=3DS5JHDBATMTC"]
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Details:**
- Device: [e.g. Macbook Pro 2016]
- macOS: [e.g. 10.15.5]
- Application version: [e.g. 2.1.11]
================================================
FILE: .github/workflows/build.yaml
================================================
name: build
on:
push:
branches:
- master
pull_request:
branches:
- master
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- run: xcodebuild -scheme Stats -destination 'platform=macOS' -configuration Release archive CODE_SIGNING_ALLOWED=NO
================================================
FILE: .github/workflows/i18n.yaml
================================================
name: i18n check
on:
push:
paths:
- '.github/workflows/i18n.yaml'
- '**/*.strings'
pull_request:
paths:
- '.github/workflows/i18n.yaml'
- '**/*.strings'
jobs:
i18n:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v2
- run: python3 Kit/scripts/i18n.py
================================================
FILE: .github/workflows/linter.yaml
================================================
name: Linter
on:
push:
paths:
- '.github/workflows/linter.yaml'
- '.swiftlint.yml'
- '**/*.swift'
pull_request:
paths:
- '.github/workflows/linter.yaml'
- '.swiftlint.yml'
- '**/*.swift'
jobs:
SwiftLint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: norio-nomura/action-swiftlint@3.2.1
================================================
FILE: .gitignore
================================================
.DS_Store
Pods
Carthage
build
xcuserdata
Stats.dmg
Stats.app
create-dmg
dSYMs.zip
Stats.dmg.zip
SMC/smc
Cartfile.resolved
web
================================================
FILE: .swiftlint.yml
================================================
disabled_rules:
- force_cast # todo
- type_name # todo
- cyclomatic_complexity # todo
- trailing_whitespace
- opening_brace
- implicit_getter
- implicit_optional_initialization
- large_tuple
- function_body_length
opt_in_rules:
- control_statement
- empty_count
- trailing_newline
- colon
- comma
identifier_name:
min_length: 1
excluded:
- AppUpdateIntervals
- TemperatureUnits
- SpeedBase
- SensorsWidgetMode
- SpeedPictogram
- BatteryAdditionals
- ShortLong
- ReaderUpdateIntervals
- NumbersOfProcesses
- NetworkReaders
- SensorsList
- Alignments
- _devices
- _uuidAddress
- AppleSiliconSensorsList
- FanValues
- CombinedModulesSpacings
- BatteryInfo
- PublicIPAddressRefreshIntervals
- _values
- _writeTS
- LineChartHistory
- SpeedPictogramColor
- SensorsWidgetValue
- access_token
- refresh_token
- device_code
- user_code
- verification_uri_complete
- expires_in
line_length: 200
type_body_length:
- 700
- 1000
file_length:
- 1400
- 1800
================================================
FILE: Kit/Supporting Files/Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Kit/Supporting Files/Assets.xcassets/calendar.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "baseline_calendar_month_black_24pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "baseline_calendar_month_black_24pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "baseline_calendar_month_black_24pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Kit/Supporting Files/Assets.xcassets/cancel.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "outline_close_white_12pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "outline_close_white_12pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "outline_close_white_12pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Kit/Supporting Files/Assets.xcassets/chart.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "baseline_insert_chart_outlined_white_24pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "baseline_insert_chart_outlined_white_24pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "baseline_insert_chart_outlined_white_24pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Kit/Supporting Files/Assets.xcassets/close.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "baseline_cancel_white_24pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "baseline_cancel_white_24pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "baseline_cancel_white_24pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Kit/Supporting Files/Assets.xcassets/refresh.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "outline_refresh_black_18pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "outline_refresh_black_18pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "outline_refresh_black_18pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Kit/Supporting Files/Assets.xcassets/settings.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "baseline_settings_black_24pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "baseline_settings_black_24pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "baseline_settings_black_24pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Kit/Supporting Files/Assets.xcassets/tune.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "outline_tune_black_18pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "outline_tune_black_18pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "outline_tune_black_18pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Kit/Supporting Files/Info.plist
================================================
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
1.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSHumanReadableCopyright
Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
================================================
FILE: Kit/Supporting Files/Kit.h
================================================
//
// Kit.h
// Kit
//
// Created by Serhiy Mytrovtsiy on 05/02/2024
// Using Swift 5.0
// Running on macOS 14.3
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//
#import
//! Project version number for Kit.
FOUNDATION_EXPORT double KitVersionNumber;
//! Project version string for Kit.
FOUNDATION_EXPORT const unsigned char KitVersionString[];
#import "lldb.h"
================================================
FILE: Kit/Widgets/BarChart.swift
================================================
//
// BarChart.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 26/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public class BarChart: WidgetWrapper {
private var labelState: Bool = false
private var boxState: Bool = true
private var frameState: Bool = false
public var colorState: SColor = .systemAccent
private var colors: [SColor] = SColor.allCases
private var _value: [[ColorValue]] = [[]]
private var _pressureLevel: RAMPressure = .normal
private var _colorZones: colorZones = (0.6, 0.8)
private var boxSettingsView: NSSwitch? = nil
private var frameSettingsView: NSSwitch? = nil
public var NSLabelCharts: [NSAttributedString] = []
public init(title: String, config: NSDictionary?, preview: Bool = false) {
var widgetTitle: String = title
if config != nil {
var configuration = config!
if let titleFromConfig = config!["Title"] as? String {
widgetTitle = titleFromConfig
}
if preview {
if let previewConfig = config!["Preview"] as? NSDictionary {
configuration = previewConfig
if let value = configuration["Value"] as? String {
self._value = value.split(separator: ",").map{ ([ColorValue(Double($0) ?? 0)]) }
}
}
}
if let label = configuration["Label"] as? Bool {
self.labelState = label
}
if let box = configuration["Box"] as? Bool {
self.boxState = box
}
if let unsupportedColors = configuration["Unsupported colors"] as? [String] {
self.colors = self.colors.filter{ !unsupportedColors.contains($0.key) }
}
if let color = configuration["Color"] as? String {
if let defaultColor = self.colors.first(where: { "\($0.self)" == color }) {
self.colorState = defaultColor
}
}
}
super.init(.barChart, title: widgetTitle, frame: CGRect(
x: Constants.Widget.margin.x,
y: Constants.Widget.margin.y,
width: Constants.Widget.width + (2*Constants.Widget.margin.x),
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
))
self.canDrawConcurrently = true
if !preview {
self.boxState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_box", defaultValue: self.boxState)
self.frameState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_frame", defaultValue: self.frameState)
self.labelState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_label", defaultValue: self.labelState)
self.colorState = SColor.fromString(Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_color", defaultValue: self.colorState.key))
}
if preview {
if self._value.isEmpty {
self._value = [[ColorValue(0.72)], [ColorValue(0.38)]]
}
self.setFrameSize(NSSize(width: 36, height: self.frame.size.height))
self.invalidateIntrinsicContentSize()
self.display()
}
let style = NSMutableParagraphStyle()
style.alignment = .center
let stringAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 7, weight: .regular),
NSAttributedString.Key.foregroundColor: NSColor.textColor,
NSAttributedString.Key.paragraphStyle: style
]
for char in String(self.title.prefix(3)).uppercased().reversed() {
let str = NSAttributedString.init(string: "\(char)", attributes: stringAttributes)
self.NSLabelCharts.append(str)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
var value: [[ColorValue]] = []
var pressureLevel: RAMPressure = .normal
var colorZones: colorZones = (0.6, 0.8)
self.queue.sync {
value = self._value
pressureLevel = self._pressureLevel
colorZones = self._colorZones
}
guard !value.isEmpty else {
self.setWidth(0)
return
}
var width: CGFloat = Constants.Widget.margin.x*2
var x: CGFloat = 0
let lineWidth = 1 / (NSScreen.main?.backingScaleFactor ?? 1)
let offset = lineWidth / 2
switch value.count {
case 0, 1:
width += 10 + (offset*2)
case 2:
width += 22
case 3...4: // 3,4
width += 30
case 5...8: // 5,6,7,8
width += 40
case 9...12: // 9..12
width += 50
case 13...16: // 13..16
width += 76
case 17...32: // 17..32
width += 84
default: // > 32
width += 118
}
if self.labelState {
let letterHeight = self.frame.height / 3
let letterWidth: CGFloat = 6.0
var yMargin: CGFloat = 0
for char in self.NSLabelCharts {
let rect = CGRect(x: x, y: yMargin, width: letterWidth, height: letterHeight)
char.draw(with: rect)
yMargin += letterHeight
}
width += letterWidth + Constants.Widget.spacing
x = letterWidth + Constants.Widget.spacing
}
let box = NSBezierPath(roundedRect: NSRect(
x: x + offset,
y: offset,
width: width - x - (offset*2) - (Constants.Widget.margin.x*2),
height: self.frame.size.height - (offset*2)
), xRadius: 2, yRadius: 2)
if self.boxState {
(isDarkMode ? NSColor.white : NSColor.black).set()
box.stroke()
box.fill()
}
let widthForBarChart = box.bounds.width
let partitionMargin: CGFloat = 0.5
let partitionsMargin: CGFloat = (CGFloat(value.count - 1)) * partitionMargin / CGFloat(value.count - 1)
let partitionWidth: CGFloat = (widthForBarChart / CGFloat(value.count)) - CGFloat(partitionsMargin.isNaN ? 0 : partitionsMargin)
let maxPartitionHeight: CGFloat = box.bounds.height
x += offset
for i in 0.. tolerance || val1.color != val2.color
}
}
guard isDifferent else { return }
self._value = newValue
self.redraw()
})
}
public func setPressure(_ newPressureLevel: RAMPressure) {
DispatchQueue.main.async(execute: {
guard self._pressureLevel != newPressureLevel else { return }
self._pressureLevel = newPressureLevel
self.redraw()
})
}
public func setColorZones(_ newColorZones: colorZones) {
DispatchQueue.main.async(execute: {
guard self._colorZones != newColorZones else { return }
self._colorZones = newColorZones
self.redraw()
})
}
// MARK: - Settings
public override func settings() -> NSView {
let view = SettingsContainerView()
let box = switchView(
action: #selector(self.toggleBox),
state: self.boxState
)
self.boxSettingsView = box
let frame = switchView(
action: #selector(self.toggleFrame),
state: self.frameState
)
self.frameSettingsView = frame
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Label"), component: switchView(
action: #selector(self.toggleLabel),
state: self.labelState
)),
PreferencesRow(localizedString("Color"), component: selectView(
action: #selector(self.toggleColor),
items: self.colors,
selected: self.colorState.key
)),
PreferencesRow(localizedString("Box"), component: box),
PreferencesRow(localizedString("Frame"), component: frame)
]))
return view
}
@objc private func toggleLabel(_ sender: NSControl) {
self.labelState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_label", value: self.labelState)
self.redraw()
}
@objc private func toggleBox(_ sender: NSControl) {
self.boxState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_box", value: self.boxState)
if self.frameState {
self.frameSettingsView?.state = .off
self.frameState = false
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_frame", value: self.frameState)
}
self.redraw()
}
@objc private func toggleFrame(_ sender: NSControl) {
self.frameState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_frame", value: self.frameState)
if self.boxState {
self.boxSettingsView?.state = .off
self.boxState = false
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_box", value: self.boxState)
}
self.redraw()
}
@objc private func toggleColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
if let newColor = self.colors.first(where: { $0.key == key }) {
self.colorState = newColor
}
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_color", value: key)
self.redraw()
}
}
================================================
FILE: Kit/Widgets/Battery.swift
================================================
//
// Battery.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 06/06/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public class BatteryWidget: WidgetWrapper {
private var additional: String = "none"
private var timeFormat: String = "short"
private var iconState: Bool = true
private var colorState: Bool = false
private var hideAdditionalWhenFull: Bool = true
private var xlSizeState: Bool = false
private var chargerIconInside: Bool = true
private var _percentage: Double? = nil
private var _time: Int = 0
private var _charging: Bool = false
private var _ACStatus: Bool = false
private var _optimizedCharging: Bool = false
public init(title: String, preview: Bool = false) {
let widgetTitle: String = title
super.init(.battery, title: widgetTitle, frame: CGRect(
x: Constants.Widget.margin.x,
y: Constants.Widget.margin.y,
width: 30 + (2*Constants.Widget.margin.x),
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
))
self.canDrawConcurrently = true
if !preview {
self.additional = Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_additional", defaultValue: self.additional)
self.timeFormat = Store.shared.string(key: "\(self.title)_timeFormat", defaultValue: self.timeFormat)
self.iconState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_icon", defaultValue: self.iconState)
self.colorState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_color", defaultValue: self.colorState)
self.hideAdditionalWhenFull = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_hideAdditionalWhenFull", defaultValue: self.hideAdditionalWhenFull)
self.xlSizeState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_xlSize", defaultValue: self.xlSizeState)
self.chargerIconInside = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_chargerInside", defaultValue: self.chargerIconInside)
}
if preview {
self._percentage = 0.72
self.additional = "none"
self.iconState = true
self.colorState = false
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
var percentage: Double? = nil
var time: Int = 0
var charging: Bool = false
var ACStatus: Bool = false
var optimizedCharging: Bool = false
self.queue.sync {
percentage = self._percentage
time = self._time
charging = self._charging
ACStatus = self._ACStatus
optimizedCharging = self._optimizedCharging
}
var width: CGFloat = 0
var x: CGFloat = 0
let isShortTimeFormat: Bool = self.timeFormat == "short"
if !self.hideAdditionalWhenFull || (self.hideAdditionalWhenFull && percentage != 1 && !optimizedCharging) {
switch self.additional {
case "percentage":
var value = "n/a"
if let percentage {
value = "\(Int((percentage.rounded(toPlaces: 2)) * 100))%"
}
let rowWidth = self.drawOneRow(value: value, x: x).rounded(.up)
width += rowWidth
x += rowWidth + Constants.Widget.spacing
case "time":
let rowWidth = self.drawOneRow(
value: Double(time*60).printSecondsToHoursMinutesSeconds(short: isShortTimeFormat),
x: x
).rounded(.up)
width += rowWidth
x += rowWidth + Constants.Widget.spacing
case "percentageAndTime":
var value = "n/a"
if let percentage {
value = "\(Int((percentage.rounded(toPlaces: 2)) * 100))%"
}
let rowWidth = self.drawTwoRows(
first: value,
second: Double(time*60).printSecondsToHoursMinutesSeconds(short: isShortTimeFormat),
x: x
).rounded(.up)
width += rowWidth
x += rowWidth + Constants.Widget.spacing
case "timeAndPercentage":
var value = "n/a"
if let percentage {
value = "\(Int((percentage.rounded(toPlaces: 2)) * 100))%"
}
let rowWidth = self.drawTwoRows(
first: Double(time*60).printSecondsToHoursMinutesSeconds(short: isShortTimeFormat),
second: value,
x: x
).rounded(.up)
width += rowWidth
x += rowWidth + Constants.Widget.spacing
default: break
}
}
let batterySize: CGSize = self.xlSizeState ? CGSize(width: 26, height: 14) : CGSize(width: 22, height: 12)
if ACStatus && !self.chargerIconInside {
if x != 0 {
width += Constants.Widget.spacing
x += Constants.Widget.spacing
}
self.drawACIcon(
ctx: ctx,
center: CGPoint(x: x+3, y: self.frame.size.height/2),
height: 12,
charging: charging
)
width += 6
x += 6 + Constants.Widget.spacing
}
let borderWidth: CGFloat = 1
let batteryRadius: CGFloat = self.xlSizeState ? 3 : 2
let offset: CGFloat = 0.5 // contant!
width += batterySize.width + borderWidth*2 // add battery width
if x != 0 {
width += Constants.Widget.spacing
x += Constants.Widget.spacing
}
let batteryFrame = NSBezierPath(roundedRect: NSRect(
x: x + borderWidth + offset,
y: ((self.frame.size.height - batterySize.height)/2) + offset,
width: batterySize.width - borderWidth,
height: batterySize.height - borderWidth
), xRadius: batteryRadius, yRadius: batteryRadius)
NSColor.textColor.withAlphaComponent(0.5).set()
batteryFrame.lineWidth = borderWidth
batteryFrame.stroke()
let bPX: CGFloat = batteryFrame.bounds.origin.x + batteryFrame.bounds.width + 1
let bPY: CGFloat = batteryFrame.bounds.origin.y + batteryFrame.bounds.height/2 - 2
let batteryPoint = NSBezierPath(roundedRect: NSRect(x: bPX - 1, y: bPY, width: 3, height: 4), xRadius: 2, yRadius: 2)
batteryPoint.fill()
let batteryPointSeparator = NSBezierPath()
batteryPointSeparator.move(to: CGPoint(x: bPX, y: batteryFrame.bounds.origin.y))
batteryPointSeparator.line(to: CGPoint(x: bPX, y: batteryFrame.bounds.origin.y + batteryFrame.bounds.height))
ctx.saveGState()
ctx.setBlendMode(.destinationOut)
NSColor.white.set()
batteryPointSeparator.lineWidth = borderWidth
batteryPointSeparator.stroke()
ctx.restoreGState()
width += 2 // add battery point width
if let percentage {
let maxWidth = batterySize.width - offset*2 - borderWidth*2 - 1
let innerWidth: CGFloat = max(1, maxWidth * CGFloat(percentage))
let innerOffset: CGFloat = -offset + borderWidth + 1
let innerRadius: CGFloat = self.xlSizeState ? 2 : 1
var colorState = self.colorState
let color = percentage.batteryColor(color: colorState)
let innerPercentage = self.additional == "innerPercentage" && (!ACStatus || !self.chargerIconInside)
if innerPercentage {
colorState = false
let innerUnderground = NSBezierPath(roundedRect: NSRect(
x: batteryFrame.bounds.origin.x + innerOffset,
y: batteryFrame.bounds.origin.y + innerOffset,
width: maxWidth,
height: batterySize.height - offset*2 - borderWidth*2 - 1
), xRadius: innerRadius, yRadius: innerRadius)
(self.colorState ? color : NSColor.textColor).withAlphaComponent(0.5).set()
innerUnderground.fill()
}
let inner = NSBezierPath(roundedRect: NSRect(
x: batteryFrame.bounds.origin.x + innerOffset,
y: batteryFrame.bounds.origin.y + innerOffset,
width: innerWidth,
height: batterySize.height - offset*2 - borderWidth*2 - 1
), xRadius: innerRadius, yRadius: innerRadius)
color.set()
inner.fill()
if innerPercentage {
let fontSize: CGFloat = self.xlSizeState ? 9 : 8
let style = NSMutableParagraphStyle()
style.alignment = .center
let attributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: fontSize, weight: .bold),
NSAttributedString.Key.foregroundColor: NSColor.clear,
NSAttributedString.Key.paragraphStyle: style
]
let value = "\(Int((percentage.rounded(toPlaces: 2)) * 100))"
let rect = CGRect(x: inner.bounds.origin.x, y: (Constants.Widget.height-(fontSize+2))/2, width: maxWidth, height: fontSize)
let str = NSAttributedString.init(string: value, attributes: attributes)
ctx.saveGState()
ctx.setBlendMode(.destinationIn)
str.draw(with: rect)
ctx.restoreGState()
}
} else {
let attributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 11, weight: .regular),
NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor,
NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
]
let batteryCenter: CGPoint = CGPoint(
x: batteryFrame.bounds.origin.x + (batteryFrame.bounds.width/2),
y: batteryFrame.bounds.origin.y + (batteryFrame.bounds.height/2)
)
let rect = CGRect(x: batteryCenter.x-3, y: batteryCenter.y-4, width: 8, height: 12)
NSAttributedString.init(string: "?", attributes: attributes).draw(with: rect)
}
if ACStatus && self.chargerIconInside {
let batteryCenter: CGPoint = CGPoint(
x: batteryFrame.bounds.origin.x + (batteryFrame.bounds.width/2),
y: batteryFrame.bounds.origin.y + (batteryFrame.bounds.height/2)
)
self.drawACIcon(
ctx: ctx,
center: batteryCenter,
height: 12,
charging: charging
)
}
self.setWidth(width)
}
private func drawOneRow(value: String, x: CGFloat) -> CGFloat {
let attributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 12, weight: .regular),
NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor,
NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
]
let rowWidth = value.widthOfString(usingFont: .systemFont(ofSize: 12, weight: .regular))
let rect = CGRect(x: x, y: (Constants.Widget.height-13)/2, width: rowWidth, height: 12)
let str = NSAttributedString.init(string: value, attributes: attributes)
str.draw(with: rect)
return rowWidth
}
private func drawTwoRows(first: String, second: String, x: CGFloat) -> CGFloat {
let style = NSMutableParagraphStyle()
style.alignment = .center
let attributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .regular),
NSAttributedString.Key.foregroundColor: NSColor.textColor,
NSAttributedString.Key.paragraphStyle: style
]
let rowHeight: CGFloat = self.frame.height / 2
let rowWidth = max(
first.widthOfString(usingFont: .systemFont(ofSize: 9, weight: .regular)),
second.widthOfString(usingFont: .systemFont(ofSize: 9, weight: .regular))
)
var str = NSAttributedString.init(string: first, attributes: attributes)
str.draw(with: CGRect(x: x, y: rowHeight+1, width: rowWidth, height: rowHeight))
str = NSAttributedString.init(string: second, attributes: attributes)
str.draw(with: CGRect(x: x, y: 1, width: rowWidth, height: rowHeight))
return rowWidth
}
private func drawACIcon(ctx: CGContext, center batteryCenter: CGPoint, height: CGFloat, charging: Bool) {
var points: [CGPoint] = []
if charging {
let iconSize: CGSize = CGSize(width: 9, height: height + 6)
let min = CGPoint(
x: batteryCenter.x - (iconSize.width/2),
y: batteryCenter.y - (iconSize.height/2)
)
let max = CGPoint(
x: batteryCenter.x + (iconSize.width/2),
y: batteryCenter.y + (iconSize.height/2)
)
points = [
CGPoint(x: batteryCenter.x-3, y: min.y), // bottom
CGPoint(x: max.x, y: batteryCenter.y+1.5),
CGPoint(x: batteryCenter.x+1, y: batteryCenter.y+1.5),
CGPoint(x: batteryCenter.x+3, y: max.y), // top
CGPoint(x: min.x, y: batteryCenter.y-1.5),
CGPoint(x: batteryCenter.x-1, y: batteryCenter.y-1.5)
]
} else {
let iconSize: CGSize = CGSize(width: 9, height: height + 2)
let minY = batteryCenter.y - (iconSize.height/2)
let maxY = batteryCenter.y + (iconSize.height/2)
points = [
CGPoint(x: batteryCenter.x-1.5, y: minY+0.5),
CGPoint(x: batteryCenter.x+1.5, y: minY+0.5),
CGPoint(x: batteryCenter.x+1.5, y: batteryCenter.y - 2.5),
CGPoint(x: batteryCenter.x+4, y: batteryCenter.y + 0.5),
CGPoint(x: batteryCenter.x+4, y: batteryCenter.y + 4.25),
// right
CGPoint(x: batteryCenter.x+2.75, y: batteryCenter.y + 4.25),
CGPoint(x: batteryCenter.x+2.75, y: maxY-0.25),
CGPoint(x: batteryCenter.x+0.25, y: maxY-0.25),
CGPoint(x: batteryCenter.x+0.25, y: batteryCenter.y + 4.25),
// left
CGPoint(x: batteryCenter.x-0.25, y: batteryCenter.y + 4.25),
CGPoint(x: batteryCenter.x-0.25, y: maxY-0.25),
CGPoint(x: batteryCenter.x-2.75, y: maxY-0.25),
CGPoint(x: batteryCenter.x-2.75, y: batteryCenter.y + 4.25),
CGPoint(x: batteryCenter.x-4, y: batteryCenter.y + 4.25),
CGPoint(x: batteryCenter.x-4, y: batteryCenter.y + 0.5),
CGPoint(x: batteryCenter.x-1.5, y: batteryCenter.y - 2.5),
CGPoint(x: batteryCenter.x-1.5, y: minY+0.5)
]
}
let linePath = NSBezierPath()
linePath.move(to: CGPoint(x: points[0].x, y: points[0].y))
for i in 1.. NSView {
let view = SettingsContainerView()
var additionalOptions = BatteryAdditionals
if self.title == "Bluetooth" {
additionalOptions = additionalOptions.filter({ $0.key == "none" || $0.key == "percentage" })
}
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Additional information"), component: selectView(
action: #selector(self.toggleAdditional),
items: additionalOptions,
selected: self.additional
)),
PreferencesRow(localizedString("Hide additional information when full"), component: switchView(
action: #selector(self.toggleHideAdditionalWhenFull),
state: self.hideAdditionalWhenFull
)),
PreferencesRow(localizedString("Colorize"), component: switchView(
action: #selector(self.toggleColor),
state: self.colorState
)),
PreferencesRow(localizedString("XL size"), component: switchView(
action: #selector(self.toggleXLSize),
state: self.xlSizeState
)),
PreferencesRow(localizedString("Charger state inside the battery"), component: switchView(
action: #selector(self.toggleChargerIconInside),
state: self.chargerIconInside
))
]))
return view
}
@objc private func toggleAdditional(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.additional = key
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_additional", value: key)
self.display()
}
@objc private func toggleHideAdditionalWhenFull(_ sender: NSControl) {
self.hideAdditionalWhenFull = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_hideAdditionalWhenFull", value: self.hideAdditionalWhenFull)
self.display()
}
@objc private func toggleColor(_ sender: NSControl) {
self.colorState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_color", value: self.colorState)
self.display()
}
@objc private func toggleXLSize(_ sender: NSControl) {
self.xlSizeState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_xlSize", value: self.xlSizeState)
self.display()
}
@objc private func toggleChargerIconInside(_ sender: NSControl) {
self.chargerIconInside = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_chargerInside", value: self.chargerIconInside)
self.display()
}
}
public class BatteryDetailsWidget: WidgetWrapper {
private var mode: String = "percentage"
private var timeFormat: String = "short"
private var percentage: Double? = nil
private var time: Int = 0
public init(title: String, preview: Bool = false) {
super.init(.batteryDetails, title: title, frame: CGRect(
x: Constants.Widget.margin.x,
y: Constants.Widget.margin.y,
width: 20 + (2*Constants.Widget.margin.x),
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
))
self.canDrawConcurrently = true
if preview {
self.percentage = 0.72
self.time = 415
self.mode = "percentageAndTime"
} else {
self.mode = Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_mode", defaultValue: self.mode)
self.timeFormat = Store.shared.string(key: "\(self.title)_timeFormat", defaultValue: self.timeFormat)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
var width: CGFloat = Constants.Widget.margin.x*2
let x: CGFloat = Constants.Widget.margin.x
let isShortTimeFormat: Bool = self.timeFormat == "short"
switch self.mode {
case "percentage":
var value = "n/a"
if let percentage = self.percentage {
value = "\(Int((percentage.rounded(toPlaces: 2)) * 100))%"
}
width = self.drawOneRow(value: value, x: x).rounded(.up)
case "time":
width = self.drawOneRow(
value: Double(self.time*60).printSecondsToHoursMinutesSeconds(short: isShortTimeFormat),
x: x
).rounded(.up)
case "percentageAndTime":
var value = "n/a"
if let percentage = self.percentage {
value = "\(Int((percentage.rounded(toPlaces: 2)) * 100))%"
}
if self.time > 0 {
width = self.drawTwoRows(
first: value,
second: Double(self.time*60).printSecondsToHoursMinutesSeconds(short: isShortTimeFormat),
x: x
).rounded(.up)
} else {
width = self.drawOneRow(value: value, x: x).rounded(.up)
}
case "timeAndPercentage":
var value = "n/a"
if let percentage = self.percentage {
value = "\(Int((percentage.rounded(toPlaces: 2)) * 100))%"
}
if self.time > 0 {
width = self.drawTwoRows(
first: Double(self.time*60).printSecondsToHoursMinutesSeconds(short: isShortTimeFormat),
second: value,
x: x
).rounded(.up)
} else {
width = self.drawOneRow(value: value, x: x).rounded(.up)
}
default: break
}
self.setWidth(width)
}
private func drawOneRow(value: String, x: CGFloat) -> CGFloat {
let attributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 12, weight: .regular),
NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor,
NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
]
let rowWidth = value.widthOfString(usingFont: .systemFont(ofSize: 12, weight: .regular))
let rect = CGRect(x: x, y: (Constants.Widget.height-12)/2, width: rowWidth, height: 12)
let str = NSAttributedString.init(string: value, attributes: attributes)
str.draw(with: rect)
return rowWidth
}
private func drawTwoRows(first: String, second: String, x: CGFloat) -> CGFloat {
let style = NSMutableParagraphStyle()
style.alignment = .center
let attributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .regular),
NSAttributedString.Key.foregroundColor: NSColor.textColor,
NSAttributedString.Key.paragraphStyle: style
]
let rowHeight: CGFloat = self.frame.height / 2
let rowWidth = max(
first.widthOfString(usingFont: .systemFont(ofSize: 9, weight: .regular)),
second.widthOfString(usingFont: .systemFont(ofSize: 9, weight: .regular))
)
var str = NSAttributedString.init(string: first, attributes: attributes)
str.draw(with: CGRect(x: x, y: rowHeight+1, width: rowWidth, height: rowHeight))
str = NSAttributedString.init(string: second, attributes: attributes)
str.draw(with: CGRect(x: x, y: 1, width: rowWidth, height: rowHeight))
return rowWidth
}
public func setValue(percentage: Double? = nil, time: Int? = nil) {
var updated: Bool = false
let timeFormat: String = Store.shared.string(key: "\(self.title)_timeFormat", defaultValue: self.timeFormat)
if self.percentage != percentage {
self.percentage = percentage
updated = true
}
if let time = time, self.time != time {
self.time = time
updated = true
}
if self.timeFormat != timeFormat {
self.timeFormat = timeFormat
updated = true
}
if updated {
self.needsDisplay = true
DispatchQueue.main.async(execute: {
self.display()
})
}
}
// MARK: - Settings
public override func settings() -> NSView {
let view = SettingsContainerView()
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Details"), component: selectView(
action: #selector(self.toggleMode),
items: BatteryInfo,
selected: self.mode
))
]))
return view
}
@objc private func toggleMode(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.mode = key
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_mode", value: key)
self.display()
}
}
================================================
FILE: Kit/Widgets/Label.swift
================================================
//
// Label.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 30/03/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public class Label: WidgetWrapper {
private var label: String
internal init(title: String, config: NSDictionary) {
if let title = config["Title"] as? String {
self.label = title
} else {
self.label = title
}
super.init(.label, title: title, frame: CGRect(
x: 0,
y: Constants.Widget.margin.y,
width: 6 + (2*Constants.Widget.margin.x),
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
))
self.canDrawConcurrently = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let size: CGSize = CGSize(width: 6, height: self.frame.height / 3)
var margin: CGPoint = CGPoint(x: Constants.Widget.margin.x, y: 0)
let style = NSMutableParagraphStyle()
style.alignment = .center
let stringAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 7, weight: .regular),
NSAttributedString.Key.foregroundColor: NSColor.textColor,
NSAttributedString.Key.paragraphStyle: style
]
for char in String(self.label.prefix(3)).uppercased().reversed() {
let rect = CGRect(x: margin.x, y: margin.y, width: size.width, height: size.height)
let str = NSAttributedString.init(string: "\(char)", attributes: stringAttributes)
str.draw(with: rect)
margin.y += size.height
}
}
}
================================================
FILE: Kit/Widgets/LineChart.swift
================================================
//
// Chart.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 18/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public class LineChart: WidgetWrapper {
private var labelState: Bool = false
private var boxState: Bool = true
private var frameState: Bool = false
private var valueState: Bool = false
private var valueColorState: Bool = false
private var colorState: SColor = .systemAccent
private var historyCount: Int = 60
private var scaleState: Scale = .none
private var chart: LineChartView = LineChartView(frame: NSRect(
x: 0,
y: 0,
width: 32,
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
), num: 60)
private var colors: [SColor] = SColor.allCases.filter({ $0 != SColor.cluster })
private var _value: Double = 0
private var _pressureLevel: RAMPressure = .normal
private var historyNumbers: [KeyValue_p] = [
KeyValue_t(key: "30", value: "30"),
KeyValue_t(key: "60", value: "60"),
KeyValue_t(key: "90", value: "90"),
KeyValue_t(key: "120", value: "120")
]
private var width: CGFloat {
get {
switch self.historyCount {
case 30:
return 24
case 60:
return 32
case 90:
return 42
case 120:
return 52
default:
return 32
}
}
}
private var boxSettingsView: NSSwitch? = nil
private var frameSettingsView: NSSwitch? = nil
public var NSLabelCharts: [NSAttributedString] = []
public init(title: String, config: NSDictionary?, preview: Bool = false) {
var widgetTitle: String = title
if config != nil {
if let titleFromConfig = config!["Title"] as? String {
widgetTitle = titleFromConfig
}
if let label = config!["Label"] as? Bool {
self.labelState = label
}
if let box = config!["Box"] as? Bool {
self.boxState = box
}
if let value = config!["Value"] as? Bool {
self.valueState = value
}
if let unsupportedColors = config!["Unsupported colors"] as? [String] {
self.colors = self.colors.filter{ !unsupportedColors.contains($0.key) }
}
if let color = config!["Color"] as? String {
if let defaultColor = colors.first(where: { "\($0.self)" == color }) {
self.colorState = defaultColor
}
}
}
super.init(.lineChart, title: widgetTitle, frame: CGRect(
x: Constants.Widget.margin.x,
y: Constants.Widget.margin.y,
width: 32 + (Constants.Widget.margin.x*2),
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
))
self.canDrawConcurrently = true
if !preview {
self.boxState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_box", defaultValue: self.boxState)
self.frameState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_frame", defaultValue: self.frameState)
self.valueState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_value", defaultValue: self.valueState)
self.labelState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_label", defaultValue: self.labelState)
self.valueColorState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_valueColor", defaultValue: self.valueColorState)
self.colorState = SColor.fromString(Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_color", defaultValue: self.colorState.key))
self.historyCount = Store.shared.int(key: "\(self.title)_\(self.type.rawValue)_historyCount", defaultValue: self.historyCount)
self.scaleState = Scale.fromString(Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_scale", defaultValue: self.scaleState.key))
self.chart.setScale(self.scaleState)
self.chart.reinit(self.historyCount)
}
if self.labelState {
self.setFrameSize(NSSize(width: Constants.Widget.width + 6 + (Constants.Widget.margin.x*2), height: self.frame.size.height))
}
if preview {
var list: [DoubleValue] = []
for _ in 0..<16 {
list.append(DoubleValue(Double.random(in: 0..<1)))
}
self.chart.points = list
self._value = 0.38
}
let style = NSMutableParagraphStyle()
style.alignment = .center
let stringAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 7, weight: .regular),
NSAttributedString.Key.foregroundColor: NSColor.textColor,
NSAttributedString.Key.paragraphStyle: style
]
for char in String(self.title.prefix(3)).uppercased().reversed() {
let str = NSAttributedString.init(string: "\(char)", attributes: stringAttributes)
self.NSLabelCharts.append(str)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
guard let context = NSGraphicsContext.current?.cgContext else { return }
var value: Double = 0
var pressureLevel: RAMPressure = .normal
self.queue.sync {
value = self._value
pressureLevel = self._pressureLevel
}
var width = self.width + (Constants.Widget.margin.x*2)
var x: CGFloat = 0
let lineWidth = 1 / (NSScreen.main?.backingScaleFactor ?? 1)
let offset = lineWidth / 2
var boxSize: CGSize = CGSize(width: self.width - (Constants.Widget.margin.x*2), height: self.frame.size.height)
var color: NSColor = .controlAccentColor
switch self.colorState {
case .systemAccent: color = .controlAccentColor
case .utilization: color = value.usageColor()
case .pressure: color = pressureLevel.pressureColor()
case .monochrome:
if self.boxState {
color = (isDarkMode ? NSColor.black : NSColor.white)
} else {
color = (isDarkMode ? NSColor.white : NSColor.black)
}
default: color = self.colorState.additional as? NSColor ?? .controlAccentColor
}
if self.labelState {
let letterHeight = self.frame.height / 3
let letterWidth: CGFloat = 6.0
var yMargin: CGFloat = 0
for char in self.NSLabelCharts {
let rect = CGRect(x: x, y: yMargin, width: letterWidth, height: letterHeight)
char.draw(with: rect)
yMargin += letterHeight
}
width += letterWidth + Constants.Widget.spacing
x = letterWidth + Constants.Widget.spacing
}
if self.valueState {
let style = NSMutableParagraphStyle()
style.alignment = .right
var valueColor = isDarkMode ? NSColor.white : NSColor.black
if self.valueColorState {
valueColor = color
}
let stringAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 8, weight: .regular),
NSAttributedString.Key.foregroundColor: valueColor,
NSAttributedString.Key.paragraphStyle: style
]
let rect = CGRect(x: x+2, y: boxSize.height-7, width: boxSize.width - 2, height: 7)
let str = NSAttributedString.init(string: "\(Int((value.rounded(toPlaces: 2)) * 100))%", attributes: stringAttributes)
str.draw(with: rect)
boxSize.height = offset == 0.5 ? 10 : 9
}
let box = NSBezierPath(roundedRect: NSRect(
x: x+offset,
y: offset,
width: self.width - offset*2,
height: boxSize.height - (offset*2)
), xRadius: 2, yRadius: 2)
if self.boxState {
(isDarkMode ? NSColor.white : NSColor.black).set()
box.stroke()
box.fill()
self.chart.transparent = false
} else if self.frameState {
self.chart.transparent = true
} else {
self.chart.transparent = true
}
context.saveGState()
let chartFrame = NSRect(
x: x+offset+lineWidth,
y: offset,
width: box.bounds.width - (offset*2+lineWidth),
height: box.bounds.height - offset
)
self.chart.color = color
self.chart.setFrameSize(NSSize(width: chartFrame.width, height: chartFrame.height))
self.chart.draw(chartFrame)
context.restoreGState()
if self.boxState || self.frameState {
(isDarkMode ? NSColor.white : NSColor.black).set()
box.lineWidth = lineWidth
box.stroke()
}
self.setWidth(width)
}
public func setValue(_ newValue: Double) {
DispatchQueue.main.async(execute: {
self._value = newValue
self.chart.addValue(newValue)
self.display()
})
}
public func setPressure(_ newPressureLevel: RAMPressure) {
DispatchQueue.main.async(execute: {
guard self._pressureLevel != newPressureLevel else { return }
self._pressureLevel = newPressureLevel
self.display()
})
}
// MARK: - Settings
public override func settings() -> NSView {
let view = SettingsContainerView()
let box = switchView(
action: #selector(self.toggleBox),
state: self.boxState
)
self.boxSettingsView = box
let frame = switchView(
action: #selector(self.toggleFrame),
state: self.frameState
)
self.frameSettingsView = frame
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Label"), component: switchView(
action: #selector(self.toggleLabel),
state: self.labelState
)),
PreferencesRow(localizedString("Value"), component: switchView(
action: #selector(self.toggleValue),
state: self.valueState
)),
PreferencesRow(localizedString("Box"), component: box),
PreferencesRow(localizedString("Frame"), component: frame),
PreferencesRow(localizedString("Color"), component: selectView(
action: #selector(self.toggleColor),
items: self.colors,
selected: self.colorState.key
)),
PreferencesRow(localizedString("Colorize value"), component: switchView(
action: #selector(self.toggleValueColor),
state: self.valueColorState
)),
PreferencesRow(localizedString("Number of reads in the chart"), component: selectView(
action: #selector(self.toggleHistoryCount),
items: self.historyNumbers,
selected: "\(self.historyCount)"
)),
PreferencesRow(localizedString("Scaling"), component: selectView(
action: #selector(self.toggleScale),
items: Scale.allCases.filter({ $0 != .fixed }),
selected: self.scaleState.key
))
]))
return view
}
@objc private func toggleLabel(_ sender: NSControl) {
self.labelState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_label", value: self.labelState)
self.display()
}
@objc private func toggleBox(_ sender: NSControl) {
self.boxState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_box", value: self.boxState)
if self.frameState {
self.frameSettingsView?.state = .off
self.frameState = false
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_frame", value: self.frameState)
}
self.display()
}
@objc private func toggleFrame(_ sender: NSControl) {
self.frameState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_frame", value: self.frameState)
if self.boxState {
self.boxSettingsView?.state = .off
self.boxState = false
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_box", value: self.boxState)
}
self.display()
}
@objc private func toggleValue(_ sender: NSControl) {
self.valueState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_value", value: self.valueState)
self.display()
}
@objc private func toggleColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
if let newColor = SColor.allCases.first(where: { $0.key == key }) {
self.colorState = newColor
}
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_color", value: key)
self.display()
}
@objc private func toggleValueColor(_ sender: NSControl) {
self.valueColorState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_valueColor", value: self.valueColorState)
self.display()
}
@objc private func toggleHistoryCount(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let value = Int(key) else { return }
self.historyCount = value
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_historyCount", value: value)
self.chart.reinit(value)
self.display()
}
@objc private func toggleScale(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let value = Scale.allCases.first(where: { $0.key == key }) else { return }
self.scaleState = value
self.chart.setScale(value)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_scale", value: key)
self.display()
}
}
================================================
FILE: Kit/Widgets/Memory.swift
================================================
//
// Memory.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 30/06/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public class MemoryWidget: WidgetWrapper {
private var orderReversedState: Bool = false
private var value: (String, String) = ("0", "0")
private var percentage: Double = 0
private var pressureLevel: RAMPressure = .normal
private var symbolsState: Bool = true
private var colorState: SColor = .monochrome
private let width: CGFloat = 50
public init(title: String, config: NSDictionary?, preview: Bool = false) {
if config != nil {
var configuration = config!
if preview {
if let previewConfig = config!["Preview"] as? NSDictionary {
configuration = previewConfig
if let value = configuration["Value"] as? String {
let values = value.split(separator: ",").map{ (String($0) ) }
if values.count == 2 {
self.value.0 = values[0]
self.value.1 = values[1]
}
}
}
}
}
super.init(.memory, title: title, frame: CGRect(
x: Constants.Widget.margin.x,
y: Constants.Widget.margin.y,
width: self.width + (Constants.Widget.margin.x*2),
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
))
self.canDrawConcurrently = true
if !preview {
self.orderReversedState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_orderReversed", defaultValue: self.orderReversedState)
self.symbolsState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_symbols", defaultValue: self.symbolsState)
self.colorState = SColor.fromString(Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_color", defaultValue: self.colorState.key))
}
if preview {
self.orderReversedState = false
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let letterWidth: CGFloat = 8
let rowHeight: CGFloat = self.frame.height / 2
var width: CGFloat = self.width
var x: CGFloat = 0
let freeY: CGFloat = !self.orderReversedState ? rowHeight+1 : 1
let usedY: CGFloat = !self.orderReversedState ? 1 : rowHeight+1
let style = NSMutableParagraphStyle()
style.alignment = .right
var attributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .light),
NSAttributedString.Key.foregroundColor: NSColor.textColor,
NSAttributedString.Key.paragraphStyle: style
]
if self.symbolsState {
var rect = CGRect(x: Constants.Widget.margin.x, y: freeY, width: letterWidth, height: rowHeight)
var str = NSAttributedString.init(string: "F:", attributes: attributes)
str.draw(with: rect)
rect = CGRect(x: Constants.Widget.margin.x, y: usedY, width: letterWidth, height: rowHeight)
str = NSAttributedString.init(string: "U:", attributes: attributes)
str.draw(with: rect)
x = letterWidth + Constants.Widget.spacing*2
width += x
}
var freeColor: NSColor = .controlAccentColor
var usedColor: NSColor = .controlAccentColor
switch self.colorState {
case .systemAccent:
freeColor = .controlAccentColor
usedColor = .controlAccentColor
case .utilization:
freeColor = (1 - self.percentage).usageColor()
usedColor = self.percentage.usageColor()
case .pressure:
usedColor = self.pressureLevel.pressureColor()
freeColor = self.pressureLevel.pressureColor()
case .monochrome:
freeColor = (isDarkMode ? NSColor.white : NSColor.black)
usedColor = (isDarkMode ? NSColor.white : NSColor.black)
default:
freeColor = self.colorState.additional as? NSColor ?? .controlAccentColor
usedColor = self.colorState.additional as? NSColor ?? .controlAccentColor
}
attributes[NSAttributedString.Key.foregroundColor] = freeColor
var rect = CGRect(x: x, y: freeY, width: width - x, height: rowHeight)
var str = NSAttributedString.init(string: self.value.0, attributes: attributes)
str.draw(with: rect)
attributes[NSAttributedString.Key.foregroundColor] = usedColor
rect = CGRect(x: x, y: usedY, width: width - x, height: rowHeight)
str = NSAttributedString.init(string: self.value.1, attributes: attributes)
str.draw(with: rect)
self.setWidth(width + (Constants.Widget.margin.x*2))
}
public func setValue(_ value: (String, String), usedPercentage: Double) {
self.value = value
self.percentage = usedPercentage
DispatchQueue.main.async(execute: {
self.display()
})
}
public func setPressure(_ newPressureLevel: RAMPressure) {
guard self.pressureLevel != newPressureLevel else { return }
self.pressureLevel = newPressureLevel
DispatchQueue.main.async(execute: {
self.display()
})
}
public override func settings() -> NSView {
let view = SettingsContainerView()
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Color"), component: selectView(
action: #selector(self.toggleColor),
items: SColor.allCases.filter({ $0 != .cluster }),
selected: self.colorState.key
)),
PreferencesRow(localizedString("Show symbols"), component: switchView(
action: #selector(self.toggleSymbols),
state: self.symbolsState
)),
PreferencesRow(localizedString("Reverse order"), component: switchView(
action: #selector(self.toggleOrder),
state: self.orderReversedState
))
]))
return view
}
@objc private func toggleOrder(_ sender: NSControl) {
self.orderReversedState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_orderReversed", value: self.orderReversedState)
self.display()
}
@objc private func toggleSymbols(_ sender: NSControl) {
self.symbolsState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_symbols", value: self.symbolsState)
self.display()
}
@objc private func toggleColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
if let newColor = SColor.allCases.first(where: { $0.key == key }) {
self.colorState = newColor
}
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_color", value: key)
self.display()
}
}
================================================
FILE: Kit/Widgets/Mini.swift
================================================
//
// Mini.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 10/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public class Mini: WidgetWrapper {
private var labelState: Bool = true
private var colorState: SColor = .monochrome
private var alignmentState: String = "left"
private var colors: [SColor] = SColor.allCases
private var _value: Double = 0
private var _pressureLevel: RAMPressure = .normal
private var _colorZones: colorZones = (0.6, 0.8)
private var _suffix: String = "%"
private var defaultLabel: String
private var _label: String
private var width: CGFloat {
(self.labelState ? 31 : 36) + (2*Constants.Widget.margin.x)
}
private var alignment: NSTextAlignment {
if let alignmentPair = Alignments.first(where: { $0.key == self.alignmentState }) {
return alignmentPair.additional as? NSTextAlignment ?? .left
}
return .left
}
public init(title: String, config: NSDictionary?, preview: Bool = false) {
var widgetTitle: String = title
if config != nil {
var configuration = config!
if preview {
if let previewConfig = config!["Preview"] as? NSDictionary {
configuration = previewConfig
if let value = configuration["Value"] as? String {
self._value = Double(value) ?? 0
}
}
}
if let titleFromConfig = configuration["Title"] as? String {
widgetTitle = titleFromConfig
}
if let label = configuration["Label"] as? Bool {
self.labelState = label
}
if let unsupportedColors = configuration["Unsupported colors"] as? [String] {
self.colors = self.colors.filter{ !unsupportedColors.contains($0.key) }
}
if let color = configuration["Color"] as? String {
if let defaultColor = colors.first(where: { "\($0.self)" == color }) {
self.colorState = defaultColor
}
}
}
self.defaultLabel = widgetTitle
self._label = widgetTitle
super.init(.mini, title: widgetTitle, frame: CGRect(
x: 0,
y: Constants.Widget.margin.y,
width: Constants.Widget.width + (2*Constants.Widget.margin.x),
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
))
self.canDrawConcurrently = true
if !preview {
self.colorState = SColor.fromString(Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_color", defaultValue: self.colorState.key))
self.labelState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_label", defaultValue: self.labelState)
self.alignmentState = Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_alignment", defaultValue: self.alignmentState)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
var value: Double = 0
var pressureLevel: RAMPressure = .normal
var colorZones: colorZones = (0.6, 0.8)
var label: String = ""
var suffix: String = ""
self.queue.sync {
value = self._value
pressureLevel = self._pressureLevel
colorZones = self._colorZones
label = self._label
suffix = self._suffix
}
let valueSize: CGFloat = self.labelState ? 12 : 14
var origin: CGPoint = CGPoint(x: Constants.Widget.margin.x, y: (Constants.Widget.height-valueSize)/2)
let style = NSMutableParagraphStyle()
style.alignment = self.labelState ? self.alignment : .center
if self.labelState {
let style = NSMutableParagraphStyle()
style.alignment = self.alignment
let stringAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 7, weight: .light),
NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor,
NSAttributedString.Key.paragraphStyle: style
]
let rect = CGRect(x: origin.x, y: 12, width: self.width - (Constants.Widget.margin.x*2), height: 7)
let str = NSAttributedString.init(string: label, attributes: stringAttributes)
str.draw(with: rect)
origin.y = 1
}
var color: NSColor = .controlAccentColor
switch self.colorState {
case .systemAccent: color = .controlAccentColor
case .utilization: color = value.usageColor(zones: colorZones, reversed: self.title == "BAT")
case .pressure: color = pressureLevel.pressureColor()
case .monochrome: color = (isDarkMode ? NSColor.white : NSColor.black)
default: color = self.colorState.additional as? NSColor ?? .controlAccentColor
}
let stringAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: valueSize, weight: .regular),
NSAttributedString.Key.foregroundColor: color,
NSAttributedString.Key.paragraphStyle: style
]
let rect = CGRect(x: origin.x, y: origin.y, width: self.width - (Constants.Widget.margin.x*2), height: valueSize+1)
let str = NSAttributedString.init(string: "\(Int(value.rounded(toPlaces: 2) * 100))\(suffix)", attributes: stringAttributes)
str.draw(with: rect)
self.setWidth(width)
}
public func setValue(_ newValue: Double) {
guard self._value != newValue else { return }
self._value = newValue
DispatchQueue.main.async(execute: {
self.display()
})
}
public func setPressure(_ newPressureLevel: RAMPressure) {
guard self._pressureLevel != newPressureLevel else { return }
self._pressureLevel = newPressureLevel
DispatchQueue.main.async(execute: {
self.needsDisplay = true
})
}
public func setTitle(_ newTitle: String?) {
var title = self.defaultLabel
if let new = newTitle {
title = new
}
guard self._label != title else { return }
self._label = title
DispatchQueue.main.async(execute: {
self.needsDisplay = true
})
}
public func setColorZones(_ newColorZones: colorZones) {
guard self._colorZones != newColorZones else { return }
self._colorZones = newColorZones
DispatchQueue.main.async(execute: {
self.display()
})
}
public func setSuffix(_ newSuffix: String) {
guard self._suffix != newSuffix else { return }
self._suffix = newSuffix
DispatchQueue.main.async(execute: {
self.display()
})
}
// MARK: - Settings
public override func settings() -> NSView {
let view = SettingsContainerView()
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Label"), component: switchView(
action: #selector(self.toggleLabel),
state: self.labelState
)),
PreferencesRow(localizedString("Color"), component: selectView(
action: #selector(self.toggleColor),
items: self.colors,
selected: self.colorState.key
)),
PreferencesRow(localizedString("Alignment"), component: selectView(
action: #selector(self.toggleAlignment),
items: Alignments,
selected: self.alignmentState
))
]))
return view
}
@objc private func toggleColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
if let newColor = SColor.allCases.first(where: { $0.key == key }) {
self.colorState = newColor
}
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_color", value: key)
self.display()
}
@objc private func toggleLabel(_ sender: NSControl) {
self.labelState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_label", value: self.labelState)
self.display()
}
@objc private func toggleAlignment(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
if let newAlignment = Alignments.first(where: { $0.key == key }) {
self.alignmentState = newAlignment.key
}
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_alignment", value: key)
self.display()
}
}
================================================
FILE: Kit/Widgets/NetworkChart.swift
================================================
//
// NetworkChart.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 19/01/2021.
// Using Swift 5.0.
// Running on macOS 11.1.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public class NetworkChart: WidgetWrapper {
private var boxState: Bool = false
private var frameState: Bool = false
private var labelState: Bool = false
private var historyCount: Int = 60
private var downloadColor: SColor = .secondBlue
private var uploadColor: SColor = .secondRed
private var scaleState: Scale = .linear
private var reverseOrderState: Bool = false
private var points: [(Double, Double)] = Array(repeating: (0, 0), count: 60)
private var width: CGFloat {
get {
switch self.historyCount {
case 30:
return 22
case 60:
return 30
case 90:
return 40
case 120:
return 50
default:
return 30
}
}
}
private var historyNumbers: [KeyValue_p] = [
KeyValue_t(key: "30", value: "30"),
KeyValue_t(key: "60", value: "60"),
KeyValue_t(key: "90", value: "90"),
KeyValue_t(key: "120", value: "120")
]
private var colors: [SColor] = SColor.allCases
private var boxSettingsView: NSSwitch? = nil
private var frameSettingsView: NSSwitch? = nil
public var NSLabelCharts: [NSAttributedString] = []
public init(title: String, config: NSDictionary?, preview: Bool = false) {
var widgetTitle: String = title
if let config = config {
if let titleFromConfig = config["Title"] as? String {
widgetTitle = titleFromConfig
}
if let unsupportedColors = config["Unsupported colors"] as? [String] {
self.colors = self.colors.filter{ !unsupportedColors.contains($0.key) }
}
}
super.init(.networkChart, title: widgetTitle, frame: CGRect(
x: Constants.Widget.margin.x,
y: Constants.Widget.margin.y,
width: 30 + (2*Constants.Widget.margin.x),
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
))
self.canDrawConcurrently = true
if !preview {
self.boxState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_box", defaultValue: self.boxState)
self.frameState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_frame", defaultValue: self.frameState)
self.labelState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_label", defaultValue: self.labelState)
self.historyCount = Store.shared.int(key: "\(self.title)_\(self.type.rawValue)_historyCount", defaultValue: self.historyCount)
self.downloadColor = SColor.fromString(Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_downloadColor", defaultValue: self.downloadColor.key))
self.uploadColor = SColor.fromString(Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_uploadColor", defaultValue: self.uploadColor.key))
self.scaleState = Scale.fromString(Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_scale", defaultValue: self.scaleState.key))
self.reverseOrderState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_reverseOrder", defaultValue: self.reverseOrderState)
}
if preview {
var list: [(Double, Double)] = []
for _ in 0..<60 {
list.append((Double.random(in: 0..<23), Double.random(in: 0..<23)))
}
self.points = list
}
let style = NSMutableParagraphStyle()
style.alignment = .center
let stringAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 7, weight: .regular),
NSAttributedString.Key.foregroundColor: NSColor.textColor,
NSAttributedString.Key.paragraphStyle: style
]
for char in String(self.title.prefix(3)).uppercased().reversed() {
let str = NSAttributedString.init(string: "\(char)", attributes: stringAttributes)
self.NSLabelCharts.append(str)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
guard let context = NSGraphicsContext.current?.cgContext else { return }
var points: [(Double, Double)] = []
var labelState: Bool = false
var boxState: Bool = false
var frameState: Bool = false
var scaleState: Scale = .linear
var reverseOrderState: Bool = false
var originWidth: CGFloat = 0
var labelString: [NSAttributedString] = []
var downloadColor: SColor = .secondBlue
var uploadColor: SColor = .secondRed
self.queue.sync {
points = self.points
labelState = self.labelState
boxState = self.boxState
frameState = self.frameState
scaleState = self.scaleState
reverseOrderState = self.reverseOrderState
labelString = self.NSLabelCharts
originWidth = self.width
downloadColor = self.downloadColor
uploadColor = self.uploadColor
}
let lineWidth = 1 / (NSScreen.main?.backingScaleFactor ?? 1)
let offset = lineWidth / 2
let boxSize: CGSize = CGSize(width: originWidth - (Constants.Widget.margin.x*2), height: self.frame.size.height)
var x: CGFloat = 0
var width = originWidth + (Constants.Widget.margin.x*2)
if labelState {
let letterHeight = self.frame.height / 3
let letterWidth: CGFloat = 6.0
var yMargin: CGFloat = 0
for char in labelString {
let rect = CGRect(x: x, y: yMargin, width: letterWidth, height: letterHeight)
char.draw(with: rect)
yMargin += letterHeight
}
width += letterWidth + Constants.Widget.spacing
x = letterWidth + Constants.Widget.spacing
}
let box = NSBezierPath(roundedRect: NSRect(
x: x + offset,
y: offset,
width: originWidth - offset*2,
height: boxSize.height - (offset*2)
), xRadius: 2, yRadius: 2)
if boxState {
(isDarkMode ? NSColor.white : NSColor.black).set()
box.stroke()
box.fill()
}
context.saveGState()
let chartFrame = NSRect(
x: x+offset+lineWidth,
y: offset,
width: box.bounds.width - (offset*2+lineWidth),
height: box.bounds.height - offset
)
var topMax: Double = (reverseOrderState ? points.map{ $0.1 }.max() : points.map{ $0.0 }.max()) ?? 0
var bottomMax: Double = (reverseOrderState ? points.map{ $0.0 }.max() : points.map{ $0.1 }.max()) ?? 0
if topMax == 0 {
topMax = 1
}
if bottomMax == 0 {
bottomMax = 1
}
let zero: CGFloat = (chartFrame.height/2) + chartFrame.origin.y
let xRatio: CGFloat = (chartFrame.width + (lineWidth*3)) / CGFloat(points.count)
let xCenter: CGFloat = chartFrame.height/2 + chartFrame.origin.y
let columnXPoint = { (point: Int) -> CGFloat in
return (CGFloat(point) * xRatio) + (chartFrame.origin.x - lineWidth)
}
let topYPoint = { (point: Int) -> CGFloat in
let value = reverseOrderState ? points[point].1 : points[point].0
return scaleValue(scale: scaleState, value: value, maxValue: topMax, zeroValue: 256.0, maxHeight: chartFrame.height/2, limit: 1) + xCenter
}
let bottomYPoint = { (point: Int) -> CGFloat in
let value = reverseOrderState ? points[point].0 : points[point].1
return xCenter - scaleValue(scale: scaleState, value: value, maxValue: bottomMax, zeroValue: 256.0, maxHeight: chartFrame.height/2, limit: 1)
}
let topLinePath = NSBezierPath()
topLinePath.move(to: CGPoint(x: columnXPoint(0), y: topYPoint(0)))
let bottomLinePath = NSBezierPath()
bottomLinePath.move(to: CGPoint(x: columnXPoint(0), y: bottomYPoint(0)))
for i in 1.. NSView {
let view = SettingsContainerView()
let box = switchView(
action: #selector(self.toggleBox),
state: self.boxState
)
self.boxSettingsView = box
let frame = switchView(
action: #selector(self.toggleFrame),
state: self.frameState
)
self.frameSettingsView = frame
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Label"), component: switchView(
action: #selector(self.toggleLabel),
state: self.labelState
)),
PreferencesRow(localizedString("Box"), component: box),
PreferencesRow(localizedString("Frame"), component: frame),
PreferencesRow(localizedString("Reverse order"), component: switchView(
action: #selector(self.toggleReverseOrder),
state: self.reverseOrderState
)),
PreferencesRow(localizedString("Color of download"), component: selectView(
action: #selector(self.toggleDownloadColor),
items: self.colors,
selected: self.downloadColor.key
)),
PreferencesRow(localizedString("Color of upload"), component: selectView(
action: #selector(self.toggleUploadColor),
items: self.colors,
selected: self.uploadColor.key
)),
PreferencesRow(localizedString("Number of reads in the chart"), component: selectView(
action: #selector(self.toggleHistoryCount),
items: self.historyNumbers,
selected: "\(self.historyCount)"
)),
PreferencesRow(localizedString("Scaling"), component: selectView(
action: #selector(self.toggleScale),
items: Scale.allCases.filter({ $0 != .fixed }),
selected: self.scaleState.key
))
]))
return view
}
@objc private func toggleLabel(_ sender: NSControl) {
self.labelState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_label", value: self.labelState)
self.display()
}
@objc private func toggleBox(_ sender: NSControl) {
self.boxState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_box", value: self.boxState)
if self.frameState {
self.frameSettingsView?.state = .off
self.frameState = false
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_frame", value: self.frameState)
}
self.display()
}
@objc private func toggleFrame(_ sender: NSControl) {
self.frameState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_frame", value: self.frameState)
if self.boxState {
self.boxSettingsView?.state = .off
self.boxState = false
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_box", value: self.boxState)
}
self.display()
}
@objc private func toggleHistoryCount(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let num = Int(key) else { return }
self.historyCount = num
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_historyCount", value: self.historyCount)
if num < self.points.count {
self.points = Array(self.points.suffix(num))
} else if num > self.points.count {
self.points = Array(repeating: (0, 0), count: num - self.points.count) + self.points
}
self.display()
}
@objc private func toggleDownloadColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
if let newColor = SColor.allCases.first(where: { $0.key == key }) {
self.downloadColor = newColor
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_downloadColor", value: newColor.key)
}
self.display()
}
@objc private func toggleUploadColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
if let newColor = SColor.allCases.first(where: { $0.key == key }) {
self.uploadColor = newColor
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_uploadColor", value: newColor.key)
}
self.display()
}
@objc private func toggleScale(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let value = Scale.allCases.first(where: { $0.key == key }) else { return }
self.scaleState = value
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_scale", value: key)
self.display()
}
@objc private func toggleReverseOrder(_ sender: NSControl) {
self.reverseOrderState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_reverseOrder", value: self.reverseOrderState)
self.display()
}
}
================================================
FILE: Kit/Widgets/PieChart.swift
================================================
//
// PieChart.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 30/11/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public class PieChart: WidgetWrapper {
private var labelState: Bool = false
private var monochromeState: Bool = false
private var chart: PieChartView = PieChartView(
frame: NSRect(
x: Constants.Widget.margin.x,
y: Constants.Widget.margin.y,
width: Constants.Widget.height,
height: Constants.Widget.height
),
segments: [], filled: true, drawValue: false
)
private var labelView: NSView? = nil
private let size: CGFloat = Constants.Widget.height - (Constants.Widget.margin.y*2) + (Constants.Widget.margin.x*2)
public init(title: String, config: NSDictionary?, preview: Bool = false) {
var widgetTitle: String = title
if config != nil {
if let titleFromConfig = config!["Title"] as? String {
widgetTitle = titleFromConfig
}
}
super.init(.pieChart, title: widgetTitle, frame: CGRect(
x: Constants.Widget.margin.x,
y: Constants.Widget.margin.y,
width: self.size,
height: Constants.Widget.height - (Constants.Widget.margin.y*2)
))
self.canDrawConcurrently = true
if preview {
if self.title == "CPU" {
self.chart.setSegments([
circle_segment(value: 0.16, color: NSColor.systemRed),
circle_segment(value: 0.28, color: NSColor.systemBlue)
])
} else if self.title == "RAM" {
self.chart.setSegments([
circle_segment(value: 0.36, color: NSColor.systemBlue),
circle_segment(value: 0.12, color: NSColor.systemOrange),
circle_segment(value: 0.08, color: NSColor.systemPink)
])
} else if self.title == "Disk" {
self.chart.setSegments([
circle_segment(value: 0.86, color: NSColor.systemBlue)
])
}
} else {
self.labelState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_label", defaultValue: self.labelState)
self.monochromeState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_monochrome", defaultValue: self.monochromeState)
}
self.draw()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func draw() {
let x: CGFloat = self.labelState ? 8 + Constants.Widget.spacing : 0
self.labelView = WidgetLabelView(self.title, height: self.frame.height)
self.labelView!.isHidden = !self.labelState
self.addSubview(self.labelView!)
self.addSubview(self.chart)
self.chart.setFrame(NSRect(x: x, y: 0, width: self.frame.size.height, height: self.frame.size.height))
self.setFrameSize(NSSize(width: self.size + x, height: self.frame.size.height))
self.setWidth(self.size + x)
}
public func setValue(_ list: [circle_segment]) {
var segments = list
if self.monochromeState {
for i in 0.. NSView {
let view = SettingsContainerView()
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Label"), component: switchView(
action: #selector(self.toggleLabel),
state: self.labelState
)),
PreferencesRow(localizedString("Monochrome accent"), component: switchView(
action: #selector(self.toggleMonochrome),
state: self.monochromeState
))
]))
return view
}
@objc private func toggleLabel(_ sender: NSControl) {
self.labelState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_label", value: self.labelState)
let x = self.labelState ? 6 + Constants.Widget.spacing : 0
self.labelView!.isHidden = !self.labelState
self.chart.setFrameOrigin(NSPoint(x: x, y: 0))
self.setWidth(self.labelState ? self.size+x : self.size)
}
@objc private func toggleMonochrome(_ sender: NSControl) {
self.monochromeState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_monochrome", value: self.monochromeState)
}
}
================================================
FILE: Kit/Widgets/Speed.swift
================================================
//
// Speed.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 24/05/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public class SpeedWidget: WidgetWrapper {
private var icon: String = "dots"
private var valueState: Bool = true
private var unitsState: Bool = true
private var monochromeState: Bool = false
private var valueColorState: String = "none"
private var iconColorState: String = "default"
private var valueAlignmentState: String = "right"
private var modeState: String = "twoRows"
private var iconAlignmentState: String = "left"
private var displayValueState: String = "oi"
private var inputColorState: SColor = .secondBlue
private var outputColorState: SColor = .secondRed
private var symbols: (input: String, output: String) = ("I", "O")
private var words: (input: String, output: String) = ("Input", "Output")
private var inputValue: Int64 = 0
private var outputValue: Int64 = 0
private var width: CGFloat = 58
private var valueColorView: NSPopUpButton? = nil
private var valueAlignmentView: NSPopUpButton? = nil
private var iconAlignmentView: NSPopUpButton? = nil
private var iconColorView: NSPopUpButton? = nil
private var displayModeView: NSPopUpButton? = nil
private var inputColor: (String) -> NSColor {{ state in
if state == "none" { return .textColor }
var color = self.monochromeState ? MonochromeColor.blue : (self.inputColorState.additional as? NSColor ?? NSColor.systemBlue)
if self.inputValue < 1024 {
if state == "transparent" {
color = .clear
} else if state == "default" {
color = .textColor
}
}
return color
}}
private var outputColor: (String) -> NSColor {{ state in
if state == "none" { return .textColor }
var color = self.monochromeState ? MonochromeColor.red : (self.outputColorState.additional as? NSColor ?? NSColor.red)
if self.outputValue < 1024 {
if state == "transparent" {
color = .clear
} else if state == "default" {
color = .textColor
}
}
return color
}}
private var valueAlignment: NSTextAlignment {
get {
if let alignmentPair = Alignments.first(where: { $0.key == self.valueAlignmentState }) {
return alignmentPair.additional as? NSTextAlignment ?? .left
}
return .left
}
}
private var base: DataSizeBase {
DataSizeBase(rawValue: Store.shared.string(key: "\(self.title)_base", defaultValue: "byte")) ?? .byte
}
public init(title: String, config: NSDictionary?, preview: Bool = false) {
let widgetTitle: String = title
if config != nil {
if let symbols = config!["Symbols"] as? NSDictionary {
if let i = symbols["Input"] as? String { self.symbols.input = i }
if let o = symbols["Output"] as? String { self.symbols.output = o }
}
if let icon = config!["Icon"] as? String { self.icon = icon }
if let words = config!["Words"] as? NSDictionary {
if let i = words["Input"] as? String { self.words.input = i }
if let o = words["Output"] as? String { self.words.output = o }
}
}
super.init(.speed, title: widgetTitle, frame: CGRect(
x: 0,
y: Constants.Widget.margin.y,
width: width,
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
))
self.canDrawConcurrently = true
if !preview {
self.valueState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_value", defaultValue: self.valueState)
self.icon = Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_icon", defaultValue: self.icon)
self.unitsState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_units", defaultValue: self.unitsState)
self.monochromeState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_monochrome", defaultValue: self.monochromeState)
self.valueColorState = Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_valueColor", defaultValue: self.valueColorState)
if self.valueColorState == "0" {
self.valueColorState = "none"
}
self.inputColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_downloadColor", defaultValue: self.inputColorState.key))
self.outputColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_uploadColor", defaultValue: self.outputColorState.key))
self.valueAlignmentState = Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_valueAlignment", defaultValue: self.valueAlignmentState)
self.modeState = Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_mode", defaultValue: self.modeState)
self.iconAlignmentState = Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_iconAlignment", defaultValue: self.iconAlignmentState)
self.iconColorState = Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_iconColor", defaultValue: self.iconColorState)
self.displayValueState = Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_displayValue", defaultValue: self.displayValueState)
}
if preview {
self.inputValue = 8947141
self.outputValue = 478678
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
var width: CGFloat = 0
switch self.modeState {
case "oneRow":
width = self.drawOneRow()
case "twoRows":
width = self.drawTwoRows()
default:
width = 0
}
self.setWidth(width)
}
// MARK: - one row
private func drawOneRow() -> CGFloat {
var width: CGFloat = Constants.Widget.margin.x
if self.displayValueState.first == "i" {
width = self.drawRowItem(
initWidth: width,
symbol: self.symbols.input,
iconColor: self.inputColor(self.iconColorState),
value: self.inputValue,
valueColor: self.inputColor(self.valueColorState)
)
} else {
width = self.drawRowItem(
initWidth: width,
symbol: self.symbols.output,
iconColor: self.outputColor(self.iconColorState),
value: self.outputValue,
valueColor: self.outputColor(self.valueColorState)
)
}
if self.displayValueState.count > 1 {
width += Constants.Widget.spacing*3
if self.displayValueState.last == "i" {
width = self.drawRowItem(
initWidth: width,
symbol: self.symbols.input,
iconColor: self.inputColor(self.iconColorState),
value: self.inputValue,
valueColor: self.inputColor(self.valueColorState)
)
} else {
width = self.drawRowItem(
initWidth: width,
symbol: self.symbols.output,
iconColor: self.outputColor(self.iconColorState),
value: self.outputValue,
valueColor: self.outputColor(self.valueColorState)
)
}
}
return width + Constants.Widget.margin.x
}
private func drawRowItem(initWidth: CGFloat, symbol: String, iconColor: NSColor, value: Int64, valueColor: NSColor) -> CGFloat {
var width = initWidth
if self.iconAlignmentState == "left" {
switch self.icon {
case "dots":
width += self.drawDot(CGPoint(x: width, y: 0), color: iconColor)
case "arrows":
width += self.drawArrow(CGPoint(x: width, y: 0), symbol: symbol, color: iconColor)
case "chars":
width += self.drawChar(CGPoint(x: width, y: 0), symbol: symbol, color: iconColor)
default: break
}
width += self.valueState && self.icon != "none" ? 2 : 0
}
if self.valueState {
width += self.drawValue(value, offset: CGPoint(x: width, y: 0), color: valueColor)
}
if self.iconAlignmentState == "right" {
if self.valueState {
width += 2
}
switch self.icon {
case "dots":
width += self.drawDot(CGPoint(x: width, y: 0), color: iconColor)
case "arrows":
width += self.drawArrow(CGPoint(x: width, y: 0), symbol: symbol, color: iconColor)
case "chars":
width += self.drawChar(CGPoint(x: width, y: 0), symbol: symbol, color: iconColor)
default: break
}
}
return width
}
private func drawValue(_ value: Int64, offset: CGPoint, color: NSColor) -> CGFloat {
let rowWidth: CGFloat = self.unitsState ? 58 : 32
let height: CGFloat = self.frame.height
let style = NSMutableParagraphStyle()
style.alignment = self.valueAlignment
let size: CGFloat = 10
let inputStringAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 11, weight: .regular),
NSAttributedString.Key.foregroundColor: color,
NSAttributedString.Key.paragraphStyle: style
]
let rect = CGRect(x: offset.x, y: (height-size)/2 + offset.y + 1, width: rowWidth - (Constants.Widget.margin.x*2), height: size)
let value = NSAttributedString.init(
string: Units(bytes: value).getReadableSpeed(base: base, omitUnits: !self.unitsState),
attributes: inputStringAttributes
)
value.draw(with: rect)
return rowWidth
}
private func drawDot(_ offset: CGPoint, color: NSColor) -> CGFloat {
var size: CGFloat = 8
var height: CGFloat = self.frame.height
if self.modeState == "twoRows" {
size = 6
height /= 2
}
var circle = NSBezierPath()
circle = NSBezierPath(ovalIn: CGRect(x: offset.x, y: (height-size)/2 + offset.y, width: size, height: size))
color.set()
circle.fill()
return size
}
private func drawArrow(_ offset: CGPoint, symbol: String, color: NSColor) -> CGFloat {
let height = self.frame.height
let size = height * 0.8
let scaleFactor = NSScreen.main?.backingScaleFactor ?? 1
let lineWidth: CGFloat = 1
let arrowSize: CGFloat = 3 + (scaleFactor/2)
let x = arrowSize + (lineWidth / 2)
let y = (height - size)/2
var start: CGPoint = CGPoint(x: offset.x + x, y: y)
var end: CGPoint = CGPoint(x: offset.x + x, y: size + y)
if symbol == "D" || symbol == "R" {
start = CGPoint(x: offset.x + x, y: size + y)
end = CGPoint(x: offset.x + x, y: y)
} else if symbol == "U" || symbol == "W" {
start = CGPoint(x: offset.x + x, y: y)
end = CGPoint(x: offset.x + x, y: size + y)
}
let arrow = NSBezierPath()
arrow.addArrow(
start: start,
end: end,
pointerLineLength: arrowSize,
arrowAngle: CGFloat(Double.pi / 5)
)
color.set()
arrow.lineWidth = lineWidth
arrow.stroke()
arrow.close()
return arrowSize
}
private func drawChar(_ offset: CGPoint, symbol: String, color: NSColor) -> CGFloat {
let rowHeight: CGFloat = self.frame.height
let height: CGFloat = 10
let inputAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 12, weight: .regular),
NSAttributedString.Key.foregroundColor: color,
NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
]
let rect = CGRect(x: offset.x, y: offset.y + ((rowHeight-height)/2) + 1, width: 10, height: height)
let str = NSAttributedString.init(string: symbol, attributes: inputAttributes)
str.draw(with: rect)
return 10
}
// MARK: - two rows
private func drawTwoRows() -> CGFloat {
var width: CGFloat = 7
var x: CGFloat = 7
if self.iconAlignmentState == "right" {
x = 0
}
if self.icon == "none" {
x = 0
width = 0
}
if self.valueState {
let rowWidth: CGFloat = self.unitsState ? 48 : 30
let rowHeight: CGFloat = self.frame.height / 2
let style = NSMutableParagraphStyle()
style.alignment = self.valueAlignment
let inputStringAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .light),
NSAttributedString.Key.foregroundColor: self.inputColor(self.valueColorState),
NSAttributedString.Key.paragraphStyle: style
]
let outputStringAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .light),
NSAttributedString.Key.foregroundColor: self.outputColor(self.valueColorState),
NSAttributedString.Key.paragraphStyle: style
]
let inputY: CGFloat = self.displayValueState == "io" ? rowHeight + 1 : 1
let outputY: CGFloat = self.displayValueState == "io" ? 1 : rowHeight + 1
var rect = CGRect(x: Constants.Widget.margin.x + x, y: inputY, width: rowWidth - (Constants.Widget.margin.x*2), height: rowHeight)
let input = NSAttributedString.init(
string: Units(bytes: self.inputValue).getReadableSpeed(base: base, omitUnits: !self.unitsState),
attributes: inputStringAttributes
)
input.draw(with: rect)
rect = CGRect(x: Constants.Widget.margin.x + x, y: outputY, width: rowWidth - (Constants.Widget.margin.x*2), height: rowHeight)
let output = NSAttributedString.init(
string: Units(bytes: self.outputValue).getReadableSpeed(base: base, omitUnits: !self.unitsState),
attributes: outputStringAttributes
)
output.draw(with: rect)
width += rowWidth
}
switch self.icon {
case "dots":
self.drawDots(width)
case "arrows":
self.drawArrows(width)
case "chars":
self.drawChars(width)
default: break
}
return width
}
private func drawDots(_ width: CGFloat) {
let rowHeight: CGFloat = self.frame.height / 2
let size: CGFloat = 6
let y: CGFloat = (rowHeight-size)/2
let x: CGFloat = self.iconAlignmentState == "left" ? Constants.Widget.margin.x : Constants.Widget.margin.x+(width-6)
let inputY: CGFloat = self.displayValueState == "io" ? 10.5 : y-0.2
let outputdY: CGFloat = self.displayValueState == "io" ? y-0.2 : 10.5
var inputCircle = NSBezierPath()
inputCircle = NSBezierPath(ovalIn: CGRect(x: x, y: inputY, width: size, height: size))
self.inputColor(self.iconColorState).set()
inputCircle.fill()
var outputCircle = NSBezierPath()
outputCircle = NSBezierPath(ovalIn: CGRect(x: x, y: outputdY, width: size, height: size))
self.outputColor(self.iconColorState).set()
outputCircle.fill()
}
private func drawArrows(_ width: CGFloat) {
let arrowAngle = CGFloat(Double.pi / 5)
let half = self.frame.size.height / 2
let scaleFactor = NSScreen.main?.backingScaleFactor ?? 1
let lineWidth: CGFloat = 1
let arrowSize: CGFloat = 3 + (scaleFactor/2)
var x = Constants.Widget.margin.x + arrowSize + (lineWidth / 2)
if self.iconAlignmentState == "right" {
x += (width-7)
}
let inputYStart: CGFloat = self.displayValueState == "io" ? self.frame.size.height : half - Constants.Widget.spacing/2
let inputYEnd: CGFloat = self.displayValueState == "io" ? (half + Constants.Widget.spacing/2)+1 : 1
let outputYStart: CGFloat = self.displayValueState == "io" ? 0 : half + Constants.Widget.spacing/2
let uploadYEnd: CGFloat = self.displayValueState == "io" ? (half - Constants.Widget.spacing/2)-1 : self.frame.size.height-1
let inputArrow = NSBezierPath()
inputArrow.addArrow(
start: CGPoint(x: x, y: inputYStart),
end: CGPoint(x: x, y: inputYEnd),
pointerLineLength: arrowSize,
arrowAngle: arrowAngle
)
self.inputColor(self.iconColorState).set()
inputArrow.lineWidth = lineWidth
inputArrow.stroke()
inputArrow.close()
let outputArrow = NSBezierPath()
outputArrow.addArrow(
start: CGPoint(x: x, y: outputYStart),
end: CGPoint(x: x, y: uploadYEnd),
pointerLineLength: arrowSize,
arrowAngle: arrowAngle
)
self.outputColor(self.iconColorState).set()
outputArrow.lineWidth = lineWidth
outputArrow.stroke()
outputArrow.close()
}
private func drawChars(_ width: CGFloat) {
let rowHeight: CGFloat = self.frame.height / 2
let inputY: CGFloat = self.displayValueState == "io" ? rowHeight+1 : 1
let outputY: CGFloat = self.displayValueState == "io" ? 1 : rowHeight+1
let x: CGFloat = self.iconAlignmentState == "left" ? Constants.Widget.margin.x : Constants.Widget.margin.x+(width-6)
let inputAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .regular),
NSAttributedString.Key.foregroundColor: self.inputColor(self.iconColorState),
NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
]
var rect = CGRect(x: x, y: inputY, width: 8, height: rowHeight)
var str = NSAttributedString.init(string: self.symbols.input, attributes: inputAttributes)
str.draw(with: rect)
let outputAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .regular),
NSAttributedString.Key.foregroundColor: self.outputColor(self.iconColorState),
NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
]
rect = CGRect(x: x, y: outputY, width: 8, height: rowHeight)
str = NSAttributedString.init(string: self.symbols.output, attributes: outputAttributes)
str.draw(with: rect)
}
// MARK: - settings
public override func settings() -> NSView {
let view = SettingsContainerView()
let valueAlignment = selectView(
action: #selector(self.toggleValueAlignment),
items: Alignments,
selected: self.valueAlignmentState
)
valueAlignment.isEnabled = self.valueState
self.valueAlignmentView = valueAlignment
let iconAlignment = selectView(
action: #selector(self.toggleIconAlignment),
items: Alignments.filter({ $0.key != "center" }),
selected: self.iconAlignmentState
)
iconAlignment.isEnabled = self.icon != "none"
self.iconAlignmentView = iconAlignment
let iconColor = selectView(
action: #selector(self.toggleIconColor),
items: SpeedPictogramColor.filter({ $0.key != "none" }),
selected: self.iconColorState
)
iconColor.isEnabled = self.icon != "none"
self.iconColorView = iconColor
let valueColor = selectView(
action: #selector(self.toggleValueColor),
items: SpeedPictogramColor,
selected: self.valueColorState
)
valueColor.isEnabled = self.valueState
self.valueColorView = valueColor
let displayMode = selectView(
action: #selector(self.changeDisplayMode),
items: SensorsWidgetMode.filter({ $0.key == "oneRow" || $0.key == "twoRows"}),
selected: self.modeState
)
displayMode.isEnabled = self.displayValueState.count > 1
self.displayModeView = displayMode
let sensorWidgetValue = SensorsWidgetValue.map { v in
var value = v.value.replacingOccurrences(of: "input", with: localizedString(self.words.input), options: .literal, range: nil)
value = value.replacingOccurrences(of: "output", with: localizedString(self.words.output), options: .literal, range: nil)
return KeyValue_t(key: v.key, value: value)
}
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Value"), component: selectView(
action: #selector(self.changeDisplayValue),
items: sensorWidgetValue,
selected: self.displayValueState
)),
PreferencesRow(localizedString("Display mode"), component: displayMode)
]))
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Pictogram"), component: selectView(
action: #selector(self.toggleIcon),
items: SpeedPictogram,
selected: self.icon
)),
PreferencesRow(localizedString("Colorize"), component: iconColor),
PreferencesRow(localizedString("Alignment"), component: iconAlignment)
]))
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Value"), component: switchView(
action: #selector(self.toggleValue),
state: self.valueState
)),
PreferencesRow(localizedString("Colorize value"), component: valueColor),
PreferencesRow(localizedString("Alignment"), component: valueAlignment),
PreferencesRow(localizedString("Units"), component: switchView(
action: #selector(self.toggleUnits),
state: self.unitsState
))
]))
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Monochrome accent"), component: switchView(
action: #selector(self.toggleMonochrome),
state: self.monochromeState
)),
PreferencesRow(localizedString("Color of download"), component: selectView(
action: #selector(self.toggleInputColor),
items: SColor.allColors,
selected: self.inputColorState.key
)),
PreferencesRow(localizedString("Color of upload"), component: selectView(
action: #selector(self.toggleOutputColor),
items: SColor.allColors,
selected: self.outputColorState.key
))
]))
return view
}
@objc private func changeDisplayValue(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.displayValueState = key
if key.count == 1 {
if self.modeState != "oneRow" {
self.modeState = "oneRow"
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_mode", value: self.modeState)
}
self.displayModeView?.selectItem(at: 0)
}
self.displayModeView?.isEnabled = key.count > 1
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_displayValue", value: key)
self.display()
}
@objc private func changeDisplayMode(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.modeState = key
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_mode", value: key)
self.display()
}
@objc private func toggleValue(_ sender: NSControl) {
self.valueState = controlState(sender)
self.valueColorView?.isEnabled = self.valueState
self.valueAlignmentView?.isEnabled = self.valueState
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_value", value: self.valueState)
self.display()
}
@objc private func toggleUnits(_ sender: NSControl) {
self.unitsState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_units", value: self.unitsState)
self.display()
}
@objc private func toggleIcon(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.icon = key
self.iconColorView?.isEnabled = self.icon != "none"
self.iconAlignmentView?.isEnabled = self.icon != "none"
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_icon", value: key)
self.display()
}
@objc private func toggleMonochrome(_ sender: NSControl) {
self.monochromeState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_monochrome", value: self.monochromeState)
self.display()
}
@objc private func toggleValueColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
if let newColor = SpeedPictogramColor.first(where: { $0.key == key }) {
self.valueColorState = newColor.key
}
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_valueColor", value: key)
self.display()
}
@objc private func toggleOutputColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.outputColorState = newValue
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_uploadColor", value: key)
}
@objc private func toggleInputColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.inputColorState = newValue
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_downloadColor", value: key)
}
public func setValue(input: Int64, output: Int64) {
var updated: Bool = false
if self.inputValue != input {
self.inputValue = abs(input)
updated = true
}
if self.outputValue != output {
self.outputValue = abs(output)
updated = true
}
if updated {
DispatchQueue.main.async(execute: {
self.display()
})
}
}
@objc private func toggleValueAlignment(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
if let newAlignment = Alignments.first(where: { $0.key == key }) {
self.valueAlignmentState = newAlignment.key
}
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_valueAlignment", value: key)
self.display()
}
@objc private func toggleIconAlignment(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
if let newAlignment = Alignments.first(where: { $0.key == key }) {
self.iconAlignmentState = newAlignment.key
}
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_iconAlignment", value: key)
self.display()
}
@objc private func toggleIconColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
if let newColor = SpeedPictogramColor.first(where: { $0.key == key }) {
self.iconColorState = newColor.key
}
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_iconColor", value: key)
self.display()
}
}
================================================
FILE: Kit/Widgets/Stack.swift
================================================
//
// Sensors.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 17/06/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public struct Stack_t: KeyValue_p {
public var key: String
public var value: String
var index: Int {
get { Store.shared.int(key: "stack_\(self.key)_index", defaultValue: -1) }
set { Store.shared.set(key: "stack_\(self.key)_index", value: newValue) }
}
public init(key: String, value: String) {
self.key = key
self.value = value
}
}
public class StackWidget: WidgetWrapper {
private var modeState: StackMode = .auto
private var fixedSizeState: Bool = false
private var monospacedFontState: Bool = false
private var alignmentState: String = "left"
private var values: [Stack_t] = []
private var oneRowWidth: CGFloat = 45
private var twoRowWidth: CGFloat = 32
private let orderTableView: OrderTableView
private var alignment: NSTextAlignment {
if let alignmentPair = Alignments.first(where: { $0.key == self.alignmentState }) {
return alignmentPair.additional as? NSTextAlignment ?? .left
}
return .left
}
public init(title: String, config: NSDictionary?, preview: Bool = false) {
if let config, preview {
if let previewConfig = config["Preview"] as? NSDictionary {
if let value = previewConfig["Values"] as? String {
for (i, value) in value.split(separator: ",").enumerated() {
self.values.append(Stack_t(key: "\(i)", value: String(value)))
}
}
}
}
self.orderTableView = OrderTableView(&self.values)
super.init(.stack, title: title, frame: CGRect(
x: 0,
y: Constants.Widget.margin.y,
width: Constants.Widget.width,
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
))
if !preview {
self.modeState = StackMode(rawValue: Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_mode", defaultValue: self.modeState.rawValue)) ?? .auto
self.fixedSizeState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_size", defaultValue: self.fixedSizeState)
self.monospacedFontState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_monospacedFont", defaultValue: self.monospacedFontState)
self.alignmentState = Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_alignment", defaultValue: self.alignmentState)
}
self.orderTableView.reorderCallback = { [weak self] in
self?.display()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
var values: [Stack_t] = []
var mode: StackMode = .auto
self.queue.sync {
values = self.values
mode = self.modeState
}
guard !values.isEmpty else {
self.setWidth(0)
return
}
let num: Int = Int(round(Double(values.count) / 2))
var totalWidth: CGFloat = Constants.Widget.spacing // opening space
var x: CGFloat = Constants.Widget.spacing
var i = 0
while i < values.count {
switch mode {
case .auto, .twoRows:
let firstElement: Stack_t = values[i]
let secondElement: Stack_t? = values.indices.contains(i+1) ? values[i+1] : nil
var width: CGFloat = 0
if mode == .auto && secondElement == nil {
width += self.drawOneRow(x, firstElement)
} else {
width += self.drawTwoRows(x, firstElement, secondElement)
}
x += width
totalWidth += width
if num != 1 && (i/2) != num {
x += Constants.Widget.spacing
totalWidth += Constants.Widget.spacing
}
i += 1
case .oneRow:
let width = self.drawOneRow(x, values[i])
x += width
totalWidth += width
// add margins between columns
if values.count != 1 && i != values.count {
x += Constants.Widget.spacing
totalWidth += Constants.Widget.spacing
}
}
i += 1
}
totalWidth += Constants.Widget.spacing // closing space
guard abs(self.frame.width - totalWidth) > 2 else { return }
self.setWidth(totalWidth)
}
private func drawOneRow(_ x: CGFloat, _ element: Stack_t) -> CGFloat {
var monospacedFontState: Bool = false
var fixedSizeState: Bool = false
var alignment: NSTextAlignment = .left
self.queue.sync {
monospacedFontState = self.monospacedFontState
fixedSizeState = self.fixedSizeState
alignment = self.alignment
}
var font: NSFont = NSFont.systemFont(ofSize: 13, weight: .regular)
if monospacedFontState {
font = NSFont.monospacedDigitSystemFont(ofSize: 13, weight: .regular)
}
let style = NSMutableParagraphStyle()
style.alignment = alignment
var width: CGFloat = self.oneRowWidth
if !fixedSizeState {
width = element.value.widthOfString(usingFont: font).rounded(.up) + 2
}
let rect = CGRect(x: x, y: (Constants.Widget.height-13)/2, width: width, height: 13)
let str = NSAttributedString.init(string: element.value, attributes: [
NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: NSColor.textColor,
NSAttributedString.Key.paragraphStyle: style
])
str.draw(with: rect)
return width
}
private func drawTwoRows(_ x: CGFloat, _ topElement: Stack_t, _ bottomElement: Stack_t?) -> CGFloat {
let rowHeight: CGFloat = self.frame.height / 2
var monospacedFontState: Bool = false
var fixedSizeState: Bool = false
var alignment: NSTextAlignment = .left
self.queue.sync {
monospacedFontState = self.monospacedFontState
fixedSizeState = self.fixedSizeState
alignment = self.alignment
}
var font: NSFont
if monospacedFontState {
font = NSFont.monospacedDigitSystemFont(ofSize: 10, weight: .light)
} else {
font = NSFont.systemFont(ofSize: 10, weight: .light)
}
let style = NSMutableParagraphStyle()
style.alignment = alignment
let attributes = [
NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: NSColor.textColor,
NSAttributedString.Key.paragraphStyle: style
]
var width: CGFloat = self.twoRowWidth
if !fixedSizeState {
let firstRowWidth = topElement.value.widthOfString(usingFont: font)
let secondRowWidth = bottomElement?.value.widthOfString(usingFont: font) ?? 0
width = max(20, max(firstRowWidth, secondRowWidth)).rounded(.up) + 2
}
var rect = CGRect(x: x, y: rowHeight+1, width: width, height: rowHeight)
var str = NSAttributedString.init(string: topElement.value, attributes: attributes)
str.draw(with: rect)
if bottomElement != nil {
rect = CGRect(x: x, y: 1, width: width, height: rowHeight)
str = NSAttributedString.init(string: bottomElement!.value, attributes: attributes)
str.draw(with: rect)
}
return width
}
public func setValues(_ values: [Stack_t]) {
DispatchQueue.main.async(execute: {
var tableNeedsToBeUpdated: Bool = false
values.forEach { (p: Stack_t) in
if let idx = self.values.firstIndex(where: { $0.key == p.key }) {
self.values[idx].value = p.value
return
}
tableNeedsToBeUpdated = true
self.values.append(p)
}
let diff = self.values.filter({ v in values.contains(where: { $0.key == v.key }) })
if diff.count != self.values.count {
tableNeedsToBeUpdated = true
}
self.values = diff.sorted(by: { $0.index < $1.index })
if tableNeedsToBeUpdated {
self.orderTableView.update()
}
self.display()
})
}
// MARK: - Settings
public override func settings() -> NSView {
let view = SettingsContainerView()
var rows = [
PreferencesRow(localizedString("Display mode"), component: selectView(
action: #selector(self.changeDisplayMode),
items: SensorsWidgetMode,
selected: self.modeState.rawValue
)),
PreferencesRow(localizedString("Monospaced font"), component: switchView(
action: #selector(self.toggleMonospacedFont),
state: self.monospacedFontState
)),
PreferencesRow(localizedString("Alignment"), component: selectView(
action: #selector(self.toggleAlignment),
items: Alignments,
selected: self.alignmentState
))
]
if self.title != "Clock" {
rows.append(PreferencesRow(localizedString("Static width"), component: switchView(
action: #selector(self.toggleSize),
state: self.fixedSizeState
)))
}
view.addArrangedSubview(PreferencesSection(rows))
view.addArrangedSubview(self.orderTableView)
return view
}
@objc private func changeDisplayMode(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.modeState = StackMode(rawValue: key) ?? .auto
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_mode", value: key)
self.display()
}
@objc private func toggleSize(_ sender: NSControl) {
self.fixedSizeState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_size", value: self.fixedSizeState)
self.display()
}
@objc private func toggleMonospacedFont(_ sender: NSControl) {
self.monospacedFontState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_monospacedFont", value: self.monospacedFontState)
self.display()
}
@objc private func toggleAlignment(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
if let newAlignment = Alignments.first(where: { $0.key == key }) {
self.alignmentState = newAlignment.key
}
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_alignment", value: key)
self.display()
}
}
private class OrderTableView: NSView, NSTableViewDelegate, NSTableViewDataSource {
private let scrollView = NSScrollView()
private let tableView = NSTableView()
private var dragDropType = NSPasteboard.PasteboardType(rawValue: "\(Bundle.main.bundleIdentifier!).sensors-row")
fileprivate var reorderCallback: () -> Void = {}
private let list: UnsafeMutablePointer<[Stack_t]>
init(_ list: UnsafeMutablePointer<[Stack_t]>) {
self.list = list
super.init(frame: NSRect.zero)
self.wantsLayer = true
self.layer?.cornerRadius = 3
self.scrollView.translatesAutoresizingMaskIntoConstraints = false
self.scrollView.documentView = self.tableView
self.scrollView.hasHorizontalScroller = false
self.scrollView.hasVerticalScroller = true
self.scrollView.autohidesScrollers = true
self.scrollView.backgroundColor = NSColor.clear
self.scrollView.drawsBackground = true
self.tableView.frame = self.scrollView.bounds
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.headerView = nil
self.tableView.backgroundColor = NSColor.clear
self.tableView.columnAutoresizingStyle = .firstColumnOnlyAutoresizingStyle
self.tableView.registerForDraggedTypes([dragDropType])
self.tableView.gridColor = .gridColor
self.tableView.gridStyleMask = [.solidVerticalGridLineMask, .solidHorizontalGridLineMask]
self.tableView.style = .plain
self.tableView.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "name")))
self.addSubview(self.scrollView)
NSLayoutConstraint.activate([
self.scrollView.leftAnchor.constraint(equalTo: self.leftAnchor),
self.scrollView.rightAnchor.constraint(equalTo: self.rightAnchor),
self.scrollView.topAnchor.constraint(equalTo: self.topAnchor),
self.scrollView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
self.heightAnchor.constraint(equalToConstant: 120)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
NotificationCenter.default.removeObserver(self)
}
fileprivate func update() {
self.tableView.reloadData()
}
func numberOfRows(in tableView: NSTableView) -> Int {
return list.pointee.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
if !self.list.pointee.indices.contains(row) { return nil }
let item = self.list.pointee[row]
let text: NSTextField = NSTextField()
text.drawsBackground = false
text.isBordered = false
text.isEditable = false
text.isSelectable = false
text.translatesAutoresizingMaskIntoConstraints = false
text.identifier = NSUserInterfaceItemIdentifier(item.key)
switch tableColumn?.identifier.rawValue {
case "name": text.stringValue = item.key
default: break
}
text.sizeToFit()
let cell = NSTableCellView()
cell.addSubview(text)
NSLayoutConstraint.activate([
text.widthAnchor.constraint(equalTo: cell.widthAnchor),
text.centerYAnchor.constraint(equalTo: cell.centerYAnchor)
])
return cell
}
func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
let item = NSPasteboardItem()
item.setString(String(row), forType: self.dragDropType)
return item
}
func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation {
if dropOperation == .above {
return .move
}
return []
}
func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableView.DropOperation) -> Bool {
var oldIndexes = [Int]()
info.enumerateDraggingItems(options: [], for: tableView, classes: [NSPasteboardItem.self], searchOptions: [:]) { dragItem, _, _ in
if let str = (dragItem.item as! NSPasteboardItem).string(forType: self.dragDropType), let index = Int(str) {
oldIndexes.append(index)
}
}
var oldIndexOffset = 0
var newIndexOffset = 0
tableView.beginUpdates()
for oldIndex in oldIndexes {
if oldIndex < row {
let currentIdx = oldIndex + oldIndexOffset
let newIdx = row - 1
self.list.pointee[currentIdx].index = newIdx
self.list.pointee[newIdx].index = currentIdx
oldIndexOffset -= 1
} else {
let currentIdx = oldIndex
let newIdx = row + newIndexOffset
self.list.pointee[currentIdx].index = newIdx
self.list.pointee[newIdx].index = currentIdx
newIndexOffset += 1
}
self.list.pointee = self.list.pointee.sorted(by: { $0.index < $1.index })
self.reorderCallback()
tableView.reloadData()
}
tableView.endUpdates()
return true
}
}
================================================
FILE: Kit/Widgets/State.swift
================================================
//
// State.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 18/09/2022.
// Using Swift 5.0.
// Running on macOS 12.6.
//
// Copyright © 2022 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public class StateWidget: WidgetWrapper {
private var activeColorState: SColor = .secondGreen
private var nonactiveColorState: SColor = .secondRed
private var value: Bool = false
private var colors: [SColor] = SColor.allColors
public init(title: String, config: NSDictionary?, preview: Bool = false) {
if config != nil {
var configuration = config!
if preview {
if let previewConfig = config!["Preview"] as? NSDictionary {
configuration = previewConfig
if let value = configuration["Value"] as? Bool {
self.value = value
}
}
}
}
super.init(.state, title: title, frame: CGRect(
x: 0,
y: Constants.Widget.margin.y,
width: 8 + (2*Constants.Widget.margin.x),
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
))
self.canDrawConcurrently = true
if !preview {
self.activeColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_activeColor", defaultValue: self.activeColorState.key))
self.nonactiveColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_\(self.type.rawValue)_nonactiveColor", defaultValue: self.nonactiveColorState.key))
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let circle = NSBezierPath(ovalIn: CGRect(x: Constants.Widget.margin.x, y: (self.frame.height - 8)/2, width: 8, height: 8))
let color = self.value ? self.activeColorState : self.nonactiveColorState
(color.additional as? NSColor)?.set()
circle.fill()
}
public func setValue(_ value: Bool) {
guard self.value != value else { return }
self.value = value
DispatchQueue.main.async(execute: {
self.display()
})
}
// MARK: - Settings
public override func settings() -> NSView {
let view = SettingsContainerView()
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Active state color"), component: selectView(
action: #selector(self.toggleActiveColor),
items: self.colors,
selected: self.activeColorState.key
)),
PreferencesRow(localizedString("Nonactive state color"), component: selectView(
action: #selector(self.toggleNonactiveColor),
items: self.colors,
selected: self.nonactiveColorState.key
))
]))
return view
}
@objc private func toggleActiveColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
if let newColor = SColor.allCases.first(where: { $0.key == key }) {
self.activeColorState = newColor
}
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_activeColor", value: key)
self.display()
}
@objc private func toggleNonactiveColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
if let newColor = SColor.allCases.first(where: { $0.key == key }) {
self.nonactiveColorState = newColor
}
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_nonactiveColor", value: key)
self.display()
}
}
================================================
FILE: Kit/Widgets/Tachometer.swift
================================================
//
// Tachometer.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 11/10/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public class Tachometer: WidgetWrapper {
private var labelState: Bool = false
private var monochromeState: Bool = false
private var chart: TachometerGraphView = TachometerGraphView(
frame: NSRect(
x: Constants.Widget.margin.x,
y: Constants.Widget.margin.y,
width: Constants.Widget.height,
height: Constants.Widget.height
), segments: []
)
private var labelView: NSView? = nil
private let size: CGFloat = Constants.Widget.height - (Constants.Widget.margin.y*2) + (Constants.Widget.margin.x*2)
public init(title: String, preview: Bool = false) {
let widgetTitle: String = title
super.init(.tachometer, title: widgetTitle, frame: CGRect(
x: Constants.Widget.margin.x,
y: Constants.Widget.margin.y,
width: self.size,
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
))
self.canDrawConcurrently = true
if preview {
self.chart.setSegments([
circle_segment(value: 0.20, color: NSColor.systemRed),
circle_segment(value: 0.57, color: NSColor.systemBlue)
])
} else {
self.labelState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_label", defaultValue: self.labelState)
self.monochromeState = Store.shared.bool(key: "\(self.title)_\(self.type.rawValue)_monochrome", defaultValue: self.monochromeState)
}
self.draw()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func draw() {
let x: CGFloat = self.labelState ? 8 + Constants.Widget.spacing : 0
self.labelView = WidgetLabelView(self.title, height: self.frame.height)
self.labelView!.isHidden = !self.labelState
self.addSubview(self.labelView!)
self.addSubview(self.chart)
self.chart.setFrame(NSRect(x: x, y: 0, width: self.frame.size.height, height: self.frame.size.height))
self.setFrameSize(NSSize(width: self.size + x, height: self.frame.size.height))
self.setWidth(self.size + x)
}
public func setValue(_ list: [circle_segment]) {
var segments = list
if self.monochromeState {
for i in 0.. NSView {
let view = SettingsContainerView()
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Label"), component: switchView(
action: #selector(self.toggleLabel),
state: self.labelState
)),
PreferencesRow(localizedString("Monochrome accent"), component: switchView(
action: #selector(self.toggleMonochrome),
state: self.monochromeState
))
]))
return view
}
@objc private func toggleLabel(_ sender: NSControl) {
self.labelState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_label", value: self.labelState)
let x = self.labelState ? 6 + Constants.Widget.spacing : 0
self.labelView!.isHidden = !self.labelState
self.chart.setFrameOrigin(NSPoint(x: x, y: 0))
self.setWidth(self.labelState ? self.size+x : self.size)
}
@objc private func toggleMonochrome(_ sender: NSControl) {
self.monochromeState = controlState(sender)
Store.shared.set(key: "\(self.title)_\(self.type.rawValue)_monochrome", value: self.monochromeState)
}
}
================================================
FILE: Kit/Widgets/Text.swift
================================================
//
// Text.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 08/09/2024
// Using Swift 5.0
// Running on macOS 14.6
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public class TextWidget: WidgetWrapper {
private var value: String = ""
public init(title: String, config: NSDictionary?, preview: Bool = false) {
super.init(.text, title: title, frame: CGRect(
x: 0,
y: Constants.Widget.margin.y,
width: 30 + (2*Constants.Widget.margin.x),
height: Constants.Widget.height - (2*Constants.Widget.margin.y)
))
if preview {
self.value = "Text"
}
self.canDrawConcurrently = true
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
var value: String = ""
self.queue.sync {
value = self.value
}
if value.isEmpty {
self.setWidth(0)
return
}
let valueSize: CGFloat = 12
let style = NSMutableParagraphStyle()
style.alignment = .center
let stringAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: valueSize, weight: .regular),
NSAttributedString.Key.foregroundColor: NSColor.textColor,
NSAttributedString.Key.paragraphStyle: style
]
let attributedString = NSAttributedString(string: value, attributes: stringAttributes)
let size = attributedString.boundingRect(
with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading]
)
let width = (size.width+Constants.Widget.margin.x*2).roundedUpToNearestTen()
let origin: CGPoint = CGPoint(x: Constants.Widget.margin.x, y: ((Constants.Widget.height-valueSize-1)/2))
let rect = CGRect(x: origin.x, y: origin.y, width: width - (Constants.Widget.margin.x*2), height: valueSize)
attributedString.draw(with: rect)
self.setWidth(width)
}
public func setValue(_ newValue: String) {
guard self.value != newValue else { return }
self.value = newValue
DispatchQueue.main.async(execute: {
self.display()
})
}
static public func parseText(_ raw: String) -> [KeyValue_t] {
var pairs: [KeyValue_t] = []
do {
let regex = try NSRegularExpression(pattern: "(\\$[a-zA-Z0-9_]+)(?:\\.([a-zA-Z0-9_]+))?")
let matches = regex.matches(in: raw, range: NSRange(raw.startIndex..., in: raw))
for match in matches {
if let keyRange = Range(match.range(at: 1), in: raw) {
let key = String(raw[keyRange])
let value: String?
if match.range(at: 2).location != NSNotFound, let valueRange = Range(match.range(at: 2), in: raw) {
value = String(raw[valueRange])
} else {
value = nil
}
pairs.append(KeyValue_t(key: key, value: value ?? ""))
}
}
} catch {
print("Error creating regex: \(error.localizedDescription)")
}
return pairs
}
}
================================================
FILE: Kit/constants.swift
================================================
//
// constants.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 15/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public struct Popup_c_s {
public let width: CGFloat = 264
public let height: CGFloat = 300
public let margins: CGFloat = 8
public let spacing: CGFloat = 2
public let headerHeight: CGFloat = 42
public let separatorHeight: CGFloat = 30
public let portalHeight: CGFloat = 120
}
public struct Settings_c_s {
public let width: CGFloat = 540
public let height: CGFloat = 480
public let margin: CGFloat = 10
public let row: CGFloat = 30
}
public struct Widget_c_s {
public let width: CGFloat = 32
public var height: CGFloat {
get {
let systemHeight = NSApplication.shared.mainMenu?.menuBarHeight
return (systemHeight == 0 ? 22 : systemHeight) ?? 22
}
}
public var margin: CGPoint {
get { CGPoint(x: 0, y: 2) }
}
public let spacing: CGFloat = 2
}
public struct Constants {
public static let Popup: Popup_c_s = Popup_c_s()
public static let Settings: Settings_c_s = Settings_c_s()
public static let Widget: Widget_c_s = Widget_c_s()
public static let defaultProcessIcon = NSWorkspace.shared.icon(forFile: "/bin/bash")
}
public enum ModuleType: Int {
case CPU
case RAM
case GPU
case disk
case sensors
case network
case battery
case bluetooth
case clock
case combined
public var stringValue: String {
switch self {
case .CPU: return "CPU"
case .RAM: return "RAM"
case .GPU: return "GPU"
case .disk: return "Disk"
case .sensors: return "Sensors"
case .network: return "Network"
case .battery: return "Battery"
case .bluetooth: return "Bluetooth"
case .clock: return "Clock"
case .combined: return ""
}
}
}
================================================
FILE: Kit/extensions.swift
================================================
//
// extensions.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 10/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Carbon
extension String: @retroactive LocalizedError {
public var errorDescription: String? { return self }
public var nilIfEmpty: String? { self.isEmpty ? nil : self }
public var digits: String {
return components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
}
public func widthOfString(usingFont font: NSFont) -> CGFloat {
let fontAttributes = [NSAttributedString.Key.font: font]
let size = self.size(withAttributes: fontAttributes)
return size.width
}
public func condenseWhitespace() -> String {
let components = self.components(separatedBy: .whitespacesAndNewlines)
return components.filter { !$0.isEmpty }.joined(separator: " ")
}
public func findAndCrop(pattern: String) -> (cropped: String, remain: String) {
do {
let regex = try NSRegularExpression(pattern: pattern)
let range = NSRange(self.startIndex..., in: self)
if let match = regex.firstMatch(in: self, options: [], range: range) {
if let range = Range(match.range, in: self) {
let cropped = String(self[range]).trimmingCharacters(in: .whitespaces)
let remaining = self.replacingOccurrences(of: cropped, with: "", options: .regularExpression).trimmingCharacters(in: .whitespaces)
return (cropped, remaining)
}
}
} catch {
print("Error creating regex: \(error.localizedDescription)")
}
return ("", self)
}
public func find(pattern: String) -> String {
do {
let regex = try NSRegularExpression(pattern: pattern)
let stringRange = NSRange(location: 0, length: self.utf16.count)
if let searchRange = regex.firstMatch(in: self, options: [], range: stringRange) {
let start = self.index(self.startIndex, offsetBy: searchRange.range.lowerBound)
let end = self.index(self.startIndex, offsetBy: searchRange.range.upperBound)
let value = String(self[start.. Bool {
return self.range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil
}
public func removedRegexMatches(pattern: String, replaceWith: String = "") -> String {
do {
let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options.caseInsensitive)
let range = NSRange(location: 0, length: self.count)
return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith)
} catch {
return self
}
}
func removingWhitespaces() -> String {
return components(separatedBy: .whitespaces).joined()
}
}
public extension DispatchSource.MemoryPressureEvent {
func pressureColor() -> NSColor {
switch self {
case .normal:
return NSColor.systemGreen
case .warning:
return NSColor.systemYellow
case .critical:
return NSColor.systemRed
default:
return .controlAccentColor
}
}
}
public extension Double {
func roundTo(decimalPlaces: Int) -> String {
return NSString(format: "%.\(decimalPlaces)f" as NSString, self) as String
}
func rounded(toPlaces places: Int) -> Double {
let divisor = pow(10.0, Double(places))
return (self * divisor).rounded() / divisor
}
func usageColor(zones: colorZones = (0.6, 0.8), reversed: Bool = false) -> NSColor {
let firstColor: NSColor = NSColor.systemBlue
let secondColor: NSColor = NSColor.orange
let thirdColor: NSColor = NSColor.red
if reversed {
switch self {
case 0...zones.orange:
return thirdColor
case zones.orange...zones.red:
return secondColor
default:
return firstColor
}
} else {
switch self {
case 0...zones.orange:
return firstColor
case zones.orange...zones.red:
return secondColor
default:
return thirdColor
}
}
}
func batteryColor(color: Bool = false, lowPowerMode: Bool? = nil) -> NSColor {
if let mode = lowPowerMode, mode {
return NSColor.systemOrange
}
switch self {
case 0.2...0.4:
if !color {
return NSColor.textColor
}
return NSColor.systemOrange
case 0.4...1:
if self == 1 {
return NSColor.textColor
}
if !color {
return NSColor.textColor
}
return NSColor.systemGreen
default:
return NSColor.systemRed
}
}
func secondsToHoursMinutesSeconds() -> (Int, Int) {
let mins = (self.truncatingRemainder(dividingBy: 3600)) / 60
return (Int(self / 3600), Int(mins))
}
func printSecondsToHoursMinutesSeconds(short: Bool = false) -> String {
let (h, m) = self.secondsToHoursMinutesSeconds()
if self == 0 || h < 0 || m < 0 {
return "n/a"
}
let minutes = m > 9 ? "\(m)" : "0\(m)"
if short {
return "\(h):\(minutes)"
}
if h == 0 {
return "\(minutes)min"
} else if m == 0 {
return "\(h)h"
}
return "\(h)h \(minutes)min"
}
func power(_ unit: String) -> Double {
switch unit {
case "mJ":
return self / 1e3
case "uJ":
return self / 1e6
case "nJ":
return self / 1e9
default:
return 0
}
}
}
public extension NSView {
var isDarkMode: Bool {
switch effectiveAppearance.name {
case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark:
return true
default:
return false
}
}
func toggleSettingRow(title: String, action: Selector, state: Bool) -> NSView {
let view: NSStackView = NSStackView()
view.translatesAutoresizingMaskIntoConstraints = false
view.heightAnchor.constraint(equalToConstant: Constants.Settings.row).isActive = true
view.orientation = .horizontal
view.alignment = .centerY
view.distribution = .fill
view.spacing = 0
let titleField: NSTextField = LabelField(frame: NSRect(x: 0, y: 0, width: 0, height: 0), title)
titleField.font = NSFont.systemFont(ofSize: 12, weight: .regular)
titleField.textColor = .textColor
let state: NSControl.StateValue = state ? .on : .off
var toggle: NSControl = NSControl()
if #available(OSX 10.15, *) {
let switchButton = NSSwitch()
switchButton.state = state
switchButton.action = action
switchButton.target = self
toggle = switchButton
} else {
let button: NSButton = NSButton()
button.setButtonType(.switch)
button.state = state
button.title = ""
button.action = action
button.isBordered = false
button.isTransparent = false
button.target = self
button.wantsLayer = true
toggle = button
}
view.addArrangedSubview(titleField)
view.addArrangedSubview(NSView())
view.addArrangedSubview(toggle)
return view
}
func selectSettingsRow(title: String, action: Selector, items: [KeyValue_p], selected: String) -> NSView {
let view = NSStackView()
view.translatesAutoresizingMaskIntoConstraints = false
view.heightAnchor.constraint(equalToConstant: Constants.Settings.row).isActive = true
view.orientation = .horizontal
view.alignment = .centerY
view.distribution = .fill
view.spacing = 0
let titleField: NSTextField = LabelField(frame: NSRect(x: 0, y: 0, width: 0, height: 0), title)
titleField.font = NSFont.systemFont(ofSize: 12, weight: .regular)
titleField.textColor = .textColor
let select: NSPopUpButton = selectView(action: action, items: items, selected: selected)
select.sizeToFit()
view.addArrangedSubview(titleField)
view.addArrangedSubview(NSView())
view.addArrangedSubview(select)
return view
}
func selectView(action: Selector, items: [KeyValue_p], selected: String) -> NSPopUpButton {
let select: NSPopUpButton = NSPopUpButton(frame: NSRect(x: 0, y: 4, width: 50, height: 28))
select.target = self
select.action = action
let menu = NSMenu()
items.forEach { (item) in
if item.key.contains("separator") {
menu.addItem(NSMenuItem.separator())
} else {
let interfaceMenu = NSMenuItem(title: localizedString(item.value), action: nil, keyEquivalent: "")
interfaceMenu.representedObject = item.key
menu.addItem(interfaceMenu)
if selected == item.key {
interfaceMenu.state = .on
}
}
}
select.menu = menu
return select
}
func switchView(action: Selector, state: Bool) -> NSSwitch {
let s = NSSwitch()
s.heightAnchor.constraint(equalToConstant: 25).isActive = true
s.controlSize = .mini
s.state = state ? .on : .off
s.action = action
s.target = self
return s
}
func buttonView(_ action: Selector, text: String) -> NSButton {
let button = NSButton()
button.title = text
button.contentTintColor = .labelColor
button.action = action
button.target = self
return button
}
func buttonIconView(_ action: Selector, icon: NSImage, height: CGFloat = 22) -> NSButton {
let button = NSButton()
button.heightAnchor.constraint(equalToConstant: height).isActive = true
button.bezelStyle = .regularSquare
button.translatesAutoresizingMaskIntoConstraints = false
button.imageScaling = .scaleNone
button.image = icon
button.contentTintColor = .labelColor
button.isBordered = false
button.action = action
button.target = self
button.focusRingType = .none
return button
}
func textView(_ value: String, alignment: NSTextAlignment = .left) -> NSTextField {
let field: NSTextField = TextView()
field.font = NSFont.systemFont(ofSize: 13, weight: .regular)
field.stringValue = value
field.isSelectable = true
field.alignment = alignment
return field
}
func sliderView(action: Selector, value: Int, initialValue: String, min: Double = 1, max: Double = 100, valueWidth: CGFloat = 40) -> NSView {
let view: NSStackView = NSStackView()
view.orientation = .horizontal
view.widthAnchor.constraint(equalToConstant: 195).isActive = true
let valueField: NSTextField = LabelField(initialValue)
valueField.font = NSFont.systemFont(ofSize: 12, weight: .regular)
valueField.textColor = .textColor
valueField.alignment = .center
valueField.widthAnchor.constraint(equalToConstant: valueWidth).isActive = true
let slider = NSSlider()
slider.controlSize = .small
slider.minValue = min
slider.maxValue = max
slider.intValue = Int32(value)
slider.target = self
slider.isContinuous = true
slider.action = action
slider.sizeToFit()
view.addArrangedSubview(slider)
view.addArrangedSubview(valueField)
return view
}
}
public class NSButtonWithPadding: NSButton {
public var horizontalPadding: CGFloat = 0
public var verticalPadding: CGFloat = 0
public override var intrinsicContentSize: NSSize {
var size = super.intrinsicContentSize
size.width += self.horizontalPadding
size.height += self.verticalPadding
return size
}
}
public class TextView: NSTextField {
public override init(frame: NSRect = .zero) {
super.init(frame: frame)
self.isEditable = false
self.isSelectable = false
self.isBezeled = false
self.wantsLayer = true
self.textColor = .labelColor
self.backgroundColor = .clear
self.canDrawSubviewsIntoLayer = true
self.alignment = .natural
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
public extension OperatingSystemVersion {
func getFullVersion(separator: String = ".") -> String {
return "\(majorVersion)\(separator)\(minorVersion)\(separator)\(patchVersion)"
}
}
extension URL {
func checkFileExist() -> Bool {
return FileManager.default.fileExists(atPath: self.path)
}
}
public extension NSColor {
func grayscaled() -> NSColor {
guard let space = CGColorSpace(name: CGColorSpace.extendedGray),
let cg = self.cgColor.converted(to: space, intent: .perceptual, options: nil),
let color = NSColor.init(cgColor: cg) else {
return self
}
return color
}
}
public class FlippedStackView: NSStackView {
public override var isFlipped: Bool { return true }
}
open class ScrollableStackView: NSView {
public var stackView: NSStackView = FlippedStackView()
private let clipView: NSClipView = NSClipView()
private let scrollView: NSScrollView = NSScrollView()
public var scrollWidth: CGFloat? {
self.scrollView.verticalScroller?.frame.size.width
}
public init(frame: NSRect = NSRect.zero, orientation: NSUserInterfaceLayoutOrientation = .vertical) {
super.init(frame: frame)
self.clipView.drawsBackground = false
self.stackView.orientation = orientation
self.stackView.translatesAutoresizingMaskIntoConstraints = false
self.scrollView.translatesAutoresizingMaskIntoConstraints = false
if orientation == .vertical {
self.scrollView.hasVerticalScroller = true
self.scrollView.hasHorizontalScroller = false
self.scrollView.autohidesScrollers = true
self.scrollView.horizontalScrollElasticity = .none
} else {
self.scrollView.hasVerticalScroller = false
self.scrollView.hasHorizontalScroller = true
self.scrollView.autohidesScrollers = true
self.scrollView.verticalScrollElasticity = .none
}
self.scrollView.drawsBackground = false
self.scrollView.contentView = self.clipView
self.scrollView.documentView = self.stackView
self.addSubview(self.scrollView)
NSLayoutConstraint.activate([
self.scrollView.leftAnchor.constraint(equalTo: self.leftAnchor),
self.scrollView.rightAnchor.constraint(equalTo: self.rightAnchor),
self.scrollView.topAnchor.constraint(equalTo: self.topAnchor),
self.scrollView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
self.stackView.leftAnchor.constraint(equalTo: self.clipView.leftAnchor),
self.stackView.topAnchor.constraint(equalTo: self.clipView.topAnchor)
])
if orientation == .vertical {
self.stackView.rightAnchor.constraint(equalTo: self.clipView.rightAnchor).isActive = true
} else {
self.stackView.bottomAnchor.constraint(equalTo: self.clipView.bottomAnchor).isActive = true
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// https://stackoverflow.com/a/54492165
extension NSTextView {
override open func performKeyEquivalent(with event: NSEvent) -> Bool {
let commandKey = NSEvent.ModifierFlags.command.rawValue
let commandShiftKey = NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue
if event.type == NSEvent.EventType.keyDown {
if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey {
switch event.charactersIgnoringModifiers! {
case "x":
if NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: self) { return true }
case "c":
if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: self) { return true }
case "v":
if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: self) { return true }
case "z":
if NSApp.sendAction(Selector(("undo:")), to: nil, from: self) { return true }
case "a":
if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self) { return true }
default:
break
}
} else if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandShiftKey {
if event.charactersIgnoringModifiers == "Z" {
if NSApp.sendAction(Selector(("redo:")), to: nil, from: self) { return true }
}
}
}
return super.performKeyEquivalent(with: event)
}
}
public extension Data {
var socketAddress: sockaddr {
return withUnsafeBytes { $0.load(as: sockaddr.self) }
}
}
public extension Date {
func convertToTimeZone(_ timeZone: TimeZone) -> Date {
return addingTimeInterval(TimeInterval(timeZone.secondsFromGMT(for: self) - TimeZone.current.secondsFromGMT(for: self)))
}
func currentTimeSeconds() -> Int {
return Int(self.timeIntervalSince1970)
}
}
public extension TimeZone {
init(from: String) {
if let tz = TimeZone(identifier: from) {
self = tz
return
}
if from == "local" {
self = TimeZone.current
return
}
let arr = from.split(separator: ":")
guard !arr.isEmpty else {
self = TimeZone.current
return
}
var secondsFromGMT = 0
if arr.indices.contains(0), let h = Int(arr[0]) {
secondsFromGMT += h*3600
}
if arr.indices.contains(1), let m = Int(arr[1]) {
if secondsFromGMT < 0 {
secondsFromGMT -= m*60
} else {
secondsFromGMT += m*60
}
}
if let tz = TimeZone(secondsFromGMT: secondsFromGMT) {
self = tz
} else {
self = TimeZone.current
}
}
}
extension CGFloat {
func roundedUpToNearestTen() -> CGFloat {
return ceil(self / 10) * 10
}
}
public class KeyboardShartcutView: NSStackView {
private let callback: (_ value: [UInt16]) -> Void
private var startIcon: NSImage { iconFromSymbol(name: "record.circle", scale: .large) }
private var stopIcon: NSImage { iconFromSymbol(name: "stop.circle.fill", scale: .large) }
private var valueField: NSTextField? = nil
private var startButton: NSButton? = nil
private var stopButton: NSButton? = nil
private var recording: Bool = false
private var keyCodes: [UInt16] = []
private var value: [UInt16] = []
private var interaction: Bool = false
public init(callback: @escaping (_ value: [UInt16]) -> Void, value: [UInt16]) {
self.callback = callback
self.value = value
super.init(frame: NSRect.zero)
self.orientation = .horizontal
let stringValue = value.isEmpty ? localizedString("Disabled") : self.parseValue(value)
let valueField: NSTextField = LabelField(stringValue)
valueField.font = NSFont.systemFont(ofSize: 13, weight: .regular)
valueField.textColor = .textColor
valueField.alignment = .center
let startButton = buttonIconView(#selector(self.startListening), icon: self.startIcon, height: 15)
let stopButton = buttonIconView(#selector(self.stopListening), icon: self.stopIcon, height: 15)
self.addArrangedSubview(valueField)
self.addArrangedSubview(startButton)
self.valueField = valueField
self.startButton = startButton
self.stopButton = stopButton
NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .flagsChanged]) { [weak self] event in
self?.handleKeyEvent(event)
return event
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func startListening() {
guard AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary) else { return }
if let btn = self.stopButton {
self.startButton?.removeFromSuperview()
self.addArrangedSubview(btn)
}
self.valueField?.stringValue = localizedString("Listening...")
self.keyCodes = []
self.recording = true
}
@objc private func stopListening() {
if let btn = self.startButton {
self.stopButton?.removeFromSuperview()
self.addArrangedSubview(btn)
}
if self.keyCodes.isEmpty && !self.interaction {
self.value = []
self.valueField?.stringValue = localizedString("Disabled")
}
self.recording = false
self.interaction = false
self.callback(self.value)
}
private func handleKeyEvent(_ event: NSEvent) {
guard self.recording else { return }
self.interaction = true
if event.type == .flagsChanged {
self.keyCodes = []
if event.modifierFlags.contains(.control) { self.keyCodes.append(59) }
if event.modifierFlags.contains(.shift) { self.keyCodes.append(60) }
if event.modifierFlags.contains(.command) { self.keyCodes.append(55) }
if event.modifierFlags.contains(.option) { self.keyCodes.append(58) }
} else if event.type == .keyDown {
self.keyCodes.append(event.keyCode)
self.value = self.keyCodes
}
let list = self.keyCodes.isEmpty ? self.value : self.keyCodes
self.valueField?.stringValue = self.parseValue(list)
}
private func parseValue(_ list: [UInt16]) -> String {
return list.compactMap { self.keyName(virtualKeyCode: $0) }.joined(separator: " + ")
}
private func keyName(virtualKeyCode: UInt16) -> String? {
if virtualKeyCode == 59 {
return "Control"
} else if virtualKeyCode == 60 {
return "Shift"
} else if virtualKeyCode == 55 {
return "Command"
} else if virtualKeyCode == 58 {
return "Option"
}
let maxNameLength = 4
var nameBuffer = [UniChar](repeating: 0, count: maxNameLength)
var nameLength = 0
let modifierKeys = UInt32(alphaLock >> 8) & 0xFF // Caps Lock
var deadKeys: UInt32 = 0
let keyboardType = UInt32(LMGetKbdType())
let source = TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
guard let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else {
NSLog("Could not get keyboard layout data")
return nil
}
let layoutData = Unmanaged.fromOpaque(ptr).takeUnretainedValue() as Data
let osStatus = layoutData.withUnsafeBytes {
UCKeyTranslate($0.bindMemory(to: UCKeyboardLayout.self).baseAddress, virtualKeyCode, UInt16(kUCKeyActionDown),
modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
&deadKeys, maxNameLength, &nameLength, &nameBuffer)
}
guard osStatus == noErr else {
NSLog("Code: 0x%04X Status: %+i", virtualKeyCode, osStatus)
return nil
}
return String(utf16CodeUnits: nameBuffer, count: nameLength)
}
}
================================================
FILE: Kit/helpers.swift
================================================
//
// helpers.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 29/09/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// swiftlint:disable file_length
import Cocoa
import ServiceManagement
import UserNotifications
import WebKit
import Metal
public struct LaunchAtLogin {
private static let id = "\(Bundle.main.bundleIdentifier!).LaunchAtLogin"
public static var isEnabled: Bool {
get {
if #available(macOS 13, *) {
return isEnabledNext
} else {
return isEnabledLegacy
}
}
set {
if #available(macOS 13, *) {
isEnabledNext = newValue
} else {
isEnabledLegacy = newValue
}
}
}
private static var isEnabledLegacy: Bool {
get {
guard let jobs = (LaunchAtLogin.self as DeprecationWarningWorkaround.Type).jobsDict else {
return false
}
let job = jobs.first { $0["Label"] as! String == id }
return job?["OnDemand"] as? Bool ?? false
}
set {
SMLoginItemSetEnabled(id as CFString, newValue)
}
}
@available(macOS 13, *)
private static var isEnabledNext: Bool {
get { SMAppService.mainApp.status == .enabled }
set {
do {
if newValue {
if SMAppService.mainApp.status == .enabled {
try? SMAppService.mainApp.unregister()
}
try SMAppService.mainApp.register()
} else {
try SMAppService.mainApp.unregister()
}
} catch {
print("failed to \(newValue ? "enable" : "disable") launch at login: \(error.localizedDescription)")
}
}
}
public static func migrate() {
guard #available(macOS 13, *), !Store.shared.exist(key: "LaunchAtLoginNext") else {
return
}
Store.shared.set(key: "LaunchAtLoginNext", value: true)
isEnabledNext = isEnabledLegacy
isEnabledLegacy = false
try? SMAppService.loginItem(identifier: id).unregister()
}
}
private protocol DeprecationWarningWorkaround {
static var jobsDict: [[String: AnyObject]]? { get }
}
extension LaunchAtLogin: DeprecationWarningWorkaround {
@available(*, deprecated)
static var jobsDict: [[String: AnyObject]]? {
SMCopyAllJobDictionaries(kSMDomainUserLaunchd)?.takeRetainedValue() as? [[String: AnyObject]]
}
}
public protocol KeyValue_p {
var key: String { get }
var value: String { get }
}
public struct KeyValue_t: KeyValue_p, Codable {
public let key: String
public let value: String
public let additional: Any?
private enum CodingKeys: String, CodingKey {
case key, value
}
public init(key: String, value: String, additional: Any? = nil) {
self.key = key
self.value = value
self.additional = additional
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.key = try container.decode(String.self, forKey: .key)
self.value = try container.decode(String.self, forKey: .value)
self.additional = nil
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(key, forKey: .key)
try container.encode(value, forKey: .value)
}
}
public struct Units {
public let bytes: Int64
public init(bytes: Int64) {
self.bytes = bytes
}
public var kilobytes: Double {
return Double(bytes) / 1_000
}
public var megabytes: Double {
return kilobytes / 1_000
}
public var gigabytes: Double {
return megabytes / 1_000
}
public var terabytes: Double {
return gigabytes / 1_000
}
public func getReadableTuple(base: DataSizeBase = .byte) -> (String, String) {
let stringBase = base == .byte ? "B" : "b"
let multiplier: Double = base == .byte ? 1 : 8
switch bytes {
case 0..<1_000:
return ("0", "K\(stringBase)/s")
case 1_000..<(1_000 * 1_000):
return (String(format: "%.0f", kilobytes*multiplier), "K\(stringBase)/s")
case 1_000..<(1_000 * 1_000 * 100):
return (String(format: "%.1f", megabytes*multiplier), "M\(stringBase)/s")
case (1_000 * 1_000 * 100)..<(1_000 * 1_000 * 1_000):
return (String(format: "%.0f", megabytes*multiplier), "M\(stringBase)/s")
case (1_000 * 1_000 * 1_000)...Int64.max:
return (String(format: "%.1f", gigabytes*multiplier), "G\(stringBase)/s")
default:
return (String(format: "%.0f", kilobytes*multiplier), "K\(stringBase)B/s")
}
}
public func getReadableSpeed(base: DataSizeBase = .byte, omitUnits: Bool = false) -> String {
let stringBase = base == .byte ? "B" : "b"
let multiplier: Double = base == .byte ? 1 : 8
switch bytes*Int64(multiplier) {
case 0..<1_000:
let unit = omitUnits ? "" : " K\(stringBase)/s"
return "0\(unit)"
case 1_000..<(1_000 * 1_000):
let unit = omitUnits ? "" : " K\(stringBase)/s"
return String(format: "%.0f\(unit)", kilobytes*multiplier)
case 1_000..<(1_000 * 1_000 * 100):
let unit = omitUnits ? "" : " M\(stringBase)/s"
return String(format: "%.1f\(unit)", megabytes*multiplier)
case (1_000 * 1_000 * 100)..<(1_000 * 1_000 * 1_000):
let unit = omitUnits ? "" : " M\(stringBase)/s"
return String(format: "%.0f\(unit)", megabytes*multiplier)
case (1_000 * 1_000 * 1_000)...Int64.max:
let unit = omitUnits ? "" : " G\(stringBase)/s"
return String(format: "%.1f\(unit)", gigabytes*multiplier)
default:
let unit = omitUnits ? "" : " K\(stringBase)/s"
return String(format: "%.0f\(unit)", kilobytes*multiplier)
}
}
public func getReadableMemory(style: ByteCountFormatter.CountStyle = .file) -> String {
let formatter: ByteCountFormatter = ByteCountFormatter()
formatter.countStyle = style
formatter.includesUnit = true
formatter.isAdaptive = true
var value = formatter.string(fromByteCount: Int64(self.bytes))
if let idx = value.lastIndex(of: ",") {
value.replaceSubrange(idx...idx, with: ".")
}
return value
}
public func toUnit(_ unit: SizeUnit) -> Double {
switch unit {
case .KB: return self.kilobytes
case .MB: return self.megabytes
case .GB: return self.gigabytes
case .TB: return self.terabytes
default: return Double(self.bytes)
}
}
}
public struct DiskSize {
public let value: Int64
public init(_ size: Int64) {
self.value = size
}
public var kilobytes: Double {
return Double(value) / 1_000
}
public var megabytes: Double {
return kilobytes / 1_000
}
public var gigabytes: Double {
return megabytes / 1_000
}
public var terabytes: Double {
return gigabytes / 1_000
}
public func getReadableMemory() -> String {
switch value {
case 0..<1_000:
return "0 KB"
case 1_000..<(1_000 * 1_000):
return String(format: "%.0f KB", kilobytes)
case 1_000..<(1_000 * 1_000 * 1_000):
return String(format: "%.0f MB", megabytes)
case 1_000..<(1_000 * 1_000 * 1_000 * 1_000):
return String(format: "%.1f GB", gigabytes)
case (1_000 * 1_000 * 1_000 * 1_000)...Int64.max:
return String(format: "%.1f TB", terabytes)
default:
return String(format: "%.0f KB", kilobytes)
}
}
}
public class LabelField: NSTextField {
public init(frame: NSRect = NSRect.zero, _ label: String = "") {
super.init(frame: frame)
self.isEditable = false
self.isSelectable = false
self.isBezeled = false
self.wantsLayer = true
self.backgroundColor = .clear
self.canDrawSubviewsIntoLayer = true
self.stringValue = label
self.textColor = .secondaryLabelColor
self.alignment = .natural
self.font = NSFont.systemFont(ofSize: 12, weight: .regular)
self.cell?.truncatesLastVisibleLine = true
self.cell?.usesSingleLineMode = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
public class ValueField: NSTextField {
public init(frame: NSRect = NSRect.zero, _ value: String = "") {
super.init(frame: frame)
self.isEditable = false
self.isSelectable = false
self.isBezeled = false
self.wantsLayer = true
self.backgroundColor = .clear
self.canDrawSubviewsIntoLayer = true
self.stringValue = value
self.textColor = .textColor
self.alignment = .right
self.font = NSFont.systemFont(ofSize: 13, weight: .regular)
self.cell?.usesSingleLineMode = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
public extension NSBezierPath {
func addArrow(start: CGPoint, end: CGPoint, pointerLineLength: CGFloat, arrowAngle: CGFloat) {
self.move(to: start)
self.line(to: end)
let startEndAngle = atan((end.y - start.y) / (end.x - start.x)) + ((end.x - start.x) < 0 ? CGFloat(Double.pi) : 0)
let arrowLine1 = CGPoint(
x: end.x + pointerLineLength * cos(CGFloat(Double.pi) - startEndAngle + arrowAngle),
y: end.y - pointerLineLength * sin(CGFloat(Double.pi) - startEndAngle + arrowAngle)
)
let arrowLine2 = CGPoint(
x: end.x + pointerLineLength * cos(CGFloat(Double.pi) - startEndAngle - arrowAngle),
y: end.y - pointerLineLength * sin(CGFloat(Double.pi) - startEndAngle - arrowAngle)
)
self.line(to: arrowLine1)
self.move(to: end)
self.line(to: arrowLine2)
}
}
public func separatorView(_ title: String, origin: NSPoint = NSPoint(x: 0, y: 0), width: CGFloat = 0) -> NSView {
let view: NSView = NSView(frame: NSRect(x: origin.x, y: origin.y, width: width, height: 30))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let labelView: NSTextField = TextView(frame: NSRect(x: 0, y: (view.frame.height-15)/2, width: view.frame.width, height: 15))
labelView.stringValue = title
labelView.alignment = .center
labelView.textColor = .secondaryLabelColor
labelView.font = NSFont.systemFont(ofSize: 12, weight: .medium)
view.addSubview(labelView)
return view
}
public func popupRow(_ view: NSView? = nil, title: String, value: String, multiline: Bool = false) -> (LabelField, ValueField, NSView) {
let lines: CGFloat = CGFloat(multiline ? value.filter { $0 == "\n" }.count + 1 : 1)
let width = view?.frame.width ?? 0
let height = multiline ? ((lines*16) + (22-16)): 22
let rowView: NSView = NSView(frame: NSRect(x: 0, y: 0, width: width, height: height))
let labelWidth = title.widthOfString(usingFont: .systemFont(ofSize: 12, weight: .regular)) + 4
let labelView: LabelField = LabelField(frame: NSRect(x: 0, y: ((22-16)/2) + ((lines-1)*16), width: labelWidth, height: 16), title)
let valueView: ValueField = ValueField(frame: NSRect(x: labelWidth, y: (22-16)/2, width: rowView.frame.width - labelWidth, height: multiline ? 16*lines : 16), value)
if multiline {
valueView.cell?.usesSingleLineMode = false
}
rowView.addSubview(labelView)
rowView.addSubview(valueView)
if let view = view as? NSStackView {
rowView.heightAnchor.constraint(equalToConstant: rowView.bounds.height).isActive = true
view.addArrangedSubview(rowView)
} else if let view {
view.addSubview(rowView)
}
return (labelView, valueView, rowView)
}
public func portalRow(_ v: NSStackView, title: String, value: String = "", isSelectable: Bool = false) -> (LabelField, ValueField, NSStackView) {
let view: NSStackView = NSStackView()
view.orientation = .horizontal
view.distribution = .fillProportionally
view.spacing = 1
let labelView: LabelField = LabelField(title)
labelView.font = NSFont.systemFont(ofSize: 11, weight: .regular)
let valueView: ValueField = ValueField(value)
valueView.isSelectable = isSelectable
valueView.font = NSFont.systemFont(ofSize: 12, weight: .regular)
view.addArrangedSubview(labelView)
view.addArrangedSubview(NSView())
view.addArrangedSubview(valueView)
v.addArrangedSubview(view)
view.widthAnchor.constraint(equalTo: v.widthAnchor).isActive = true
return (labelView, valueView, view)
}
public func popupWithColorRow(_ view: NSView, color: NSColor, title: String, value: String) -> (NSView, LabelField, ValueField) {
let rowView: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 22))
let colorView: NSView = NSView(frame: NSRect(x: 2, y: 5, width: 12, height: 12))
colorView.wantsLayer = true
colorView.layer?.backgroundColor = color.cgColor
colorView.layer?.cornerRadius = 2
let labelWidth = min(180, title.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .regular)) + 5)
let labelView: LabelField = LabelField(frame: NSRect(x: 18, y: (22-16)/2, width: labelWidth, height: 16), title)
let valueView: ValueField = ValueField(frame: NSRect(x: 18 + labelWidth, y: (22-16)/2, width: rowView.frame.width - labelWidth - 18, height: 16), value)
rowView.addSubview(colorView)
rowView.addSubview(labelView)
rowView.addSubview(valueView)
if let view = view as? NSStackView {
rowView.heightAnchor.constraint(equalToConstant: rowView.bounds.height).isActive = true
view.addArrangedSubview(rowView)
} else {
view.addSubview(rowView)
}
return (colorView, labelView, valueView)
}
public func portalWithColorRow(_ v: NSStackView, color: NSColor, title: String) -> (NSView, ValueField) {
let view: NSStackView = NSStackView()
view.orientation = .horizontal
view.distribution = .fillProportionally
view.spacing = 1
let colorView: NSView = NSView()
colorView.widthAnchor.constraint(equalToConstant: 5).isActive = true
colorView.wantsLayer = true
colorView.layer?.backgroundColor = color.cgColor
colorView.layer?.cornerRadius = 2
let labelView: LabelField = LabelField(title)
labelView.font = NSFont.systemFont(ofSize: 11, weight: .regular)
let valueView: ValueField = ValueField()
valueView.font = NSFont.systemFont(ofSize: 12, weight: .regular)
valueView.widthAnchor.constraint(equalToConstant: 40).isActive = true
view.addArrangedSubview(colorView)
view.addArrangedSubview(labelView)
view.addArrangedSubview(NSView())
view.addArrangedSubview(valueView)
v.addArrangedSubview(view)
view.widthAnchor.constraint(equalTo: v.widthAnchor).isActive = true
return (colorView, valueView)
}
public extension Array where Element: Hashable {
func difference(from other: [Element]) -> [Element] {
let thisSet = Set(self)
let otherSet = Set(other)
return Array(thisSet.symmetricDifference(otherSet))
}
}
public func toggleNSControlState(_ control: NSControl?, state: NSControl.StateValue) {
if #available(OSX 10.15, *) {
if let checkbox = control as? NSSwitch {
checkbox.state = state
}
} else {
if let checkbox = control as? NSButton {
checkbox.state = state
}
}
}
public func asyncShell(_ args: String) {
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", args]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
}
public func syncShell(_ args: String) -> String {
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", args]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
task.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output
}
public func isNewestVersion(currentVersion: String, latestVersion: String) -> Bool {
let currentNumber = currentVersion.replacingOccurrences(of: "v", with: "")
let latestNumber = latestVersion.replacingOccurrences(of: "v", with: "")
let currentArray = currentNumber.condenseWhitespace().split(separator: ".")
let latestArray = latestNumber.condenseWhitespace().split(separator: ".")
var current = Version(major: Int(currentArray[0]) ?? 0, minor: Int(currentArray[1]) ?? 0, patch: Int(currentArray[2]) ?? 0)
var latest = Version(major: Int(latestArray[0]) ?? 0, minor: Int(latestArray[1]) ?? 0, patch: Int(latestArray[2]) ?? 0)
if let patch = currentArray.last, patch.contains("-") {
let arr = patch.split(separator: "-")
if let patchNumber = arr.first {
current.patch = Int(patchNumber) ?? 0
}
if let beta = arr.last {
current.beta = Int(beta.replacingOccurrences(of: "beta", with: "")) ?? 0
}
}
if let patch = latestArray.last, patch.contains("-") {
let arr = patch.split(separator: "-")
if let patchNumber = arr.first {
latest.patch = Int(patchNumber) ?? 0
}
if let beta = arr.last {
latest.beta = Int(beta.replacingOccurrences(of: "beta", with: "")) ?? 0
}
}
// current is not beta + latest is not beta
if current.beta == nil && latest.beta == nil {
if latest.major > current.major {
return true
}
if latest.minor > current.minor && latest.major >= current.major {
return true
}
if latest.patch > current.patch && latest.minor >= current.minor && latest.major >= current.major {
return true
}
}
// current version is beta + last version is not beta
if current.beta != nil && latest.beta == nil {
if latest.major > current.major {
return true
}
if latest.minor > current.minor && latest.major >= current.major {
return true
}
if latest.patch >= current.patch && latest.minor >= current.minor && latest.major >= current.major {
return true
}
}
// current version is beta + last version is beta
if current.beta != nil && latest.beta != nil {
if latest.major > current.major {
return true
}
if latest.minor > current.minor && latest.major >= current.major {
return true
}
if latest.patch >= current.patch && latest.minor >= current.minor && latest.major >= current.major {
return true
}
if latest.beta! > current.beta! && latest.patch >= current.patch && latest.minor >= current.minor && latest.major >= current.major {
return true
}
}
return false
}
public func showNotification(title: String, subtitle: String? = nil, userInfo: [AnyHashable: Any] = [:], delegate: UNUserNotificationCenterDelegate? = nil) -> String {
let id = UUID().uuidString
let content = UNMutableNotificationContent()
content.title = title
if let value = subtitle {
content.subtitle = value
}
content.userInfo = userInfo
content.sound = UNNotificationSound.default
let request = UNNotificationRequest(identifier: id, content: content, trigger: nil)
let center = UNUserNotificationCenter.current()
center.delegate = delegate
center.requestAuthorization(options: [.alert, .sound]) { _, _ in }
center.add(request) { (error: Error?) in
if let err = error {
print(err)
}
}
return id
}
public func removeNotification(_ id: String) {
let center = UNUserNotificationCenter.current()
center.removeDeliveredNotifications(withIdentifiers: [id])
}
public struct TopProcess: Codable, Process_p {
public var pid: Int
public var name: String
public var usage: Double
public var icon: NSImage {
get {
if let app = NSRunningApplication(processIdentifier: pid_t(self.pid)), let icon = app.icon {
return icon
}
return Constants.defaultProcessIcon
}
}
public init(pid: Int, name: String, usage: Double) {
self.pid = pid
self.name = name
self.usage = usage
}
}
public func fetchIOService(_ name: String) -> [NSDictionary]? {
var iterator: io_iterator_t = io_iterator_t()
var obj: io_registry_entry_t = 1
var list: [NSDictionary] = []
let result = IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching(name), &iterator)
if result != kIOReturnSuccess {
print("Error IOServiceGetMatchingServices(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
}
while obj != 0 {
obj = IOIteratorNext(iterator)
if let props = getIOProperties(obj) {
list.append(props)
}
IOObjectRelease(obj)
}
IOObjectRelease(iterator)
return list.isEmpty ? nil : list
}
public func getIOProperties(_ entry: io_registry_entry_t) -> NSDictionary? {
var properties: Unmanaged? = nil
if IORegistryEntryCreateCFProperties(entry, &properties, kCFAllocatorDefault, 0) != kIOReturnSuccess {
return nil
}
defer {
properties?.release()
}
return properties?.takeUnretainedValue()
}
internal func getIOName(_ entry: io_registry_entry_t) -> String? {
let pointer = UnsafeMutablePointer.allocate(capacity: 1)
defer { pointer.deallocate() }
let result = IORegistryEntryGetName(entry, pointer)
if result != kIOReturnSuccess {
print("Error IORegistryEntryGetName(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
}
return String(cString: UnsafeRawPointer(pointer).assumingMemoryBound(to: CChar.self))
}
internal func convertCFDataToArr(_ data: CFData, _ isM4: Bool = false) -> [Int32] {
let length = CFDataGetLength(data)
var bytes = [UInt8](repeating: 0, count: length)
CFDataGetBytes(data, CFRange(location: 0, length: length), &bytes)
var multiplier: UInt32 = 1000 * 1000
if isM4 {
multiplier = 1000
}
var arr: [Int32] = []
var chunks = stride(from: 0, to: bytes.count, by: 8).map { Array(bytes[$0.. String {
var string = NSLocalizedString(key, comment: comment)
if !params.isEmpty {
for (index, param) in params.enumerated() {
string = string.replacingOccurrences(of: "%\(index)", with: param)
}
}
return string
}
public extension UnitTemperature {
static var system: UnitTemperature {
let measureFormatter = MeasurementFormatter()
let measurement = Measurement(value: 0, unit: UnitTemperature.celsius)
return measureFormatter.string(from: measurement).hasSuffix("C") ? .celsius : .fahrenheit
}
static var current: UnitTemperature {
let stringUnit: String = Store.shared.string(key: "temperature_units", defaultValue: "system")
var unit = UnitTemperature.system
if stringUnit != "system" {
if let value = TemperatureUnits.first(where: { $0.key == stringUnit }), let temperatureUnit = value.additional as? UnitTemperature {
unit = temperatureUnit
}
}
return unit
}
}
public func temperature(_ value: Double, defaultUnit: UnitTemperature = UnitTemperature.celsius, fractionDigits: Int = 0) -> String {
let formatter = MeasurementFormatter()
formatter.locale = Locale.init(identifier: "en_US")
formatter.numberFormatter.maximumFractionDigits = fractionDigits
if fractionDigits != 0 {
formatter.numberFormatter.minimumFractionDigits = fractionDigits
}
formatter.unitOptions = .providedUnit
var measurement = Measurement(value: value, unit: defaultUnit)
measurement.convert(to: UnitTemperature.current)
return formatter.string(from: measurement)
}
public func sysctlByName(_ name: String) -> Int64 {
var num: Int64 = 0
var size = MemoryLayout.size
if sysctlbyname(name, &num, &size, nil, 0) != 0 {
print(POSIXError.Code(rawValue: errno).map { POSIXError($0) } ?? CocoaError(.fileReadUnknown))
}
return num
}
internal class WidgetLabelView: NSView {
private var title: String
internal init(_ title: String, height: CGFloat) {
self.title = title
super.init(frame: NSRect(
x: 0,
y: 0,
width: 6,
height: height
))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let style = NSMutableParagraphStyle()
style.alignment = .center
let stringAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 7, weight: .regular),
NSAttributedString.Key.foregroundColor: NSColor.textColor,
NSAttributedString.Key.paragraphStyle: style
]
let title = String(self.title.prefix(3)).uppercased().reversed()
let letterHeight = self.frame.height / 3
let letterWidth: CGFloat = self.frame.height / CGFloat(title.count)
var yMargin: CGFloat = 0
for char in title {
let rect = CGRect(x: 0, y: yMargin, width: letterWidth, height: letterHeight-1)
let str = NSAttributedString.init(string: "\(char)", attributes: stringAttributes)
str.draw(with: rect)
yMargin += letterHeight
}
}
}
public func process(path: String, arguments: [String]) -> String? {
let task = Process()
task.launchPath = path
task.arguments = arguments
let outputPipe = Pipe()
defer {
outputPipe.fileHandleForReading.closeFile()
}
task.standardOutput = outputPipe
do {
try task.run()
} catch let error {
debug("system_profiler SPMemoryDataType: \(error.localizedDescription)")
return nil
}
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: outputData, encoding: .utf8)
guard let output, !output.isEmpty else { return nil }
return output
}
public class SettingsContainerView: NSStackView {
public init() {
super.init(frame: NSRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.orientation = .vertical
self.spacing = Constants.Settings.margin
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
public class SMCHelper {
public static let shared = SMCHelper()
public var isInstalled: Bool {
syncShell("ls /Library/PrivilegedHelperTools/").contains("eu.exelban.Stats.SMC.Helper")
}
private var connection: NSXPCConnection? = nil
public func setFanSpeed(_ id: Int, speed: Int) {
guard let helper = self.helper(nil) else { return }
helper.setFanSpeed(id: id, value: speed) { result in
if let result, !result.isEmpty {
NSLog("set fan speed: \(result)")
}
}
}
public func setFanMode(_ id: Int, mode: Int) {
guard let helper = self.helper(nil) else { return }
helper.setFanMode(id: id, mode: mode) { result in
if let result, !result.isEmpty {
NSLog("set fan mode: \(result)")
}
}
}
public func resetFanControl() {
guard let helper = self.helper(nil) else { return }
helper.resetFanControl { _ in }
}
public func isActive() -> Bool {
return self.connection != nil
}
public func checkForUpdate() {
let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LaunchServices/eu.exelban.Stats.SMC.Helper")
guard let helperBundleInfo = CFBundleCopyInfoDictionaryForURL(helperURL as CFURL) as? [String: Any],
let helperVersion = helperBundleInfo["CFBundleShortVersionString"] as? String,
let helper = self.helper(nil) else { return }
helper.version { installedHelperVersion in
guard installedHelperVersion != helperVersion else { return }
print("new version of SMC helper is detected, going to update...")
self.uninstall(silent: true)
self.install { installed in
if installed {
print("the new version of SMC helper was successfully installed")
} else {
print("error when installing a new version of the SMC helper")
}
}
}
}
public func install(completion: @escaping (_ installed: Bool) -> Void) {
var authRef: AuthorizationRef?
var authStatus = AuthorizationCreate(nil, nil, [.preAuthorize], &authRef)
guard authStatus == errAuthorizationSuccess else {
print("Unable to get a valid empty authorization reference to load Helper daemon")
completion(false)
return
}
let authItem = kSMRightBlessPrivilegedHelper.withCString { authorizationString in
AuthorizationItem(name: authorizationString, valueLength: 0, value: nil, flags: 0)
}
let pointer = UnsafeMutablePointer.allocate(capacity: 1)
pointer.initialize(to: authItem)
defer {
pointer.deinitialize(count: 1)
pointer.deallocate()
}
var authRights = AuthorizationRights(count: 1, items: pointer)
let flags: AuthorizationFlags = [.interactionAllowed, .extendRights, .preAuthorize]
authStatus = AuthorizationCreate(&authRights, nil, flags, &authRef)
guard authStatus == errAuthorizationSuccess else {
print("Unable to get a valid loading authorization reference to load Helper daemon")
completion(false)
return
}
var error: Unmanaged?
if SMJobBless(kSMDomainUserLaunchd, "eu.exelban.Stats.SMC.Helper" as CFString, authRef, &error) == false {
let blessError = error!.takeRetainedValue() as Error
print("Error while installing the Helper: \(blessError.localizedDescription)")
completion(false)
return
}
AuthorizationFree(authRef!, [])
completion(true)
}
private func helperConnection() -> NSXPCConnection? {
guard self.connection == nil else {
return self.connection
}
let connection = NSXPCConnection(machServiceName: "eu.exelban.Stats.SMC.Helper", options: .privileged)
connection.exportedObject = self
connection.remoteObjectInterface = NSXPCInterface(with: HelperProtocol.self)
connection.invalidationHandler = {
self.connection?.invalidationHandler = nil
OperationQueue.main.addOperation {
self.connection = nil
}
}
self.connection = connection
self.connection?.resume()
return self.connection
}
private func helper(_ completion: ((Bool) -> Void)?) -> HelperProtocol? {
guard let helper = self.helperConnection() else {
completion?(false)
return nil
}
guard let service = helper.remoteObjectProxyWithErrorHandler({ error in
print(error)
}) as? HelperProtocol else {
completion?(false)
return nil
}
service.setSMCPath(Bundle.main.path(forResource: "smc", ofType: nil)!)
return service
}
public func uninstall(silent: Bool = false) {
if let count = SMC.shared.getValue("FNum") {
for i in 0.. NSImage? {
guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
return nil
}
let bitmap = NSBitmapImageRep(cgImage: cgImage)
guard let grayscale = bitmap.converting(to: .genericGray, renderingIntent: .default) else {
return nil
}
let greyImage = NSImage(size: image.size)
greyImage.addRepresentation(grayscale)
return greyImage
}
public class ViewCopy: CALayer {
public init(_ view: NSView) {
super.init()
guard let bitmap = view.bitmapImageRepForCachingDisplay(in: view.bounds) else { return }
view.cacheDisplay(in: view.bounds, to: bitmap)
frame = view.frame
contents = bitmap.cgImage
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
public class EmptyView: NSStackView {
public init(height: CGFloat = 120, isHidden: Bool = false, msg: String) {
super.init(frame: NSRect())
if height != 0 {
self.heightAnchor.constraint(equalToConstant: height).isActive = true
}
self.translatesAutoresizingMaskIntoConstraints = true
self.orientation = .vertical
self.distribution = .fillEqually
self.isHidden = isHidden
self.identifier = NSUserInterfaceItemIdentifier(rawValue: "emptyView")
let textView: NSTextView = NSTextView()
if height != 0 {
textView.heightAnchor.constraint(equalToConstant: ((height)/2)+6).isActive = true
}
textView.alignment = .center
textView.isEditable = false
textView.isSelectable = false
textView.drawsBackground = false
textView.string = msg
self.addArrangedSubview(NSView())
self.addArrangedSubview(textView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
internal func saveNSStatusItemPosition(id: String) {
let position = Store.shared.int(key: "NSStatusItem Preferred Position \(id)", defaultValue: -1)
if position != -1 {
Store.shared.set(key: "NSStatusItem Restore Position \(id)", value: position)
}
}
internal func restoreNSStatusItemPosition(id: String) {
let prevPosition = Store.shared.int(key: "NSStatusItem Restore Position \(id)", defaultValue: -1)
if prevPosition != -1 {
Store.shared.set(key: "NSStatusItem Preferred Position \(id)", value: prevPosition)
Store.shared.remove("NSStatusItem Restore Position \(id)")
}
}
public class AppIcon: NSView {
public static let size: CGSize = CGSize(width: 16, height: 16)
public init() {
super.init(frame: NSRect(x: 0, y: 3, width: AppIcon.size.width, height: AppIcon.size.height))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
ctx.setShouldAntialias(true)
NSColor.textColor.set()
NSBezierPath(roundedRect: NSRect(
x: 0,
y: 0,
width: AppIcon.size.width,
height: AppIcon.size.height
), xRadius: 4, yRadius: 4).fill()
NSColor.controlTextColor.set()
NSBezierPath(roundedRect: NSRect(
x: 1.5,
y: 1.5,
width: AppIcon.size.width - 3,
height: AppIcon.size.height - 3
), xRadius: 3, yRadius: 3).fill()
let lineWidth = 1 / (NSScreen.main?.backingScaleFactor ?? 1) / 2
let offset = lineWidth/2
let zero = (AppIcon.size.height - 3 + 1.5)/2 + lineWidth
let x = 1.5
let downloadLine = drawLine(points: [
(x+0, zero-offset),
(x+1, zero-offset),
(x+2, zero-offset-2.5),
(x+3, zero-offset-4),
(x+4, zero-offset),
(x+5, zero-offset-2),
(x+6, zero-offset),
(x+7, zero-offset),
(x+8, zero-offset-2),
(x+9, zero-offset),
(x+10, zero-offset-4),
(x+11, zero-offset-0.5),
(x+12, zero-offset)
], color: NSColor.systemBlue, lineWidth: lineWidth)
let uploadLine = drawLine(points: [
(x+0, zero+offset),
(x+1, zero+offset),
(x+2, zero+offset+2),
(x+3, zero+offset),
(x+4, zero+offset),
(x+5, zero+offset),
(x+6, zero+offset+3),
(x+7, zero+offset+3),
(x+8, zero+offset),
(x+9, zero+offset+1),
(x+10, zero+offset+5),
(x+11, zero+offset),
(x+12, zero+offset)
], color: NSColor.systemRed, lineWidth: lineWidth)
ctx.saveGState()
drawUnderLine(dirtyRect, path: downloadLine, color: NSColor.systemBlue, x: x, y: zero-offset)
ctx.restoreGState()
ctx.saveGState()
drawUnderLine(dirtyRect, path: uploadLine, color: NSColor.systemRed, x: x, y: zero+offset)
ctx.restoreGState()
}
private func drawLine(points: [(CGFloat, CGFloat)], color: NSColor, lineWidth: CGFloat) -> NSBezierPath {
let linePath = NSBezierPath()
linePath.move(to: CGPoint(x: points[0].0, y: points[0].1))
for i in 1.. Bool {
var state: NSControl.StateValue
if #available(OSX 10.15, *) {
state = sender is NSSwitch ? (sender as! NSSwitch).state : .off
} else {
state = sender is NSButton ? (sender as! NSButton).state : .off
}
return state == .on
}
public func iconFromSymbol(name: String, scale: NSImage.SymbolScale) -> NSImage {
let config = NSImage.SymbolConfiguration(textStyle: .body, scale: scale)
if let symbol = NSImage(systemSymbolName: name, accessibilityDescription: nil), let icon = symbol.withSymbolConfiguration(config) {
return icon
}
return NSImage()
}
public func showAlert(_ message: String, _ information: String? = nil, _ style: NSAlert.Style = .informational) {
let alert = NSAlert()
alert.messageText = message
if let information = information {
alert.informativeText = information
}
alert.addButton(withTitle: "OK")
alert.alertStyle = style
alert.runModal()
}
var isDarkMode: Bool {
switch NSAppearance.current.name {
case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark:
return true
default:
return false
}
}
public class PreferencesSection: NSStackView {
private let container: NSStackView = NSStackView()
public init(label: String = "", id: String? = nil, _ components: [NSView] = []) {
super.init(frame: .zero)
self.orientation = .vertical
self.spacing = 0
if let id {
self.identifier = NSUserInterfaceItemIdentifier(id)
}
if label != "" {
self.addLabel(label)
}
self.container.orientation = .vertical
self.container.wantsLayer = true
self.container.layer?.backgroundColor = NSColor.quaternaryLabelColor.withAlphaComponent(0.025).cgColor
self.container.layer?.cornerRadius = Constants.Settings.margin
self.container.edgeInsets = NSEdgeInsets(
top: Constants.Settings.margin/1.25,
left: Constants.Settings.margin,
bottom: Constants.Settings.margin/1.25,
right: Constants.Settings.margin
)
self.container.spacing = Constants.Settings.margin/1.25
self.addArrangedSubview(self.container)
for item in components {
self.add(item)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func updateLayer() {
self.container.layer?.backgroundColor = NSColor.quaternaryLabelColor.withAlphaComponent(0.025).cgColor
}
private func addLabel(_ value: String) {
let view = NSStackView()
view.heightAnchor.constraint(equalToConstant: 26).isActive = true
let space = NSView()
space.widthAnchor.constraint(equalToConstant: 4).isActive = true
let field: NSTextField = TextView()
field.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
field.stringValue = value
view.addArrangedSubview(space)
view.addArrangedSubview(field)
view.addArrangedSubview(NSView())
self.addArrangedSubview(view)
}
public func add(_ view: NSView) {
if !self.container.subviews.isEmpty {
self.container.addArrangedSubview(PreferencesSeparator())
}
self.container.addArrangedSubview(view)
}
public func delete(_ id: String) {
let views = self.container.subviews
views.enumerated().forEach { (i, v) in
guard v.identifier?.rawValue == id else { return }
if self.container.subviews.indices.contains(i-1) {
let prev = self.container.subviews[i-1]
if prev.identifier?.rawValue == "PreferencesSeparator" {
prev.removeFromSuperview()
}
}
v.removeFromSuperview()
}
}
public func contains(_ id: String) -> Bool {
self.container.subviews.contains(where: { $0.identifier?.rawValue == id })
}
public func setRowVisibility(_ at: Int, newState: Bool) {
if at == 0 {
self.container.subviews[0].isHidden = !newState
if self.container.subviews.count > 1 {
self.container.subviews[1].isHidden = !newState
}
return
}
for i in self.container.subviews.indices where i/2 == at && Double(i).remainder(dividingBy: 2) == 0 {
self.container.subviews[i-1].isHidden = !newState
self.container.subviews[i].isHidden = !newState
}
}
public func setRowVisibility(_ id: String, newState: Bool) {
guard let at = self.container.subviews.firstIndex(where: { $0.identifier?.rawValue == id }) else { return }
self.setRowVisibility(at/2, newState: newState)
}
public func setRowVisibility(_ row: PreferencesRow, newState: Bool) {
guard let at = self.container.subviews.firstIndex(where: { $0 == row }) else { return }
self.setRowVisibility(at/2, newState: newState)
}
public func findRow(_ id: String) -> PreferencesRow? {
let rows: [PreferencesRow] = self.container.subviews.filter({ $0 is PreferencesRow }).compactMap({ $0 as? PreferencesRow })
return rows.first(where: { $0.identifier?.rawValue == id })
}
}
private class PreferencesSeparator: NSView {
public init() {
super.init(frame: .zero)
self.wantsLayer = true
self.layer?.backgroundColor = NSColor.separatorColor.withAlphaComponent(0.05).cgColor
self.heightAnchor.constraint(equalToConstant: 1).isActive = true
self.identifier = NSUserInterfaceItemIdentifier("PreferencesSeparator")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func updateLayer() {
self.layer?.backgroundColor = NSColor.separatorColor.withAlphaComponent(0.05).cgColor
}
}
public class PreferencesRow: NSStackView {
private var helpCallback: (() -> Void)?
public init(_ title: String? = nil, _ description: String? = nil, id: String? = nil, component: NSView, help: (() -> Void)? = nil) {
self.helpCallback = help
super.init(frame: .zero)
self.orientation = .horizontal
self.distribution = .fill
self.alignment = .centerY
self.edgeInsets = NSEdgeInsets(top: Constants.Settings.margin/2, left: 0, bottom: (Constants.Settings.margin/2) - 1, right: 0)
self.spacing = 0
if let id {
self.identifier = NSUserInterfaceItemIdentifier(id)
}
self.addArrangedSubview(self.text(title, description))
if help != nil {
let helpBtn = NSButton()
helpBtn.bezelStyle = .helpButton
helpBtn.controlSize = .small
helpBtn.title = ""
helpBtn.action = #selector(self.help)
helpBtn.target = self
let space = NSView()
space.widthAnchor.constraint(equalToConstant: 5).isActive = true
self.addArrangedSubview(space)
self.addArrangedSubview(helpBtn)
}
self.addArrangedSubview(NSView())
self.addArrangedSubview(component)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func replaceComponent(with view: NSView) {
self.subviews.removeLast()
self.addArrangedSubview(view)
}
fileprivate func text(_ title: String? = nil, _ description: String? = nil) -> NSView {
let view: NSStackView = NSStackView()
view.orientation = .vertical
view.spacing = 0
if let title {
let field: NSTextField = TextView()
field.font = NSFont.systemFont(ofSize: 12, weight: .regular)
field.stringValue = title
view.addArrangedSubview(field)
}
if let description {
let field: NSTextField = TextView()
field.font = NSFont.systemFont(ofSize: 12, weight: .regular)
field.textColor = .secondaryLabelColor
field.stringValue = description
view.addArrangedSubview(field)
view.addArrangedSubview(NSView())
view.alignment = .leading
}
return view
}
@objc private func help() {
self.helpCallback?()
}
}
public func restartApp(_ sender: Any, afterDelay seconds: TimeInterval = 0.5) -> Never {
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", "sleep \(seconds); open \"\(Bundle.main.bundlePath)\""]
task.launch()
NSApp.terminate(sender)
exit(0)
}
public class StepperInput: NSStackView, NSTextFieldDelegate, PreferencesSwitchWith_p {
private var callback: ((Int) -> Void)
private var unitCallback: ((KeyValue_p) -> Void)
private let valueView: NSTextField = NSTextField()
private let stepperView: NSStepper = NSStepper()
private var symbolView: NSTextField? = nil
private var unitsView: NSPopUpButton? = nil
private let range: NSRange?
private var units: [KeyValue_p]? = nil
private var _isEnabled: Bool = true
public var isEnabled: Bool {
get { self._isEnabled }
set {
self.valueView.isEnabled = newValue
self.stepperView.isEnabled = newValue
self.symbolView?.isEnabled = newValue
self.unitsView?.isEnabled = newValue
self._isEnabled = newValue
}
}
public init(
_ value: Int,
range: NSRange = NSRange(location: 1, length: 99),
unit: String = "%",
visibileUnit: Bool = true,
units: [KeyValue_p]? = nil,
callback: @escaping (Int) -> Void = {_ in },
unitCallback: @escaping (KeyValue_p) -> Void = {_ in }
) {
self.range = range
self.callback = callback
self.unitCallback = unitCallback
super.init(frame: .zero)
self.orientation = .horizontal
self.spacing = 2
self.valueView.font = NSFont.systemFont(ofSize: 12, weight: .regular)
self.valueView.textColor = .textColor
self.valueView.isEditable = true
self.valueView.isSelectable = true
self.valueView.usesSingleLineMode = true
self.valueView.maximumNumberOfLines = 1
self.valueView.focusRingType = .none
self.valueView.delegate = self
self.valueView.stringValue = "\(value)"
self.valueView.translatesAutoresizingMaskIntoConstraints = false
self.stepperView.font = NSFont.systemFont(ofSize: 12, weight: .regular)
self.stepperView.doubleValue = Double(value)/100
self.stepperView.minValue = Double(range.lowerBound)/100
self.stepperView.maxValue = Double(range.upperBound)/100
self.stepperView.increment = 0.01
self.stepperView.valueWraps = false
self.stepperView.target = self
self.stepperView.action = #selector(self.onStepperChange)
self.addArrangedSubview(self.valueView)
self.addArrangedSubview(self.stepperView)
if units == nil {
if unit == "%" {
self.widthAnchor.constraint(equalToConstant: 68).isActive = true
}
if visibileUnit {
let symbol: NSTextField = LabelField(unit)
symbol.textColor = .textColor
self.addArrangedSubview(symbol)
self.symbolView = symbol
}
} else if let units {
self.units = units
self.unitsView = selectView(
action: #selector(self.onUnitChange),
items: units,
selected: unit
)
self.addArrangedSubview(self.unitsView!)
self.widthAnchor.constraint(equalToConstant: 124).isActive = true
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func controlTextDidChange(_ obj: Notification) {
guard let field = obj.object as? NSTextField else { return }
let filtered = field.stringValue.filter{"0123456789".contains($0)}
if filtered != field.stringValue {
field.stringValue = filtered
}
guard var v = Int(field.stringValue) else { return }
if let range = self.range {
if v > range.upperBound {
field.stringValue = "\(range.upperBound)"
v = range.upperBound
} else if v < range.lowerBound {
field.stringValue = "\(range.lowerBound)"
v = range.lowerBound
}
}
self.callback(v)
}
@objc private func onStepperChange(_ sender: NSStepper) {
let value = Int(sender.doubleValue*100)
self.valueView.stringValue = "\(value)"
self.callback(value)
}
@objc private func onUnitChange(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let units = self.units,
let value = units.first(where: { $0.key == key }) else { return }
self.unitCallback(value)
}
}
public protocol PreferencesSwitchWith_p: NSView {
var isEnabled: Bool { get set }
}
extension NSPopUpButton: PreferencesSwitchWith_p {}
extension NSTextField: PreferencesSwitchWith_p {}
public class PreferencesSwitch: NSStackView {
private let action: (_ sender: NSControl) -> Void
private let with: PreferencesSwitchWith_p
public init(action: @escaping (_ sender: NSControl) -> Void, state: Bool, with: PreferencesSwitchWith_p) {
self.action = action
self.with = with
super.init(frame: .zero)
self.orientation = .horizontal
self.alignment = .centerY
self.spacing = Constants.Settings.margin
let btn = switchView(action: #selector(self.callback), state: state)
with.isEnabled = state
self.addArrangedSubview(NSView())
self.addArrangedSubview(btn)
self.addArrangedSubview(with)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func callback(_ sender: NSControl) {
self.action(sender)
self.with.isEnabled = controlState(sender)
}
}
public class HelpHUD: NSPanel {
private let text: String
public init(_ text: String, origin: CGPoint = CGPoint(x: 0, y: 0), size: CGSize = CGSize(width: 420, height: 300)) {
self.text = text
super.init(
contentRect: NSRect(origin: origin, size: size),
styleMask: [.hudWindow, .utilityWindow, .titled, .closable],
backing: .buffered, defer: false
)
self.isFloatingPanel = true
self.isMovableByWindowBackground = true
self.level = .floating
self.title = "Help"
}
public func show() {
if self.contentView as? WKWebView == nil {
let webView = WKWebView()
webView.setValue(false, forKey: "drawsBackground")
webView.loadHTMLString("\(self.text)", baseURL: nil)
self.contentView = webView
}
self.makeKeyAndOrderFront(self)
self.center()
}
}
public class VerticallyCenteredTextFieldCell: NSTextFieldCell {
public override func titleRect(forBounds rect: NSRect) -> NSRect {
var titleRect = super.titleRect(forBounds: rect)
let textSize = self.attributedStringValue.size()
let verticalOffset = (rect.size.height - textSize.height) / 2.0
titleRect.origin.y += verticalOffset
return titleRect
}
public override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
let titleRect = self.titleRect(forBounds: cellFrame)
self.attributedStringValue.draw(in: titleRect)
}
}
public class CPUeStressTest {
public var isRunning: Bool = false
private var workers: [DispatchWorkItem] = []
private let queue = DispatchQueue.global(qos: .background)
public init() {}
public func start() {
guard !self.isRunning else { return }
self.isRunning = true
let efficientCoreCount: Int = Int(SystemKit.shared.device.info.cpu?.eCores ?? 2)
self.workers.removeAll()
for index in 0.. 100000 { x = 1.0 }
OSMemoryBarrier()
}
}
}
public class CPUpStressTest {
public var isRunning = false
private var workers: [DispatchWorkItem] = []
private let queue = DispatchQueue.global(qos: .userInteractive)
public init() {}
public func start() {
guard !self.isRunning else { return }
self.isRunning = true
let performanceCoreCount: Int = Int(SystemKit.shared.device.info.cpu?.pCores ?? 4)
self.workers.removeAll()
for index in 0.. 100000 { x = 1.0 }
OSMemoryBarrier()
}
}
}
public class GPUStressTest {
public var isRunning = false
private let device: MTLDevice
private let commandQueue: MTLCommandQueue
private let pipeline: MTLComputePipelineState
private let dataSize = 50_000_000 // Large data size for GPU workload
private var bufferA: MTLBuffer?
private var bufferB: MTLBuffer?
private var bufferC: MTLBuffer?
public init?() {
guard let device = MTLCreateSystemDefaultDevice(), let queue = device.makeCommandQueue() else {
return nil
}
self.device = device
self.commandQueue = queue
let source = """
#include
using namespace metal;
kernel void full_load_kernel(const device float* inA [[buffer(0)]],
const device float* inB [[buffer(1)]],
device float* outC [[buffer(2)]],
uint id [[thread_position_in_grid]]) {
outC[id] = (inA[id] * inB[id]) + sin(inA[id]) + cos(inB[id]) + tan(inA[id]) + log(inB[id]);
}
"""
do {
let library = try device.makeLibrary(source: source, options: nil)
let function = library.makeFunction(name: "full_load_kernel")!
self.pipeline = try device.makeComputePipelineState(function: function)
} catch {
return nil
}
}
private func allocateMemory() {
guard self.bufferA == nil, self.bufferB == nil, self.bufferC == nil else { return }
self.bufferA = self.device.makeBuffer(length: self.dataSize * MemoryLayout.size, options: .storageModeShared)
self.bufferB = self.device.makeBuffer(length: self.dataSize * MemoryLayout.size, options: .storageModeShared)
self.bufferC = self.device.makeBuffer(length: self.dataSize * MemoryLayout.size, options: .storageModeShared)
let dataA = [Float](repeating: 1.0, count: self.dataSize)
let dataB = [Float](repeating: 2.0, count: self.dataSize)
memcpy(self.bufferA?.contents(), dataA, dataA.count * MemoryLayout.size)
memcpy(self.bufferB?.contents(), dataB, dataB.count * MemoryLayout.size)
}
private func freeMemory() {
self.bufferA = nil
self.bufferB = nil
self.bufferC = nil
}
public func start() {
guard !self.isRunning else { return }
self.isRunning = true
self.allocateMemory()
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.test()
}
}
public func stop() {
self.isRunning = false
self.freeMemory()
}
private func test() {
let threadGroupSize = MTLSize(width: 256, height: 1, depth: 1)
let gridSize = MTLSize(width: self.dataSize, height: 1, depth: 1)
while self.isRunning {
guard let commandBuffer = self.commandQueue.makeCommandBuffer(),
let commandEncoder = commandBuffer.makeComputeCommandEncoder(),
let bufferA = self.bufferA, let bufferB = self.bufferB, let bufferC = self.bufferC else {
break
}
commandEncoder.setComputePipelineState(self.pipeline)
commandEncoder.setBuffer(bufferA, offset: 0, index: 0)
commandEncoder.setBuffer(bufferB, offset: 0, index: 1)
commandEncoder.setBuffer(bufferC, offset: 0, index: 2)
commandEncoder.dispatchThreads(gridSize, threadsPerThreadgroup: threadGroupSize)
commandEncoder.endEncoding()
commandBuffer.commit()
commandBuffer.waitUntilCompleted()
}
}
}
public func isWidgetActive(_ defaults: UserDefaults?, _ widgets: [String]) -> Bool {
for name in widgets {
guard let lastUpdate = defaults?.double(forKey: name) else { return false }
let timeSinceUpdate = Date().timeIntervalSince1970 - lastUpdate
if timeSinceUpdate < 60 {
return true
}
}
return false
}
public func countryFlag(_ code: String) -> String? {
let uppercased = code.uppercased()
guard uppercased.count == 2 else { return nil }
let scalars = uppercased.unicodeScalars.compactMap { UnicodeScalar(127397 + $0.value) }
return scalars.count == 2 ? String(String.UnicodeScalarView(scalars)) : nil
}
================================================
FILE: Kit/lldb/LICENSE.txt
================================================
Copyright (c) 2011 The LevelDB Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: Kit/lldb/include/c.h
================================================
/* Copyright (c) 2011 The LevelDB Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. See the AUTHORS file for names of contributors.
C bindings for leveldb. May be useful as a stable ABI that can be
used by programs that keep leveldb in a shared library, or for
a JNI api.
Does not support:
. getters for the option types
. custom comparators that implement key shortening
. custom iter, db, env, cache implementations using just the C bindings
Some conventions:
(1) We expose just opaque struct pointers and functions to clients.
This allows us to change internal representations without having to
recompile clients.
(2) For simplicity, there is no equivalent to the Slice type. Instead,
the caller has to pass the pointer and length as separate
arguments.
(3) Errors are represented by a null-terminated c string. NULL
means no error. All operations that can raise an error are passed
a "char** errptr" as the last argument. One of the following must
be true on entry:
*errptr == NULL
*errptr points to a malloc()ed null-terminated error message
(On Windows, *errptr must have been malloc()-ed by this library.)
On success, a leveldb routine leaves *errptr unchanged.
On failure, leveldb frees the old value of *errptr and
set *errptr to a malloc()ed error message.
(4) Bools have the type uint8_t (0 == false; rest == true)
(5) All of the pointer arguments must be non-NULL.
*/
#ifndef STORAGE_LEVELDB_INCLUDE_C_H_
#define STORAGE_LEVELDB_INCLUDE_C_H_
#include
#include
#include
#include "export.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Exported types */
typedef struct leveldb_t leveldb_t;
typedef struct leveldb_cache_t leveldb_cache_t;
typedef struct leveldb_comparator_t leveldb_comparator_t;
typedef struct leveldb_env_t leveldb_env_t;
typedef struct leveldb_filelock_t leveldb_filelock_t;
typedef struct leveldb_filterpolicy_t leveldb_filterpolicy_t;
typedef struct leveldb_iterator_t leveldb_iterator_t;
typedef struct leveldb_logger_t leveldb_logger_t;
typedef struct leveldb_options_t leveldb_options_t;
typedef struct leveldb_randomfile_t leveldb_randomfile_t;
typedef struct leveldb_readoptions_t leveldb_readoptions_t;
typedef struct leveldb_seqfile_t leveldb_seqfile_t;
typedef struct leveldb_snapshot_t leveldb_snapshot_t;
typedef struct leveldb_writablefile_t leveldb_writablefile_t;
typedef struct leveldb_writebatch_t leveldb_writebatch_t;
typedef struct leveldb_writeoptions_t leveldb_writeoptions_t;
/* DB operations */
LEVELDB_EXPORT leveldb_t* leveldb_open(const leveldb_options_t* options,
const char* name, char** errptr);
LEVELDB_EXPORT void leveldb_close(leveldb_t* db);
LEVELDB_EXPORT void leveldb_put(leveldb_t* db,
const leveldb_writeoptions_t* options,
const char* key, size_t keylen, const char* val,
size_t vallen, char** errptr);
LEVELDB_EXPORT void leveldb_delete(leveldb_t* db,
const leveldb_writeoptions_t* options,
const char* key, size_t keylen,
char** errptr);
LEVELDB_EXPORT void leveldb_write(leveldb_t* db,
const leveldb_writeoptions_t* options,
leveldb_writebatch_t* batch, char** errptr);
/* Returns NULL if not found. A malloc()ed array otherwise.
Stores the length of the array in *vallen. */
LEVELDB_EXPORT char* leveldb_get(leveldb_t* db,
const leveldb_readoptions_t* options,
const char* key, size_t keylen, size_t* vallen,
char** errptr);
LEVELDB_EXPORT leveldb_iterator_t* leveldb_create_iterator(
leveldb_t* db, const leveldb_readoptions_t* options);
LEVELDB_EXPORT const leveldb_snapshot_t* leveldb_create_snapshot(leveldb_t* db);
LEVELDB_EXPORT void leveldb_release_snapshot(
leveldb_t* db, const leveldb_snapshot_t* snapshot);
/* Returns NULL if property name is unknown.
Else returns a pointer to a malloc()-ed null-terminated value. */
LEVELDB_EXPORT char* leveldb_property_value(leveldb_t* db,
const char* propname);
LEVELDB_EXPORT void leveldb_approximate_sizes(
leveldb_t* db, int num_ranges, const char* const* range_start_key,
const size_t* range_start_key_len, const char* const* range_limit_key,
const size_t* range_limit_key_len, uint64_t* sizes);
LEVELDB_EXPORT void leveldb_compact_range(leveldb_t* db, const char* start_key,
size_t start_key_len,
const char* limit_key,
size_t limit_key_len);
/* Management operations */
LEVELDB_EXPORT void leveldb_destroy_db(const leveldb_options_t* options,
const char* name, char** errptr);
LEVELDB_EXPORT void leveldb_repair_db(const leveldb_options_t* options,
const char* name, char** errptr);
/* Iterator */
LEVELDB_EXPORT void leveldb_iter_destroy(leveldb_iterator_t*);
LEVELDB_EXPORT uint8_t leveldb_iter_valid(const leveldb_iterator_t*);
LEVELDB_EXPORT void leveldb_iter_seek_to_first(leveldb_iterator_t*);
LEVELDB_EXPORT void leveldb_iter_seek_to_last(leveldb_iterator_t*);
LEVELDB_EXPORT void leveldb_iter_seek(leveldb_iterator_t*, const char* k,
size_t klen);
LEVELDB_EXPORT void leveldb_iter_next(leveldb_iterator_t*);
LEVELDB_EXPORT void leveldb_iter_prev(leveldb_iterator_t*);
LEVELDB_EXPORT const char* leveldb_iter_key(const leveldb_iterator_t*,
size_t* klen);
LEVELDB_EXPORT const char* leveldb_iter_value(const leveldb_iterator_t*,
size_t* vlen);
LEVELDB_EXPORT void leveldb_iter_get_error(const leveldb_iterator_t*,
char** errptr);
/* Write batch */
LEVELDB_EXPORT leveldb_writebatch_t* leveldb_writebatch_create(void);
LEVELDB_EXPORT void leveldb_writebatch_destroy(leveldb_writebatch_t*);
LEVELDB_EXPORT void leveldb_writebatch_clear(leveldb_writebatch_t*);
LEVELDB_EXPORT void leveldb_writebatch_put(leveldb_writebatch_t*,
const char* key, size_t klen,
const char* val, size_t vlen);
LEVELDB_EXPORT void leveldb_writebatch_delete(leveldb_writebatch_t*,
const char* key, size_t klen);
LEVELDB_EXPORT void leveldb_writebatch_iterate(
const leveldb_writebatch_t*, void* state,
void (*put)(void*, const char* k, size_t klen, const char* v, size_t vlen),
void (*deleted)(void*, const char* k, size_t klen));
LEVELDB_EXPORT void leveldb_writebatch_append(
leveldb_writebatch_t* destination, const leveldb_writebatch_t* source);
/* Options */
LEVELDB_EXPORT leveldb_options_t* leveldb_options_create(void);
LEVELDB_EXPORT void leveldb_options_destroy(leveldb_options_t*);
LEVELDB_EXPORT void leveldb_options_set_comparator(leveldb_options_t*,
leveldb_comparator_t*);
LEVELDB_EXPORT void leveldb_options_set_filter_policy(leveldb_options_t*,
leveldb_filterpolicy_t*);
LEVELDB_EXPORT void leveldb_options_set_create_if_missing(leveldb_options_t*,
uint8_t);
LEVELDB_EXPORT void leveldb_options_set_error_if_exists(leveldb_options_t*,
uint8_t);
LEVELDB_EXPORT void leveldb_options_set_paranoid_checks(leveldb_options_t*,
uint8_t);
LEVELDB_EXPORT void leveldb_options_set_env(leveldb_options_t*, leveldb_env_t*);
LEVELDB_EXPORT void leveldb_options_set_info_log(leveldb_options_t*,
leveldb_logger_t*);
LEVELDB_EXPORT void leveldb_options_set_write_buffer_size(leveldb_options_t*,
size_t);
LEVELDB_EXPORT void leveldb_options_set_max_open_files(leveldb_options_t*, int);
LEVELDB_EXPORT void leveldb_options_set_cache(leveldb_options_t*,
leveldb_cache_t*);
LEVELDB_EXPORT void leveldb_options_set_block_size(leveldb_options_t*, size_t);
LEVELDB_EXPORT void leveldb_options_set_block_restart_interval(
leveldb_options_t*, int);
LEVELDB_EXPORT void leveldb_options_set_max_file_size(leveldb_options_t*,
size_t);
enum { leveldb_no_compression = 0, leveldb_snappy_compression = 1 };
LEVELDB_EXPORT void leveldb_options_set_compression(leveldb_options_t*, int);
/* Comparator */
LEVELDB_EXPORT leveldb_comparator_t* leveldb_comparator_create(
void* state, void (*destructor)(void*),
int (*compare)(void*, const char* a, size_t alen, const char* b,
size_t blen),
const char* (*name)(void*));
LEVELDB_EXPORT void leveldb_comparator_destroy(leveldb_comparator_t*);
/* Filter policy */
LEVELDB_EXPORT leveldb_filterpolicy_t* leveldb_filterpolicy_create(
void* state, void (*destructor)(void*),
char* (*create_filter)(void*, const char* const* key_array,
const size_t* key_length_array, int num_keys,
size_t* filter_length),
uint8_t (*key_may_match)(void*, const char* key, size_t length,
const char* filter, size_t filter_length),
const char* (*name)(void*));
LEVELDB_EXPORT void leveldb_filterpolicy_destroy(leveldb_filterpolicy_t*);
LEVELDB_EXPORT leveldb_filterpolicy_t* leveldb_filterpolicy_create_bloom(
int bits_per_key);
/* Read options */
LEVELDB_EXPORT leveldb_readoptions_t* leveldb_readoptions_create(void);
LEVELDB_EXPORT void leveldb_readoptions_destroy(leveldb_readoptions_t*);
LEVELDB_EXPORT void leveldb_readoptions_set_verify_checksums(
leveldb_readoptions_t*, uint8_t);
LEVELDB_EXPORT void leveldb_readoptions_set_fill_cache(leveldb_readoptions_t*,
uint8_t);
LEVELDB_EXPORT void leveldb_readoptions_set_snapshot(leveldb_readoptions_t*,
const leveldb_snapshot_t*);
/* Write options */
LEVELDB_EXPORT leveldb_writeoptions_t* leveldb_writeoptions_create(void);
LEVELDB_EXPORT void leveldb_writeoptions_destroy(leveldb_writeoptions_t*);
LEVELDB_EXPORT void leveldb_writeoptions_set_sync(leveldb_writeoptions_t*,
uint8_t);
/* Cache */
LEVELDB_EXPORT leveldb_cache_t* leveldb_cache_create_lru(size_t capacity);
LEVELDB_EXPORT void leveldb_cache_destroy(leveldb_cache_t* cache);
/* Env */
LEVELDB_EXPORT leveldb_env_t* leveldb_create_default_env(void);
LEVELDB_EXPORT void leveldb_env_destroy(leveldb_env_t*);
/* If not NULL, the returned buffer must be released using leveldb_free(). */
LEVELDB_EXPORT char* leveldb_env_get_test_directory(leveldb_env_t*);
/* Utility */
/* Calls free(ptr).
REQUIRES: ptr was malloc()-ed and returned by one of the routines
in this file. Note that in certain cases (typically on Windows), you
may need to call this routine instead of free(ptr) to dispose of
malloc()-ed memory returned by this library. */
LEVELDB_EXPORT void leveldb_free(void* ptr);
/* Return the major version number for this release. */
LEVELDB_EXPORT int leveldb_major_version(void);
/* Return the minor version number for this release. */
LEVELDB_EXPORT int leveldb_minor_version(void);
#ifdef __cplusplus
} /* end extern "C" */
#endif
#endif /* STORAGE_LEVELDB_INCLUDE_C_H_ */
================================================
FILE: Kit/lldb/include/cache.h
================================================
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
//
// A Cache is an interface that maps keys to values. It has internal
// synchronization and may be safely accessed concurrently from
// multiple threads. It may automatically evict entries to make room
// for new entries. Values have a specified charge against the cache
// capacity. For example, a cache where the values are variable
// length strings, may use the length of the string as the charge for
// the string.
//
// A builtin cache implementation with a least-recently-used eviction
// policy is provided. Clients may use their own implementations if
// they want something more sophisticated (like scan-resistance, a
// custom eviction policy, variable cache sizing, etc.)
#ifndef STORAGE_LEVELDB_INCLUDE_CACHE_H_
#define STORAGE_LEVELDB_INCLUDE_CACHE_H_
#include
#include "export.h"
#include "slice.h"
namespace leveldb {
class LEVELDB_EXPORT Cache;
// Create a new cache with a fixed size capacity. This implementation
// of Cache uses a least-recently-used eviction policy.
LEVELDB_EXPORT Cache* NewLRUCache(size_t capacity);
class LEVELDB_EXPORT Cache {
public:
Cache() = default;
Cache(const Cache&) = delete;
Cache& operator=(const Cache&) = delete;
// Destroys all existing entries by calling the "deleter"
// function that was passed to the constructor.
virtual ~Cache();
// Opaque handle to an entry stored in the cache.
struct Handle {};
// Insert a mapping from key->value into the cache and assign it
// the specified charge against the total cache capacity.
//
// Returns a handle that corresponds to the mapping. The caller
// must call this->Release(handle) when the returned mapping is no
// longer needed.
//
// When the inserted entry is no longer needed, the key and
// value will be passed to "deleter".
virtual Handle* Insert(const Slice& key, void* value, size_t charge,
void (*deleter)(const Slice& key, void* value)) = 0;
// If the cache has no mapping for "key", returns nullptr.
//
// Else return a handle that corresponds to the mapping. The caller
// must call this->Release(handle) when the returned mapping is no
// longer needed.
virtual Handle* Lookup(const Slice& key) = 0;
// Release a mapping returned by a previous Lookup().
// REQUIRES: handle must not have been released yet.
// REQUIRES: handle must have been returned by a method on *this.
virtual void Release(Handle* handle) = 0;
// Return the value encapsulated in a handle returned by a
// successful Lookup().
// REQUIRES: handle must not have been released yet.
// REQUIRES: handle must have been returned by a method on *this.
virtual void* Value(Handle* handle) = 0;
// If the cache contains entry for key, erase it. Note that the
// underlying entry will be kept around until all existing handles
// to it have been released.
virtual void Erase(const Slice& key) = 0;
// Return a new numeric id. May be used by multiple clients who are
// sharing the same cache to partition the key space. Typically the
// client will allocate a new id at startup and prepend the id to
// its cache keys.
virtual uint64_t NewId() = 0;
// Remove all cache entries that are not actively in use. Memory-constrained
// applications may wish to call this method to reduce memory usage.
// Default implementation of Prune() does nothing. Subclasses are strongly
// encouraged to override the default implementation. A future release of
// leveldb may change Prune() to a pure abstract method.
virtual void Prune() {}
// Return an estimate of the combined charges of all elements stored in the
// cache.
virtual size_t TotalCharge() const = 0;
};
} // namespace leveldb
#endif // STORAGE_LEVELDB_INCLUDE_CACHE_H_
================================================
FILE: Kit/lldb/include/comparator.h
================================================
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef STORAGE_LEVELDB_INCLUDE_COMPARATOR_H_
#define STORAGE_LEVELDB_INCLUDE_COMPARATOR_H_
#include
#include "export.h"
namespace leveldb {
class Slice;
// A Comparator object provides a total order across slices that are
// used as keys in an sstable or a database. A Comparator implementation
// must be thread-safe since leveldb may invoke its methods concurrently
// from multiple threads.
class LEVELDB_EXPORT Comparator {
public:
virtual ~Comparator();
// Three-way comparison. Returns value:
// < 0 iff "a" < "b",
// == 0 iff "a" == "b",
// > 0 iff "a" > "b"
virtual int Compare(const Slice& a, const Slice& b) const = 0;
// The name of the comparator. Used to check for comparator
// mismatches (i.e., a DB created with one comparator is
// accessed using a different comparator.
//
// The client of this package should switch to a new name whenever
// the comparator implementation changes in a way that will cause
// the relative ordering of any two keys to change.
//
// Names starting with "leveldb." are reserved and should not be used
// by any clients of this package.
virtual const char* Name() const = 0;
// Advanced functions: these are used to reduce the space requirements
// for internal data structures like index blocks.
// If *start < limit, changes *start to a short string in [start,limit).
// Simple comparator implementations may return with *start unchanged,
// i.e., an implementation of this method that does nothing is correct.
virtual void FindShortestSeparator(std::string* start,
const Slice& limit) const = 0;
// Changes *key to a short string >= *key.
// Simple comparator implementations may return with *key unchanged,
// i.e., an implementation of this method that does nothing is correct.
virtual void FindShortSuccessor(std::string* key) const = 0;
};
// Return a builtin comparator that uses lexicographic byte-wise
// ordering. The result remains the property of this module and
// must not be deleted.
LEVELDB_EXPORT const Comparator* BytewiseComparator();
} // namespace leveldb
#endif // STORAGE_LEVELDB_INCLUDE_COMPARATOR_H_
================================================
FILE: Kit/lldb/include/db.h
================================================
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef STORAGE_LEVELDB_INCLUDE_DB_H_
#define STORAGE_LEVELDB_INCLUDE_DB_H_
#include
#include
#include "export.h"
#include "iterator.h"
#include "options.h"
namespace leveldb {
// Update CMakeLists.txt if you change these
static const int kMajorVersion = 1;
static const int kMinorVersion = 23;
struct Options;
struct ReadOptions;
struct WriteOptions;
class WriteBatch;
// Abstract handle to particular state of a DB.
// A Snapshot is an immutable object and can therefore be safely
// accessed from multiple threads without any external synchronization.
class LEVELDB_EXPORT Snapshot {
protected:
virtual ~Snapshot();
};
// A range of keys
struct LEVELDB_EXPORT Range {
Range() = default;
Range(const Slice& s, const Slice& l) : start(s), limit(l) {}
Slice start; // Included in the range
Slice limit; // Not included in the range
};
// A DB is a persistent ordered map from keys to values.
// A DB is safe for concurrent access from multiple threads without
// any external synchronization.
class LEVELDB_EXPORT DB {
public:
// Open the database with the specified "name".
// Stores a pointer to a heap-allocated database in *dbptr and returns
// OK on success.
// Stores nullptr in *dbptr and returns a non-OK status on error.
// Caller should delete *dbptr when it is no longer needed.
static Status Open(const Options& options, const std::string& name,
DB** dbptr);
DB() = default;
DB(const DB&) = delete;
DB& operator=(const DB&) = delete;
virtual ~DB();
// Set the database entry for "key" to "value". Returns OK on success,
// and a non-OK status on error.
// Note: consider setting options.sync = true.
virtual Status Put(const WriteOptions& options, const Slice& key,
const Slice& value) = 0;
// Remove the database entry (if any) for "key". Returns OK on
// success, and a non-OK status on error. It is not an error if "key"
// did not exist in the database.
// Note: consider setting options.sync = true.
virtual Status Delete(const WriteOptions& options, const Slice& key) = 0;
// Apply the specified updates to the database.
// Returns OK on success, non-OK on failure.
// Note: consider setting options.sync = true.
virtual Status Write(const WriteOptions& options, WriteBatch* updates) = 0;
// If the database contains an entry for "key" store the
// corresponding value in *value and return OK.
//
// If there is no entry for "key" leave *value unchanged and return
// a status for which Status::IsNotFound() returns true.
//
// May return some other Status on an error.
virtual Status Get(const ReadOptions& options, const Slice& key,
std::string* value) = 0;
// Return a heap-allocated iterator over the contents of the database.
// The result of NewIterator() is initially invalid (caller must
// call one of the Seek methods on the iterator before using it).
//
// Caller should delete the iterator when it is no longer needed.
// The returned iterator should be deleted before this db is deleted.
virtual Iterator* NewIterator(const ReadOptions& options) = 0;
// Return a handle to the current DB state. Iterators created with
// this handle will all observe a stable snapshot of the current DB
// state. The caller must call ReleaseSnapshot(result) when the
// snapshot is no longer needed.
virtual const Snapshot* GetSnapshot() = 0;
// Release a previously acquired snapshot. The caller must not
// use "snapshot" after this call.
virtual void ReleaseSnapshot(const Snapshot* snapshot) = 0;
// DB implementations can export properties about their state
// via this method. If "property" is a valid property understood by this
// DB implementation, fills "*value" with its current value and returns
// true. Otherwise returns false.
//
//
// Valid property names include:
//
// "leveldb.num-files-at-level" - return the number of files at level ,
// where is an ASCII representation of a level number (e.g. "0").
// "leveldb.stats" - returns a multi-line string that describes statistics
// about the internal operation of the DB.
// "leveldb.sstables" - returns a multi-line string that describes all
// of the sstables that make up the db contents.
// "leveldb.approximate-memory-usage" - returns the approximate number of
// bytes of memory in use by the DB.
virtual bool GetProperty(const Slice& property, std::string* value) = 0;
// For each i in [0,n-1], store in "sizes[i]", the approximate
// file system space used by keys in "[range[i].start .. range[i].limit)".
//
// Note that the returned sizes measure file system space usage, so
// if the user data compresses by a factor of ten, the returned
// sizes will be one-tenth the size of the corresponding user data size.
//
// The results may not include the sizes of recently written data.
virtual void GetApproximateSizes(const Range* range, int n,
uint64_t* sizes) = 0;
// Compact the underlying storage for the key range [*begin,*end].
// In particular, deleted and overwritten versions are discarded,
// and the data is rearranged to reduce the cost of operations
// needed to access the data. This operation should typically only
// be invoked by users who understand the underlying implementation.
//
// begin==nullptr is treated as a key before all keys in the database.
// end==nullptr is treated as a key after all keys in the database.
// Therefore the following call will compact the entire database:
// db->CompactRange(nullptr, nullptr);
virtual void CompactRange(const Slice* begin, const Slice* end) = 0;
};
// Destroy the contents of the specified database.
// Be very careful using this method.
//
// Note: For backwards compatibility, if DestroyDB is unable to list the
// database files, Status::OK() will still be returned masking this failure.
LEVELDB_EXPORT Status DestroyDB(const std::string& name,
const Options& options);
// If a DB cannot be opened, you may attempt to call this method to
// resurrect as much of the contents of the database as possible.
// Some data may be lost, so be careful when calling this function
// on a database that contains important information.
LEVELDB_EXPORT Status RepairDB(const std::string& dbname,
const Options& options);
} // namespace leveldb
#endif // STORAGE_LEVELDB_INCLUDE_DB_H_
================================================
FILE: Kit/lldb/include/dumpfile.h
================================================
// Copyright (c) 2014 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef STORAGE_LEVELDB_INCLUDE_DUMPFILE_H_
#define STORAGE_LEVELDB_INCLUDE_DUMPFILE_H_
#include
#include "env.h"
#include "export.h"
#include "status.h"
namespace leveldb {
// Dump the contents of the file named by fname in text format to
// *dst. Makes a sequence of dst->Append() calls; each call is passed
// the newline-terminated text corresponding to a single item found
// in the file.
//
// Returns a non-OK result if fname does not name a leveldb storage
// file, or if the file cannot be read.
LEVELDB_EXPORT Status DumpFile(Env* env, const std::string& fname,
WritableFile* dst);
} // namespace leveldb
#endif // STORAGE_LEVELDB_INCLUDE_DUMPFILE_H_
================================================
FILE: Kit/lldb/include/env.h
================================================
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
//
// An Env is an interface used by the leveldb implementation to access
// operating system functionality like the filesystem etc. Callers
// may wish to provide a custom Env object when opening a database to
// get fine gain control; e.g., to rate limit file system operations.
//
// All Env implementations are safe for concurrent access from
// multiple threads without any external synchronization.
#ifndef STORAGE_LEVELDB_INCLUDE_ENV_H_
#define STORAGE_LEVELDB_INCLUDE_ENV_H_
#include
#include
#include
#include
#include "export.h"
#include "status.h"
// This workaround can be removed when leveldb::Env::DeleteFile is removed.
#if defined(_WIN32)
// On Windows, the method name DeleteFile (below) introduces the risk of
// triggering undefined behavior by exposing the compiler to different
// declarations of the Env class in different translation units.
//
// This is because , a fairly popular header file for Windows
// applications, defines a DeleteFile macro. So, files that include the Windows
// header before this header will contain an altered Env declaration.
//
// This workaround ensures that the compiler sees the same Env declaration,
// independently of whether was included.
#if defined(DeleteFile)
#undef DeleteFile
#define LEVELDB_DELETEFILE_UNDEFINED
#endif // defined(DeleteFile)
#endif // defined(_WIN32)
namespace leveldb {
class FileLock;
class Logger;
class RandomAccessFile;
class SequentialFile;
class Slice;
class WritableFile;
class LEVELDB_EXPORT Env {
public:
Env();
Env(const Env&) = delete;
Env& operator=(const Env&) = delete;
virtual ~Env();
// Return a default environment suitable for the current operating
// system. Sophisticated users may wish to provide their own Env
// implementation instead of relying on this default environment.
//
// The result of Default() belongs to leveldb and must never be deleted.
static Env* Default();
// Create an object that sequentially reads the file with the specified name.
// On success, stores a pointer to the new file in *result and returns OK.
// On failure stores nullptr in *result and returns non-OK. If the file does
// not exist, returns a non-OK status. Implementations should return a
// NotFound status when the file does not exist.
//
// The returned file will only be accessed by one thread at a time.
virtual Status NewSequentialFile(const std::string& fname,
SequentialFile** result) = 0;
// Create an object supporting random-access reads from the file with the
// specified name. On success, stores a pointer to the new file in
// *result and returns OK. On failure stores nullptr in *result and
// returns non-OK. If the file does not exist, returns a non-OK
// status. Implementations should return a NotFound status when the file does
// not exist.
//
// The returned file may be concurrently accessed by multiple threads.
virtual Status NewRandomAccessFile(const std::string& fname,
RandomAccessFile** result) = 0;
// Create an object that writes to a new file with the specified
// name. Deletes any existing file with the same name and creates a
// new file. On success, stores a pointer to the new file in
// *result and returns OK. On failure stores nullptr in *result and
// returns non-OK.
//
// The returned file will only be accessed by one thread at a time.
virtual Status NewWritableFile(const std::string& fname,
WritableFile** result) = 0;
// Create an object that either appends to an existing file, or
// writes to a new file (if the file does not exist to begin with).
// On success, stores a pointer to the new file in *result and
// returns OK. On failure stores nullptr in *result and returns
// non-OK.
//
// The returned file will only be accessed by one thread at a time.
//
// May return an IsNotSupportedError error if this Env does
// not allow appending to an existing file. Users of Env (including
// the leveldb implementation) must be prepared to deal with
// an Env that does not support appending.
virtual Status NewAppendableFile(const std::string& fname,
WritableFile** result);
// Returns true iff the named file exists.
virtual bool FileExists(const std::string& fname) = 0;
// Store in *result the names of the children of the specified directory.
// The names are relative to "dir".
// Original contents of *results are dropped.
virtual Status GetChildren(const std::string& dir,
std::vector* result) = 0;
// Delete the named file.
//
// The default implementation calls DeleteFile, to support legacy Env
// implementations. Updated Env implementations must override RemoveFile and
// ignore the existence of DeleteFile. Updated code calling into the Env API
// must call RemoveFile instead of DeleteFile.
//
// A future release will remove DeleteDir and the default implementation of
// RemoveDir.
virtual Status RemoveFile(const std::string& fname);
// DEPRECATED: Modern Env implementations should override RemoveFile instead.
//
// The default implementation calls RemoveFile, to support legacy Env user
// code that calls this method on modern Env implementations. Modern Env user
// code should call RemoveFile.
//
// A future release will remove this method.
virtual Status DeleteFile(const std::string& fname);
// Create the specified directory.
virtual Status CreateDir(const std::string& dirname) = 0;
// Delete the specified directory.
//
// The default implementation calls DeleteDir, to support legacy Env
// implementations. Updated Env implementations must override RemoveDir and
// ignore the existence of DeleteDir. Modern code calling into the Env API
// must call RemoveDir instead of DeleteDir.
//
// A future release will remove DeleteDir and the default implementation of
// RemoveDir.
virtual Status RemoveDir(const std::string& dirname);
// DEPRECATED: Modern Env implementations should override RemoveDir instead.
//
// The default implementation calls RemoveDir, to support legacy Env user
// code that calls this method on modern Env implementations. Modern Env user
// code should call RemoveDir.
//
// A future release will remove this method.
virtual Status DeleteDir(const std::string& dirname);
// Store the size of fname in *file_size.
virtual Status GetFileSize(const std::string& fname, uint64_t* file_size) = 0;
// Rename file src to target.
virtual Status RenameFile(const std::string& src,
const std::string& target) = 0;
// Lock the specified file. Used to prevent concurrent access to
// the same db by multiple processes. On failure, stores nullptr in
// *lock and returns non-OK.
//
// On success, stores a pointer to the object that represents the
// acquired lock in *lock and returns OK. The caller should call
// UnlockFile(*lock) to release the lock. If the process exits,
// the lock will be automatically released.
//
// If somebody else already holds the lock, finishes immediately
// with a failure. I.e., this call does not wait for existing locks
// to go away.
//
// May create the named file if it does not already exist.
virtual Status LockFile(const std::string& fname, FileLock** lock) = 0;
// Release the lock acquired by a previous successful call to LockFile.
// REQUIRES: lock was returned by a successful LockFile() call
// REQUIRES: lock has not already been unlocked.
virtual Status UnlockFile(FileLock* lock) = 0;
// Arrange to run "(*function)(arg)" once in a background thread.
//
// "function" may run in an unspecified thread. Multiple functions
// added to the same Env may run concurrently in different threads.
// I.e., the caller may not assume that background work items are
// serialized.
virtual void Schedule(void (*function)(void* arg), void* arg) = 0;
// Start a new thread, invoking "function(arg)" within the new thread.
// When "function(arg)" returns, the thread will be destroyed.
virtual void StartThread(void (*function)(void* arg), void* arg) = 0;
// *path is set to a temporary directory that can be used for testing. It may
// or may not have just been created. The directory may or may not differ
// between runs of the same process, but subsequent calls will return the
// same directory.
virtual Status GetTestDirectory(std::string* path) = 0;
// Create and return a log file for storing informational messages.
virtual Status NewLogger(const std::string& fname, Logger** result) = 0;
// Returns the number of micro-seconds since some fixed point in time. Only
// useful for computing deltas of time.
virtual uint64_t NowMicros() = 0;
// Sleep/delay the thread for the prescribed number of micro-seconds.
virtual void SleepForMicroseconds(int micros) = 0;
};
// A file abstraction for reading sequentially through a file
class LEVELDB_EXPORT SequentialFile {
public:
SequentialFile() = default;
SequentialFile(const SequentialFile&) = delete;
SequentialFile& operator=(const SequentialFile&) = delete;
virtual ~SequentialFile();
// Read up to "n" bytes from the file. "scratch[0..n-1]" may be
// written by this routine. Sets "*result" to the data that was
// read (including if fewer than "n" bytes were successfully read).
// May set "*result" to point at data in "scratch[0..n-1]", so
// "scratch[0..n-1]" must be live when "*result" is used.
// If an error was encountered, returns a non-OK status.
//
// REQUIRES: External synchronization
virtual Status Read(size_t n, Slice* result, char* scratch) = 0;
// Skip "n" bytes from the file. This is guaranteed to be no
// slower that reading the same data, but may be faster.
//
// If end of file is reached, skipping will stop at the end of the
// file, and Skip will return OK.
//
// REQUIRES: External synchronization
virtual Status Skip(uint64_t n) = 0;
};
// A file abstraction for randomly reading the contents of a file.
class LEVELDB_EXPORT RandomAccessFile {
public:
RandomAccessFile() = default;
RandomAccessFile(const RandomAccessFile&) = delete;
RandomAccessFile& operator=(const RandomAccessFile&) = delete;
virtual ~RandomAccessFile();
// Read up to "n" bytes from the file starting at "offset".
// "scratch[0..n-1]" may be written by this routine. Sets "*result"
// to the data that was read (including if fewer than "n" bytes were
// successfully read). May set "*result" to point at data in
// "scratch[0..n-1]", so "scratch[0..n-1]" must be live when
// "*result" is used. If an error was encountered, returns a non-OK
// status.
//
// Safe for concurrent use by multiple threads.
virtual Status Read(uint64_t offset, size_t n, Slice* result,
char* scratch) const = 0;
};
// A file abstraction for sequential writing. The implementation
// must provide buffering since callers may append small fragments
// at a time to the file.
class LEVELDB_EXPORT WritableFile {
public:
WritableFile() = default;
WritableFile(const WritableFile&) = delete;
WritableFile& operator=(const WritableFile&) = delete;
virtual ~WritableFile();
virtual Status Append(const Slice& data) = 0;
virtual Status Close() = 0;
virtual Status Flush() = 0;
virtual Status Sync() = 0;
};
// An interface for writing log messages.
class LEVELDB_EXPORT Logger {
public:
Logger() = default;
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
virtual ~Logger();
// Write an entry to the log file with the specified format.
virtual void Logv(const char* format, std::va_list ap) = 0;
};
// Identifies a locked file.
class LEVELDB_EXPORT FileLock {
public:
FileLock() = default;
FileLock(const FileLock&) = delete;
FileLock& operator=(const FileLock&) = delete;
virtual ~FileLock();
};
// Log the specified data to *info_log if info_log is non-null.
void Log(Logger* info_log, const char* format, ...)
#if defined(__GNUC__) || defined(__clang__)
__attribute__((__format__(__printf__, 2, 3)))
#endif
;
// A utility routine: write "data" to the named file.
LEVELDB_EXPORT Status WriteStringToFile(Env* env, const Slice& data,
const std::string& fname);
// A utility routine: read contents of named file into *data
LEVELDB_EXPORT Status ReadFileToString(Env* env, const std::string& fname,
std::string* data);
// An implementation of Env that forwards all calls to another Env.
// May be useful to clients who wish to override just part of the
// functionality of another Env.
class LEVELDB_EXPORT EnvWrapper : public Env {
public:
// Initialize an EnvWrapper that delegates all calls to *t.
explicit EnvWrapper(Env* t) : target_(t) {}
virtual ~EnvWrapper();
// Return the target to which this Env forwards all calls.
Env* target() const { return target_; }
// The following text is boilerplate that forwards all methods to target().
Status NewSequentialFile(const std::string& f, SequentialFile** r) override {
return target_->NewSequentialFile(f, r);
}
Status NewRandomAccessFile(const std::string& f,
RandomAccessFile** r) override {
return target_->NewRandomAccessFile(f, r);
}
Status NewWritableFile(const std::string& f, WritableFile** r) override {
return target_->NewWritableFile(f, r);
}
Status NewAppendableFile(const std::string& f, WritableFile** r) override {
return target_->NewAppendableFile(f, r);
}
bool FileExists(const std::string& f) override {
return target_->FileExists(f);
}
Status GetChildren(const std::string& dir,
std::vector* r) override {
return target_->GetChildren(dir, r);
}
Status RemoveFile(const std::string& f) override {
return target_->RemoveFile(f);
}
Status CreateDir(const std::string& d) override {
return target_->CreateDir(d);
}
Status RemoveDir(const std::string& d) override {
return target_->RemoveDir(d);
}
Status GetFileSize(const std::string& f, uint64_t* s) override {
return target_->GetFileSize(f, s);
}
Status RenameFile(const std::string& s, const std::string& t) override {
return target_->RenameFile(s, t);
}
Status LockFile(const std::string& f, FileLock** l) override {
return target_->LockFile(f, l);
}
Status UnlockFile(FileLock* l) override { return target_->UnlockFile(l); }
void Schedule(void (*f)(void*), void* a) override {
return target_->Schedule(f, a);
}
void StartThread(void (*f)(void*), void* a) override {
return target_->StartThread(f, a);
}
Status GetTestDirectory(std::string* path) override {
return target_->GetTestDirectory(path);
}
Status NewLogger(const std::string& fname, Logger** result) override {
return target_->NewLogger(fname, result);
}
uint64_t NowMicros() override { return target_->NowMicros(); }
void SleepForMicroseconds(int micros) override {
target_->SleepForMicroseconds(micros);
}
private:
Env* target_;
};
} // namespace leveldb
// This workaround can be removed when leveldb::Env::DeleteFile is removed.
// Redefine DeleteFile if it was undefined earlier.
#if defined(_WIN32) && defined(LEVELDB_DELETEFILE_UNDEFINED)
#if defined(UNICODE)
#define DeleteFile DeleteFileW
#else
#define DeleteFile DeleteFileA
#endif // defined(UNICODE)
#endif // defined(_WIN32) && defined(LEVELDB_DELETEFILE_UNDEFINED)
#endif // STORAGE_LEVELDB_INCLUDE_ENV_H_
================================================
FILE: Kit/lldb/include/export.h
================================================
// Copyright (c) 2017 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef STORAGE_LEVELDB_INCLUDE_EXPORT_H_
#define STORAGE_LEVELDB_INCLUDE_EXPORT_H_
#if !defined(LEVELDB_EXPORT)
#if defined(LEVELDB_SHARED_LIBRARY)
#if defined(_WIN32)
#if defined(LEVELDB_COMPILE_LIBRARY)
#define LEVELDB_EXPORT __declspec(dllexport)
#else
#define LEVELDB_EXPORT __declspec(dllimport)
#endif // defined(LEVELDB_COMPILE_LIBRARY)
#else // defined(_WIN32)
#if defined(LEVELDB_COMPILE_LIBRARY)
#define LEVELDB_EXPORT __attribute__((visibility("default")))
#else
#define LEVELDB_EXPORT
#endif
#endif // defined(_WIN32)
#else // defined(LEVELDB_SHARED_LIBRARY)
#define LEVELDB_EXPORT
#endif
#endif // !defined(LEVELDB_EXPORT)
#endif // STORAGE_LEVELDB_INCLUDE_EXPORT_H_
================================================
FILE: Kit/lldb/include/filter_policy.h
================================================
// Copyright (c) 2012 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
//
// A database can be configured with a custom FilterPolicy object.
// This object is responsible for creating a small filter from a set
// of keys. These filters are stored in leveldb and are consulted
// automatically by leveldb to decide whether or not to read some
// information from disk. In many cases, a filter can cut down the
// number of disk seeks form a handful to a single disk seek per
// DB::Get() call.
//
// Most people will want to use the builtin bloom filter support (see
// NewBloomFilterPolicy() below).
#ifndef STORAGE_LEVELDB_INCLUDE_FILTER_POLICY_H_
#define STORAGE_LEVELDB_INCLUDE_FILTER_POLICY_H_
#include
#include "export.h"
namespace leveldb {
class Slice;
class LEVELDB_EXPORT FilterPolicy {
public:
virtual ~FilterPolicy();
// Return the name of this policy. Note that if the filter encoding
// changes in an incompatible way, the name returned by this method
// must be changed. Otherwise, old incompatible filters may be
// passed to methods of this type.
virtual const char* Name() const = 0;
// keys[0,n-1] contains a list of keys (potentially with duplicates)
// that are ordered according to the user supplied comparator.
// Append a filter that summarizes keys[0,n-1] to *dst.
//
// Warning: do not change the initial contents of *dst. Instead,
// append the newly constructed filter to *dst.
virtual void CreateFilter(const Slice* keys, int n,
std::string* dst) const = 0;
// "filter" contains the data appended by a preceding call to
// CreateFilter() on this class. This method must return true if
// the key was in the list of keys passed to CreateFilter().
// This method may return true or false if the key was not on the
// list, but it should aim to return false with a high probability.
virtual bool KeyMayMatch(const Slice& key, const Slice& filter) const = 0;
};
// Return a new filter policy that uses a bloom filter with approximately
// the specified number of bits per key. A good value for bits_per_key
// is 10, which yields a filter with ~ 1% false positive rate.
//
// Callers must delete the result after any database that is using the
// result has been closed.
//
// Note: if you are using a custom comparator that ignores some parts
// of the keys being compared, you must not use NewBloomFilterPolicy()
// and must provide your own FilterPolicy that also ignores the
// corresponding parts of the keys. For example, if the comparator
// ignores trailing spaces, it would be incorrect to use a
// FilterPolicy (like NewBloomFilterPolicy) that does not ignore
// trailing spaces in keys.
LEVELDB_EXPORT const FilterPolicy* NewBloomFilterPolicy(int bits_per_key);
} // namespace leveldb
#endif // STORAGE_LEVELDB_INCLUDE_FILTER_POLICY_H_
================================================
FILE: Kit/lldb/include/iterator.h
================================================
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
//
// An iterator yields a sequence of key/value pairs from a source.
// The following class defines the interface. Multiple implementations
// are provided by this library. In particular, iterators are provided
// to access the contents of a Table or a DB.
//
// Multiple threads can invoke const methods on an Iterator without
// external synchronization, but if any of the threads may call a
// non-const method, all threads accessing the same Iterator must use
// external synchronization.
#ifndef STORAGE_LEVELDB_INCLUDE_ITERATOR_H_
#define STORAGE_LEVELDB_INCLUDE_ITERATOR_H_
#include "export.h"
#include "slice.h"
#include "status.h"
namespace leveldb {
class LEVELDB_EXPORT Iterator {
public:
Iterator();
Iterator(const Iterator&) = delete;
Iterator& operator=(const Iterator&) = delete;
virtual ~Iterator();
// An iterator is either positioned at a key/value pair, or
// not valid. This method returns true iff the iterator is valid.
virtual bool Valid() const = 0;
// Position at the first key in the source. The iterator is Valid()
// after this call iff the source is not empty.
virtual void SeekToFirst() = 0;
// Position at the last key in the source. The iterator is
// Valid() after this call iff the source is not empty.
virtual void SeekToLast() = 0;
// Position at the first key in the source that is at or past target.
// The iterator is Valid() after this call iff the source contains
// an entry that comes at or past target.
virtual void Seek(const Slice& target) = 0;
// Moves to the next entry in the source. After this call, Valid() is
// true iff the iterator was not positioned at the last entry in the source.
// REQUIRES: Valid()
virtual void Next() = 0;
// Moves to the previous entry in the source. After this call, Valid() is
// true iff the iterator was not positioned at the first entry in source.
// REQUIRES: Valid()
virtual void Prev() = 0;
// Return the key for the current entry. The underlying storage for
// the returned slice is valid only until the next modification of
// the iterator.
// REQUIRES: Valid()
virtual Slice key() const = 0;
// Return the value for the current entry. The underlying storage for
// the returned slice is valid only until the next modification of
// the iterator.
// REQUIRES: Valid()
virtual Slice value() const = 0;
// If an error has occurred, return it. Else return an ok status.
virtual Status status() const = 0;
// Clients are allowed to register function/arg1/arg2 triples that
// will be invoked when this iterator is destroyed.
//
// Note that unlike all of the preceding methods, this method is
// not abstract and therefore clients should not override it.
using CleanupFunction = void (*)(void* arg1, void* arg2);
void RegisterCleanup(CleanupFunction function, void* arg1, void* arg2);
private:
// Cleanup functions are stored in a single-linked list.
// The list's head node is inlined in the iterator.
struct CleanupNode {
// True if the node is not used. Only head nodes might be unused.
bool IsEmpty() const { return function == nullptr; }
// Invokes the cleanup function.
void Run() {
assert(function != nullptr);
(*function)(arg1, arg2);
}
// The head node is used if the function pointer is not null.
CleanupFunction function;
void* arg1;
void* arg2;
CleanupNode* next;
};
CleanupNode cleanup_head_;
};
// Return an empty iterator (yields nothing).
LEVELDB_EXPORT Iterator* NewEmptyIterator();
// Return an empty iterator with the specified status.
LEVELDB_EXPORT Iterator* NewErrorIterator(const Status& status);
} // namespace leveldb
#endif // STORAGE_LEVELDB_INCLUDE_ITERATOR_H_
================================================
FILE: Kit/lldb/include/options.h
================================================
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef STORAGE_LEVELDB_INCLUDE_OPTIONS_H_
#define STORAGE_LEVELDB_INCLUDE_OPTIONS_H_
#include
#include "export.h"
namespace leveldb {
class Cache;
class Comparator;
class Env;
class FilterPolicy;
class Logger;
class Snapshot;
// DB contents are stored in a set of blocks, each of which holds a
// sequence of key,value pairs. Each block may be compressed before
// being stored in a file. The following enum describes which
// compression method (if any) is used to compress a block.
enum CompressionType {
// NOTE: do not change the values of existing entries, as these are
// part of the persistent format on disk.
kNoCompression = 0x0,
kSnappyCompression = 0x1,
kZstdCompression = 0x2,
};
// Options to control the behavior of a database (passed to DB::Open)
struct LEVELDB_EXPORT Options {
// Create an Options object with default values for all fields.
Options();
// -------------------
// Parameters that affect behavior
// Comparator used to define the order of keys in the table.
// Default: a comparator that uses lexicographic byte-wise ordering
//
// REQUIRES: The client must ensure that the comparator supplied
// here has the same name and orders keys *exactly* the same as the
// comparator provided to previous open calls on the same DB.
const Comparator* comparator;
// If true, the database will be created if it is missing.
bool create_if_missing = false;
// If true, an error is raised if the database already exists.
bool error_if_exists = false;
// If true, the implementation will do aggressive checking of the
// data it is processing and will stop early if it detects any
// errors. This may have unforeseen ramifications: for example, a
// corruption of one DB entry may cause a large number of entries to
// become unreadable or for the entire DB to become unopenable.
bool paranoid_checks = false;
// Use the specified object to interact with the environment,
// e.g. to read/write files, schedule background work, etc.
// Default: Env::Default()
Env* env;
// Any internal progress/error information generated by the db will
// be written to info_log if it is non-null, or to a file stored
// in the same directory as the DB contents if info_log is null.
Logger* info_log = nullptr;
// -------------------
// Parameters that affect performance
// Amount of data to build up in memory (backed by an unsorted log
// on disk) before converting to a sorted on-disk file.
//
// Larger values increase performance, especially during bulk loads.
// Up to two write buffers may be held in memory at the same time,
// so you may wish to adjust this parameter to control memory usage.
// Also, a larger write buffer will result in a longer recovery time
// the next time the database is opened.
size_t write_buffer_size = 4 * 1024 * 1024;
// Number of open files that can be used by the DB. You may need to
// increase this if your database has a large working set (budget
// one open file per 2MB of working set).
int max_open_files = 1000;
// Control over blocks (user data is stored in a set of blocks, and
// a block is the unit of reading from disk).
// If non-null, use the specified cache for blocks.
// If null, leveldb will automatically create and use an 8MB internal cache.
Cache* block_cache = nullptr;
// Approximate size of user data packed per block. Note that the
// block size specified here corresponds to uncompressed data. The
// actual size of the unit read from disk may be smaller if
// compression is enabled. This parameter can be changed dynamically.
size_t block_size = 4 * 1024;
// Number of keys between restart points for delta encoding of keys.
// This parameter can be changed dynamically. Most clients should
// leave this parameter alone.
int block_restart_interval = 16;
// Leveldb will write up to this amount of bytes to a file before
// switching to a new one.
// Most clients should leave this parameter alone. However if your
// filesystem is more efficient with larger files, you could
// consider increasing the value. The downside will be longer
// compactions and hence longer latency/performance hiccups.
// Another reason to increase this parameter might be when you are
// initially populating a large database.
size_t max_file_size = 2 * 1024 * 1024;
// Compress blocks using the specified compression algorithm. This
// parameter can be changed dynamically.
//
// Default: kSnappyCompression, which gives lightweight but fast
// compression.
//
// Typical speeds of kSnappyCompression on an Intel(R) Core(TM)2 2.4GHz:
// ~200-500MB/s compression
// ~400-800MB/s decompression
// Note that these speeds are significantly faster than most
// persistent storage speeds, and therefore it is typically never
// worth switching to kNoCompression. Even if the input data is
// incompressible, the kSnappyCompression implementation will
// efficiently detect that and will switch to uncompressed mode.
CompressionType compression = kSnappyCompression;
// Compression level for zstd.
// Currently only the range [-5,22] is supported. Default is 1.
int zstd_compression_level = 1;
// EXPERIMENTAL: If true, append to existing MANIFEST and log files
// when a database is opened. This can significantly speed up open.
//
// Default: currently false, but may become true later.
bool reuse_logs = false;
// If non-null, use the specified filter policy to reduce disk reads.
// Many applications will benefit from passing the result of
// NewBloomFilterPolicy() here.
const FilterPolicy* filter_policy = nullptr;
};
// Options that control read operations
struct LEVELDB_EXPORT ReadOptions {
// If true, all data read from underlying storage will be
// verified against corresponding checksums.
bool verify_checksums = false;
// Should the data read for this iteration be cached in memory?
// Callers may wish to set this field to false for bulk scans.
bool fill_cache = true;
// If "snapshot" is non-null, read as of the supplied snapshot
// (which must belong to the DB that is being read and which must
// not have been released). If "snapshot" is null, use an implicit
// snapshot of the state at the beginning of this read operation.
const Snapshot* snapshot = nullptr;
};
// Options that control write operations
struct LEVELDB_EXPORT WriteOptions {
WriteOptions() = default;
// If true, the write will be flushed from the operating system
// buffer cache (by calling WritableFile::Sync()) before the write
// is considered complete. If this flag is true, writes will be
// slower.
//
// If this flag is false, and the machine crashes, some recent
// writes may be lost. Note that if it is just the process that
// crashes (i.e., the machine does not reboot), no writes will be
// lost even if sync==false.
//
// In other words, a DB write with sync==false has similar
// crash semantics as the "write()" system call. A DB write
// with sync==true has similar crash semantics to a "write()"
// system call followed by "fsync()".
bool sync = false;
};
} // namespace leveldb
#endif // STORAGE_LEVELDB_INCLUDE_OPTIONS_H_
================================================
FILE: Kit/lldb/include/slice.h
================================================
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
//
// Slice is a simple structure containing a pointer into some external
// storage and a size. The user of a Slice must ensure that the slice
// is not used after the corresponding external storage has been
// deallocated.
//
// Multiple threads can invoke const methods on a Slice without
// external synchronization, but if any of the threads may call a
// non-const method, all threads accessing the same Slice must use
// external synchronization.
#ifndef STORAGE_LEVELDB_INCLUDE_SLICE_H_
#define STORAGE_LEVELDB_INCLUDE_SLICE_H_
#include
#include
#include
#include
#include "export.h"
namespace leveldb {
class LEVELDB_EXPORT Slice {
public:
// Create an empty slice.
Slice() : data_(""), size_(0) {}
// Create a slice that refers to d[0,n-1].
Slice(const char* d, size_t n) : data_(d), size_(n) {}
// Create a slice that refers to the contents of "s"
Slice(const std::string& s) : data_(s.data()), size_(s.size()) {}
// Create a slice that refers to s[0,strlen(s)-1]
Slice(const char* s) : data_(s), size_(strlen(s)) {}
// Intentionally copyable.
Slice(const Slice&) = default;
Slice& operator=(const Slice&) = default;
// Return a pointer to the beginning of the referenced data
const char* data() const { return data_; }
// Return the length (in bytes) of the referenced data
size_t size() const { return size_; }
// Return true iff the length of the referenced data is zero
bool empty() const { return size_ == 0; }
// Return the ith byte in the referenced data.
// REQUIRES: n < size()
char operator[](size_t n) const {
assert(n < size());
return data_[n];
}
// Change this slice to refer to an empty array
void clear() {
data_ = "";
size_ = 0;
}
// Drop the first "n" bytes from this slice.
void remove_prefix(size_t n) {
assert(n <= size());
data_ += n;
size_ -= n;
}
// Return a string that contains the copy of the referenced data.
std::string ToString() const { return std::string(data_, size_); }
// Three-way comparison. Returns value:
// < 0 iff "*this" < "b",
// == 0 iff "*this" == "b",
// > 0 iff "*this" > "b"
int compare(const Slice& b) const;
// Return true iff "x" is a prefix of "*this"
bool starts_with(const Slice& x) const {
return ((size_ >= x.size_) && (memcmp(data_, x.data_, x.size_) == 0));
}
private:
const char* data_;
size_t size_;
};
inline bool operator==(const Slice& x, const Slice& y) {
return ((x.size() == y.size()) &&
(memcmp(x.data(), y.data(), x.size()) == 0));
}
inline bool operator!=(const Slice& x, const Slice& y) { return !(x == y); }
inline int Slice::compare(const Slice& b) const {
const size_t min_len = (size_ < b.size_) ? size_ : b.size_;
int r = memcmp(data_, b.data_, min_len);
if (r == 0) {
if (size_ < b.size_)
r = -1;
else if (size_ > b.size_)
r = +1;
}
return r;
}
} // namespace leveldb
#endif // STORAGE_LEVELDB_INCLUDE_SLICE_H_
================================================
FILE: Kit/lldb/include/status.h
================================================
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
//
// A Status encapsulates the result of an operation. It may indicate success,
// or it may indicate an error with an associated error message.
//
// Multiple threads can invoke const methods on a Status without
// external synchronization, but if any of the threads may call a
// non-const method, all threads accessing the same Status must use
// external synchronization.
#ifndef STORAGE_LEVELDB_INCLUDE_STATUS_H_
#define STORAGE_LEVELDB_INCLUDE_STATUS_H_
#include
#include
#include "export.h"
#include "slice.h"
namespace leveldb {
class LEVELDB_EXPORT Status {
public:
// Create a success status.
Status() noexcept : state_(nullptr) {}
~Status() { delete[] state_; }
Status(const Status& rhs);
Status& operator=(const Status& rhs);
Status(Status&& rhs) noexcept : state_(rhs.state_) { rhs.state_ = nullptr; }
Status& operator=(Status&& rhs) noexcept;
// Return a success status.
static Status OK() { return Status(); }
// Return error status of an appropriate type.
static Status NotFound(const Slice& msg, const Slice& msg2 = Slice()) {
return Status(kNotFound, msg, msg2);
}
static Status Corruption(const Slice& msg, const Slice& msg2 = Slice()) {
return Status(kCorruption, msg, msg2);
}
static Status NotSupported(const Slice& msg, const Slice& msg2 = Slice()) {
return Status(kNotSupported, msg, msg2);
}
static Status InvalidArgument(const Slice& msg, const Slice& msg2 = Slice()) {
return Status(kInvalidArgument, msg, msg2);
}
static Status IOError(const Slice& msg, const Slice& msg2 = Slice()) {
return Status(kIOError, msg, msg2);
}
// Returns true iff the status indicates success.
bool ok() const { return (state_ == nullptr); }
// Returns true iff the status indicates a NotFound error.
bool IsNotFound() const { return code() == kNotFound; }
// Returns true iff the status indicates a Corruption error.
bool IsCorruption() const { return code() == kCorruption; }
// Returns true iff the status indicates an IOError.
bool IsIOError() const { return code() == kIOError; }
// Returns true iff the status indicates a NotSupportedError.
bool IsNotSupportedError() const { return code() == kNotSupported; }
// Returns true iff the status indicates an InvalidArgument.
bool IsInvalidArgument() const { return code() == kInvalidArgument; }
// Return a string representation of this status suitable for printing.
// Returns the string "OK" for success.
std::string ToString() const;
private:
enum Code {
kOk = 0,
kNotFound = 1,
kCorruption = 2,
kNotSupported = 3,
kInvalidArgument = 4,
kIOError = 5
};
Code code() const {
return (state_ == nullptr) ? kOk : static_cast(state_[4]);
}
Status(Code code, const Slice& msg, const Slice& msg2);
static const char* CopyState(const char* s);
// OK status has a null state_. Otherwise, state_ is a new[] array
// of the following form:
// state_[0..3] == length of message
// state_[4] == code
// state_[5..] == message
const char* state_;
};
inline Status::Status(const Status& rhs) {
state_ = (rhs.state_ == nullptr) ? nullptr : CopyState(rhs.state_);
}
inline Status& Status::operator=(const Status& rhs) {
// The following condition catches both aliasing (when this == &rhs),
// and the common case where both rhs and *this are ok.
if (state_ != rhs.state_) {
delete[] state_;
state_ = (rhs.state_ == nullptr) ? nullptr : CopyState(rhs.state_);
}
return *this;
}
inline Status& Status::operator=(Status&& rhs) noexcept {
std::swap(state_, rhs.state_);
return *this;
}
} // namespace leveldb
#endif // STORAGE_LEVELDB_INCLUDE_STATUS_H_
================================================
FILE: Kit/lldb/include/table.h
================================================
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef STORAGE_LEVELDB_INCLUDE_TABLE_H_
#define STORAGE_LEVELDB_INCLUDE_TABLE_H_
#include
#include "export.h"
#include "iterator.h"
namespace leveldb {
class Block;
class BlockHandle;
class Footer;
struct Options;
class RandomAccessFile;
struct ReadOptions;
class TableCache;
// A Table is a sorted map from strings to strings. Tables are
// immutable and persistent. A Table may be safely accessed from
// multiple threads without external synchronization.
class LEVELDB_EXPORT Table {
public:
// Attempt to open the table that is stored in bytes [0..file_size)
// of "file", and read the metadata entries necessary to allow
// retrieving data from the table.
//
// If successful, returns ok and sets "*table" to the newly opened
// table. The client should delete "*table" when no longer needed.
// If there was an error while initializing the table, sets "*table"
// to nullptr and returns a non-ok status. Does not take ownership of
// "*source", but the client must ensure that "source" remains live
// for the duration of the returned table's lifetime.
//
// *file must remain live while this Table is in use.
static Status Open(const Options& options, RandomAccessFile* file,
uint64_t file_size, Table** table);
Table(const Table&) = delete;
Table& operator=(const Table&) = delete;
~Table();
// Returns a new iterator over the table contents.
// The result of NewIterator() is initially invalid (caller must
// call one of the Seek methods on the iterator before using it).
Iterator* NewIterator(const ReadOptions&) const;
// Given a key, return an approximate byte offset in the file where
// the data for that key begins (or would begin if the key were
// present in the file). The returned value is in terms of file
// bytes, and so includes effects like compression of the underlying data.
// E.g., the approximate offset of the last key in the table will
// be close to the file length.
uint64_t ApproximateOffsetOf(const Slice& key) const;
private:
friend class TableCache;
struct Rep;
static Iterator* BlockReader(void*, const ReadOptions&, const Slice&);
explicit Table(Rep* rep) : rep_(rep) {}
// Calls (*handle_result)(arg, ...) with the entry found after a call
// to Seek(key). May not make such a call if filter policy says
// that key is not present.
Status InternalGet(const ReadOptions&, const Slice& key, void* arg,
void (*handle_result)(void* arg, const Slice& k,
const Slice& v));
void ReadMeta(const Footer& footer);
void ReadFilter(const Slice& filter_handle_value);
Rep* const rep_;
};
} // namespace leveldb
#endif // STORAGE_LEVELDB_INCLUDE_TABLE_H_
================================================
FILE: Kit/lldb/include/table_builder.h
================================================
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
//
// TableBuilder provides the interface used to build a Table
// (an immutable and sorted map from keys to values).
//
// Multiple threads can invoke const methods on a TableBuilder without
// external synchronization, but if any of the threads may call a
// non-const method, all threads accessing the same TableBuilder must use
// external synchronization.
#ifndef STORAGE_LEVELDB_INCLUDE_TABLE_BUILDER_H_
#define STORAGE_LEVELDB_INCLUDE_TABLE_BUILDER_H_
#include
#include "export.h"
#include "options.h"
#include "status.h"
namespace leveldb {
class BlockBuilder;
class BlockHandle;
class WritableFile;
class LEVELDB_EXPORT TableBuilder {
public:
// Create a builder that will store the contents of the table it is
// building in *file. Does not close the file. It is up to the
// caller to close the file after calling Finish().
TableBuilder(const Options& options, WritableFile* file);
TableBuilder(const TableBuilder&) = delete;
TableBuilder& operator=(const TableBuilder&) = delete;
// REQUIRES: Either Finish() or Abandon() has been called.
~TableBuilder();
// Change the options used by this builder. Note: only some of the
// option fields can be changed after construction. If a field is
// not allowed to change dynamically and its value in the structure
// passed to the constructor is different from its value in the
// structure passed to this method, this method will return an error
// without changing any fields.
Status ChangeOptions(const Options& options);
// Add key,value to the table being constructed.
// REQUIRES: key is after any previously added key according to comparator.
// REQUIRES: Finish(), Abandon() have not been called
void Add(const Slice& key, const Slice& value);
// Advanced operation: flush any buffered key/value pairs to file.
// Can be used to ensure that two adjacent entries never live in
// the same data block. Most clients should not need to use this method.
// REQUIRES: Finish(), Abandon() have not been called
void Flush();
// Return non-ok iff some error has been detected.
Status status() const;
// Finish building the table. Stops using the file passed to the
// constructor after this function returns.
// REQUIRES: Finish(), Abandon() have not been called
Status Finish();
// Indicate that the contents of this builder should be abandoned. Stops
// using the file passed to the constructor after this function returns.
// If the caller is not going to call Finish(), it must call Abandon()
// before destroying this builder.
// REQUIRES: Finish(), Abandon() have not been called
void Abandon();
// Number of calls to Add() so far.
uint64_t NumEntries() const;
// Size of the file generated so far. If invoked after a successful
// Finish() call, returns the size of the final generated file.
uint64_t FileSize() const;
private:
bool ok() const { return status().ok(); }
void WriteBlock(BlockBuilder* block, BlockHandle* handle);
void WriteRawBlock(const Slice& data, CompressionType, BlockHandle* handle);
struct Rep;
Rep* rep_;
};
} // namespace leveldb
#endif // STORAGE_LEVELDB_INCLUDE_TABLE_BUILDER_H_
================================================
FILE: Kit/lldb/include/write_batch.h
================================================
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
//
// WriteBatch holds a collection of updates to apply atomically to a DB.
//
// The updates are applied in the order in which they are added
// to the WriteBatch. For example, the value of "key" will be "v3"
// after the following batch is written:
//
// batch.Put("key", "v1");
// batch.Delete("key");
// batch.Put("key", "v2");
// batch.Put("key", "v3");
//
// Multiple threads can invoke const methods on a WriteBatch without
// external synchronization, but if any of the threads may call a
// non-const method, all threads accessing the same WriteBatch must use
// external synchronization.
#ifndef STORAGE_LEVELDB_INCLUDE_WRITE_BATCH_H_
#define STORAGE_LEVELDB_INCLUDE_WRITE_BATCH_H_
#include
#include "export.h"
#include "status.h"
namespace leveldb {
class Slice;
class LEVELDB_EXPORT WriteBatch {
public:
class LEVELDB_EXPORT Handler {
public:
virtual ~Handler();
virtual void Put(const Slice& key, const Slice& value) = 0;
virtual void Delete(const Slice& key) = 0;
};
WriteBatch();
// Intentionally copyable.
WriteBatch(const WriteBatch&) = default;
WriteBatch& operator=(const WriteBatch&) = default;
~WriteBatch();
// Store the mapping "key->value" in the database.
void Put(const Slice& key, const Slice& value);
// If the database contains a mapping for "key", erase it. Else do nothing.
void Delete(const Slice& key);
// Clear all updates buffered in this batch.
void Clear();
// The size of the database changes caused by this batch.
//
// This number is tied to implementation details, and may change across
// releases. It is intended for LevelDB usage metrics.
size_t ApproximateSize() const;
// Copies the operations in "source" to this batch.
//
// This runs in O(source size) time. However, the constant factor is better
// than calling Iterate() over the source batch with a Handler that replicates
// the operations into this batch.
void Append(const WriteBatch& source);
// Support for iterating over the contents of a batch.
Status Iterate(Handler* handler) const;
private:
friend class WriteBatchInternal;
std::string rep_; // See comment in write_batch.cc for the format of rep_
};
} // namespace leveldb
#endif // STORAGE_LEVELDB_INCLUDE_WRITE_BATCH_H_
================================================
FILE: Kit/lldb/lldb.h
================================================
//
// lldb.h
// Kit
//
// Created by Serhiy Mytrovtsiy on 03/02/2024
// Using Swift 5.0
// Running on macOS 14.3
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//
#import
@interface LLDB:NSObject
-(instancetype)init:(NSString *) path;
-(NSArray *)keys:(NSString *)key;
-(bool)insert:(NSString *)key value:(NSString *)value;
-(NSString *)findOne:(NSString *)key;
-(NSString *)findLast:(NSString *)prefix;
-(NSArray *)findMany:(NSString *)prefix;
-(bool)deleteOne:(NSString *)key;
-(bool)deleteMany:(NSArray*)keys;
-(void)close;
@end
================================================
FILE: Kit/lldb/lldb.m
================================================
//
// lldb.m
// Kit
//
// Created by Serhiy Mytrovtsiy on 03/02/2024
// Using Swift 5.0
// Running on macOS 14.3
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//
#import "lldb.h"
#include
#include
#include
#import
#import
using namespace std;
@implementation LLDB {
leveldb::DB *db;
}
- (instancetype) init:(NSString *) name {
self = [super init];
if (self) {
bool status = [self createDB:name];
if (!status) {
return nil;
}
}
return self;
}
-(bool)createDB:(NSString *) path {
leveldb::Options options;
options.create_if_missing = true;
leveldb::Status status = leveldb::DB::Open(options, [path UTF8String], &self->db);
if (false == status.ok()) {
NSLog(@"ERROR: Unable to open/create database: %s", status.ToString().c_str());
return false;
}
return true;
}
-(NSArray *)keys:(NSString *)key {
leveldb::ReadOptions readOptions;
leveldb::Iterator *it = db->NewIterator(readOptions);
leveldb::Slice slice = leveldb::Slice(key.UTF8String);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (it->Seek(slice); it->Valid() && it->key().starts_with(slice); it->Next()) {
NSString *value = [[NSString alloc] initWithCString:it->key().ToString().c_str() encoding: NSUTF8StringEncoding];
[array addObject:value];
}
delete it;
return array;
}
-(bool)insert:(NSString *)key value:(NSString *)value {
ostringstream keyStream;
keyStream << key.UTF8String;
ostringstream valueStream;
valueStream << value.UTF8String;
leveldb::WriteOptions writeOptions;
leveldb::Status s = self->db->Put(writeOptions, keyStream.str(), valueStream.str());
return s.ok();
}
-(NSString *)findOne:(NSString *)key {
ostringstream keyStream;
keyStream << key.UTF8String;
leveldb::ReadOptions readOptions;
string value;
leveldb::Status s = self->db->Get(readOptions, keyStream.str(), &value);
NSString *nsstr = [[NSString alloc] initWithUTF8String:value.c_str()];
return [nsstr stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
}
-(NSString *)findLast:(NSString *)prefix {
leveldb::ReadOptions readOptions;
leveldb::Iterator *it = db->NewIterator(readOptions);
leveldb::Slice slice = leveldb::Slice(prefix.UTF8String);
NSString *value;
it->SeekToLast();
for (it->SeekToLast(); it->Valid() && it->key().starts_with(slice);) {
value = [[NSString alloc] initWithCString:it->value().ToString().c_str() encoding:[NSString defaultCStringEncoding]];
break;
}
delete it;
return value;
}
-(NSArray *)findMany:(NSString *)prefix {
leveldb::ReadOptions readOptions;
leveldb::Iterator *it = db->NewIterator(readOptions);
leveldb::Slice slice = leveldb::Slice(prefix.UTF8String);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (it->Seek(slice); it->Valid() && it->key().starts_with(slice); it->Next()) {
NSString *value = [[NSString alloc] initWithCString:it->value().ToString().c_str() encoding:[NSString defaultCStringEncoding]];
[array addObject:value];
}
delete it;
return array;
}
-(bool)deleteOne:(NSString *)key {
ostringstream keySream;
keySream << key.UTF8String;
leveldb::WriteOptions writeOptions;
leveldb::Status s = self->db->Delete(writeOptions, keySream.str());
return s.ok();
}
-(bool)deleteMany:(NSArray*)keys {
leveldb::WriteBatch batch;
for (int i=0; i <[keys count]; i++) {
NSString *key = [keys objectAtIndex:i];
leveldb::Slice slice = leveldb::Slice(key.UTF8String);
batch.Delete(slice);
}
leveldb::Status s = self->db->Write(leveldb::WriteOptions(), &batch);
return s.ok();
}
-(void)close {
delete self->db;
}
@end
================================================
FILE: Kit/module/module.swift
================================================
//
// module.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 09/04/2020.
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public struct module_c {
public var name: String
public var icon: NSImage?
public var defaultState: Bool = false
internal var defaultWidget: widget_t = .unknown
internal var availableWidgets: [widget_t] = []
internal var widgetsConfig: NSDictionary = NSDictionary()
internal var settingsConfig: NSDictionary = NSDictionary()
internal var previewConfig: NSDictionary = NSDictionary()
public var hasPreview: Bool { self.previewConfig["enabled"] as? Bool ?? false }
init(in path: String) {
let dict: NSDictionary = NSDictionary(contentsOfFile: path)!
if let name = dict["Name"] as? String {
self.name = name
} else {
fatalError("failed to initialize module, name is missing")
}
if let state = dict["State"] as? Bool {
self.defaultState = state
}
if let symbol = dict["Symbol"] as? String {
self.icon = NSImage(systemSymbolName: symbol, accessibilityDescription: nil)
}
if self.icon == nil, let symbol = dict["AlternativeSymbol"] as? String {
self.icon = NSImage(systemSymbolName: symbol, accessibilityDescription: nil)
}
if let widgetsDict = dict["Widgets"] as? NSDictionary {
var list: [String: Int] = [:]
self.widgetsConfig = widgetsDict
for widgetName in widgetsDict.allKeys {
if let widget = widget_t(rawValue: widgetName as! String) {
let widgetDict = widgetsDict[widgetName as! String] as! NSDictionary
if widgetDict["Default"] as! Bool {
self.defaultWidget = widget
}
var order = 0
if let o = widgetDict["Order"] as? Int {
order = o
}
list[widgetName as! String] = order
}
}
self.availableWidgets = list.sorted(by: { $0.1 < $1.1 }).map{ (widget_t(rawValue: $0.key) ?? .unknown) }
}
if let settingsDict = dict["Settings"] as? NSDictionary {
self.settingsConfig = settingsDict
}
if let previewDict = dict["Preview"] as? NSDictionary {
self.previewConfig = previewDict
}
}
}
open class Module {
public var config: module_c
public var available: Bool = false
public var enabled: Bool = false
public var menuBar: MenuBar
public var window: Window? = nil
public let portal: Portal_p?
public var name: String { config.name }
public var combinedPosition: Int {
get { Store.shared.int(key: "\(self.name)_position", defaultValue: 0) }
set { Store.shared.set(key: "\(self.name)_position", value: newValue) }
}
public var userDefaults: UserDefaults? = UserDefaults(suiteName: "\(Bundle.main.object(forInfoDictionaryKey: "TeamId") as! String).eu.exelban.Stats.widgets")
public var popupKeyboardShortcut: [UInt16] { self.popupView?.keyboardShortcut ?? [] }
private var moduleType: ModuleType
private var settingsView: Settings_v? = nil
private var popup: PopupWindow? = nil
private var popupView: Popup_p? = nil
private var notificationsView: NotificationsWrapper? = nil
private var previewView: Preview_v? = nil
private let log: NextLog
private var readers: [Reader_p] = []
private var pauseState: Bool {
get { Store.shared.bool(key: "pause", defaultValue: false) }
set { Store.shared.set(key: "pause", value: newValue) }
}
public init(
moduleType: ModuleType,
popup: Popup_p? = nil,
settings: Settings_v? = nil,
portal: Portal_p? = nil,
notifications: NotificationsWrapper? = nil,
preview: Preview_v? = nil
) {
self.moduleType = moduleType
self.portal = portal
self.config = module_c(in: Bundle(for: type(of: self)).path(forResource: "config", ofType: "plist")!)
self.log = NextLog.shared.copy(category: self.config.name)
self.settingsView = settings
self.popupView = popup
self.notificationsView = notifications
self.previewView = preview
self.menuBar = MenuBar(moduleName: self.config.name)
self.available = self.isAvailable()
self.enabled = Store.shared.bool(key: "\(self.config.name)_state", defaultValue: self.config.defaultState)
self.userDefaults?.set(self.enabled, forKey: "\(self.config.name)_state")
if !self.available {
debug("Module is not available", log: self.log)
if self.enabled {
self.enabled = false
Store.shared.set(key: "\(self.config.name)_state", value: false)
}
return
} else if self.pauseState {
self.disable()
}
NotificationCenter.default.addObserver(self, selector: #selector(listenForMouseDownInSettings), name: .clickInSettings, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(listenForModuleToggle), name: .toggleModule, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(listenForPopupToggle), name: .togglePopup, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(listenForToggleWidget), name: .toggleWidget, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(listenForWindowOpen), name: .openWindow, object: nil)
// swiftlint:disable empty_count
if self.config.widgetsConfig.count != 0 {
// swiftlint:enable empty_count
self.initWidgets()
} else {
debug("Module started without widget", log: self.log)
}
self.window = Window(
config: &self.config,
widgets: &self.menuBar.widgets,
modulePreview: self.previewView,
moduleSettings: self.settingsView,
popupSettings: self.popupView,
notificationsSettings: self.notificationsView
)
self.popup = PopupWindow(title: self.config.name, module: self.moduleType, view: self.popupView, visibilityCallback: self.popupVisibilityCallback)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// load function which call when app start
public func mount() {
guard self.enabled else { return }
self.readers.forEach { (reader: Reader_p) in
reader.initStoreValues(title: self.config.name)
reader.start()
}
self.menuBar.enable()
}
// disable module
public func unmount() {
self.enabled = false
self.available = false
}
// terminate function which call before app termination
public func terminate() {
self.willTerminate()
self.readers.forEach{
$0.stop()
$0.terminate()
}
self.menuBar.disable()
debug("Module terminated", log: self.log)
}
// function to call before module terminate
open func willTerminate() {}
// set module state to enabled
public func enable() {
guard self.available else { return }
self.enabled = true
Store.shared.set(key: "\(self.config.name)_state", value: true)
self.userDefaults?.set(true, forKey: "\(self.config.name)_state")
self.readers.forEach { (reader: Reader_p) in
reader.initStoreValues(title: self.config.name)
reader.start()
}
self.menuBar.enable()
self.window?.setState(self.enabled)
debug("Module enabled", log: self.log)
}
// set module state to disabled
public func disable() {
guard self.available else { return }
self.enabled = false
if !self.pauseState { // omit saving the disable state when toggle by pause, need for resume state restoration
Store.shared.set(key: "\(self.config.name)_state", value: false)
self.userDefaults?.set(false, forKey: "\(self.config.name)_state")
}
self.readers.forEach{ $0.stop() }
self.menuBar.disable()
self.window?.setState(self.enabled)
self.popup?.setIsVisible(false)
debug("Module disabled", log: self.log)
}
public func setReaders(_ list: [Reader_p?]) {
self.readers = list.filter({ $0 != nil }).map({ $0! as Reader_p })
}
// determine if module is available (can be overrided in module)
open func isAvailable() -> Bool { return true }
// load the widget and set up. Calls when module init
private func initWidgets() {
guard self.available else { return }
self.config.availableWidgets.forEach { (widgetType: widget_t) in
if let widget = widgetType.new(
module: self.config.name,
config: self.config.widgetsConfig,
defaultWidget: self.config.defaultWidget
) {
self.menuBar.append(widget)
}
}
}
// call when popup appear/disappear
private func popupVisibilityCallback(_ state: Bool) {
self.readers.filter{ $0.popup || $0.sleep }.forEach { (reader: Reader_p) in
if state {
reader.unlock()
reader.start()
} else {
reader.pause()
reader.lock()
}
}
}
@objc private func listenForWindowOpen(_ notification: Notification) {
guard var state = notification.userInfo?["state"] as? Bool else { return }
if state, let name = notification.userInfo?["module"] as? String, self.config.name != name {
state = false
}
self.readers.filter{ $0.preview || $0.sleep }.forEach { (reader: Reader_p) in
if state {
reader.unlock()
reader.start()
} else {
reader.pause()
reader.lock()
}
}
}
@objc private func listenForPopupToggle(_ notification: Notification) {
guard let popup = self.popup,
let name = notification.userInfo?["module"] as? String,
let buttonOrigin = notification.userInfo?["origin"] as? CGPoint,
let buttonCenter = notification.userInfo?["center"] as? CGFloat,
self.config.name == name else {
return
}
let openedWindows = NSApplication.shared.windows.filter{ $0 is NSPanel }
openedWindows.forEach{ $0.setIsVisible(false) }
var reopen: Bool = false
if let widget = notification.userInfo?["widget"] as? widget_t {
reopen = popup.openedBy != nil && popup.openedBy != widget
popup.openedBy = widget
}
if popup.occlusionState.rawValue == 8192 || reopen {
NSApplication.shared.activate(ignoringOtherApps: true)
popup.contentView?.invalidateIntrinsicContentSize()
let windowCenter = popup.contentView!.intrinsicContentSize.width / 2
var x = buttonOrigin.x - windowCenter + buttonCenter
let y = buttonOrigin.y - popup.contentView!.intrinsicContentSize.height - 3
let maxWidth = NSScreen.screens.map{ $0.frame.width }.reduce(0, +)
if x + popup.contentView!.intrinsicContentSize.width > maxWidth {
x = maxWidth - popup.contentView!.intrinsicContentSize.width - 3
}
popup.setFrameOrigin(NSPoint(x: x, y: y))
popup.setIsVisible(true)
} else {
popup.locked = false
popup.openedBy = nil
popup.setIsVisible(false)
}
}
@objc private func listenForModuleToggle(_ notification: Notification) {
if let name = notification.userInfo?["module"] as? String {
if name == self.config.name {
if let state = notification.userInfo?["state"] as? Bool {
if state && !self.enabled {
self.enable()
} else if !state && self.enabled {
self.disable()
}
} else {
if self.enabled {
self.disable()
} else {
self.enable()
}
}
}
if self.pauseState == true {
self.pauseState = false
NotificationCenter.default.post(name: .pause, object: nil, userInfo: ["state": false])
}
}
}
@objc private func listenForMouseDownInSettings() {
if let popup = self.popup, popup.isVisible && !popup.locked {
self.popup?.setIsVisible(false)
}
}
@objc private func listenForToggleWidget(_ notification: Notification) {
guard let name = notification.userInfo?["module"] as? String, name == self.config.name else {
return
}
let isEmpty = self.menuBar.widgets.filter({ $0.isActive }).isEmpty
if !isEmpty && !self.enabled {
NotificationCenter.default.post(name: .toggleModule, object: nil, userInfo: ["module": self.config.name, "state": true])
}
}
}
================================================
FILE: Kit/module/notifications.swift
================================================
//
// notifications.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 04/12/2023
// Using Swift 5.0
// Running on macOS 14.1
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import UserNotifications
open class NotificationsWrapper: NSStackView {
public let module: String
private var ids: [String: Bool?] = [:]
public init(_ module: ModuleType, _ ids: [String] = []) {
self.module = module.stringValue
super.init(frame: NSRect.zero)
self.initIDs(ids)
self.orientation = .vertical
self.distribution = .gravityAreas
self.translatesAutoresizingMaskIntoConstraints = false
self.spacing = Constants.Settings.margin
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func willTerminate() {
for id in self.ids {
removeNotification(id.key)
}
}
public func initIDs(_ ids: [String]) {
for id in ids {
let notificationID = "Stats_\(self.module)_\(id)"
self.ids[notificationID] = nil
removeNotification(notificationID)
}
}
public func checkDouble(id rid: String, value: Double, threshold: Double, title: String, subtitle: String, less: Bool = false) {
let id = "Stats_\(self.module)_\(rid)"
let first = less ? value > threshold : value < threshold
let second = less ? value <= threshold : value >= threshold
if self.ids[id] != nil, first {
removeNotification(id)
self.ids[id] = nil
}
if self.ids[id] == nil && second {
self.showNotification(id: id, title: title, subtitle: subtitle)
self.ids[id] = true
}
}
public func newNotification(id rid: String, title: String, subtitle: String? = nil) {
let id = "Stats_\(self.module)_\(rid)"
if self.ids[id] != nil {
removeNotification(id)
self.ids[id] = nil
}
self.showNotification(id: id, title: title, subtitle: subtitle)
self.ids[id] = true
}
public func hideNotification(_ rid: String) {
let id = "Stats_\(self.module)_\(rid)"
if self.ids[id] != nil {
removeNotification(id)
self.ids[id] = nil
}
}
private func showNotification(id: String, title: String, subtitle: String? = nil) {
let content = UNMutableNotificationContent()
content.title = title
if let value = subtitle {
content.subtitle = value
}
content.sound = UNNotificationSound.default
let request = UNNotificationRequest(identifier: id, content: content, trigger: nil)
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound]) { _, _ in }
center.add(request) { (error: Error?) in
if let err = error {
print(err)
}
}
}
}
================================================
FILE: Kit/module/popup.swift
================================================
//
// popup.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 11/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public protocol Popup_p: NSView {
var keyboardShortcut: [UInt16] { get }
var sizeCallback: ((NSSize) -> Void)? { get set }
func settings() -> NSView?
func appear()
func disappear()
func setKeyboardShortcut(_ binding: [UInt16])
}
open class PopupWrapper: NSStackView, Popup_p {
public var title: String
public var keyboardShortcut: [UInt16] = []
open var sizeCallback: ((NSSize) -> Void)? = nil
public init(_ typ: ModuleType, frame: NSRect) {
self.title = typ.stringValue
self.keyboardShortcut = Store.shared.array(key: "\(typ.stringValue)_popup_keyboardShortcut", defaultValue: []) as? [UInt16] ?? []
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
open func settings() -> NSView? { return nil }
open func appear() {}
open func disappear() {}
open func setKeyboardShortcut(_ binding: [UInt16]) {
self.keyboardShortcut = binding
Store.shared.set(key: "\(self.title)_popup_keyboardShortcut", value: binding)
}
}
public class PopupWindow: NSWindow, NSWindowDelegate {
private let viewController: PopupViewController
internal var locked: Bool = false
internal var openedBy: widget_t? = nil
public init(title: String, module: ModuleType, view: Popup_p?, visibilityCallback: @escaping (_ state: Bool) -> Void) {
self.viewController = PopupViewController(module: module)
self.viewController.setup(title: title, view: view)
super.init(
contentRect: NSRect(
x: 0,
y: 0,
width: self.viewController.view.frame.width,
height: self.viewController.view.frame.height
),
styleMask: [.titled, .fullSizeContentView],
backing: .buffered,
defer: true
)
self.viewController.visibilityCallback = { [weak self] state in
self?.locked = false
visibilityCallback(state)
}
self.title = title
self.titleVisibility = .hidden
self.contentViewController = self.viewController
self.titlebarAppearsTransparent = true
self.animationBehavior = .default
self.collectionBehavior = .moveToActiveSpace
self.backgroundColor = .clear
self.hasShadow = true
self.setIsVisible(false)
self.delegate = self
}
public func windowWillMove(_ notification: Notification) {
self.viewController.setCloseButton(true)
self.locked = true
}
public func windowDidResignKey(_ notification: Notification) {
if self.locked {
return
}
self.viewController.setCloseButton(false)
self.setIsVisible(false)
}
}
internal class PopupViewController: NSViewController {
fileprivate var visibilityCallback: (_ state: Bool) -> Void = {_ in }
private var popup: PopupView
public init(module: ModuleType) {
self.popup = PopupView(frame: NSRect(
x: 0,
y: 0,
width: Constants.Popup.width + (Constants.Popup.margins * 2),
height: Constants.Popup.height+Constants.Popup.headerHeight
), module: module)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
self.view = self.popup
}
override func viewWillAppear() {
super.viewWillAppear()
self.popup.appear()
self.visibilityCallback(true)
}
override func viewWillDisappear() {
super.viewWillDisappear()
self.popup.disappear()
self.visibilityCallback(false)
}
fileprivate func setup(title: String, view: Popup_p?) {
self.title = title
self.popup.setTitle(title)
self.popup.setView(view)
}
fileprivate func setCloseButton(_ state: Bool) {
self.popup.setCloseButton(state)
}
}
internal class PopupView: NSView {
private var view: Popup_p? = nil
private var foreground: NSVisualEffectView
private var background: NSView
private let header: HeaderView
private let body: NSScrollView
override var intrinsicContentSize: CGSize {
return CGSize(width: self.frame.width, height: self.frame.height)
}
private var windowHeight: CGFloat?
private var containerHeight: CGFloat?
init(frame: NSRect, module: ModuleType) {
self.header = HeaderView(frame: NSRect(
x: 0,
y: frame.height - Constants.Popup.headerHeight,
width: frame.width,
height: Constants.Popup.headerHeight
), module: module)
self.body = NSScrollView(frame: NSRect(
x: Constants.Popup.margins,
y: Constants.Popup.margins,
width: frame.width - Constants.Popup.margins*2,
height: frame.height - self.header.frame.height - Constants.Popup.margins*2
))
self.windowHeight = NSScreen.main?.visibleFrame.height
self.containerHeight = self.body.documentView?.frame.height
self.foreground = NSVisualEffectView(frame: frame)
self.foreground.material = .titlebar
self.foreground.blendingMode = .behindWindow
self.foreground.state = .active
self.foreground.wantsLayer = true
self.foreground.layer?.backgroundColor = NSColor.red.cgColor
self.foreground.layer?.cornerRadius = 6
self.background = NSView(frame: frame)
self.background.wantsLayer = true
self.foreground.addSubview(self.background)
super.init(frame: frame)
self.body.drawsBackground = false
self.body.translatesAutoresizingMaskIntoConstraints = true
self.body.borderType = .noBorder
self.body.hasVerticalScroller = true
self.body.hasHorizontalScroller = false
self.body.autohidesScrollers = true
self.body.horizontalScrollElasticity = .none
self.addSubview(self.foreground, positioned: .below, relativeTo: .none)
self.addSubview(self.header)
self.addSubview(self.body)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateLayer() {
self.background.layer?.backgroundColor = self.isDarkMode ? .clear : NSColor.white.cgColor
}
fileprivate func setView(_ view: Popup_p?) {
self.view = view
var isScrollVisible: Bool = false
var size: NSSize = NSSize(
width: (view?.frame.width ?? Constants.Popup.width) + (Constants.Popup.margins*2),
height: (view?.frame.height ?? 0) + Constants.Popup.headerHeight + (Constants.Popup.margins*2)
)
self.windowHeight = NSScreen.main?.visibleFrame.height // for height recalculate when appear/disappear
self.containerHeight = self.body.documentView?.frame.height // for scroll diff calculation
if let screenHeight = NSScreen.main?.visibleFrame.height, size.height > screenHeight {
size.height = screenHeight - Constants.Widget.height
isScrollVisible = true
}
if let screenWidth = NSScreen.main?.visibleFrame.width, size.width > screenWidth {
size.width = screenWidth
}
self.setFrameSize(size)
self.foreground.setFrameSize(size)
self.background.setFrameSize(size)
self.body.setFrameSize(NSSize(
width: size.width - (Constants.Popup.margins*2) + (isScrollVisible ? 20 : 0),
height: size.height - Constants.Popup.headerHeight - (Constants.Popup.margins*2)
))
self.header.setFrameOrigin(NSPoint(x: 0, y: size.height - Constants.Popup.headerHeight))
if let view = view {
self.body.documentView = view
view.sizeCallback = { [weak self] size in
self?.recalculateHeight(size)
}
}
}
fileprivate func setTitle(_ newTitle: String) {
self.header.setTitle(newTitle)
}
fileprivate func setCloseButton(_ state: Bool) {
self.header.setCloseButton(state)
}
internal func appear() {
self.display()
self.body.subviews.first?.display()
if let screenHeight = NSScreen.main?.visibleFrame.height, let size = self.body.documentView?.frame.size {
if screenHeight != self.windowHeight {
self.recalculateHeight(size)
}
}
if let documentView = self.body.documentView {
documentView.scroll(NSPoint(x: 0, y: documentView.bounds.size.height))
}
self.view?.appear()
}
internal func disappear() {
self.header.setCloseButton(false)
self.view?.disappear()
}
private func recalculateHeight(_ size: NSSize) {
var isScrollVisible: Bool = false
var windowSize: NSSize = NSSize(
width: size.width + (Constants.Popup.margins*2),
height: size.height + Constants.Popup.headerHeight + (Constants.Popup.margins*2)
)
let h0 = self.containerHeight ?? 0
self.windowHeight = NSScreen.main?.visibleFrame.height // for height recalculate when appear/disappear
self.containerHeight = self.body.documentView?.frame.height // for scroll diff calculation
if let screenHeight = NSScreen.main?.visibleFrame.height, windowSize.height > screenHeight {
windowSize.height = screenHeight - Constants.Widget.height
isScrollVisible = true
}
if let screenWidth = NSScreen.main?.visibleFrame.width, windowSize.width > screenWidth {
windowSize.width = screenWidth
}
self.window?.setContentSize(windowSize)
self.foreground.setFrameSize(windowSize)
self.background.setFrameSize(windowSize)
self.body.setFrameSize(NSSize(
width: windowSize.width - (Constants.Popup.margins*2) + (isScrollVisible ? 20 : 0),
height: windowSize.height - Constants.Popup.headerHeight - (Constants.Popup.margins*2)
))
self.header.setFrameOrigin(NSPoint(
x: self.header.frame.origin.x,
y: self.body.frame.height + (Constants.Popup.margins*2)
))
if let documentView = self.body.documentView {
let diff = h0 - (self.body.documentView?.frame.height ?? 0)
documentView.scroll(NSPoint(
x: 0,
y: self.body.documentVisibleRect.origin.y - (diff < 0 ? diff : 0)
))
}
}
}
internal class HeaderView: NSStackView {
private var titleView: NSTextField? = nil
private var activityButton: NSButton?
private var title: String = ""
private var isCloseAction: Bool = false
private let activityMonitor: URL?
private let calendar: URL?
init(frame: NSRect, module: ModuleType) {
self.activityMonitor = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.ActivityMonitor")
self.calendar = URL(fileURLWithPath: "/System/Applications/Calendar.app")
super.init(frame: CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.width, height: frame.height))
self.orientation = .horizontal
self.distribution = .gravityAreas
self.spacing = 0
let activity = NSButtonWithPadding()
activity.frame = CGRect(x: 0, y: 0, width: 24, height: self.frame.height)
activity.horizontalPadding = activity.frame.height - 24
activity.bezelStyle = .regularSquare
activity.translatesAutoresizingMaskIntoConstraints = false
activity.imageScaling = .scaleNone
activity.contentTintColor = .lightGray
activity.isBordered = false
activity.target = self
if module == .clock {
activity.action = #selector(self.openCalendar)
activity.image = Bundle(for: type(of: self)).image(forResource: "calendar")!
activity.toolTip = localizedString("Open Calendar")
} else {
activity.action = #selector(self.openActivityMonitor)
activity.image = Bundle(for: type(of: self)).image(forResource: "chart")!
activity.toolTip = localizedString("Open Activity Monitor")
}
activity.focusRingType = .none
self.activityButton = activity
let title = NSTextField(frame: NSRect(x: 0, y: 0, width: frame.width/2, height: 18))
title.isEditable = false
title.isSelectable = false
title.isBezeled = false
title.wantsLayer = true
title.textColor = .textColor
title.backgroundColor = .clear
title.canDrawSubviewsIntoLayer = true
title.alignment = .center
title.font = NSFont.systemFont(ofSize: 16, weight: .regular)
title.stringValue = ""
self.titleView = title
let settings = NSButtonWithPadding()
settings.frame = CGRect(x: 0, y: 0, width: 24, height: self.frame.height)
settings.horizontalPadding = activity.frame.height - 24
settings.bezelStyle = .regularSquare
settings.translatesAutoresizingMaskIntoConstraints = false
settings.imageScaling = .scaleNone
settings.image = Bundle(for: type(of: self)).image(forResource: "settings")!
settings.contentTintColor = .lightGray
settings.isBordered = false
settings.action = #selector(self.openSettings)
settings.target = self
settings.toolTip = localizedString("Open module settings")
settings.focusRingType = .none
self.addArrangedSubview(activity)
self.addArrangedSubview(title)
self.addArrangedSubview(settings)
NSLayoutConstraint.activate([
title.widthAnchor.constraint(
equalToConstant: self.frame.width - activity.intrinsicContentSize.width - settings.intrinsicContentSize.width
)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func setTitle(_ newTitle: String) {
self.title = newTitle
self.titleView?.stringValue = localizedString(newTitle)
}
@objc func openActivityMonitor() {
guard let app = self.activityMonitor else { return }
NSWorkspace.shared.open([], withApplicationAt: app, configuration: NSWorkspace.OpenConfiguration())
}
@objc func openCalendar() {
guard let app = self.calendar else { return }
NSWorkspace.shared.open([], withApplicationAt: app, configuration: NSWorkspace.OpenConfiguration())
}
@objc func openSettings() {
NotificationCenter.default.post(name: .toggleSettings, object: nil, userInfo: ["module": self.title])
}
@objc private func closePopup() {
self.window?.setIsVisible(false)
self.setCloseButton(false)
return
}
fileprivate func setCloseButton(_ state: Bool) {
if state && !self.isCloseAction {
self.activityButton?.image = Bundle(for: type(of: self)).image(forResource: "close")!
self.activityButton?.toolTip = localizedString("Close")
self.activityButton?.action = #selector(self.closePopup)
self.isCloseAction = true
} else if !state && self.isCloseAction {
self.activityButton?.image = Bundle(for: type(of: self)).image(forResource: "chart")!
self.activityButton?.toolTip = localizedString("Open Activity Monitor")
self.activityButton?.action = #selector(self.openActivityMonitor)
self.isCloseAction = false
}
}
}
================================================
FILE: Kit/module/portal.swift
================================================
//
// portal.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 17/02/2023
// Using Swift 5.0
// Running on macOS 13.2
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public protocol Portal_p: NSView {
var name: String { get }
}
open class PortalWrapper: NSStackView, Portal_p {
public var name: String
private let header: PortalHeader
public init(_ type: ModuleType, height: CGFloat = Constants.Popup.portalHeight) {
self.name = type.stringValue
self.header = PortalHeader(type.stringValue)
super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: height))
self.wantsLayer = true
self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
self.layer?.cornerRadius = 3
self.orientation = .vertical
self.distribution = .fillEqually
self.spacing = Constants.Popup.spacing*2
self.edgeInsets = NSEdgeInsets(
top: Constants.Popup.spacing*2,
left: Constants.Popup.spacing*2,
bottom: Constants.Popup.spacing*2,
right: Constants.Popup.spacing*2
)
self.addArrangedSubview(self.header)
self.load()
self.heightAnchor.constraint(equalToConstant: Constants.Popup.portalHeight).isActive = true
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func updateLayer() {
self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
}
open func load() {
self.addArrangedSubview(NSView())
}
}
public class PortalHeader: NSStackView {
private let name: String
public init(_ name: String) {
self.name = name
super.init(frame: NSRect.zero)
self.heightAnchor.constraint(equalToConstant: 20).isActive = true
let title = NSTextField()
title.isEditable = false
title.isSelectable = false
title.isBezeled = false
title.wantsLayer = true
title.textColor = .textColor
title.backgroundColor = .clear
title.canDrawSubviewsIntoLayer = true
title.alignment = .center
title.font = NSFont.systemFont(ofSize: 12, weight: .regular)
title.stringValue = localizedString(name)
let settings = NSButton()
settings.heightAnchor.constraint(equalToConstant: 18).isActive = true
settings.bezelStyle = .regularSquare
settings.translatesAutoresizingMaskIntoConstraints = false
settings.imageScaling = .scaleProportionallyDown
settings.image = Bundle(for: type(of: self)).image(forResource: "settings")!
settings.contentTintColor = .lightGray
settings.isBordered = false
settings.action = #selector(self.openSettings)
settings.target = self
settings.toolTip = localizedString("Open module settings")
settings.focusRingType = .none
self.addArrangedSubview(title)
self.addArrangedSubview(NSView())
self.addArrangedSubview(settings)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func openSettings() {
self.window?.setIsVisible(false)
NotificationCenter.default.post(name: .toggleSettings, object: nil, userInfo: ["module": self.name])
}
}
================================================
FILE: Kit/module/reader.swift
================================================
//
// reader.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 10/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public protocol Reader_p {
var popup: Bool { get }
var preview: Bool { get }
var sleep: Bool { get }
func setup()
func read()
func terminate()
func start()
func pause()
func stop()
func lock()
func unlock()
func initStoreValues(title: String)
func setInterval(_ value: Int)
func sleepMode(state: Bool)
}
public protocol ReaderInternal_p {
associatedtype T
var value: T? { get }
func read()
}
open class Reader: NSObject, ReaderInternal_p {
public var log: NextLog {
NextLog.shared.copy(category: "\(String(describing: self))")
}
private let valueQueue = DispatchQueue(label: "eu.exelban.readerActiveQueue")
private var _value: T?
public var value: T? {
get { self.valueQueue.sync { self._value } }
set { self.valueQueue.sync { self._value = newValue } }
}
public var name: String {
String(NSStringFromClass(type(of: self)).split(separator: ".").last ?? "unknown")
}
public var interval: Double? = nil
public var defaultInterval: Int = 1
public var optional: Bool = false
public var popup: Bool = false
public var preview: Bool = false
public var sleep: Bool = false
public var alignToSecondBoundary: Bool = false
public var alignOffset: TimeInterval = 0
public var callbackHandler: (T?) -> Void
private let module: ModuleType
private var history: Bool
private var repeatTask: Repeater?
private var locked: Bool = true
private var initlizalized: Bool = false
private let activeQueue = DispatchQueue(label: "eu.exelban.readerActiveQueue")
private var _active: Bool = false
public var active: Bool {
get { self.activeQueue.sync { self._active } }
set { self.activeQueue.sync { self._active = newValue } }
}
private var lastDBWrite: Date? = nil
private var alignWorkItem: DispatchWorkItem?
private let alignQueue = DispatchQueue(label: "eu.exelban.readerAlignQueue")
public init(_ module: ModuleType, popup: Bool = false, preview: Bool = false, history: Bool = false, callback: @escaping (T?) -> Void = {_ in }) {
self.popup = popup
self.preview = preview
self.module = module
self.history = history
self.callbackHandler = callback
super.init()
DB.shared.setup(T.self, "\(module.stringValue)@\(self.name)")
if let lastValue = DB.shared.findOne(T.self, key: "\(module.stringValue)@\(self.name)") {
self.value = lastValue
callback(lastValue)
}
self.setup()
debug("Successfully initialize reader", log: self.log)
}
deinit {
DB.shared.insert(key: "\(self.module.stringValue)@\(self.name)", value: self.value, ts: self.history)
}
public func initStoreValues(title: String) {
guard self.interval == nil else { return }
let updateInterval = Store.shared.int(key: "\(title)_updateInterval", defaultValue: self.defaultInterval)
self.interval = Double(updateInterval)
}
public func callback(_ value: T?) {
let moduleKey = "\(self.module.stringValue)@\(self.name)"
self.value = value
if let value {
self.callbackHandler(value)
Remote.shared.send(key: moduleKey, value: value)
if let ts = self.lastDBWrite, let interval = self.interval, Date().timeIntervalSince(ts) > interval * 10 {
DB.shared.insert(key: moduleKey, value: value, ts: self.history)
self.lastDBWrite = Date()
} else if self.lastDBWrite == nil {
DB.shared.insert(key: moduleKey, value: value, ts: self.history)
self.lastDBWrite = Date()
}
}
}
open func read() {}
open func setup() {}
open func terminate() {}
open func start() {
if (self.popup || self.preview) && self.locked {
DispatchQueue.global(qos: .background).async {
self.read()
}
return
}
if !self.initlizalized {
if self.alignToSecondBoundary {
self.startAlignedRepeater()
} else {
self.startNormalRepeater()
DispatchQueue.global(qos: .background).async { self.read() }
self.repeatTask?.start()
}
self.initlizalized = true
} else {
self.repeatTask?.start()
}
self.active = true
}
open func pause() {
self.alignWorkItem?.cancel()
self.repeatTask?.pause()
self.active = false
}
open func stop() {
self.alignWorkItem?.cancel()
self.repeatTask?.pause()
self.repeatTask = nil
self.active = false
self.initlizalized = false
}
public func setInterval(_ value: Int) {
debug("Set update interval: \(value) sec", log: self.log)
self.interval = Double(value)
if self.alignToSecondBoundary {
self.repeatTask?.pause()
self.repeatTask = nil
self.alignWorkItem?.cancel()
if self.active {
self.startAlignedRepeater()
}
} else {
self.repeatTask?.reset(seconds: value, restart: true)
}
}
public func save(_ value: T) {
DB.shared.insert(key: "\(self.module.stringValue)@\(self.name)", value: value, ts: self.history, force: true)
}
private func delayToNextSecondBoundary() -> TimeInterval {
let now = Date().addingTimeInterval(self.alignOffset)
let fractional = now.timeIntervalSince1970.truncatingRemainder(dividingBy: 1.0)
let baseDelay = (fractional == 0) ? 0.0 : (1.0 - fractional)
let safety: TimeInterval = 0.005 // 5ms past the boundary
return baseDelay + safety
}
private func startNormalRepeater() {
guard let interval = self.interval, self.repeatTask == nil else { return }
if !self.popup && !self.preview {
debug("Set up update interval: \(Int(interval)) sec", log: self.log)
}
self.repeatTask = Repeater(seconds: Int(interval)) { [weak self] in
self?.read()
}
}
private func startAlignedRepeater() {
guard let interval = self.interval, self.repeatTask == nil else { return }
if !self.popup && !self.preview {
debug("Set up update interval: \(Int(interval)) sec (aligned)", log: self.log)
}
let work = DispatchWorkItem { [weak self] in
guard let self else { return }
self.read()
self.repeatTask = Repeater(seconds: Int(interval)) { [weak self] in
self?.read()
}
self.repeatTask?.start()
}
self.alignWorkItem?.cancel()
self.alignWorkItem = work
self.alignQueue.asyncAfter(deadline: .now() + self.delayToNextSecondBoundary(), execute: work)
}
public func sleepMode(state: Bool) {
guard state != self.sleep else { return }
debug("Sleep mode: \(state ? "on" : "off")", log: self.log)
self.sleep = state
if state {
self.pause()
} else {
self.start()
}
}
}
extension Reader: Reader_p {
public func lock() {
self.locked = true
}
public func unlock() {
self.locked = false
}
}
================================================
FILE: Kit/module/widget.swift
================================================
//
// widget.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 10/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public enum widget_t: String {
case unknown = ""
case mini = "mini"
case lineChart = "line_chart"
case barChart = "bar_chart"
case pieChart = "pie_chart"
case networkChart = "network_chart"
case speed = "speed"
case battery = "battery"
case batteryDetails = "battery_details"
case stack = "sensors" // to replace
case memory = "memory"
case label = "label"
case tachometer = "tachometer"
case state = "state"
case text = "text"
public func new(module: String, config: NSDictionary, defaultWidget: widget_t) -> SWidget? {
guard let widgetConfig: NSDictionary = config[self.rawValue] as? NSDictionary else { return nil }
var image: NSImage? = nil
var preview: widget_p? = nil
var item: widget_p? = nil
switch self {
case .mini:
preview = Mini(title: module, config: widgetConfig, preview: true)
item = Mini(title: module, config: widgetConfig, preview: false)
case .lineChart:
preview = LineChart(title: module, config: widgetConfig, preview: true)
item = LineChart(title: module, config: widgetConfig, preview: false)
case .barChart:
preview = BarChart(title: module, config: widgetConfig, preview: true)
item = BarChart(title: module, config: widgetConfig, preview: false)
case .pieChart:
preview = PieChart(title: module, config: widgetConfig, preview: true)
item = PieChart(title: module, config: widgetConfig, preview: false)
case .networkChart:
preview = NetworkChart(title: module, config: widgetConfig, preview: true)
item = NetworkChart(title: module, config: widgetConfig, preview: false)
case .speed:
preview = SpeedWidget(title: module, config: widgetConfig, preview: true)
item = SpeedWidget(title: module, config: widgetConfig, preview: false)
case .battery:
preview = BatteryWidget(title: module, preview: true)
item = BatteryWidget(title: module, preview: false)
case .batteryDetails:
preview = BatteryDetailsWidget(title: module, preview: true)
item = BatteryDetailsWidget(title: module, preview: false)
case .stack:
preview = StackWidget(title: module, config: widgetConfig, preview: true)
item = StackWidget(title: module, config: widgetConfig, preview: false)
case .memory:
preview = MemoryWidget(title: module, config: widgetConfig, preview: true)
item = MemoryWidget(title: module, config: widgetConfig, preview: false)
case .label:
preview = Label(title: module, config: widgetConfig)
item = Label(title: module, config: widgetConfig)
case .tachometer:
preview = Tachometer(title: module, preview: true)
item = Tachometer(title: module, preview: false)
case .state:
preview = StateWidget(title: module, config: widgetConfig, preview: true)
item = StateWidget(title: module, config: widgetConfig, preview: false)
case .text:
preview = TextWidget(title: module, config: widgetConfig, preview: true)
item = TextWidget(title: module, config: widgetConfig, preview: false)
default: break
}
if let view = preview {
var width: CGFloat = view.bounds.width
switch preview {
case is Mini:
if module == "Battery" {
width = view.bounds.width + 3
}
case is BarChart:
if module == "GPU" || module == "RAM" || module == "Disk" || module == "Battery" {
width = 11 + (Constants.Widget.margin.x*2)
} else if module == "Sensors" {
width = 22 + (Constants.Widget.margin.x*2)
} else if module == "CPU" {
width = 30 + (Constants.Widget.margin.x*2)
}
case is StackWidget:
if module == "Sensors" {
width = 25
} else if module == "Clock" {
width = 114
}
case is MemoryWidget:
width = view.bounds.width + 8 + Constants.Widget.spacing*2
case is BatteryWidget:
width = view.bounds.width - 3
default: width = view.bounds.width
}
let r = NSRect(
x: -view.frame.origin.x/2,
y: 0,
width: width - view.frame.origin.x,
height: view.bounds.height
)
image = NSImage(data: view.dataWithPDF(inside: r))
}
if let item = item, let image = image {
return SWidget(self, defaultWidget: defaultWidget, module: module, item: item, image: image)
}
return nil
}
public func name() -> String {
switch self {
case .mini: return localizedString("Mini widget")
case .lineChart: return localizedString("Line chart widget")
case .barChart: return localizedString("Bar chart widget")
case .pieChart: return localizedString("Pie chart widget")
case .networkChart: return localizedString("Network chart widget")
case .speed: return localizedString("Speed widget")
case .battery: return localizedString("Battery widget")
case .batteryDetails: return localizedString("Battery details widget")
case .stack: return localizedString("Stack widget")
case .memory: return localizedString("Memory widget")
case .label: return localizedString("Label widget")
case .tachometer: return localizedString("Tachometer widget")
case .state: return localizedString("State widget")
case .text: return localizedString("Text widget")
default: return ""
}
}
}
extension widget_t: CaseIterable {}
public protocol widget_p: NSView {
var widthHandler: (() -> Void)? { get set }
var onClick: (() -> Void)? { get set }
func settings() -> NSView
}
open class WidgetWrapper: NSView, widget_p {
public var type: widget_t
public var title: String
public var widthHandler: (() -> Void)? = nil
public var onClick: (() -> Void)? = nil
public var shadowSize: CGSize
internal var queue: DispatchQueue
public init(_ type: widget_t, title: String, frame: NSRect) {
self.type = type
self.title = title
self.shadowSize = frame.size
self.queue = DispatchQueue(label: "eu.exelban.Stats.WidgetWrapper.\(type.rawValue).\(title)")
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func setWidth(_ width: CGFloat) {
var newWidth = width
if width == 0 || width == 1 {
newWidth = self.emptyView()
}
guard self.shadowSize.width != newWidth else { return }
self.shadowSize.width = newWidth
DispatchQueue.main.async {
self.setFrameSize(NSSize(width: newWidth, height: self.frame.size.height))
self.widthHandler?()
}
}
public func emptyView() -> CGFloat {
let size: CGFloat = 15
let lineWidth = 1 / (NSScreen.main?.backingScaleFactor ?? 1)
let offset = lineWidth / 2
let width: CGFloat = (Constants.Widget.margin.x*2) + size + (lineWidth*2)
NSColor.textColor.set()
var circle = NSBezierPath()
circle = NSBezierPath(ovalIn: CGRect(x: Constants.Widget.margin.x+offset, y: 1+offset, width: size, height: size))
circle.stroke()
circle.lineWidth = lineWidth
let line = NSBezierPath()
line.move(to: NSPoint(x: 3, y: 3.5))
line.line(to: NSPoint(x: 13.5, y: 14))
line.lineWidth = lineWidth
line.stroke()
return width
}
public func redraw() {
DispatchQueue.main.async { [weak self] in
self?.display()
}
}
open func settings() -> NSView { return NSView() }
open override func mouseDown(with event: NSEvent) {
if let f = self.onClick {
f()
return
}
super.mouseDown(with: event)
}
}
public class SWidget {
public let type: widget_t
public let defaultWidget: widget_t
public let module: String
public let image: NSImage
public var item: widget_p
public var isActive: Bool {
get { self.list.contains{ $0 == self.type } }
set {
if newValue {
self.list.append(self.type)
} else {
self.list.removeAll{ $0 == self.type }
}
}
}
public var toggleCallback: ((widget_t, Bool) -> Void)? = nil
public var sizeCallback: (() -> Void)? = nil
public var log: NextLog {
NextLog.shared.copy(category: self.module)
}
public var position: Int {
get { Store.shared.int(key: "\(self.module)_\(self.type)_position", defaultValue: 0) }
set { Store.shared.set(key: "\(self.module)_\(self.type)_position", value: newValue) }
}
private var list: [widget_t] {
get {
let string = Store.shared.string(key: "\(self.module)_widget", defaultValue: self.defaultWidget.rawValue)
return string.split(separator: ",").map{ (widget_t(rawValue: String($0)) ?? .unknown)}
}
set { Store.shared.set(key: "\(self.module)_widget", value: newValue.map{ $0.rawValue }.joined(separator: ",")) }
}
private var menuBarItem: NSStatusItem? = nil
private var originX: CGFloat
public init(_ type: widget_t, defaultWidget: widget_t, module: String, item: widget_p, image: NSImage) {
self.type = type
self.module = module
self.item = item
self.defaultWidget = defaultWidget
self.image = image
self.originX = item.frame.origin.x
self.item.widthHandler = { [weak self] in
self?.sizeCallback?()
if let s = self, let item = s.menuBarItem, let width: CGFloat = self?.item.frame.width, item.length != width {
item.length = width
}
}
self.item.identifier = NSUserInterfaceItemIdentifier(self.type.rawValue)
}
// show item in the menu bar
public func enable() {
guard self.isActive else { return }
self.toggleCallback?(self.type, true)
debug("widget \(self.type.rawValue) enabled", log: self.log)
}
// remove item from the menu bar
public func disable() {
self.toggleCallback?(self.type, false)
debug("widget \(self.type.rawValue) disabled", log: self.log)
}
// toggle the widget
public func toggle(_ state: Bool? = nil) {
var newState: Bool = !self.isActive
if let state = state {
newState = state
}
if self.isActive == newState {
return
}
self.isActive = newState
if !self.isActive {
self.disable()
} else {
self.enable()
}
NotificationCenter.default.post(name: .toggleWidget, object: nil, userInfo: ["module": self.module])
}
public func setMenuBarItem(state: Bool) {
if state {
DispatchQueue.main.async(execute: {
self.menuBarItem = NSStatusBar.system.statusItem(withLength: self.item.frame.width)
DispatchQueue.main.async(execute: {
self.menuBarItem?.autosaveName = "\(self.module)_\(self.type.rawValue)"
})
if self.item.frame.origin.x != self.originX {
self.item.setFrameOrigin(NSPoint(x: self.originX, y: self.item.frame.origin.y))
}
self.menuBarItem?.button?.addSubview(self.item)
self.menuBarItem?.button?.image = NSImage()
self.menuBarItem?.button?.toolTip = "\(localizedString(self.module)): \(self.type.name())"
if let item = self.menuBarItem, !item.isVisible {
self.menuBarItem?.isVisible = true
}
self.menuBarItem?.button?.target = self
self.menuBarItem?.button?.action = #selector(self.togglePopup)
self.menuBarItem?.button?.sendAction(on: [.leftMouseDown, .rightMouseDown])
})
} else if let item = self.menuBarItem {
NSStatusBar.system.removeStatusItem(item)
self.menuBarItem = nil
}
}
@objc private func togglePopup() {
if let item = self.menuBarItem, let window = item.button?.window {
NotificationCenter.default.post(name: .togglePopup, object: nil, userInfo: [
"module": self.module,
"widget": self.type,
"origin": window.frame.origin,
"center": window.frame.width/2
])
}
}
}
public class MenuBar {
public var callback: (() -> Void)? = nil
public var widgets: [SWidget] = []
private var moduleName: String
private var menuBarItem: NSStatusItem? = nil
private var queue: DispatchQueue
private var combinedModules: Bool {
Store.shared.bool(key: "CombinedModules", defaultValue: false)
}
public var view: MenuBarView = MenuBarView()
public var oneView: Bool = false
public var activeWidgets: [SWidget] {
self.widgets.filter({ $0.isActive })
}
public var sortedWidgets: [widget_t] {
get {
var list: [widget_t: Int] = [:]
self.activeWidgets.forEach { (w: SWidget) in
list[w.type] = w.position
}
return list.sorted { $0.1 < $1.1 }.map{ $0.key }
}
}
private var _active: Bool = false
public var active: Bool {
get { self.queue.sync { self._active } }
set { self.queue.sync { self._active = newValue } }
}
init(moduleName: String) {
self.moduleName = moduleName
self.queue = DispatchQueue(label: "eu.exelban.Stats.MenuBar.\(moduleName)")
self.oneView = Store.shared.bool(key: "\(self.moduleName)_oneView", defaultValue: self.oneView)
self.view.identifier = NSUserInterfaceItemIdentifier(rawValue: moduleName)
if self.combinedModules {
self.oneView = true
} else {
self.setupMenuBarItem(self.oneView)
}
NotificationCenter.default.addObserver(self, selector: #selector(listenForOneView), name: .toggleOneView, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(listenForWidgetRearrange), name: .widgetRearrange, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self, name: .toggleOneView, object: nil)
NotificationCenter.default.removeObserver(self, name: .widgetRearrange, object: nil)
}
public func append(_ widget: SWidget) {
widget.toggleCallback = { [weak self] (type, state) in
if let s = self, s.oneView {
if state, let w = s.activeWidgets.first(where: { $0.type == type }) {
DispatchQueue.main.async(execute: {
s.recalculateWidth()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
s.view.addWidget(w.item)
s.view.recalculate(s.sortedWidgets)
}
})
} else {
DispatchQueue.main.async(execute: {
s.view.removeWidget(type: type)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
s.recalculateWidth()
s.view.recalculate(s.sortedWidgets)
}
})
}
} else {
widget.setMenuBarItem(state: state)
}
}
widget.sizeCallback = { [weak self] in
self?.recalculateWidth()
}
self.widgets.append(widget)
}
public func enable() {
if self.oneView && !self.combinedModules {
self.setupMenuBarItem(true)
}
self.active = true
self.widgets.forEach{ $0.enable() }
self.callback?()
}
public func disable() {
self.widgets.forEach{ $0.disable() }
self.active = false
if self.oneView {
self.setupMenuBarItem(false)
}
self.callback?()
}
private func setupMenuBarItem(_ state: Bool) {
DispatchQueue.main.async(execute: {
if state && self.active {
restoreNSStatusItemPosition(id: self.moduleName)
self.menuBarItem = NSStatusBar.system.statusItem(withLength: 0)
DispatchQueue.main.async(execute: {
self.menuBarItem?.autosaveName = self.moduleName
})
self.menuBarItem?.isVisible = true
self.menuBarItem?.button?.addSubview(self.view)
self.menuBarItem?.button?.image = NSImage()
self.menuBarItem?.button?.toolTip = "\(localizedString(self.moduleName))"
self.menuBarItem?.button?.target = self
self.menuBarItem?.button?.action = #selector(self.togglePopup)
self.menuBarItem?.button?.sendAction(on: [.leftMouseDown, .rightMouseDown])
self.recalculateWidth()
} else if let item = self.menuBarItem {
saveNSStatusItemPosition(id: self.moduleName)
NSStatusBar.system.removeStatusItem(item)
self.menuBarItem = nil
}
})
}
private func recalculateWidth() {
guard self.oneView, self.active else { return }
let w = self.activeWidgets.map({ $0.item.frame.width }).reduce(0, +) +
(CGFloat(self.activeWidgets.count - 1) * Constants.Widget.spacing) +
Constants.Widget.spacing * 2
self.menuBarItem?.length = w
self.view.setFrameOrigin(NSPoint(x: 0, y: 0))
self.view.setFrameSize(NSSize(width: w, height: Constants.Widget.height))
self.view.recalculate(self.sortedWidgets)
self.callback?()
}
@objc private func togglePopup() {
if let item = self.menuBarItem, let window = item.button?.window {
NotificationCenter.default.post(name: .togglePopup, object: nil, userInfo: [
"module": self.moduleName,
"origin": window.frame.origin,
"center": window.frame.width/2
])
}
}
@objc private func listenForOneView(_ notification: Notification) {
if notification.userInfo?["module"] as? String == nil {
self.toggleOneView()
} else if let name = notification.userInfo?["module"] as? String, name == self.moduleName, self.active {
self.toggleOneView()
}
}
private func toggleOneView() {
self.activeWidgets.forEach { (w: SWidget) in
w.disable()
}
if self.combinedModules {
self.oneView = true
self.setupMenuBarItem(false)
} else if self.active {
self.oneView = Store.shared.bool(key: "\(self.moduleName)_oneView", defaultValue: self.oneView)
self.setupMenuBarItem(self.oneView)
}
self.activeWidgets.forEach { (w: SWidget) in
w.enable()
}
}
@objc private func listenForWidgetRearrange(_ notification: Notification) {
guard let name = notification.userInfo?["module"] as? String, name == self.moduleName else {
return
}
self.view.recalculate(self.sortedWidgets)
}
}
public class MenuBarView: NSView {
init() {
super.init(frame: NSRect.zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func addWidget(_ view: NSView) {
self.addSubview(view)
}
public func removeWidget(type: widget_t) {
if let view = self.subviews.first(where: { $0.identifier == NSUserInterfaceItemIdentifier(type.rawValue) }) {
view.removeFromSuperview()
}
}
public func recalculate(_ list: [widget_t] = []) {
var x: CGFloat = Constants.Widget.spacing
list.forEach { (type: widget_t) in
if let view = self.subviews.first(where: { $0.identifier == NSUserInterfaceItemIdentifier(type.rawValue) }) {
view.setFrameOrigin(NSPoint(x: x, y: view.frame.origin.y))
x = view.frame.origin.x + view.frame.width + Constants.Widget.spacing
}
}
}
}
================================================
FILE: Kit/module/window.swift
================================================
//
// settings.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 13/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public protocol Settings_v: NSView {
func load(widgets: [widget_t])
}
public protocol Preview_v: NSView {}
open class Window: NSStackView {
private var config: UnsafePointer
private var widgets: [SWidget]
private var segmentedControl: NSSegmentedControl?
private var tabView: NSTabView?
private var modulePreview: Preview_v?
private var moduleSettings: Settings_v?
private var popupSettings: Popup_p?
private var notificationsSettings: NotificationsWrapper?
private var moduleSettingsContainer: NSStackView?
private var widgetSettingsContainer: NSStackView?
private var popupSettingsContainer: NSStackView?
private var notificationsSettingsContainer: NSStackView?
private var enableControl: NSControl?
private var oneViewBtn: NSSwitch?
private let noWidgetsView: EmptyView = EmptyView(msg: localizedString("No available widgets to configure"))
private let noPopupSettingsView: EmptyView = EmptyView(msg: localizedString("No options to configure for the popup in this module"))
private let noNotificationsView: EmptyView = EmptyView(msg: localizedString("No notifications available in this module"))
private var globalOneView: Bool {
Store.shared.bool(key: "OneView", defaultValue: false)
}
private var oneViewState: Bool {
get { Store.shared.bool(key: "\(self.config.pointee.name)_oneView", defaultValue: false) }
set { Store.shared.set(key: "\(self.config.pointee.name)_oneView", value: newValue) }
}
private var isPreviewAvailable: Bool
private var isPopupSettingsAvailable: Bool
private var isNotificationsSettingsAvailable: Bool
private var previewView: NSView? = nil
private var settingsView: NSView? = nil
init(
config: UnsafePointer,
widgets: UnsafeMutablePointer<[SWidget]>,
modulePreview: Preview_v?,
moduleSettings: Settings_v?,
popupSettings: Popup_p?,
notificationsSettings: NotificationsWrapper?
) {
self.config = config
self.widgets = widgets.pointee
self.modulePreview = modulePreview
self.moduleSettings = moduleSettings
self.popupSettings = popupSettings
self.notificationsSettings = notificationsSettings
self.isPreviewAvailable = config.pointee.previewConfig["enabled"] as? Bool ?? false
self.isPopupSettingsAvailable = config.pointee.settingsConfig["popup"] as? Bool ?? false
self.isNotificationsSettingsAvailable = config.pointee.settingsConfig["notifications"] as? Bool ?? false
super.init(frame: NSRect.zero)
self.orientation = .vertical
self.alignment = .width
self.distribution = .fill
self.spacing = Constants.Settings.margin
self.edgeInsets = NSEdgeInsets(
top: 0,
left: Constants.Settings.margin,
bottom: Constants.Settings.margin,
right: Constants.Settings.margin
)
let settingsView = self.settings()
self.settingsView = settingsView
let previewView = self.preview()
self.previewView = previewView
self.addArrangedSubview(settingsView)
self.addArrangedSubview(previewView)
NotificationCenter.default.addObserver(self, selector: #selector(listenForOneView), name: .toggleOneView, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(listenForToggleView), name: .togglePreview, object: nil)
self.segmentedControl?.widthAnchor.constraint(equalTo: self.widthAnchor, constant: -(Constants.Settings.margin*2)).isActive = true
if self.isPreviewAvailable {
self.toggleView()
}
}
deinit {
NotificationCenter.default.removeObserver(self, name: .toggleOneView, object: nil)
NotificationCenter.default.removeObserver(self, name: .togglePreview, object: nil)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func setState(_ newState: Bool) {
toggleNSControlState(self.enableControl, state: newState ? .on : .off)
}
private func preview() -> NSView {
let container = NSStackView()
container.isHidden = true
container.orientation = .vertical
var view: NSView = EmptyView(height: 0, msg: localizedString("Preview is not available for that module"))
if self.isPreviewAvailable, let v = self.modulePreview {
view = v
}
container.addArrangedSubview(view)
return container
}
private func settings() -> NSView {
let view = NSStackView()
view.orientation = .vertical
view.spacing = Constants.Settings.margin
var labels: [String] = [
localizedString("Module"),
localizedString("Widgets")
]
if self.isPopupSettingsAvailable {
labels.append(localizedString("Popup"))
}
if self.isNotificationsSettingsAvailable {
labels.append(localizedString("Notifications"))
}
let segmentedControl = NSSegmentedControl(labels: labels, trackingMode: .selectOne, target: self, action: #selector(self.switchTabs))
segmentedControl.segmentDistribution = .fillEqually
segmentedControl.selectSegment(withTag: 0)
self.segmentedControl = segmentedControl
let tabView = NSTabView()
tabView.tabViewType = .noTabsNoBorder
tabView.tabViewBorderType = .none
tabView.drawsBackground = false
self.tabView = tabView
let moduleTab: NSTabViewItem = NSTabViewItem()
moduleTab.label = localizedString("Module")
moduleTab.view = {
let container = NSStackView()
container.translatesAutoresizingMaskIntoConstraints = false
let scrollView = ScrollableStackView()
self.moduleSettingsContainer = scrollView.stackView
self.loadModuleSettings()
container.addArrangedSubview(scrollView)
return container
}()
tabView.addTabViewItem(moduleTab)
let widgetTab: NSTabViewItem = NSTabViewItem()
widgetTab.label = localizedString("Widgets")
widgetTab.view = {
let view = ScrollableStackView(frame: tabView.frame)
view.stackView.spacing = 0
self.widgetSettingsContainer = view.stackView
self.loadWidgetSettings()
return view
}()
tabView.addTabViewItem(widgetTab)
if self.isPopupSettingsAvailable {
let popupTab: NSTabViewItem = NSTabViewItem()
popupTab.label = localizedString("Popup")
popupTab.view = {
let view = ScrollableStackView(frame: tabView.frame)
view.stackView.spacing = 0
self.popupSettingsContainer = view.stackView
self.loadPopupSettings()
return view
}()
tabView.addTabViewItem(popupTab)
}
if self.isNotificationsSettingsAvailable {
let notificationsTab: NSTabViewItem = NSTabViewItem()
notificationsTab.label = localizedString("Notifications")
notificationsTab.view = {
let view = ScrollableStackView(frame: tabView.frame)
view.stackView.spacing = 0
self.notificationsSettingsContainer = view.stackView
self.loadNotificationsSettings()
return view
}()
tabView.addTabViewItem(notificationsTab)
}
let widgetSelector = WidgetSelectorView(module: self.config.pointee.name, widgets: self.widgets, stateCallback: self.loadWidget)
view.addArrangedSubview(widgetSelector)
view.addArrangedSubview(segmentedControl)
view.addArrangedSubview(tabView)
return view
}
private func loadWidget() {
self.loadModuleSettings()
self.loadWidgetSettings()
}
private func loadModuleSettings() {
self.moduleSettingsContainer?.subviews.forEach{ $0.removeFromSuperview() }
if let settingsView = self.moduleSettings {
settingsView.load(widgets: self.widgets.filter{ $0.isActive }.map{ $0.type })
self.moduleSettingsContainer?.addArrangedSubview(settingsView)
} else {
self.moduleSettingsContainer?.addArrangedSubview(NSView())
}
}
private func loadWidgetSettings() {
self.widgetSettingsContainer?.subviews.forEach{ $0.removeFromSuperview() }
let list = self.widgets.filter({ $0.isActive && $0.type != .label })
guard !list.isEmpty else {
self.widgetSettingsContainer?.addArrangedSubview(self.noWidgetsView)
return
}
if self.widgets.filter({ $0.isActive }).count > 1 {
let btn = switchView(
action: #selector(self.toggleOneView),
state: self.oneViewState
)
self.oneViewBtn = btn
self.widgetSettingsContainer?.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Merge widgets"), component: btn)
]))
}
for i in 0...list.count - 1 {
self.widgetSettingsContainer?.addArrangedSubview(WidgetSettings(
title: list[i].type.name(),
image: list[i].image,
settingsView: list[i].item.settings()
))
}
}
private func loadPopupSettings() {
self.popupSettingsContainer?.subviews.forEach{ $0.removeFromSuperview() }
if let settingsView = self.popupSettings, let view = settingsView.settings() {
self.popupSettingsContainer?.addArrangedSubview(view)
} else {
self.popupSettingsContainer?.addArrangedSubview(self.noPopupSettingsView)
}
}
private func loadNotificationsSettings() {
self.notificationsSettingsContainer?.subviews.forEach{ $0.removeFromSuperview() }
if let notificationsView = self.notificationsSettings {
self.notificationsSettingsContainer?.addArrangedSubview(notificationsView)
} else {
self.notificationsSettingsContainer?.addArrangedSubview(self.noNotificationsView)
}
}
@objc func switchTabs(sender: NSSegmentedControl) {
self.tabView?.selectTabViewItem(at: sender.selectedSegment)
}
@objc private func toggleOneView(_ sender: NSControl) {
guard !self.globalOneView else { return }
self.oneViewState = controlState(sender)
NotificationCenter.default.post(name: .toggleOneView, object: nil, userInfo: ["module": self.config.pointee.name])
}
@objc private func listenForOneView(_ notification: Notification) {
guard notification.userInfo?["module"] == nil else { return }
self.oneViewBtn?.isEnabled = !self.globalOneView
if !self.globalOneView {
self.oneViewBtn?.state = self.oneViewState ? .on : .off
}
}
@objc private func listenForToggleView(_ notification: Notification) {
guard let moduleName = notification.userInfo?["module"], self.config.pointee.name == moduleName as? String else { return }
self.toggleView()
}
private func toggleView() {
guard let preview = self.previewView, let settings = self.settingsView else { return }
preview.isHidden = !preview.isHidden
settings.isHidden = !settings.isHidden
}
}
private class WidgetSelectorView: NSStackView {
private var module: String
private var stateCallback: () -> Void = {}
private var moved: Bool = false
private var background: NSVisualEffectView = {
let view = NSVisualEffectView(frame: .zero)
view.blendingMode = .withinWindow
view.translatesAutoresizingMaskIntoConstraints = false
if #available(macOS 26.0, *) {
view.material = .titlebar
} else {
view.material = .contentBackground
}
view.state = .active
view.wantsLayer = true
view.layer?.cornerRadius = 5
return view
}()
private var separator: NSView?
fileprivate init(module: String, widgets: [SWidget], stateCallback: @escaping () -> Void) {
self.module = module
self.stateCallback = stateCallback
super.init(frame: NSRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.edgeInsets = NSEdgeInsets(
top: Constants.Settings.margin,
left: Constants.Settings.margin,
bottom: Constants.Settings.margin,
right: Constants.Settings.margin
)
self.spacing = Constants.Settings.margin
var active: [WidgetPreview] = []
var inactive: [WidgetPreview] = []
if !widgets.isEmpty {
for i in 0...widgets.count - 1 {
let widget = widgets[i]
let preview = WidgetPreview(
id: "\(widget.module)_\(widget.type)",
type: widget.type,
image: widget.image,
isActive: widget.isActive, { [weak self] state in
widget.toggle(state)
self?.stateCallback()
})
if widget.isActive {
active.append(preview)
} else {
inactive.append(preview)
}
}
}
active.sort(by: { $0.position < $1.position })
inactive.sort(by: { $0.position < $1.position })
active.forEach { (widget: WidgetPreview) in
self.addArrangedSubview(widget)
}
let separator = NSView()
separator.identifier = NSUserInterfaceItemIdentifier(rawValue: "separator")
separator.wantsLayer = true
separator.layer?.backgroundColor = NSColor.separatorColor.withAlphaComponent(isDarkMode ? 0.35 : 0.15).cgColor
self.addArrangedSubview(separator)
self.separator = separator
inactive.forEach { (widget: WidgetPreview) in
self.addArrangedSubview(widget)
}
self.addArrangedSubview(NSView())
self.addSubview(self.background, positioned: .below, relativeTo: .none)
NSLayoutConstraint.activate([
self.heightAnchor.constraint(equalToConstant: Constants.Widget.height + (Constants.Settings.margin*2)),
separator.widthAnchor.constraint(equalToConstant: 1),
separator.heightAnchor.constraint(equalTo: self.heightAnchor, constant: -18),
self.background.widthAnchor.constraint(equalTo: self.widthAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateLayer() {
self.background.setFrameSize(self.frame.size)
self.separator?.layer?.backgroundColor = NSColor.separatorColor.withAlphaComponent(isDarkMode ? 0.35 : 0.15).cgColor
}
override func mouseUp(with event: NSEvent) {
guard !self.moved else { return }
let location = convert(event.locationInWindow, from: nil)
guard let targetIdx = self.views.firstIndex(where: { $0.hitTest(location) != nil }),
let separatorIdx = self.views.firstIndex(where: { $0.identifier?.rawValue == "separator" }),
self.views[targetIdx].identifier != nil, let view = self.views[targetIdx] as? WidgetPreview else {
super.mouseUp(with: event)
return
}
let newIdx = separatorIdx
view.removeFromSuperviewWithoutNeedingDisplay()
self.insertArrangedSubview(view, at: newIdx)
self.layoutSubtreeIfNeeded()
for (i, v) in self.views(in: .leading).compactMap({$0 as? WidgetPreview}).enumerated() {
v.position = i
}
view.status(separatorIdx < targetIdx)
NotificationCenter.default.post(name: .widgetRearrange, object: nil, userInfo: ["module": self.module])
}
override func mouseDown(with event: NSEvent) {
self.moved = false
let location = convert(event.locationInWindow, from: nil)
guard let targetIdx = self.views.firstIndex(where: { $0.hitTest(location) != nil }),
let separatorIdx = self.views.firstIndex(where: { $0.identifier?.rawValue == "separator" }),
let window = self.window, self.views[targetIdx].identifier != nil else {
super.mouseDragged(with: event)
return
}
let view = self.views[targetIdx]
let copy = ViewCopy(view)
copy.zPosition = 2
copy.transform = CATransform3DMakeScale(0.9, 0.9, 1)
// hide the original view, show the copy
view.subviews.forEach({ $0.isHidden = true })
self.layer?.addSublayer(copy)
// hide the copy view, show the original
defer {
copy.removeFromSuperlayer()
view.subviews.forEach({ $0.isHidden = false })
}
var newIdx = -1
let originCenter = view.frame.midX
let originX = view.frame.origin.x
let p0 = convert(event.locationInWindow, from: nil).x
window.trackEvents(matching: [.leftMouseDragged, .leftMouseUp], timeout: 1e6, mode: .eventTracking) { event, stop in
guard let event = event else {
stop.pointee = true
return
}
if event.type == .leftMouseDragged {
let p1 = self.convert(event.locationInWindow, from: nil).x
let diff = p1 - p0
CATransaction.begin()
CATransaction.setDisableActions(true)
copy.frame.origin.x = originX + diff
CATransaction.commit()
let reordered = self.views.map{
(view: $0, x: $0 !== view ? $0.frame.midX : originCenter + diff)
}.sorted{ $0.x < $1.x }.map { $0.view }
guard let nextIndex = reordered.firstIndex(of: view),
let prevIndex = self.views.firstIndex(of: view) else {
stop.pointee = true
return
}
if nextIndex != prevIndex && nextIndex != self.views.count - 1 {
newIdx = nextIndex
view.removeFromSuperviewWithoutNeedingDisplay()
self.insertArrangedSubview(view, at: newIdx)
self.layoutSubtreeIfNeeded()
for (i, v) in self.views(in: .leading).compactMap({$0 as? WidgetPreview}).enumerated() {
v.position = i
}
}
self.moved = abs(diff) > 1
} else {
if newIdx != -1, let view = self.views[newIdx] as? WidgetPreview {
if newIdx <= separatorIdx && newIdx < targetIdx {
view.status(true)
} else if newIdx >= separatorIdx {
view.status(false)
}
NotificationCenter.default.post(name: .widgetRearrange, object: nil, userInfo: ["module": self.module])
}
view.mouseUp(with: event)
stop.pointee = true
self.moved = true
}
}
}
}
private class WidgetPreview: NSStackView {
private var stateCallback: (_ status: Bool) -> Void = {_ in }
private let rgbImage: NSImage
private let grayImage: NSImage
private let imageView: NSImageView
private var state: Bool
private let id: String
fileprivate var position: Int {
get { Store.shared.int(key: "\(self.id)_position", defaultValue: 0) }
set { Store.shared.set(key: "\(self.id)_position", value: newValue) }
}
fileprivate init(id: String, type: widget_t, image: NSImage, isActive: Bool, _ callback: @escaping (_ status: Bool) -> Void) {
self.id = id
self.stateCallback = callback
self.rgbImage = image
self.grayImage = grayscaleImage(image) ?? image
self.imageView = NSImageView(frame: NSRect(origin: .zero, size: image.size))
self.state = isActive
super.init(frame: NSRect(x: 0, y: 0, width: 0, height: Constants.Widget.height))
self.wantsLayer = true
self.layer?.cornerRadius = 2
self.layer?.borderColor = NSColor(red: 221/255, green: 221/255, blue: 221/255, alpha: 1).cgColor
self.layer?.borderWidth = 1
self.layer?.backgroundColor = NSColor.white.cgColor
self.identifier = NSUserInterfaceItemIdentifier(rawValue: type.rawValue)
self.setAccessibilityElement(true)
self.toolTip = type.name()
self.orientation = .vertical
self.distribution = .fill
self.alignment = .centerY
self.spacing = 0
self.imageView.image = isActive ? self.rgbImage : self.grayImage
self.imageView.alphaValue = isActive ? 1 : 0.75
self.addArrangedSubview(self.imageView)
self.addTrackingArea(NSTrackingArea(
rect: NSRect(
x: Constants.Widget.spacing,
y: 0,
width: self.imageView.frame.width + Constants.Widget.spacing*2,
height: self.frame.height
),
options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp],
owner: self,
userInfo: nil
))
NSLayoutConstraint.activate([
self.widthAnchor.constraint(equalToConstant: self.imageView.frame.width + Constants.Widget.spacing*2),
self.heightAnchor.constraint(equalToConstant: self.frame.height)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func status(_ newState: Bool) {
self.state = newState
self.stateCallback(newState)
self.imageView.image = newState ? self.rgbImage : self.grayImage
self.imageView.alphaValue = newState ? 1 : 0.8
}
override func mouseEntered(with: NSEvent) {
NSCursor.pointingHand.set()
if !self.state {
self.imageView.image = self.rgbImage
self.imageView.alphaValue = 0.9
}
}
override func mouseExited(with: NSEvent) {
NSCursor.arrow.set()
if !self.state {
self.imageView.image = self.grayImage
self.imageView.alphaValue = 0.8
}
}
}
private class WidgetSettings: NSStackView {
fileprivate init(title: String, image: NSImage, settingsView: NSView) {
super.init(frame: NSRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.orientation = .vertical
self.spacing = 0
self.addArrangedSubview(self.header(title, image))
self.addArrangedSubview(settingsView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func header(_ title: String, _ image: NSImage) -> NSView {
let container = NSStackView()
container.translatesAutoresizingMaskIntoConstraints = false
container.orientation = .horizontal
container.edgeInsets = NSEdgeInsets(
top: 6,
left: 0,
bottom: 6,
right: 0
)
container.spacing = 0
container.distribution = .equalCentering
let content = NSStackView()
content.translatesAutoresizingMaskIntoConstraints = false
content.orientation = .vertical
content.distribution = .fill
content.spacing = 0
let title: NSTextField = LabelField(frame: NSRect(x: 0, y: 0, width: 0, height: 0), title)
title.font = NSFont.systemFont(ofSize: 13, weight: .regular)
title.textColor = .textColor
let imageContainer = NSStackView()
imageContainer.orientation = .vertical
imageContainer.spacing = 0
imageContainer.wantsLayer = true
imageContainer.layer?.backgroundColor = NSColor.white.cgColor
imageContainer.layer?.cornerRadius = 2
imageContainer.edgeInsets = NSEdgeInsets(
top: 2,
left: 2,
bottom: 2,
right: 2
)
let imageView = NSImageView(frame: NSRect(origin: .zero, size: image.size))
imageView.image = image
imageContainer.addArrangedSubview(imageView)
content.addArrangedSubview(imageContainer)
content.addArrangedSubview(title)
container.addArrangedSubview(NSView())
container.addArrangedSubview(content)
container.addArrangedSubview(NSView())
return container
}
}
================================================
FILE: Kit/plugins/Charts.swift
================================================
//
// Chart.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 17/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public struct circle_segment {
public let value: Double
public var color: NSColor
public init(value: Double, color: NSColor) {
self.value = value
self.color = color
}
}
internal func scaleValue(scale: Scale = .linear, value: Double, maxValue: Double, zeroValue: Double, maxHeight: CGFloat, limit: Double) -> CGFloat {
var value = value
if scale == .none && value > 1 && maxValue != 0 {
value /= maxValue
}
var localMaxValue = maxValue
var y = value * maxHeight
switch scale {
case .square:
if value > 0 {
value = sqrt(value)
}
if localMaxValue > 0 {
localMaxValue = sqrt(maxValue)
}
case .cube:
if value > 0 {
value = cbrt(value)
}
if localMaxValue > 0 {
localMaxValue = cbrt(maxValue)
}
case .logarithmic:
if value > 0 {
value = log(value/zeroValue)
}
if localMaxValue > 0 {
localMaxValue = log(maxValue/zeroValue)
}
case .fixed:
if value > limit {
value = limit
}
localMaxValue = limit
default: break
}
if value < 0 {
value = 0
}
if localMaxValue <= 0 {
localMaxValue = 1
}
if scale != .none {
y = (maxHeight * value)/localMaxValue
}
return y
}
private func drawToolTip(_ frame: NSRect, _ point: CGPoint, _ size: CGSize, value: String, subtitle: String? = nil) {
guard !value.isEmpty else { return }
let style = NSMutableParagraphStyle()
style.alignment = .left
var position: CGPoint = point
let textHeight: CGFloat = subtitle != nil ? 22 : 12
let valueOffset: CGFloat = subtitle != nil ? 11 : 1
position.x = max(frame.origin.x, min(position.x, frame.origin.x + frame.size.width - size.width))
position.y = max(frame.origin.y, min(position.y, frame.origin.y + frame.size.height - textHeight - 2))
if position.x + size.width > frame.size.width+frame.origin.x {
position.x = point.x - size.width
style.alignment = .right
}
if position.y + textHeight > size.height {
position.y = point.y - textHeight - 20
}
if position.y < 2 {
position.y = 2
}
let box = NSBezierPath(roundedRect: NSRect(x: position.x-3, y: position.y-2, width: size.width, height: textHeight+2), xRadius: 2, yRadius: 2)
NSColor.gray.setStroke()
box.stroke()
(isDarkMode ? NSColor.black : NSColor.white).withAlphaComponent(0.8).setFill()
box.fill()
var attributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 12, weight: .regular),
NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor
]
var rect = CGRect(x: position.x, y: position.y+valueOffset, width: size.width, height: 12)
var str = NSAttributedString.init(string: value, attributes: attributes)
str.draw(with: rect)
if let subtitle {
attributes[NSAttributedString.Key.font] = NSFont.systemFont(ofSize: 9, weight: .medium)
attributes[NSAttributedString.Key.foregroundColor] = (isDarkMode ? NSColor.white : NSColor.textColor).withAlphaComponent(0.7)
rect = CGRect(x: position.x, y: position.y, width: size.width-8, height: 9)
str = NSAttributedString.init(string: subtitle, attributes: attributes)
str.draw(with: rect)
}
}
public class LineChartView: NSView {
public var id: String = UUID().uuidString
private let dateFormatter = DateFormatter()
private var queue: DispatchQueue = DispatchQueue(label: "eu.exelban.Stats.charts.line", attributes: .concurrent)
public var points: [DoubleValue?]
public var shadowPoints: [DoubleValue?] = []
public var transparent: Bool = true
public var flipY: Bool = false
public var minMax: Bool = false
public var color: NSColor
public var suffix: String
public var toolTipFunc: ((DoubleValue) -> String)?
public var isTooltipEnabled: Bool = true
private var scale: Scale
private var fixedScale: Double
private var zeroValue: Double
private var cursor: NSPoint? = nil
private var stop: Bool = false
public init(frame: NSRect = .zero, num: Int, suffix: String = "%", color: NSColor = .controlAccentColor, scale: Scale = .none, fixedScale: Double = 1, zeroValue: Double = 0.01) {
self.points = Array(repeating: nil, count: max(num, 1))
self.suffix = suffix
self.color = color
self.scale = scale
self.fixedScale = fixedScale
self.zeroValue = zeroValue
super.init(frame: frame)
self.dateFormatter.dateFormat = "dd/MM HH:mm:ss"
self.addTrackingArea(NSTrackingArea(
rect: CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height),
options: [
.activeAlways,
.mouseEnteredAndExited,
.mouseMoved,
.inVisibleRect
],
owner: self, userInfo: nil
))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
var originalPoints: [DoubleValue?] = []
var shadowPoints: [DoubleValue?] = []
var transparent: Bool = true
var flipY: Bool = false
var minMax: Bool = false
var color: NSColor = .controlAccentColor
var suffix: String = "%"
var toolTipFunc: ((DoubleValue) -> String)?
var isTooltipEnabled: Bool = true
self.queue.sync {
originalPoints = self.points
shadowPoints = self.shadowPoints
transparent = self.transparent
flipY = self.flipY
minMax = self.minMax
color = self.color
suffix = self.suffix
toolTipFunc = self.toolTipFunc
isTooltipEnabled = self.isTooltipEnabled
}
let points = stop ? shadowPoints : originalPoints
guard let context = NSGraphicsContext.current?.cgContext, !points.isEmpty else { return }
context.setShouldAntialias(true)
let maxValue = points.compactMap { $0 }.max() ?? 0
let lineColor: NSColor = color
var gradientColor: NSColor = color.withAlphaComponent(0.5)
if !transparent {
gradientColor = color.withAlphaComponent(0.8)
}
let gradient = NSGradient(colors: [
gradientColor.withAlphaComponent(0.5),
gradientColor.withAlphaComponent(1.0)
])
let offset: CGFloat = 1 / (NSScreen.main?.backingScaleFactor ?? 1)
let height: CGFloat = self.frame.height - offset
let xRatio: CGFloat = self.frame.width / CGFloat(points.count-1)
let zero: CGFloat = flipY ? self.frame.height : 0
var lines: [[CGPoint]] = []
var line: [CGPoint] = []
var list: [(value: DoubleValue, point: CGPoint)] = []
for (i, v) in points.enumerated() {
guard let v else {
if !line.isEmpty {
lines.append(line)
line = []
}
continue
}
var y = scaleValue(scale: scale, value: v.value, maxValue: maxValue, zeroValue: zeroValue, maxHeight: height, limit: fixedScale)
if flipY {
y = height - y
}
let point = CGPoint(
x: CGFloat(i) * xRatio,
y: y
)
line.append(point)
list.append((value: v, point: point))
}
if lines.isEmpty && !line.isEmpty {
lines.append(line)
}
var path = NSBezierPath()
for linePoints in lines {
if linePoints.count == 1 {
path = NSBezierPath(ovalIn: CGRect(x: linePoints[0].x-offset, y: linePoints[0].y-offset, width: 1, height: 1))
lineColor.set()
path.stroke()
gradientColor.set()
path.fill()
continue
}
path = NSBezierPath()
path.move(to: linePoints[0])
for i in 1..= p.x }
let underPoints = list.filter { $0.point.x <= p.x }
if let over = overPoints.min(by: { $0.point.x < $1.point.x }), let under = underPoints.max(by: { $0.point.x < $1.point.x }) {
let diffOver = over.point.x - p.x
let diffUnder = p.x - under.point.x
let nearest = (diffOver < diffUnder) ? over : under
let vLine = NSBezierPath()
let hLine = NSBezierPath()
vLine.setLineDash([4, 4], count: 2, phase: 0)
hLine.setLineDash([6, 6], count: 2, phase: 0)
vLine.move(to: CGPoint(x: p.x, y: 0))
vLine.line(to: CGPoint(x: p.x, y: height))
vLine.close()
hLine.move(to: CGPoint(x: 0, y: p.y))
hLine.line(to: CGPoint(x: self.frame.size.width, y: p.y))
hLine.close()
NSColor.tertiaryLabelColor.set()
vLine.lineWidth = offset
hLine.lineWidth = offset
vLine.stroke()
hLine.stroke()
let dotSize: CGFloat = 4
let path = NSBezierPath(ovalIn: CGRect(
x: nearest.point.x-(dotSize/2),
y: nearest.point.y-(dotSize/2),
width: dotSize,
height: dotSize
))
NSColor.red.set()
path.stroke()
let date = self.dateFormatter.string(from: nearest.value.ts)
let roundedValue = (nearest.value.value * 100).rounded(toPlaces: 2)
let strValue = roundedValue >= 1 ? "\(Int(roundedValue))\(suffix)" : "\(roundedValue)\(suffix)"
let value = toolTipFunc != nil ? toolTipFunc!(nearest.value) : strValue
drawToolTip(self.frame, CGPoint(x: nearest.point.x+4, y: nearest.point.y+4), CGSize(width: 78, height: height), value: value, subtitle: date)
}
}
}
public override func updateTrackingAreas() {
self.trackingAreas.forEach({ self.removeTrackingArea($0) })
self.addTrackingArea(NSTrackingArea(
rect: .zero,
options: [
.activeAlways,
.mouseEnteredAndExited,
.mouseMoved,
.inVisibleRect
],
owner: self, userInfo: nil
))
super.updateTrackingAreas()
}
public func addValue(_ value: DoubleValue) {
self.queue.async(flags: .barrier) {
guard !self.points.isEmpty else { return }
self.points.remove(at: 0)
self.points.append(value)
}
if self.window?.isVisible ?? false {
self.display()
}
}
public func addValue(_ value: Double) {
self.addValue(DoubleValue(value))
}
public func reinit(_ num: Int = 60) {
guard self.points.count != num else { return }
if num < self.points.count {
self.points = Array(self.points[self.points.count-num.. 1 ? value/100 : value
}
if self.window?.isVisible ?? false {
self.display()
}
}
public func setText(_ value: String) {
self.queue.async(flags: .barrier) {
self.text = value
}
if self.window?.isVisible ?? false {
self.display()
}
}
}
internal class TachometerGraphView: NSView {
private var filled: Bool
private var segments: [circle_segment]
private var queue: DispatchQueue = DispatchQueue(label: "eu.exelban.Stats.charts.tachometer")
internal init(frame: NSRect, segments: [circle_segment], filled: Bool = true) {
self.filled = filled
self.segments = segments
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ rect: CGRect) {
var filled: Bool = false
var segments: [circle_segment] = []
self.queue.sync {
filled = self.filled
segments = self.segments
}
let arcWidth: CGFloat = filled ? min(self.frame.width, self.frame.height) / 2 : 7
let totalAmount = segments.reduce(0) { $0 + $1.value }
if totalAmount < 1 {
segments.append(circle_segment(value: Double(1-totalAmount), color: NSColor.lightGray.withAlphaComponent(0.5)))
}
let centerPoint = CGPoint(x: self.frame.width/2, y: self.frame.height/2)
let radius = (min(self.frame.width, self.frame.height) - arcWidth) / 2
guard let context = NSGraphicsContext.current?.cgContext else { return }
context.setShouldAntialias(true)
context.setLineWidth(arcWidth)
context.setLineCap(.butt)
context.translateBy(x: self.frame.width, y: -4)
context.scaleBy(x: -1, y: 1)
let startAngle: CGFloat = 0
let endCircle: CGFloat = CGFloat.pi
var previousAngle = startAngle
for segment in segments {
let currentAngle: CGFloat = previousAngle + (CGFloat(segment.value) * endCircle)
context.setStrokeColor(segment.color.cgColor)
context.addArc(center: centerPoint, radius: radius, startAngle: previousAngle, endAngle: currentAngle, clockwise: false)
context.strokePath()
previousAngle = currentAngle
}
}
internal func setSegments(_ segments: [circle_segment]) {
self.queue.async(flags: .barrier) {
self.segments = segments
}
if self.window?.isVisible ?? false {
self.display()
}
}
internal func setFrame(_ frame: NSRect) {
var original = self.frame
original = frame
self.frame = original
}
}
public class ColumnChartView: NSView {
private var values: [ColorValue] = []
private var cursor: CGPoint? = nil
private var queue: DispatchQueue = DispatchQueue(label: "eu.exelban.Stats.charts.column")
public init(frame: NSRect = NSRect.zero, num: Int) {
super.init(frame: frame)
self.values = Array(repeating: ColorValue(0, color: .controlAccentColor), count: num)
self.addTrackingArea(NSTrackingArea(
rect: CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height),
options: [
.activeAlways,
.mouseEnteredAndExited,
.mouseMoved,
.inVisibleRect
],
owner: self, userInfo: nil
))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
var values: [ColorValue] = []
self.queue.sync {
values = self.values
}
guard !values.isEmpty else { return }
let blocks: Int = 16
let spacing: CGFloat = 2
let count: CGFloat = CGFloat(values.count)
guard count > 0, self.frame.width > 0, self.frame.height > 0 else { return }
let partitionSize: CGSize = CGSize(width: (self.frame.width - (count*spacing)) / count, height: self.frame.height)
let blockSize = CGSize(width: partitionSize.width-(spacing*2), height: ((partitionSize.height - spacing - 1)/CGFloat(blocks))-1)
var list: [(value: Double, path: NSBezierPath)] = []
var x: CGFloat = 0
for i in 0.. 0.1 ? 32 : 24
let tooltipX = min(p.x+4, self.frame.width - width)
let tooltipY = min(p.y+4, self.frame.height - partitionSize.height)
drawToolTip(self.frame, CGPoint(x: tooltipX, y: tooltipY), CGSize(width: width, height: min(partitionSize.height, self.frame.height)), value: value)
}
}
}
public func setValues(_ values: [ColorValue]) {
self.queue.async(flags: .barrier) {
self.values = values
}
if self.window?.isVisible ?? false {
self.display()
}
}
public override func mouseEntered(with event: NSEvent) {
self.cursor = convert(event.locationInWindow, from: nil)
self.display()
}
public override func mouseMoved(with event: NSEvent) {
self.cursor = convert(event.locationInWindow, from: nil)
self.display()
}
public override func mouseDragged(with event: NSEvent) {
self.cursor = convert(event.locationInWindow, from: nil)
self.display()
}
public override func mouseExited(with event: NSEvent) {
self.cursor = nil
self.display()
}
public override func updateTrackingAreas() {
self.trackingAreas.forEach({ self.removeTrackingArea($0) })
self.addTrackingArea(NSTrackingArea(
rect: .zero,
options: [
.activeAlways,
.mouseEnteredAndExited,
.mouseMoved,
.inVisibleRect
],
owner: self, userInfo: nil
))
super.updateTrackingAreas()
}
}
public class GridChartView: NSView {
private let okColor: NSColor = .systemGreen
private let notOkColor: NSColor = .systemRed
private let inactiveColor: NSColor = .underPageBackgroundColor.withAlphaComponent(0.4)
private var values: [NSColor] = []
private let grid: (rows: Int, columns: Int)
private var queue: DispatchQueue = DispatchQueue(label: "eu.exelban.Stats.charts.grid")
public init(frame: NSRect, grid: (rows: Int, columns: Int)) {
self.grid = grid
super.init(frame: frame)
let totalCells = max(grid.rows * grid.columns, 1)
self.values = Array(repeating: self.inactiveColor, count: totalCells)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
var grid: (rows: Int, columns: Int) = (0, 0)
var values: [NSColor] = []
self.queue.sync {
grid = self.grid
values = self.values
}
let spacing: CGFloat = 2
let size: CGSize = CGSize(
width: (self.frame.width - ((CGFloat(grid.rows)-1) * spacing)) / CGFloat(grid.rows),
height: (self.frame.height - ((CGFloat(grid.columns)-1) * spacing)) / CGFloat(grid.columns)
)
var origin: CGPoint = CGPoint(x: 0, y: (size.height + spacing) * CGFloat(grid.columns - 1))
var i: Int = 0
for _ in 0.. 0 else { return }
guard let context = NSGraphicsContext.current?.cgContext else { return }
let barRect: NSRect = isHorizontal
? NSRect(x: 0, y: (self.frame.height - barSize) / 2, width: self.frame.width, height: barSize)
: NSRect(x: (self.frame.width - barSize) / 2, y: 0, width: barSize, height: self.frame.height)
let clipPath = NSBezierPath(roundedRect: barRect, xRadius: 3, yRadius: 3)
context.saveGState()
clipPath.addClip()
var list: [(value: Double, path: NSBezierPath)] = []
var offset: CGFloat = 0
for value in values {
let color = value.color ?? .controlAccentColor
let segmentLength = CGFloat(value.value / adjustedTotal) * (isHorizontal ? self.frame.width : self.frame.height)
let rect: NSRect = isHorizontal
? NSRect(x: offset, y: (self.frame.height - barSize) / 2, width: segmentLength, height: barSize)
: NSRect(x: (self.frame.width - barSize) / 2, y: offset, width: barSize, height: segmentLength)
let path = NSBezierPath(rect: rect)
color.setFill()
path.fill()
list.append((value: value.value, path: path))
offset += segmentLength
}
context.restoreGState()
}
public func setValue(_ values: ColorValue) {
self.queue.async(flags: .barrier) {
self.values = [values]
}
if self.window?.isVisible ?? false {
self.display()
}
}
public func setValues(_ values: [ColorValue]) {
self.queue.async(flags: .barrier) {
self.values = values
}
if self.window?.isVisible ?? false {
self.display()
}
}
}
================================================
FILE: Kit/plugins/DB.swift
================================================
//
// DB.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 03/02/2024
// Using Swift 5.0
// Running on macOS 14.3
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//
import Foundation
public class DB {
public static let shared = DB()
private var lldb: LLDB? = nil
private let queue = DispatchQueue(label: "eu.exelban.db")
private let ttl: Int = 60*60
public var _writeTS: [String: Date] = [:]
public var writeTS: [String: Date] {
get { self.queue.sync { self._writeTS } }
set { self.queue.sync { self._writeTS = newValue } }
}
private var _values: [String: Codable] = [:]
public var values: [String: Codable] {
get { self.queue.sync { self._values } }
set { self.queue.sync { self._values = newValue } }
}
init() {
let fileManager = FileManager.default
let supportPath = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("Stats")
let tmpPath = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("Stats")
try? fileManager.createDirectory(at: supportPath, withIntermediateDirectories: true, attributes: nil)
try? fileManager.createDirectory(at: tmpPath, withIntermediateDirectories: true, attributes: nil)
var dbURL: URL?
var tmpURL: URL?
if let values = try? supportPath.resourceValues(forKeys: [.isDirectoryKey]), values.isDirectory ?? false {
dbURL = supportPath.appendingPathComponent("lldb")
}
if let values = try? tmpPath.resourceValues(forKeys: [.isDirectoryKey]), values.isDirectory ?? false {
tmpURL = tmpPath.appendingPathComponent("lldb")
}
if dbURL == nil && tmpURL != nil {
dbURL = tmpURL
}
if let url = dbURL, let lldb = LLDB(url.path) {
self.lldb = lldb
return
}
if let url = tmpURL, let lldb = LLDB(url.path) {
self.lldb = lldb
return
}
print("ERROR INITIALIZE DB")
}
deinit {
self.lldb?.close()
}
public func setup(_ type: T.Type, _ key: String) {
self.clean(key)
if let raw = self.lldb?.findOne(key), let value = try? JSONDecoder().decode(type, from: Data(raw.utf8)) {
self.values[key] = value
}
}
public func insert(key: String, value: Codable, ts: Bool = true, force: Bool = false) {
self.values[key] = value
guard let blobData = try? JSONEncoder().encode(value), let str = String(data: blobData, encoding: .utf8) else { return }
if ts {
self.lldb?.insert("\(key)@\(Date().currentTimeSeconds())", value: str)
}
if !force, let ts = self.writeTS[key], (Date().timeIntervalSince1970-ts.timeIntervalSince1970) < 30 { return }
self.lldb?.insert(key, value: str)
self.writeTS[key] = Date()
}
public func findOne(_ dynamicType: T.Type, key: String) -> T? {
return self.values[key] as? T
}
private func clean(_ key: String) {
guard let keys = self.lldb?.keys(key) as? [String] else { return }
let maxLiveTS = Date().currentTimeSeconds() - self.ttl
var toDeleteKeys: [String] = []
keys.forEach { (key: String) in
if let ts = key.split(separator: "@").last, let ts = Int(ts), ts < maxLiveTS {
toDeleteKeys.append(key)
}
}
self.lldb?.deleteMany(toDeleteKeys)
}
}
================================================
FILE: Kit/plugins/Logger.swift
================================================
//
// Logger.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 24/06/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
import Darwin
import Foundation
public enum LogLevel: String {
case debug = "DBG"
case info = "INF"
case error = "ERR"
}
public enum LogOption: Int {
case timestamp
case level
case file
case line
public static func new() -> [LogOption] {
return [timestamp, file, line, level]
}
}
public enum LogWriter: Int {
case stdout
case stderr
case file
}
public protocol Writer: TextOutputStream {
var type: LogWriter { get }
}
public class NextLog {
public static let shared = NextLog()
private var writer: Writer = StderrOutputStream()
private var category: String? = nil
public init(writer: LogWriter = .stdout) {
self.setWriter(writer)
}
public func copy(category: String? = nil) -> NextLog {
let logger = NextLog()
logger.writer = NextLog.shared.writer
if let category = category {
logger.category = category
}
return logger
}
public func log(level: LogLevel, options: [LogOption] = LogOption.new(), message: String, file: String = #file, line: UInt = #line) {
self.writer.write(self.prefix(level, options, file, line) + " " + message + "\n")
}
public func setWriter(_ writer: LogWriter) {
switch writer {
case .stdout:
self.writer = StdoutOutputStream()
case .stderr:
self.writer = StderrOutputStream()
case .file:
let fm = FileManager.default
let fileURL = fm.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("log.txt")
if !fm.fileExists(atPath: fileURL.path) {
try? Data("".utf8).write(to: fileURL)
}
do {
let handle = try FileHandle(forWritingTo: fileURL)
handle.seekToEndOfFile()
handle.write(Data("----------------\n".utf8))
self.writer = FileHandlerOutputStream(handle)
} catch let err {
print("error to init file handler: \(err)")
self.writer = StdoutOutputStream()
}
}
}
private func prefix(_ level: LogLevel, _ options: [LogOption], _ file: String = #file, _ line: UInt = #line) -> String {
var prefix = ""
if options.contains(.timestamp) {
self.space(&prefix, NextLog.timestampFormatter.string(from: Date()))
}
if options.contains(.file) {
if let f = file.split(separator: "/").last {
self.space(&prefix, String(f))
}
if options.contains(.line) {
prefix += ":\(line)"
}
} else if options.contains(.line) {
self.space(&prefix, "\(line)")
}
if options.contains(.level) {
self.space(&prefix, level.rawValue)
}
if let category = self.category {
self.space(&prefix, "[\(category)]")
}
return prefix
}
private func space(_ origin: inout String, _ str: String) {
if origin.last != " " && !origin.isEmpty {
origin += " "
}
origin += str
}
}
extension NextLog {
private static var timestampFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return formatter
}
private struct StdoutOutputStream: Writer {
public let type: LogWriter = .stdout
mutating func write(_ string: String) {
fputs(string, stdout)
}
}
private struct StderrOutputStream: Writer {
public let type: LogWriter = .stderr
mutating func write(_ string: String) {
fputs(string, stderr)
}
}
struct FileHandlerOutputStream: Writer {
public let type: LogWriter = .file
private let fileHandle: FileHandle
private let encoding: String.Encoding
init(_ fileHandle: FileHandle, encoding: String.Encoding = .utf8) {
self.fileHandle = fileHandle
self.encoding = encoding
}
mutating func write(_ string: String) {
if let data = string.data(using: encoding) {
self.fileHandle.write(data)
}
}
}
}
public func debug(_ message: String, log: NextLog = NextLog.shared, file: String = #file, line: UInt = #line) {
log.log(level: .debug, message: message, file: file, line: line)
}
public func info(_ message: String, log: NextLog = NextLog.shared, file: String = #file, line: UInt = #line) {
log.log(level: .info, message: message, file: file, line: line)
}
public func error(_ message: String, log: NextLog = NextLog.shared, file: String = #file, line: UInt = #line) {
log.log(level: .error, message: message, file: file, line: line)
}
public func error_msg(_ message: String, log: NextLog = NextLog.shared, file: String = #file, line: UInt = #line) {
log.log(level: .error, message: message, file: file, line: line)
}
================================================
FILE: Kit/plugins/Reachability.swift
================================================
//
// Reachability.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 15/10/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
// Inspired by https://gist.github.com/saeed-rz/d9827b312915e0dc145497532e514470 and https://github.com/ashleymills/Reachability.swift
import Foundation
import SystemConfiguration
public class Reachability {
public var isReachable: Bool = false
public var reachable: () -> Void = {}
public var unreachable: () -> Void = {}
private var isRunning = false
private var reachability: SCNetworkReachability?
private let reachabilitySerialQueue = DispatchQueue(label: "eu.exelban.ReachabilityQueue")
public init(start: Bool = false) {
var zeroAddress = sockaddr()
zeroAddress.sa_len = UInt8(MemoryLayout.size)
zeroAddress.sa_family = sa_family_t(AF_INET)
guard let ref = SCNetworkReachabilityCreateWithAddress(nil, &zeroAddress) else {
error("SCNetworkReachability create with address")
return
}
self.reachability = ref
if start {
self.start()
}
}
public func start() {
guard let reachability = self.reachability, !self.isRunning else {
error("reachability is nil or already started")
return
}
let callback: SCNetworkReachabilityCallBack = { (_, flags, info) in
guard let info = info else { return }
Unmanaged.fromOpaque(info).takeUnretainedValue().setFlags(flags)
}
var context = SCNetworkReachabilityContext(
version: 0,
info: Unmanaged.passUnretained(self).toOpaque(),
retain: { (info: UnsafeRawPointer) -> UnsafeRawPointer in
let unmanagedReachability = Unmanaged.fromOpaque(info)
_ = unmanagedReachability.retain()
return UnsafeRawPointer(unmanagedReachability.toOpaque())
},
release: { (info: UnsafeRawPointer) in
Unmanaged.fromOpaque(info).release()
},
copyDescription: nil
)
guard SCNetworkReachabilitySetCallback(reachability, callback, &context) else {
error("SCNetworkReachability set dispatch callback")
self.stop()
return
}
guard SCNetworkReachabilitySetDispatchQueue(reachability, reachabilitySerialQueue) else {
error("SCNetworkReachability set dispatch queue")
self.stop()
return
}
self.reachabilitySerialQueue.sync { [unowned self] in
guard let reachability = self.reachability else {
error("reachability is nil")
return
}
var flags = SCNetworkReachabilityFlags()
if !SCNetworkReachabilityGetFlags(reachability, &flags) {
error("SCNetworkReachability get flags")
self.stop()
return
}
self.setFlags(flags)
}
self.isRunning = true
}
public func stop() {
defer { self.isRunning = false }
guard let reachability = self.reachability, self.isRunning else {
error("reachability is nil or already stopped")
return
}
SCNetworkReachabilitySetCallback(reachability, nil, nil)
SCNetworkReachabilitySetDispatchQueue(reachability, nil)
}
private func setFlags(_ flags: SCNetworkReachabilityFlags) {
self.isReachable = flags.contains(.reachable)
if self.isReachable {
self.reachable()
} else {
self.unreachable()
}
}
}
================================================
FILE: Kit/plugins/Remote.swift
================================================
//
// Remote.swift
// Stats
//
// Created by Serhiy Mytrovtsiy on 16/03/2025
// Using Swift 6.0
// Running on macOS 15.3
//
// Copyright © 2025 Serhiy Mytrovtsiy. All rights reserved.
//
import Foundation
import Cocoa
import CoreAudio
public protocol RemoteType {
func remote() -> Data?
}
public class Remote {
public static let shared = Remote()
static public var host = URL(string: "https://api.system-stats.com")!
static public var authHost = URL(string: "https://oauth.system-stats.com")!
static public var brokerHost = URL(string: "wss://broker.system-stats.com:8084/mqtt")!
public var monitoring: Bool {
get { Store.shared.bool(key: "remote_monitoring", defaultValue: false) }
set {
Store.shared.set(key: "remote_monitoring", value: newValue)
if newValue {
self.start()
self.registerDevice()
} else if !self.control {
self.stop()
}
}
}
public var control: Bool {
get { Store.shared.bool(key: "remote_control", defaultValue: false) }
set {
Store.shared.set(key: "remote_control", value: newValue)
if newValue {
self.start()
self.registerDevice()
} else if !self.monitoring {
self.stop()
}
}
}
public let id: UUID
public var isAuthorized: Bool = false
public var auth: RemoteAuth = RemoteAuth()
private let log: NextLog
private var mqtt: MQTTManager = MQTTManager()
private var isConnecting = false
private var lastSleepTime: Date?
private var lastRegisterTime: Date?
struct Details: Codable {
let client: Client
let system: System
let hardware: Hardware
}
struct Client: Codable {
let version: String
let control: Bool
}
struct OS: Codable {
let name: String?
let version: String?
let build: String?
}
struct System: Codable {
let platform: String
let vendor: String?
let model: String?
let modelID: String?
let os: OS
let arch: String?
}
struct Hardware: Codable {
let cpu: cpu_s?
let gpu: [gpu_s]?
let ram: [dimm_s]?
let disk: [disk_s]?
}
public init() {
self.log = NextLog.shared.copy(category: "Remote")
var id = UUID(uuidString: Store.shared.string(key: "remote_id", defaultValue: UUID().uuidString)) ?? UUID()
if Store.shared.exist(key: "telemetry_id") {
id = UUID(uuidString: Store.shared.string(key: "telemetry_id", defaultValue: UUID().uuidString)) ?? UUID()
Store.shared.remove("telemetry_id")
}
if !Store.shared.exist(key: "remote_id") {
Store.shared.set(key: "remote_id", value: id.uuidString)
}
self.id = id
self.mqtt.commandCallback = { [weak self] cmd, payload in
self?.command(cmd: cmd, payload: payload)
}
self.mqtt.registerCallback = { [weak self] in
self?.registerDevice()
}
self.mqtt.unregisterHandler = { [weak self] in
guard let self else { return }
info("Unregistered from MQTT broker, stopping Remote...", log: self.log)
self.logout()
}
if self.auth.hasCredentials() {
info("Found auth credentials for remote monitoring, starting Remote...", log: self.log)
self.start()
}
NotificationCenter.default.addObserver(self, selector: #selector(self.successLogin), name: .remoteLoginSuccess, object: nil)
}
deinit {
self.mqtt.disconnect()
NotificationCenter.default.removeObserver(self, name: .remoteLoginSuccess, object: nil)
}
public func login() {
self.auth.login { url in
guard let url else {
error("Empty url when try to login", log: self.log)
return
}
debug("Open \(url) to login to Stats Remote", log: self.log)
NSWorkspace.shared.open(url)
}
}
public func logout() {
self.mqtt.disconnect()
self.auth.logout()
self.isAuthorized = false
debug("Logout successfully from Stats Remote", log: self.log)
NotificationCenter.default.post(name: .remoteState, object: nil, userInfo: ["auth": self.isAuthorized])
}
public func send(key: String, value: Any) {
guard self.monitoring && self.isAuthorized, let v = value as? RemoteType, let data = v.remote() else { return }
let topic = "stats/\(self.id.uuidString)/metrics/\(key)"
self.mqtt.publish(topic: topic, data: data)
}
@objc private func successLogin() {
self.isAuthorized = true
NotificationCenter.default.post(name: .remoteState, object: nil, userInfo: ["auth": self.isAuthorized])
self.mqtt.connect()
debug("Login successfully on Stats Remote", log: self.log)
}
public func start() {
self.auth.isAuthorized { [weak self] status in
guard let self else { return }
self.isAuthorized = status
NotificationCenter.default.post(name: .remoteState, object: nil, userInfo: ["auth": self.isAuthorized])
if status {
self.mqtt.connect()
}
}
}
private func stop() {
self.mqtt.disconnect()
NotificationCenter.default.post(name: .remoteState, object: nil, userInfo: ["auth": self.isAuthorized])
}
public func terminate() {
self.mqtt.disconnect()
}
private func registerDevice() {
let oneHour: TimeInterval = 3600
let now = Date()
if let lastTime = self.lastRegisterTime, now.timeIntervalSince(lastTime) < oneHour {
debug("Device registration skipped: cooldown period not met", log: self.log)
return
}
self.lastRegisterTime = now
guard let url = URL(string: "\(Remote.host)/remote/device") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(Remote.shared.auth.accessToken)", forHTTPHeaderField: "Authorization")
struct RegisterPayload: Codable {
let id: String
let details: Remote.Details
}
let payload = RegisterPayload(
id: Remote.shared.id.uuidString,
details: Remote.Details(
client: Client(
version: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown",
control: Remote.shared.control
),
system: Remote.System(
platform: "macOS",
vendor: "Apple",
model: SystemKit.shared.device.model.name,
modelID: SystemKit.shared.device.model.id,
os: Remote.OS(
name: SystemKit.shared.device.os?.name,
version: SystemKit.shared.device.os?.version.getFullVersion(),
build: SystemKit.shared.device.os?.build
),
arch: SystemKit.shared.device.arch
),
hardware: Remote.Hardware(
cpu: SystemKit.shared.device.info.cpu,
gpu: SystemKit.shared.device.info.gpu,
ram: SystemKit.shared.device.info.ram?.dimms,
disk: SystemKit.shared.device.info.disk
)
)
)
guard let body = try? JSONEncoder().encode(payload) else { return }
request.httpBody = body
URLSession.shared.dataTask(with: request) { data, response, _ in
guard let httpResponse = response as? HTTPURLResponse else { return }
if httpResponse.statusCode == 200 {
debug("Registered device: \(Remote.shared.id.uuidString)", log: self.log)
} else {
let bodyString = data.flatMap { String(data: $0, encoding: .utf8) } ?? ""
debug("Register remote failed (\(httpResponse.statusCode)): \(bodyString)", log: self.log)
}
}.resume()
}
private func command(cmd: String, payload: Data?) {
guard self.control else { return }
debug("received command '\(cmd)' with payload: \(String(data: payload ?? Data(), encoding: .utf8) ?? "")", log: self.log)
switch cmd {
case "disable": self.disableControl()
case "sleep": self.sleep()
case "volume":
guard let payload else { return }
let value = String(data: payload, encoding: .utf8)
let step: Float32 = 0.0625
switch value {
case "up":
if let current = self.getSystemVolume() {
if self.isSystemMuted() {
self.setSystemMute(false)
} else {
self.setSystemVolume(min(current + step, 1.0))
}
}
case "down":
if let current = self.getSystemVolume() {
if self.isSystemMuted() {
self.setSystemMute(false)
} else {
self.setSystemVolume(max(current - step, 0.0))
}
}
case "mute":
self.setSystemMute(true)
case "unmute":
self.setSystemMute(false)
default: break
}
default: break
}
}
}
extension Remote {
func disableControl() {
self.control = false
}
func sleep() {
let minInterval: TimeInterval = 300
let now = Date()
if let last = self.lastSleepTime, now.timeIntervalSince(last) < minInterval {
debug("Sleep command ignored due to cooldown", log: self.log)
return
}
self.lastSleepTime = now
let process = Process()
process.launchPath = "/usr/bin/pmset"
process.arguments = ["sleepnow"]
process.launch()
}
func isSystemMuted() -> Bool {
var defaultOutputDeviceID = AudioDeviceID(0)
var propertyAddress = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var size = UInt32(MemoryLayout.size)
let status = AudioObjectGetPropertyData(
AudioObjectID(kAudioObjectSystemObject),
&propertyAddress,
0,
nil,
&size,
&defaultOutputDeviceID
)
guard status == noErr else { return false }
propertyAddress = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyMute,
mScope: kAudioDevicePropertyScopeOutput,
mElement: 0
)
var muteValue: UInt32 = 0
size = UInt32(MemoryLayout.size)
let muteStatus = AudioObjectGetPropertyData(
defaultOutputDeviceID,
&propertyAddress,
0,
nil,
&size,
&muteValue
)
return muteStatus == noErr && muteValue == 1
}
func setSystemMute(_ mute: Bool) {
var defaultOutputDeviceID = AudioDeviceID(0)
var propertyAddress = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var size = UInt32(MemoryLayout.size)
let status = AudioObjectGetPropertyData(
AudioObjectID(kAudioObjectSystemObject),
&propertyAddress,
0,
nil,
&size,
&defaultOutputDeviceID
)
guard status == noErr else { return }
propertyAddress = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyMute,
mScope: kAudioDevicePropertyScopeOutput,
mElement: 0
)
var muteValue: UInt32 = mute ? 1 : 0
AudioObjectSetPropertyData(
defaultOutputDeviceID,
&propertyAddress,
0,
nil,
UInt32(MemoryLayout.size),
&muteValue
)
}
func getSystemVolume() -> Float32? {
var defaultOutputDeviceID = AudioDeviceID(0)
var propertyAddress = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var size = UInt32(MemoryLayout.size)
let status = AudioObjectGetPropertyData(
AudioObjectID(kAudioObjectSystemObject),
&propertyAddress,
0,
nil,
&size,
&defaultOutputDeviceID
)
guard status == noErr else { return nil }
propertyAddress = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyVolumeScalar,
mScope: kAudioDevicePropertyScopeOutput,
mElement: 0
)
var volume: Float32 = 0
size = UInt32(MemoryLayout.size)
let volStatus = AudioObjectGetPropertyData(
defaultOutputDeviceID,
&propertyAddress,
0,
nil,
&size,
&volume
)
return volStatus == noErr ? volume : nil
}
func setSystemVolume(_ volume: Float32) {
var defaultOutputDeviceID = AudioDeviceID(0)
var propertyAddress = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var size = UInt32(MemoryLayout.size)
let status = AudioObjectGetPropertyData(
AudioObjectID(kAudioObjectSystemObject),
&propertyAddress,
0,
nil,
&size,
&defaultOutputDeviceID
)
guard status == noErr else { return }
propertyAddress = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyVolumeScalar,
mScope: kAudioDevicePropertyScopeOutput,
mElement: 0
)
var vol = max(0.0, min(1.0, volume))
AudioObjectSetPropertyData(
defaultOutputDeviceID,
&propertyAddress,
0,
nil,
UInt32(MemoryLayout.size),
&vol
)
}
}
public class RemoteAuth {
public var accessToken: String {
get { Store.shared.string(key: "access_token", defaultValue: "") }
set { Store.shared.set(key: "access_token", value: newValue) }
}
private var refreshToken: String {
get { Store.shared.string(key: "refresh_token", defaultValue: "") }
set { Store.shared.set(key: "refresh_token", value: newValue) }
}
private var clientID: String = "stats"
private var deviceCode: String = ""
private var userCode: String = ""
private var interval: Int = 5
private var repeater: Repeater?
private var lastValidationTime: Date?
private var validationAttempts: Int = 0
private let baseCooldown: TimeInterval = 2.0 // Start with 2 seconds
private let maxCooldown: TimeInterval = 60.0 // Max 60 seconds
public init() {
NotificationCenter.default.addObserver(self, selector: #selector(self.successLogin), name: .remoteLoginSuccess, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self, name: .remoteLoginSuccess, object: nil)
}
public func isAuthorized(completion: @escaping (Bool) -> Void) {
if !self.hasCredentials() {
completion(false)
return
}
if !self.accessToken.isEmpty && !self.isTokenExpired() {
DispatchQueue.main.async {
completion(true)
}
return
}
self.validate(completion)
}
public func hasCredentials() -> Bool {
return !self.accessToken.isEmpty && !self.refreshToken.isEmpty
}
public func login(completion: @escaping (URL?) -> Void) {
self.registerDevice { device in
guard let device else {
completion(nil)
return
}
completion(device.verification_uri_complete)
self.deviceCode = device.device_code
self.userCode = device.user_code
self.interval = device.interval ?? 5
self.repeater = Repeater(seconds: self.interval) {
self.pollForToken { error in
guard error == nil else {
print(error?.localizedDescription ?? "error pooling for token")
self.repeater?.pause()
self.repeater = nil
return
}
if !self.accessToken.isEmpty {
self.repeater?.pause()
self.repeater = nil
}
}
}
self.repeater?.start()
}
}
public func logout() {
self.accessToken = ""
self.refreshToken = ""
}
private func validate(_ completion: @escaping (Bool) -> Void) {
guard !self.accessToken.isEmpty && !self.refreshToken.isEmpty, let url = URL(string: "\(Remote.authHost)/me") else {
completion(false)
return
}
let now = Date()
let dynamicCooldown = min(self.baseCooldown * pow(2.0, Double(self.validationAttempts)), self.maxCooldown)
if let lastTime = self.lastValidationTime, now.timeIntervalSince(lastTime) < dynamicCooldown {
let remainingTime = dynamicCooldown - now.timeIntervalSince(lastTime)
DispatchQueue.main.asyncAfter(deadline: .now() + remainingTime) {
self.validate(completion)
}
return
}
self.lastValidationTime = now
self.validationAttempts += 1
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(self.accessToken)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { [weak self] _, response, error in
guard let self = self, error == nil, let httpResponse = response as? HTTPURLResponse else {
completion(false)
return
}
if httpResponse.statusCode == 401 {
self.refreshTokenFunc { ok in
if ok == true {
self.validationAttempts = 0
self.lastValidationTime = nil
}
completion(ok ?? false)
}
} else if httpResponse.statusCode == 200 {
self.validationAttempts = 0
self.lastValidationTime = nil
completion(true)
} else {
completion(false)
}
}.resume()
}
private func refreshTokenFunc(completion: @escaping (Bool?) -> Void) {
guard let url = URL(string: "\(Remote.authHost)/token") else {
completion(nil)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let body = "grant_type=refresh_token&refresh_token=\(self.refreshToken)"
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
request.httpBody = body?.data(using: .utf8)
URLSession.shared.dataTask(with: request) { data, response, error in
guard error == nil, let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200,
let data = data, let token = try? JSONDecoder().decode(TokenResponse.self, from: data) else {
completion(nil)
return
}
self.accessToken = token.access_token
self.refreshToken = token.refresh_token
completion(true)
}.resume()
}
private func registerDevice(completion: @escaping (DeviceResponse?) -> Void) {
guard let url = URL(string: "\(Remote.authHost)/device") else {
completion(nil)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let body = "client_id=\(self.clientID)"
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
request.httpBody = body?.data(using: .utf8)
URLSession.shared.dataTask(with: request) { data, response, error in
guard error == nil, let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200,
let data = data, let resp = try? JSONDecoder().decode(DeviceResponse.self, from: data) else {
completion(nil)
return
}
completion(resp)
}.resume()
}
private func pollForToken(completion: @escaping (Error?) -> Void) {
guard let url = URL(string: "\(Remote.authHost)/token") else {
completion(nil)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let body = "client_id=\(self.clientID)&device_code=\(self.deviceCode)&grant_type=urn:ietf:params:oauth:grant-type:device_code"
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
request.httpBody = body?.data(using: .utf8)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(error)
return
}
guard let httpResponse = response as? HTTPURLResponse else {
completion(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid response"]))
return
}
if httpResponse.statusCode == 200 {
guard let data = data else {
completion(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "No data returned"]))
return
}
do {
let result = try JSONDecoder().decode(TokenResponse.self, from: data)
NotificationCenter.default.post(name: .remoteLoginSuccess, object: nil, userInfo: [
"access_token": result.access_token,
"refresh_token": result.refresh_token
])
completion(nil)
} catch {
completion(error)
}
} else if httpResponse.statusCode == 400 {
guard let data = data, let responseString = String(data: data, encoding: .utf8) else {
completion(NSError(domain: "", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "Bad request"]))
return
}
if responseString.contains("authorization_pending") {
completion(nil)
} else if responseString.contains("expired_token") {
completion(NSError(domain: "", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "Device code expired, please re-register"]))
} else if responseString.contains("slow_down") {
DispatchQueue.global().asyncAfter(deadline: .now() + Double(self.interval)) {
completion(nil)
}
} else {
completion(NSError(domain: "", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: responseString]))
}
} else {
let errorMessage = data.flatMap { String(data: $0, encoding: .utf8) } ?? "Unknown error"
completion(NSError(domain: "", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "Failed to get token (\(httpResponse.statusCode)): \(errorMessage)"]))
}
}.resume()
}
@objc private func successLogin(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let accessToken = userInfo["access_token"] as? String,
let refreshToken = userInfo["refresh_token"] as? String else { return }
self.accessToken = accessToken
self.refreshToken = refreshToken
}
private func isTokenExpired() -> Bool {
let parts = self.accessToken.components(separatedBy: ".")
guard parts.count == 3 else { return true }
let payload = parts[1]
var base64 = payload
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
while base64.count % 4 != 0 {
base64 += "="
}
guard let data = Data(base64Encoded: base64),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let exp = json["exp"] as? TimeInterval else {
return true
}
return Date().timeIntervalSince1970 >= exp
}
}
struct MQTTMessage {
let topic: String
let payload: Data
let qos: UInt8
let retain: Bool
}
enum MQTTPacketType: UInt8 {
case connect = 1
case connack = 2
case publish = 3
case puback = 4
case subscribe = 8
case suback = 9
case pingreq = 12
case pingresp = 13
case disconnect = 14
}
class MQTTManager: NSObject {
public var registerCallback: (() -> Void)? = nil
public var commandCallback: ((String, Data?) -> Void)? = nil
public var unregisterHandler: (() -> Void)? = nil
private var webSocket: URLSessionWebSocketTask?
private var session: URLSession?
private var isConnected = false
private var isDisconnected = false
private var isReconnecting = false
private var reconnectAttempts = 0
private var maxReconnectDelay: TimeInterval = 60.0
private var pingTimer: Timer?
private var reachability: Reachability = Reachability(start: true)
private let log: NextLog
private var packetIdentifier: UInt16 = 1
override init() {
self.log = NextLog.shared.copy(category: "Remote MQTT")
super.init()
self.session = URLSession(configuration: .default, delegate: self, delegateQueue: .main)
self.reachability.reachable = {
if Remote.shared.isAuthorized {
self.connect()
}
}
self.reachability.unreachable = {
if self.isConnected {
self.disconnect()
}
}
}
public func connect() {
guard !self.isConnected else { return }
Remote.shared.auth.isAuthorized { [weak self] status in
guard let self else { return }
if status {
self.webSocket = self.session?.webSocketTask(with: Remote.brokerHost, protocols: ["mqtt"])
self.webSocket?.resume()
self.receiveMessage()
self.isDisconnected = false
debug("MQTT WebSocket connecting...", log: self.log)
} else {
debug("Authorization failed, retrying connection...", log: self.log)
self.reconnect()
}
}
}
public func disconnect() {
if self.webSocket == nil && !self.isConnected { return }
self.isDisconnected = true
self.sendStatus(false)
self.sendDisconnect()
self.webSocket?.cancel(with: .normalClosure, reason: nil)
self.webSocket = nil
self.isConnected = false
self.stopPingTimer()
debug("MQTT disconnected gracefully", log: self.log)
}
private func reconnect() {
guard !self.isDisconnected && !self.isReconnecting else { return }
self.isReconnecting = true
let delays: [TimeInterval] = [1, 3, 5, 10, 20, 40]
let delayIndex = min(self.reconnectAttempts, delays.count - 1)
let delay = self.reconnectAttempts >= delays.count ? self.maxReconnectDelay : delays[delayIndex]
debug("Waiting \(delay) seconds before next MQTT reconnection attempt...", log: self.log)
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
guard let self else { return }
self.isReconnecting = false
guard !self.isDisconnected && !self.isConnected else {
self.reconnectAttempts = 0
return
}
self.reconnectAttempts += 1
debug("Attempting MQTT reconnection #\(self.reconnectAttempts)", log: self.log)
self.connect()
}
}
public func sendStatus(_ value: Bool) {
let status = value ? "online" : "offline"
let topic = "stats/\(Remote.shared.id.uuidString)/status"
let payload = status.data(using: .utf8)
if let payload = payload {
self.publish(topic: topic, data: payload)
}
}
private func sendConnect() {
let connectPacket = createConnectPacket(username: Remote.shared.id.uuidString, password: Remote.shared.auth.accessToken)
self.webSocket?.send(.data(connectPacket)) { error in
if let error = error {
print("Error sending MQTT CONNECT: \(error)")
}
}
}
private func sendDisconnect() {
let disconnectPacket = Data([MQTTPacketType.disconnect.rawValue << 4, 0])
self.webSocket?.send(.data(disconnectPacket)) { _ in }
}
private func sendPingRequest() {
let pingPacket = Data([MQTTPacketType.pingreq.rawValue << 4, 0])
self.webSocket?.send(.data(pingPacket)) { error in
if let error = error {
print("Error sending MQTT PINGREQ: \(error)")
}
}
}
public func publish(topic: String, data: Data) {
guard self.isConnected else { return }
let publishPacket = createPublishPacket(topic: topic, payload: data)
self.webSocket?.send(.data(publishPacket)) { error in
if let error = error {
print("Error publishing MQTT message: \(error)")
}
}
}
private func subscribe(to topic: String) {
guard self.isConnected else { return }
let subscribePacket = createSubscribePacket(topic: topic)
self.webSocket?.send(.data(subscribePacket)) { error in
if let error = error {
print("Error subscribing to MQTT topic: \(error)")
}
}
}
private func createConnectPacket(username: String, password: String) -> Data {
var packet = Data()
// Fixed header - packet type only (remaining length will be added later)
let fixedHeaderByte = MQTTPacketType.connect.rawValue << 4
// Variable header
var variableHeader = Data()
variableHeader.append(contentsOf: encodeString("MQTT"))
variableHeader.append(4)
var connectFlags: UInt8 = 0x00 // Clean session
connectFlags |= 0x80 // Username flag
connectFlags |= 0x40 // Password flag
variableHeader.append(connectFlags)
variableHeader.append(contentsOf: [0x03, 0x84])
// Payload
var payload = Data()
payload.append(contentsOf: encodeString("stats-\(username)"))
payload.append(contentsOf: encodeString(username))
payload.append(contentsOf: encodeString(password))
let remainingLength = variableHeader.count + payload.count
packet.append(fixedHeaderByte)
packet.append(contentsOf: encodeRemainingLength(remainingLength))
packet.append(variableHeader)
packet.append(payload)
return packet
}
private func createPublishPacket(topic: String, payload: Data) -> Data {
var packet = Data()
// Fixed header - packet type only
let fixedHeaderByte = (MQTTPacketType.publish.rawValue << 4) | 0x00 // QoS 0
// Variable header
var variableHeader = Data()
variableHeader.append(contentsOf: encodeString(topic))
// Calculate remaining length
let remainingLength = variableHeader.count + payload.count
// Build final packet
packet.append(fixedHeaderByte)
packet.append(contentsOf: encodeRemainingLength(remainingLength))
packet.append(variableHeader)
packet.append(payload)
return packet
}
private func createSubscribePacket(topic: String) -> Data {
var packet = Data()
// Fixed header - packet type only
let fixedHeaderByte = (MQTTPacketType.subscribe.rawValue << 4) | 0x02
// Variable header
var variableHeader = Data()
// Packet identifier
let packetId = self.getNextPacketId()
variableHeader.append(contentsOf: [UInt8(packetId >> 8), UInt8(packetId & 0xFF)])
// Payload
var payload = Data()
payload.append(contentsOf: encodeString(topic))
payload.append(0x00) // QoS 0
// Calculate remaining length
let remainingLength = variableHeader.count + payload.count
// Build final packet
packet.append(fixedHeaderByte)
packet.append(contentsOf: encodeRemainingLength(remainingLength))
packet.append(variableHeader)
packet.append(payload)
return packet
}
private func encodeString(_ string: String) -> [UInt8] {
let data = string.data(using: .utf8) ?? Data()
let length = data.count
return [UInt8(length >> 8), UInt8(length & 0xFF)] + Array(data)
}
private func encodeRemainingLength(_ length: Int) -> [UInt8] {
var bytes: [UInt8] = []
var remainingLength = length
repeat {
var byte = UInt8(remainingLength % 128)
remainingLength /= 128
if remainingLength > 0 {
byte |= 128
}
bytes.append(byte)
} while remainingLength > 0
return bytes
}
private func getNextPacketId() -> UInt16 {
self.packetIdentifier += 1
if self.packetIdentifier == 0 {
self.packetIdentifier = 1
}
return self.packetIdentifier
}
private func handleMQTTPacket(_ data: Data) {
guard data.count >= 2 else { return }
let packetType = MQTTPacketType(rawValue: (data[0] >> 4) & 0x0F)
switch packetType {
case .connack:
self.handleConnAck(data)
case .pingresp:
break
case .suback:
break
case .publish:
self.handlePublish(data)
default:
break
}
}
private func handleConnAck(_ data: Data) {
guard data.count >= 4 else { return }
let returnCode = data[3]
if returnCode == 0 {
self.isConnected = true
self.isReconnecting = false
self.reconnectAttempts = 0
self.startPingTimer()
self.subscribeToTopics()
self.sendStatus(true)
debug("MQTT connected successfully", log: self.log)
self.registerCallback?()
} else {
debug("MQTT connection failed with code: \(returnCode)", log: self.log)
}
}
private func subscribeToTopics() {
self.subscribe(to: "stats/\(Remote.shared.id.uuidString)/control/+")
self.subscribe(to: "stats/\(Remote.shared.id.uuidString)/unregister")
}
private func receiveMessage() {
self.webSocket?.receive { [weak self] result in
switch result {
case .failure(let error):
self?.isConnected = false
self?.handleWebSocketError(error)
case .success(let message):
switch message {
case .data(let data):
self?.handleMQTTPacket(data)
case .string:
break
@unknown default:
break
}
self?.receiveMessage()
}
}
}
private func startPingTimer() {
self.stopPingTimer()
self.pingTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] _ in
self?.sendPingRequest()
}
}
private func stopPingTimer() {
self.pingTimer?.invalidate()
self.pingTimer = nil
}
private func handleWebSocketError(_ error: Error) {
if let urlError = error as? URLError, urlError.code.rawValue == 401 {
Remote.shared.start()
} else {
self.reconnect()
}
}
private func handlePublish(_ data: Data) {
var offset = 1
while data[offset] & 0x80 != 0 { offset += 1 }
offset += 1
guard data.count > offset + 1 else { return }
let topicLength = Int(data[offset]) << 8 | Int(data[offset + 1])
offset += 2
guard data.count >= offset + topicLength else { return }
let topicData = data.subdata(in: offset..<(offset + topicLength))
let topic = String(data: topicData, encoding: .utf8) ?? ""
offset += topicLength
if topic.hasSuffix("unregister") {
self.unregisterHandler?()
return
}
let prefix = "stats/\(Remote.shared.id.uuidString)/control/"
let commandName = topic.hasPrefix(prefix) ? String(topic.dropFirst(prefix.count)) : topic
let payload = data.subdata(in: offset.. Void)
private var state: RepeaterState = .paused
private var timer: DispatchSourceTimer = DispatchSource.makeTimerSource(queue: DispatchQueue(label: "eu.exelban.Stats.Repeater", qos: .default))
internal init(seconds: Int, callback: @escaping (() -> Void)) {
self.callback = callback
self.setupTimer(seconds)
}
deinit {
self.timer.cancel()
self.start()
}
private func setupTimer(_ interval: Int) {
self.timer.schedule(
deadline: DispatchTime.now() + Double(interval),
repeating: .seconds(interval),
leeway: .seconds(0)
)
self.timer.setEventHandler { [weak self] in
self?.callback()
}
}
internal func start() {
guard self.state == .paused else { return }
self.timer.resume()
self.state = .running
}
internal func pause() {
guard self.state == .running else { return }
self.timer.suspend()
self.state = .paused
}
internal func reset(seconds: Int, restart: Bool = false) {
if self.state == .running {
self.pause()
}
self.setupTimer(seconds)
if restart {
self.callback()
self.start()
}
}
}
================================================
FILE: Kit/plugins/Store.swift
================================================
//
// store.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 10/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public class Store {
public static let shared = Store()
private let defaults = UserDefaults.standard
private var cache: [String: Any] = [:]
private let cacheQueue = DispatchQueue(label: "eu.exelban.Stats.Store.cache", attributes: .concurrent)
public init() {
self.loadCache()
}
private func loadCache() {
guard let bundleId = Bundle.main.bundleIdentifier,
let domain = self.defaults.persistentDomain(forName: bundleId) else { return }
self.cache = domain
}
private func getValue(for key: String, type: T.Type) -> T? {
return self.cacheQueue.sync {
return self.cache[key] as? T
}
}
private func setValue(_ value: Any?, for key: String) {
self.cacheQueue.async(flags: .barrier) {
self.cache[key] = value
}
if let value = value {
self.defaults.set(value, forKey: key)
} else {
self.defaults.removeObject(forKey: key)
}
}
public func exist(key: String) -> Bool {
return self.cacheQueue.sync {
self.cache.keys.contains(key) || self.defaults.object(forKey: key) != nil
}
}
public func remove(_ key: String) {
self.setValue(nil, for: key)
}
public func bool(key: String, defaultValue value: Bool) -> Bool {
return self.getValue(for: key, type: Bool.self) ?? value
}
public func string(key: String, defaultValue value: String) -> String {
return self.getValue(for: key, type: String.self) ?? value
}
public func int(key: String, defaultValue value: Int) -> Int {
return self.getValue(for: key, type: Int.self) ?? value
}
public func array(key: String, defaultValue value: [Any]) -> [Any] {
return self.getValue(for: key, type: [Any].self) ?? value
}
public func data(key: String) -> Data? {
return self.getValue(for: key, type: Data.self)
}
public func set(key: String, value: Bool) {
self.setValue(value, for: key)
}
public func set(key: String, value: String) {
self.setValue(value, for: key)
}
public func set(key: String, value: Int) {
self.setValue(value, for: key)
}
public func set(key: String, value: Data) {
self.setValue(value, for: key)
}
public func set(key: String, value: [Any]) {
self.setValue(value, for: key)
}
public func reset() {
self.cacheQueue.async(flags: .barrier) {
self.cache.removeAll()
}
self.defaults.dictionaryRepresentation().keys.forEach { key in
self.defaults.removeObject(forKey: key)
}
}
public func export(to url: URL) {
guard let id = Bundle.main.bundleIdentifier,
var dictionary = self.defaults.persistentDomain(forName: id) else { return }
dictionary.removeValue(forKey: "remote_id")
dictionary.removeValue(forKey: "access_token")
dictionary.removeValue(forKey: "refresh_token")
NSDictionary(dictionary: dictionary).write(to: url, atomically: true)
}
public func `import`(from url: URL) {
guard let id = Bundle.main.bundleIdentifier,
let dict = NSDictionary(contentsOf: url) as? [String: Any] else { return }
let keysToPreserve = ["remote_id", "access_token", "refresh_token"]
var importedDict = dict
for key in keysToPreserve {
if let existingValue = getValue(for: key, type: String.self) {
importedDict[key] = existingValue
}
}
self.cacheQueue.async(flags: .barrier) {
self.cache = importedDict
}
self.defaults.setPersistentDomain(importedDict, forName: id)
restartApp(self)
}
}
================================================
FILE: Kit/plugins/SystemKit.swift
================================================
//
// SystemKit.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 13/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public enum Platform: String, Codable {
case intel
case m1
case m1Pro
case m1Max
case m1Ultra
case m2
case m2Pro
case m2Max
case m2Ultra
case m3
case m3Pro
case m3Max
case m3Ultra
case m4
case m4Pro
case m4Max
case m4Ultra
case m5
case m5Pro
case m5Max
case m5Ultra
public static var apple: [Platform] {
return [
.m1, .m1Pro, .m1Max, .m1Ultra,
.m2, .m2Pro, .m2Max, .m2Ultra,
.m3, .m3Pro, .m3Max, .m3Ultra,
.m4, .m4Pro, .m4Max, .m4Ultra,
.m5, .m5Pro, .m5Max, .m5Ultra
]
}
public static var m1Gen: [Platform] {
return [.m1, .m1Pro, .m1Max, .m1Ultra]
}
public static var m2Gen: [Platform] {
return [.m2, .m2Pro, .m2Max, .m2Ultra]
}
public static var m3Gen: [Platform] {
return [.m3, .m3Pro, .m3Max, .m3Ultra]
}
public static var m4Gen: [Platform] {
return [.m4, .m4Pro, .m4Max, .m4Ultra]
}
public static var m5Gen: [Platform] {
return [.m5, .m5Pro, .m5Max, .m5Ultra]
}
public static var all: [Platform] {
return apple + [.intel]
}
}
public enum deviceType: String {
case unknown
case macMini
case macPro
case iMac
case iMacPro
case macbook
case macbookNeo
case macbookAir
case macbookPro
case macStudio
public static var all: [deviceType] {
return [.macMini, .macPro, .iMac, .iMacPro, .macbook, .macbookNeo, .macbookAir, .macbookPro, .macStudio]
}
}
public enum coreType: Int, Codable {
case unknown = -1
case efficiency = 1
case performance = 2
}
public struct model_s {
public var id: String = ""
public let name: String
public let year: Int
public let type: deviceType
public var icon: NSImage = NSImage(named: NSImage.Name("imacPro"))!
}
public struct os_s {
public let name: String
public let version: OperatingSystemVersion
public let build: String
}
public struct core_s: Codable {
public var id: Int32
public var type: coreType
}
public struct cpu_s: Codable {
public var name: String? = nil
public var physicalCores: Int8? = nil
public var logicalCores: Int8? = nil
public var eCores: Int32? = nil
public var pCores: Int32? = nil
public var cores: [core_s]? = nil
public var eCoreFrequencies: [Int32]? = nil
public var pCoreFrequencies: [Int32]? = nil
}
public struct dimm_s: Codable {
public var bank: Int? = nil
public var channel: String? = nil
public var type: String? = nil
public var size: String? = nil
public var speed: String? = nil
}
public struct ram_s: Codable {
public var dimms: [dimm_s] = []
}
public struct gpu_s: Codable {
public var id: String? = nil
public var name: String? = nil
public var vendor: String? = nil
public var vram: String? = nil
public var cores: Int? = nil
public var frequencies: [Int32]? = nil
}
public struct disk_s: Codable {
public var id: String? = nil
public var name: String? = nil
public var size: Int64? = nil
}
public struct display_s: Codable {
public var id: String? = nil
public var name: String? = nil
public var resolution: CGSize? = nil
public var size: Double? = nil
public var refreshRate: Double? = nil
public var isBuiltIn: Bool? = nil
public var isMain: Bool? = nil
public var vendor: String? = nil
public var model: String? = nil
public var serialNumber: String? = nil
}
public struct info_s {
public var cpu: cpu_s? = nil
public var ram: ram_s? = nil
public var gpu: [gpu_s]? = nil
public var disk: [disk_s]? = nil
}
public struct device_s {
public var model: model_s = model_s(
name: localizedString("Unknown"),
year: Calendar.current.component(.year, from: Date()),
type: .unknown,
)
public var arch: String = "unknown"
public var serialNumber: String? = nil
public var bootDate: Date? = nil
public var os: os_s? = nil
public var info: info_s = info_s()
public var platform: Platform? = nil
public var display: [display_s]? = nil
}
public class SystemKit {
public static let shared = SystemKit()
public var device: device_s = device_s()
public init() {
let (modelID, serialNumber) = self.modelAndSerialNumber()
if let serialNumber {
self.device.serialNumber = serialNumber
}
if let modelName = modelID ?? self.getModelID(), let model = deviceDict[modelName] {
self.device.model = model
self.device.model.id = modelName
self.device.model.icon = self.getIcon(type: self.device.model.type, year: self.device.model.year)
} else if let model = self.getModel() {
self.device.model = model
}
#if arch(x86_64)
self.device.arch = "x86_64"
#elseif arch(arm64)
self.device.arch = "arm64"
#endif
self.device.bootDate = self.bootDate()
let procInfo = ProcessInfo()
let systemVersion = procInfo.operatingSystemVersion
var build = localizedString("Unknown")
let buildArr = procInfo.operatingSystemVersionString.split(separator: "(")
if buildArr.indices.contains(1) {
build = buildArr[1].replacingOccurrences(of: "Build ", with: "").replacingOccurrences(of: ")", with: "")
}
let version = systemVersion.majorVersion > 10 ? "\(systemVersion.majorVersion)" : "\(systemVersion.majorVersion).\(systemVersion.minorVersion)"
self.device.os = os_s(name: osDict[version] ?? localizedString("Unknown"), version: systemVersion, build: build)
self.device.info.cpu = self.getCPUInfo()
self.device.info.ram = self.getRamInfo()
self.device.info.gpu = self.getGPUInfo()
self.device.info.disk = self.getDiskInfo()
self.device.platform = self.getPlatform()
self.device.display = self.getDisplayInfo()
}
public func getModelID() -> String? {
var mib = [CTL_HW, HW_MODEL]
var size = MemoryLayout.size
let pointer = UnsafeMutablePointer.allocate(capacity: 1)
defer {
pointer.deallocate()
}
let result = sysctl(&mib, u_int(mib.count), pointer, &size, nil, 0)
if result == KERN_SUCCESS {
return String(cString: UnsafeRawPointer(pointer).assumingMemoryBound(to: CChar.self))
}
error("error call sysctl(): \(String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error")")
return nil
}
func modelAndSerialNumber() -> (String?, String?) {
let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"))
var modelIdentifier: String?
if let property = IORegistryEntryCreateCFProperty(service, "model" as CFString, kCFAllocatorDefault, 0), let value = property.takeUnretainedValue() as? Data {
modelIdentifier = String(data: value, encoding: .utf8)?.trimmingCharacters(in: .controlCharacters)
}
var serialNumber: String?
if let property = IORegistryEntryCreateCFProperty(service, kIOPlatformSerialNumberKey as CFString, kCFAllocatorDefault, 0), let value = property.takeUnretainedValue() as? String {
serialNumber = value.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
}
IOObjectRelease(service)
return (modelIdentifier, serialNumber)
}
func bootDate() -> Date? {
var mib = [CTL_KERN, KERN_BOOTTIME]
var bootTime = timeval()
var bootTimeSize = MemoryLayout.size
let result = sysctl(&mib, UInt32(mib.count), &bootTime, &bootTimeSize, nil, 0)
if result == KERN_SUCCESS {
return Date(timeIntervalSince1970: Double(bootTime.tv_sec) + Double(bootTime.tv_usec) / 1_000_000.0)
}
error("error get boot time: \(String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error")")
return nil
}
private func getCPUInfo() -> cpu_s? {
var cpu = cpu_s()
var sizeOfName = 0
sysctlbyname("machdep.cpu.brand_string", nil, &sizeOfName, nil, 0)
var nameChars = [CChar](repeating: 0, count: sizeOfName)
sysctlbyname("machdep.cpu.brand_string", &nameChars, &sizeOfName, nil, 0)
var name = String(cString: nameChars)
if name != "" {
name = name.replacingOccurrences(of: "(TM)", with: "")
name = name.replacingOccurrences(of: "(R)", with: "")
name = name.replacingOccurrences(of: "CPU", with: "")
name = name.replacingOccurrences(of: "@", with: "")
cpu.name = name.condenseWhitespace()
}
var size = UInt32(MemoryLayout.size / MemoryLayout.size)
let hostInfo = host_basic_info_t.allocate(capacity: 1)
defer {
hostInfo.deallocate()
}
let result = hostInfo.withMemoryRebound(to: integer_t.self, capacity: Int(size)) {
host_info(mach_host_self(), HOST_BASIC_INFO, $0, &size)
}
if result != KERN_SUCCESS {
error("read cores number: \(String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error")")
return nil
}
let data = hostInfo.move()
cpu.physicalCores = Int8(data.physical_cpu)
cpu.logicalCores = Int8(data.logical_cpu)
if let cores = getCPUCores() {
cpu.eCores = cores.0
cpu.pCores = cores.1
cpu.cores = cores.2
}
if let freq = getFrequencies(cpuName: cpu.name ?? "") {
cpu.eCoreFrequencies = freq.0
cpu.pCoreFrequencies = freq.1
}
return cpu
}
func getCPUCores() -> (Int32?, Int32?, [core_s])? {
var iterator: io_iterator_t = io_iterator_t()
let result = IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching("AppleARMPE"), &iterator)
if result != kIOReturnSuccess {
print("Error find AppleARMPE: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
}
var service: io_registry_entry_t = 1
var list: [core_s] = []
var pCores: Int32? = nil
var eCores: Int32? = nil
while service != 0 {
service = IOIteratorNext(iterator)
var entry: io_iterator_t = io_iterator_t()
if IORegistryEntryGetChildIterator(service, kIOServicePlane, &entry) != kIOReturnSuccess {
continue
}
var child: io_registry_entry_t = 1
while child != 0 {
child = IOIteratorNext(entry)
guard child != 0 else {
continue
}
guard let name = getIOName(child),
let props = getIOProperties(child) else { continue }
if name.matches("^cpu\\d") {
var type: coreType = .unknown
if let rawType = props.object(forKey: "cluster-type") as? Data,
let typ = String(data: rawType, encoding: .utf8)?.trimmed {
switch typ {
case "E":
type = .efficiency
case "P":
type = .performance
default:
type = .unknown
}
}
let rawCPUId = props.object(forKey: "cpu-id") as? Data
let id = rawCPUId?.withUnsafeBytes { pointer in
return pointer.load(as: Int32.self)
}
list.append(core_s(id: id ?? -1, type: type))
} else if name.trimmed == "cpus" {
eCores = (props.object(forKey: "e-core-count") as? Data)?.withUnsafeBytes { pointer in
return pointer.load(as: Int32.self)
}
pCores = (props.object(forKey: "p-core-count") as? Data)?.withUnsafeBytes { pointer in
return pointer.load(as: Int32.self)
}
}
IOObjectRelease(child)
}
IOObjectRelease(entry)
IOObjectRelease(service)
}
IOObjectRelease(iterator)
return (eCores, pCores, list)
}
private func getGPUInfo() -> [gpu_s]? {
guard let res = process(path: "/usr/sbin/system_profiler", arguments: ["SPDisplaysDataType", "-json"]) else {
return nil
}
var list: [gpu_s] = []
var i = 0
do {
if let json = try JSONSerialization.jsonObject(with: Data(res.utf8), options: []) as? [String: Any] {
if let arr = json["SPDisplaysDataType"] as? [[String: Any]] {
for obj in arr {
var gpu: gpu_s = gpu_s()
gpu.id = "\(i)"
gpu.name = obj["sppci_model"] as? String
gpu.vendor = obj["spdisplays_vendor"] as? String
gpu.cores = Int(obj["sppci_cores"] as? String ?? "")
if let vram = obj["spdisplays_vram_shared"] as? String {
gpu.vram = vram
} else if let vram = obj["spdisplays_vram"] as? String {
gpu.vram = vram
}
list.append(gpu)
i += 1
}
}
}
} catch let err as NSError {
error("error to parse system_profiler SPDisplaysDataType: \(err.localizedDescription)")
return nil
}
return list
}
private func getDiskInfo() -> [disk_s]? {
var bootableDisks: [disk_s] = []
guard let output = process(path: "/usr/sbin/diskutil", arguments: ["list", "-plist"]) else {
return nil
}
do {
if let data = output.data(using: .utf8),
let plist = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any],
let allDisksAndPartitions = plist["AllDisksAndPartitions"] as? [[String: Any]] {
for disk in allDisksAndPartitions {
if let partitions = disk["Partitions"] as? [[String: Any]] {
for partition in partitions {
if let bootable = partition["Bootable"] as? Bool, bootable {
var bootableDisk = disk_s()
if let id = partition["DiskUUID"] as? String {
bootableDisk.id = id
} else if let deviceIdentifier = partition["DeviceIdentifier"] as? String {
bootableDisk.id = "/dev/" + deviceIdentifier
}
if let volumeName = partition["VolumeName"] as? String {
bootableDisk.name = volumeName
}
if let size = partition["Size"] as? Int64 {
bootableDisk.size = size
}
if bootableDisk.id != nil {
bootableDisks.append(bootableDisk)
}
}
}
}
if let contentType = disk["Content"] as? String, contentType == "Apple_APFS_Container" || contentType == "Apple_CoreStorage" {
if let deviceIdentifier = disk["DeviceIdentifier"] as? String,
let infoOutput = process(path: "/usr/sbin/diskutil", arguments: ["info", "-plist", deviceIdentifier]),
let infoData = infoOutput.data(using: .utf8),
let info = try PropertyListSerialization.propertyList(from: infoData, options: [], format: nil) as? [String: Any] {
if let isBootDisk = info["BootableVolume"] as? Bool, isBootDisk {
var bootableDisk = disk_s()
if let id = info["DiskUUID"] as? String {
bootableDisk.id = id
} else {
bootableDisk.id = "/dev/" + deviceIdentifier
}
if let name = info["VolumeName"] as? String {
bootableDisk.name = name
} else if let name = disk["DeviceIdentifier"] as? String {
bootableDisk.name = name
}
if let size = disk["Size"] as? Int64 {
bootableDisk.size = size
} else if let total = info["TotalSize"] as? Int64 {
bootableDisk.size = total
}
if bootableDisk.id != nil {
bootableDisks.append(bootableDisk)
}
}
}
}
}
}
} catch {
print("Error parsing diskutil output: \(error)")
return nil
}
if bootableDisks.isEmpty {
if let startupDiskInfo = process(path: "/usr/sbin/diskutil", arguments: ["info", "-plist", "/"]) {
if let data = startupDiskInfo.data(using: .utf8),
let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] {
var bootDisk = disk_s()
if let id = plist["DiskUUID"] as? String {
bootDisk.id = id
} else if let deviceNode = plist["DeviceNode"] as? String {
bootDisk.id = deviceNode
}
if let volumeName = plist["VolumeName"] as? String {
bootDisk.name = volumeName
}
if let totalSize = plist["TotalSize"] as? Int64 {
bootDisk.size = totalSize
}
if bootDisk.id != nil {
bootableDisks.append(bootDisk)
}
}
}
}
return bootableDisks
}
private func getFrequencies(cpuName: String) -> ([Int32], [Int32])? {
var iterator = io_iterator_t()
let result = IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching("AppleARMIODevice"), &iterator)
if result != kIOReturnSuccess {
print("Error find AppleARMIODevice: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
}
let chipsToMatch = ["m4", "m5"]
let isCpuStartFromM4 = chipsToMatch.contains { cpuName.lowercased().contains($0) }
var eFreq: [Int32] = []
var pFreq: [Int32] = []
while case let child = IOIteratorNext(iterator), child != 0 {
defer { IOObjectRelease(child) }
guard let name = getIOName(child), name == "pmgr", let props = getIOProperties(child) else { continue }
if let data = props.value(forKey: "voltage-states1-sram") {
eFreq = convertCFDataToArr(data as! CFData, isCpuStartFromM4)
}
if let data = props.value(forKey: "voltage-states5-sram") {
pFreq = convertCFDataToArr(data as! CFData, isCpuStartFromM4)
}
}
return (eFreq, pFreq)
}
public func getRamInfo() -> ram_s? {
guard let res = process(path: "/usr/sbin/system_profiler", arguments: ["SPMemoryDataType", "-json"]) else {
return nil
}
do {
if let json = try JSONSerialization.jsonObject(with: Data(res.utf8), options: []) as? [String: Any] {
var ram: ram_s = ram_s()
if let obj = json["SPMemoryDataType"] as? [[String: Any]], !obj.isEmpty {
if let items = obj[0]["_items"] as? [[String: Any]] {
for i in 0.. NSImage {
switch type {
case .macMini:
if year >= 2024 {
return NSImage(named: NSImage.Name("macMini2024"))!
}
if year >= 2020 && year <= 2023 {
return NSImage(named: NSImage.Name("macMini2020"))!
}
return NSImage(named: NSImage.Name("macMini"))!
case .macStudio:
return NSImage(named: NSImage.Name("macStudio"))!
case .iMacPro:
return NSImage(named: NSImage.Name("imacPro"))!
case .macPro:
switch year {
case 2019:
return NSImage(named: NSImage.Name("macPro2019"))!
default:
return NSImage(named: NSImage.Name("macPro"))!
}
case .iMac:
return NSImage(named: NSImage.Name("imac"))!
case .macbook:
return NSImage(named: NSImage.Name("macbookAir"))!
case .macbookNeo:
return NSImage(named: NSImage.Name("macbookNeo"))!
case .macbookAir:
if year >= 2022 {
return NSImage(named: NSImage.Name("macbookAir"))!
}
return NSImage(named: NSImage.Name("macbookAir4thGen"))!
case .macbookPro:
if year >= 2021 {
return NSImage(named: NSImage.Name("macbookPro5thGen"))!
}
return NSImage(named: NSImage.Name("macbookPro"))!
default:
return NSImage(named: NSImage.Name("imacPro"))!
}
}
private func getModel() -> model_s? {
guard let res = process(path: "/usr/sbin/system_profiler", arguments: ["SPHardwareDataType", "-json"]) else {
return nil
}
do {
if let json = try JSONSerialization.jsonObject(with: Data(res.utf8), options: []) as? [String: Any],
let obj = json["SPHardwareDataType"] as? [[String: Any]], !obj.isEmpty, let val = obj.first,
let name = val["machine_name"] as? String, let model = val["machine_model"] as? String, let cpu = val["chip_type"] as? String {
let year = Calendar.current.component(.year, from: Date())
let type = deviceType.all.first{ $0.rawValue.lowercased() == name.lowercased().removingWhitespaces() } ?? .unknown
return model_s(
id: model,
name: "\(name) (\(cpu.removedRegexMatches(pattern: "Apple ", replaceWith: "")))",
year: year,
type: type,
icon: self.getIcon(type: type, year: year)
)
}
} catch let err as NSError {
error("error to parse system_profiler SPHardwareDataType: \(err.localizedDescription)")
return nil
}
return nil
}
private func getPlatform() -> Platform? {
if let name = self.device.info.cpu?.name?.lowercased() {
if name.contains("intel") {
return .intel
} else if name.contains("m1") {
if name.contains("pro") {
return .m1Pro
} else if name.contains("max") {
return .m1Max
} else if name.contains("ultra") {
return .m1Ultra
} else {
return .m1
}
} else if name.contains("m2") {
if name.contains("pro") {
return .m2Pro
} else if name.contains("max") {
return .m2Max
} else if name.contains("ultra") {
return .m2Ultra
} else {
return .m2
}
} else if name.contains("m3") {
if name.contains("pro") {
return .m3Pro
} else if name.contains("max") {
return .m3Max
} else if name.contains("ultra") {
return .m3Ultra
} else {
return .m3
}
} else if name.contains("m4") {
if name.contains("pro") {
return .m4Pro
} else if name.contains("max") {
return .m4Max
} else if name.contains("ultra") {
return .m4Ultra
} else {
return .m4
}
} else if name.contains("m5") {
if name.contains("pro") {
return .m5Pro
} else if name.contains("max") {
return .m5Max
} else if name.contains("ultra") {
return .m5Ultra
} else {
return .m5
}
}
}
return nil
}
private func getDisplayInfo() -> [display_s]? {
var displays: [display_s] = []
for screen in NSScreen.screens {
guard let displayID = screen.deviceDescription[NSDeviceDescriptionKey(rawValue: "NSScreenNumber")] as? CGDirectDisplayID else {
continue
}
let mmSize = CGDisplayScreenSize(displayID)
let widthInches = mmSize.width / 25.4
let heightInches = mmSize.height / 25.4
let diagonal = sqrt(widthInches * widthInches + heightInches * heightInches)
var display = display_s(
id: String(displayID),
size: diagonal.rounded(),
isBuiltIn: CGDisplayIsBuiltin(displayID) != 0,
isMain: CGDisplayIsMain(displayID) != 0,
vendor: String(CGDisplayVendorNumber(displayID)),
model: String(CGDisplayModelNumber(displayID)),
serialNumber: String(CGDisplaySerialNumber(displayID))
)
if let mode = CGDisplayCopyDisplayMode(displayID) {
let pw = mode.pixelWidth
let ph = mode.pixelHeight
let width = pw > 0 ? pw : CGDisplayPixelsWide(displayID)
let height = ph > 0 ? ph : CGDisplayPixelsHigh(displayID)
display.resolution = CGSize(width: width, height: height)
let hz = mode.refreshRate
display.refreshRate = hz > 0 ? hz : nil
} else {
let width = CGDisplayPixelsWide(displayID)
let height = CGDisplayPixelsHigh(displayID)
if width > 0 && height > 0 {
display.resolution = CGSize(width: width, height: height)
}
}
display.name = screen.localizedName
if display.name == nil {
if display.isMain == true {
display.name = display.isBuiltIn == true ? "Built-in Display" : "External Display (Main)"
} else {
display.name = display.isBuiltIn == true ? "Built-in Display" : "External Display"
}
}
displays.append(display)
}
return displays.isEmpty ? nil : displays
}
}
let deviceDict: [String: model_s] = [
// Mac Mini
"Macmini1,1": model_s(name: "Mac mini", year: 2006, type: .macMini),
"Macmini2,1": model_s(name: "Mac mini", year: 2007, type: .macMini),
"Macmini3,1": model_s(name: "Mac mini", year: 2009, type: .macMini),
"Macmini4,1": model_s(name: "Mac mini", year: 2010, type: .macMini),
"Macmini5,1": model_s(name: "Mac mini", year: 2011, type: .macMini),
"Macmini5,2": model_s(name: "Mac mini", year: 2011, type: .macMini),
"Macmini5,3": model_s(name: "Mac mini", year: 2011, type: .macMini),
"Macmini6,1": model_s(name: "Mac mini", year: 2012, type: .macMini),
"Macmini6,2": model_s(name: "Mac mini", year: 2012, type: .macMini),
"Macmini7,1": model_s(name: "Mac mini", year: 2014, type: .macMini),
"Macmini8,1": model_s(name: "Mac mini", year: 2018, type: .macMini),
"Macmini9,1": model_s(name: "Mac mini (M1)", year: 2020, type: .macMini),
"Mac14,3": model_s(name: "Mac mini (M2)", year: 2023, type: .macMini),
"Mac14,12": model_s(name: "Mac mini (M2 Pro)", year: 2023, type: .macMini),
"Mac16,10": model_s(name: "Mac mini (M4)", year: 2024, type: .macMini),
"Mac16,11": model_s(name: "Mac mini (M4 Pro)", year: 2024, type: .macMini),
// Mac Studio
"Mac13,1": model_s(name: "Mac Studio (M1 Max)", year: 2022, type: .macStudio),
"Mac13,2": model_s(name: "Mac Studio (M1 Ultra)", year: 2022, type: .macStudio),
"Mac14,13": model_s(name: "Mac Studio (M2 Max)", year: 2023, type: .macStudio),
"Mac14,14": model_s(name: "Mac Studio (M2 Ultra)", year: 2023, type: .macStudio),
"Mac15,14": model_s(name: "Mac Studio (M3 Max)", year: 2023, type: .macStudio),
"Mac16,9": model_s(name: "Mac Studio (M4 Max)", year: 2024, type: .macStudio),
// Mac Pro
"MacPro1,1": model_s(name: "Mac Pro", year: 2006, type: .macPro),
"MacPro2,1": model_s(name: "Mac Pro", year: 2007, type: .macPro),
"MacPro3,1": model_s(name: "Mac Pro", year: 2008, type: .macPro),
"MacPro4,1": model_s(name: "Mac Pro", year: 2009, type: .macPro),
"MacPro5,1": model_s(name: "Mac Pro", year: 2010, type: .macPro),
"MacPro6,1": model_s(name: "Mac Pro", year: 2016, type: .macPro),
"MacPro7,1": model_s(name: "Mac Pro", year: 2019, type: .macPro),
"Mac14,8": model_s(name: "Mac Pro (M2 Ultra)", year: 2023, type: .macPro),
// iMac
"iMac10,1": model_s(name: "iMac 21.5-Inch", year: 2009, type: .iMac),
"iMac11,2": model_s(name: "iMac 21.5-Inch", year: 2010, type: .iMac),
"iMac11,3": model_s(name: "iMac 27-Inch", year: 2010, type: .iMac),
"iMac12,1": model_s(name: "iMac 21.5-Inch", year: 2011, type: .iMac),
"iMac12,2": model_s(name: "iMac 27-Inch", year: 2011, type: .iMac),
"iMac13,1": model_s(name: "iMac 21.5-Inch", year: 2012, type: .iMac),
"iMac13,2": model_s(name: "iMac 27-Inch", year: 2012, type: .iMac),
"iMac14,1": model_s(name: "iMac 21.5-Inch", year: 2013, type: .iMac),
"iMac14,2": model_s(name: "iMac 27-Inch", year: 2013, type: .iMac),
"iMac14,3": model_s(name: "iMac 21.5-Inch", year: 2013, type: .iMac),
"iMac14,4": model_s(name: "iMac 21.5-Inch", year: 2014, type: .iMac),
"iMac15,1": model_s(name: "iMac 27-Inch", year: 2014, type: .iMac),
"iMac16,1": model_s(name: "iMac 21.5-Inch", year: 2015, type: .iMac),
"iMac16,2": model_s(name: "iMac 21.5-Inch", year: 2015, type: .iMac),
"iMac17,1": model_s(name: "iMac 27-Inch", year: 2015, type: .iMac),
"iMac18,1": model_s(name: "iMac 21.5-Inch", year: 2017, type: .iMac),
"iMac18,2": model_s(name: "iMac 21.5-Inch", year: 2017, type: .iMac),
"iMac18,3": model_s(name: "iMac 27-Inch", year: 2017, type: .iMac),
"iMac19,1": model_s(name: "iMac 27-Inch", year: 2019, type: .iMac),
"iMac19,2": model_s(name: "iMac 21.5-Inch", year: 2019, type: .iMac),
"iMac20,1": model_s(name: "iMac 27-Inch", year: 2020, type: .iMac),
"iMac20,2": model_s(name: "iMac 27-Inch", year: 2020, type: .iMac),
"iMac21,1": model_s(name: "iMac 24-Inch (M1)", year: 2021, type: .iMac),
"iMac21,2": model_s(name: "iMac 24-Inch (M1)", year: 2021, type: .iMac),
"Mac15,4": model_s(name: "iMac 24-Inch (M3, 8 CPU/8 GPU)", year: 2023, type: .iMac),
"Mac15,5": model_s(name: "iMac 24-Inch (M3, 8 CPU/10 GPU)", year: 2023, type: .iMac),
"Mac16,2": model_s(name: "iMac 24-Inch (M4, 8 CPU/8 GPU)", year: 2024, type: .iMac),
"Mac16,3": model_s(name: "iMac 24-Inch (M4, 10 CPU/10 GPU)", year: 2024, type: .iMac),
// iMac Pro
"iMacPro1,1": model_s(name: "iMac Pro", year: 2017, type: .iMacPro),
// MacBook
"MacBook8,1": model_s(name: "MacBook", year: 2015, type: .macbook),
"MacBook9,1": model_s(name: "MacBook", year: 2016, type: .macbook),
"MacBook10,1": model_s(name: "MacBook", year: 2017, type: .macbook),
// MacBook Neo
"Mac17,5": model_s(name: "MacBook Neo", year: 2026, type: .macbookNeo),
// MacBook Air
"MacBookAir1,1": model_s(name: "MacBook Air 13\"", year: 2008, type: .macbookAir),
"MacBookAir2,1": model_s(name: "MacBook Air 13\"", year: 2009, type: .macbookAir),
"MacBookAir3,1": model_s(name: "MacBook Air 11\"", year: 2010, type: .macbookAir),
"MacBookAir3,2": model_s(name: "MacBook Air 13\"", year: 2010, type: .macbookAir),
"MacBookAir4,1": model_s(name: "MacBook Air 11\"", year: 2011, type: .macbookAir),
"MacBookAir4,2": model_s(name: "MacBook Air 13\"", year: 2011, type: .macbookAir),
"MacBookAir5,1": model_s(name: "MacBook Air 11\"", year: 2012, type: .macbookAir),
"MacBookAir5,2": model_s(name: "MacBook Air 13\"", year: 2012, type: .macbookAir),
"MacBookAir6,1": model_s(name: "MacBook Air 11\"", year: 2014, type: .macbookAir),
"MacBookAir6,2": model_s(name: "MacBook Air 13\"", year: 2014, type: .macbookAir),
"MacBookAir7,1": model_s(name: "MacBook Air 11\"", year: 2015, type: .macbookAir),
"MacBookAir7,2": model_s(name: "MacBook Air 13\"", year: 2015, type: .macbookAir),
"MacBookAir8,1": model_s(name: "MacBook Air 13\"", year: 2018, type: .macbookAir),
"MacBookAir8,2": model_s(name: "MacBook Air 13\"", year: 2019, type: .macbookAir),
"MacBookAir9,1": model_s(name: "MacBook Air 13\"", year: 2020, type: .macbookAir),
"MacBookAir10,1": model_s(name: "MacBook Air 13\" (M1)", year: 2020, type: .macbookAir),
"Mac14,2": model_s(name: "MacBook Air 13\" (M2)", year: 2022, type: .macbookAir),
"Mac14,15": model_s(name: "MacBook Air 15\" (M2)", year: 2023, type: .macbookAir),
"Mac15,12": model_s(name: "MacBook Air 13\" (M3)", year: 2024, type: .macbookAir),
"Mac15,13": model_s(name: "MacBook Air 15\" (M3)", year: 2024, type: .macbookAir),
"Mac16,12": model_s(name: "MacBook Air 13\" (M4)", year: 2025, type: .macbookAir),
"Mac16,13": model_s(name: "MacBook Air 15\" (M4)", year: 2025, type: .macbookAir),
"Mac17,2": model_s(name: "MacBook Air 14\" (M5)", year: 2026, type: .macbookAir),
"Mac17,3": model_s(name: "MacBook Air 13\" (M5)", year: 2026, type: .macbookAir),
"Mac17,4": model_s(name: "MacBook Air 15\" (M5)", year: 2026, type: .macbookAir),
// MacBook Pro
"MacBookPro1,1": model_s(name: "MacBook Pro 15\"", year: 2006, type: .macbookPro),
"MacBookPro1,2": model_s(name: "MacBook Pro 17\"", year: 2006, type: .macbookPro),
"MacBookPro2,1": model_s(name: "MacBook Pro 17\"", year: 2006, type: .macbookPro),
"MacBookPro2,2": model_s(name: "MacBook Pro 15\"", year: 2006, type: .macbookPro),
"MacBookPro3,1": model_s(name: "MacBook Pro", year: 2007, type: .macbookPro),
"MacBookPro4,1": model_s(name: "MacBook Pro", year: 2008, type: .macbookPro),
"MacBookPro5,1": model_s(name: "MacBook Pro 15\"", year: 2008, type: .macbookPro),
"MacBookPro5,2": model_s(name: "MacBook Pro 17\"", year: 2009, type: .macbookPro),
"MacBookPro5,3": model_s(name: "MacBook Pro 15\"", year: 2009, type: .macbookPro),
"MacBookPro5,4": model_s(name: "MacBook Pro 15\"", year: 2009, type: .macbookPro),
"MacBookPro5,5": model_s(name: "MacBook Pro 13\"", year: 2009, type: .macbookPro),
"MacBookPro6,1": model_s(name: "MacBook Pro 17\"", year: 2010, type: .macbookPro),
"MacBookPro6,2": model_s(name: "MacBook Pro 15\"", year: 2010, type: .macbookPro),
"MacBookPro7,1": model_s(name: "MacBook Pro 13\"", year: 2010, type: .macbookPro),
"MacBookPro8,1": model_s(name: "MacBook Pro 13\"", year: 2011, type: .macbookPro),
"MacBookPro8,2": model_s(name: "MacBook Pro 15\"", year: 2011, type: .macbookPro),
"MacBookPro8,3": model_s(name: "MacBook Pro 17\"", year: 2011, type: .macbookPro),
"MacBookPro9,1": model_s(name: "MacBook Pro 15\"", year: 2012, type: .macbookPro),
"MacBookPro9,2": model_s(name: "MacBook Pro 13\"", year: 2012, type: .macbookPro),
"MacBookPro10,1": model_s(name: "MacBook Pro 15\"", year: 2012, type: .macbookPro),
"MacBookPro10,2": model_s(name: "MacBook Pro 13\"", year: 2012, type: .macbookPro),
"MacBookPro11,1": model_s(name: "MacBook Pro 13\"", year: 2014, type: .macbookPro),
"MacBookPro11,2": model_s(name: "MacBook Pro 15\"", year: 2014, type: .macbookPro),
"MacBookPro11,3": model_s(name: "MacBook Pro 15\"", year: 2014, type: .macbookPro),
"MacBookPro11,4": model_s(name: "MacBook Pro 15\"", year: 2015, type: .macbookPro),
"MacBookPro11,5": model_s(name: "MacBook Pro 15\"", year: 2015, type: .macbookPro),
"MacBookPro12,1": model_s(name: "MacBook Pro 13\"", year: 2015, type: .macbookPro),
"MacBookPro13,1": model_s(name: "MacBook Pro 13\"", year: 2016, type: .macbookPro),
"MacBookPro13,2": model_s(name: "MacBook Pro 13\"", year: 2016, type: .macbookPro),
"MacBookPro13,3": model_s(name: "MacBook Pro 15\"", year: 2016, type: .macbookPro),
"MacBookPro14,1": model_s(name: "MacBook Pro 13\"", year: 2017, type: .macbookPro),
"MacBookPro14,2": model_s(name: "MacBook Pro 13\"", year: 2017, type: .macbookPro),
"MacBookPro14,3": model_s(name: "MacBook Pro 15\"", year: 2017, type: .macbookPro),
"MacBookPro15,1": model_s(name: "MacBook Pro 15\"", year: 2018, type: .macbookPro),
"MacBookPro15,2": model_s(name: "MacBook Pro 13\"", year: 2019, type: .macbookPro),
"MacBookPro15,3": model_s(name: "MacBook Pro 15\"", year: 2019, type: .macbookPro),
"MacBookPro15,4": model_s(name: "MacBook Pro 13\"", year: 2019, type: .macbookPro),
"MacBookPro16,1": model_s(name: "MacBook Pro 16\"", year: 2019, type: .macbookPro),
"MacBookPro16,2": model_s(name: "MacBook Pro 13\"", year: 2019, type: .macbookPro),
"MacBookPro16,3": model_s(name: "MacBook Pro 13\"", year: 2020, type: .macbookPro),
"MacBookPro16,4": model_s(name: "MacBook Pro 16\"", year: 2019, type: .macbookPro),
"MacBookPro17,1": model_s(name: "MacBook Pro 13\" (M1)", year: 2020, type: .macbookPro),
"MacBookPro18,1": model_s(name: "MacBook Pro 16\" (M1 Pro)", year: 2021, type: .macbookPro),
"MacBookPro18,2": model_s(name: "MacBook Pro 16\" (M1 Max)", year: 2021, type: .macbookPro),
"MacBookPro18,3": model_s(name: "MacBook Pro 14\" (M1 Pro)", year: 2021, type: .macbookPro),
"MacBookPro18,4": model_s(name: "MacBook Pro 14\" (M1 Max)", year: 2021, type: .macbookPro),
"Mac14,7": model_s(name: "MacBook Pro 13\" (M2)", year: 2022, type: .macbookPro),
"Mac14,5": model_s(name: "MacBook Pro 14\" (M2 Max)", year: 2023, type: .macbookPro),
"Mac14,6": model_s(name: "MacBook Pro 16\" (M2 Max)", year: 2023, type: .macbookPro),
"Mac14,9": model_s(name: "MacBook Pro 14\" (M2 Pro)", year: 2023, type: .macbookPro),
"Mac14,10": model_s(name: "MacBook Pro 16\" (M2 Pro)", year: 2023, type: .macbookPro),
"Mac15,3": model_s(name: "MacBook Pro 14\" (M3)", year: 2023, type: .macbookPro),
"Mac15,6": model_s(name: "MacBook Pro 14\" (M3 Pro)", year: 2023, type: .macbookPro),
"Mac15,7": model_s(name: "MacBook Pro 16\" (M3 Pro)", year: 2023, type: .macbookPro),
"Mac15,8": model_s(name: "MacBook Pro 14\" (M3 Max)", year: 2023, type: .macbookPro),
"Mac15,9": model_s(name: "MacBook Pro 16\" (M3 Max)", year: 2023, type: .macbookPro),
"Mac15,10": model_s(name: "MacBook Pro 14\" (M3 Max)", year: 2023, type: .macbookPro),
"Mac16,1": model_s(name: "MacBook Pro 14\" (M4)", year: 2024, type: .macbookPro),
"Mac16,5": model_s(name: "MacBook Pro 16\" (M4 Max)", year: 2024, type: .macbookPro),
"Mac16,6": model_s(name: "MacBook Pro 14\" (M4 Max)", year: 2024, type: .macbookPro),
"Mac16,7": model_s(name: "MacBook Pro 16\" (M4 Pro)", year: 2024, type: .macbookPro),
"Mac16,8": model_s(name: "MacBook Pro 14\" (M4 Pro)", year: 2024, type: .macbookPro),
"Mac17,6": model_s(name: "MacBook Pro 16\" (M5 Max)", year: 2026, type: .macbookPro),
"Mac17,7": model_s(name: "MacBook Pro 14\" (M5 Max)", year: 2026, type: .macbookPro),
"Mac17,8": model_s(name: "MacBook Pro 16\" (M5 Pro)", year: 2026, type: .macbookPro),
"Mac17,9": model_s(name: "MacBook Pro 14\" (M5 Pro)", year: 2024, type: .macbookPro)
]
let osDict: [String: String] = [
"10.13": "High Sierra",
"10.14": "Mojave",
"10.15": "Catalina",
"11": "Big Sur",
"12": "Monterey",
"13": "Ventura",
"14": "Sonoma",
"15": "Sequoia",
"26": "Tahoe"
]
================================================
FILE: Kit/plugins/Updater.swift
================================================
//
// Updater.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 14/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import SystemConfiguration
public struct version_s {
public let current: String
public let latest: String
public let newest: Bool
public let url: String
public init(current: String, latest: String, newest: Bool, url: String) {
self.current = current
self.latest = latest
self.newest = newest
self.url = url
}
}
internal struct Version {
var major: Int = 0
var minor: Int = 0
var patch: Int = 0
var beta: Int? = nil
}
public class Updater {
private let github: URL
private let server: URL
private let appName: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as! String
private let currentVersion: String = "v\(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String)"
private var observation: NSKeyValueObservation?
private var lastCheckTS: Int {
get {
return Store.shared.int(key: "updater_check_ts", defaultValue: -1)
}
set {
Store.shared.set(key: "updater_check_ts", value: newValue)
}
}
private var lastInstallTS: Int {
get {
return Store.shared.int(key: "updater_install_ts", defaultValue: -1)
}
set {
Store.shared.set(key: "updater_install_ts", value: newValue)
}
}
public init(github: String, url: String) {
self.github = URL(string: "https://api.github.com/repos/\(github)/releases/latest")!
self.server = URL(string: "\(url)?macOS=\(ProcessInfo().operatingSystemVersion.getFullVersion())")!
}
deinit {
observation?.invalidate()
}
public func check(force: Bool = false, completion: @escaping (_ result: version_s?, _ error: Error?) -> Void) {
if !isConnectedToNetwork() {
completion(nil, "No internet connection")
return
}
let diff = (Int(Date().timeIntervalSince1970) - self.lastCheckTS) / 60
if !force && diff <= 10 {
completion(nil, "last check was \(diff) minutes ago, stopping...")
return
}
defer {
self.lastCheckTS = Int(Date().timeIntervalSince1970)
}
self.fetchRelease(uri: self.server) { (result, err) in
guard let result = result, err == nil else {
self.fetchRelease(uri: self.github) { (result, err) in
guard let result = result, err == nil else {
completion(nil, err)
return
}
completion(version_s(
current: self.currentVersion,
latest: result.tag,
newest: isNewestVersion(currentVersion: self.currentVersion, latestVersion: result.tag),
url: result.url
), nil)
}
return
}
completion(version_s(
current: self.currentVersion,
latest: result.tag,
newest: isNewestVersion(currentVersion: self.currentVersion, latestVersion: result.tag),
url: result.url
), nil)
}
}
private func fetchRelease(uri: URL, completion: @escaping (_ result: (tag: String, url: String)?, _ error: Error?) -> Void) {
let task = URLSession.shared.dataTask(with: uri) { data, _, error in
guard let data = data, error == nil else {
completion(nil, "no data")
return
}
do {
let jsonResponse = try JSONSerialization.jsonObject(with: data, options: [])
guard let jsonArray = jsonResponse as? [String: Any],
let lastVersion = jsonArray["tag_name"] as? String,
let assets = jsonArray["assets"] as? [[String: Any]],
let asset = assets.first(where: {$0["name"] as! String == "\(self.appName).dmg"}),
let downloadURL = asset["browser_download_url"] as? String else {
completion(nil, "parse json")
return
}
completion((lastVersion, downloadURL), nil)
} catch let parsingError {
completion(nil, parsingError)
}
}
task.resume()
}
public func download(_ url: URL, progress: @escaping (_ progress: Progress) -> Void = {_ in }, completion: @escaping (_ path: String) -> Void = {_ in }) {
let downloadTask = URLSession.shared.downloadTask(with: url) { urlOrNil, _, _ in
guard let fileURL = urlOrNil else { return }
do {
let downloadsURL = try FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let destinationURL = downloadsURL.appendingPathComponent(url.lastPathComponent)
self.copyFile(from: fileURL, to: destinationURL) { (path, error) in
if error != nil {
print("copy file error: \(error ?? "copy error")")
return
}
completion(path)
}
} catch {
print("file error: \(error)")
}
}
self.observation = downloadTask.progress.observe(\.fractionCompleted) { value, _ in
progress(value)
}
downloadTask.resume()
}
public func install(path: String, completion: @escaping (_ error: String?) -> Void) {
let pwd = Bundle.main.bundleURL.absoluteString
.replacingOccurrences(of: "file://", with: "")
.replacingOccurrences(of: "Stats.app", with: "")
.replacingOccurrences(of: "//", with: "/")
let dmg = path.replacingOccurrences(of: "file://", with: "")
if !FileManager.default.isWritableFile(atPath: pwd) {
completion("has no write permission on \(pwd)")
return
}
let diff = (Int(Date().timeIntervalSince1970) - self.lastInstallTS) / 60
if diff <= 3 {
completion("last install was \(diff) minutes ago, stopping...")
return
}
defer {
self.lastInstallTS = Int(Date().timeIntervalSince1970)
}
print("Started new version installation...")
_ = syncShell("mkdir /tmp/Stats") // make sure that directory exist
let res = syncShell("/usr/bin/hdiutil attach \(path) -mountpoint /tmp/Stats -noverify -nobrowse -noautoopen") // mount the dmg
print("DMG is mounted")
if res.contains("is busy") { // dmg can be busy, if yes, unmount it and mount again
print("DMG is busy, remounting")
_ = syncShell("/usr/bin/hdiutil detach $TMPDIR/Stats")
_ = syncShell("/usr/bin/hdiutil attach \(path) -mountpoint /tmp/Stats -noverify -nobrowse -noautoopen")
}
_ = syncShell("cp -rf /tmp/Stats/Stats.app/Contents/Resources/Scripts/updater.sh $TMPDIR/updater.sh") // copy updater script to tmp folder
print("Script is copied to $TMPDIR/updater.sh")
asyncShell("sh $TMPDIR/updater.sh --app \(pwd) --dmg \(dmg) >/dev/null &") // run updater script in in background
print("Run updater.sh with app: \(pwd) and dmg: \(dmg)")
exit(0)
}
private func copyFile(from: URL, to: URL, completionHandler: @escaping (_ path: String, _ error: Error?) -> Void) {
var toPath = to
let fileName = (URL(fileURLWithPath: to.absoluteString)).lastPathComponent
let fileExt = (URL(fileURLWithPath: to.absoluteString)).pathExtension
var fileNameWithoutSuffix: String!
var newFileName: String!
var counter = 0
if fileName.hasSuffix(fileExt) {
fileNameWithoutSuffix = String(fileName.prefix(fileName.count - (fileExt.count+1)))
}
while toPath.checkFileExist() {
counter += 1
newFileName = "\(fileNameWithoutSuffix!)-\(counter).\(fileExt)"
toPath = to.deletingLastPathComponent().appendingPathComponent(newFileName)
}
do {
try FileManager.default.moveItem(at: from, to: toPath)
completionHandler(toPath.absoluteString, nil)
} catch {
completionHandler("", error)
}
}
// https://stackoverflow.com/questions/30743408/check-for-internet-connection-with-swift
private func isConnectedToNetwork() -> Bool {
var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress))
zeroAddress.sin_family = sa_family_t(AF_INET)
let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in
SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress)
}
}
var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0)
if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false {
return false
}
let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0
let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0
let ret = (isReachable && !needsConnection)
return ret
}
}
================================================
FILE: Kit/process.swift
================================================
//
// process.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 05/01/2024
// Using Swift 5.0
// Running on macOS 14.3
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public protocol Process_p {
var pid: Int { get }
var name: String { get }
var icon: NSImage { get }
}
public typealias ProcessHeader = (title: String, color: NSColor?)
public class ProcessesView: NSStackView {
public var count: Int {
self.list.count
}
private var list: [ProcessView] = []
private var colorViews: [ColorView] = []
public init(frame: NSRect = .zero, values: [ProcessHeader], n: Int = 0) {
super.init(frame: frame)
self.orientation = .vertical
self.spacing = 0
let header = self.generateHeaderView(values)
self.addArrangedSubview(header)
for _ in 0.. NSView {
let view = NSStackView()
view.widthAnchor.constraint(equalToConstant: self.bounds.width).isActive = true
view.heightAnchor.constraint(equalToConstant: ProcessView.height).isActive = true
view.orientation = .horizontal
view.distribution = .fillProportionally
view.spacing = 0
let iconView: NSImageView = NSImageView()
iconView.widthAnchor.constraint(equalToConstant: ProcessView.height).isActive = true
iconView.heightAnchor.constraint(equalToConstant: ProcessView.height).isActive = true
view.addArrangedSubview(iconView)
let titleField = LabelField()
titleField.cell?.truncatesLastVisibleLine = true
titleField.toolTip = localizedString("Process")
titleField.stringValue = localizedString("Process")
titleField.textColor = .tertiaryLabelColor
titleField.font = NSFont.systemFont(ofSize: 12, weight: .medium)
view.addArrangedSubview(titleField)
if values.count == 1, let v = values.first {
let field = LabelField()
field.cell?.truncatesLastVisibleLine = true
field.toolTip = v.title
field.stringValue = v.title
field.alignment = .right
field.textColor = .tertiaryLabelColor
field.font = NSFont.systemFont(ofSize: 12, weight: .medium)
view.addArrangedSubview(field)
} else {
for v in values {
if let color = v.color {
let container: NSView = NSView()
container.widthAnchor.constraint(equalToConstant: 60).isActive = true
container.heightAnchor.constraint(equalToConstant: ProcessView.height).isActive = true
let colorBlock: ColorView = ColorView(frame: NSRect(x: 48, y: 5, width: 12, height: 12), color: color, state: true, radius: 4)
colorBlock.toolTip = v.title
colorBlock.widthAnchor.constraint(equalToConstant: 12).isActive = true
colorBlock.heightAnchor.constraint(equalToConstant: 12).isActive = true
self.colorViews.append(colorBlock)
container.addSubview(colorBlock)
view.addArrangedSubview(container)
}
}
}
return view
}
public func setLock(_ newValue: Bool) {
self.list.forEach{ $0.setLock(newValue) }
}
public func clear(_ symbol: String = "") {
self.list.forEach{ $0.clear(symbol) }
}
public func set(_ idx: Int, _ process: Process_p, _ values: [String]) {
if self.list.indices.contains(idx) {
self.list[idx].set(process, values)
}
}
public func setColor(_ idx: Int, _ newColor: NSColor) {
if self.colorViews.indices.contains(idx) {
self.colorViews[idx].setColor(newColor)
}
}
}
public class ProcessView: NSStackView {
static let height: CGFloat = 22
private var pid: Int? = nil
private var lock: Bool = false
private var imageView: NSImageView = NSImageView()
private var killView: NSButton = NSButton()
private var labelView: LabelField = {
let view = LabelField()
view.cell?.truncatesLastVisibleLine = true
return view
}()
private var valueViews: [ValueField] = []
public init(size: CGSize = CGSize(width: 264, height: 22), n: Int = 1) {
var rect = NSRect(x: 2, y: 5, width: 12, height: 12)
if size.height != 22 {
rect = NSRect(x: 1, y: 3, width: 12, height: 12)
}
self.imageView = NSImageView(frame: rect)
self.killView = NSButton(frame: rect)
super.init(frame: NSRect(x: 0, y: 0, width: size.width, height: size.height))
self.wantsLayer = true
self.orientation = .horizontal
self.distribution = .fillProportionally
self.spacing = 0
self.layer?.cornerRadius = 3
let imageBox: NSView = {
let view = NSView()
self.killView.bezelStyle = .regularSquare
self.killView.translatesAutoresizingMaskIntoConstraints = false
self.killView.imageScaling = .scaleNone
self.killView.image = Bundle(for: type(of: self)).image(forResource: "cancel")!
self.killView.contentTintColor = .lightGray
self.killView.isBordered = false
self.killView.action = #selector(self.kill)
self.killView.target = self
self.killView.toolTip = localizedString("Kill process")
self.killView.focusRingType = .none
self.killView.isHidden = true
view.addSubview(self.imageView)
view.addSubview(self.killView)
return view
}()
self.addArrangedSubview(imageBox)
self.addArrangedSubview(self.labelView)
self.valuesViews(n).forEach{ self.addArrangedSubview($0) }
self.addTrackingArea(NSTrackingArea(
rect: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height),
options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp],
owner: self,
userInfo: nil
))
NSLayoutConstraint.activate([
imageBox.widthAnchor.constraint(equalToConstant: self.bounds.height),
imageBox.heightAnchor.constraint(equalToConstant: self.bounds.height),
self.labelView.heightAnchor.constraint(equalToConstant: 16),
self.widthAnchor.constraint(equalToConstant: self.bounds.width),
self.heightAnchor.constraint(equalToConstant: self.bounds.height)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func valuesViews(_ n: Int) -> [NSView] {
var list: [ValueField] = []
for _ in 0.. "):
raise CheckException("%s designated requirement malformed" % programType, programPath)
return reqLines[0][len("designated => "):]
def readInfoPlistFromPath(infoPath):
"""Reads an "Info.plist" file from the specified path."""
try:
with open(infoPath, 'rb') as fp:
info = plistlib.load(fp)
except:
raise CheckException("'Info.plist' not readable", infoPath)
if not isinstance(info, dict):
raise CheckException("'Info.plist' root must be a dictionary", infoPath)
return info
def readPlistFromToolSection(toolPath, segmentName, sectionName):
"""Reads a dictionary property list from the specified section within the specified executable."""
# Run otool -s to get a hex dump of the section.
args = [
# "false",
"otool",
"-V",
"-arch",
platform.machine(),
"-s",
segmentName,
sectionName,
toolPath
]
try:
plistDump = subprocess.check_output(args, encoding="utf-8")
except subprocess.CalledProcessError as e:
raise CheckException("tool %s / %s section unreadable" % (segmentName, sectionName), toolPath)
# Convert that dump to an property list.
plistLines = plistDump.strip().splitlines(keepends=True)
if len(plistLines) < 3:
raise CheckException("tool %s / %s section dump malformed (1)" % (segmentName, sectionName), toolPath)
header = plistLines[1].strip()
if not header.endswith("(%s,%s) section" % (segmentName, sectionName)):
raise CheckException("tool %s / %s section dump malformed (2)" % (segmentName, sectionName), toolPath)
del plistLines[0:2]
try:
if header.startswith('Contents of'):
data = []
for line in plistLines:
# line looks like this:
#
# '100000000 3c 3f 78 6d 6c 20 76 65 72 73 69 6f 6e 3d 22 31 |= 2
del columns[0]
for hexStr in columns:
data.append(int(hexStr, 16))
data = bytes(data)
else:
data = bytes("".join(plistLines), encoding="utf-8")
plist = plistlib.loads(data)
except:
raise CheckException("tool %s / %s section dump malformed (3)" % (segmentName, sectionName), toolPath)
# Check the root of the property list.
if not isinstance(plist, dict):
raise CheckException("tool %s / %s property list root must be a dictionary" % (segmentName, sectionName), toolPath)
return plist
def checkStep1(appPath):
"""Checks that the app and the tool are both correctly code signed."""
if not os.path.isdir(appPath):
raise CheckException("app not found", appPath)
# Check the app's code signature.
checkCodeSignature(appPath, "app")
# Check the tool directory.
toolDirPath = os.path.join(appPath, "Contents", "Library", "LaunchServices")
if not os.path.isdir(toolDirPath):
raise CheckException("tool directory not found", toolDirPath)
# Check each tool's code signature.
toolPathList = []
for toolName in os.listdir(toolDirPath):
if toolName != ".DS_Store":
toolPath = os.path.join(toolDirPath, toolName)
if not os.path.isfile(toolPath):
raise CheckException("tool directory contains a directory", toolPath)
checkCodeSignature(toolPath, "tool")
toolPathList.append(toolPath)
# Check that we have at least one tool.
if len(toolPathList) == 0:
raise CheckException("no tools found", toolDirPath)
return toolPathList
def checkStep2(appPath, toolPathList):
"""Checks the SMPrivilegedExecutables entry in the app's "Info.plist"."""
# Create a map from the tool name (not path) to its designated requirement.
toolNameToReqMap = dict()
for toolPath in toolPathList:
req = readDesignatedRequirement(toolPath, "tool")
toolNameToReqMap[os.path.basename(toolPath)] = req
# Read the Info.plist for the app and extract the SMPrivilegedExecutables value.
infoPath = os.path.join(appPath, "Contents", "Info.plist")
info = readInfoPlistFromPath(infoPath)
if "SMPrivilegedExecutables" not in info:
raise CheckException("'SMPrivilegedExecutables' not found", infoPath)
infoToolDict = info["SMPrivilegedExecutables"]
if not isinstance(infoToolDict, dict):
raise CheckException("'SMPrivilegedExecutables' must be a dictionary", infoPath)
# Check that the list of tools matches the list of SMPrivilegedExecutables entries.
if sorted(infoToolDict.keys()) != sorted(toolNameToReqMap.keys()):
raise CheckException("'SMPrivilegedExecutables' and tools in 'Contents/Library/LaunchServices' don't match")
# Check that all the requirements match.
# This is an interesting policy choice. Technically the tool just needs to match
# the requirement listed in SMPrivilegedExecutables, and we can check that by
# putting the requirement into tmp.req and then running
#
# $ codesign -v -R tmp.req /path/to/tool
#
# However, for a Developer ID signed tool we really want to have the SMPrivilegedExecutables
# entry contain the tool's designated requirement because Xcode has built a
# more complex DR that does lots of useful and important checks. So, as a matter
# of policy we require that the value in SMPrivilegedExecutables match the tool's DR.
for toolName in infoToolDict:
if infoToolDict[toolName] != toolNameToReqMap[toolName]:
raise CheckException("tool designated requirement (%s) doesn't match entry in 'SMPrivilegedExecutables' (%s)" % (toolNameToReqMap[toolName], infoToolDict[toolName]))
def checkStep3(appPath, toolPathList):
"""Checks the "Info.plist" embedded in each helper tool."""
# First get the app's designated requirement.
appReq = readDesignatedRequirement(appPath, "app")
# Then check that the tool's SMAuthorizedClients value matches it.
for toolPath in toolPathList:
info = readPlistFromToolSection(toolPath, "__TEXT", "__info_plist")
if "CFBundleInfoDictionaryVersion" not in info or info["CFBundleInfoDictionaryVersion"] != "6.0":
raise CheckException("'CFBundleInfoDictionaryVersion' in tool __TEXT / __info_plist section must be '6.0'", toolPath)
if "CFBundleIdentifier" not in info or info["CFBundleIdentifier"] != os.path.basename(toolPath):
raise CheckException("'CFBundleIdentifier' in tool __TEXT / __info_plist section must match tool name", toolPath)
if "SMAuthorizedClients" not in info:
raise CheckException("'SMAuthorizedClients' in tool __TEXT / __info_plist section not found", toolPath)
infoClientList = info["SMAuthorizedClients"]
if not isinstance(infoClientList, list):
raise CheckException("'SMAuthorizedClients' in tool __TEXT / __info_plist section must be an array", toolPath)
if len(infoClientList) != 1:
raise CheckException("'SMAuthorizedClients' in tool __TEXT / __info_plist section must have one entry", toolPath)
# Again, as a matter of policy we require that the SMAuthorizedClients entry must
# match exactly the designated requirement of the app.
if infoClientList[0] != appReq:
raise CheckException("app designated requirement (%s) doesn't match entry in 'SMAuthorizedClients' (%s)" % (appReq, infoClientList[0]), toolPath)
def checkStep4(appPath, toolPathList):
"""Checks the "launchd.plist" embedded in each helper tool."""
for toolPath in toolPathList:
launchd = readPlistFromToolSection(toolPath, "__TEXT", "__launchd_plist")
if "Label" not in launchd or launchd["Label"] != os.path.basename(toolPath):
raise CheckException("'Label' in tool __TEXT / __launchd_plist section must match tool name", toolPath)
# We don't need to check that the label matches the bundle identifier because
# we know it matches the tool name and step 4 checks that the tool name matches
# the bundle identifier.
def checkStep5(appPath):
"""There's nothing to do here; we effectively checked for this is steps 1 and 2."""
pass
def check(appPath):
"""Checks the SMJobBless setup of the specified app."""
# Each of the following steps matches a bullet point in the SMJobBless header doc.
toolPathList = checkStep1(appPath)
checkStep2(appPath, toolPathList)
checkStep3(appPath, toolPathList)
checkStep4(appPath, toolPathList)
checkStep5(appPath)
def setreq(appPath, appInfoPlistPath, toolInfoPlistPaths):
"""
Reads information from the built app and uses it to set the SMJobBless setup
in the specified app and tool Info.plist source files.
"""
if not os.path.isdir(appPath):
raise CheckException("app not found", appPath)
if not os.path.isfile(appInfoPlistPath):
raise CheckException("app 'Info.plist' not found", appInfoPlistPath)
for toolInfoPlistPath in toolInfoPlistPaths:
if not os.path.isfile(toolInfoPlistPath):
raise CheckException("app 'Info.plist' not found", toolInfoPlistPath)
# Get the designated requirement for the app and each of the tools.
appReq = readDesignatedRequirement(appPath, "app")
toolDirPath = os.path.join(appPath, "Contents", "Library", "LaunchServices")
if not os.path.isdir(toolDirPath):
raise CheckException("tool directory not found", toolDirPath)
toolNameToReqMap = {}
for toolName in os.listdir(toolDirPath):
req = readDesignatedRequirement(os.path.join(toolDirPath, toolName), "tool")
toolNameToReqMap[toolName] = req
if len(toolNameToReqMap) > len(toolInfoPlistPaths):
raise CheckException("tool directory has more tools (%d) than you've supplied tool 'Info.plist' paths (%d)" % (len(toolNameToReqMap), len(toolInfoPlistPaths)), toolDirPath)
if len(toolNameToReqMap) < len(toolInfoPlistPaths):
raise CheckException("tool directory has fewer tools (%d) than you've supplied tool 'Info.plist' paths (%d)" % (len(toolNameToReqMap), len(toolInfoPlistPaths)), toolDirPath)
# Build the new value for SMPrivilegedExecutables.
appToolDict = {}
toolInfoPlistPathToToolInfoMap = {}
for toolInfoPlistPath in toolInfoPlistPaths:
toolInfo = readInfoPlistFromPath(toolInfoPlistPath)
toolInfoPlistPathToToolInfoMap[toolInfoPlistPath] = toolInfo
if "CFBundleIdentifier" not in toolInfo:
raise CheckException("'CFBundleIdentifier' not found", toolInfoPlistPath)
bundleID = toolInfo["CFBundleIdentifier"]
if not isinstance(bundleID, str):
raise CheckException("'CFBundleIdentifier' must be a string", toolInfoPlistPath)
appToolDict[bundleID] = toolNameToReqMap[bundleID]
# Set the SMPrivilegedExecutables value in the app "Info.plist".
appInfo = readInfoPlistFromPath(appInfoPlistPath)
needsUpdate = "SMPrivilegedExecutables" not in appInfo
if not needsUpdate:
oldAppToolDict = appInfo["SMPrivilegedExecutables"]
if not isinstance(oldAppToolDict, dict):
raise CheckException("'SMPrivilegedExecutables' must be a dictionary", appInfoPlistPath)
appToolDictSorted = sorted(appToolDict.items(), key=operator.itemgetter(0))
oldAppToolDictSorted = sorted(oldAppToolDict.items(), key=operator.itemgetter(0))
needsUpdate = (appToolDictSorted != oldAppToolDictSorted)
if needsUpdate:
appInfo["SMPrivilegedExecutables"] = appToolDict
with open(appInfoPlistPath, 'wb') as fp:
plistlib.dump(appInfo, fp)
print ("%s: updated" % appInfoPlistPath, file = sys.stdout)
# Set the SMAuthorizedClients value in each tool's "Info.plist".
toolAppListSorted = [ appReq ] # only one element, so obviously sorted (-:
for toolInfoPlistPath in toolInfoPlistPaths:
toolInfo = toolInfoPlistPathToToolInfoMap[toolInfoPlistPath]
needsUpdate = "SMAuthorizedClients" not in toolInfo
if not needsUpdate:
oldToolAppList = toolInfo["SMAuthorizedClients"]
if not isinstance(oldToolAppList, list):
raise CheckException("'SMAuthorizedClients' must be an array", toolInfoPlistPath)
oldToolAppListSorted = sorted(oldToolAppList)
needsUpdate = (toolAppListSorted != oldToolAppListSorted)
if needsUpdate:
toolInfo["SMAuthorizedClients"] = toolAppListSorted
with open(toolInfoPlistPath, 'wb') as f:
plistlib.dump(toolInfo, f)
print("%s: updated" % toolInfoPlistPath, file = sys.stdout)
def main():
options, appArgs = getopt.getopt(sys.argv[1:], "d")
debug = False
for opt, val in options:
if opt == "-d":
debug = True
else:
raise UsageException()
if len(appArgs) == 0:
raise UsageException()
command = appArgs[0]
if command == "check":
if len(appArgs) != 2:
raise UsageException()
check(appArgs[1])
elif command == "setreq":
if len(appArgs) < 4:
raise UsageException()
setreq(appArgs[1], appArgs[2], appArgs[3:])
else:
raise UsageException()
if __name__ == "__main__":
try:
main()
except CheckException as e:
if e.path is None:
print("%s: %s" % (os.path.basename(sys.argv[0]), e.message), file = sys.stderr)
else:
path = e.path
if path.endswith("/"):
path = path[:-1]
print("%s: %s" % (path, e.message), file = sys.stderr)
sys.exit(1)
except UsageException as e:
print("usage: %s check /path/to/app" % os.path.basename(sys.argv[0]), file = sys.stderr)
print(" %s setreq /path/to/app /path/to/app/Info.plist /path/to/tool/Info.plist..." % os.path.basename(sys.argv[0]), file = sys.stderr)
sys.exit(1)
================================================
FILE: Kit/scripts/changelog.py
================================================
import subprocess
import re
def last_release_tag():
cmd = "git describe --abbrev=0 --tags"
output = subprocess.check_output(['bash', '-c', cmd])
return output.decode("utf-8").strip()
def git_hash(tag):
cmd = "git rev-list -n 1 {0}".format(tag)
output = subprocess.check_output(['bash', '-c', cmd])
return output.decode("utf-8").strip()
class Changelog:
fixPattern = re.compile("^- fix:")
featPattern = re.compile("^- feat:")
langPattern = re.compile("^- lang:")
def generate(self):
tag = last_release_tag()
tag_hash = git_hash(tag)
fix, feat, lang = self.commits(tag_hash)
changelog = ""
if len(fix) != 0:
changelog += "## Bug fixes \n{}".format("\n".join(fix))
if len(feat) != 0:
if len(changelog) != 0:
changelog += "\n\n"
changelog += "## New features \n{}".format("\n".join(feat))
if len(lang) != 0:
if len(changelog) != 0:
changelog += "\n\n"
changelog += "## Localization \n{}".format("\n".join(lang))
print(changelog)
def commits(self, first_commit):
cmd = f"git log --pretty=\"- %s\" {first_commit}..HEAD"
output = subprocess.check_output(['bash', '-c', cmd])
lines = output.decode("utf-8").splitlines()
fix = []
feat = []
lang = []
for line in lines:
if self.fixPattern.match(line) and "translation" not in line and "localization" not in line:
fix.append(line)
elif self.featPattern.match(line) and "translation" not in line and "localization" not in line:
feat.append(line)
elif self.langPattern.match(line) or "translation" in line or "localization" in line:
lang.append(line)
else:
print("Failed to detect commit {} type".format(line))
return fix, feat, lang
if __name__ == "__main__":
Changelog().generate()
================================================
FILE: Kit/scripts/i18n.py
================================================
import os
import sys
import json
import urllib.request
import subprocess
import unicodedata
try:
import langcodes
except Exception:
langcodes = None
def dictionary(lines):
parsed_lines = {}
for i, line in enumerate(lines):
if line.startswith("//") or len(line) == 0 or line == "\n":
continue
line = line.replace("\n", "")
pair = line.split(" = ")
parsed_lines[i] = {
"key": pair[0].replace('"', ""),
"value": pair[1].replace('"', "").replace(';', "")
}
return parsed_lines
class i18n:
path = os.getcwd() + "/Stats/Supporting Files/"
def __init__(self):
if "Kit/scripts" in os.getcwd():
self.path = os.getcwd() + "/../../Stats/Supporting Files/"
self.languages = list(filter(lambda x: x.endswith(".lproj"), os.listdir(self.path)))
def en_file(self):
with open(f"{self.path}/en.lproj/Localizable.strings", "r") as f:
en_file = f.readlines()
if en_file is None:
sys.exit("English language not found.")
return en_file
def check(self):
en_file = self.en_file()
en_dict = dictionary(en_file)
for lang in self.languages:
with open(f"{self.path}/{lang}/Localizable.strings", "r") as f:
file = f.readlines()
name = lang.replace(".lproj", "")
lang_dict = dictionary(file)
for v in en_dict:
en_key = en_dict[v].get("key")
if v not in lang_dict:
sys.exit(f"missing key `{en_key}` in `{name}` on line `{v}`")
lang_key = lang_dict[v].get("key")
if lang_key != en_key:
sys.exit(f"missing or wrong key `{lang_key}` in `{name}` on line `{v}`, must be `{en_key}`")
print(f"All fine, found {len(en_file)} lines in {len(self.languages)} languages.")
def fix(self):
en_file = self.en_file()
en_dict = dictionary(en_file)
for v in en_dict:
en_key = en_dict[v].get("key")
en_value = en_dict[v].get("value")
for lang in self.languages:
lang_path = f"{self.path}/{lang}/Localizable.strings"
with open(lang_path, "r") as f:
file = f.readlines()
lang_dict = dictionary(file)
if v not in lang_dict or en_key != lang_dict[v].get("key"):
file.insert(v, f"\"{en_key}\" = \"{en_value}\";\n")
with open(lang_path, "w") as f:
f.write("".join(file))
self.check()
def _normalize_lang_code(self, code):
code = (code or "").strip()
if code.endswith(".lproj"):
code = code[:-6]
return code.replace("-", "_")
def _extract_translation(self, raw, fallback):
raw = (raw or "").strip()
if not raw:
return fallback
def _clean(s):
return (s or "").strip().strip("*").strip('"').strip("'").strip()
def _from_dict(obj):
if not isinstance(obj, dict):
return None
role = (obj.get("role") or "").strip().lower()
obj_type = (obj.get("type") or "").strip().lower()
text = obj.get("text")
if isinstance(text, str) and text.strip():
if role in ("assistant", "translation") or obj_type == "translation":
return _clean(text)
content = obj.get("content")
if isinstance(content, list):
for item in content:
if not isinstance(item, dict):
continue
item_role = (item.get("role") or role).strip().lower()
item_type = (item.get("type") or "").strip().lower()
t = item.get("text")
if isinstance(t, str) and t.strip():
if item_role in ("assistant", "translation") or item_type in ("translation", "text"):
return _clean(t)
return None
try:
parsed = json.loads(raw)
if isinstance(parsed, dict):
hit = _from_dict(parsed)
if hit:
return hit
elif isinstance(parsed, list):
for item in parsed:
hit = _from_dict(item)
if hit:
return hit
except json.JSONDecodeError:
pass
if "\n" not in raw and len(raw) <= 200:
candidate = _clean(raw)
if candidate and not candidate.startswith("{") and not candidate.startswith("["):
return candidate
for line in raw.splitlines():
line = _clean(line)
if line and not line.startswith("{") and not line.startswith("["):
return line
return fallback
def _lang_name_from_code(self, code):
c = self._normalize_lang_code(code).replace("_", "-").strip()
if not c:
return "Unknown"
if langcodes:
try:
name = langcodes.get(c).display_name("en")
if name:
return name
except Exception:
pass
return c
def _script_hint(self, lang_code):
lang = self._normalize_lang_code(lang_code).lower()
hints = {
"el": "Greek script only (Α-Ω, α-ω) except numbers/punctuation/brand names.",
"ru": "Cyrillic script only except numbers/punctuation/brand names.",
"uk": "Cyrillic script only except numbers/punctuation/brand names.",
"bg": "Cyrillic script only except numbers/punctuation/brand names.",
"ja": "Japanese writing system (Hiragana/Katakana/Kanji), no romaji unless required.",
"zh_cn": "Simplified Chinese characters.",
"zh_hans": "Simplified Chinese characters.",
"zh_tw": "Traditional Chinese characters.",
"zh_hant": "Traditional Chinese characters.",
"ko": "Korean Hangul preferred.",
"et": "Use Estonian only. Do not use Russian.",
}
return hints.get(lang, "")
def _ollama_translate(self, text, target_lang, model="translategemma:4b", retries=2):
url = "http://ai:11434/api/generate"
tgt = self._normalize_lang_code(target_lang)
lang = self._lang_name_from_code(tgt)
script_hint = self._script_hint(tgt)
prompt = (
f"You are a professional English (en) to {lang} ({tgt}) translator. Your goal is to accurately convey the meaning and nuances of the original English text while adhering to {lang} grammar, vocabulary, and cultural sensitivities. Produce only the {lang} translation, without any additional explanations or commentary. Output only the final translated text. Do not add explanations, notes, JSON, markdown, or quotes. Preserve placeholders/tokens exactly \\(e\\.g\\. `%@`, `%d`, `{0}`, `MB/s`\\). Preserve punctuation, casing intent, and technical abbreviations. {script_hint} Please translate the following English text into {lang}:\\n\\n"
f"{text}"
)
payload = {
"model": model,
"prompt": prompt,
"stream": False,
}
req = urllib.request.Request(
url,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST"
)
with urllib.request.urlopen(req, timeout=240) as resp:
data = json.loads(resp.read().decode("utf-8"))
raw = data.get("response", "").strip()
return self._extract_translation(raw, fallback=text)
def _line_authors(self, file_path):
cmd = ["git", "blame", "--line-porcelain", file_path]
out = subprocess.check_output(cmd, text=True, cwd=os.getcwd(), stderr=subprocess.DEVNULL)
authors = []
for line in out.splitlines():
if line.startswith("author "):
authors.append(line[len("author "):].strip())
return authors
def _my_git_author(self):
try:
return subprocess.check_output(
["git", "config", "user.name"],
text=True,
cwd=os.getcwd()
).strip()
except Exception:
return ""
def _strings_escape(self, value):
s = "" if value is None else str(value)
s = s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
return s
def translate(self, model="translategemma:4b", accept=False):
en_lines = self.en_file()
en_dict = dictionary(en_lines)
my_author = self._my_git_author()
omit_keys = ["Swap"]
ai_tag = f"// {model}"
target_languages = [
l for l in self.languages
if not self._normalize_lang_code(l).lower().startswith("en")
# if self._normalize_lang_code(l).lower() in ("sk")
]
total_langs = len(target_languages)
for lang_idx, lang in enumerate(target_languages, start=1):
lang_code = lang.replace(".lproj", "")
lang_name = self._lang_name_from_code(lang_code)
lang_path = f"{self.path}/{lang}/Localizable.strings"
with open(lang_path, "r") as f:
old_lines = f.readlines()
new_lines = old_lines[:]
lang_dict = dictionary(old_lines)
changed = False
try:
authors = self._line_authors(lang_path)
except Exception:
authors = [""] * len(old_lines)
candidates = []
for i, en_item in en_dict.items():
en_key = en_item.get("key")
en_value = en_item.get("value")
translate_item = lang_dict.get(i)
translate_key = translate_item.get("key") if translate_item else None
translate_value = translate_item.get("value") if translate_item else None
if translate_item is None or translate_key != en_key:
line = f"\"{en_key}\" = \"{en_value}\";\n"
if i < len(new_lines):
new_lines.insert(i, line)
else:
new_lines.append(line)
if i <= len(authors):
authors.insert(i, my_author)
changed = True
translate_value = en_value
if translate_key != en_key:
continue
if en_key in omit_keys:
continue
if i < len(authors) and my_author and authors[i] != my_author and en_value != translate_value:
continue
if translate_value is None or translate_value == en_value:
candidates.append((i, en_key, en_value))
print("Candidates for translation in {} ({}): {}".format(lang_name, lang_code, len(candidates)))
for idx, (i, en_key, en_value) in enumerate(candidates, start=1):
translated = self._ollama_translate(en_value, lang_code, model=model)
safe_translated = self._strings_escape(translated)
print(f"[{lang_name} {lang_idx}/{total_langs}] {idx}/{len(candidates)} {en_key} -> {safe_translated}")
translated_line = f"\"{en_key}\" = \"{safe_translated}\";\n"
update_line = f"\"{en_key}\" = \"{safe_translated}\"; {ai_tag}\n"
if i < len(new_lines):
if new_lines[i] != translated_line:
new_lines[i] = update_line
changed = True
else:
new_lines.append(update_line)
changed = True
if not changed:
print(f"No changes for {lang_code} ({lang_code}).")
continue
if accept:
with open(lang_path, "w") as f:
f.write("".join(new_lines))
print(f"Saved: {lang_path}")
else:
answer = input(f"Save changes to {lang_path}? [Y/n]: ").strip().lower()
if answer in ("", "y", "yes"):
with open(lang_path, "w") as f:
f.write("".join(new_lines))
print(f"Saved: {lang_path}")
else:
print(f"Skipped: {lang_path}")
print("Translation completed.")
if __name__ == "__main__":
i18n = i18n()
args = sys.argv[1:]
accept = "--accept" in args
args = [a for a in args if a != "--accept"]
if len(sys.argv) >= 2 and sys.argv[1] == "fix":
print("running fix command...")
i18n.fix()
elif len(sys.argv) >= 2 and sys.argv[1] == "translate":
print("running translate command...")
i18n.translate(accept=accept)
else:
print("running check command...")
i18n.check()
print("done")
================================================
FILE: Kit/scripts/uninstall.sh
================================================
#! /bin/sh
sudo launchctl unload /Library/LaunchDaemons/eu.exelban.Stats.SMC.Helper.plist
sudo rm /Library/LaunchDaemons/eu.exelban.Stats.SMC.Helper.plist
sudo rm /Library/PrivilegedHelperTools/eu.exelban.Stats.SMC.Helper
sudo rm $HOME/Library/Application Support/Stats
================================================
FILE: Kit/scripts/updater.sh
================================================
#!/bin/bash
DMG_PATH="$HOME/Downloads/Stats.dmg"
MOUNT_PATH="/tmp/Stats"
APPLICATION_PATH="/Applications/"
STEP=""
while [[ "$#" > 0 ]]; do case $1 in
-s|--step) STEP="$2"; shift;;
-d|--dmg) DMG_PATH="$2"; shift;;
-a|--app) APPLICATION_PATH="$2"; shift;;
-m|--mount) MOUNT_PATH="$2"; shift;;
*) echo "Unknown parameter passed: $1"; exit 1;;
esac; shift; done
if [[ "$STEP" == "2" ]]; then
rm -rf $APPLICATION_PATH/Stats.app
cp -rf $MOUNT_PATH/Stats.app $APPLICATION_PATH/Stats.app
$APPLICATION_PATH/Stats.app/Contents/MacOS/Stats --dmg "$DMG_PATH"
echo "New version started"
elif [[ "$STEP" == "3" ]]; then
/usr/bin/hdiutil detach "$MOUNT_PATH"
/bin/rm -rf "$MOUNT_PATH"
/bin/rm -rf "$DMG_PATH"
echo "Done"
else
rm -rf $APPLICATION_PATH/Stats.app
cp -rf $MOUNT_PATH/Stats.app $APPLICATION_PATH/Stats.app
$APPLICATION_PATH/Stats.app/Contents/MacOS/Stats --dmg-path "$DMG_PATH" --mount-path "$MOUNT_PATH"
echo "New version started"
fi
================================================
FILE: Kit/types.swift
================================================
//
// types.swift
// Kit
//
// Created by Serhiy Mytrovtsiy on 10/04/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
public struct DoubleValue {
public var ts: Date = Date()
public let value: Double
public init(_ value: Double = 0) {
self.value = value
}
}
extension [DoubleValue] {
public func max() -> Double? { self.max(by: { $0.value < $1.value })?.value }
}
public struct ColorValue: Equatable {
public let value: Double
public let color: NSColor?
public init(_ value: Double, color: NSColor? = nil) {
self.value = value
self.color = color
}
// swiftlint:disable function_name_whitespace
public static func ==(lhs: ColorValue, rhs: ColorValue) -> Bool {
return lhs.value == rhs.value
}
// swiftlint:enable function_name_whitespace
}
public enum AppUpdateInterval: String {
case silent = "Silent"
case atStart = "At start"
case separator1 = "separator_1"
case oncePerDay = "Once per day"
case oncePerWeek = "Once per week"
case oncePerMonth = "Once per month"
case separator2 = "separator_2"
case never = "Never"
}
public let AppUpdateIntervals: [KeyValue_t] = [
KeyValue_t(key: "Silent", value: AppUpdateInterval.silent.rawValue),
KeyValue_t(key: "At start", value: AppUpdateInterval.atStart.rawValue),
KeyValue_t(key: "separator_1", value: "separator_1"),
KeyValue_t(key: "Once per day", value: AppUpdateInterval.oncePerDay.rawValue),
KeyValue_t(key: "Once per week", value: AppUpdateInterval.oncePerWeek.rawValue),
KeyValue_t(key: "Once per month", value: AppUpdateInterval.oncePerMonth.rawValue),
KeyValue_t(key: "separator_2", value: "separator_2"),
KeyValue_t(key: "Never", value: AppUpdateInterval.never.rawValue)
]
public let TemperatureUnits: [KeyValue_t] = [
KeyValue_t(key: "system", value: "System"),
KeyValue_t(key: "separator", value: "separator"),
KeyValue_t(key: "celsius", value: "Celsius", additional: UnitTemperature.celsius),
KeyValue_t(key: "fahrenheit", value: "Fahrenheit", additional: UnitTemperature.fahrenheit)
]
public let CombinedModulesSpacings: [KeyValue_t] = [
KeyValue_t(key: "none", value: "None"),
KeyValue_t(key: "1", value: "1", additional: 1),
KeyValue_t(key: "2", value: "2", additional: 2),
KeyValue_t(key: "3", value: "3", additional: 3),
KeyValue_t(key: "4", value: "4", additional: 4),
KeyValue_t(key: "5", value: "5", additional: 5),
KeyValue_t(key: "6", value: "6", additional: 6),
KeyValue_t(key: "7", value: "7", additional: 7),
KeyValue_t(key: "8", value: "8", additional: 8)
]
public let PublicIPAddressRefreshIntervals: [KeyValue_t] = [
KeyValue_t(key: "never", value: "Never"),
KeyValue_t(key: "separator", value: "separator"),
KeyValue_t(key: "hour", value: "Every hour"),
KeyValue_t(key: "12", value: "Every 12 hours"),
KeyValue_t(key: "24", value: "Every 24 hours")
]
public enum DataSizeBase: String {
case bit
case byte
}
public let SpeedBase: [KeyValue_t] = [
KeyValue_t(key: "bit", value: "Bit", additional: DataSizeBase.bit),
KeyValue_t(key: "byte", value: "Byte", additional: DataSizeBase.byte)
]
internal enum StackMode: String {
case auto = "automatic"
case oneRow = "oneRow"
case twoRows = "twoRows"
}
internal let SensorsWidgetValue: [KeyValue_t] = [
KeyValue_t(key: "oi", value: "output/input"),
KeyValue_t(key: "io", value: "input/output"),
KeyValue_t(key: "separator", value: "separator"),
KeyValue_t(key: "i", value: "input"),
KeyValue_t(key: "o", value: "output")
]
internal let SensorsWidgetMode: [KeyValue_t] = [
KeyValue_t(key: StackMode.auto.rawValue, value: "Automatic"),
KeyValue_t(key: "separator", value: "separator"),
KeyValue_t(key: StackMode.oneRow.rawValue, value: "One row"),
KeyValue_t(key: StackMode.twoRows.rawValue, value: "Two rows")
]
internal let SpeedPictogram: [KeyValue_t] = [
KeyValue_t(key: "none", value: "None"),
KeyValue_t(key: "separator", value: "separator"),
KeyValue_t(key: "dots", value: "Dots"),
KeyValue_t(key: "arrows", value: "Arrows"),
KeyValue_t(key: "chars", value: "Characters")
]
internal let SpeedPictogramColor: [KeyValue_t] = [
KeyValue_t(key: "none", value: "None"),
KeyValue_t(key: "separator", value: "separator"),
KeyValue_t(key: "default", value: "Default color"),
KeyValue_t(key: "transparent", value: "Transparent when no activity"),
KeyValue_t(key: "constant", value: "Constant color")
]
internal let BatteryAdditionals: [KeyValue_t] = [
KeyValue_t(key: "none", value: "None"),
KeyValue_t(key: "separator", value: "separator"),
KeyValue_t(key: "innerPercentage", value: "Percentage inside the icon"),
KeyValue_t(key: "separator", value: "separator"),
KeyValue_t(key: "percentage", value: "Percentage"),
KeyValue_t(key: "time", value: "Time"),
KeyValue_t(key: "percentageAndTime", value: "Percentage and time"),
KeyValue_t(key: "timeAndPercentage", value: "Time and percentage")
]
internal let BatteryInfo: [KeyValue_t] = [
KeyValue_t(key: "percentage", value: "Percentage"),
KeyValue_t(key: "time", value: "Time"),
KeyValue_t(key: "percentageAndTime", value: "Percentage and time"),
KeyValue_t(key: "timeAndPercentage", value: "Time and percentage")
]
public let ShortLong: [KeyValue_t] = [
KeyValue_t(key: "short", value: "Short"),
KeyValue_t(key: "long", value: "Long")
]
public let ReaderUpdateIntervals: [KeyValue_t] = [
KeyValue_t(key: "1", value: "1 sec"),
KeyValue_t(key: "2", value: "2 sec"),
KeyValue_t(key: "3", value: "3 sec"),
KeyValue_t(key: "5", value: "5 sec"),
KeyValue_t(key: "10", value: "10 sec"),
KeyValue_t(key: "15", value: "15 sec"),
KeyValue_t(key: "30", value: "30 sec"),
KeyValue_t(key: "60", value: "60 sec")
]
public let NumbersOfProcesses: [Int] = [0, 3, 5, 8, 10, 15]
public let NetworkReaders: [KeyValue_t] = [
KeyValue_t(key: "interface", value: "Interface based"),
KeyValue_t(key: "process", value: "Processes based")
]
internal let Alignments: [KeyValue_t] = [
KeyValue_t(key: "left", value: "Left alignment", additional: NSTextAlignment.left),
KeyValue_t(key: "center", value: "Center alignment", additional: NSTextAlignment.center),
KeyValue_t(key: "right", value: "Right alignment", additional: NSTextAlignment.right)
]
public struct SColor: KeyValue_p, Equatable {
public let key: String
public let value: String
public var additional: Any?
public static func == (lhs: SColor, rhs: SColor) -> Bool {
return lhs.key == rhs.key
}
}
extension SColor: CaseIterable {
public static var utilization: SColor { return SColor(key: "utilization", value: "Based on utilization", additional: NSColor.black) }
public static var pressure: SColor { return SColor(key: "pressure", value: "Based on pressure", additional: NSColor.black) }
public static var cluster: SColor { return SColor(key: "cluster", value: "Based on cluster", additional: NSColor.controlAccentColor) }
public static var separator1: SColor { return SColor(key: "separator_1", value: "separator_1", additional: NSColor.black) }
public static var systemAccent: SColor { return SColor(key: "system", value: "System accent", additional: NSColor.controlAccentColor) }
public static var monochrome: SColor { return SColor(key: "monochrome", value: "Monochrome accent", additional: NSColor.textColor) }
public static var separator2: SColor { return SColor(key: "separator_2", value: "separator_2", additional: NSColor.black) }
public static var clear: SColor { return SColor(key: "clear", value: "Clear", additional: NSColor.clear) }
public static var white: SColor { return SColor(key: "white", value: "White", additional: NSColor.white) }
public static var black: SColor { return SColor(key: "black", value: "Black", additional: NSColor.black) }
public static var gray: SColor { return SColor(key: "gray", value: "Gray", additional: NSColor.gray) }
public static var secondGray: SColor { return SColor(key: "secondGray", value: "Second gray", additional: NSColor.systemGray) }
public static var darkGray: SColor { return SColor(key: "darkGray", value: "Dark gray", additional: NSColor.darkGray) }
public static var lightGray: SColor { return SColor(key: "lightGray", value: "Light gray", additional: NSColor.lightGray) }
public static var red: SColor { return SColor(key: "red", value: "Red", additional: NSColor.red) }
public static var secondRed: SColor { return SColor(key: "secondRed", value: "Second red", additional: NSColor.systemRed) }
public static var green: SColor { return SColor(key: "green", value: "Green", additional: NSColor.green) }
public static var secondGreen: SColor { return SColor(key: "secondGreen", value: "Second green", additional: NSColor.systemGreen) }
public static var blue: SColor { return SColor(key: "blue", value: "Blue", additional: NSColor.blue) }
public static var secondBlue: SColor { return SColor(key: "secondBlue", value: "Second blue", additional: NSColor.systemBlue) }
public static var yellow: SColor { return SColor(key: "yellow", value: "Yellow", additional: NSColor.yellow) }
public static var secondYellow: SColor { return SColor(key: "secondYellow", value: "Second yellow", additional: NSColor.systemYellow) }
public static var orange: SColor { return SColor(key: "orange", value: "Orange", additional: NSColor.orange) }
public static var secondOrange: SColor { return SColor(key: "secondOrange", value: "Second orange", additional: NSColor.systemOrange) }
public static var purple: SColor { return SColor(key: "purple", value: "Purple", additional: NSColor.purple) }
public static var secondPurple: SColor { return SColor(key: "secondPurple", value: "Second purple", additional: NSColor.systemPurple) }
public static var brown: SColor { return SColor(key: "brown", value: "Brown", additional: NSColor.brown) }
public static var secondBrown: SColor { return SColor(key: "secondBrown", value: "Second brown", additional: NSColor.systemBrown) }
public static var cyan: SColor { return SColor(key: "cyan", value: "Cyan", additional: NSColor.cyan) }
public static var magenta: SColor { return SColor(key: "magenta", value: "Magenta", additional: NSColor.magenta) }
public static var pink: SColor { return SColor(key: "pink", value: "Pink", additional: NSColor.systemPink) }
public static var teal: SColor { return SColor(key: "teal", value: "Teal", additional: NSColor.systemTeal) }
public static var indigo: SColor { if #available(OSX 10.15, *) {
return SColor(key: "indigo", value: "Indigo", additional: NSColor.systemIndigo)
} else {
return SColor(key: "indigo", value: "Indigo", additional: NSColor(red: 75, green: 0, blue: 130, alpha: 1))
} }
public static var allCases: [SColor] {
return [.utilization, .pressure, .cluster, separator1,
.systemAccent, .monochrome, separator2,
.clear, .white, .black, .gray, .secondGray, .darkGray, .lightGray,
.red, .secondRed, .green, .secondGreen, .blue, .secondBlue, .yellow, .secondYellow,
.orange, .secondOrange, .purple, .secondPurple, .brown, .secondBrown,
.cyan, .magenta, .pink, .teal, .indigo
]
}
public static var allColors: [SColor] {
return [.systemAccent, .monochrome, .separator2, .clear, .white, .black, .gray, .secondGray, .darkGray, .lightGray,
.red, .secondRed, .green, .secondGreen, .blue, .secondBlue, .yellow, .secondYellow,
.orange, .secondOrange, .purple, .secondPurple, .brown, .secondBrown,
.cyan, .magenta, .pink, .teal, .indigo
]
}
public static func fromString(_ key: String, defaultValue: SColor = .systemAccent) -> SColor {
return SColor.allCases.first{ $0.key == key } ?? defaultValue
}
}
internal class MonochromeColor {
static internal let red: NSColor = NSColor(red: (145), green: (145), blue: (145), alpha: 1)
static internal let blue: NSColor = NSColor(red: (113), green: (113), blue: (113), alpha: 1)
}
public typealias colorZones = (orange: Double, red: Double)
public extension Notification.Name {
static let toggleSettings = Notification.Name("toggleSettings")
static let toggleModule = Notification.Name("toggleModule")
static let togglePopup = Notification.Name("togglePopup")
static let toggleWidget = Notification.Name("toggleWidget")
static let togglePreview = Notification.Name("togglePreview")
static let openModuleSettings = Notification.Name("openModuleSettings")
static let clickInSettings = Notification.Name("clickInSettings")
static let refreshPublicIP = Notification.Name("refreshPublicIP")
static let resetTotalNetworkUsage = Notification.Name("resetTotalNetworkUsage")
static let syncFansControl = Notification.Name("syncFansControl")
static let checkFanModes = Notification.Name("checkFanModes")
static let fanHelperState = Notification.Name("fanHelperState")
static let toggleOneView = Notification.Name("toggleOneView")
static let widgetRearrange = Notification.Name("widgetRearrange")
static let moduleRearrange = Notification.Name("moduleRearrange")
static let pause = Notification.Name("pause")
static let toggleFanControl = Notification.Name("toggleFanControl")
static let combinedModulesPopup = Notification.Name("combinedModulesPopup")
static let remoteLoginSuccess = Notification.Name("remoteLoginSuccess")
static let remoteState = Notification.Name("remoteState")
static let openWindow = Notification.Name("openWindow")
}
public var isARM: Bool {
SystemKit.shared.device.platform != .intel
}
public let notificationLevels: [KeyValue_t] = [
KeyValue_t(key: "", value: "Disabled"),
KeyValue_t(key: "0.03", value: "3%"),
KeyValue_t(key: "0.05", value: "5%"),
KeyValue_t(key: "0.1", value: "10%"),
KeyValue_t(key: "0.15", value: "15%"),
KeyValue_t(key: "0.2", value: "20%"),
KeyValue_t(key: "0.25", value: "25%"),
KeyValue_t(key: "0.3", value: "30%"),
KeyValue_t(key: "0.35", value: "35%"),
KeyValue_t(key: "0.4", value: "40%"),
KeyValue_t(key: "0.45", value: "45%"),
KeyValue_t(key: "0.5", value: "50%"),
KeyValue_t(key: "0.55", value: "55%"),
KeyValue_t(key: "0.6", value: "60%"),
KeyValue_t(key: "0.65", value: "65%"),
KeyValue_t(key: "0.7", value: "70%"),
KeyValue_t(key: "0.75", value: "75%"),
KeyValue_t(key: "0.8", value: "80%"),
KeyValue_t(key: "0.85", value: "85%"),
KeyValue_t(key: "0.9", value: "90%"),
KeyValue_t(key: "0.95", value: "95%"),
KeyValue_t(key: "0.97", value: "97%"),
KeyValue_t(key: "1.0", value: "100%")
]
public struct Scale: KeyValue_p, Equatable {
public let key: String
public let value: String
public static func == (lhs: Scale, rhs: Scale) -> Bool {
return lhs.key == rhs.key
}
}
extension Scale: CaseIterable {
public static var none: Scale { return Scale(key: "none", value: "None") }
public static var separator: Scale { return Scale(key: "separator", value: "separator") }
public static var linear: Scale { return Scale(key: "linear", value: "Linear") }
public static var square: Scale { return Scale(key: "square", value: "Square") }
public static var cube: Scale { return Scale(key: "cube", value: "Cube") }
public static var logarithmic: Scale { return Scale(key: "logarithmic", value: "Logarithmic") }
public static var separator2: Scale { return Scale(key: "separator", value: "separator") }
public static var fixed: Scale { return Scale(key: "fixed", value: "Fixed scale") }
public static var allCases: [Scale] {
return [.none, .separator, .linear, .square, .cube, .logarithmic, .separator2, .fixed]
}
public static func fromString(_ key: String, defaultValue: Scale = .linear) -> Scale {
return Scale.allCases.first{ $0.key == key } ?? defaultValue
}
}
public enum FanValue: String {
case rpm
case percentage
}
public let FanValues: [KeyValue_t] = [
KeyValue_t(key: "rpm", value: "RPM", additional: FanValue.rpm),
KeyValue_t(key: "percentage", value: "Percentage", additional: FanValue.percentage)
]
public var LineChartHistory: [KeyValue_p] = [
KeyValue_t(key: "60", value: "1 minute"),
KeyValue_t(key: "120", value: "2 minutes"),
KeyValue_t(key: "180", value: "3 minutes"),
KeyValue_t(key: "300", value: "5 minutes"),
KeyValue_t(key: "600", value: "10 minutes")
]
public struct SizeUnit: KeyValue_p, Equatable {
public let key: String
public let value: String
public static func == (lhs: SizeUnit, rhs: SizeUnit) -> Bool {
return lhs.key == rhs.key
}
}
extension SizeUnit: CaseIterable {
public static var byte: SizeUnit { return SizeUnit(key: "byte", value: "Bytes") }
public static var KB: SizeUnit { return SizeUnit(key: "KB", value: "KB") }
public static var MB: SizeUnit { return SizeUnit(key: "MB", value: "MB") }
public static var GB: SizeUnit { return SizeUnit(key: "GB", value: "GB") }
public static var TB: SizeUnit { return SizeUnit(key: "TB", value: "TB") }
public static var allCases: [SizeUnit] {
[.byte, .KB, .MB, .GB, .TB]
}
public static func fromString(_ key: String, defaultValue: SizeUnit = .byte) -> SizeUnit {
return SizeUnit.allCases.first{ $0.key == key } ?? defaultValue
}
public func toBytes(_ value: Int) -> Int {
switch self {
case .KB:
return value * 1_000
case .MB:
return value * 1_000 * 1_000
case .GB:
return value * 1_000 * 1_000 * 1_000
case .TB:
return value * 1_000 * 1_000 * 1_000 * 1_000
default:
return value
}
}
}
public enum RAMPressure: String, Codable {
case normal
case warning
case critical
func pressureColor() -> NSColor {
switch self {
case .normal:
return NSColor.systemGreen
case .warning:
return NSColor.systemYellow
case .critical:
return NSColor.systemRed
}
}
}
public struct TokenResponse: Codable {
public let access_token: String
public let refresh_token: String
}
public struct DeviceResponse: Codable {
public let device_code: String
public let user_code: String
public let verification_uri_complete: URL
public let interval: Int?
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 Serhiy Mytrovtsiy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: LaunchAtLogin/Info.plist
================================================
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
1.0
CFBundleVersion
1
LSApplicationCategoryType
public.app-category.utilities
LSBackgroundOnly
LSMinimumSystemVersion
$(MACOSX_DEPLOYMENT_TARGET)
NSHumanReadableCopyright
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
NSPrincipalClass
NSApplication
================================================
FILE: LaunchAtLogin/LaunchAtLogin.entitlements
================================================
com.apple.security.app-sandbox
================================================
FILE: LaunchAtLogin/main.swift
================================================
//
// main.swift
// LaunchAtLogin
//
// Created by Serhiy Mytrovtsiy on 08/04/2020.
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
func main() {
let mainBundleId = Bundle.main.bundleIdentifier!.replacingOccurrences(of: ".LaunchAtLogin", with: "")
if !NSRunningApplication.runningApplications(withBundleIdentifier: mainBundleId).isEmpty {
exit(0)
}
let pathComponents = (Bundle.main.bundlePath as NSString).pathComponents
let mainPath = NSString.path(withComponents: Array(pathComponents[0...(pathComponents.count - 5)]))
NSWorkspace.shared.openApplication(at: NSURL.fileURL(withPath: mainPath), configuration: NSWorkspace.OpenConfiguration(), completionHandler: { _, _ in
exit(0)
})
}
main()
================================================
FILE: Makefile
================================================
APP = Stats
BUNDLE_ID = eu.exelban.$(APP)
BUILD_PATH = $(PWD)/build
APP_PATH = "$(BUILD_PATH)/$(APP).app"
ZIP_PATH = "$(BUILD_PATH)/$(APP).zip"
.SILENT: archive notarize sign prepare-dmg prepare-dSYM clean next-version check history disk smc leveldb
.PHONY: build archive notarize sign prepare-dmg prepare-dSYM clean next-version check history open smc leveldb
build: clean next-version archive notarize sign prepare-dmg prepare-dSYM open
# --- MAIN WORLFLOW FUNCTIONS --- #
archive: clean
osascript -e 'display notification "Exporting application archive..." with title "Build the Stats"'
echo "Exporting application archive..."
xcodebuild \
-scheme $(APP) \
-destination 'platform=OS X,arch=x86_64' \
-configuration Release archive \
-archivePath $(BUILD_PATH)/$(APP).xcarchive
echo "Application built, starting the export archive..."
xcodebuild -exportArchive \
-exportOptionsPlist "$(PWD)/exportOptions.plist" \
-archivePath $(BUILD_PATH)/$(APP).xcarchive \
-exportPath $(BUILD_PATH)
ditto -c -k --keepParent $(APP_PATH) $(ZIP_PATH)
echo "Project archived successfully"
notarize:
osascript -e 'display notification "Submitting app for notarization..." with title "Build the Stats"'
echo "Submitting app for notarization..."
xcrun notarytool submit --keychain-profile "AC_PASSWORD" --wait $(ZIP_PATH)
echo "Stats successfully notarized"
sign:
osascript -e 'display notification "Stampling the Stats..." with title "Build the Stats"'
echo "Going to staple an application..."
xcrun stapler staple $(APP_PATH)
spctl -a -t exec -vvv $(APP_PATH)
osascript -e 'display notification "Stats successfully stapled" with title "Build the Stats"'
echo "Stats successfully stapled"
prepare-dmg:
if [ ! -d $(PWD)/create-dmg ]; then \
git clone https://github.com/create-dmg/create-dmg; \
fi
./create-dmg/create-dmg \
--volname $(APP) \
--background "./Stats/Supporting Files/background.png" \
--window-pos 200 120 \
--window-size 500 320 \
--icon-size 80 \
--icon "Stats.app" 125 175 \
--hide-extension "Stats.app" \
--app-drop-link 375 175 \
--no-internet-enable \
$(PWD)/$(APP).dmg \
$(APP_PATH)
rm -rf ./create-dmg
prepare-dSYM:
echo "Zipping dSYMs..."
cd $(BUILD_PATH)/Stats.xcarchive/dSYMs && zip -r $(PWD)/dSYMs.zip .
echo "Created zip with dSYMs"
# --- HELPERS --- #
clean:
rm -rf $(BUILD_PATH)
if [ -a $(PWD)/dSYMs.zip ]; then rm $(PWD)/dSYMs.zip; fi;
if [ -a $(PWD)/Stats.dmg ]; then rm $(PWD)/Stats.dmg; fi;
next-version:
versionNumber=$$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "$(PWD)/Stats/Supporting Files/Info.plist") ;\
echo "Actual version is: $$versionNumber" ;\
versionNumber=$$((versionNumber + 1)) ;\
echo "Next version is: $$versionNumber" ;\
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $$versionNumber" "$(PWD)/Stats/Supporting Files/Info.plist" ;\
check:
xcrun notarytool log 2d0045cc-8f0d-4f4c-ba6f-728895fd064a --keychain-profile "AC_PASSWORD"
history:
xcrun notarytool history --keychain-profile "AC_PASSWORD"
open:
osascript -e 'display notification "Stats signed and ready for distribution" with title "Build the Stats"'
echo "Opening working folder..."
open $(PWD)
smc:
$(MAKE) --directory=./smc
open $(PWD)/smc
leveldb:
if [ ! -d $(PWD)/leveldb-source ]; then \
git clone --recurse-submodules https://github.com/google/leveldb.git leveldb-source; \
fi
mkdir -p $(PWD)/leveldb-source/build
cd $(PWD)/leveldb-source/build && cmake -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -DCMAKE_BUILD_TYPE=Release .. && cmake --build .
cp $(PWD)/leveldb-source/build/libleveldb.a $(PWD)/Kit/lldb/libleveldb.a
rm -rf $(PWD)/leveldb-source
================================================
FILE: Modules/Battery/Info.plist
================================================
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
1.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSHumanReadableCopyright
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
================================================
FILE: Modules/Battery/config.plist
================================================
Name
Battery
State
Symbol
battery.100
Widgets
label
Default
Order
0
mini
Default
Label
Title
BAT
Color
monochrome
Preview
Label
Title
BAT
Value
0.72
Unsupported colors
pressure
Order
1
bar_chart
Default
Preview
Value
0.9
Color
Unsupported colors
pressure
cluster
Order
2
battery
Default
Order
3
battery_details
Default
Order
4
Settings
popup
notifications
================================================
FILE: Modules/Battery/main.swift
================================================
//
// main.swift
// Battery
//
// Created by Serhiy Mytrovtsiy on 06/06/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
import IOKit.ps
struct Battery_Usage: Codable {
var powerSource: String = ""
var state: String? = nil
var isCharged: Bool = false
var isCharging: Bool = false
var isBatteryPowered: Bool = false
var optimizedChargingEngaged: Bool = false
var level: Double = 0
var cycles: Int = 0
var health: Int = 0
var designedCapacity: Int = 0
var maxCapacity: Int = 0
var currentCapacity: Int = 0
var amperage: Int = 0
var voltage: Double = 0
var temperature: Double = 0
var ACwatts: Int = 0
var chargingCurrent: Int = 0
var chargingVoltage: Int = 0
var timeToEmpty: Int = 0
var timeToCharge: Int = 0
var timeOnACPower: Date? = nil
}
public class Battery: Module {
private let popupView: Popup
private let settingsView: Settings
private let portalView: Portal
private let notificationsView: Notifications
private var usageReader: UsageReader? = nil
private var processReader: ProcessReader? = nil
private var lowLevelNotificationState: Bool = false
private var highLevelNotificationState: Bool = false
private var notificationID: String? = nil
public init() {
self.settingsView = Settings(.battery)
self.popupView = Popup(.battery)
self.portalView = Portal(.battery)
self.notificationsView = Notifications(.battery)
super.init(
moduleType: .battery,
popup: self.popupView,
settings: self.settingsView,
portal: self.portalView,
notifications: self.notificationsView
)
guard self.available else { return }
self.usageReader = UsageReader(.battery) { [weak self] value in
self?.usageCallback(value)
}
self.processReader = ProcessReader(.battery) { [weak self] value in
if let list = value {
self?.popupView.processCallback(list)
}
}
self.settingsView.callback = { [weak self] in
DispatchQueue.global(qos: .background).async {
self?.usageReader?.read()
}
}
self.settingsView.callbackWhenUpdateNumberOfProcesses = { [weak self] in
self?.popupView.numberOfProcessesUpdated()
DispatchQueue.global(qos: .background).async {
self?.processReader?.read()
}
}
self.setReaders([self.usageReader, self.processReader])
}
public override func willTerminate() {
guard self.isAvailable() else { return }
self.notificationsView.willTerminate()
}
public override func isAvailable() -> Bool {
let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue()
let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array
return !sources.isEmpty
}
private func usageCallback(_ raw: Battery_Usage?) {
guard let value = raw, self.enabled else { return }
self.popupView.usageCallback(value)
self.portalView.loadCallback(value)
self.notificationsView.usageCallback(value)
self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: SWidget) in
switch w.item {
case let widget as Mini:
widget.setValue(abs(value.level))
widget.setColorZones((0.15, 0.3))
case let widget as BarChart:
widget.setValue([[ColorValue(value.level)]])
widget.setColorZones((0.15, 0.3))
case let widget as BatteryWidget:
widget.setValue(
percentage: value.level,
ACStatus: !value.isBatteryPowered,
isCharging: value.isCharging,
optimizedCharging: value.optimizedChargingEngaged,
time: value.timeToEmpty == 0 && value.timeToCharge != 0 ? value.timeToCharge : value.timeToEmpty
)
case let widget as BatteryDetailsWidget:
widget.setValue(
percentage: value.level,
time: value.timeToEmpty == 0 && value.timeToCharge != 0 ? value.timeToCharge : value.timeToEmpty
)
default: break
}
}
}
}
================================================
FILE: Modules/Battery/notifications.swift
================================================
//
// notifications.swift
// Battery
//
// Created by Serhiy Mytrovtsiy on 17/12/2023
// Using Swift 5.0
// Running on macOS 14.2
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
class Notifications: NotificationsWrapper {
private let lowID: String = "low"
private let highID: String = "high"
private var lowLevel: String = ""
private var highLevel: String = ""
public init(_ module: ModuleType) {
super.init(module, [self.lowID, self.highID])
if Store.shared.exist(key: "\(self.module)_lowLevelNotification") {
let value = Store.shared.string(key: "\(self.module)_lowLevelNotification", defaultValue: self.lowID)
Store.shared.set(key: "\(self.module)_notifications_low", value: value)
Store.shared.remove("\(self.module)_lowLevelNotification")
}
if Store.shared.exist(key: "\(self.module)_highLevelNotification") {
let value = Store.shared.string(key: "\(self.module)_highLevelNotification", defaultValue: self.highLevel)
Store.shared.set(key: "\(self.module)_notifications_high", value: value)
Store.shared.remove("\(self.module)_highLevelNotification")
}
self.lowLevel = Store.shared.string(key: "\(self.module)_notifications_low", defaultValue: self.lowLevel)
self.highLevel = Store.shared.string(key: "\(self.module)_notifications_high", defaultValue: self.highLevel)
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Low level notification"), component: selectView(
action: #selector(self.changeLowLevel),
items: notificationLevels,
selected: self.lowLevel
)),
PreferencesRow(localizedString("High level notification"), component: selectView(
action: #selector(self.changeHighLevel),
items: notificationLevels,
selected: self.highLevel
))
]))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func usageCallback(_ value: Battery_Usage) {
if value.isCharging || !value.isBatteryPowered {
self.hideNotification(self.lowID)
}
if let threshold = Double(self.lowLevel), !value.isCharging {
let title = localizedString("Low battery")
var subtitle = localizedString("Battery remaining", "\(Int(value.level*100))")
if value.timeToEmpty > 0 {
subtitle += " (\(Double(value.timeToEmpty*60).printSecondsToHoursMinutesSeconds()))"
}
self.checkDouble(id: self.lowID, value: value.level, threshold: threshold, title: title, subtitle: subtitle, less: true)
}
if value.isBatteryPowered {
self.hideNotification(self.highID)
}
if let threshold = Double(self.highLevel), value.isCharging {
let title = localizedString("High battery")
var subtitle = localizedString("Battery remaining to full charge", "\(Int((1-value.level)*100))")
if value.timeToCharge > 0 {
subtitle += " (\(Double(value.timeToCharge*60).printSecondsToHoursMinutesSeconds()))"
}
self.checkDouble(id: self.highID, value: value.level, threshold: threshold, title: title, subtitle: subtitle)
}
}
@objc private func changeLowLevel(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.lowLevel = key.isEmpty ? "" : "\(Double(key) ?? 0)"
Store.shared.set(key: "\(self.module)_notifications_low", value: self.lowLevel)
}
@objc private func changeHighLevel(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.highLevel = key.isEmpty ? "" : "\(Double(key) ?? 0)"
Store.shared.set(key: "\(self.module)_notifications_high", value: self.highLevel)
}
}
================================================
FILE: Modules/Battery/popup.swift
================================================
//
// popup.swift
// Battery
//
// Created by Serhiy Mytrovtsiy on 06/06/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class Popup: PopupWrapper {
private let dashboardHeight: CGFloat = 90
private var detailsHeight: CGFloat = (22 * 4) + Constants.Popup.separatorHeight
private let batteryHeight: CGFloat = (22 * 7) + Constants.Popup.separatorHeight
private let adapterHeight: CGFloat = (22 * 4) + Constants.Popup.separatorHeight
private let processHeight: CGFloat = 22
private var dashboardView: NSView? = nil
private var dashboardBatteryView: BatteryView? = nil
private var detailsView: NSView? = nil
private var batteryView: NSView? = nil
private var adapterView: NSView? = nil
private var processesView: NSView? = nil
private var levelField: NSTextField? = nil
private var sourceField: NSTextField? = nil
private var timeLabelField: NSTextField? = nil
private var timeField: NSTextField? = nil
private var healthField: NSTextField? = nil
private var capacityField: NSTextField? = nil
private var cyclesField: NSTextField? = nil
private var lastChargeField: NSTextField? = nil
private var amperageField: NSTextField? = nil
private var voltageField: NSTextField? = nil
private var batteryPowerField: NSTextField? = nil
private var temperatureField: NSTextField? = nil
private var powerField: NSTextField? = nil
private var chargingStateField: NSTextField? = nil
private var chargingCurrentField: NSTextField? = nil
private var chargingVoltageField: NSTextField? = nil
private var processes: ProcessesView? = nil
private var processesInitialized: Bool = false
private var colorState: Bool = false
private var numberOfProcesses: Int {
Store.shared.int(key: "\(self.title)_processes", defaultValue: 8)
}
private var processesHeight: CGFloat {
(self.processHeight*CGFloat(self.numberOfProcesses)) + (self.numberOfProcesses == 0 ? 0 : Constants.Popup.separatorHeight + 22)
}
private var timeFormat: String {
Store.shared.string(key: "\(self.title)_timeFormat", defaultValue: "short")
}
public init(_ module: ModuleType) {
super.init(module, frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
self.spacing = 0
self.orientation = .vertical
self.colorState = Store.shared.bool(key: "\(self.title)_color", defaultValue: self.colorState)
self.addArrangedSubview(self.initDashboard())
self.addArrangedSubview(self.initDetails())
self.addArrangedSubview(self.initBattery())
self.addArrangedSubview(self.initProcesses())
self.recalculateHeight()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func disappear() {
self.processes?.setLock(false)
}
private func recalculateHeight() {
var h: CGFloat = 0
self.arrangedSubviews.forEach { v in
if let v = v as? NSStackView {
h += v.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +)
} else {
h += v.bounds.height
}
}
if self.frame.size.height != h {
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.sizeCallback?(self.frame.size)
}
}
private func initDashboard() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.dashboardHeight))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let container: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: self.dashboardHeight))
self.dashboardBatteryView = BatteryView(frame: NSRect(
x: Constants.Popup.margins,
y: Constants.Popup.margins,
width: view.frame.width - (Constants.Popup.margins*2),
height: view.frame.height - (Constants.Popup.margins*2)
))
container.addSubview(self.dashboardBatteryView!)
view.addSubview(container)
return view
}
private func initDetails() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.detailsHeight))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let separator = separatorView(localizedString("Details"), origin: NSPoint(x: 0, y: self.detailsHeight-Constants.Popup.separatorHeight), width: self.frame.width)
let container: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: separator.frame.origin.y))
container.orientation = .vertical
container.spacing = 0
self.levelField = popupRow(container, title: "\(localizedString("Level")):", value: "").1
self.sourceField = popupRow(container, title: "\(localizedString("Source")):", value: "").1
let t = self.labelValue(container, title: "\(localizedString("Time")):", value: "")
self.timeLabelField = t.0
self.timeField = t.1
self.lastChargeField = popupRow(container, title: "\(localizedString("Last charge")):", value: "").1
view.addSubview(separator)
view.addSubview(container)
return view
}
private func initBattery() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.batteryHeight))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let separator = separatorView(localizedString("Battery"), origin: NSPoint(x: 0, y: self.batteryHeight-Constants.Popup.separatorHeight), width: self.frame.width)
let container: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: separator.frame.origin.y))
container.orientation = .vertical
container.spacing = 0
self.healthField = popupRow(container, title: "\(localizedString("Health")):", value: "").1
self.capacityField = popupRow(container, title: "\(localizedString("Capacity")):", value: "").1
self.capacityField?.toolTip = localizedString("current / maximum / designed")
self.cyclesField = popupRow(container, title: "\(localizedString("Cycles")):", value: "").1
self.temperatureField = popupRow(container, title: "\(localizedString("Temperature")):", value: "").1
self.batteryPowerField = popupRow(container, title: "\(localizedString("Power")):", value: "").1
self.amperageField = popupRow(container, title: "\(localizedString("Current")):", value: "").1
self.voltageField = popupRow(container, title: "\(localizedString("Voltage")):", value: "").1
view.addSubview(separator)
view.addSubview(container)
return view
}
private func initAdapter() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.adapterHeight))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let separator = separatorView(localizedString("Power adapter"), origin: NSPoint(x: 0, y: self.adapterHeight-Constants.Popup.separatorHeight), width: self.frame.width)
let container: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: separator.frame.origin.y))
container.orientation = .vertical
container.spacing = 0
self.chargingStateField = popupRow(container, title: "\(localizedString("Is charging")):", value: "").1
self.powerField = popupRow(container, title: "\(localizedString("Power")):", value: "").1
self.chargingCurrentField = popupRow(container, title: "\(localizedString("Current")):", value: "").1
self.chargingVoltageField = popupRow(container, title: "\(localizedString("Voltage")):", value: "").1
self.adapterView = view
view.addSubview(separator)
view.addSubview(container)
return view
}
private func initProcesses() -> NSView {
if self.numberOfProcesses == 0 { return NSView() }
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.processesHeight))
let separator = separatorView(localizedString("Top processes"), origin: NSPoint(x: 0, y: self.processesHeight-Constants.Popup.separatorHeight), width: self.frame.width)
let container: ProcessesView = ProcessesView(
frame: NSRect(x: 0, y: 0, width: self.frame.width, height: separator.frame.origin.y),
values: [(localizedString("Usage"), nil)],
n: self.numberOfProcesses
)
self.processes = container
view.addSubview(separator)
view.addSubview(container)
self.processesView = view
return view
}
private func labelValue(_ view: NSView, title: String, value: String) -> (NSTextField, NSTextField) {
let rowView: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 22))
let labelView: LabelField = LabelField(frame: NSRect(x: 0, y: (22-15)/2, width: view.frame.width/2, height: 15), title)
let valueView: ValueField = ValueField(frame: NSRect(x: view.frame.width/2, y: (22-16)/2, width: view.frame.width/2, height: 16), value)
rowView.addSubview(labelView)
rowView.addSubview(valueView)
if let view = view as? NSStackView {
rowView.heightAnchor.constraint(equalToConstant: rowView.bounds.height).isActive = true
view.addArrangedSubview(rowView)
} else {
view.addSubview(rowView)
}
return (labelView, valueView)
}
public func usageCallback(_ value: Battery_Usage) {
DispatchQueue.main.async(execute: {
self.dashboardBatteryView?.setValue(abs(value.level))
self.levelField?.stringValue = "\(Int(abs(value.level) * 100))%"
self.levelField?.toolTip = "\(value.currentCapacity) mAh"
self.sourceField?.stringValue = localizedString(value.powerSource)
self.timeField?.stringValue = ""
if value.isBatteryPowered {
self.timeLabelField?.stringValue = "\(localizedString("Time to discharge")):"
if value.timeToEmpty != -1 && value.timeToEmpty != 0 {
self.timeField?.stringValue = Double(value.timeToEmpty*60).printSecondsToHoursMinutesSeconds(short: self.timeFormat == "short")
} else {
self.timeField?.stringValue = localizedString("Unknown")
}
if self.adapterView != nil {
self.adapterView?.removeFromSuperview()
self.adapterView = nil
self.recalculateHeight()
}
} else {
self.timeLabelField?.stringValue = "\(localizedString("Time to charge")):"
if value.timeToCharge != -1 && value.timeToCharge != 0 {
self.timeField?.stringValue = Double(value.timeToCharge*60).printSecondsToHoursMinutesSeconds(short: self.timeFormat == "short")
} else {
self.timeField?.stringValue = localizedString("Unknown")
}
if self.adapterView == nil {
self.insertArrangedSubview(self.initAdapter(), at: 3)
self.recalculateHeight()
}
}
if value.timeToEmpty == -1 || value.timeToCharge == -1 {
self.timeField?.stringValue = localizedString("Calculating")
}
if value.isCharged {
self.timeField?.stringValue = localizedString("Fully charged")
}
self.healthField?.stringValue = "\(value.health)%"
self.capacityField?.stringValue = "\(value.currentCapacity) / \(value.maxCapacity) / \(value.designedCapacity) mAh"
if let state = value.state {
self.healthField?.stringValue += " (\(state))"
}
self.cyclesField?.stringValue = "\(value.cycles)"
let form = DateComponentsFormatter()
form.maximumUnitCount = 2
form.unitsStyle = .full
form.allowedUnits = [.day, .hour, .minute]
if let timestamp = value.timeOnACPower {
if let duration = form.string(from: timestamp, to: Date()) {
let formatter = DateFormatter()
formatter.timeStyle = .short
formatter.dateStyle = .medium
self.lastChargeField?.stringValue = duration
self.lastChargeField?.toolTip = formatter.string(from: timestamp)
} else {
self.lastChargeField?.stringValue = localizedString("Unknown")
self.lastChargeField?.toolTip = localizedString("Unknown")
}
} else {
self.lastChargeField?.stringValue = localizedString("Unknown")
self.lastChargeField?.toolTip = localizedString("Unknown")
}
self.amperageField?.stringValue = "\(abs(value.amperage)) mA"
self.voltageField?.stringValue = "\(value.voltage.roundTo(decimalPlaces: 2)) V"
let batteryPower = value.voltage * (Double(abs(value.amperage))/1000)
self.batteryPowerField?.stringValue = "\(batteryPower.roundTo(decimalPlaces: 2)) W"
self.temperatureField?.stringValue = temperature(value.temperature)
self.powerField?.stringValue = value.isBatteryPowered ? localizedString("Not connected") : "\(value.ACwatts) W"
self.chargingStateField?.stringValue = value.isCharging ? localizedString("Yes") : localizedString("No")
self.chargingCurrentField?.stringValue = value.isBatteryPowered ? localizedString("Not connected") : "\(value.chargingCurrent) mA"
self.chargingVoltageField?.stringValue = value.isBatteryPowered ? localizedString("Not connected") : "\(value.chargingVoltage) mV"
})
}
public func processCallback(_ list: [TopProcess]) {
DispatchQueue.main.async(execute: {
if !(self.window?.isVisible ?? false) && self.processesInitialized {
return
}
let list = list.map { $0 }
if list.count != self.processes?.count { self.processes?.clear() }
for i in 0.. NSView? {
let view = SettingsContainerView()
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Keyboard shortcut"), component: KeyboardShartcutView(
callback: self.setKeyboardShortcut,
value: self.keyboardShortcut
))
]))
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Colorize battery"), component: switchView(
action: #selector(self.toggleColor),
state: self.colorState
))
]))
return view
}
@objc private func toggleColor(_ sender: NSControl) {
self.colorState = controlState(sender)
Store.shared.set(key: "\(self.title)_color", value: self.colorState)
self.dashboardBatteryView?.display()
}
}
internal class BatteryView: NSView {
private var percentage: Double = 0
private var colorState: Bool {
return Store.shared.bool(key: "Battery_color", defaultValue: false)
}
public override init(frame: NSRect = NSRect.zero) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
guard let ctx = NSGraphicsContext.current?.cgContext else { return }
let w: CGFloat = min(self.frame.width, 120)
let h: CGFloat = min(self.frame.height, 50)
let x: CGFloat = (self.frame.width - w)/2
let y: CGFloat = (self.frame.size.height - h) / 2
let batteryFrame = NSBezierPath(roundedRect: NSRect(x: x+1, y: y+1, width: w-8, height: h-2), xRadius: 3, yRadius: 3)
NSColor.textColor.set()
let bPX: CGFloat = batteryFrame.bounds.origin.x + batteryFrame.bounds.width
let bPY: CGFloat = batteryFrame.bounds.origin.y + (batteryFrame.bounds.height/2) - 4
let batteryPoint = NSBezierPath(roundedRect: NSRect(x: bPX-2, y: bPY, width: 8, height: 8), xRadius: 4, yRadius: 4)
batteryPoint.fill()
let batteryPointSeparator = NSBezierPath()
batteryPointSeparator.move(to: CGPoint(x: bPX, y: batteryFrame.bounds.origin.y))
batteryPointSeparator.line(to: CGPoint(x: bPX, y: batteryFrame.bounds.origin.y + batteryFrame.bounds.height))
ctx.saveGState()
ctx.setBlendMode(.destinationOut)
NSColor.textColor.set()
batteryPointSeparator.lineWidth = 4
batteryPointSeparator.stroke()
ctx.restoreGState()
batteryFrame.lineWidth = 1
batteryFrame.stroke()
let inner = NSBezierPath(roundedRect: NSRect(
x: x+2,
y: y+2,
width: (w-10) * CGFloat(self.percentage),
height: h-4
), xRadius: 3, yRadius: 3)
self.percentage.batteryColor(color: self.colorState).set()
inner.lineWidth = 0
inner.stroke()
inner.close()
inner.fill()
}
public func setValue(_ value: Double) {
if self.percentage == value {
return
}
self.percentage = value
DispatchQueue.main.async(execute: {
self.display()
})
}
}
================================================
FILE: Modules/Battery/portal.swift
================================================
//
// portal.swift
// Battery
//
// Created by Serhiy Mytrovtsiy on 16/03/2023
// Using Swift 5.0
// Running on macOS 13.2
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class Portal: NSStackView, Portal_p {
var name: String
private let batteryView: BatteryView = BatteryView()
private var levelField: NSTextField = ValueField(frame: NSRect.zero, "")
private var timeField: NSTextField = ValueField(frame: NSRect.zero, "")
private var chargingField: NSTextField = ValueField(frame: NSRect.zero, "")
private var initialized: Bool = false
private var timeFormat: String {
Store.shared.string(key: "\(self.name)_timeFormat", defaultValue: "short")
}
public init(_ module: ModuleType) {
self.name = module.stringValue
super.init(frame: NSRect.zero)
self.wantsLayer = true
self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
self.layer?.cornerRadius = 3
self.orientation = .vertical
self.distribution = .fillEqually
self.spacing = Constants.Popup.spacing*2
self.edgeInsets = NSEdgeInsets(
top: Constants.Popup.spacing*2,
left: Constants.Popup.spacing*2,
bottom: Constants.Popup.spacing*2,
right: Constants.Popup.spacing*2
)
self.addArrangedSubview(PortalHeader(name))
let box: NSStackView = NSStackView()
box.heightAnchor.constraint(equalToConstant: 15).isActive = true
box.orientation = .horizontal
box.distribution = .fillEqually
box.spacing = 0
box.edgeInsets = NSEdgeInsets(top: 0, left: Constants.Popup.spacing*2, bottom: 0, right: Constants.Popup.spacing*2)
self.levelField.font = NSFont.systemFont(ofSize: 12, weight: .medium)
self.levelField.alignment = .left
self.chargingField.font = NSFont.systemFont(ofSize: 12, weight: .medium)
self.chargingField.alignment = .center
self.timeField.font = NSFont.systemFont(ofSize: 12, weight: .medium)
self.timeField.alignment = .right
let leftStack = NSStackView(views: [self.levelField])
leftStack.orientation = .horizontal
leftStack.alignment = .leading
let centerStack = NSStackView(views: [self.chargingField])
centerStack.orientation = .horizontal
centerStack.alignment = .centerX
let rightStack = NSStackView(views: [self.timeField])
rightStack.orientation = .horizontal
rightStack.alignment = .trailing
box.addArrangedSubview(leftStack)
box.addArrangedSubview(NSView())
box.addArrangedSubview(centerStack)
box.addArrangedSubview(NSView())
box.addArrangedSubview(rightStack)
self.addArrangedSubview(self.batteryView)
self.addArrangedSubview(box)
self.heightAnchor.constraint(equalToConstant: Constants.Popup.portalHeight).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func updateLayer() {
self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
}
public func loadCallback(_ value: Battery_Usage) {
DispatchQueue.main.async(execute: {
self.levelField.stringValue = "\(Int(abs(value.level) * 100))%"
var seconds: Double = 0
if value.timeToEmpty != -1 && value.timeToEmpty != 0 {
seconds = Double((value.isBatteryPowered ? value.timeToEmpty : value.timeToCharge)*60)
}
self.timeField.stringValue = seconds != 0 ? seconds.printSecondsToHoursMinutesSeconds(short: self.timeFormat == "short") : ""
self.batteryView.setValue(abs(value.level))
if !value.isBatteryPowered {
self.chargingField.stringValue = value.isCharging ? localizedString("Charging") : localizedString("Connected")
} else if self.chargingField.stringValue != "" {
self.chargingField.stringValue = ""
}
self.initialized = true
})
}
}
================================================
FILE: Modules/Battery/readers.swift
================================================
//
// readers.swift
// Battery
//
// Created by Serhiy Mytrovtsiy on 06/06/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class UsageReader: Reader {
private var service: io_connect_t = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleSmartBattery"))
private var source: CFRunLoopSource?
private var loop: CFRunLoop?
private var usage: Battery_Usage = Battery_Usage()
public override func start() {
self.active = true
let context = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
self.source = IOPSNotificationCreateRunLoopSource({ (context) in
guard let ctx = context else {
return
}
let watcher = Unmanaged.fromOpaque(ctx).takeUnretainedValue()
if watcher.active {
watcher.read()
}
}, context).takeRetainedValue()
self.loop = RunLoop.current.getCFRunLoop()
CFRunLoopAddSource(self.loop, source, .defaultMode)
self.read()
}
public override func stop() {
guard let runLoop = loop, let source = source else {
return
}
self.active = false
CFRunLoopRemoveSource(runLoop, source, .defaultMode)
}
public override func read() {
let psInfo = IOPSCopyPowerSourcesInfo().takeRetainedValue()
let psList = IOPSCopyPowerSourcesList(psInfo).takeRetainedValue() as [CFTypeRef]
if psList.isEmpty {
return
}
for ps in psList {
if let list = IOPSGetPowerSourceDescription(psInfo, ps).takeUnretainedValue() as? [String: Any] {
self.usage.powerSource = list[kIOPSPowerSourceStateKey] as? String ?? "AC Power"
self.usage.isBatteryPowered = self.usage.powerSource == "Battery Power"
self.usage.isCharged = list[kIOPSIsChargedKey] as? Bool ?? false
self.usage.isCharging = self.getBoolValue("IsCharging" as CFString) ?? false
self.usage.optimizedChargingEngaged = list["Optimized Battery Charging Engaged"] as? Int == 1
self.usage.level = Double(list[kIOPSCurrentCapacityKey] as? Int ?? 0) / 100
if let time = list[kIOPSTimeToEmptyKey] as? Int {
self.usage.timeToEmpty = Int(time)
}
if let time = list[kIOPSTimeToFullChargeKey] as? Int {
self.usage.timeToCharge = Int(time)
}
if self.usage.powerSource == "AC Power" {
self.usage.timeOnACPower = Date()
}
self.usage.cycles = self.getIntValue("CycleCount" as CFString) ?? 0
self.usage.currentCapacity = self.getIntValue("AppleRawCurrentCapacity" as CFString) ?? 0
self.usage.designedCapacity = self.getIntValue("DesignCapacity" as CFString) ?? 1
if self.usage.designedCapacity == 0 {
self.usage.designedCapacity = 1
}
self.usage.maxCapacity = self.getIntValue((isARM ? "AppleRawMaxCapacity" : "MaxCapacity") as CFString) ?? 1
if !isARM {
self.usage.state = list[kIOPSBatteryHealthKey] as? String
}
self.usage.health = Int((Double(100 * self.usage.maxCapacity) / Double(self.usage.designedCapacity)).rounded(.toNearestOrEven))
self.usage.amperage = self.getIntValue("Amperage" as CFString) ?? 0
self.usage.voltage = self.getVoltage() ?? 0
self.usage.temperature = self.getTemperature() ?? 0
var ACwatts: Int = 0
if let ACDetails = IOPSCopyExternalPowerAdapterDetails() {
if let ACList = ACDetails.takeRetainedValue() as? [String: Any] {
guard let watts = ACList[kIOPSPowerAdapterWattsKey] else {
return
}
ACwatts = Int(watts as! Int)
}
}
self.usage.ACwatts = ACwatts
if let chargerData = self.getChargerData() {
self.usage.chargingCurrent = chargerData["ChargingCurrent"] as? Int ?? 0
self.usage.chargingVoltage = chargerData["ChargingVoltage"] as? Int ?? 0
}
self.callback(self.usage)
}
}
}
private func getBoolValue(_ forIdentifier: CFString) -> Bool? {
if let value = IORegistryEntryCreateCFProperty(self.service, forIdentifier, kCFAllocatorDefault, 0) {
return value.takeRetainedValue() as? Bool
}
return nil
}
private func getIntValue(_ identifier: CFString) -> Int? {
if let value = IORegistryEntryCreateCFProperty(self.service, identifier, kCFAllocatorDefault, 0) {
return value.takeRetainedValue() as? Int
}
return nil
}
private func getDoubleValue(_ identifier: CFString) -> Double? {
if let value = IORegistryEntryCreateCFProperty(self.service, identifier, kCFAllocatorDefault, 0) {
return value.takeRetainedValue() as? Double
}
return nil
}
private func getVoltage() -> Double? {
if let value = self.getDoubleValue("Voltage" as CFString) {
return value / 1000.0
}
return nil
}
private func getTemperature() -> Double? {
if let value = IORegistryEntryCreateCFProperty(self.service, "Temperature" as CFString, kCFAllocatorDefault, 0) {
return value.takeRetainedValue() as! Double / 100.0
}
return nil
}
private func getChargerData() -> [String: Any]? {
if let chargerData = IORegistryEntryCreateCFProperty(service, "ChargerData" as CFString, kCFAllocatorDefault, 0) {
return chargerData.takeRetainedValue() as? [String: Any]
}
return nil
}
}
public class ProcessReader: Reader<[TopProcess]> {
private var numberOfProcesses: Int {
get {
return Store.shared.int(key: "Battery_processes", defaultValue: 8)
}
}
public override func setup() {
self.popup = true
}
public override func read() {
if self.numberOfProcesses == 0 {
return
}
let task = Process()
task.launchPath = "/usr/bin/top"
task.arguments = ["-o", "power", "-l", "2", "-n", "\(self.numberOfProcesses)", "-stats", "pid,command,power"]
let outputPipe = Pipe()
defer {
outputPipe.fileHandleForReading.closeFile()
}
task.standardOutput = outputPipe
do {
try task.run()
} catch let err {
error("error read ps: \(err.localizedDescription)", log: self.log)
return
}
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
if outputData.isEmpty {
return
}
let output = String(data: outputData.advanced(by: outputData.count/2), encoding: .utf8)
guard let output, !output.isEmpty else { return }
var processes: [TopProcess] = []
output.enumerateLines { (line, _) in
if line.matches("^\\d+ *[^(\\d)]*\\d+\\.*\\d* *$") {
let str = line.trimmingCharacters(in: .whitespaces)
let pidFind = str.findAndCrop(pattern: "^\\d+")
let usageFind = pidFind.remain.findAndCrop(pattern: " +[0-9]+.*[0-9]*$")
let command = usageFind.remain.trimmingCharacters(in: .whitespaces)
let pid = Int(pidFind.cropped) ?? 0
guard let usage = Double(usageFind.cropped.filter("01234567890.".contains)) else {
return
}
var name: String = command
if let app = NSRunningApplication(processIdentifier: pid_t(pid)), let n = app.localizedName {
name = n
}
processes.append(TopProcess(pid: pid, name: name, usage: usage))
}
}
self.callback(processes.suffix(self.numberOfProcesses).sorted(by: { $0.usage > $1.usage }))
}
}
================================================
FILE: Modules/Battery/settings.swift
================================================
//
// settings.swift
// Battery
//
// Created by Serhiy Mytrovtsiy on 15/07/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
import SystemConfiguration
internal class Settings: NSStackView, Settings_v {
public var callback: (() -> Void) = {}
public var callbackWhenUpdateNumberOfProcesses: (() -> Void) = {}
private let title: String
private var button: NSPopUpButton?
private var numberOfProcesses: Int = 8
private var timeFormat: String = "short"
public init(_ module: ModuleType) {
self.title = module.stringValue
self.numberOfProcesses = Store.shared.int(key: "\(self.title)_processes", defaultValue: self.numberOfProcesses)
self.timeFormat = Store.shared.string(key: "\(self.title)_timeFormat", defaultValue: self.timeFormat)
super.init(frame: NSRect.zero)
self.orientation = .vertical
self.spacing = Constants.Settings.margin
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func load(widgets: [widget_t]) {
self.subviews.forEach{ $0.removeFromSuperview() }
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Number of top processes"), component: selectView(
action: #selector(self.changeNumberOfProcesses),
items: NumbersOfProcesses.map{ KeyValue_t(key: "\($0)", value: "\($0)") },
selected: "\(self.numberOfProcesses)"
))
]))
if !widgets.filter({ $0 == .battery }).isEmpty {
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Time format"), component: selectView(
action: #selector(toggleTimeFormat),
items: ShortLong,
selected: self.timeFormat
))
]))
}
}
@objc private func changeNumberOfProcesses(_ sender: NSMenuItem) {
if let value = Int(sender.title) {
self.numberOfProcesses = value
Store.shared.set(key: "\(self.title)_processes", value: value)
self.callbackWhenUpdateNumberOfProcesses()
}
}
@objc private func toggleTimeFormat(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.timeFormat = key
Store.shared.set(key: "\(self.title)_timeFormat", value: key)
self.callback()
}
}
================================================
FILE: Modules/Bluetooth/Info.plist
================================================
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
1.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSHumanReadableCopyright
Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
================================================
FILE: Modules/Bluetooth/config.plist
================================================
Name
Bluetooth
State
Symbol
point.3.filled.connected.trianglepath.dotted
AlternativeSymbol
scale.3d
Widgets
label
Default
Title
BLE
Order
0
sensors
Default
Preview
Values
98%
Order
1
Settings
popup
notifications
================================================
FILE: Modules/Bluetooth/main.swift
================================================
//
// main.swift
// Bluetooth
//
// Created by Serhiy Mytrovtsiy on 08/06/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
import Foundation
import Kit
import CoreBluetooth
public struct BLEDevice: Codable {
let address: String
var name: String
var uuid: UUID?
var RSSI: Int? = nil
var batteryLevel: [KeyValue_t] = []
var isConnected: Bool = false
var isPaired: Bool = false
var peripheral: CBPeripheral? = nil
var isPeripheralInitialized: Bool = false
var id: String {
get { self.uuid?.uuidString ?? self.address }
}
var state: Bool {
get { Store.shared.bool(key: "ble_\(self.id)", defaultValue: false) }
}
var notificationThreshold: String {
Store.shared.string(key: "ble_\(self.id)_notification", defaultValue: "")
}
private enum CodingKeys: String, CodingKey {
case address, name, uuid, RSSI, batteryLevel, isConnected, isPaired
}
init(address: String, name: String, uuid: UUID?, RSSI: Int?, batteryLevel: [KeyValue_t], isConnected: Bool, isPaired: Bool) {
self.address = address
self.name = name
self.uuid = uuid
self.RSSI = RSSI
self.batteryLevel = batteryLevel
self.isConnected = isConnected
self.isPaired = isPaired
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.address = try container.decode(String.self, forKey: .address)
self.name = try container.decode(String.self, forKey: .name)
self.uuid = try? container.decode(UUID.self, forKey: .uuid)
self.RSSI = try? container.decode(Int.self, forKey: .RSSI)
self.batteryLevel = try container.decode(Array.self, forKey: .batteryLevel)
self.isConnected = try container.decode(Bool.self, forKey: .isConnected)
self.isPaired = try container.decode(Bool.self, forKey: .isPaired)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(address, forKey: .address)
try container.encode(name, forKey: .name)
try container.encode(uuid, forKey: .uuid)
try container.encode(RSSI, forKey: .RSSI)
try container.encode(batteryLevel, forKey: .batteryLevel)
try container.encode(isConnected, forKey: .isConnected)
try container.encode(isPaired, forKey: .isPaired)
}
}
public class Bluetooth: Module {
private var devicesReader: DevicesReader?
private let popupView: Popup = Popup()
private let settingsView: Settings = Settings()
private let notificationsView: Notifications
public init() {
self.notificationsView = Notifications(.bluetooth)
super.init(
moduleType: .bluetooth,
popup: self.popupView,
settings: self.settingsView,
notifications: self.notificationsView
)
guard self.available else { return }
self.devicesReader = DevicesReader { [weak self] value in
self?.batteryCallback(value)
}
self.settingsView.callback = { [weak self] in
self?.devicesReader?.read()
}
self.setReaders([self.devicesReader])
}
private func batteryCallback(_ raw: [BLEDevice]?) {
guard let value = raw, self.enabled else { return }
let active = value.filter{ $0.isPaired || ($0.isConnected && !$0.batteryLevel.isEmpty) }
DispatchQueue.main.async(execute: {
self.popupView.batteryCallback(active)
self.settingsView.setList(active)
self.notificationsView.callback(active)
})
var list: [Stack_t] = []
active.forEach { (d: BLEDevice) in
if d.state {
d.batteryLevel.forEach { (p: KeyValue_t) in
list.append(Stack_t(key: "\(d.address)-\(p.key)", value: "\(p.value)%"))
}
}
}
self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: SWidget) in
switch w.item {
case let widget as StackWidget: widget.setValues(list)
default: break
}
}
}
}
================================================
FILE: Modules/Bluetooth/notifications.swift
================================================
//
// notifications.swift
// Bluetooth
//
// Created by Serhiy Mytrovtsiy on 24/06/2024
// Using Swift 5.0
// Running on macOS 14.5
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
class Notifications: NotificationsWrapper {
private var list: [String: Bool] = [:]
private let emptyView: EmptyView = EmptyView(msg: localizedString("No Bluetooth devices are available"))
private var section: PreferencesSection = PreferencesSection()
public init(_ module: ModuleType) {
super.init(module)
self.addArrangedSubview(self.emptyView)
self.addArrangedSubview(self.section)
self.section.isHidden = true
self.addArrangedSubview(NSView())
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func callback(_ list: [BLEDevice]) {
if self.list.count != list.count && !self.list.isEmpty {
self.section.removeFromSuperview()
self.section = PreferencesSection()
self.addArrangedSubview(self.section)
self.list = [:]
}
if list.isEmpty && self.emptyView.isHidden {
self.emptyView.isHidden = false
self.section.isHidden = true
return
} else if !list.isEmpty && !self.emptyView.isHidden {
self.emptyView.isHidden = true
self.section.isHidden = false
}
list.forEach { (d: BLEDevice) in
if self.list[d.id] == nil {
let btn = selectView(
action: #selector(self.changeSensorNotificaion),
items: notificationLevels,
selected: d.notificationThreshold
)
btn.identifier = NSUserInterfaceItemIdentifier(rawValue: "\(d.uuid?.uuidString ?? d.address)")
section.add(PreferencesRow(d.name, component: btn))
self.list[d.id] = true
}
}
let devices = list.filter({ !$0.notificationThreshold.isEmpty })
let title = localizedString("Bluetooth threshold")
for d in devices {
if let threshold = Double(d.notificationThreshold) {
for l in d.batteryLevel {
let subtitle = localizedString("\(localizedString(d.name)): \(l.value)%")
if let value = Double(l.value) {
self.checkDouble(id: d.id, value: value/100, threshold: threshold, title: title, subtitle: subtitle, less: true)
}
}
}
}
}
@objc private func changeSensorNotificaion(_ sender: NSMenuItem) {
guard let id = sender.identifier, let key = sender.representedObject as? String else { return }
Store.shared.set(key: "ble_\(id.rawValue)_notification", value: key)
}
}
================================================
FILE: Modules/Bluetooth/popup.swift
================================================
//
// popup.swift
// Bluetooth
//
// Created by Serhiy Mytrovtsiy on 22/06/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class Popup: PopupWrapper {
private let emptyView: EmptyView = EmptyView(height: 30, isHidden: false, msg: localizedString("No Bluetooth devices are available"))
public init() {
super.init(ModuleType.bluetooth, frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 30))
self.orientation = .vertical
self.spacing = Constants.Popup.margins
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func batteryCallback(_ list: [BLEDevice]) {
defer {
if list.isEmpty && self.emptyView.superview == nil {
self.addArrangedSubview(self.emptyView)
} else if !list.isEmpty && self.emptyView.superview != nil {
self.emptyView.removeFromSuperview()
}
let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - self.spacing
if h > 0 && self.frame.size.height != h {
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.sizeCallback?(self.frame.size)
}
}
var views = self.subviews.filter{ $0 is BLEView }.map{ $0 as! BLEView }
if list.count < views.count && !views.isEmpty {
views.forEach{ $0.removeFromSuperview() }
views = []
}
list.reversed().forEach { (ble: BLEDevice) in
if let view = self.subviews.filter({ $0 is BLEView }).map({ $0 as! BLEView }).first(where: { $0.address == ble.address }) {
view.update(ble.batteryLevel)
} else {
self.addArrangedSubview(BLEView(
width: self.frame.width,
address: ble.address,
name: ble.name,
batteryLevel: ble.batteryLevel
))
}
}
}
}
internal class BLEView: NSStackView {
public var address: String
open override var intrinsicContentSize: CGSize {
return CGSize(width: self.bounds.width, height: self.bounds.height)
}
private var levels: [NSTextField] = []
public init(width: CGFloat, address: String, name: String, batteryLevel: [KeyValue_t]) {
self.address = address
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 30))
self.orientation = .horizontal
self.alignment = .centerY
self.spacing = 0
self.wantsLayer = true
self.edgeInsets = NSEdgeInsets(top: 0, left: 5, bottom: 0, right: 5)
self.layer?.cornerRadius = 2
let nameView: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 16))
nameView.font = NSFont.systemFont(ofSize: 13, weight: .light)
nameView.stringValue = name
nameView.toolTip = address
self.addArrangedSubview(nameView)
self.addArrangedSubview(NSView())
batteryLevel.forEach { (pair: KeyValue_t) in
self.addLevel(pair)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateLayer() {
self.layer?.backgroundColor = (isDarkMode ? NSColor(red: 17/255, green: 17/255, blue: 17/255, alpha: 0.25) : NSColor(red: 245/255, green: 245/255, blue: 245/255, alpha: 1)).cgColor
}
public func update(_ batteryLevel: [KeyValue_t]) {
self.levels.filter{ v in !batteryLevel.contains(where: { $0.key == v.identifier?.rawValue }) }.forEach { (v: NSView) in
v.removeFromSuperview()
}
self.levels = self.levels.filter{ v in batteryLevel.contains(where: { $0.key == v.identifier?.rawValue }) }
batteryLevel.forEach { (pair: KeyValue_t) in
if let view = self.levels.first(where: { $0.identifier?.rawValue == pair.key }) {
view.stringValue = "\(pair.value)%"
if let additional = pair.additional as? String {
view.toolTip = "\(pair.key) - \(additional)"
} else {
view.toolTip = pair.key
}
} else {
self.addLevel(pair)
}
}
}
private func addLevel(_ pair: KeyValue_t) {
let valueView: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 13))
valueView.identifier = NSUserInterfaceItemIdentifier(rawValue: pair.key)
valueView.font = NSFont.systemFont(ofSize: 12, weight: .regular)
valueView.stringValue = "\(pair.value)%"
if let additional = pair.additional as? String {
valueView.toolTip = "\(pair.key) - \(additional)"
} else {
valueView.toolTip = pair.key
}
self.addArrangedSubview(valueView)
self.levels.append(valueView)
}
}
================================================
FILE: Modules/Bluetooth/readers.swift
================================================
//
// readers.swift
// Bluetooth
//
// Created by Serhiy Mytrovtsiy on 08/06/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
import Foundation
import Kit
import CoreBluetooth
import IOBluetooth
private struct bleDevice {
var name: String?
var address: String
var uuid: UUID?
var batteryLevel: [KeyValue_t]
}
private struct ioDevice {
var name: String
var address: String
var rssi: Int8
var isConnected: Bool
var isPaired: Bool
}
internal class DevicesReader: Reader<[BLEDevice]>, CBCentralManagerDelegate, CBPeripheralDelegate {
private var devices: [BLEDevice] = []
private var devicesToRemove: [UUID] = []
private var manager: CBCentralManager!
private var characteristicsDict: [UUID: CBCharacteristic] = [:]
private var bleLevels: [UUID: KeyValue_t] = [:]
static let batteryServiceUUID = CBUUID(string: "0x180F")
static let batteryCharacteristicsUUID = CBUUID(string: "0x2A19")
init(callback: @escaping (T?) -> Void = {_ in }) {
super.init(.bluetooth, callback: callback)
self.manager = CBCentralManager(delegate: self, queue: nil)
}
public override func read() {
let hid = self.HIDDevices()
let SPB = self.profilerDevices()
var list = self.cacheDevices()
let pmsetLevels = self.pmsetAccessoryLevels()
hid.forEach { v in
if !list.contains(where: {$0.address == v.address}) {
list.append(v)
}
}
SPB.0.forEach { v in
if !list.contains(where: {$0.address == v.address}) {
list.append(v)
}
}
let pairedDevices: [ioDevice] = IOBluetoothDevice.pairedDevices()?.compactMap({
if let device = $0 as? IOBluetoothDevice, device.isPaired() || device.isConnected() {
return ioDevice(
name: device.nameOrAddress,
address: device.addressString,
rssi: device.rssi(),
isConnected: device.isConnected(),
isPaired: device.isPaired()
)
}
return nil
}) ?? []
pairedDevices.forEach { (device: ioDevice) in
guard let data = list.first(where: { $0.address == device.address }) else {
return
}
let rssi = device.rssi == 127 ? nil : Int(device.rssi)
if let idx = self.devices.firstIndex(where: { $0.address == data.address }) {
self.devices[idx].RSSI = rssi
self.devices[idx].batteryLevel = data.batteryLevel
self.devices[idx].isPaired = device.isPaired
self.devices[idx].isConnected = device.isConnected
return
}
self.devices.append(BLEDevice(
address: data.address,
name: data.name ?? device.name,
uuid: data.uuid,
RSSI: rssi,
batteryLevel: data.batteryLevel,
isConnected: device.isConnected,
isPaired: device.isPaired
))
}
let peripherals = self.manager.retrievePeripherals(withIdentifiers: self.devices.compactMap({ $0.uuid }))
peripherals.forEach { (p: CBPeripheral) in
guard let idx = self.devices.firstIndex(where: { $0.uuid == p.identifier }) else {
return
}
if self.devices[idx].peripheral == nil {
self.devices[idx].peripheral = p
}
if p.state == .disconnected {
if self.manager.isScanning {
self.manager.connect(p, options: nil)
}
} else if p.state == .disconnecting {
self.devicesToRemove.append(p.identifier)
} else if p.state == .connected && !self.devices[idx].isPeripheralInitialized {
p.delegate = self
p.discoverServices([DevicesReader.batteryServiceUUID])
self.devices[idx].isPeripheralInitialized = true
}
}
for (i, d) in self.devices.enumerated() {
if let uuid = d.uuid, let val = self.bleLevels[uuid] {
self.devices[i].batteryLevel = [val]
}
}
if !self.devicesToRemove.isEmpty {
self.devices = self.devices.filter { (d: BLEDevice) -> Bool in
if let uuid = d.uuid, self.devicesToRemove.contains(uuid) {
return false
}
return true
}
self.devicesToRemove = []
}
if !SPB.1.isEmpty {
self.devices = self.devices.filter({ !SPB.1.contains($0.address) })
}
pmsetLevels.forEach { p in
let pmsetName = (p.name ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
if !pmsetName.isEmpty,
let idx = self.devices.firstIndex(where: {
$0.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == pmsetName
}) {
if !p.batteryLevel.isEmpty {
self.devices[idx].batteryLevel = p.batteryLevel
}
return
}
if !p.address.isEmpty,
let idx = self.devices.firstIndex(where: {
!$0.address.isEmpty &&
$0.address.caseInsensitiveCompare(p.address) == .orderedSame
}) {
if !p.batteryLevel.isEmpty {
self.devices[idx].batteryLevel = p.batteryLevel
}
return
}
self.devices.append(BLEDevice(
address: p.address,
name: p.name ?? "",
uuid: p.uuid,
RSSI: 100,
batteryLevel: p.batteryLevel,
isConnected: true,
isPaired: false
))
}
self.callback(self.devices.filter({ $0.RSSI != nil }))
}
// MARK: - HIDDevices (connected ble peripherals to the mac: keyboard, mouse etc...)
private func HIDDevices() -> [bleDevice] {
guard let ioDevices = fetchIOService("AppleDeviceManagementHIDEventService") else {
return []
}
var list: [bleDevice] = []
ioDevices.filter{ $0.object(forKey: "BluetoothDevice") as? Bool == true }.forEach { (d: NSDictionary) in
guard let name = d.object(forKey: "Product") as? String, let batteryPercent = d.object(forKey: "BatteryPercent") as? Int else {
return
}
var address: String = ""
if let addr = d.object(forKey: "DeviceAddress") as? String, addr != "" {
address = addr
} else if let addr = d.object(forKey: "SerialNumber") as? String, addr != "" {
address = addr
} else if let bleAddr = d.object(forKey: "BD_ADDR") as? Data, let addr = String(data: bleAddr, encoding: .utf8), addr != "" {
address = addr
}
list.append(bleDevice(name: name, address: address, uuid: nil, batteryLevel: [KeyValue_t(key: "battery", value: "\(batteryPercent)")]))
}
return list
}
// MARK: - Cache
private func cacheDevices() -> [bleDevice] {
guard let cache = UserDefaults(suiteName: "/Library/Preferences/com.apple.Bluetooth"),
let deviceCache = cache.object(forKey: "DeviceCache") as? [String: [String: Any]],
let pairedDevices = cache.object(forKey: "PairedDevices") as? [String],
let coreCache = cache.object(forKey: "CoreBluetoothCache") as? [String: [String: Any]] else {
return []
}
var list: [bleDevice] = []
deviceCache.filter({ pairedDevices.contains($0.key) }).forEach { (address: String, dict: [String: Any]) in
let name = dict.first{ $0.key == "Name" }?.value as? String
var uuid: UUID? = nil
var batteryLevel: [KeyValue_t] = []
for key in ["BatteryPercent", "BatteryPercentCase", "BatteryPercentLeft", "BatteryPercentRight"] {
if let pair = dict.first(where: { $0.key == key }) {
var percentage: Int = 0
switch pair.value {
case let value as Int:
percentage = value
if percentage == 1 {
percentage *= 100
}
case let value as Double:
percentage = Int(value*100)
default: continue
}
batteryLevel.append(KeyValue_t(key: key, value: "\(percentage)"))
}
}
coreCache.forEach { (key: String, dict: [String: Any]) in
guard let field = dict.first(where: { $0.key == "DeviceAddress" }),
let value = field.value as? String,
value == address else {
return
}
uuid = UUID(uuidString: key)
}
list.append(bleDevice(name: name, address: address, uuid: uuid, batteryLevel: batteryLevel))
}
return list
}
// MARK: - system_profiler
private func profilerDevices() -> ([bleDevice], [String]) {
guard let res = process(path: "/usr/sbin/system_profiler", arguments: ["SPBluetoothDataType", "-json"]) else {
return ([], [])
}
var list: [bleDevice] = []
var notConnected: [String] = []
do {
if let json = try JSONSerialization.jsonObject(with: Data(res.utf8), options: []) as? [String: Any] {
guard let arr = json["SPBluetoothDataType"] as? [[String: Any]], let data = arr.first else {
return (list, notConnected)
}
if let rawList = data["device_connected"] as? [[String: [String: Any]]], let devices = rawList.first {
for obj in devices {
var batteryLevel: [KeyValue_t] = []
for key in ["device_batteryLevelCase", "device_batteryLevelLeft", "device_batteryLevelRight", "Left Battery Level", "Right Battery Level", "device_batteryLevelMain"] {
if let pair = obj.value.first(where: { $0.key == key }) {
batteryLevel.append(KeyValue_t(key: key, value: (pair.value as? String)?.replacingOccurrences(of: "%", with: "") ?? "-1"))
}
}
let address = obj.value["device_address"] as? String ?? ""
list.append(bleDevice(
name: obj.key,
address: address.replacingOccurrences(of: ":", with: "-").lowercased(),
batteryLevel: batteryLevel
))
}
}
if let rawList = data["device_not_connected"] as? [[String: [String: String]]] {
for device in rawList {
for d in device.values {
if let addr = d["device_address"] {
notConnected.append(addr.replacingOccurrences(of: ":", with: "-").lowercased())
}
}
}
}
}
} catch let err as NSError {
error("error to parse system_profiler SPBluetoothDataType: \(err.localizedDescription)")
return (list, notConnected)
}
return (list, notConnected)
}
// MARK: - CBCentralManager
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOff {
central.stopScan()
} else if central.state == .poweredOn {
central.scanForPeripherals(withServices: nil, options: nil)
}
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
self.devicesToRemove.append(peripheral.identifier)
}
// MARK: - CBPeripheral
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
guard error == nil else {
error_msg("didDiscoverServices: \(error!)")
return
}
guard let service = peripheral.services?.first(where: { $0.uuid == DevicesReader.batteryServiceUUID }) else {
error_msg("battery service not found, skipping")
return
}
peripheral.discoverCharacteristics([DevicesReader.batteryCharacteristicsUUID], for: service)
}
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
guard error == nil else {
error_msg("didDiscoverCharacteristicsFor: \(error!)")
return
}
guard let batteryCharacteristics = service.characteristics?.first(where: { $0.uuid == DevicesReader.batteryCharacteristicsUUID }) else {
error_msg("characteristics not found")
return
}
self.characteristicsDict[peripheral.identifier] = batteryCharacteristics
peripheral.readValue(for: batteryCharacteristics)
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
guard error == nil else {
error_msg("didUpdateValueFor: \(error!)")
return
}
if let batteryLevel = characteristic.value?[0] {
self.bleLevels[peripheral.identifier] = KeyValue_t(key: "battery", value: "\(batteryLevel)")
}
}
// MARK: - PMSET data
private func pmsetAccessoryLevels() -> [bleDevice] {
guard let res = process(path: "/usr/bin/pmset", arguments: ["-g", "accps"]) else { return [] }
struct Entry {
let originalName: String
let normalizedName: String
let percent: Int
let id: String
let isCase: Bool
let state: String? // "charging" | "discharging"
}
var grouped: [String: [Entry]] = [:]
var displayNameForGroup: [String: String] = [:]
for raw in res.components(separatedBy: .newlines) {
let line = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard line.hasPrefix("-"), let tabIdx = line.firstIndex(of: "\t") else { continue }
var namePart = String(line[line.index(after: line.startIndex)..= 1 {
kv.append(KeyValue_t(key: "first", value: "\(buds[0].percent)", additional: buds[0].state))
}
if buds.count >= 2 {
kv.append(KeyValue_t(key: "second", value: "\(buds[1].percent)", additional: buds[1].state))
}
if kv.isEmpty, let first = entries.first {
kv = [KeyValue_t(key: "battery", value: "\(first.percent)", additional: first.state)]
}
}
let mergedAddress = entries.map { $0.id }.sorted().joined(separator: "x")
out.append(bleDevice(
name: displayName,
address: mergedAddress,
uuid: nil,
batteryLevel: kv
))
}
return out
}
}
================================================
FILE: Modules/Bluetooth/settings.swift
================================================
//
// settings.swift
// Bluetooth
//
// Created by Serhiy Mytrovtsiy on 07/07/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class Settings: NSStackView, Settings_v {
public var callback: (() -> Void) = {}
private var list: [String: Bool] = [:]
private let emptyView: EmptyView = EmptyView(msg: localizedString("No Bluetooth devices are available"))
private var section: PreferencesSection = PreferencesSection()
public init() {
super.init(frame: NSRect(x: 0, y: 0, width: 0, height: 0))
self.orientation = .vertical
self.distribution = .gravityAreas
self.spacing = Constants.Settings.margin
self.addArrangedSubview(self.emptyView)
self.addArrangedSubview(self.section)
self.section.isHidden = true
self.addArrangedSubview(NSView())
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func load(widgets: [widget_t]) {}
internal func setList(_ list: [BLEDevice]) {
if self.list.count != list.count && !self.list.isEmpty {
self.section.removeFromSuperview()
self.section = PreferencesSection()
self.addArrangedSubview(self.section)
self.list = [:]
}
if list.isEmpty && self.emptyView.isHidden {
self.emptyView.isHidden = false
self.section.isHidden = true
return
} else if !list.isEmpty && !self.emptyView.isHidden {
self.emptyView.isHidden = true
self.section.isHidden = false
}
list.forEach { (d: BLEDevice) in
if self.list[d.id] == nil {
let btn = switchView(
action: #selector(self.handleSelection),
state: d.state
)
btn.identifier = NSUserInterfaceItemIdentifier(rawValue: "\(d.uuid?.uuidString ?? d.address)")
section.add(PreferencesRow(d.name, component: btn))
self.list[d.id] = true
}
}
}
@objc private func handleSelection(_ sender: NSControl) {
guard let id = sender.identifier else { return }
let value = controlState(sender)
Store.shared.set(key: "ble_\(id.rawValue)", value: value)
self.callback()
}
}
================================================
FILE: Modules/CPU/Info.plist
================================================
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
1.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSHumanReadableCopyright
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
TeamId
RP2S87B72W
================================================
FILE: Modules/CPU/bridge.h
================================================
//
// bridge.h
// Stats
//
// Created by Serhiy Mytrovtsiy on 17/12/2024
// Using Swift 6.0
// Running on macOS 15.1
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//
#ifndef bridge_h
#define bridge_h
#include
typedef struct IOReportSubscriptionRef* IOReportSubscriptionRef;
CFDictionaryRef IOReportCopyChannelsInGroup(CFStringRef a, CFStringRef b, uint64_t c, uint64_t d, uint64_t e);
void IOReportMergeChannels(CFDictionaryRef a, CFDictionaryRef b, CFTypeRef null);
IOReportSubscriptionRef IOReportCreateSubscription(void* a, CFMutableDictionaryRef b, CFMutableDictionaryRef* c, uint64_t d, CFTypeRef e);
CFDictionaryRef IOReportCreateSamples(IOReportSubscriptionRef a, CFMutableDictionaryRef b, CFTypeRef c);
CFDictionaryRef IOReportCreateSamplesDelta(CFDictionaryRef a, CFDictionaryRef b, CFTypeRef c);
CFStringRef IOReportChannelGetGroup(CFDictionaryRef a);
CFStringRef IOReportChannelGetSubGroup(CFDictionaryRef a);
CFStringRef IOReportChannelGetChannelName(CFDictionaryRef a);
CFStringRef IOReportChannelGetUnitLabel(CFDictionaryRef a);
int32_t IOReportStateGetCount(CFDictionaryRef a);
CFStringRef IOReportStateGetNameForIndex(CFDictionaryRef a, int32_t b);
int64_t IOReportStateGetResidency(CFDictionaryRef a, int32_t b);
#endif /* bridge_h */
================================================
FILE: Modules/CPU/config.plist
================================================
Name
CPU
State
Symbol
cpu.fill
AlternativeSymbol
cpu
Widgets
label
Default
Order
0
mini
Default
Preview
Value
0.12
Unsupported colors
pressure
Order
1
line_chart
Default
Color
systemAccent
Unsupported colors
pressure
Order
2
bar_chart
Default
Preview
Value
0.36,0.28,0.32,0.26
Color
Unsupported colors
pressure
Order
3
pie_chart
Default
Order
4
tachometer
Default
Order
5
Settings
popup
notifications
================================================
FILE: Modules/CPU/main.swift
================================================
//
// main.swift
// CPU
//
// Created by Serhiy Mytrovtsiy on 09/04/2020.
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
import WidgetKit
public struct CPU_Load: Codable, RemoteType {
public var totalUsage: Double = 0
var usagePerCore: [Double] = []
var usageECores: Double? = nil
var usagePCores: Double? = nil
var systemLoad: Double = 0
var userLoad: Double = 0
var idleLoad: Double = 0
public func remote() -> Data? {
var string = "1,1,\(self.totalUsage),\(self.usagePerCore.count),"
for c in self.usagePerCore {
string += "\(c),"
}
string += "$"
return string.data(using: .utf8)
}
}
public struct CPU_Frequency: Codable, RemoteType {
var value: Double = 0
var eCore: Double = 0
var pCore: Double = 0
public func remote() -> Data? {
let string = "1,1,\(self.value)$"
return string.data(using: .utf8)
}
}
public struct CPU_Limit: Codable {
var scheduler: Int = 0
var cpus: Int = 0
var speed: Int = 0
}
public struct CPU_AverageLoad: Codable, RemoteType {
var load1: Double = 0
var load5: Double = 0
var load15: Double = 0
public func remote() -> Data? {
let string = "1,1,\(self.load1),\(self.load5),\(self.load15)$"
return string.data(using: .utf8)
}
}
public class CPU: Module {
private let popupView: Popup
private let settingsView: Settings
private let portalView: Portal
private let notificationsView: Notifications
private var loadReader: LoadReader? = nil
private var processReader: ProcessReader? = nil
private var temperatureReader: TemperatureReader? = nil
private var frequencyReader: FrequencyReader? = nil
private var limitReader: LimitReader? = nil
private var averageLoadReader: AverageLoadReader? = nil
private var usagePerCoreState: Bool {
Store.shared.bool(key: "\(self.config.name)_usagePerCore", defaultValue: false)
}
private var splitValueState: Bool {
Store.shared.bool(key: "\(self.config.name)_splitValue", defaultValue: false)
}
private var groupByClustersState: Bool {
Store.shared.bool(key: "\(self.config.name)_clustersGroup", defaultValue: false)
}
private var systemColor: NSColor {
let color = SColor.secondRed
let key = Store.shared.string(key: "\(self.config.name)_systemColor", defaultValue: color.key)
if let c = SColor.fromString(key).additional as? NSColor {
return c
}
return color.additional as! NSColor
}
private var userColor: NSColor {
let color = SColor.secondBlue
let key = Store.shared.string(key: "\(self.config.name)_userColor", defaultValue: color.key)
if let c = SColor.fromString(key).additional as? NSColor {
return c
}
return color.additional as! NSColor
}
private var eCoreColor: NSColor {
let color = SColor.teal
let key = Store.shared.string(key: "\(self.config.name)_eCoresColor", defaultValue: color.key)
if let c = SColor.fromString(key).additional as? NSColor {
return c
}
return color.additional as! NSColor
}
private var pCoreColor: NSColor {
let color = SColor.indigo
let key = Store.shared.string(key: "\(self.config.name)_pCoresColor", defaultValue: color.key)
if let c = SColor.fromString(key).additional as? NSColor {
return c
}
return color.additional as! NSColor
}
private var systemWidgetsUpdatesState: Bool {
self.userDefaults?.bool(forKey: "systemWidgetsUpdates_state") ?? false
}
public init() {
self.settingsView = Settings(.CPU)
self.popupView = Popup(.CPU)
self.portalView = Portal(.CPU)
self.notificationsView = Notifications(.CPU)
super.init(
moduleType: .CPU,
popup: self.popupView,
settings: self.settingsView,
portal: self.portalView,
notifications: self.notificationsView
)
guard self.available else { return }
self.loadReader = LoadReader(.CPU) { [weak self] value in
self?.loadCallback(value)
}
self.processReader = ProcessReader(.CPU) { [weak self] value in
self?.popupView.processCallback(value)
}
self.averageLoadReader = AverageLoadReader(.CPU, popup: true) { [weak self] value in
self?.popupView.averageCallback(value)
}
self.temperatureReader = TemperatureReader(.CPU, popup: true) { [weak self] value in
self?.popupView.temperatureCallback(value)
}
#if arch(x86_64)
self.limitReader = LimitReader(.CPU, popup: true) { [weak self] value in
self?.popupView.limitCallback(value)
}
#else
self.frequencyReader = FrequencyReader(.CPU, popup: false) { [weak self] value in
self?.popupView.frequencyCallback(value)
}
#endif
self.settingsView.callback = { [weak self] in
self?.loadReader?.read()
}
self.settingsView.callbackWhenUpdateNumberOfProcesses = {
self.popupView.numberOfProcessesUpdated()
DispatchQueue.global(qos: .background).async {
self.processReader?.read()
}
}
self.settingsView.setInterval = { [weak self] value in
self?.loadReader?.setInterval(value)
}
self.settingsView.setTopInterval = { [weak self] value in
self?.processReader?.setInterval(value)
}
self.setReaders([
self.loadReader,
self.processReader,
self.temperatureReader,
self.frequencyReader,
self.limitReader,
self.averageLoadReader
])
}
private func loadCallback(_ raw: CPU_Load?) {
guard let value = raw, self.enabled else { return }
self.popupView.loadCallback(value)
self.portalView.callback(value)
self.notificationsView.loadCallback(value)
self.menuBar.widgets.filter{ $0.isActive }.forEach { [self] (w: SWidget) in
switch w.item {
case let widget as Mini: widget.setValue(value.totalUsage)
case let widget as LineChart: widget.setValue(value.totalUsage)
case let widget as BarChart:
var val: [[ColorValue]] = [[ColorValue(value.totalUsage)]]
let cores = SystemKit.shared.device.info.cpu?.cores ?? []
if self.usagePerCoreState {
if widget.colorState == .cluster {
val = []
for (i, v) in value.usagePerCore.enumerated() {
let core = cores.first(where: {$0.id == i })
val.append([ColorValue(v, color: core?.type == .efficiency ? self.eCoreColor : self.pCoreColor)])
}
} else {
val = value.usagePerCore.map({ [ColorValue($0)] })
}
} else if self.splitValueState {
val = [[
ColorValue(value.systemLoad, color: self.systemColor),
ColorValue(value.userLoad, color: self.userColor)
]]
} else if self.groupByClustersState, let e = value.usageECores, let p = value.usagePCores {
if widget.colorState == .cluster {
val = [
[ColorValue(e, color: self.eCoreColor)],
[ColorValue(p, color: self.pCoreColor)]
]
} else {
val = [[ColorValue(e)], [ColorValue(p)]]
}
}
widget.setValue(val)
case let widget as PieChart:
widget.setValue([
circle_segment(value: value.systemLoad, color: self.systemColor),
circle_segment(value: value.userLoad, color: self.userColor)
])
case let widget as Tachometer:
widget.setValue([
circle_segment(value: value.systemLoad, color: self.systemColor),
circle_segment(value: value.userLoad, color: self.userColor)
])
default: break
}
}
if self.systemWidgetsUpdatesState {
if isWidgetActive(self.userDefaults, [CPU_entry.kind, "UnitedWidget"]), let blobData = try? JSONEncoder().encode(value) {
self.userDefaults?.set(blobData, forKey: "CPU@LoadReader")
}
WidgetCenter.shared.reloadTimelines(ofKind: CPU_entry.kind)
WidgetCenter.shared.reloadTimelines(ofKind: "UnitedWidget")
}
}
}
================================================
FILE: Modules/CPU/notifications.swift
================================================
//
// notifications.swift
// CPU
//
// Created by Serhiy Mytrovtsiy on 04/12/2023
// Using Swift 5.0
// Running on macOS 14.1
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
class Notifications: NotificationsWrapper {
private let totalLoadID: String = "totalUsage"
private let systemLoadID: String = "systemUsage"
private let userLoadID: String = "userUsage"
private let eCoresLoadID: String = "eCoresUsage"
private let pCoresLoadID: String = "pCoresUsage"
private var totalLoadState: Bool = false
private var systemLoadState: Bool = false
private var userLoadState: Bool = false
private var eCoresLoadState: Bool = false
private var pCoresLoadState: Bool = false
private var totalLoad: Int = 75
private var systemLoad: Int = 75
private var userLoad: Int = 75
private var eCoresLoad: Int = 75
private var pCoresLoad: Int = 75
public init(_ module: ModuleType) {
super.init(module, [self.totalLoadID, self.systemLoadID, self.userLoadID, self.eCoresLoadID, self.pCoresLoadID])
if Store.shared.exist(key: "\(self.module)_notifications_totalLoad") {
let value = Store.shared.string(key: "\(self.module)_notifications_totalLoad", defaultValue: "")
if let v = Double(value) {
Store.shared.set(key: "\(self.module)_notifications_totalLoad_state", value: true)
Store.shared.set(key: "\(self.module)_notifications_totalLoad_value", value: Int(v*100))
Store.shared.remove("\(self.module)_notifications_totalLoad")
}
}
if Store.shared.exist(key: "\(self.module)_notifications_systemLoad") {
let value = Store.shared.string(key: "\(self.module)_notifications_systemLoad", defaultValue: "")
if let v = Double(value) {
Store.shared.set(key: "\(self.module)_notifications_systemLoad_state", value: true)
Store.shared.set(key: "\(self.module)_notifications_systemLoad_value", value: Int(v*100))
Store.shared.remove("\(self.module)_notifications_systemLoad")
}
}
if Store.shared.exist(key: "\(self.module)_notifications_userLoad") {
let value = Store.shared.string(key: "\(self.module)_notifications_userLoad", defaultValue: "")
if let v = Double(value) {
Store.shared.set(key: "\(self.module)_notifications_userLoad_state", value: true)
Store.shared.set(key: "\(self.module)_notifications_userLoad_value", value: Int(v*100))
Store.shared.remove("\(self.module)_notifications_userLoad")
}
}
if Store.shared.exist(key: "\(self.module)_notifications_eCoresLoad") {
let value = Store.shared.string(key: "\(self.module)_notifications_eCoresLoad", defaultValue: "")
if let v = Double(value) {
Store.shared.set(key: "\(self.module)_notifications_eCoresLoad_state", value: true)
Store.shared.set(key: "\(self.module)_notifications_eCoresLoad_value", value: Int(v*100))
Store.shared.remove("\(self.module)_notifications_eCoresLoad")
}
}
if Store.shared.exist(key: "\(self.module)_notifications_pCoresLoad") {
let value = Store.shared.string(key: "\(self.module)_notifications_pCoresLoad", defaultValue: "")
if let v = Double(value) {
Store.shared.set(key: "\(self.module)_notifications_pCoresLoad_state", value: true)
Store.shared.set(key: "\(self.module)_notifications_pCoresLoad_value", value: Int(v*100))
Store.shared.remove("\(self.module)_notifications_pCoresLoad")
}
}
self.totalLoadState = Store.shared.bool(key: "\(self.module)_notifications_totalLoad_state", defaultValue: self.totalLoadState)
self.totalLoad = Store.shared.int(key: "\(self.module)_notifications_totalLoad_value", defaultValue: self.totalLoad)
self.systemLoadState = Store.shared.bool(key: "\(self.module)_notifications_systemLoad_state", defaultValue: self.systemLoadState)
self.systemLoad = Store.shared.int(key: "\(self.module)_notifications_systemLoad_value", defaultValue: self.systemLoad)
self.userLoadState = Store.shared.bool(key: "\(self.module)_notifications_userLoad_state", defaultValue: self.userLoadState)
self.userLoad = Store.shared.int(key: "\(self.module)_notifications_userLoad_value", defaultValue: self.userLoad)
self.eCoresLoadState = Store.shared.bool(key: "\(self.module)_notifications_eCoresLoad_state", defaultValue: self.eCoresLoadState)
self.eCoresLoad = Store.shared.int(key: "\(self.module)_notifications_eCoresLoad_value", defaultValue: self.eCoresLoad)
self.pCoresLoadState = Store.shared.bool(key: "\(self.module)_notifications_pCoresLoad_state", defaultValue: self.pCoresLoadState)
self.pCoresLoad = Store.shared.int(key: "\(self.module)_notifications_pCoresLoad_value", defaultValue: self.pCoresLoad)
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Total load"), component: PreferencesSwitch(
action: self.toggleTotalLoad, state: self.totalLoadState,
with: StepperInput(self.totalLoad, callback: self.changeTotalLoad)
)),
PreferencesRow(localizedString("System load"), component: PreferencesSwitch(
action: self.toggleSystemLoad, state: self.systemLoadState,
with: StepperInput(self.systemLoad, callback: self.changeSystemLoad)
)),
PreferencesRow(localizedString("User load"), component: PreferencesSwitch(
action: self.toggleUserLoad, state: self.userLoadState,
with: StepperInput(self.userLoad, callback: self.changeUserLoad)
))
]))
#if arch(arm64)
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Efficiency cores load"), component: PreferencesSwitch(
action: self.toggleECoresLoad, state: self.eCoresLoadState,
with: StepperInput(self.eCoresLoad, callback: self.changeECoresLoad)
)),
PreferencesRow(localizedString("Performance cores load"), component: PreferencesSwitch(
action: self.togglePCoresLoad, state: self.pCoresLoadState,
with: StepperInput(self.pCoresLoad, callback: self.changePCoresLoad)
))
]))
#endif
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func loadCallback(_ value: CPU_Load) {
let title = localizedString("CPU usage threshold")
if self.totalLoadState {
let subtitle = "\(localizedString("Total load")): \(Int((value.totalUsage)*100))%"
self.checkDouble(id: self.totalLoadID, value: value.totalUsage, threshold: Double(self.totalLoad)/100, title: title, subtitle: subtitle)
}
if self.systemLoadState {
let subtitle = "\(localizedString("System load")): \(Int((value.systemLoad)*100))%"
self.checkDouble(id: self.systemLoadID, value: value.systemLoad, threshold: Double(self.systemLoad)/100, title: title, subtitle: subtitle)
}
if self.userLoadState {
let subtitle = "\(localizedString("User load")): \(Int((value.userLoad)*100))%"
self.checkDouble(id: self.userLoadID, value: value.userLoad, threshold: Double(self.userLoad)/100, title: title, subtitle: subtitle)
}
if self.eCoresLoadState, let usage = value.usageECores {
let subtitle = "\(localizedString("Efficiency cores load")): \(Int((usage)*100))%"
self.checkDouble(id: self.eCoresLoadID, value: usage, threshold: Double(self.eCoresLoad)/100, title: title, subtitle: subtitle)
}
if self.pCoresLoadState, let usage = value.usagePCores {
let subtitle = "\(localizedString("Performance cores load")): \(Int((usage)*100))%"
self.checkDouble(id: self.pCoresLoadID, value: usage, threshold: Double(self.pCoresLoad)/100, title: title, subtitle: subtitle)
}
}
// MARK: - change helpers
@objc private func toggleTotalLoad(_ sender: NSControl) {
self.totalLoadState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_totalLoad_state", value: self.totalLoadState)
}
@objc private func changeTotalLoad(_ newValue: Int) {
self.totalLoad = newValue
Store.shared.set(key: "\(self.module)_notifications_totalLoad_value", value: self.totalLoad)
}
@objc private func toggleSystemLoad(_ sender: NSControl) {
self.systemLoadState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_systemLoad_state", value: self.systemLoadState)
}
@objc private func changeSystemLoad(_ newValue: Int) {
self.systemLoad = newValue
Store.shared.set(key: "\(self.module)_notifications_systemLoad_value", value: self.systemLoad)
}
@objc private func toggleUserLoad(_ sender: NSControl) {
self.userLoadState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_userLoad_state", value: self.userLoadState)
}
@objc private func changeUserLoad(_ newValue: Int) {
self.userLoad = newValue
Store.shared.set(key: "\(self.module)_notifications_userLoad_value", value: self.userLoad)
}
@objc private func toggleECoresLoad(_ sender: NSControl) {
self.eCoresLoadState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_eCoresLoad_state", value: self.eCoresLoadState)
}
@objc private func changeECoresLoad(_ newValue: Int) {
self.eCoresLoad = newValue
Store.shared.set(key: "\(self.module)_notifications_eCoresLoad_value", value: self.eCoresLoad)
}
@objc private func togglePCoresLoad(_ sender: NSControl) {
self.pCoresLoadState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_pCoresLoad_state", value: self.pCoresLoadState)
}
@objc private func changePCoresLoad(_ newValue: Int) {
self.pCoresLoad = newValue
Store.shared.set(key: "\(self.module)_notifications_pCoresLoad_value", value: self.pCoresLoad)
}
}
================================================
FILE: Modules/CPU/popup.swift
================================================
//
// popup.swift
// CPU
//
// Created by Serhiy Mytrovtsiy on 15/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class Popup: PopupWrapper {
private let dashboardHeight: CGFloat = 90
private let chartHeight: CGFloat = 120 + Constants.Popup.separatorHeight
private var detailsHeight: CGFloat {
get {
var count: CGFloat = isARM ? 4 : 6
if SystemKit.shared.device.info.cpu?.eCores != nil {
count += 1
}
if SystemKit.shared.device.info.cpu?.pCores != nil {
count += 1
}
return (22*count) + Constants.Popup.separatorHeight
}
}
private let averageHeight: CGFloat = (22*3) + Constants.Popup.separatorHeight
private var frequencyHeight: CGFloat {
get {
var count: CGFloat = 1
if SystemKit.shared.device.info.cpu?.eCores != nil {
count += 1
}
if SystemKit.shared.device.info.cpu?.pCores != nil {
count += 1
}
return (22*count) + Constants.Popup.separatorHeight
}
}
private let processHeight: CGFloat = 22
private var systemField: NSTextField? = nil
private var userField: NSTextField? = nil
private var idleField: NSTextField? = nil
private var shedulerLimitField: NSTextField? = nil
private var speedLimitField: NSTextField? = nil
private var eCoresField: NSTextField? = nil
private var pCoresField: NSTextField? = nil
private var uptimeField: NSTextField? = nil
private var average1Field: NSTextField? = nil
private var average5Field: NSTextField? = nil
private var average15Field: NSTextField? = nil
private var coresFreqField: NSTextField? = nil
private var eCoresFreqField: NSTextField? = nil
private var pCoresFreqField: NSTextField? = nil
private var eCoresFreqColorView: NSView? = nil
private var pCoresFreqColorView: NSView? = nil
private var systemColorView: NSView? = nil
private var userColorView: NSView? = nil
private var idleColorView: NSView? = nil
private var eCoresColorView: NSView? = nil
private var pCoresColorView: NSView? = nil
private var chartPrefSection: PreferencesSection? = nil
private var sliderView: NSView? = nil
private var lineChart: LineChartView? = nil
private var columnChart: ColumnChartView? = nil
private var circle: PieChartView? = nil
private var temperatureCircle: HalfCircleGraphView? = nil
private var frequencyCircle: HalfCircleGraphView? = nil
private var initialized: Bool = false
private var initializedTemperature: Bool = false
private var initializedFrequency: Bool = false
private var initializedProcesses: Bool = false
private var initializedLimits: Bool = false
private var initializedAverage: Bool = false
private var processes: ProcessesView? = nil
private var maxFreq: Double = 0
private var lineChartHistory: Int = 180
private var lineChartScale: Scale = .none
private var lineChartFixedScale: Double = 1
private var systemColorState: SColor = .secondRed
private var systemColor: NSColor { self.systemColorState.additional as? NSColor ?? NSColor.systemRed }
private var userColorState: SColor = .secondBlue
private var userColor: NSColor { self.userColorState.additional as? NSColor ?? NSColor.systemBlue }
private var idleColorState: SColor = .lightGray
private var idleColor: NSColor { self.idleColorState.additional as? NSColor ?? NSColor.lightGray }
private var chartColorState: SColor = .systemAccent
private var chartColor: NSColor { self.chartColorState.additional as? NSColor ?? NSColor.systemBlue }
private var eCoresColorState: SColor = .teal
private var eCoresColor: NSColor { self.eCoresColorState.additional as? NSColor ?? NSColor.systemTeal }
private var pCoresColorState: SColor = .indigo
private var pCoresColor: NSColor { self.pCoresColorState.additional as? NSColor ?? NSColor.systemBlue }
private var processesView: NSView? = nil
private var frequenciesView: NSView? = nil
private var numberOfProcesses: Int {
Store.shared.int(key: "\(self.title)_processes", defaultValue: 8)
}
private var processesHeight: CGFloat {
(self.processHeight*CGFloat(self.numberOfProcesses)) + (self.numberOfProcesses == 0 ? 0 : Constants.Popup.separatorHeight + 22)
}
private var uptimeValue: String {
let form = DateComponentsFormatter()
form.maximumUnitCount = 2
form.unitsStyle = .full
form.allowedUnits = [.day, .hour, .minute]
var value = localizedString("Unknown")
if let bootDate = SystemKit.shared.device.bootDate {
if let duration = form.string(from: bootDate, to: Date()) {
value = duration
}
}
return value
}
public init(_ module: ModuleType) {
super.init(module, frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
self.spacing = 0
self.orientation = .vertical
self.systemColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_systemColor", defaultValue: self.systemColorState.key))
self.userColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_userColor", defaultValue: self.userColorState.key))
self.idleColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_idleColor", defaultValue: self.idleColorState.key))
self.chartColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_chartColor", defaultValue: self.chartColorState.key))
self.eCoresColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_eCoresColor", defaultValue: self.eCoresColorState.key))
self.pCoresColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_pCoresColor", defaultValue: self.pCoresColorState.key))
self.lineChartHistory = Store.shared.int(key: "\(self.title)_lineChartHistory", defaultValue: self.lineChartHistory)
self.lineChartScale = Scale.fromString(Store.shared.string(key: "\(self.title)_lineChartScale", defaultValue: self.lineChartScale.key))
self.lineChartFixedScale = Double(Store.shared.int(key: "\(self.title)_lineChartFixedScale", defaultValue: 100)) / 100
self.addArrangedSubview(self.initDashboard())
self.addArrangedSubview(self.initChart())
self.addArrangedSubview(self.initDetails())
self.addArrangedSubview(self.initAverage())
self.addArrangedSubview(self.initProcesses())
self.recalculateHeight()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func updateLayer() {
self.lineChart?.display()
}
public override func appear() {
self.uptimeField?.stringValue = self.uptimeValue
}
public override func disappear() {
self.processes?.setLock(false)
}
private func recalculateHeight() {
var h: CGFloat = 0
self.arrangedSubviews.forEach { v in
if let v = v as? NSStackView {
h += v.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +)
} else {
h += v.bounds.height
}
}
if self.frame.size.height != h {
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.sizeCallback?(self.frame.size)
}
}
private func initDashboard() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.dashboardHeight))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let usageSize = self.dashboardHeight-20
let usageX = (view.frame.width - usageSize)/2
let usage = NSView(frame: NSRect(x: usageX, y: (view.frame.height - usageSize)/2, width: usageSize, height: usageSize))
let temperature = NSView(frame: NSRect(x: (usageX - 50)/2, y: (view.frame.height - 50)/2 - 3, width: 50, height: 50))
let frequency = NSView(frame: NSRect(x: (usageX+usageSize) + (usageX - 50)/2, y: 0, width: 50, height: self.dashboardHeight))
self.circle = PieChartView(frame: NSRect(x: 0, y: 0, width: usage.frame.width, height: usage.frame.height), segments: [], drawValue: true)
self.circle!.toolTip = localizedString("CPU usage")
usage.addSubview(self.circle!)
self.temperatureCircle = HalfCircleGraphView(frame: NSRect(x: 0, y: 0, width: temperature.frame.width, height: temperature.frame.height))
self.temperatureCircle!.toolTip = localizedString("CPU temperature")
(self.temperatureCircle! as NSView).isHidden = true
temperature.addSubview(self.temperatureCircle!)
self.frequencyCircle = HalfCircleGraphView(frame: NSRect(x: 0, y: 0, width: frequency.frame.width, height: frequency.frame.height))
self.frequencyCircle!.toolTip = localizedString("CPU frequency")
(self.frequencyCircle! as NSView).isHidden = true
frequency.addSubview(self.frequencyCircle!)
view.addSubview(temperature)
view.addSubview(usage)
view.addSubview(frequency)
return view
}
private func initChart() -> NSView {
let view: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.chartHeight))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
view.orientation = .vertical
view.spacing = 0
let separator = separatorView(localizedString("Usage history"), origin: NSPoint(x: 0, y: 0), width: self.frame.width)
let lineChartContainer: NSView = {
let box: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 70))
box.heightAnchor.constraint(equalToConstant: box.frame.height).isActive = true
box.wantsLayer = true
box.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.1).cgColor
box.layer?.cornerRadius = 3
let chartFrame = NSRect(x: 1, y: 0, width: box.frame.width, height: box.frame.height)
self.lineChart = LineChartView(frame: chartFrame, num: self.lineChartHistory, scale: self.lineChartScale, fixedScale: self.lineChartFixedScale)
self.lineChart?.color = self.chartColor
box.addSubview(self.lineChart!)
return box
}()
view.addArrangedSubview(separator)
view.addArrangedSubview(lineChartContainer)
if let cores = SystemKit.shared.device.info.cpu?.logicalCores {
let barChartContainer: NSView = {
let box: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 50))
box.heightAnchor.constraint(equalToConstant: box.frame.height).isActive = true
box.wantsLayer = true
box.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.1).cgColor
box.layer?.cornerRadius = 3
let chart = ColumnChartView(frame: NSRect(
x: Constants.Popup.spacing,
y: Constants.Popup.spacing,
width: view.frame.width - (Constants.Popup.spacing*2),
height: box.frame.height - (Constants.Popup.spacing*2)
), num: Int(cores))
self.columnChart = chart
box.addSubview(chart)
return box
}()
view.addArrangedSubview(barChartContainer)
}
return view
}
private func initDetails() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.detailsHeight))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let separator = separatorView(localizedString("Details"), origin: NSPoint(
x: 0,
y: self.detailsHeight-Constants.Popup.separatorHeight
), width: self.frame.width)
let container: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: separator.frame.origin.y))
container.orientation = .vertical
container.spacing = 0
(self.systemColorView, _, self.systemField) = popupWithColorRow(container, color: self.systemColor, title: "\(localizedString("System")):", value: "")
(self.userColorView, _, self.userField) = popupWithColorRow(container, color: self.userColor, title: "\(localizedString("User")):", value: "")
(self.idleColorView, _, self.idleField) = popupWithColorRow(container, color: self.idleColor.withAlphaComponent(0.5), title: "\(localizedString("Idle")):", value: "")
if !isARM {
self.shedulerLimitField = popupRow(container, title: "\(localizedString("Scheduler limit")):", value: "").1
self.speedLimitField = popupRow(container, title: "\(localizedString("Speed limit")):", value: "").1
}
if SystemKit.shared.device.info.cpu?.eCores != nil {
(self.eCoresColorView, _, self.eCoresField) = popupWithColorRow(container, color: self.eCoresColor, title: "\(localizedString("Efficiency cores")):", value: "")
}
if SystemKit.shared.device.info.cpu?.pCores != nil {
(self.pCoresColorView, _, self.pCoresField) = popupWithColorRow(container, color: self.pCoresColor, title: "\(localizedString("Performance cores")):", value: "")
}
self.uptimeField = popupRow(container, title: "\(localizedString("Uptime")):", value: self.uptimeValue).1
self.uptimeField?.font = NSFont.systemFont(ofSize: 11, weight: .regular)
view.addSubview(separator)
view.addSubview(container)
return view
}
private func initAverage() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.averageHeight))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let separator = separatorView(localizedString("Average load"), origin: NSPoint(x: 0, y: self.averageHeight-Constants.Popup.separatorHeight), width: self.frame.width)
let container: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: separator.frame.origin.y))
container.orientation = .vertical
container.spacing = 0
self.average1Field = popupRow(container, title: "\(localizedString("1 minute")):", value: "").1
self.average5Field = popupRow(container, title: "\(localizedString("5 minutes")):", value: "").1
self.average15Field = popupRow(container, title: "\(localizedString("15 minutes")):", value: "").1
view.addSubview(separator)
view.addSubview(container)
return view
}
private func initFrequency() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frequencyHeight))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let separator = separatorView(localizedString("Frequency"), origin: NSPoint(x: 0, y: self.frequencyHeight-Constants.Popup.separatorHeight), width: self.frame.width)
let container: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: separator.frame.origin.y))
container.orientation = .vertical
container.spacing = 0
self.coresFreqField = popupRow(container, title: "\(localizedString("All cores")):", value: "").1
if isARM {
if SystemKit.shared.device.info.cpu?.eCores != nil {
(self.eCoresFreqColorView, _, self.eCoresFreqField) = popupWithColorRow(container, color: self.eCoresColor, title: "\(localizedString("Efficiency cores")):", value: "")
}
if SystemKit.shared.device.info.cpu?.pCores != nil {
(self.pCoresFreqColorView, _, self.pCoresFreqField) = popupWithColorRow(container, color: self.pCoresColor, title: "\(localizedString("Performance cores")):", value: "")
}
}
view.addSubview(separator)
view.addSubview(container)
return view
}
private func initProcesses() -> NSView {
if self.numberOfProcesses == 0 {
let v = NSView()
self.processesView = v
return v
}
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.processesHeight))
let separator = separatorView(localizedString("Top processes"), origin: NSPoint(x: 0, y: self.processesHeight-Constants.Popup.separatorHeight), width: self.frame.width)
let container: ProcessesView = ProcessesView(
frame: NSRect(x: 0, y: 0, width: self.frame.width, height: separator.frame.origin.y),
values: [(localizedString("Usage"), nil)],
n: self.numberOfProcesses
)
self.processes = container
view.addSubview(separator)
view.addSubview(container)
self.processesView = view
return view
}
public func loadCallback(_ value: CPU_Load) {
DispatchQueue.main.async(execute: {
if (self.window?.isVisible ?? false) || !self.initialized {
self.systemField?.stringValue = "\(Int(value.systemLoad.rounded(toPlaces: 2) * 100))%"
self.userField?.stringValue = "\(Int(value.userLoad.rounded(toPlaces: 2) * 100))%"
self.idleField?.stringValue = "\(Int(value.idleLoad.rounded(toPlaces: 2) * 100))%"
self.circle?.toolTip = "\(localizedString("CPU usage")): \(Int(value.totalUsage.rounded(toPlaces: 2) * 100))%"
self.circle?.setValue(value.totalUsage)
self.circle?.setSegments([
circle_segment(value: value.systemLoad, color: self.systemColor),
circle_segment(value: value.userLoad, color: self.userColor)
])
self.circle?.setNonActiveSegmentColor(self.idleColor)
if let field = self.eCoresField, let usage = value.usageECores {
field.stringValue = "\(Int(usage * 100))%"
}
if let field = self.pCoresField, let usage = value.usagePCores {
field.stringValue = "\(Int(usage * 100))%"
}
var usagePerCore: [ColorValue] = []
if let cores = SystemKit.shared.device.info.cpu?.cores, cores.count == value.usagePerCore.count {
for i in 0.. self.maxFreq {
self.maxFreq = value.value
}
self.coresFreqField?.stringValue = "\(Int(value.value)) MHz"
if let circle = self.frequencyCircle {
circle.setValue((100*value.value)/self.maxFreq)
circle.setText("\((value.value/1000).rounded(toPlaces: 2))")
circle.toolTip = "\(localizedString("CPU frequency")): \(Int(value.value)) MHz - \(((100*value.value)/self.maxFreq).rounded(toPlaces: 2))%"
}
self.eCoresFreqField?.stringValue = "\(Int(value.eCore)) MHz"
self.pCoresFreqField?.stringValue = "\(Int(value.pCore)) MHz"
self.initializedFrequency = true
}
})
}
public func processCallback(_ list: [TopProcess]?) {
guard let list else { return }
DispatchQueue.main.async(execute: {
if !(self.window?.isVisible ?? false) && self.initializedProcesses {
return
}
let list = list.map { $0 }
if list.count != self.processes?.count { self.processes?.clear() }
for i in 0.. NSView? {
let view = SettingsContainerView()
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Keyboard shortcut"), component: KeyboardShartcutView(
callback: self.setKeyboardShortcut,
value: self.keyboardShortcut
))
]))
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("System color"), component: selectView(
action: #selector(self.toggleSystemColor),
items: SColor.allColors,
selected: self.systemColorState.key
)),
PreferencesRow(localizedString("User color"), component: selectView(
action: #selector(self.toggleUserColor),
items: SColor.allColors,
selected: self.userColorState.key
)),
PreferencesRow(localizedString("Idle color"), component: selectView(
action: #selector(self.toggleIdleColor),
items: SColor.allColors,
selected: self.idleColorState.key
))
]))
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Efficiency cores color"), component: selectView(
action: #selector(self.toggleECoresColor),
items: SColor.allColors,
selected: self.eCoresColorState.key
)),
PreferencesRow(localizedString("Performance cores color"), component: selectView(
action: #selector(self.togglePCoresColor),
items: SColor.allColors,
selected: self.pCoresColorState.key
))
]))
self.sliderView = sliderView(
action: #selector(self.toggleLineChartFixedScale),
value: Int(self.lineChartFixedScale * 100),
initialValue: "\(Int(self.lineChartFixedScale * 100)) %"
)
self.chartPrefSection = PreferencesSection([
PreferencesRow(localizedString("Chart color"), component: selectView(
action: #selector(self.toggleChartColor),
items: SColor.allColors,
selected: self.chartColorState.key
)),
PreferencesRow(localizedString("Chart history"), component: selectView(
action: #selector(self.toggleLineChartHistory),
items: LineChartHistory,
selected: "\(self.lineChartHistory)"
)),
PreferencesRow(localizedString("Main chart scaling"), component: selectView(
action: #selector(self.toggleLineChartScale),
items: Scale.allCases,
selected: self.lineChartScale.key
)),
PreferencesRow(localizedString("Scale value"), component: self.sliderView!)
])
view.addArrangedSubview(self.chartPrefSection!)
self.chartPrefSection?.setRowVisibility(3, newState: self.lineChartScale == .fixed)
return view
}
@objc private func toggleSystemColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.systemColorState = newValue
Store.shared.set(key: "\(self.title)_systemColor", value: key)
self.systemColorView?.layer?.backgroundColor = (newValue.additional as? NSColor)?.cgColor
}
@objc private func toggleUserColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.userColorState = newValue
Store.shared.set(key: "\(self.title)_userColor", value: key)
self.userColorView?.layer?.backgroundColor = (newValue.additional as? NSColor)?.cgColor
}
@objc private func toggleIdleColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.idleColorState = newValue
Store.shared.set(key: "\(self.title)_idleColor", value: key)
if let color = newValue.additional as? NSColor {
self.idleColorView?.layer?.backgroundColor = color.cgColor
}
self.idleColorView?.layer?.backgroundColor = (newValue.additional as? NSColor)?.cgColor
}
@objc private func toggleChartColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.chartColorState = newValue
Store.shared.set(key: "\(self.title)_chartColor", value: key)
if let color = newValue.additional as? NSColor {
self.lineChart?.color = color
}
}
@objc private func toggleECoresColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.eCoresColorState = newValue
Store.shared.set(key: "\(self.title)_eCoresColor", value: key)
if let color = (newValue.additional as? NSColor) {
self.eCoresColorView?.layer?.backgroundColor = color.cgColor
self.eCoresFreqColorView?.layer?.backgroundColor = color.cgColor
}
}
@objc private func togglePCoresColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.pCoresColorState = newValue
Store.shared.set(key: "\(self.title)_pCoresColor", value: key)
if let color = (newValue.additional as? NSColor) {
self.pCoresColorView?.layer?.backgroundColor = color.cgColor
self.pCoresFreqColorView?.layer?.backgroundColor = color.cgColor
}
}
@objc private func toggleLineChartHistory(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let value = Int(key) else { return }
self.lineChartHistory = value
Store.shared.set(key: "\(self.title)_lineChartHistory", value: value)
self.lineChart?.reinit(self.lineChartHistory)
}
@objc private func toggleLineChartScale(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let value = Scale.allCases.first(where: { $0.key == key }) else { return }
self.chartPrefSection?.setRowVisibility(3, newState: value == .fixed)
self.lineChartScale = value
self.lineChart?.setScale(self.lineChartScale, fixedScale: self.lineChartFixedScale)
Store.shared.set(key: "\(self.title)_lineChartScale", value: key)
self.display()
}
@objc private func toggleLineChartFixedScale(_ sender: NSSlider) {
let value = Int(sender.doubleValue)
if let field = self.sliderView?.subviews.first(where: { $0 is NSTextField }), let view = field as? NSTextField {
view.stringValue = "\(value) %"
}
self.lineChartFixedScale = sender.doubleValue / 100
self.lineChart?.setScale(self.lineChartScale, fixedScale: self.lineChartFixedScale)
Store.shared.set(key: "\(self.title)_lineChartFixedScale", value: value)
}
}
================================================
FILE: Modules/CPU/portal.swift
================================================
//
// portal.swift
// CPU
//
// Created by Serhiy Mytrovtsiy on 17/02/2023
// Using Swift 5.0
// Running on macOS 13.2
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
public class Portal: PortalWrapper {
private var circle: PieChartView? = nil
private var columnChart: ColumnChartView? = nil
private var initialized: Bool = false
private var systemField: NSTextField? = nil
private var userField: NSTextField? = nil
private var idleField: NSTextField? = nil
private var shedulerLimitField: NSTextField? = nil
private var speedLimitField: NSTextField? = nil
private var eCoresField: NSTextField? = nil
private var pCoresField: NSTextField? = nil
private var average1Field: NSTextField? = nil
private var average5Field: NSTextField? = nil
private var average15Field: NSTextField? = nil
private var systemColorView: NSView? = nil
private var userColorView: NSView? = nil
private var idleColorView: NSView? = nil
private var eCoresColorView: NSView? = nil
private var pCoresColorView: NSView? = nil
private var systemColorState: SColor = .secondRed
private var systemColor: NSColor { self.systemColorState.additional as? NSColor ?? NSColor.systemRed }
private var userColorState: SColor = .secondBlue
private var userColor: NSColor { self.userColorState.additional as? NSColor ?? NSColor.systemBlue }
private var idleColorState: SColor = .lightGray
private var idleColor: NSColor { self.idleColorState.additional as? NSColor ?? NSColor.lightGray }
private var eCoresColorState: SColor = .teal
private var eCoresColor: NSColor { self.eCoresColorState.additional as? NSColor ?? NSColor.systemTeal }
private var pCoresColorState: SColor = .indigo
private var pCoresColor: NSColor { self.pCoresColorState.additional as? NSColor ?? NSColor.systemBlue }
public override func load() {
self.loadColors()
let view = NSStackView()
view.orientation = .horizontal
view.distribution = .fillEqually
view.spacing = Constants.Popup.spacing*2
view.edgeInsets = NSEdgeInsets(
top: 0,
left: Constants.Popup.spacing*2,
bottom: 0,
right: Constants.Popup.spacing*2
)
let chartsView = self.charts()
let detailsView = self.details()
view.addArrangedSubview(chartsView)
view.addArrangedSubview(detailsView)
self.addArrangedSubview(view)
chartsView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
detailsView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
}
public func loadColors() {
self.systemColorState = SColor.fromString(Store.shared.string(key: "\(self.name)_systemColor", defaultValue: self.systemColorState.key))
self.userColorState = SColor.fromString(Store.shared.string(key: "\(self.name)_userColor", defaultValue: self.userColorState.key))
self.idleColorState = SColor.fromString(Store.shared.string(key: "\(self.name)_idleColor", defaultValue: self.idleColorState.key))
self.eCoresColorState = SColor.fromString(Store.shared.string(key: "\(self.name)_eCoresColor", defaultValue: self.eCoresColorState.key))
self.pCoresColorState = SColor.fromString(Store.shared.string(key: "\(self.name)_pCoresColor", defaultValue: self.pCoresColorState.key))
}
private func charts() -> NSView {
let view = NSStackView()
view.orientation = .vertical
view.distribution = .fillEqually
view.spacing = Constants.Popup.spacing*2
let circle = PieChartView(frame: NSRect.zero, segments: [], drawValue: true)
circle.toolTip = localizedString("CPU usage")
self.circle = circle
view.addArrangedSubview(circle)
if let cores = SystemKit.shared.device.info.cpu?.logicalCores {
let barChartContainer: NSView = {
let box: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 24))
box.heightAnchor.constraint(equalToConstant: box.frame.height).isActive = true
box.wantsLayer = true
box.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.1).cgColor
box.layer?.cornerRadius = 3
let chart = ColumnChartView(num: Int(cores))
self.columnChart = chart
box.addArrangedSubview(chart)
return box
}()
view.addArrangedSubview(barChartContainer)
}
return view
}
private func details() -> NSView {
let view = NSStackView()
view.orientation = .vertical
view.distribution = .fillEqually
view.spacing = 2
(self.systemColorView, self.systemField) = portalWithColorRow(view, color: self.systemColor, title: "\(localizedString("System")):")
(self.userColorView, self.userField) = portalWithColorRow(view, color: self.userColor, title: "\(localizedString("User")):")
(self.idleColorView, self.idleField) = portalWithColorRow(view, color: self.idleColor.withAlphaComponent(0.5), title: "\(localizedString("Idle")):")
if SystemKit.shared.device.info.cpu?.eCores != nil {
(self.eCoresColorView, self.eCoresField) = portalWithColorRow(view, color: self.eCoresColor, title: "E-cores:")
}
if SystemKit.shared.device.info.cpu?.pCores != nil {
(self.pCoresColorView, self.pCoresField) = portalWithColorRow(view, color: self.pCoresColor, title: "P-cores:")
}
return view
}
internal func callback(_ value: CPU_Load) {
DispatchQueue.main.async(execute: {
if (self.window?.isVisible ?? false) || !self.initialized {
self.systemField?.stringValue = "\(Int(value.systemLoad.rounded(toPlaces: 2) * 100))%"
self.userField?.stringValue = "\(Int(value.userLoad.rounded(toPlaces: 2) * 100))%"
self.idleField?.stringValue = "\(Int(value.idleLoad.rounded(toPlaces: 2) * 100))%"
self.circle?.toolTip = "\(localizedString("CPU usage")): \(Int(value.totalUsage.rounded(toPlaces: 2) * 100))%"
self.circle?.setValue(value.totalUsage)
self.circle?.setSegments([
circle_segment(value: value.systemLoad, color: self.systemColor),
circle_segment(value: value.userLoad, color: self.userColor)
])
self.circle?.setNonActiveSegmentColor(self.idleColor)
if let field = self.eCoresField, let usage = value.usageECores {
field.stringValue = "\(Int(usage * 100))%"
}
if let field = self.pCoresField, let usage = value.usagePCores {
field.stringValue = "\(Int(usage * 100))%"
}
var usagePerCore: [ColorValue] = []
if let cores = SystemKit.shared.device.info.cpu?.cores, cores.count == value.usagePerCore.count {
for i in 0.. {
private var cpuInfo: processor_info_array_t!
private var prevCpuInfo: processor_info_array_t?
private var numCpuInfo: mach_msg_type_number_t = 0
private var numPrevCpuInfo: mach_msg_type_number_t = 0
private var numCPUs: uint = 0
private let CPUUsageLock: NSLock = NSLock()
private var previousInfo = host_cpu_load_info()
private var hasHyperthreadingCores = false
private var response: CPU_Load = CPU_Load()
private var numCPUsU: natural_t = 0
private var usagePerCore: [Double] = []
private var cores: [core_s]? = nil
public override func setup() {
self.hasHyperthreadingCores = sysctlByName("hw.physicalcpu") != sysctlByName("hw.logicalcpu")
[CTL_HW, HW_NCPU].withUnsafeBufferPointer { mib in
var sizeOfNumCPUs: size_t = MemoryLayout.size
let status = sysctl(processor_info_array_t(mutating: mib.baseAddress), 2, &numCPUs, &sizeOfNumCPUs, nil, 0)
if status != 0 {
self.numCPUs = 1
}
}
self.cores = SystemKit.shared.device.info.cpu?.cores
}
public override func read() {
let result: kern_return_t = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &self.numCPUsU, &self.cpuInfo, &self.numCpuInfo)
if result == KERN_SUCCESS {
self.CPUUsageLock.lock()
self.usagePerCore = []
for i in 0 ..< Int32(numCPUs) {
var inUse: Int32
var total: Int32
if let prevCpuInfo = self.prevCpuInfo {
inUse = self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_USER)]
- prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_USER)]
+ self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_SYSTEM)]
- prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_SYSTEM)]
+ self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_NICE)]
- prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_NICE)]
total = inUse + (self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_IDLE)]
- prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_IDLE)])
} else {
inUse = self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_USER)]
+ self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_SYSTEM)]
+ self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_NICE)]
total = inUse + self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_IDLE)]
}
if total != 0 {
self.usagePerCore.append(Double(inUse) / Double(total))
}
}
self.CPUUsageLock.unlock()
let showHyperthratedCores = Store.shared.bool(key: "CPU_hyperhreading", defaultValue: false)
if showHyperthratedCores || !self.hasHyperthreadingCores {
self.response.usagePerCore = self.usagePerCore
} else {
var i = 0
var a = 0
self.response.usagePerCore = []
while i < Int(self.usagePerCore.count/2) {
a = i*2
if self.usagePerCore.indices.contains(a) && self.usagePerCore.indices.contains(a+1) {
self.response.usagePerCore.append((Double(self.usagePerCore[a]) + Double(self.usagePerCore[a+1])) / 2)
}
i += 1
}
}
if let prevCpuInfo = self.prevCpuInfo {
let prevCpuInfoSize: size_t = MemoryLayout.stride * Int(self.numPrevCpuInfo)
vm_deallocate(mach_task_self_, vm_address_t(bitPattern: prevCpuInfo), vm_size_t(prevCpuInfoSize))
}
self.prevCpuInfo = self.cpuInfo
self.numPrevCpuInfo = self.numCpuInfo
self.cpuInfo = nil
self.numCpuInfo = 0
} else {
error("host_processor_info(): \(String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error")", log: self.log)
}
let cpuInfo = hostCPULoadInfo()
if cpuInfo == nil {
self.callback(nil)
return
}
let userDiff = Double(cpuInfo!.cpu_ticks.0 &- self.previousInfo.cpu_ticks.0)
let sysDiff = Double(cpuInfo!.cpu_ticks.1 &- self.previousInfo.cpu_ticks.1)
let idleDiff = Double(cpuInfo!.cpu_ticks.2 &- self.previousInfo.cpu_ticks.2)
let niceDiff = Double(cpuInfo!.cpu_ticks.3 &- self.previousInfo.cpu_ticks.3)
let totalTicks = sysDiff + userDiff + niceDiff + idleDiff
let system = sysDiff / totalTicks
let user = userDiff / totalTicks
let idle = idleDiff / totalTicks
if !system.isNaN {
self.response.systemLoad = system
}
if !user.isNaN {
self.response.userLoad = user
}
if !idle.isNaN {
self.response.idleLoad = idle
}
self.previousInfo = cpuInfo!
self.response.totalUsage = self.response.systemLoad + self.response.userLoad
if let cores = self.cores {
let eCoresList: [Double] = cores.filter({ $0.type == .efficiency }).enumerated().compactMap { (i, c) -> Double? in
if self.response.usagePerCore.indices.contains(Int(c.id)) {
return self.response.usagePerCore[Int(c.id)]
}
return i < usagePerCore.count ? usagePerCore[i] : 0
}
let pCoresList: [Double] = cores.filter({ $0.type == .performance }).enumerated().compactMap { (i, c) -> Double? in
if self.response.usagePerCore.indices.contains(Int(c.id)) {
return self.response.usagePerCore[Int(c.id)]
}
return i < usagePerCore.count ? usagePerCore[i] : 0
}
self.response.usageECores = eCoresList.reduce(0, +)/Double(eCoresList.count)
self.response.usagePCores = pCoresList.reduce(0, +)/Double(pCoresList.count)
}
self.callback(self.response)
}
private func hostCPULoadInfo() -> host_cpu_load_info? {
let count = MemoryLayout.stride/MemoryLayout.stride
var size = mach_msg_type_number_t(count)
var cpuLoadInfo = host_cpu_load_info()
let result: kern_return_t = withUnsafeMutablePointer(to: &cpuLoadInfo) {
$0.withMemoryRebound(to: integer_t.self, capacity: count) {
host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &size)
}
}
if result != KERN_SUCCESS {
error("kern_result_t: \(result)", log: self.log)
return nil
}
return cpuLoadInfo
}
}
public class ProcessReader: Reader<[TopProcess]> {
private let title: String = "CPU"
private var numberOfProcesses: Int {
get { Store.shared.int(key: "\(self.title)_processes", defaultValue: 8) }
}
public override func setup() {
self.popup = true
self.setInterval(Store.shared.int(key: "\(self.title)_updateTopInterval", defaultValue: 1))
}
public override func read() {
if self.numberOfProcesses == 0 {
return
}
let task = Process()
task.launchPath = "/bin/ps"
task.arguments = ["-Aceo pid,pcpu,comm", "-r"]
let outputPipe = Pipe()
let errorPipe = Pipe()
defer {
outputPipe.fileHandleForReading.closeFile()
errorPipe.fileHandleForReading.closeFile()
}
task.standardOutput = outputPipe
task.standardError = errorPipe
do {
try task.run()
} catch let err {
error("error read ps: \(err.localizedDescription)", log: self.log)
return
}
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: outputData, encoding: .utf8)
_ = String(data: errorData, encoding: .utf8)
guard let output, !output.isEmpty else { return }
var index = 0
var processes: [TopProcess] = []
output.enumerateLines { (line, stop) in
if index != 0 {
let str = line.trimmingCharacters(in: .whitespaces)
let pidFind = str.findAndCrop(pattern: "^\\d+")
let usageFind = pidFind.remain.findAndCrop(pattern: "^[0-9,.]+ ")
let command = usageFind.remain.trimmingCharacters(in: .whitespaces)
let pid = Int(pidFind.cropped) ?? 0
let usage = Double(usageFind.cropped.replacingOccurrences(of: ",", with: ".")) ?? 0
var name: String = command
if let app = NSRunningApplication(processIdentifier: pid_t(pid)), let n = app.localizedName {
name = n
}
if command.contains("com.apple.Virtua") && name.contains("Docker") {
name = "Docker"
}
processes.append(TopProcess(pid: pid, name: name, usage: usage))
}
if index == self.numberOfProcesses { stop = true }
index += 1
}
self.callback(processes)
}
}
public class TemperatureReader: Reader {
var list: [String] = []
public override func setup() {
self.popup = true
switch SystemKit.shared.device.platform {
case .m1, .m1Pro, .m1Max, .m1Ultra:
self.list = ["Tp09", "Tp0T", "Tp01", "Tp05", "Tp0D", "Tp0H", "Tp0L", "Tp0P", "Tp0X", "Tp0b"]
case .m2, .m2Pro, .m2Max, .m2Ultra:
self.list = ["Tp1h", "Tp1t", "Tp1p", "Tp1l", "Tp01", "Tp05", "Tp09", "Tp0D", "Tp0X", "Tp0b", "Tp0f", "Tp0j"]
case .m3, .m3Pro, .m3Max, .m3Ultra:
self.list = ["Te05", "Te0L", "Te0P", "Te0S", "Tf04", "Tf09", "Tf0A", "Tf0B", "Tf0D", "Tf0E", "Tf44", "Tf49", "Tf4A", "Tf4B", "Tf4D", "Tf4E"]
case .m4, .m4Pro, .m4Max, .m4Ultra:
self.list = ["Te05", "Te09", "Te0H", "Te0S", "Tp01", "Tp05", "Tp09", "Tp0D", "Tp0V", "Tp0Y", "Tp0b", "Tp0e"]
default: break
}
}
public override func read() {
var temperature: Double? = nil
if let value = SMC.shared.getValue("TC0D"), value < 110 {
temperature = value
} else if let value = SMC.shared.getValue("TC0E"), value < 110 {
temperature = value
} else if let value = SMC.shared.getValue("TC0F"), value < 110 {
temperature = value
} else if let value = SMC.shared.getValue("TC0P"), value < 110 {
temperature = value
} else if let value = SMC.shared.getValue("TC0H"), value < 110 {
temperature = value
} else {
var total: Double = 0
var counter: Double = 0
self.list.forEach { (key: String) in
if let value = SMC.shared.getValue(key) {
total += value
counter += 1
}
}
if total != 0 && counter != 0 {
temperature = total / counter
}
}
self.callback(temperature)
}
}
// inspired by https://github.com/shank03/StatsBar/blob/e175aa71c914ce882ce2e90163f3eb18262a8e25/StatsBar/Service/IOReport.swift
public class FrequencyReader: Reader {
private var eCoreFreqs: [Int32] = []
private var pCoreFreqs: [Int32] = []
private var eCoreCount: Double = 0
private var pCoreCount: Double = 0
private var channels: CFMutableDictionary? = nil
private var subscription: IOReportSubscriptionRef? = nil
private var prev: (samples: CFDictionary, time: TimeInterval)? = nil
private let measurementCount: Int = 4
private let isReadingQueue = DispatchQueue(label: "com.example.isReadingQueue")
private var _isReading: Bool = false
private var isReading: Bool {
get { self.isReadingQueue.sync { self._isReading } }
set { self.isReadingQueue.sync { self._isReading = newValue } }
}
private struct IOSample {
let group: String
let subGroup: String
let channel: String
let unit: String
let delta: CFDictionary
}
public override func setup() {
self.popup = true
self.eCoreFreqs = SystemKit.shared.device.info.cpu?.eCoreFrequencies ?? []
self.pCoreFreqs = SystemKit.shared.device.info.cpu?.pCoreFrequencies ?? []
self.eCoreCount = Double(SystemKit.shared.device.info.cpu?.eCores ?? 0)
self.pCoreCount = Double(SystemKit.shared.device.info.cpu?.pCores ?? 0)
self.channels = self.getChannels()
var dict: Unmanaged?
self.subscription = IOReportCreateSubscription(nil, self.channels, &dict, 0, nil)
dict?.release()
}
public override func read() {
guard !self.isReading, !self.eCoreFreqs.isEmpty && !self.pCoreFreqs.isEmpty, self.channels != nil, self.subscription != nil else { return }
self.isReading = true
let minECoreFreq = Double(self.eCoreFreqs.min() ?? 0)
let minPCoreFreq = Double(self.pCoreFreqs.min() ?? 0)
Task {
var eCores: [Double] = []
var pCores: [Double] = []
for (samples, _) in await self.getSamples() {
var eCore: [Double] = []
var pCore: [Double] = []
for sample in samples {
guard sample.group == "CPU Stats" else { continue }
if sample.channel.starts(with: "ECPU") {
eCore.append(self.calculateFrequencies(dict: sample.delta, freqs: self.eCoreFreqs))
}
if sample.channel.starts(with: "PCPU") {
pCore.append(self.calculateFrequencies(dict: sample.delta, freqs: self.pCoreFreqs))
}
}
let eCoresAvgFreq: Double = eCore.isEmpty ? 0 : (eCore.reduce(0.0, { $0 + $1 }) / Double(eCore.count))
let pCoresAvgFreq: Double = pCore.isEmpty ? 0 : (pCore.reduce(0.0, { $0 + $1 }) / Double(pCore.count))
eCores.append(max(eCoresAvgFreq, minECoreFreq))
pCores.append(max(pCoresAvgFreq, minPCoreFreq))
}
let eFreq: Double = eCores.reduce(0, { $0 + $1 }) / Double(self.measurementCount)
let pFreq: Double = pCores.reduce(0, { $0 + $1 }) / Double(self.measurementCount)
let value: Double = ((eFreq * self.eCoreCount) + (pFreq * self.pCoreCount)) / (self.eCoreCount + self.pCoreCount)
self.callback(CPU_Frequency(value: value, eCore: eFreq, pCore: pFreq))
self.isReading = false
}
}
private func calculateFrequencies(dict: CFDictionary, freqs: [Int32]) -> Double {
let items = self.getResidencies(dict: dict)
guard let offset = items.firstIndex(where: { $0.0 != "IDLE" && $0.0 != "DOWN" && $0.0 != "OFF" }) else { return 0 }
let usage = items.dropFirst(offset).reduce(0.0) { $0 + Double($1.f) }
let count = freqs.count
var avgFreq: Double = 0
for i in 0.. [(ns: String, f: Int64)] {
let count = IOReportStateGetCount(dict)
var res: [(String, Int64)] = []
for i in 0.. CFMutableDictionary? {
let channelNames = [
("CPU Stats", "CPU Complex Performance States"),
("CPU Stats", "CPU Core Performance States")
]
var channels: [CFDictionary] = []
for (gname, sname) in channelNames {
let channel = IOReportCopyChannelsInGroup(gname as CFString?, sname as CFString?, 0, 0, 0)
guard let channel = channel?.takeRetainedValue() else { continue }
channels.append(channel)
}
let chan = channels[0]
for i in 1.. [([IOSample], TimeInterval)] {
let duration = 500
let step = UInt64(duration / self.measurementCount)
var samples = [([IOSample], TimeInterval)]()
guard let initialSample = self.getSample() else { return samples }
var prev = self.prev ?? initialSample
for _ in 0.. (samples: CFDictionary, time: TimeInterval)? {
guard let sample = IOReportCreateSamples(self.subscription, self.channels, nil)?.takeRetainedValue() else {
return nil
}
return (sample, Date().timeIntervalSince1970)
}
private func collectIOSamples(data: CFDictionary) -> [IOSample] {
let dict = data as! [String: Any]
let items = dict["IOReportChannels"] as! CFArray
let itemSize = CFArrayGetCount(items)
var samples = [IOSample]()
for index in 0.. {
private var limits: CPU_Limit = CPU_Limit()
public override func read() {
let task = Process()
task.launchPath = "/usr/bin/pmset"
task.arguments = ["-g", "therm"]
let outputPipe = Pipe()
defer {
outputPipe.fileHandleForReading.closeFile()
}
task.standardOutput = outputPipe
do {
try task.run()
} catch let err {
error("error read pmset: \(err.localizedDescription)", log: self.log)
return
}
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
guard let str = String(data: outputData, encoding: .utf8) else { return }
var lines = str.split(separator: "\n")
guard !lines.isEmpty else { return }
lines.removeFirst(3)
lines.forEach { (line: Substring) in
guard let value = Int(line.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()) else {
return
}
if line.contains("Scheduler") {
self.limits.scheduler = value
} else if line.contains("CPUs") {
self.limits.cpus = value
} else if line.contains("Speed") {
self.limits.speed = value
}
}
self.callback(self.limits)
}
}
public class AverageLoadReader: Reader {
private let title: String = "CPU"
private var load: CPU_AverageLoad = CPU_AverageLoad()
public override func setup() {
self.popup = false
self.setInterval(15)
}
public override func read() {
let task = Process()
task.launchPath = "/usr/bin/uptime"
let outputPipe = Pipe()
defer {
outputPipe.fileHandleForReading.closeFile()
}
task.standardOutput = outputPipe
do {
try task.run()
} catch let err {
error("error read uptime: \(err.localizedDescription)", log: self.log)
return
}
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
guard let raw = String(data: outputData, encoding: .utf8), let line = raw.split(separator: "\n").first else {
return
}
let str = line.trimmingCharacters(in: .whitespaces)
let strFind = str.findAndCrop(pattern: "(\\d+(.|,)\\d+ *){3}$")
let strArr = strFind.cropped.split(separator: " ")
guard strArr.count == 3 else { return }
var list: [Double] = []
strArr.forEach { (n: Substring) in
let value = Double(n.replacingOccurrences(of: ",", with: ".")) ?? 0
list.append(value)
}
guard list.count == 3 else { return }
self.load.load1 = list[0]
self.load.load5 = list[1]
self.load.load15 = list[2]
self.callback(self.load)
}
}
================================================
FILE: Modules/CPU/settings.swift
================================================
//
// Settings.swift
// CPU
//
// Created by Serhiy Mytrovtsiy on 18/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class Settings: NSStackView, Settings_v {
private var usagePerCoreState: Bool = false
private var hyperthreadState: Bool = false
private var splitValueState: Bool = false
private var updateIntervalValue: Int = 1
private var updateTopIntervalValue: Int = 1
private var numberOfProcesses: Int = 8
private var clustersGroupState: Bool = false
private let title: String
private var hasHyperthreadingCores = false
public var callback: (() -> Void) = {}
public var callbackWhenUpdateNumberOfProcesses: (() -> Void) = {}
public var setInterval: ((_ value: Int) -> Void) = {_ in }
public var setTopInterval: ((_ value: Int) -> Void) = {_ in }
private var hyperthreadView: NSSwitch? = nil
private var splitValueView: NSSwitch? = nil
private var usagePerCoreView: NSSwitch? = nil
private var groupByClustersView: NSSwitch? = nil
public init(_ module: ModuleType) {
self.title = module.stringValue
self.hyperthreadState = Store.shared.bool(key: "\(self.title)_hyperhreading", defaultValue: self.hyperthreadState)
self.usagePerCoreState = Store.shared.bool(key: "\(self.title)_usagePerCore", defaultValue: self.usagePerCoreState)
self.splitValueState = Store.shared.bool(key: "\(self.title)_splitValue", defaultValue: self.splitValueState)
self.updateIntervalValue = Store.shared.int(key: "\(self.title)_updateInterval", defaultValue: self.updateIntervalValue)
self.updateTopIntervalValue = Store.shared.int(key: "\(self.title)_updateTopInterval", defaultValue: self.updateTopIntervalValue)
self.numberOfProcesses = Store.shared.int(key: "\(self.title)_processes", defaultValue: self.numberOfProcesses)
if !self.usagePerCoreState {
self.hyperthreadState = false
}
self.hasHyperthreadingCores = sysctlByName("hw.physicalcpu") != sysctlByName("hw.logicalcpu")
self.clustersGroupState = Store.shared.bool(key: "\(self.title)_clustersGroup", defaultValue: self.clustersGroupState)
super.init(frame: NSRect(x: 0, y: 0, width: 0, height: 0))
self.orientation = .vertical
self.distribution = .gravityAreas
self.translatesAutoresizingMaskIntoConstraints = false
self.spacing = Constants.Settings.margin
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func load(widgets: [widget_t]) {
self.subviews.forEach{ $0.removeFromSuperview() }
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Update interval"), component: selectView(
action: #selector(self.changeUpdateInterval),
items: ReaderUpdateIntervals,
selected: "\(self.updateIntervalValue)"
)),
PreferencesRow(localizedString("Update interval for top processes"), component: selectView(
action: #selector(self.changeUpdateTopInterval),
items: ReaderUpdateIntervals,
selected: "\(self.updateTopIntervalValue)"
))
]))
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Number of top processes"), component: selectView(
action: #selector(self.changeNumberOfProcesses),
items: NumbersOfProcesses.map{ KeyValue_t(key: "\($0)", value: "\($0)") },
selected: "\(self.numberOfProcesses)"
))
]))
if !widgets.filter({ $0 == .barChart }).isEmpty {
self.splitValueView = switchView(
action: #selector(self.toggleSplitValue),
state: self.splitValueState
)
self.usagePerCoreView = switchView(
action: #selector(self.toggleUsagePerCore),
state: self.usagePerCoreState
)
if self.usagePerCoreState || self.clustersGroupState {
self.splitValueView?.isEnabled = false
self.splitValueView?.state = .off
}
var rows: [PreferencesRow] = [
PreferencesRow(localizedString("Show usage per core"), component: self.usagePerCoreView!)
]
#if arch(arm64)
self.groupByClustersView = switchView(
action: #selector(self.toggleClustersGroup),
state: self.clustersGroupState
)
rows.append(PreferencesRow(localizedString("Cluster grouping"), component: self.groupByClustersView!))
#endif
if self.hasHyperthreadingCores {
self.hyperthreadView = switchView(
action: #selector(self.toggleMultithreading),
state: self.hyperthreadState
)
if !self.usagePerCoreState {
self.hyperthreadView?.isEnabled = false
self.hyperthreadView?.state = .off
}
rows.append(PreferencesRow(localizedString("Show hyper-threading cores"), component: self.hyperthreadView!))
}
rows.append(PreferencesRow(localizedString("Split the value (System/User)"), component: self.splitValueView!))
self.addArrangedSubview(PreferencesSection(rows))
}
}
@objc private func changeUpdateInterval(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let value = Int(key) else { return }
self.updateIntervalValue = value
Store.shared.set(key: "\(self.title)_updateInterval", value: value)
self.setInterval(value)
}
@objc private func changeUpdateTopInterval(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let value = Int(key) else { return }
self.updateTopIntervalValue = value
Store.shared.set(key: "\(self.title)_updateTopInterval", value: value)
self.setTopInterval(value)
}
@objc private func changeNumberOfProcesses(_ sender: NSMenuItem) {
if let value = Int(sender.title) {
self.numberOfProcesses = value
Store.shared.set(key: "\(self.title)_processes", value: value)
self.callbackWhenUpdateNumberOfProcesses()
}
}
@objc func toggleUsagePerCore(_ sender: NSControl) {
self.usagePerCoreState = controlState(sender)
Store.shared.set(key: "\(self.title)_usagePerCore", value: self.usagePerCoreState)
self.callback()
self.hyperthreadView?.isEnabled = self.usagePerCoreState
self.splitValueView?.isEnabled = !(self.usagePerCoreState || self.clustersGroupState)
if !self.usagePerCoreState {
self.hyperthreadState = false
Store.shared.set(key: "\(self.title)_hyperhreading", value: self.hyperthreadState)
self.hyperthreadView?.state = .off
} else {
self.splitValueState = false
Store.shared.set(key: "\(self.title)_splitValue", value: self.splitValueState)
self.splitValueView?.state = .off
}
if self.clustersGroupState && self.usagePerCoreState {
self.clustersGroupState = false
Store.shared.set(key: "\(self.title)_clustersGroup", value: self.clustersGroupState)
self.groupByClustersView?.state = .off
}
}
@objc func toggleMultithreading(_ sender: NSControl) {
self.hyperthreadState = controlState(sender)
Store.shared.set(key: "\(self.title)_hyperhreading", value: self.hyperthreadState)
self.callback()
}
@objc func toggleSplitValue(_ sender: NSControl) {
self.splitValueState = controlState(sender)
Store.shared.set(key: "\(self.title)_splitValue", value: self.splitValueState)
self.callback()
}
@objc func toggleClustersGroup(_ sender: NSControl) {
self.clustersGroupState = controlState(sender)
Store.shared.set(key: "\(self.title)_clustersGroup", value: self.clustersGroupState)
self.splitValueView?.isEnabled = !(self.usagePerCoreState || self.clustersGroupState)
if self.clustersGroupState && self.usagePerCoreState {
self.usagePerCoreView?.state = .off
let toggle: NSSwitch = NSSwitch()
toggle.state = .off
self.toggleUsagePerCore(toggle)
}
self.callback()
}
}
================================================
FILE: Modules/CPU/widget.swift
================================================
//
// widget.swift
// CPU
//
// Created by Serhiy Mytrovtsiy on 01/07/2024
// Using Swift 5.0
// Running on macOS 14.5
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//
import SwiftUI
import WidgetKit
import Charts
import Kit
public struct CPU_entry: TimelineEntry {
public static let kind = "CPUWidget"
public static var snapshot: CPU_entry = CPU_entry(value: CPU_Load(totalUsage: 0.34, systemLoad: 0.11, userLoad: 0.23, idleLoad: 0.66))
public var date: Date {
Calendar.current.date(byAdding: .second, value: 5, to: Date())!
}
public var value: CPU_Load? = nil
}
public struct Provider: TimelineProvider {
public typealias Entry = CPU_entry
private let userDefaults: UserDefaults? = UserDefaults(suiteName: "\(Bundle.main.object(forInfoDictionaryKey: "TeamId") as! String).eu.exelban.Stats.widgets")
public var systemWidgetsUpdatesState: Bool {
self.userDefaults?.bool(forKey: "systemWidgetsUpdates_state") ?? false
}
public func placeholder(in context: Context) -> CPU_entry {
CPU_entry()
}
public func getSnapshot(in context: Context, completion: @escaping (CPU_entry) -> Void) {
completion(CPU_entry.snapshot)
}
public func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
self.userDefaults?.set(Date().timeIntervalSince1970, forKey: CPU_entry.kind)
var entry = CPU_entry()
if let raw = self.userDefaults?.data(forKey: "CPU@LoadReader"), let load = try? JSONDecoder().decode(CPU_Load.self, from: raw) {
entry.value = load
}
let entries: [CPU_entry] = [entry]
completion(Timeline(entries: entries, policy: .atEnd))
}
}
@available(macOS 14.0, *)
public struct CPUWidget: Widget {
var systemColor: Color = Color(nsColor: NSColor.systemRed)
var userColor: Color = Color(nsColor: NSColor.systemBlue)
var idleColor: Color = Color(nsColor: NSColor.lightGray)
public init() {}
public var body: some WidgetConfiguration {
StaticConfiguration(kind: CPU_entry.kind, provider: Provider()) { entry in
VStack(spacing: 10) {
if Provider().systemWidgetsUpdatesState, let value = entry.value {
HStack {
Chart {
SectorMark(angle: .value(localizedString("System"), value.systemLoad), innerRadius: .ratio(0.8)).foregroundStyle(self.systemColor)
SectorMark(angle: .value(localizedString("User"), value.userLoad), innerRadius: .ratio(0.8)).foregroundStyle(self.userColor)
SectorMark(angle: .value(localizedString("Idle"), value.idleLoad), innerRadius: .ratio(0.8)).foregroundStyle(self.idleColor)
}
.frame(maxWidth: .infinity, maxHeight: 84)
.chartLegend(.hidden)
.chartBackground { chartProxy in
GeometryReader { geometry in
if let anchor = chartProxy.plotFrame {
let frame = geometry[anchor]
Text("\(Int(value.totalUsage*100))%")
.font(.system(size: 16, weight: .regular))
.position(x: frame.midX, y: frame.midY-5)
Text("CPU")
.font(.system(size: 9, weight: .semibold))
.position(x: frame.midX, y: frame.midY+10)
}
}
}
}
VStack(spacing: 3) {
HStack {
Rectangle().fill(self.systemColor).frame(width: 12, height: 12).cornerRadius(2)
Text(localizedString("System")).font(.system(size: 12, weight: .regular)).foregroundColor(.secondary)
Spacer()
Text("\(Int(value.systemLoad*100))%")
}
HStack {
Rectangle().fill(self.userColor).frame(width: 12, height: 12).cornerRadius(2)
Text(localizedString("User")).font(.system(size: 12, weight: .regular)).foregroundColor(.secondary)
Spacer()
Text("\(Int(value.userLoad*100))%")
}
}
} else if !Provider().systemWidgetsUpdatesState {
Text("Enable in Settings")
.font(.system(size: 12, weight: .regular))
.foregroundColor(.secondary)
} else {
Text("No data")
}
}
.containerBackground(for: .widget) {
Color.clear
}
}
.configurationDisplayName("CPU widget")
.description("Displays CPU stats")
.supportedFamilies([.systemSmall])
}
}
================================================
FILE: Modules/Clock/config.plist
================================================
Name
Clock
State
Symbol
clock.fill
Widgets
label
Default
Title
CLK
Order
0
sensors
Default
Preview
Values
23.03.2023 13:45
Order
1
Settings
popup
notifications
================================================
FILE: Modules/Clock/main.swift
================================================
//
// main.swift
// Clock
//
// Created by Serhiy Mytrovtsiy on 23/03/2023
// Using Swift 5.0
// Running on macOS 13.2
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Foundation
import Kit
public struct Clock_t: Codable {
public var id: String = UUID().uuidString
public var enabled: Bool = true
public var name: String
public var format: String
public var tz: String
public var value: Date? = nil
var popupIndex: Int {
get { Store.shared.int(key: "clock_\(self.id)_popupIndex", defaultValue: -1) }
set { Store.shared.set(key: "clock_\(self.id)_popupIndex", value: newValue) }
}
var popupState: Bool {
get { Store.shared.bool(key: "clock_\(self.id)_popupState", defaultValue: true) }
set { Store.shared.set(key: "clock_\(self.id)_popupState", value: newValue) }
}
public func formatted() -> String {
let formatter = DateFormatter()
formatter.dateFormat = self.format
formatter.timeZone = TimeZone(from: self.tz)
return formatter.string(from: self.value ?? Date())
}
}
public class Clock: Module {
private let popupView: Popup = Popup(.clock)
private let portalView: Portal
private let settingsView: Settings = Settings(.clock)
private var reader: ClockReader?
static var list: [Clock_t] {
if let objects = Store.shared.data(key: "\(ModuleType.clock.stringValue)_list") {
let decoder = JSONDecoder()
if let objectsDecoded = try? decoder.decode(Array.self, from: objects) as [Clock_t] {
return objectsDecoded
}
}
return [Clock.local]
}
public init() {
self.portalView = Portal(.clock, list: Clock.list)
super.init(
moduleType: .clock,
popup: self.popupView,
settings: self.settingsView,
portal: self.portalView
)
guard self.available else { return }
self.reader = ClockReader(.clock) { [weak self] value in
self?.callback(value)
}
self.settingsView.callback = { [weak self] in
guard let self, self.enabled, let reader = self.reader else { return }
reader.stop()
reader.start()
}
self.setReaders([self.reader])
}
private func callback(_ value: Date?) {
guard let value else { return }
var clocks: [Clock_t] = Clock.list
var widgetList: [Stack_t] = []
for (i, c) in clocks.enumerated() {
clocks[i].value = value
if c.enabled {
widgetList.append(Stack_t(key: c.name, value: clocks[i].formatted()))
}
}
DispatchQueue.main.async(execute: {
self.popupView.callback(clocks)
self.portalView.callback(clocks)
})
self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: SWidget) in
switch w.item {
case let widget as StackWidget: widget.setValues(widgetList)
default: break
}
}
}
}
extension Clock {
static let localID: String = UUID().uuidString
static var local: Clock_t {
Clock_t(id: Clock.localID, name: localizedString("Local time"), format: "yyyy-MM-dd HH:mm:ss", tz: "local")
}
static var zones: [KeyValue_t] {
[
KeyValue_t(key: "local", value: "Local"),
KeyValue_t(key: "separator", value: "separator"),
KeyValue_t(key: "-12", value: "UTC-12:00"),
KeyValue_t(key: "-11", value: "UTC-11:00"),
KeyValue_t(key: "-10", value: "UTC-10:00"),
KeyValue_t(key: "-9", value: "UTC-9:00"),
KeyValue_t(key: "-8", value: "UTC-8:00"),
KeyValue_t(key: "-7", value: "UTC-7:00"),
KeyValue_t(key: "-6", value: "UTC-6:00"),
KeyValue_t(key: "-5", value: "UTC-5:00"),
KeyValue_t(key: "-4:30", value: "UTC-4:30"),
KeyValue_t(key: "-4", value: "UTC-4:00"),
KeyValue_t(key: "-3:30", value: "UTC-3:30"),
KeyValue_t(key: "-3", value: "UTC-3:00"),
KeyValue_t(key: "-2", value: "UTC-2:00"),
KeyValue_t(key: "-1", value: "UTC-1:00"),
KeyValue_t(key: "0", value: "UTC"),
KeyValue_t(key: "1", value: "UTC+1:00"),
KeyValue_t(key: "2", value: "UTC+2:00"),
KeyValue_t(key: "3", value: "UTC+3:00"),
KeyValue_t(key: "3:30", value: "UTC+3:30"),
KeyValue_t(key: "4", value: "UTC+4:00"),
KeyValue_t(key: "4:30", value: "UTC+4:30"),
KeyValue_t(key: "5", value: "UTC+5:00"),
KeyValue_t(key: "5:30", value: "UTC+5:30"),
KeyValue_t(key: "5:45", value: "UTC+5:45"),
KeyValue_t(key: "6", value: "UTC+6:00"),
KeyValue_t(key: "6:30", value: "UTC+6:30"),
KeyValue_t(key: "7", value: "UTC+7:00"),
KeyValue_t(key: "8", value: "UTC+8:00"),
KeyValue_t(key: "9", value: "UTC+9:00"),
KeyValue_t(key: "9:30", value: "UTC+9:30"),
KeyValue_t(key: "10", value: "UTC+10:00"),
KeyValue_t(key: "10:30", value: "UTC+10:30"),
KeyValue_t(key: "11", value: "UTC+11:00"),
KeyValue_t(key: "12", value: "UTC+12:00"),
KeyValue_t(key: "13", value: "UTC+13:00"),
KeyValue_t(key: "14", value: "UTC+14:00"),
KeyValue_t(key: "separator", value: "separator")
] + TimeZone.knownTimeZoneIdentifiers.map { KeyValue_t(key: $0, value: $0) }
}
}
================================================
FILE: Modules/Clock/popup.swift
================================================
//
// popup.swift
// Clock
//
// Created by Serhiy Mytrovtsiy on 24/03/2023
// Using Swift 5.0
// Running on macOS 13.2
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class Popup: PopupWrapper {
private let orderTableView: OrderTableView = OrderTableView()
private var list: [Clock_t] = []
private var calendarView: CalendarView? = nil
private var calendarState: Bool = true
private var weekNumbersState: Bool = false
public init(_ module: ModuleType) {
super.init(module, frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
self.orientation = .vertical
self.spacing = Constants.Popup.margins
self.calendarState = Store.shared.bool(key: "\(self.title)_calendar", defaultValue: self.calendarState)
self.weekNumbersState = Store.shared.bool(key: "\(self.title)_calendarWeekNumbers", defaultValue: self.weekNumbersState)
self.calendarView = CalendarView(self.frame.width, showWeekNumbers: self.weekNumbersState)
self.orderTableView.reorderCallback = { [weak self] in
self?.rearrange()
}
if let calendar = self.calendarView, self.calendarState {
self.addArrangedSubview(calendar)
}
self.recalculateHeight()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func callback(_ list: [Clock_t]) {
defer { self.recalculateHeight() }
var sorted = list.sorted(by: { $0.popupIndex < $1.popupIndex })
var views = self.subviews.filter{ $0 is ClockView }.compactMap{ $0 as? ClockView }
if sorted.count != self.orderTableView.list.count || self.orderTableView.window?.isVisible ?? false {
self.orderTableView.list = sorted
self.orderTableView.update()
}
sorted = sorted.filter({ $0.popupState })
if sorted.count < views.count && !views.isEmpty {
views.forEach{ $0.removeFromSuperview() }
views = []
}
sorted.forEach { (c: Clock_t) in
if let view = views.first(where: { $0.clock.id == c.id }) {
view.update(c)
} else {
self.addArrangedSubview(ClockView(width: self.frame.width, clock: c))
}
}
self.list = sorted
}
private func recalculateHeight() {
let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - self.spacing
if h > 0 && self.frame.size.height != h {
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.sizeCallback?(self.frame.size)
}
}
public override func settings() -> NSView? {
let view = SettingsContainerView()
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Keyboard shortcut"), component: KeyboardShartcutView(
callback: self.setKeyboardShortcut,
value: self.keyboardShortcut
))
]))
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Calendar"), component: switchView(
action: #selector(self.toggleCalendarState),
state: self.calendarState
)),
PreferencesRow(localizedString("Show week numbers"), component: switchView(
action: #selector(self.toggleWeekNumbersState),
state: self.weekNumbersState
))
]))
view.addArrangedSubview(self.orderTableView)
return view
}
public override func appear() {
if self.calendarState {
self.calendarView?.checkCurrentDay()
}
}
private func rearrange() {
let views = self.subviews.filter{ $0 is ClockView }.compactMap{ $0 as? ClockView }
views.forEach{ $0.removeFromSuperview() }
self.callback(self.list)
}
@objc private func toggleCalendarState(_ sender: NSControl) {
self.calendarState = controlState(sender)
Store.shared.set(key: "\(self.title)_calendar", value: self.calendarState)
guard let view = self.calendarView else { return }
if self.calendarState {
self.insertArrangedSubview(view, at: 0)
} else {
view.removeFromSuperview()
}
self.recalculateHeight()
}
@objc private func toggleWeekNumbersState(_ sender: NSControl) {
self.weekNumbersState = controlState(sender)
Store.shared.set(key: "\(self.title)_calendarWeekNumbers", value: self.weekNumbersState)
self.calendarView?.setShowWeekNumbers(self.weekNumbersState)
self.recalculateHeight()
}
}
private class CalendarView: NSStackView {
private var itemSize: CGSize
private var showWeekNumbers: Bool
private var navigationHeightConstraint: NSLayoutConstraint?
private var year: Int
private var month: Int
private var day: Int
private var currentYear: Int {
Calendar.current.component(.year, from: Date())
}
private var currentMonth: Int {
Calendar.current.component(.month, from: Date())
}
private var currentDay: Int {
Calendar.current.component(.day, from: Date())
}
private var weekDays: [String] {
let calendar = Calendar.current
let firstWeekdayIndex = calendar.firstWeekday - 1
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current
dateFormatter.calendar = calendar
let weekdaySymbols = dateFormatter.shortWeekdaySymbols
return Array(weekdaySymbols![firstWeekdayIndex...]) + weekdaySymbols![.. NSView {
let view = NSStackView()
view.distribution = .fill
view.alignment = .centerY
self.navigationHeightConstraint = view.heightAnchor.constraint(greaterThanOrEqualToConstant: max(self.itemSize.height, 24))
self.navigationHeightConstraint?.isActive = true
view.orientation = .horizontal
let details = NSTextField(labelWithString: "\(Calendar.current.standaloneMonthSymbols[self.month-1]) \(self.year)")
details.font = .systemFont(ofSize: 16, weight: .medium)
details.lineBreakMode = .byTruncatingTail
details.maximumNumberOfLines = 1
details.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
self.current = details
let buttons = NSStackView()
buttons.orientation = .horizontal
buttons.setContentCompressionResistancePriority(.required, for: .horizontal)
let prev = NSButton()
prev.bezelStyle = .regularSquare
prev.translatesAutoresizingMaskIntoConstraints = false
prev.imageScaling = .scaleNone
prev.image = iconFromSymbol(name: "arrow.left", scale: .medium)
prev.contentTintColor = .labelColor
prev.isBordered = false
prev.action = #selector(self.prevMonth)
prev.target = self
prev.toolTip = localizedString("Previous month")
prev.focusRingType = .none
let next = NSButton()
next.bezelStyle = .regularSquare
next.translatesAutoresizingMaskIntoConstraints = false
next.imageScaling = .scaleNone
next.image = iconFromSymbol(name: "arrow.right", scale: .medium)
next.contentTintColor = .labelColor
next.isBordered = false
next.action = #selector(self.nextMonth)
next.target = self
next.toolTip = localizedString("Next month")
next.focusRingType = .none
buttons.addArrangedSubview(prev)
buttons.addArrangedSubview(next)
view.addArrangedSubview(details)
view.addArrangedSubview(NSView())
view.addArrangedSubview(buttons)
return view
}
private func updateItemSize() {
let columns: CGFloat = self.showWeekNumbers ? 8 : 7
self.itemSize = NSSize(
width: (self.frame.width-(Constants.Popup.margins*2))/columns,
height: (self.frame.width-(Constants.Popup.spacing*2))/8 - 4
)
self.navigationHeightConstraint?.constant = max(self.itemSize.height, 24)
}
private func headerItem(_ value: String) -> NSView {
let view = NSTextField()
let cell = VerticallyCenteredTextFieldCell(textCell: value)
view.cell = cell
view.alignment = .center
view.textColor = .gray
view.font = .systemFont(ofSize: 12)
view.widthAnchor.constraint(equalToConstant: self.itemSize.width).isActive = true
view.heightAnchor.constraint(equalToConstant: self.itemSize.height).isActive = true
return view
}
private func rowItem(_ day: DateComponents) -> NSView {
if day.year == self.currentYear && day.month == self.currentMonth && day.day == self.currentDay {
return self.todayItem()
}
let view = NSTextField()
let cell = VerticallyCenteredTextFieldCell(textCell: "\(day.day ?? 0)")
view.cell = cell
view.alignment = .center
if day.month != self.month {
view.textColor = .lightGray
}
view.widthAnchor.constraint(equalToConstant: self.itemSize.width).isActive = true
view.heightAnchor.constraint(equalToConstant: self.itemSize.height).isActive = true
return view
}
private func weekNumberHeaderItem() -> NSView {
let view = NSTextField()
let cell = VerticallyCenteredTextFieldCell(textCell: "")
view.cell = cell
view.alignment = .center
view.textColor = .secondaryLabelColor
view.font = .systemFont(ofSize: 11)
view.widthAnchor.constraint(equalToConstant: self.itemSize.width).isActive = true
view.heightAnchor.constraint(equalToConstant: self.itemSize.height).isActive = true
self.addRightBorder(view)
return view
}
private func weekNumberItem(_ week: [DateComponents]) -> NSView {
let calendar = Calendar.current
let firstDate = week.compactMap { calendar.date(from: $0) }.first ?? Date()
let weekNumber = calendar.component(.weekOfYear, from: firstDate)
let view = NSTextField()
let cell = VerticallyCenteredTextFieldCell(textCell: "\(weekNumber)")
view.cell = cell
view.alignment = .center
view.textColor = .secondaryLabelColor
view.font = .systemFont(ofSize: 11)
view.widthAnchor.constraint(equalToConstant: self.itemSize.width).isActive = true
view.heightAnchor.constraint(equalToConstant: self.itemSize.height).isActive = true
self.addRightBorder(view)
return view
}
private func addRightBorder(_ view: NSView) {
let border = NSView()
border.wantsLayer = true
let borderColor = self.isDarkMode
? NSColor.white.withAlphaComponent(0.4)
: NSColor.separatorColor
border.layer?.backgroundColor = borderColor.cgColor
border.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(border)
let lineWidth = 1 / (NSScreen.main?.backingScaleFactor ?? 1)
NSLayoutConstraint.activate([
border.trailingAnchor.constraint(equalTo: view.trailingAnchor),
border.topAnchor.constraint(equalTo: view.topAnchor),
border.bottomAnchor.constraint(equalTo: view.bottomAnchor),
border.widthAnchor.constraint(equalToConstant: lineWidth)
])
}
private func todayItem() -> NSView {
let view = NSView()
let size: CGFloat = 25
let circle = NSView(frame: NSRect(x: (self.itemSize.width-size)/2, y: (self.itemSize.height-size)/2, width: size, height: size))
circle.wantsLayer = true
circle.layer?.backgroundColor = NSColor.systemRed.cgColor
circle.layer?.cornerRadius = size/2
let field = NSTextField()
field.translatesAutoresizingMaskIntoConstraints = false
let cell = VerticallyCenteredTextFieldCell(textCell: "\(self.currentDay)")
field.cell = cell
field.alignment = .center
field.textColor = .white
view.addSubview(circle)
view.addSubview(field)
view.widthAnchor.constraint(equalToConstant: self.itemSize.width).isActive = true
view.heightAnchor.constraint(equalToConstant: self.itemSize.height).isActive = true
field.widthAnchor.constraint(equalToConstant: self.itemSize.width).isActive = true
field.heightAnchor.constraint(equalToConstant: self.itemSize.height).isActive = true
return view
}
private func generateDays(for month: Int, in year: Int) -> [[DateComponents]] {
let calendar = Calendar.current
let dateComponents = DateComponents(year: year, month: month)
guard let range = calendar.range(of: .day, in: .month, for: calendar.date(from: dateComponents)!),
let firstDayOfMonth = calendar.date(from: dateComponents),
let firstWeekdayOfMonth = calendar.dateComponents([.weekday], from: firstDayOfMonth).weekday else {
return []
}
let localeFirstWeekday = calendar.firstWeekday
let daysFromPreviousMonth = (firstWeekdayOfMonth - localeFirstWeekday + 7) % 7
var previousMonthComponents = dateComponents
previousMonthComponents.month = (month == 1) ? 12 : month - 1
previousMonthComponents.year = (month == 1) ? year - 1 : year
let previousMonthDate = calendar.date(from: previousMonthComponents)!
let previousMonthRange = calendar.range(of: .day, in: .month, for: previousMonthDate)!
let lastDayOfPreviousMonth = previousMonthRange.upperBound - 1
var nextMonthComponents = dateComponents
nextMonthComponents.month = (month == 12) ? 1 : month + 1
nextMonthComponents.year = (month == 12) ? year + 1 : year
var weeks = [[DateComponents]]()
var currentWeek = [DateComponents]()
let validDaysFromPreviousMonth = min(daysFromPreviousMonth, lastDayOfPreviousMonth)
if validDaysFromPreviousMonth > 0 {
for day in (lastDayOfPreviousMonth - validDaysFromPreviousMonth + 1)...lastDayOfPreviousMonth {
var components = previousMonthComponents
components.day = day
currentWeek.append(components)
}
}
for day in range {
var components = dateComponents
components.day = day
currentWeek.append(components)
if currentWeek.count == 7 {
weeks.append(currentWeek)
currentWeek = []
}
}
var nextMonthDay = 1
while currentWeek.count < 7 {
var components = nextMonthComponents
components.day = nextMonthDay
currentWeek.append(components)
nextMonthDay += 1
}
weeks.append(currentWeek)
if weeks.count < 6 {
currentWeek = []
for _ in 1...7 {
var components = nextMonthComponents
components.day = nextMonthDay
currentWeek.append(components)
nextMonthDay += 1
}
weeks.append(currentWeek)
}
return weeks
}
@objc private func prevMonth() {
self.month -= 1
if self.month < 1 {
self.month = 12
self.year -= 1
}
self.setup()
}
@objc private func nextMonth() {
self.month += 1
if self.month > 12 {
self.month = 1
self.year += 1
}
self.setup()
}
}
internal class ClockView: NSStackView {
public var clock: Clock_t
open override var intrinsicContentSize: CGSize {
return CGSize(width: self.bounds.width, height: self.bounds.height)
}
private var ready: Bool = false
private let clockView: ClockChart = ClockChart(frame: CGRect(x: 0, y: 0, width: 34, height: 34))
private let nameField: NSTextField = TextView()
private let timeField: NSTextField = TextView()
init(width: CGFloat, clock: Clock_t) {
self.clock = clock
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 44))
self.orientation = .horizontal
self.spacing = 5
self.edgeInsets = NSEdgeInsets(
top: 5,
left: 5,
bottom: 5,
right: 5
)
self.wantsLayer = true
self.layer?.cornerRadius = 2
self.setAccessibilityElement(true)
self.toolTip = "\(clock.name): \(clock.formatted())"
self.clockView.widthAnchor.constraint(equalToConstant: 34).isActive = true
let container: NSStackView = NSStackView()
container.orientation = .vertical
container.spacing = 2
container.distribution = .fillEqually
container.alignment = .left
self.nameField.font = NSFont.systemFont(ofSize: 11, weight: .light)
self.setTZ()
self.nameField.cell?.truncatesLastVisibleLine = true
self.timeField.font = NSFont.systemFont(ofSize: 13, weight: .regular)
self.timeField.stringValue = clock.formatted()
self.timeField.cell?.truncatesLastVisibleLine = true
container.addArrangedSubview(self.nameField)
container.addArrangedSubview(self.timeField)
self.addArrangedSubview(self.clockView)
self.addArrangedSubview(container)
self.update(clock)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateLayer() {
self.layer?.backgroundColor = (isDarkMode ? NSColor(red: 17/255, green: 17/255, blue: 17/255, alpha: 0.25) : NSColor(red: 245/255, green: 245/255, blue: 245/255, alpha: 1)).cgColor
}
private func setTZ() {
self.nameField.stringValue = "\(self.clock.name)"
if let tz = Clock.zones.first(where: { $0.key == self.clock.tz }), tz.key != "local" {
self.nameField.stringValue += " (\(tz.value))"
}
}
public func update(_ newClock: Clock_t) {
if self.clock.tz != newClock.tz || self.clock.name != newClock.name {
self.clock = newClock
self.setTZ()
}
if (self.window?.isVisible ?? false) || !self.ready {
self.timeField.stringValue = newClock.formatted()
if let value = newClock.value {
self.clockView.setValue(value.convertToTimeZone(TimeZone(from: newClock.tz)))
}
self.ready = true
}
}
}
internal class ClockChart: NSView {
private var color: NSColor = SColor.systemAccent.additional as! NSColor
private let calendar = Calendar.current
private var hour: Int = 0
private var minute: Int = 0
private var second: Int = 0
private let hourLayer = CALayer()
private let minuteLayer = CALayer()
private let secondsLayer = CALayer()
private let pinLayer = CAShapeLayer()
override init(frame: CGRect = NSRect.zero) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
public override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let context = NSGraphicsContext.current!.cgContext
context.saveGState()
context.setFillColor(NSColor.controlBackgroundColor.cgColor)
context.setStrokeColor((isDarkMode ? NSColor.darkGray : NSColor.lightGray).cgColor)
context.setLineWidth(1)
context.addEllipse(in: CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height))
context.drawPath(using: .fillStroke)
context.restoreGState()
let anchor = CGPoint(x: 0.5, y: 0)
let center = CGPoint(x: self.frame.size.width / 2, y: self.frame.size.height / 2)
let hourAngle: CGFloat = CGFloat(Double(hour) * (360.0 / 12.0)) + CGFloat(Double(minute) * (1.0 / 60.0) * (360.0 / 12.0))
let minuteAngle: CGFloat = CGFloat(minute) * CGFloat(360.0 / 60.0)
let secondsAngle: CGFloat = CGFloat(self.second) * CGFloat(360.0 / 60.0)
self.hourLayer.backgroundColor = NSColor.labelColor.cgColor
self.hourLayer.anchorPoint = anchor
self.hourLayer.position = center
self.hourLayer.cornerRadius = 2
self.hourLayer.bounds = CGRect(x: 0, y: 0, width: 2, height: self.frame.size.width / 2 - 4)
self.hourLayer.transform = CATransform3DMakeRotation(-hourAngle / 180 * CGFloat(Double.pi), 0, 0, 1)
self.layer?.addSublayer(self.hourLayer)
self.minuteLayer.backgroundColor = NSColor.secondaryLabelColor.cgColor
self.minuteLayer.anchorPoint = anchor
self.minuteLayer.position = center
self.minuteLayer.cornerRadius = 2
self.minuteLayer.bounds = CGRect(x: 0, y: 0, width: 2, height: self.frame.size.width / 2 - 2)
self.minuteLayer.transform = CATransform3DMakeRotation(-minuteAngle / 180 * CGFloat(Double.pi), 0, 0, 1)
self.layer?.addSublayer(self.minuteLayer)
self.secondsLayer.backgroundColor = NSColor.red.cgColor
self.secondsLayer.anchorPoint = anchor
self.secondsLayer.position = center
self.secondsLayer.cornerRadius = 1
self.secondsLayer.bounds = CGRect(x: 0, y: 0, width: 1, height: self.frame.size.width / 2 - 1)
self.secondsLayer.transform = CATransform3DMakeRotation(-secondsAngle / 180 * CGFloat(Double.pi), 0, 0, 1)
self.layer?.addSublayer(self.secondsLayer)
self.pinLayer.fillColor = NSColor.controlBackgroundColor.cgColor
self.pinLayer.strokeColor = (isDarkMode ? NSColor.darkGray : NSColor.lightGray).cgColor
self.pinLayer.anchorPoint = anchor
self.pinLayer.path = CGMutablePath(roundedRect: CGRect(
x: center.x - 3 / 2,
y: center.y - 3 / 2,
width: 3,
height: 3
), cornerWidth: 4, cornerHeight: 4, transform: nil)
self.layer?.addSublayer(self.pinLayer)
}
public func setValue(_ value: Date) {
self.hour = self.calendar.component(.hour, from: value)
self.minute = self.calendar.component(.minute, from: value)
self.second = self.calendar.component(.second, from: value)
DispatchQueue.main.async(execute: {
self.display()
})
}
}
private class OrderTableView: NSView, NSTableViewDelegate, NSTableViewDataSource {
private let scrollView = NSScrollView()
private let tableView = NSTableView()
private var dragDropType = NSPasteboard.PasteboardType(rawValue: "\(Bundle.main.bundleIdentifier!).sensors-row")
public var reorderCallback: () -> Void = {}
public var list: [Clock_t] = []
init() {
super.init(frame: NSRect.zero)
self.wantsLayer = true
self.layer?.cornerRadius = 3
self.scrollView.translatesAutoresizingMaskIntoConstraints = false
self.scrollView.documentView = self.tableView
self.scrollView.hasHorizontalScroller = false
self.scrollView.hasVerticalScroller = true
self.scrollView.autohidesScrollers = true
self.scrollView.backgroundColor = NSColor.clear
self.scrollView.drawsBackground = true
self.tableView.frame = self.scrollView.bounds
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.backgroundColor = NSColor.clear
self.tableView.columnAutoresizingStyle = .firstColumnOnlyAutoresizingStyle
self.tableView.registerForDraggedTypes([dragDropType])
self.tableView.gridColor = .gridColor
self.tableView.gridStyleMask = [.solidVerticalGridLineMask, .solidHorizontalGridLineMask]
self.tableView.style = .plain
let nameColumn = NSTableColumn(identifier: nameColumnID)
nameColumn.headerCell.title = localizedString("Name")
nameColumn.headerCell.alignment = .center
let statusColumn = NSTableColumn(identifier: statusColumnID)
statusColumn.headerCell.title = ""
statusColumn.width = 16
self.tableView.addTableColumn(nameColumn)
self.tableView.addTableColumn(statusColumn)
self.addSubview(self.scrollView)
NSLayoutConstraint.activate([
self.scrollView.leftAnchor.constraint(equalTo: self.leftAnchor),
self.scrollView.rightAnchor.constraint(equalTo: self.rightAnchor),
self.scrollView.topAnchor.constraint(equalTo: self.topAnchor),
self.scrollView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
self.heightAnchor.constraint(equalToConstant: 120)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update() {
self.tableView.reloadData()
}
func numberOfRows(in tableView: NSTableView) -> Int {
return self.list.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
if !self.list.indices.contains(row) { return nil }
let item = self.list[row]
let cell = NSTableCellView()
switch tableColumn?.identifier {
case nameColumnID:
let text: NSTextField = NSTextField()
text.drawsBackground = false
text.isBordered = false
text.isEditable = false
text.isSelectable = false
text.translatesAutoresizingMaskIntoConstraints = false
text.identifier = NSUserInterfaceItemIdentifier(item.name)
text.stringValue = item.name
text.sizeToFit()
cell.addSubview(text)
NSLayoutConstraint.activate([
text.widthAnchor.constraint(equalTo: cell.widthAnchor),
text.centerYAnchor.constraint(equalTo: cell.centerYAnchor)
])
case statusColumnID:
let button: NSButton = NSButton(frame: NSRect(x: 0, y: 5, width: 10, height: 10))
button.identifier = NSUserInterfaceItemIdentifier("\(row)")
button.setButtonType(.switch)
button.state = item.popupState ? .on : .off
button.action = #selector(self.toggleClock)
button.title = ""
button.isBordered = false
button.isTransparent = false
button.target = self
button.sizeToFit()
cell.addSubview(button)
default: break
}
return cell
}
func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
let item = NSPasteboardItem()
item.setString(String(row), forType: self.dragDropType)
return item
}
func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation {
if dropOperation == .above {
return .move
}
return []
}
func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableView.DropOperation) -> Bool {
var oldIndexes = [Int]()
info.enumerateDraggingItems(options: [], for: tableView, classes: [NSPasteboardItem.self], searchOptions: [:]) { dragItem, _, _ in
if let str = (dragItem.item as! NSPasteboardItem).string(forType: self.dragDropType), let index = Int(str) {
oldIndexes.append(index)
}
}
var oldIndexOffset = 0
var newIndexOffset = 0
tableView.beginUpdates()
for oldIndex in oldIndexes {
if oldIndex < row {
let currentIdx = oldIndex + oldIndexOffset
let newIdx = row - 1
self.list[currentIdx].popupIndex = newIdx
self.list[newIdx].popupIndex = currentIdx
oldIndexOffset -= 1
} else {
let currentIdx = oldIndex
let newIdx = row + newIndexOffset
self.list[currentIdx].popupIndex = newIdx
self.list[newIdx].popupIndex = currentIdx
newIndexOffset += 1
}
self.list = self.list.sorted(by: { $0.popupIndex < $1.popupIndex })
self.reorderCallback()
tableView.reloadData()
}
tableView.endUpdates()
return true
}
@objc private func toggleClock(_ sender: NSButton) {
guard let id = sender.identifier, let i = Int(id.rawValue) else { return }
self.list[i].popupState = sender.state == NSControl.StateValue.on
}
}
================================================
FILE: Modules/Clock/portal.swift
================================================
//
// portal.swift
// Clock
//
// Created by Serhiy Mytrovtsiy on 28/12/2023
// Using Swift 5.0
// Running on macOS 14.2
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import AppKit
import Kit
public class Portal: NSStackView, Portal_p {
public var name: String
private let container = ScrollableStackView()
private var initialized: Bool = false
private var list: [Clock_t] = []
init(_ module: ModuleType, list: [Clock_t]) {
self.name = module.stringValue
super.init(frame: NSRect( x: 0, y: 0, width: Constants.Popup.width, height: Constants.Popup.portalHeight))
self.wantsLayer = true
self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
self.layer?.cornerRadius = 3
self.orientation = .vertical
self.distribution = .fill
self.spacing = Constants.Popup.spacing*2
self.edgeInsets = NSEdgeInsets(
top: Constants.Popup.spacing*2,
left: Constants.Popup.spacing*2,
bottom: Constants.Popup.spacing*2,
right: Constants.Popup.spacing*2
)
self.container.stackView.spacing = 0
self.container.widthAnchor.constraint(equalToConstant: Constants.Popup.width).isActive = true
self.addArrangedSubview(PortalHeader(name))
self.addArrangedSubview(self.container)
self.callback(list)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func updateLayer() {
self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
}
public func callback(_ list: [Clock_t]) {
var sorted = list.sorted(by: { $0.popupIndex < $1.popupIndex })
var views = self.container.stackView.subviews.filter{ $0 is ClockView }.compactMap{ $0 as? ClockView }
sorted = sorted.filter({ $0.popupState })
if sorted.count < views.count && !views.isEmpty {
views.forEach{ $0.removeFromSuperview() }
views = []
}
var width: CGFloat = self.frame.width - self.edgeInsets.left - self.edgeInsets.right
if sorted.count > 2 {
width -= self.container.scrollWidth ?? Constants.Popup.margins
}
if sorted.count != views.count {
views.forEach { c in
c.widthAnchor.constraint(equalToConstant: width).isActive = true
}
}
sorted.forEach { (c: Clock_t) in
if let view = views.first(where: { $0.clock.id == c.id }) {
view.update(c)
} else {
self.container.stackView.addArrangedSubview(ClockView(width: width, clock: c))
}
}
self.list = sorted
}
}
private func setFullWidth(_ view: NSView, width: CGFloat) {
if let widthConstraint = view.constraints.first(where: { $0.firstAttribute == .width }) {
widthConstraint.constant = width
} else {
view.widthAnchor.constraint(equalToConstant: width).isActive = true
}
}
================================================
FILE: Modules/Clock/reader.swift
================================================
//
// reader.swift
// Stats
//
// Created by Serhiy Mytrovtsiy on 05/03/2026
// Using Swift 6.0
// Running on macOS 26.3
//
// Copyright © 2026 Serhiy Mytrovtsiy. All rights reserved.
//
import Foundation
import Kit
internal class ClockReader: Reader {
private let title: String = ModuleType.clock.stringValue
private let queue = DispatchQueue(label: "eu.exelban.Stats.Clock.ntp.sync", qos: .default)
private var _offset: TimeInterval = 0
private var offset: TimeInterval {
get { self.queue.sync { self._offset } }
set { self.queue.sync { self._offset = newValue } }
}
private var now: Date { Date().addingTimeInterval(self.offset) }
private var ntpSync: Bool {
get { Store.shared.bool(key: "\(self.title)_ntpSync", defaultValue: false) }
set { Store.shared.set(key: "\(self.title)_ntpSync", value: newValue) }
}
private var ntpServer: String {
get { Store.shared.string(key: "\(self.title)_ntpServer", defaultValue: "pool.ntp.org") }
set { Store.shared.set(key: "\(self.title)_ntpServer", value: newValue) }
}
public override func setup() {
self.alignToSecondBoundary = true
self.syncWithNTP()
}
public override func read() {
let date = self.ntpSync ? self.now : Date()
self.callback(date)
if Calendar.current.component(.second, from: date) == 0 {
self.syncWithNTP()
}
}
private func syncWithNTP() {
guard self.ntpSync else {
self.offset = 0
return
}
let server = self.ntpServer
self.queue.async { [weak self] in
guard let self else { return }
guard let serverDate = self.requestTime(server: server) else { return }
let newOffset = serverDate.timeIntervalSince(Date())
self._offset = newOffset
self.alignOffset = newOffset
}
}
private func requestTime(server: String, timeout: TimeInterval = 2.0) -> Date? {
let host = CFHostCreateWithName(nil, server as CFString).takeRetainedValue()
var resolved: DarwinBoolean = false
let started = CFHostStartInfoResolution(host, .addresses, nil)
guard started else { return nil }
guard
let unmanaged = CFHostGetAddressing(host, &resolved),
resolved.boolValue,
let addresses = unmanaged.takeUnretainedValue() as? [Data],
let first = addresses.first
else { return nil }
let socketFD = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
guard socketFD >= 0 else { return nil }
defer { close(socketFD) }
var tv = timeval(tv_sec: Int(timeout), tv_usec: 0)
setsockopt(socketFD, SOL_SOCKET, SO_RCVTIMEO, &tv, socklen_t(MemoryLayout.size))
var addrStorage = sockaddr_storage()
first.withUnsafeBytes { raw in
guard let base = raw.baseAddress else { return }
memcpy(&addrStorage, base, min(raw.count, MemoryLayout.size))
}
guard addrStorage.ss_family == sa_family_t(AF_INET) else { return nil }
withUnsafeMutablePointer(to: &addrStorage) {
$0.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { p in
p.pointee.sin_port = in_port_t(123).bigEndian
}
}
var packet = Data(count: 48)
packet[0] = 0x1B
let sent = packet.withUnsafeBytes { ptr in
withUnsafePointer(to: &addrStorage) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
sendto(socketFD, ptr.baseAddress, ptr.count, 0, sa, socklen_t(MemoryLayout.size))
}
}
}
guard sent == 48 else { return nil }
var recvBuf = Data(count: 48)
let received = recvBuf.withUnsafeMutableBytes { ptr in
recv(socketFD, ptr.baseAddress, ptr.count, 0)
}
guard received >= 48 else { return nil }
let seconds1900: UInt32 = recvBuf.withUnsafeBytes { ptr in
let b = ptr.bindMemory(to: UInt8.self)
return (UInt32(b[40]) << 24) | (UInt32(b[41]) << 16) | (UInt32(b[42]) << 8) | UInt32(b[43])
}
return Date(timeIntervalSince1970: TimeInterval(seconds1900) - 2_208_988_800)
}
}
================================================
FILE: Modules/Clock/settings.swift
================================================
//
// settings.swift
// Clock
//
// Created by Serhiy Mytrovtsiy on 24/03/2023
// Using Swift 5.0
// Running on macOS 13.2
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
let nameColumnID = NSUserInterfaceItemIdentifier(rawValue: "name")
let formatColumnID = NSUserInterfaceItemIdentifier(rawValue: "format")
let tzColumnID = NSUserInterfaceItemIdentifier(rawValue: "tz")
let statusColumnID = NSUserInterfaceItemIdentifier(rawValue: "status")
internal class Settings: NSStackView, Settings_v, NSTableViewDelegate, NSTableViewDataSource, NSTextFieldDelegate {
public var callback: (() -> Void) = {}
private var cachedList: [Clock_t] = []
private var list: [Clock_t] {
get { self.cachedList }
set {
self.cachedList = newValue
if newValue.isEmpty {
Store.shared.remove("\(self.title)_list")
} else {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(newValue){
Store.shared.set(key: "\(self.title)_list", value: encoded)
}
}
}
}
private var title: String
private var selectedRow: Int = -1
private let scrollView = NSScrollView()
private let tableView = NSTableView()
private var footerView: NSStackView? = nil
private var deleteButton: NSButton? = nil
private var ntpSync: Bool = false
public init(_ module: ModuleType) {
self.title = module.stringValue
self.ntpSync = Store.shared.bool(key: "\(self.title)_ntpSync", defaultValue: self.ntpSync)
super.init(frame: NSRect.zero)
if let objects = Store.shared.data(key: "\(self.title)_list") {
let decoder = JSONDecoder()
if let objectsDecoded = try? decoder.decode(Array.self, from: objects) as [Clock_t] {
self.cachedList = objectsDecoded
}
}
if self.cachedList.isEmpty {
self.cachedList = [Clock.local]
}
self.orientation = .vertical
self.distribution = .gravityAreas
self.spacing = 0
self.scrollView.documentView = self.tableView
self.scrollView.hasHorizontalScroller = false
self.scrollView.hasVerticalScroller = true
self.scrollView.autohidesScrollers = true
self.scrollView.backgroundColor = NSColor.clear
self.scrollView.drawsBackground = true
self.tableView.frame = self.scrollView.bounds
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.allowsMultipleSelection = false
self.tableView.focusRingType = .none
self.tableView.gridColor = .gridColor
self.tableView.columnAutoresizingStyle = .firstColumnOnlyAutoresizingStyle
self.tableView.allowsColumnResizing = false
self.tableView.gridStyleMask = [.solidVerticalGridLineMask, .solidHorizontalGridLineMask]
self.tableView.usesAlternatingRowBackgroundColors = true
self.tableView.style = .plain
self.tableView.rowHeight = 32
let nameColumn = NSTableColumn(identifier: nameColumnID)
nameColumn.headerCell.title = localizedString("Name")
nameColumn.headerCell.alignment = .center
let formatColumn = NSTableColumn(identifier: formatColumnID)
formatColumn.headerCell.title = localizedString("Format")
formatColumn.headerCell.alignment = .center
formatColumn.width = 160
let tzColumn = NSTableColumn(identifier: tzColumnID)
tzColumn.headerCell.title = localizedString("Time zone")
tzColumn.headerCell.alignment = .center
tzColumn.width = 132
let statusColumn = NSTableColumn(identifier: statusColumnID)
statusColumn.headerCell.title = ""
statusColumn.width = 16
self.tableView.addTableColumn(nameColumn)
self.tableView.addTableColumn(formatColumn)
self.tableView.addTableColumn(tzColumn)
self.tableView.addTableColumn(statusColumn)
let separator = NSBox()
separator.boxType = .separator
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Sync with NTP server"), component: switchView(
action: #selector(self.toggleNTPSync),
state: self.ntpSync
))
]))
self.addArrangedSubview(self.scrollView)
self.addArrangedSubview(separator)
self.addArrangedSubview(self.footer())
var hight: CGFloat = 254
if #available(macOS 26.0, *) {
hight = 248
}
NSLayoutConstraint.activate([
self.scrollView.heightAnchor.constraint(equalToConstant: hight)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func footer() -> NSView {
let view = NSStackView()
view.heightAnchor.constraint(equalToConstant: 27).isActive = true
view.spacing = 4
view.orientation = .horizontal
var addButton: NSButton {
let btn = NSButton()
btn.widthAnchor.constraint(equalToConstant: 27).isActive = true
btn.heightAnchor.constraint(equalToConstant: 27).isActive = true
btn.bezelStyle = .rounded
btn.image = iconFromSymbol(name: "plus", scale: .medium)
btn.action = #selector(self.addNewClock)
btn.target = self
btn.toolTip = localizedString("Add new clock")
btn.focusRingType = .none
return btn
}
var deleteButton: NSButton {
let btn = NSButton()
btn.widthAnchor.constraint(equalToConstant: 27).isActive = true
btn.heightAnchor.constraint(equalToConstant: 27).isActive = true
btn.bezelStyle = .rounded
btn.image = iconFromSymbol(name: "minus", scale: .medium)
btn.action = #selector(self.deleteClock)
btn.target = self
btn.toolTip = localizedString("Delete selected clock")
btn.focusRingType = .none
return btn
}
self.deleteButton = deleteButton
view.addArrangedSubview(addButton)
view.addArrangedSubview(NSView())
let helpBtn = NSButton()
helpBtn.widthAnchor.constraint(equalToConstant: 27).isActive = true
helpBtn.heightAnchor.constraint(equalToConstant: 27).isActive = true
helpBtn.bezelStyle = .helpButton
helpBtn.title = ""
helpBtn.action = #selector(self.openFormatHelp)
helpBtn.target = self
helpBtn.toolTip = localizedString("Help with datetime format")
view.addArrangedSubview(helpBtn)
self.footerView = view
return view
}
func load(widgets: [Kit.widget_t]) {
self.tableView.reloadData()
}
func numberOfRows(in tableView: NSTableView) -> Int {
return self.list.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard let id = tableColumn?.identifier else { return nil }
let cell = NSTableCellView()
let item = self.list[row]
switch id {
case nameColumnID, formatColumnID:
let text: NSTextField = NSTextField()
text.identifier = id
text.drawsBackground = false
text.isBordered = false
text.sizeToFit()
text.delegate = self
text.stringValue = id == nameColumnID ? item.name : item.format
text.translatesAutoresizingMaskIntoConstraints = false
cell.addSubview(text)
text.widthAnchor.constraint(equalTo: cell.widthAnchor).isActive = true
text.centerYAnchor.constraint(equalTo: cell.centerYAnchor).isActive = true
case tzColumnID:
let select: NSPopUpButton = selectView(action: #selector(self.toggleTZ), items: Clock.zones, selected: item.tz)
select.identifier = NSUserInterfaceItemIdentifier("\(row)")
select.sizeToFit()
select.preferredEdge = .maxX
select.translatesAutoresizingMaskIntoConstraints = false
select.widthAnchor.constraint(lessThanOrEqualToConstant: 132).isActive = true
cell.addSubview(select)
case statusColumnID:
let button: NSButton = NSButton(frame: NSRect(x: 0, y: 8, width: 10, height: 10))
button.identifier = NSUserInterfaceItemIdentifier("\(row)")
button.setButtonType(.switch)
button.state = item.enabled ? .on : .off
button.action = #selector(self.toggleClock)
button.title = ""
button.isBordered = false
button.isTransparent = false
button.target = self
button.sizeToFit()
cell.addSubview(button)
default: break
}
return cell
}
func controlTextDidChange(_ notification: Notification) {
if let textField = notification.object as? NSTextField, let id = textField.identifier {
let i = self.tableView.selectedRow
switch id {
case nameColumnID:
self.list[i].name = textField.stringValue
case formatColumnID:
self.list[i].format = textField.stringValue
default: return
}
}
}
func tableViewSelectionDidChange(_ notification: Notification) {
if self.tableView.selectedRow == -1 {
self.deleteButton?.removeFromSuperview()
} else {
if let btn = self.deleteButton {
self.footerView?.insertArrangedSubview(btn, at: 1)
}
}
}
@objc private func toggleTZ(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let id = sender.identifier, let i = Int(id.rawValue) else { return }
self.list[i].tz = key
}
@objc private func toggleClock(_ sender: NSButton) {
guard let id = sender.identifier, let i = Int(id.rawValue) else { return }
self.list[i].enabled = sender.state == NSControl.StateValue.on
}
@objc private func addNewClock(_ sender: Any) {
self.list.append(Clock_t(name: "\(localizedString("Clock")) \(self.list.count)", format: Clock.local.format, tz: Clock.local.tz))
self.tableView.reloadData()
}
@objc private func deleteClock(_ sender: Any) {
guard self.tableView.selectedRow != -1 else { return }
self.list.remove(at: self.tableView.selectedRow)
self.tableView.reloadData()
self.deleteButton?.removeFromSuperview()
}
@objc private func openFormatHelp(_ sender: NSButton) {
NSWorkspace.shared.open(URL(string: "https://www.nsdateformatter.com")!)
}
@objc func toggleNTPSync(_ sender: NSControl) {
self.ntpSync = controlState(sender)
Store.shared.set(key: "\(self.title)_ntpSync", value: self.ntpSync)
self.callback()
}
}
================================================
FILE: Modules/Disk/Info.plist
================================================
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
1.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSHumanReadableCopyright
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
================================================
FILE: Modules/Disk/config.plist
================================================
Name
Disk
State
Symbol
opticaldiscdrive
Widgets
label
Default
Title
SSD
Order
0
mini
Default
Title
SSD
Preview
Title
SSD
Value
0.36
Unsupported colors
pressure
Order
1
bar_chart
Default
Title
SSD
Label
Box
Color
systemAccent
Preview
Label
Box
Value
0.36
Unsupported colors
pressure
cluster
Order
2
pie_chart
Default
Order
3
memory
Default
Preview
Value
47.85 GB, 184.84 GB
Order
4
speed
Default
Icon
chars
Symbols
Output
W
Input
R
Words
Output
Write speed
Input
Read speed
Order
5
network_chart
Default
Title
SSD
Order
6
Unsupported colors
utilization
pressure
system
text
Default
Order
7
Settings
popup
notifications
================================================
FILE: Modules/Disk/header.h
================================================
//
// Header.h
// Disk
//
// Created by Serhiy Mytrovtsiy on 25/03/2023
// Using Swift 5.0
// Running on macOS 13.2
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
#include
#include
struct nvme_smart_log {
UInt8 critical_warning;
UInt8 temperature[2];
UInt8 avail_spare;
UInt8 spare_thresh;
UInt8 percent_used;
UInt8 rsvd6[26];
UInt8 data_units_read[16];
UInt8 data_units_written[16];
UInt8 host_reads[16];
UInt8 host_writes[16];
UInt8 ctrl_busy_time[16];
UInt32 power_cycles[4];
UInt32 power_on_hours[4];
UInt32 unsafe_shutdowns[4];
UInt32 media_errors[4];
UInt16 temp_sensor[8];
UInt32 thm_temp1_trans_count;
UInt32 thm_temp2_trans_count;
UInt32 thm_temp1_total_time;
UInt32 thm_temp2_total_time;
UInt8 rsvd232[280];
};
typedef struct IONVMeSMARTInterface {
IUNKNOWN_C_GUTS;
UInt16 version;
UInt16 revision;
IOReturn ( *SMARTReadData )( void * interface, struct nvme_smart_log * NVMeSMARTData );
} IONVMeSMARTInterface;
================================================
FILE: Modules/Disk/main.swift
================================================
//
// main.swift
// Disk
//
// Created by Serhiy Mytrovtsiy on 07/05/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
import WidgetKit
public struct stats: Codable {
var read: Int64 = 0
var write: Int64 = 0
var readBytes: Int64 = 0
var writeBytes: Int64 = 0
}
public struct smart_t: Codable {
var temperature: Int = 0
var life: Int = 0
var totalRead: Int64 = 0
var totalWritten: Int64 = 0
var powerCycles: Int = 0
var powerOnHours: Int = 0
}
public struct drive: Codable {
var parent: io_object_t = 0
var uuid: String = ""
var mediaName: String = ""
var BSDName: String = ""
var root: Bool = false
var removable: Bool = false
var model: String = ""
var path: URL?
var connectionType: String = ""
var fileSystem: String = ""
var size: Int64 = 1
var free: Int64 = 0
var activity: stats = stats()
var smart: smart_t? = nil
public var percentage: Double {
let total = self.size
let free = self.free
var usedSpace = total - free
if usedSpace < 0 {
usedSpace = 0
}
return Double(usedSpace) / Double(total)
}
public var popupState: Bool {
Store.shared.bool(key: "Disk_\(self.uuid)_popup", defaultValue: true)
}
public func remote() -> String {
return "\(self.uuid),\(self.size),\(self.size-self.free),\(self.free),\(self.activity.read),\(self.activity.write)"
}
}
public class Disks: Codable, RemoteType {
private var queue: DispatchQueue = DispatchQueue(label: "eu.exelban.Stats.Disk.SynchronizedArray")
private var _array: [drive] = []
public var array: [drive] {
get { self.queue.sync { self._array } }
set { self.queue.sync { self._array = newValue } }
}
enum CodingKeys: String, CodingKey {
case array
}
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.array = try container.decode(Array.self, forKey: CodingKeys.array)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(array, forKey: .array)
}
init() {}
public var count: Int {
var result = 0
self.queue.sync { result = self.array.count }
return result
}
// swiftlint:disable empty_count
public var isEmpty: Bool {
self.count == 0
}
// swiftlint:enable empty_count
public func first(where predicate: (drive) -> Bool) -> drive? {
return self.array.first(where: predicate)
}
public func index(where predicate: (drive) -> Bool) -> Int? {
return self.array.firstIndex(where: predicate)
}
public func map(_ transform: (drive) -> ElementOfResult?) -> [ElementOfResult] {
return self.array.compactMap(transform)
}
public func reversed() -> [drive] {
return self.array.reversed()
}
func forEach(_ body: (drive) -> Void) {
self.array.forEach(body)
}
public func append( _ element: drive) {
if !self.array.contains(where: {$0.BSDName == element.BSDName}) {
self.array.append(element)
}
}
public func remove(at index: Int) {
self.array.remove(at: index)
}
public func sort() {
self.array.sort{ $1.removable }
}
func updateFreeSize(_ idx: Int, newValue: Int64) {
self.array[idx].free = newValue
}
func updateReadWrite(_ idx: Int, read: Int64, write: Int64) {
self.array[idx].activity.readBytes = read
self.array[idx].activity.writeBytes = write
}
func updateRead(_ idx: Int, newValue: Int64) {
self.array[idx].activity.read = newValue
}
func updateWrite(_ idx: Int, newValue: Int64) {
self.array[idx].activity.write = newValue
}
func updateSMARTData(_ idx: Int, smart: smart_t?) {
self.array[idx].smart = smart
}
public func remote() -> Data? {
var string = "\(self.array.count),"
for (i, v) in self.array.enumerated() {
string += v.remote()
if i != self.array.count {
string += ","
}
}
string += "$"
return string.data(using: .utf8)
}
}
public struct Disk_process: Process_p, Codable {
public var base: DataSizeBase {
DataSizeBase(rawValue: Store.shared.string(key: "\(ModuleType.disk.stringValue)_base", defaultValue: "byte")) ?? .byte
}
public var pid: Int
public var name: String
public var icon: NSImage {
if let app = NSRunningApplication(processIdentifier: pid_t(self.pid)) {
return app.icon ?? Constants.defaultProcessIcon
}
return Constants.defaultProcessIcon
}
var read: Int
var write: Int
init(pid: Int, name: String, read: Int, write: Int) {
self.pid = pid
self.name = name
self.read = read
self.write = write
if let app = NSRunningApplication(processIdentifier: pid_t(pid)) {
if let name = app.localizedName {
self.name = name
}
}
}
}
public class Disk: Module {
private let popupView: Popup = Popup(.disk)
private let settingsView: Settings = Settings(.disk)
private let portalView: Portal = Portal(.disk)
private let notificationsView: Notifications = Notifications(.disk)
private var capacityReader: CapacityReader?
private var activityReader: ActivityReader?
private var processReader: ProcessReader?
private var selectedDisk: String = ""
private var textValue: String {
Store.shared.string(key: "\(self.name)_textWidgetValue", defaultValue: "$capacity.free/$capacity.total")
}
private var systemWidgetsUpdatesState: Bool {
self.userDefaults?.bool(forKey: "systemWidgetsUpdates_state") ?? false
}
public init() {
super.init(
moduleType: .disk,
popup: self.popupView,
settings: self.settingsView,
portal: self.portalView,
notifications: self.notificationsView
)
guard self.available else { return }
self.capacityReader = CapacityReader(.disk) { [weak self] value in
if let value {
self?.capacityCallback(value)
}
}
self.activityReader = ActivityReader(.disk) { [weak self] value in
if let value {
self?.activityCallback(value)
}
}
self.processReader = ProcessReader(.disk) { [weak self] value in
if let list = value {
self?.popupView.processCallback(list)
}
}
self.popupView.refreshCallback = { [weak self] uuid in
self?.capacityReader?.resetPurgableSpace(for: uuid)
self?.capacityReader?.read()
}
self.selectedDisk = Store.shared.string(key: "\(ModuleType.disk.stringValue)_disk", defaultValue: self.selectedDisk)
self.settingsView.selectedDiskHandler = { [weak self] value in
self?.selectedDisk = value
self?.capacityReader?.read()
}
self.settingsView.callback = { [weak self] in
self?.capacityReader?.read()
}
self.settingsView.setInterval = { [weak self] value in
self?.capacityReader?.setInterval(value)
}
self.settingsView.callbackWhenUpdateNumberOfProcesses = { [weak self] in
self?.popupView.numberOfProcessesUpdated()
DispatchQueue.global(qos: .background).async {
self?.processReader?.read()
}
}
self.setReaders([self.capacityReader, self.activityReader, self.processReader])
}
private func capacityCallback(_ value: Disks) {
guard self.enabled else { return }
DispatchQueue.main.async(execute: {
self.popupView.capacityCallback(value)
})
self.settingsView.setList(value)
guard let d = value.first(where: { $0.mediaName == self.selectedDisk }) ?? value.first(where: { $0.root }) else {
return
}
self.portalView.utilizationCallback(d)
self.notificationsView.utilizationCallback(d.percentage)
self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: SWidget) in
switch w.item {
case let widget as Mini: widget.setValue(d.percentage)
case let widget as BarChart: widget.setValue([[ColorValue(d.percentage)]])
case let widget as MemoryWidget:
widget.setValue((DiskSize(d.free).getReadableMemory(), DiskSize(d.size - d.free).getReadableMemory()), usedPercentage: d.percentage)
case let widget as PieChart:
widget.setValue([
circle_segment(value: d.percentage, color: NSColor.systemBlue)
])
case let widget as TextWidget:
var text = "\(self.textValue)"
let pairs = TextWidget.parseText(text)
pairs.forEach { pair in
var replacement: String? = nil
switch pair.key {
case "$capacity":
switch pair.value {
case "total": replacement = DiskSize(d.size).getReadableMemory()
case "used": replacement = DiskSize(d.size - d.free).getReadableMemory()
case "free": replacement = DiskSize(d.free).getReadableMemory()
default: return
}
case "$percentage":
var percentage: Int
switch pair.value {
case "used": percentage = Int((Double(d.size - d.free) / Double(d.size)) * 100)
case "free": percentage = Int((Double(d.free) / Double(d.size)) * 100)
default: return
}
replacement = "\(percentage < 0 ? 0 : percentage)%"
default: return
}
if let replacement {
let key = pair.value.isEmpty ? pair.key : "\(pair.key).\(pair.value)"
text = text.replacingOccurrences(of: key, with: replacement)
}
}
widget.setValue(text)
default: break
}
}
if self.systemWidgetsUpdatesState {
if isWidgetActive(self.userDefaults, [Disk_entry.kind, "UnitedWidget"]), let blobData = try? JSONEncoder().encode(d) {
self.userDefaults?.set(blobData, forKey: "Disk@CapacityReader")
}
WidgetCenter.shared.reloadTimelines(ofKind: Disk_entry.kind)
WidgetCenter.shared.reloadTimelines(ofKind: "UnitedWidget")
}
}
private func activityCallback(_ value: Disks) {
guard self.enabled else { return }
DispatchQueue.main.async(execute: {
self.popupView.activityCallback(value)
})
guard let d = value.first(where: { $0.mediaName == self.selectedDisk }) ?? value.first(where: { $0.root }) else {
return
}
self.portalView.activityCallback(d)
self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: SWidget) in
switch w.item {
case let widget as SpeedWidget:
widget.setValue(input: d.activity.read, output: d.activity.write)
case let widget as NetworkChart:
widget.setValue(upload: Double(d.activity.write), download: Double(d.activity.read))
if self.capacityReader?.interval != 1 {
self.settingsView.setUpdateInterval(value: 1)
}
default: break
}
}
}
}
================================================
FILE: Modules/Disk/notifications.swift
================================================
//
// notifications.swift
// Disk
//
// Created by Serhiy Mytrovtsiy on 05/12/2023
// Using Swift 5.0
// Running on macOS 14.1
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
class Notifications: NotificationsWrapper {
private let utilizationID: String = "usage"
private var utilizationState: Bool = false
private var utilization: Int = 80
public init(_ module: ModuleType) {
super.init(module, [self.utilizationID])
if Store.shared.exist(key: "\(self.module)_notificationLevel") {
let value = Store.shared.string(key: "\(self.module)_notifications_free", defaultValue: "")
if let v = Double(value) {
Store.shared.set(key: "\(self.module)_notifications_utilization_state", value: true)
Store.shared.set(key: "\(self.module)_notifications_utilization_value", value: Int(v*100))
Store.shared.remove("\(self.module)_notificationLevel")
}
}
self.utilizationState = Store.shared.bool(key: "\(self.module)_notifications_utilization_state", defaultValue: self.utilizationState)
self.utilization = Store.shared.int(key: "\(self.module)_notifications_utilization_value", defaultValue: self.utilization)
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Usage"), component: PreferencesSwitch(
action: self.toggleUtilization, state: self.utilizationState, with: StepperInput(self.utilization, callback: self.changeUtilization)
))
]))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func utilizationCallback(_ value: Double) {
let title = localizedString("Disk utilization threshold")
if self.utilizationState {
let subtitle = localizedString("Disk utilization is", "\(Int((value)*100))%")
self.checkDouble(id: self.utilizationID, value: value, threshold: Double(self.utilization)/100, title: title, subtitle: subtitle)
}
}
@objc private func toggleUtilization(_ sender: NSControl) {
self.utilizationState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_utilization_state", value: self.utilizationState)
}
@objc private func changeUtilization(_ newValue: Int) {
self.utilization = newValue
Store.shared.set(key: "\(self.module)_notifications_utilization_value", value: self.utilization)
}
}
================================================
FILE: Modules/Disk/popup.swift
================================================
//
// popup.swift
// Disk
//
// Created by Serhiy Mytrovtsiy on 11/05/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class Popup: PopupWrapper {
public var refreshCallback: ((String) -> Void) = {_ in }
private var readColorState: SColor = .secondBlue
private var readColor: NSColor { self.readColorState.additional as? NSColor ?? NSColor.systemRed }
private var writeColorState: SColor = .secondRed
private var writeColor: NSColor { self.writeColorState.additional as? NSColor ?? NSColor.systemBlue }
private var reverseOrderState: Bool = false
private var disks: NSStackView = {
let view = NSStackView()
view.spacing = Constants.Popup.margins
view.orientation = .vertical
return view
}()
private var processesInitialized: Bool = false
private var numberOfProcesses: Int {
Store.shared.int(key: "\(self.title)_processes", defaultValue: 8)
}
private var processesHeight: CGFloat {
(22*CGFloat(self.numberOfProcesses)) + (self.numberOfProcesses == 0 ? 0 : Constants.Popup.separatorHeight + 22)
}
private var processes: ProcessesView? = nil
private var processesView: NSView? = nil
private let settingsSection = PreferencesSection(label: localizedString("Drives"))
private var lastList: [String] = []
public init(_ module: ModuleType) {
super.init(module, frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
self.readColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_readColor", defaultValue: self.readColorState.key))
self.writeColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_writeColor", defaultValue: self.writeColorState.key))
self.reverseOrderState = Store.shared.bool(key: "\(self.title)_reverseOrder", defaultValue: self.reverseOrderState)
self.orientation = .vertical
self.distribution = .fill
self.spacing = 0
self.addArrangedSubview(self.disks)
self.addArrangedSubview(self.initProcesses())
self.recalculateHeight()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func recalculateHeight() {
var h: CGFloat = 0
h += self.disks.subviews.map({ $0.frame.height + self.disks.spacing }).reduce(0, +) - self.disks.spacing
h += self.processesHeight
if h > 0 && self.frame.size.height != h {
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.sizeCallback?(self.frame.size)
}
}
private func initProcesses() -> NSView {
if self.numberOfProcesses == 0 {
let v = NSView()
self.processesView = v
return v
}
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.processesHeight))
let separator = separatorView(localizedString("Top processes"), origin: NSPoint(x: 0, y: self.processesHeight-Constants.Popup.separatorHeight), width: self.frame.width)
let container: ProcessesView = ProcessesView(
frame: NSRect(x: 0, y: 0, width: self.frame.width, height: separator.frame.origin.y),
values: [(localizedString("Read"), self.readColor), (localizedString("Write"), self.writeColor)],
n: self.numberOfProcesses
)
self.processes = container
view.addSubview(separator)
view.addSubview(container)
self.processesView = view
return view
}
// MARK: - callbacks
internal func capacityCallback(_ value: Disks) {
defer {
let h = self.disks.subviews.map({ $0.bounds.height + self.disks.spacing }).reduce(0, +) - self.disks.spacing
if h > 0 && self.disks.frame.size.height != h {
self.disks.setFrameSize(NSSize(width: self.frame.width, height: h))
self.recalculateHeight()
} else if h < 0 && self.disks.frame.size.height != 0 {
self.disks.setFrameSize(NSSize(width: self.frame.width, height: 0))
self.recalculateHeight()
}
self.lastList = value.array.compactMap{ $0.uuid }
}
if self.settingsSection.contains("empty_view") {
self.settingsSection.delete("empty_view")
}
self.lastList.filter { !value.map { $0.uuid }.contains($0) }.forEach { self.settingsSection.delete($0) }
value.forEach { (drive: drive) in
if !self.settingsSection.contains(drive.uuid) {
let btn = switchView(
action: #selector(self.toggleDisk),
state: drive.popupState
)
btn.identifier = NSUserInterfaceItemIdentifier(drive.uuid)
self.settingsSection.add(PreferencesRow(drive.mediaName, id: drive.uuid, component: btn))
}
}
self.disks.subviews.filter{ $0 is DiskView }.map{ $0 as! DiskView }.forEach { (v: DiskView) in
if !value.array.filter({ $0.popupState }).map({$0.uuid}).contains(v.uuid) {
v.removeFromSuperview()
}
}
value.array.filter({ $0.popupState }).forEach { (drive: drive) in
if let view = self.disks.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }).first(where: { $0.uuid == drive.uuid }) {
view.update(free: drive.free, smart: drive.smart)
} else {
self.disks.addArrangedSubview(DiskView(
width: Constants.Popup.width,
uuid: drive.uuid,
name: drive.mediaName,
size: drive.size,
free: drive.free,
path: drive.path,
smart: drive.smart,
resize: self.recalculateHeight,
refresh: self.refreshCallback
))
}
}
}
internal func activityCallback(_ value: Disks) {
let views = self.disks.subviews.filter{ $0 is DiskView }.map{ $0 as! DiskView }
value.reversed().forEach { (drive: drive) in
if let view = views.first(where: { $0.name == drive.mediaName }) {
view.updateStats(stats: drive.activity)
}
}
}
internal func processCallback(_ list: [Disk_process]) {
DispatchQueue.main.async(execute: {
if !(self.window?.isVisible ?? false) && self.processesInitialized {
return
}
let list = list.map{ $0 }
if list.count != self.processes?.count { self.processes?.clear("-") }
for i in 0.. NSView? {
let view = SettingsContainerView()
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Keyboard shortcut"), component: KeyboardShartcutView(
callback: self.setKeyboardShortcut,
value: self.keyboardShortcut
))
]))
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Write color"), component: selectView(
action: #selector(self.toggleWriteColor),
items: SColor.allColors,
selected: self.writeColorState.key
)),
PreferencesRow(localizedString("Read color"), component: selectView(
action: #selector(self.toggleReadColor),
items: SColor.allColors,
selected: self.readColorState.key
))
]))
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Reverse order"), component: switchView(
action: #selector(self.toggleReverseOrder),
state: self.reverseOrderState
))
]))
let empty = NSView()
empty.identifier = NSUserInterfaceItemIdentifier("empty_view")
self.settingsSection.add(empty)
view.addArrangedSubview(self.settingsSection)
return view
}
@objc private func toggleWriteColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.writeColorState = newValue
Store.shared.set(key: "\(self.title)_writeColor", value: key)
if let color = newValue.additional as? NSColor {
self.processes?.setColor(1, color)
for view in self.disks.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }) {
view.setChartColor(write: color)
}
}
}
@objc private func toggleReadColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.readColorState = newValue
Store.shared.set(key: "\(self.title)_readColor", value: key)
if let color = newValue.additional as? NSColor {
self.processes?.setColor(0, color)
for view in self.disks.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }) {
view.setChartColor(read: color)
}
}
}
@objc private func toggleReverseOrder(_ sender: NSControl) {
self.reverseOrderState = controlState(sender)
for view in self.disks.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }) {
view.setChartReverseOrder(self.reverseOrderState)
}
Store.shared.set(key: "\(self.title)_reverseOrder", value: self.reverseOrderState)
self.display()
}
@objc private func toggleDisk(_ sender: NSControl) {
guard let id = sender.identifier else { return }
Store.shared.set(key: "\(self.title)_\(id.rawValue)_popup", value: controlState(sender))
}
}
internal class DiskView: NSStackView {
internal var sizeCallback: (() -> Void) = {}
internal var refreshCallback: ((String) -> Void) = {_ in }
public var name: String
public var uuid: String
private let width: CGFloat
private let size: Int64
private var nameView: NameView
private var chartView: ChartView
private var barView: BarChartView
private var legendView: LegendView
private var detailsView: DetailsView
private var detailsState: Bool {
get { Store.shared.bool(key: "\(self.uuid)_details", defaultValue: false) }
set { Store.shared.set(key: "\(self.uuid)_details", value: newValue) }
}
init(width: CGFloat, uuid: String, name: String, size: Int64 = 1, free: Int64 = 1, path: URL? = nil, smart: smart_t? = nil, resize: @escaping () -> Void, refresh: @escaping (String) -> Void) {
self.sizeCallback = resize
self.refreshCallback = refresh
self.uuid = uuid
self.name = name
self.width = width
self.size = size
let innerWidth: CGFloat = width - (Constants.Popup.margins * 2)
self.nameView = NameView(width: innerWidth, name: name, size: size, free: free, path: path)
self.chartView = ChartView(width: innerWidth)
self.barView = BarChartView(frame: NSRect(x: 0, y: 0, width: innerWidth, height: 10), horizontal: true)
self.barView.widthAnchor.constraint(equalToConstant: innerWidth).isActive = true
self.barView.heightAnchor.constraint(equalToConstant: 10).isActive = true
if size != 0 {
self.barView.setValue(ColorValue(Double(size - free) / Double(size)))
}
self.legendView = LegendView(width: innerWidth, id: "\(name)_\(path?.absoluteString ?? "")", size: size, free: free)
self.detailsView = DetailsView(width: innerWidth, id: "\(name)_\(path?.absoluteString ?? "")", smart: smart)
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 0))
self.widthAnchor.constraint(equalToConstant: width).isActive = true
self.orientation = .vertical
self.distribution = .fillProportionally
self.spacing = 5
self.edgeInsets = NSEdgeInsets(top: 5, left: 0, bottom: 5, right: 0)
self.wantsLayer = true
self.layer?.cornerRadius = 2
self.nameView.detailsCallback = { [weak self] in
guard let s = self else { return }
s.detailsState = !s.detailsState
s.toggleDetails()
}
self.nameView.refreshCallback = { [weak self] in
guard let uuid = self?.uuid else { return }
self?.refreshCallback(uuid)
}
self.addArrangedSubview(self.nameView)
self.addArrangedSubview(self.chartView)
self.addArrangedSubview(self.barView)
self.addArrangedSubview(self.legendView)
self.addArrangedSubview(self.detailsView)
self.toggleDetails()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateLayer() {
self.layer?.backgroundColor = (isDarkMode ? NSColor(red: 17/255, green: 17/255, blue: 17/255, alpha: 0.25) : NSColor(red: 245/255, green: 245/255, blue: 245/255, alpha: 1)).cgColor
}
public func update(free: Int64, smart: smart_t?) {
self.nameView.update(free: free, read: nil, write: nil)
self.legendView.update(free: free)
if size != 0 {
self.barView.setValue(ColorValue(Double(self.size - free) / Double(self.size)))
}
self.detailsView.update(smart: smart)
}
public func updateStats(stats: stats) {
self.nameView.update(free: nil, read: stats.read, write: stats.write)
self.chartView.update(read: stats.read, write: stats.write)
self.detailsView.update(stats: stats)
}
public func setChartColor(read: NSColor? = nil, write: NSColor? = nil) {
self.chartView.setColors(read: read, write: write)
}
public func setChartReverseOrder(_ newValue: Bool) {
self.chartView.setReverseOrder(newValue)
}
private func toggleDetails() {
if self.detailsState {
self.addArrangedSubview(self.detailsView)
} else {
self.detailsView.removeFromSuperview()
}
let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - 5 + 10
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.sizeCallback()
}
}
internal class NameView: NSStackView {
internal var detailsCallback: (() -> Void) = {}
internal var refreshCallback: (() -> Void) = {}
private let size: Int64
private let uri: URL?
private let finder: URL?
private var ready: Bool = false
private var readState: NSView? = nil
private var writeState: NSView? = nil
private var readColor: NSColor {
SColor.fromString(Store.shared.string(key: "\(ModuleType.disk.stringValue)_readColor", defaultValue: SColor.secondBlue.key)).additional as! NSColor
}
private var writeColor: NSColor {
SColor.fromString(Store.shared.string(key: "\(ModuleType.disk.stringValue)_writeColor", defaultValue: SColor.secondRed.key)).additional as! NSColor
}
public init(width: CGFloat, name: String, size: Int64, free: Int64, path: URL?) {
self.size = size
self.uri = path
self.finder = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.Finder")
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 16))
self.orientation = .horizontal
self.alignment = .centerY
self.spacing = 4
self.toolTip = localizedString("Open disk")
let nameField = NSButton()
nameField.bezelStyle = .inline
nameField.isBordered = false
nameField.contentTintColor = .labelColor
nameField.action = #selector(self.openDisk)
nameField.target = self
nameField.toolTip = name
nameField.title = name
nameField.cell?.truncatesLastVisibleLine = true
let activity: NSStackView = NSStackView()
activity.distribution = .fill
activity.spacing = 2
let readState: NSView = NSView()
readState.widthAnchor.constraint(equalToConstant: 8).isActive = true
readState.heightAnchor.constraint(equalToConstant: 8).isActive = true
readState.wantsLayer = true
readState.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.75).cgColor
readState.layer?.cornerRadius = 4
readState.toolTip = localizedString("Read")
let writeState: NSView = NSView()
writeState.widthAnchor.constraint(equalToConstant: 8).isActive = true
writeState.heightAnchor.constraint(equalToConstant: 8).isActive = true
writeState.wantsLayer = true
writeState.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.75).cgColor
writeState.layer?.cornerRadius = 4
writeState.toolTip = localizedString("Write")
self.readState = readState
self.writeState = writeState
activity.addArrangedSubview(readState)
activity.addArrangedSubview(writeState)
let refreshButton = NSButton()
refreshButton.frame = CGRect(x: (self.frame.width/3)-40, y: 10, width: 15, height: 15)
refreshButton.bezelStyle = .regularSquare
refreshButton.isBordered = false
refreshButton.imageScaling = NSImageScaling.scaleAxesIndependently
refreshButton.contentTintColor = .lightGray
refreshButton.action = #selector(self.refreshDisk)
refreshButton.target = self
refreshButton.toolTip = localizedString("Refresh disk information")
refreshButton.image = Bundle(for: Module.self).image(forResource: "refresh")!
let detailsButton = NSButton()
detailsButton.frame = CGRect(x: (self.frame.width/3)-20, y: 10, width: 15, height: 15)
detailsButton.bezelStyle = .regularSquare
detailsButton.isBordered = false
detailsButton.imageScaling = NSImageScaling.scaleAxesIndependently
detailsButton.contentTintColor = .lightGray
detailsButton.action = #selector(self.toggleDetails)
detailsButton.target = self
detailsButton.toolTip = localizedString("Disk details")
detailsButton.image = Bundle(for: Module.self).image(forResource: "tune")!
self.addArrangedSubview(nameField)
self.addArrangedSubview(activity)
self.addArrangedSubview(NSView())
self.addArrangedSubview(refreshButton)
self.addArrangedSubview(detailsButton)
self.widthAnchor.constraint(equalToConstant: self.frame.width).isActive = true
self.heightAnchor.constraint(equalToConstant: self.frame.height).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(free: Int64?, read: Int64?, write: Int64?) {
if (self.window?.isVisible ?? false) || !self.ready {
if let read = read {
self.readState?.toolTip = "Read: \(Units(bytes: read).getReadableSpeed())"
self.readState?.layer?.backgroundColor = read != 0 ? self.readColor.cgColor : NSColor.lightGray.withAlphaComponent(0.75).cgColor
}
if let write = write {
self.writeState?.toolTip = "Write: \(Units(bytes: write).getReadableSpeed())"
self.writeState?.layer?.backgroundColor = write != 0 ? self.writeColor.cgColor : NSColor.lightGray.withAlphaComponent(0.75).cgColor
}
self.ready = true
}
}
@objc private func openDisk() {
if let uri = self.uri, let finder = self.finder {
NSWorkspace.shared.open([uri], withApplicationAt: finder, configuration: NSWorkspace.OpenConfiguration())
}
}
@objc private func toggleDetails() {
self.detailsCallback()
}
@objc private func refreshDisk() {
self.refreshCallback()
}
}
internal class ChartView: NSStackView {
private var chart: NetworkChartView? = nil
private var ready: Bool = false
private var readColor: NSColor {
SColor.fromString(Store.shared.string(key: "\(ModuleType.disk.stringValue)_readColor", defaultValue: SColor.secondBlue.key)).additional as! NSColor
}
private var writeColor: NSColor {
SColor.fromString(Store.shared.string(key: "\(ModuleType.disk.stringValue)_writeColor", defaultValue: SColor.secondRed.key)).additional as! NSColor
}
private var reverseOrder: Bool {
Store.shared.bool(key: "\(ModuleType.disk.stringValue)_reverseOrder", defaultValue: false)
}
public init(width: CGFloat) {
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 36))
self.wantsLayer = true
self.layer?.cornerRadius = 3
let chart = NetworkChartView(frame: NSRect(
x: 0,
y: 1,
width: self.frame.width,
height: self.frame.height - 2
), num: 120, reversedOrder: self.reverseOrder, outColor: self.writeColor, inColor: self.readColor)
chart.setTooltipState(false)
self.chart = chart
self.addArrangedSubview(chart)
self.widthAnchor.constraint(equalToConstant: self.frame.width).isActive = true
self.heightAnchor.constraint(equalToConstant: self.frame.height).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateLayer() {
self.layer?.backgroundColor = self.isDarkMode ? NSColor.lightGray.withAlphaComponent(0.1).cgColor : NSColor.white.cgColor
}
public func update(read: Int64, write: Int64) {
self.chart?.addValue(upload: Double(write), download: Double(read))
}
public func setColors(read: NSColor? = nil, write: NSColor? = nil) {
self.chart?.setColors(in: read, out: write)
}
public func setReverseOrder(_ newValue: Bool) {
self.chart?.setReverseOrder(newValue)
}
}
internal class LegendView: NSView {
private let size: Int64
private var free: Int64
private let id: String
private var ready: Bool = false
private var showUsedSpace: Bool {
get { Store.shared.bool(key: "\(self.id)_usedSpace", defaultValue: false) }
set { Store.shared.set(key: "\(self.id)_usedSpace", value: newValue) }
}
private var legendField: NSTextField? = nil
private var percentageField: NSTextField? = nil
public init(width: CGFloat, id: String, size: Int64, free: Int64) {
self.id = id
self.size = size
self.free = free
super.init(frame: CGRect(x: 0, y: 0, width: width, height: 16))
self.toolTip = localizedString("Switch view")
let height: CGFloat = 14
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height))
let legendField = TextView(frame: NSRect(x: 0, y: (view.frame.height-height)/2, width: view.frame.width - 40, height: height))
legendField.font = NSFont.systemFont(ofSize: 11, weight: .light)
legendField.stringValue = self.legend(free: free)
legendField.cell?.truncatesLastVisibleLine = true
let percentageField = TextView(frame: NSRect(x: view.frame.width - 40, y: (view.frame.height-height)/2, width: 40, height: height))
percentageField.font = NSFont.systemFont(ofSize: 11, weight: .regular)
percentageField.alignment = .right
percentageField.stringValue = self.percentage(free: free)
view.addSubview(legendField)
view.addSubview(percentageField)
self.addSubview(view)
self.legendField = legendField
self.percentageField = percentageField
let trackingArea = NSTrackingArea(
rect: CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height),
options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp],
owner: self,
userInfo: nil
)
self.addTrackingArea(trackingArea)
self.widthAnchor.constraint(equalToConstant: self.frame.width).isActive = true
self.heightAnchor.constraint(equalToConstant: self.frame.height).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(free: Int64) {
self.free = free
if (self.window?.isVisible ?? false) || !self.ready {
if let view = self.legendField {
view.stringValue = self.legend(free: free)
}
if let view = self.percentageField {
view.stringValue = self.percentage(free: free)
}
self.ready = true
}
}
private func legend(free: Int64) -> String {
var value: String
if self.showUsedSpace {
var usedSpace = self.size - free
if usedSpace < 0 {
usedSpace = 0
}
value = localizedString("Used disk memory", DiskSize(usedSpace).getReadableMemory(), DiskSize(self.size).getReadableMemory())
} else {
value = localizedString("Free disk memory", DiskSize(free).getReadableMemory(), DiskSize(self.size).getReadableMemory())
}
return value
}
private func percentage(free: Int64) -> String {
guard self.size != 0 else {
return "0%"
}
var percentage: Int
if self.showUsedSpace {
percentage = Int((Double(self.size - free) / Double(self.size)) * 100)
} else {
percentage = Int((Double(free) / Double(self.size)).rounded(toPlaces: 2) * 100)
}
return "\(percentage < 0 ? 0 : percentage)%"
}
override func mouseEntered(with: NSEvent) {
NSCursor.pointingHand.set()
}
override func mouseExited(with: NSEvent) {
NSCursor.arrow.set()
}
override func mouseDown(with: NSEvent) {
self.showUsedSpace = !self.showUsedSpace
if let view = self.legendField {
view.stringValue = self.legend(free: self.free)
}
if let view = self.percentageField {
view.stringValue = self.percentage(free: self.free)
}
}
}
internal class DetailsView: NSStackView {
private var smartHeight: CGFloat {
get { (22*6) + Constants.Popup.separatorHeight }
}
private var readSpeedValueField: ValueField?
private var writeSpeedValueField: ValueField?
private var totalReadValueField: ValueField?
private var totalWrittenValueField: ValueField?
private var smartTotalReadValueField: ValueField?
private var smartTotalWrittenValueField: ValueField?
private var temperatureValueField: ValueField?
private var healthValueField: ValueField?
private var powerCyclesValueField: ValueField?
private var powerOnHoursValueField: ValueField?
private var readColor: NSColor {
SColor.fromString(Store.shared.string(key: "\(ModuleType.disk.stringValue)_readColor", defaultValue: SColor.secondBlue.key)).additional as! NSColor
}
private var writeColor: NSColor {
SColor.fromString(Store.shared.string(key: "\(ModuleType.disk.stringValue)_writeColor", defaultValue: SColor.secondRed.key)).additional as! NSColor
}
public init(width: CGFloat, id: String, smart: smart_t? = nil) {
super.init(frame: CGRect(x: 0, y: 0, width: width, height: 0))
self.orientation = .vertical
self.distribution = .fillProportionally
self.spacing = 0
self.addArrangedSubview(self.initSpeed())
self.addArrangedSubview(self.initSmart())
self.recalculateHeight()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func recalculateHeight() {
var h: CGFloat = 0
self.arrangedSubviews.forEach { v in
if let v = v as? NSStackView {
h += v.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +)
} else {
h += v.bounds.height
}
}
if self.frame.size.height != h {
self.setFrameSize(NSSize(width: self.frame.width, height: h))
}
}
private func initSpeed() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 88))
view.widthAnchor.constraint(equalToConstant: view.bounds.width).isActive = true
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let container: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
container.orientation = .vertical
container.spacing = 0
(_, _, self.readSpeedValueField) = popupWithColorRow(container, color: self.readColor, title: "\(localizedString("Read")):", value: "0 KB/s")
(_, _, self.writeSpeedValueField) = popupWithColorRow(container, color: self.writeColor, title: "\(localizedString("Write")):", value: "0 KB/s")
self.totalReadValueField = popupRow(container, title: "\(localizedString("Total read")):", value: "0 KB").1
self.totalWrittenValueField = popupRow(container, title: "\(localizedString("Total written")):", value: "0 KB").1
self.readSpeedValueField?.font = NSFont.systemFont(ofSize: 11, weight: .regular)
self.writeSpeedValueField?.font = NSFont.systemFont(ofSize: 11, weight: .regular)
self.totalReadValueField?.font = NSFont.systemFont(ofSize: 11, weight: .regular)
self.totalWrittenValueField?.font = NSFont.systemFont(ofSize: 11, weight: .regular)
view.addSubview(container)
return view
}
private func initSmart() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.smartHeight))
view.widthAnchor.constraint(equalToConstant: view.bounds.width).isActive = true
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let separator = separatorView(localizedString("SMART"), origin: NSPoint(x: 0, y: self.smartHeight-Constants.Popup.separatorHeight), width: self.frame.width)
let container: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: separator.frame.origin.y))
container.orientation = .vertical
container.spacing = 0
self.smartTotalReadValueField = popupRow(container, title: "\(localizedString("Total read")):", value: "0 KB").1
self.smartTotalWrittenValueField = popupRow(container, title: "\(localizedString("Total written")):", value: "0 KB").1
self.temperatureValueField = popupRow(container, title: "\(localizedString("Temperature")):", value: "\(temperature(0))").1
self.healthValueField = popupRow(container, title: "\(localizedString("Health")):", value: "0%").1
self.powerCyclesValueField = popupRow(container, title: "\(localizedString("Power cycles")):", value: "0").1
self.powerOnHoursValueField = popupRow(container, title: "\(localizedString("Power on hours")):", value: "0").1
self.smartTotalReadValueField?.font = NSFont.systemFont(ofSize: 11, weight: .regular)
self.smartTotalWrittenValueField?.font = NSFont.systemFont(ofSize: 11, weight: .regular)
self.temperatureValueField?.font = NSFont.systemFont(ofSize: 11, weight: .regular)
self.healthValueField?.font = NSFont.systemFont(ofSize: 11, weight: .regular)
self.powerCyclesValueField?.font = NSFont.systemFont(ofSize: 11, weight: .regular)
self.powerOnHoursValueField?.font = NSFont.systemFont(ofSize: 11, weight: .regular)
view.addSubview(separator)
view.addSubview(container)
return view
}
public func update(stats: stats) {
guard self.window?.isVisible ?? false else { return }
self.readSpeedValueField?.stringValue = Units(bytes: stats.read).getReadableSpeed()
self.writeSpeedValueField?.stringValue = Units(bytes: stats.write).getReadableSpeed()
self.totalReadValueField?.stringValue = Units(bytes: stats.readBytes).getReadableMemory()
self.totalReadValueField?.toolTip = "\(stats.readBytes / (512 * 1000))"
self.totalWrittenValueField?.stringValue = Units(bytes: stats.writeBytes).getReadableMemory()
self.totalWrittenValueField?.toolTip = "\(stats.writeBytes / (512 * 1000))"
}
public func update(smart: smart_t?) {
guard self.window?.isVisible ?? false, let smart else { return }
self.smartTotalReadValueField?.toolTip = "\(smart.totalRead / (512 * 1000))"
self.smartTotalWrittenValueField?.toolTip = "\(smart.totalWritten / (512 * 1000))"
self.smartTotalReadValueField?.stringValue = Units(bytes: smart.totalRead).getReadableMemory()
self.smartTotalWrittenValueField?.stringValue = Units(bytes: smart.totalWritten).getReadableMemory()
self.temperatureValueField?.stringValue = "\(temperature(Double(smart.temperature)))"
self.healthValueField?.stringValue = "\(smart.life)%"
self.powerCyclesValueField?.stringValue = "\(smart.powerCycles)"
self.powerOnHoursValueField?.stringValue = "\(smart.powerOnHours)"
}
}
================================================
FILE: Modules/Disk/portal.swift
================================================
//
// portal.swift
// Disk
//
// Created by Serhiy Mytrovtsiy on 20/02/2023
// Using Swift 5.0
// Running on macOS 13.2
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
public class Portal: PortalWrapper {
private var circle: PieChartView? = nil
private var chart: NetworkChartView? = nil
private var nameField: NSTextField? = nil
private var usedField: NSTextField? = nil
private var freeField: NSTextField? = nil
private var valueColorState: SColor = .secondBlue
private var valueColor: NSColor { self.valueColorState.additional as? NSColor ?? NSColor.systemBlue }
private var readColor: NSColor {
SColor.fromString(Store.shared.string(key: "\(self.name)_readColor", defaultValue: SColor.secondBlue.key)).additional as! NSColor
}
private var writeColor: NSColor {
SColor.fromString(Store.shared.string(key: "\(self.name)_writeColor", defaultValue: SColor.secondRed.key)).additional as! NSColor
}
private var initialized: Bool = false
public override func load() {
self.loadColors()
let view = NSStackView()
view.orientation = .horizontal
view.distribution = .fillEqually
view.spacing = Constants.Popup.spacing*2
view.edgeInsets = NSEdgeInsets(
top: 0,
left: Constants.Popup.spacing*2,
bottom: 0,
right: Constants.Popup.spacing*2
)
let chartsView = self.charts()
let detailsView = self.details()
view.addArrangedSubview(chartsView)
view.addArrangedSubview(detailsView)
self.addArrangedSubview(view)
chartsView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
}
private func loadColors() {
self.valueColorState = SColor.fromString(Store.shared.string(key: "\(self.name)_valueColor", defaultValue: self.valueColorState.key))
}
private func charts() -> NSView {
let view = NSStackView()
view.orientation = .vertical
view.distribution = .fillEqually
view.spacing = Constants.Popup.spacing*2
view.edgeInsets = NSEdgeInsets(
top: Constants.Popup.spacing*4,
left: Constants.Popup.spacing*4,
bottom: Constants.Popup.spacing*4,
right: Constants.Popup.spacing*4
)
let chart = PieChartView(frame: NSRect.zero, segments: [], drawValue: true)
chart.toolTip = localizedString("Disk usage")
view.addArrangedSubview(chart)
self.circle = chart
return view
}
private func details() -> NSView {
let view = NSStackView()
view.orientation = .vertical
view.distribution = .fillEqually
view.spacing = Constants.Popup.spacing*2
self.nameField = portalRow(view, title: "\(localizedString("Name")):").1
self.usedField = portalRow(view, title: "\(localizedString("Used")):").1
self.freeField = portalRow(view, title: "\(localizedString("Free")):").1
let chart = NetworkChartView(frame: NSRect.zero, num: 120, minMax: false, outColor: self.writeColor, inColor: self.readColor)
chart.heightAnchor.constraint(equalToConstant: 26).isActive = true
self.chart = chart
view.addArrangedSubview(chart)
return view
}
internal func utilizationCallback(_ value: drive) {
DispatchQueue.main.async(execute: {
if (self.window?.isVisible ?? false) || !self.initialized {
self.nameField?.stringValue = value.mediaName
self.usedField?.stringValue = DiskSize(value.size - value.free).getReadableMemory()
self.freeField?.stringValue = DiskSize(value.free).getReadableMemory()
self.circle?.toolTip = "\(localizedString("Disk usage")): \(Int(value.percentage*100))%"
self.circle?.setValue(value.percentage)
self.circle?.setSegments([
circle_segment(value: value.percentage, color: self.valueColor)
])
self.initialized = true
}
})
}
internal func activityCallback(_ value: drive) {
DispatchQueue.main.async(execute: {
self.chart?.addValue(upload: Double(value.activity.write), download: Double(value.activity.read))
})
}
}
================================================
FILE: Modules/Disk/readers.swift
================================================
//
// readers.swift
// Disk
//
// Created by Serhiy Mytrovtsiy on 07/05/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
import IOKit.storage
import CoreServices
let kIONVMeSMARTUserClientTypeID = CFUUIDGetConstantUUIDWithBytes(nil,
0xAA, 0x0F, 0xA6, 0xF9,
0xC2, 0xD6, 0x45, 0x7F,
0xB1, 0x0B, 0x59, 0xA1,
0x32, 0x53, 0x29, 0x2F
)
let kIONVMeSMARTInterfaceID = CFUUIDGetConstantUUIDWithBytes(nil,
0xCC, 0xD1, 0xDB, 0x19,
0xFD, 0x9A, 0x4D, 0xAF,
0xBF, 0x95, 0x12, 0x45,
0x4B, 0x23, 0x0A, 0xB6
)
let kIOCFPlugInInterfaceID = CFUUIDGetConstantUUIDWithBytes(nil,
0xC2, 0x44, 0xE8, 0x58,
0x10, 0x9C, 0x11, 0xD4,
0x91, 0xD4, 0x00, 0x50,
0xE4, 0xC6, 0x42, 0x6F
)
internal class CapacityReader: Reader {
internal var list: Disks = Disks()
private var SMART: Bool {
Store.shared.bool(key: "\(ModuleType.disk.stringValue)_SMART", defaultValue: true)
}
private var purgableSpace: [URL: (Date, Int64)] = [:]
public override func read() {
let keys: [URLResourceKey] = [.volumeNameKey]
let removableState = Store.shared.bool(key: "Disk_removable", defaultValue: false)
let paths = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: keys, options: [.skipHiddenVolumes])!
guard let session = DASessionCreate(kCFAllocatorDefault) else {
error("cannot create main DASessionCreate()", log: self.log)
return
}
var active: [String] = []
for url in paths {
if url.pathComponents.count == 1 || (url.pathComponents.count > 1 && url.pathComponents[1] == "Volumes") {
if let disk = DADiskCreateFromVolumePath(kCFAllocatorDefault, session, url as CFURL) {
if let diskName = DADiskGetBSDName(disk) {
let BSDName: String = String(cString: diskName)
active.append(BSDName)
if let d = self.list.first(where: { $0.BSDName == BSDName}), let idx = self.list.index(where: { $0.BSDName == BSDName}) {
if d.removable && !removableState {
self.list.remove(at: idx)
continue
}
if let path = d.path {
self.list.updateFreeSize(idx, newValue: self.freeDiskSpaceInBytes(path))
self.list.updateSMARTData(idx, smart: self.getSMARTDetails(for: BSDName))
}
continue
}
if var d = driveDetails(disk, removableState: removableState) {
if let path = d.path {
d.free = self.freeDiskSpaceInBytes(path)
d.size = self.totalDiskSpaceInBytes(path)
}
d.smart = self.getSMARTDetails(for: BSDName)
guard d.size != 0 else { continue }
self.list.append(d)
self.list.sort()
}
}
}
}
}
active.difference(from: self.list.map{ $0.BSDName }).forEach { (BSDName: String) in
if let idx = self.list.index(where: { $0.BSDName == BSDName }) {
self.list.remove(at: idx)
}
}
self.callback(self.list)
}
public func resetPurgableSpace(for uuid: String) {
if let disk = self.list.first(where: { $0.uuid == uuid }), let path = disk.path {
self.purgableSpace.removeValue(forKey: path)
}
}
private func freeDiskSpaceInBytes(_ path: URL) -> Int64 {
var stat = statfs()
if statfs(path.path, &stat) == 0 {
var purgeable: Int64 = 0
if self.purgableSpace[path] == nil {
let value = CSDiskSpaceGetRecoveryEstimate(path as NSURL)
purgeable = Int64(value)
self.purgableSpace[path] = (Date(), purgeable)
} else if let pair = self.purgableSpace[path] {
let delta = Date().timeIntervalSince(pair.0)
if delta > 30 {
let value = CSDiskSpaceGetRecoveryEstimate(path as NSURL)
purgeable = Int64(value)
self.purgableSpace[path] = (Date(), purgeable)
} else {
purgeable = pair.1
}
}
return (Int64(stat.f_bfree) * Int64(stat.f_bsize)) + Int64(purgeable)
}
do {
if let url = URL(string: path.absoluteString) {
let values = try url.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey])
if let capacity = values.volumeAvailableCapacityForImportantUsage, capacity != 0 {
return capacity
}
}
} catch let err {
error("error retrieving free space #1: \(err.localizedDescription)", log: self.log)
}
do {
let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: path.path)
if let freeSpace = (systemAttributes[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value {
return freeSpace
}
} catch let err {
error("error retrieving free space: \(err.localizedDescription)", log: self.log)
}
return 0
}
private func totalDiskSpaceInBytes(_ path: URL) -> Int64 {
do {
let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: path.path)
if let totalSpace = (systemAttributes[FileAttributeKey.systemSize] as? NSNumber)?.int64Value {
return totalSpace
}
} catch let err {
error("error retrieving total space: \(err.localizedDescription)", log: self.log)
}
return 0
}
private func getSMARTDetails(for BSDName: String) -> smart_t? {
guard self.SMART else { return nil }
var disk = IOServiceGetMatchingService(kIOMasterPortDefault, IOBSDNameMatching(kIOMasterPortDefault, 0, BSDName.cString(using: .utf8)))
guard disk != kIOReturnSuccess else { return nil }
defer { IOObjectRelease(disk) }
var parent = disk
while IOObjectConformsTo(disk, kIOBlockStorageDeviceClass) == 0 {
let error = IORegistryEntryGetParentEntry(disk, kIOServicePlane, &parent)
if error != kIOReturnSuccess || parent == kIOReturnSuccess { return nil }
disk = parent
}
guard IOObjectConformsTo(disk, kIOBlockStorageDeviceClass) > 0,
let raw = IORegistryEntryCreateCFProperty(disk, "NVMe SMART Capable" as CFString, kCFAllocatorDefault, 0),
let val = raw.takeRetainedValue() as? Bool, val else {
return nil
}
var pluginInterface: UnsafeMutablePointer?>?
var smartInterface: UnsafeMutablePointer?>?
var score: Int32 = 0
var result = IOCreatePlugInInterfaceForService(disk, kIONVMeSMARTUserClientTypeID, kIOCFPlugInInterfaceID, &pluginInterface, &score)
guard result == kIOReturnSuccess else { return nil }
defer {
if pluginInterface != nil {
IODestroyPlugInInterface(pluginInterface)
}
}
result = withUnsafeMutablePointer(to: &smartInterface) {
$0.withMemoryRebound(to: Optional.self, capacity: 1) {
pluginInterface?.pointee?.pointee.QueryInterface(pluginInterface, CFUUIDGetUUIDBytes(kIONVMeSMARTInterfaceID), $0) ?? KERN_NOT_FOUND
}
}
guard result == kIOReturnSuccess else { return nil }
defer {
if smartInterface != nil {
_ = pluginInterface?.pointee?.pointee.Release(smartInterface)
}
}
guard let smart = smartInterface?.pointee else { return nil }
var smartData: nvme_smart_log = nvme_smart_log()
guard smart.pointee.SMARTReadData(smartInterface, &smartData) == kIOReturnSuccess else { return nil }
let temperatures: [UInt8] = [UInt8(smartData.temperature.1), UInt8(smartData.temperature.0)]
var temperature: UInt16 = 0
let data = NSData(bytes: temperatures, length: 2)
data.getBytes(&temperature, length: 2)
let dataUnitsRead = self.extractUInt128(smartData.data_units_read)
let dataUnitsWritten = self.extractUInt128(smartData.data_units_written)
let bytesPerDataUnit: Int64 = 512 * 1000
let powerCycles = withUnsafeBytes(of: smartData.power_cycles) { $0.load(as: UInt32.self) }
let powerOnHours = withUnsafeBytes(of: smartData.power_on_hours) { $0.load(as: UInt32.self) }
return smart_t(
temperature: Int(UInt16(bigEndian: temperature) - 273),
life: 100 - Int(smartData.percent_used),
totalRead: dataUnitsRead * bytesPerDataUnit,
totalWritten: dataUnitsWritten * bytesPerDataUnit,
powerCycles: Int(powerCycles),
powerOnHours: Int(powerOnHours)
)
}
private func extractUInt128(_ tuple: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) -> Int64 {
let byteArray: [UInt8] = [
tuple.0, tuple.1, tuple.2, tuple.3, tuple.4, tuple.5, tuple.6, tuple.7,
tuple.8, tuple.9, tuple.10, tuple.11, tuple.12, tuple.13, tuple.14, tuple.15
]
let uint64Value = byteArray.prefix(8).withUnsafeBytes { $0.load(as: UInt64.self) }
let hasHigherBytes = byteArray.suffix(8).contains(where: { $0 != 0 })
if hasHigherBytes || uint64Value > UInt64(Int64.max) {
return Int64.max
}
return Int64(uint64Value)
}
}
internal class ActivityReader: Reader {
internal var list: Disks = Disks()
override func setup() {
self.setInterval(1)
}
public override func read() {
let keys: [URLResourceKey] = [.volumeNameKey]
let removableState = Store.shared.bool(key: "Disk_removable", defaultValue: false)
let paths = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: keys)!
guard let session = DASessionCreate(kCFAllocatorDefault) else {
error("cannot create a DASessionCreate()", log: self.log)
return
}
var active: [String] = []
for url in paths {
if url.pathComponents.count == 1 || (url.pathComponents.count > 1 && url.pathComponents[1] == "Volumes") {
if let disk = DADiskCreateFromVolumePath(kCFAllocatorDefault, session, url as CFURL) {
if let diskName = DADiskGetBSDName(disk) {
let BSDName: String = String(cString: diskName)
active.append(BSDName)
if let d = self.list.first(where: { $0.BSDName == BSDName}), let idx = self.list.index(where: { $0.BSDName == BSDName}) {
if d.removable && !removableState {
self.list.remove(at: idx)
continue
}
self.driveStats(idx, d)
continue
}
if let d = driveDetails(disk, removableState: removableState) {
self.list.append(d)
self.list.sort()
}
}
}
}
}
active.difference(from: self.list.map{ $0.BSDName }).forEach { (BSDName: String) in
if let idx = self.list.index(where: { $0.BSDName == BSDName }) {
self.list.remove(at: idx)
}
}
self.callback(self.list)
}
private func driveStats(_ idx: Int, _ d: drive) {
let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOBSDNameMatching(kIOMasterPortDefault, 0, d.BSDName))
if service == 0 { return }
IOObjectRelease(service)
guard let props = getIOProperties(d.parent) else { return }
if let statistics = props.object(forKey: "Statistics") as? NSDictionary {
let readBytes = statistics.object(forKey: "Bytes (Read)") as? Int64 ?? 0
let writeBytes = statistics.object(forKey: "Bytes (Write)") as? Int64 ?? 0
if d.activity.readBytes != 0 {
self.list.updateRead(idx, newValue: readBytes - d.activity.readBytes)
}
if d.activity.writeBytes != 0 {
self.list.updateWrite(idx, newValue: writeBytes - d.activity.writeBytes)
}
self.list.updateReadWrite(idx, read: readBytes, write: writeBytes)
}
return
}
}
private func driveDetails(_ disk: DADisk, removableState: Bool) -> drive? {
var d: drive = drive()
if let bsdName = DADiskGetBSDName(disk) {
d.BSDName = String(cString: bsdName)
}
if let diskDescription = DADiskCopyDescription(disk) {
if let dict = diskDescription as? [String: AnyObject] {
if let removable = dict[kDADiskDescriptionMediaRemovableKey as String] {
if removable as! Bool {
if !removableState {
return nil
}
d.removable = true
}
}
if let mediaUUID = dict[kDADiskDescriptionMediaUUIDKey as String] {
d.uuid = CFUUIDCreateString(kCFAllocatorDefault, (mediaUUID as! CFUUID)) as String
}
if let mediaName = dict[kDADiskDescriptionVolumeNameKey as String] {
d.mediaName = mediaName as! String
if d.mediaName == "Recovery" {
return nil
}
}
if d.mediaName == "" {
if let mediaName = dict[kDADiskDescriptionMediaNameKey as String] {
d.mediaName = mediaName as! String
if d.mediaName == "Recovery" {
return nil
}
}
}
if let deviceModel = dict[kDADiskDescriptionDeviceModelKey as String] {
d.model = (deviceModel as! String).trimmingCharacters(in: .whitespacesAndNewlines)
}
if let deviceProtocol = dict[kDADiskDescriptionDeviceProtocolKey as String] {
d.connectionType = deviceProtocol as! String
}
if let volumePath = dict[kDADiskDescriptionVolumePathKey as String] {
if let url = volumePath as? NSURL {
d.path = url as URL
if let components = url.pathComponents {
d.root = components.count == 1
if components.count > 1 && components[1] == "Volumes" {
if let name: String = url.lastPathComponent, name != "" {
d.mediaName = name
}
}
}
}
}
if let volumeKind = dict[kDADiskDescriptionVolumeKindKey as String] {
d.fileSystem = volumeKind as! String
}
}
}
if d.path == nil {
return nil
}
if d.uuid == "" || d.uuid == "00000000-0000-0000-0000-000000000000" {
d.uuid = d.BSDName
}
let partitionLevel = d.BSDName.filter { "0"..."9" ~= $0 }.count
if let parent = getDeviceIOParent(DADiskCopyIOMedia(disk), level: Int(partitionLevel)) {
d.parent = parent
}
return d
}
// https://opensource.apple.com/source/bless/bless-152/libbless/APFS/BLAPFSUtilities.c.auto.html
public func getDeviceIOParent(_ obj: io_registry_entry_t, level: Int) -> io_registry_entry_t? {
var parent: io_registry_entry_t = 0
if IORegistryEntryGetParentEntry(obj, kIOServicePlane, &parent) != KERN_SUCCESS {
return nil
}
for _ in 1...level where IORegistryEntryGetParentEntry(parent, kIOServicePlane, &parent) != KERN_SUCCESS {
IOObjectRelease(parent)
return nil
}
return parent
}
struct io {
var read: Int
var write: Int
}
public class ProcessReader: Reader<[Disk_process]> {
private let queue = DispatchQueue(label: "eu.exelban.Disk.processReader")
private var _list: [Int32: io] = [:]
private var list: [Int32: io] {
get {
self.queue.sync { self._list }
}
set {
self.queue.sync { self._list = newValue }
}
}
private var numberOfProcesses: Int {
Store.shared.int(key: "\(ModuleType.disk.stringValue)_processes", defaultValue: 5)
}
public override func setup() {
self.popup = true
self.setInterval(1)
}
public override func read() {
guard self.numberOfProcesses != 0, let output = runProcess(path: "/bin/ps", args: ["-Aceo pid,args", "-r"]) else { return }
var processes: [Disk_process] = []
output.enumerateLines { (line, _) in
let str = line.trimmingCharacters(in: .whitespaces)
let pidFind = str.findAndCrop(pattern: "^\\d+")
guard let pid = Int32(pidFind.cropped) else { return }
let name = pidFind.remain.findAndCrop(pattern: "^[^ ]+").cropped
var usage = rusage_info_current()
let result = withUnsafeMutablePointer(to: &usage) {
$0.withMemoryRebound(to: (rusage_info_t?.self), capacity: 1) {
proc_pid_rusage(pid, RUSAGE_INFO_CURRENT, $0)
}
}
guard result != -1 else { return }
let bytesRead = Int(usage.ri_diskio_bytesread)
let bytesWritten = Int(usage.ri_diskio_byteswritten)
if self.list[pid] == nil {
self.list[pid] = io(read: bytesRead, write: bytesWritten)
}
if let v = self.list[pid] {
let read = bytesRead - v.read
let write = bytesWritten - v.write
if read != 0 || write != 0 {
processes.append(Disk_process(pid: Int(pid), name: name, read: read, write: write))
}
}
self.list[pid]?.read = bytesRead
self.list[pid]?.write = bytesWritten
}
processes.sort {
let firstMax = max($0.read, $0.write)
let secondMax = max($1.read, $1.write)
let firstMin = min($0.read, $0.write)
let secondMin = min($1.read, $1.write)
if firstMax == secondMax && firstMin != secondMin { // max values are the same, min not. Sort by min values
return firstMin < secondMin
}
return firstMax < secondMax // max values are not the same, sort by max value
}
self.callback(processes.suffix(self.numberOfProcesses).reversed())
}
}
private func runProcess(path: String, args: [String] = []) -> String? {
let task = Process()
task.launchPath = path
task.arguments = args
let outputPipe = Pipe()
defer {
outputPipe.fileHandleForReading.closeFile()
}
task.standardOutput = outputPipe
do {
try task.run()
} catch {
return nil
}
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
return String(data: outputData, encoding: .utf8)
}
================================================
FILE: Modules/Disk/settings.swift
================================================
//
// settings.swift
// Disk
//
// Created by Serhiy Mytrovtsiy on 12/05/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
var textWidgetHelp = """
Description
You can use a combination of any of the variables.
Examples:
- $capacity.free/$capacity.total
- Free: $capacity.free ($percentage.used)
- Used: $capacity.used ($percentage.used)
Available variables
- $capacity.free: Free space of active drive.
- $capacity.used: Used space of active drive.
- $capacity.total: Total space of active drive.
- $percentage.free: Free space (percentage) of active drive.
- $percentage.used: Used space (percentage) of active drive.
"""
internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate {
private let title: String
private var removableState: Bool = false
private var updateIntervalValue: Int = 10
private var numberOfProcesses: Int = 5
private var baseValue: String = "byte"
private var SMARTState: Bool = true
private var textValue: String = "$capacity.free/$capacity.total"
public var selectedDiskHandler: (String) -> Void = {_ in }
public var callback: (() -> Void) = {}
public var setInterval: ((_ value: Int) -> Void) = {_ in }
public var callbackWhenUpdateNumberOfProcesses: (() -> Void) = {}
private var selectedDisk: String
private var button: NSPopUpButton?
private var list: [String] = []
private let textWidgetHelpPanel: HelpHUD = HelpHUD(textWidgetHelp)
public init(_ module: ModuleType) {
self.title = module.stringValue
self.selectedDisk = Store.shared.string(key: "\(self.title)_disk", defaultValue: "")
self.removableState = Store.shared.bool(key: "\(self.title)_removable", defaultValue: self.removableState)
self.updateIntervalValue = Store.shared.int(key: "\(self.title)_updateInterval", defaultValue: self.updateIntervalValue)
self.numberOfProcesses = Store.shared.int(key: "\(self.title)_processes", defaultValue: self.numberOfProcesses)
self.baseValue = Store.shared.string(key: "\(self.title)_base", defaultValue: self.baseValue)
self.SMARTState = Store.shared.bool(key: "\(self.title)_SMART", defaultValue: self.SMARTState)
self.textValue = Store.shared.string(key: "\(self.title)_textWidgetValue", defaultValue: self.textValue)
super.init(frame: NSRect.zero)
self.orientation = .vertical
self.spacing = Constants.Settings.margin
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func load(widgets: [widget_t]) {
self.subviews.forEach{ $0.removeFromSuperview() }
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Update interval"), component: selectView(
action: #selector(self.changeUpdateInterval),
items: ReaderUpdateIntervals,
selected: "\(self.updateIntervalValue)"
))
]))
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Number of top processes"), component: selectView(
action: #selector(self.changeNumberOfProcesses),
items: NumbersOfProcesses.map{ KeyValue_t(key: "\($0)", value: "\($0)") },
selected: "\(self.numberOfProcesses)"
))
]))
self.button = selectView(
action: #selector(self.handleSelection),
items: [],
selected: ""
)
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Disk to show"), component: self.button!),
PreferencesRow(localizedString("Show removable disks"), component: switchView(
action: #selector(self.toggleRemovable),
state: self.removableState
))
]))
if widgets.contains(where: { $0 == .speed }) {
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Base"), component: selectView(
action: #selector(self.toggleBase),
items: SpeedBase,
selected: self.baseValue
))
]))
}
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("SMART data"), component: switchView(
action: #selector(self.toggleSMART),
state: self.SMARTState
))
]))
if widgets.contains(where: { $0 == .text }) {
let textField = self.inputField(id: "text", value: self.textValue, placeholder: localizedString("This will be visible in the text widget"))
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Text widget value"), component: textField) { [weak self] in
self?.textWidgetHelpPanel.show()
}
]))
}
}
internal func setList(_ list: Disks) {
DispatchQueue.main.async(execute: {
let disks = list.map{ $0.mediaName }
if self.button?.itemTitles.count != disks.count {
self.button?.removeAllItems()
}
if disks != self.button?.itemTitles {
self.button?.addItems(withTitles: disks)
self.list = disks
if self.selectedDisk != "" {
self.button?.selectItem(withTitle: self.selectedDisk)
}
}
})
}
private func inputField(id: String, value: String, placeholder: String) -> NSView {
let field: NSTextField = NSTextField()
field.identifier = NSUserInterfaceItemIdentifier(id)
field.widthAnchor.constraint(equalToConstant: 250).isActive = true
field.font = NSFont.systemFont(ofSize: 12, weight: .regular)
field.textColor = .textColor
field.isEditable = true
field.isSelectable = true
field.usesSingleLineMode = true
field.maximumNumberOfLines = 1
field.focusRingType = .none
field.stringValue = value
field.delegate = self
field.placeholderString = placeholder
return field
}
@objc private func changeNumberOfProcesses(_ sender: NSMenuItem) {
if let value = Int(sender.title) {
self.numberOfProcesses = value
Store.shared.set(key: "\(self.title)_processes", value: value)
self.callbackWhenUpdateNumberOfProcesses()
}
}
@objc private func handleSelection(_ sender: NSPopUpButton) {
guard let item = sender.selectedItem else { return }
self.selectedDisk = item.title
Store.shared.set(key: "\(self.title)_disk", value: item.title)
self.selectedDiskHandler(item.title)
}
@objc private func toggleRemovable(_ sender: NSControl) {
self.removableState = controlState(sender)
Store.shared.set(key: "\(self.title)_removable", value: self.removableState)
self.callback()
}
@objc private func changeUpdateInterval(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let value = Int(key) else { return }
self.setUpdateInterval(value: value)
}
public func setUpdateInterval(value: Int) {
self.updateIntervalValue = value
Store.shared.set(key: "\(self.title)_updateInterval", value: value)
self.setInterval(value)
}
@objc private func toggleBase(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.baseValue = key
Store.shared.set(key: "\(self.title)_base", value: self.baseValue)
}
@objc private func toggleSMART(_ sender: NSControl) {
self.SMARTState = controlState(sender)
Store.shared.set(key: "\(self.title)_SMART", value: self.SMARTState)
self.callback()
}
func controlTextDidChange(_ notification: Notification) {
if let field = notification.object as? NSTextField {
if field.identifier == NSUserInterfaceItemIdentifier("text") {
self.textValue = field.stringValue
Store.shared.set(key: "\(self.title)_textWidgetValue", value: self.textValue)
}
}
}
}
================================================
FILE: Modules/Disk/widget.swift
================================================
//
// widget.swift
// Disk
//
// Created by Serhiy Mytrovtsiy on 16/07/2024
// Using Swift 5.0
// Running on macOS 14.5
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//
import SwiftUI
import WidgetKit
import Charts
import Kit
public struct Disk_entry: TimelineEntry {
public static let kind = "DiskWidget"
public static var snapshot: Disk_entry = Disk_entry(value: drive(size: 494384795648, free: 251460125440))
public var date: Date {
Calendar.current.date(byAdding: .second, value: 5, to: Date())!
}
public var value: drive? = nil
}
public struct Provider: TimelineProvider {
public typealias Entry = Disk_entry
private let userDefaults: UserDefaults? = UserDefaults(suiteName: "\(Bundle.main.object(forInfoDictionaryKey: "TeamId") as! String).eu.exelban.Stats.widgets")
public func placeholder(in context: Context) -> Disk_entry {
Disk_entry()
}
public func getSnapshot(in context: Context, completion: @escaping (Disk_entry) -> Void) {
completion(Disk_entry.snapshot)
}
public func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
self.userDefaults?.set(Date().timeIntervalSince1970, forKey: Disk_entry.kind)
var entry = Disk_entry()
if let raw = userDefaults?.data(forKey: "Disk@CapacityReader"), let load = try? JSONDecoder().decode(drive.self, from: raw) {
entry.value = load
}
let entries: [Disk_entry] = [entry]
completion(Timeline(entries: entries, policy: .atEnd))
}
}
@available(macOS 14.0, *)
public struct DiskWidget: Widget {
var usedColor: Color = Color(nsColor: NSColor.systemBlue)
var freeColor: Color = Color(nsColor: NSColor.lightGray)
public init() {}
public var body: some WidgetConfiguration {
StaticConfiguration(kind: Disk_entry.kind, provider: Provider()) { entry in
VStack(spacing: 10) {
if let value = entry.value {
HStack {
Chart {
SectorMark(angle: .value(localizedString("Used"), (100*(value.size-value.free))/value.size), innerRadius: .ratio(0.8)).foregroundStyle(self.usedColor)
SectorMark(angle: .value(localizedString("Free"), (100*value.free)/value.size), innerRadius: .ratio(0.8)).foregroundStyle(self.freeColor)
}
.frame(maxWidth: .infinity, maxHeight: 84)
.chartLegend(.hidden)
.chartBackground { chartProxy in
GeometryReader { geometry in
if let anchor = chartProxy.plotFrame {
let frame = geometry[anchor]
Text("\(Int(value.percentage.rounded(toPlaces: 2) * 100))%")
.font(.system(size: 16, weight: .regular))
.position(x: frame.midX, y: frame.midY-5)
Text("Disk")
.font(.system(size: 9, weight: .semibold))
.position(x: frame.midX, y: frame.midY+10)
}
}
}
}
VStack(spacing: 3) {
HStack {
Rectangle().fill(self.usedColor).frame(width: 12, height: 12).cornerRadius(2)
Text(localizedString("Used")).font(.system(size: 12, weight: .regular)).foregroundColor(.secondary)
Spacer()
Text(DiskSize(value.size - value.free).getReadableMemory())
}
HStack {
Rectangle().fill(self.freeColor).frame(width: 12, height: 12).cornerRadius(2)
Text(localizedString("Free")).font(.system(size: 12, weight: .regular)).foregroundColor(.secondary)
Spacer()
Text(DiskSize(value.free).getReadableMemory())
}
}
} else {
Text("No data")
}
}
.containerBackground(for: .widget) {
Color.clear
}
}
.configurationDisplayName("Disk widget")
.description("Displays disk stats")
.supportedFamilies([.systemSmall])
}
}
================================================
FILE: Modules/GPU/Info.plist
================================================
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
1.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSHumanReadableCopyright
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
================================================
FILE: Modules/GPU/config.plist
================================================
Name
GPU
State
Symbol
tv
Widgets
label
Default
Order
0
mini
Default
Preview
Value
0.32
Unsupported colors
pressure
Order
1
line_chart
Default
Color
systemAccent
Unsupported colors
pressure
Order
2
bar_chart
Default
Preview
Value
0.58
Color
Unsupported colors
pressure
cluster
Order
3
tachometer
Default
Order
4
Settings
popup
notifications
================================================
FILE: Modules/GPU/main.swift
================================================
//
// main.swift
// GPU
//
// Created by Serhiy Mytrovtsiy on 17/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
import WidgetKit
public typealias GPU_type = String
public enum GPU_types: GPU_type {
case unknown = ""
case integrated = "i"
case external = "e"
case discrete = "d"
}
public struct GPU_Info: Codable {
public let id: String
public let type: GPU_type
public let IOClass: String
public var vendor: String? = nil
public let model: String
public var cores: Int? = nil
public var state: Bool = true
public var fanSpeed: Int? = nil
public var coreClock: Int? = nil
public var memoryClock: Int? = nil
public var temperature: Double? = nil
public var utilization: Double? = nil
public var renderUtilization: Double? = nil
public var tilerUtilization: Double? = nil
init(id: String, type: GPU_type, IOClass: String, vendor: String? = nil, model: String, cores: Int?, utilization: Double? = nil, render: Double? = nil, tiler: Double? = nil) {
self.id = id
self.type = type
self.IOClass = IOClass
self.vendor = vendor
self.model = model
self.cores = cores
self.utilization = utilization
self.renderUtilization = render
self.tilerUtilization = tiler
}
public func remote() -> String {
var id = self.id
if self.id.isEmpty {
id = "0"
}
return "\(id),1,\(self.utilization ?? 0),\(self.renderUtilization ?? 0),\(self.tilerUtilization ?? 0),,"
}
}
public class GPUs: Codable, RemoteType {
private var queue: DispatchQueue = DispatchQueue(label: "eu.exelban.Stats.GPU.SynchronizedArray")
private var _list: [GPU_Info] = []
public var list: [GPU_Info] {
get { self.queue.sync { self._list } }
set { self.queue.sync { self._list = newValue } }
}
enum CodingKeys: String, CodingKey {
case list
}
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.list = try container.decode(Array.self, forKey: CodingKeys.list)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(list, forKey: .list)
}
init() {}
internal func active() -> [GPU_Info] {
return self.list.filter{ $0.state && $0.utilization != nil }.sorted{ $0.utilization ?? 0 > $1.utilization ?? 0 }
}
public func remote() -> Data? {
var string = "\(self.list.count),"
for (i, v) in self.list.enumerated() {
string += v.remote()
if i != self.list.count {
string += ","
}
}
string += "$"
return string.data(using: .utf8)
}
}
public class GPU: Module {
private let popupView: Popup
private let settingsView: Settings
private let portalView: Portal
private let notificationsView: Notifications
private var infoReader: InfoReader? = nil
private var selectedGPU: String = ""
private var notificationLevelState: Bool = false
private var notificationID: String? = nil
private var showType: Bool {
Store.shared.bool(key: "\(self.config.name)_showType", defaultValue: false)
}
private var systemWidgetsUpdatesState: Bool {
self.userDefaults?.bool(forKey: "systemWidgetsUpdates_state") ?? false
}
public init() {
self.popupView = Popup()
self.settingsView = Settings(.GPU)
self.portalView = Portal(.GPU)
self.notificationsView = Notifications(.GPU)
super.init(
moduleType: .GPU,
popup: self.popupView,
settings: self.settingsView,
portal: self.portalView,
notifications: self.notificationsView
)
guard self.available else { return }
self.infoReader = InfoReader(.GPU) { [weak self] value in
self?.infoCallback(value)
}
self.selectedGPU = Store.shared.string(key: "\(self.config.name)_gpu", defaultValue: self.selectedGPU)
self.settingsView.selectedGPUHandler = { [weak self] value in
self?.selectedGPU = value
self?.infoReader?.read()
}
self.settingsView.setInterval = { [weak self] value in
self?.infoReader?.setInterval(value)
}
self.settingsView.callback = { [weak self] in
self?.infoReader?.read()
}
self.setReaders([self.infoReader])
}
private func infoCallback(_ raw: GPUs?) {
guard raw != nil && !raw!.list.isEmpty, let value = raw, self.enabled else { return }
DispatchQueue.main.async(execute: {
self.popupView.infoCallback(value)
})
self.settingsView.setList(value)
let activeGPUs = value.active()
guard let activeGPU = activeGPUs.first(where: { $0.state }) ?? activeGPUs.first else {
return
}
let selectedGPU: GPU_Info = activeGPUs.first{ $0.model == self.selectedGPU } ?? activeGPU
guard let utilization = selectedGPU.utilization else {
return
}
self.portalView.callback(selectedGPU)
self.notificationsView.usageCallback(utilization)
self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: SWidget) in
switch w.item {
case let widget as Mini:
widget.setValue(utilization)
widget.setTitle(self.showType ? "\(selectedGPU.type)GPU" : nil)
case let widget as LineChart: widget.setValue(utilization)
case let widget as BarChart: widget.setValue([[ColorValue(utilization)]])
case let widget as Tachometer:
widget.setValue([
circle_segment(value: utilization, color: NSColor.systemBlue)
])
default: break
}
}
if self.systemWidgetsUpdatesState {
if isWidgetActive(self.userDefaults, [GPU_entry.kind, "UnitedWidget"]), let blobData = try? JSONEncoder().encode(selectedGPU) {
self.userDefaults?.set(blobData, forKey: "GPU@InfoReader")
}
WidgetCenter.shared.reloadTimelines(ofKind: GPU_entry.kind)
WidgetCenter.shared.reloadTimelines(ofKind: "UnitedWidget")
}
}
}
================================================
FILE: Modules/GPU/notifications.swift
================================================
//
// notifications.swift
// GPU
//
// Created by Serhiy Mytrovtsiy on 05/12/2023
// Using Swift 5.0
// Running on macOS 14.1
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
class Notifications: NotificationsWrapper {
private let usageID: String = "usage"
private var usageState: Bool = false
private var usageLevel: Int = 75
public init(_ module: ModuleType) {
super.init(module, [self.usageID])
if Store.shared.exist(key: "\(self.module)_notifications_usage") {
let value = Store.shared.string(key: "\(self.module)_notifications_usage", defaultValue: "")
if let v = Double(value) {
Store.shared.set(key: "\(self.module)_notifications_usage_state", value: true)
Store.shared.set(key: "\(self.module)_notifications_usage_value", value: Int(v*100))
Store.shared.remove("\(self.module)_notifications_usage")
}
}
self.usageState = Store.shared.bool(key: "\(self.module)_notifications_usage_state", defaultValue: self.usageState)
self.usageLevel = Store.shared.int(key: "\(self.module)_notifications_usage_value", defaultValue: self.usageLevel)
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Usage"), component: PreferencesSwitch(
action: self.toggleUsage, state: self.usageState,
with: StepperInput(self.usageLevel, callback: self.changeUsage)
))
]))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func usageCallback(_ value: Double) {
let title = localizedString("GPU usage threshold")
if self.usageState {
let subtitle = localizedString("GPU usage is", "\(Int((value)*100))%")
self.checkDouble(id: self.usageID, value: value, threshold: Double(self.usageLevel)/100, title: title, subtitle: subtitle)
}
}
@objc private func toggleUsage(_ sender: NSControl) {
self.usageState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_usage_state", value: self.usageState)
}
@objc private func changeUsage(_ newValue: Int) {
self.usageLevel = newValue
Store.shared.set(key: "\(self.module)_notifications_usage_value", value: self.usageLevel)
}
}
================================================
FILE: Modules/GPU/popup.swift
================================================
//
// popup.swift
// GPU
//
// Created by Serhiy Mytrovtsiy on 17/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class Popup: PopupWrapper {
public init() {
super.init(ModuleType.GPU, frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
self.orientation = .vertical
self.spacing = Constants.Popup.margins
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func infoCallback(_ value: GPUs) {
if self.arrangedSubviews.filter({ $0 is GPUView }).count != value.list.count {
self.arrangedSubviews.forEach{ $0.removeFromSuperview() }
}
value.list.reversed().forEach { (gpu: GPU_Info) in
if let view = self.arrangedSubviews.filter({ $0 is GPUView }).map({ $0 as! GPUView }).first(where: { $0.value.id == gpu.id }) {
view.update(gpu)
} else {
self.addArrangedSubview(GPUView(
width: self.frame.width,
gpu: gpu,
callback: self.recalculateHeight
))
}
}
self.recalculateHeight()
}
private func recalculateHeight() {
let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - self.spacing
if self.frame.size.height != h && h >= 0 {
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.sizeCallback?(self.frame.size)
}
}
// MARK: - Settings
public override func settings() -> NSView? {
let view = SettingsContainerView()
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Keyboard shortcut"), component: KeyboardShartcutView(
callback: self.setKeyboardShortcut,
value: self.keyboardShortcut
))
]))
return view
}
}
private class GPUView: NSStackView {
public var value: GPU_Info
private var detailsView: GPUDetails
private let circleSize: CGFloat = 50
private let chartSize: CGFloat = 60
private var stateView: NSView? = nil
private var circleRow: NSStackView? = nil
private var chartRow: NSStackView? = nil
private var temperatureChart: LineChartView? = nil
private var utilizationChart: LineChartView? = nil
private var renderUtilizationChart: LineChartView? = nil
private var tilerUtilizationChart: LineChartView? = nil
public var sizeCallback: (() -> Void)
open override var intrinsicContentSize: CGSize {
return CGSize(width: self.bounds.width, height: self.bounds.height)
}
public init(width: CGFloat, gpu: GPU_Info, callback: @escaping (() -> Void)) {
self.value = gpu
self.detailsView = GPUDetails(width: width, value: gpu)
self.sizeCallback = callback
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 0))
self.orientation = .vertical
self.alignment = .centerX
self.distribution = .fillProportionally
self.spacing = 0
self.wantsLayer = true
self.layer?.cornerRadius = 2
self.addArrangedSubview(self.title())
self.addArrangedSubview(self.stats())
self.addArrangedSubview(NSView())
let h = self.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +)
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.sizeCallback()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateLayer() {
self.layer?.backgroundColor = (isDarkMode ? NSColor(red: 17/255, green: 17/255, blue: 17/255, alpha: 0.25) : NSColor(red: 245/255, green: 245/255, blue: 245/255, alpha: 1)).cgColor
}
private func title() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 24))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let width: CGFloat = self.value.model.widthOfString(usingFont: NSFont.systemFont(ofSize: 13, weight: .regular)) + 16
let labelView: NSTextField = TextView(frame: NSRect(x: 0, y: (view.frame.height-16)/2, width: width - 8, height: 16))
labelView.alignment = .center
labelView.textColor = .secondaryLabelColor
labelView.font = NSFont.systemFont(ofSize: 13, weight: .regular)
labelView.stringValue = self.value.model
let stateView: NSView = NSView(frame: NSRect(x: width - 8, y: (view.frame.height-7)/2, width: 6, height: 6))
stateView.wantsLayer = true
stateView.layer?.backgroundColor = (self.value.state ? NSColor.systemGreen : NSColor.systemRed).cgColor
stateView.toolTip = localizedString("GPU \(self.value.state ? "enabled" : "disabled")")
stateView.layer?.cornerRadius = 4
let details = localizedString("Details").uppercased()
let w = details.widthOfString(usingFont: NSFont.systemFont(ofSize: 9, weight: .regular)) + 8
let button = NSButtonWithPadding()
button.frame = CGRect(x: view.frame.width - w, y: 2, width: w, height: view.frame.height-2)
button.verticalPadding = 9
button.bezelStyle = .regularSquare
button.translatesAutoresizingMaskIntoConstraints = false
button.isBordered = false
button.action = #selector(self.showDetails)
button.target = self
button.toolTip = localizedString("Details")
button.title = details
button.font = NSFont.systemFont(ofSize: 9, weight: .regular)
view.addSubview(labelView)
view.addSubview(stateView)
view.addSubview(button)
self.stateView = stateView
return view
}
private func stats() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
let container: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
container.orientation = .vertical
container.spacing = 0
let circles: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
circles.orientation = .horizontal
circles.distribution = .fillEqually
circles.alignment = .bottom
self.circleRow = circles
let charts: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
charts.orientation = .horizontal
charts.distribution = .fillEqually
self.chartRow = charts
self.addStats(id: "GPU temperature", self.value.temperature)
self.addStats(id: "GPU utilization", self.value.utilization)
self.addStats(id: "Render utilization", self.value.renderUtilization)
self.addStats(id: "Tiler utilization", self.value.tilerUtilization)
container.addArrangedSubview(circles)
container.addArrangedSubview(charts)
view.addSubview(container)
let h = container.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +)
view.setFrameSize(NSSize(width: self.frame.width, height: h))
container.setFrameSize(NSSize(width: self.frame.width, height: view.bounds.height))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
container.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
return view
}
private func addStats(id: String, _ val: Double? = nil) {
guard let value = val else { return }
var circle: HalfCircleGraphView
var chart: LineChartView
if let view = self.circleRow?.arrangedSubviews.filter({ $0 is HalfCircleGraphView }).first(where: { ($0 as! HalfCircleGraphView).id == id }) {
circle = view as! HalfCircleGraphView
} else {
circle = HalfCircleGraphView(frame: NSRect(x: 0, y: 0, width: circleSize, height: circleSize))
circle.id = id
circle.toolTip = localizedString(id)
if let row = self.circleRow {
row.setFrameSize(NSSize(width: row.frame.width, height: self.circleSize + 20))
row.edgeInsets = NSEdgeInsets(top: 10, left: 10, bottom: 0, right: 10)
row.heightAnchor.constraint(equalToConstant: row.bounds.height).isActive = true
row.addArrangedSubview(circle)
}
}
if let view = self.chartRow?.arrangedSubviews.filter({ $0 is LineChartView }).first(where: { ($0 as! LineChartView).id == id }) {
chart = view as! LineChartView
} else {
chart = LineChartView(frame: NSRect(x: 0, y: 0, width: 100, height: self.chartSize), num: 120)
chart.isTooltipEnabled = false
chart.wantsLayer = true
chart.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.1).cgColor
chart.layer?.cornerRadius = 3
chart.id = id
chart.toolTip = localizedString(id)
if let row = self.chartRow {
row.setFrameSize(NSSize(width: row.frame.width, height: self.chartSize + 20))
row.spacing = Constants.Popup.margins
row.edgeInsets = NSEdgeInsets(
top: Constants.Popup.margins,
left: Constants.Popup.margins,
bottom: Constants.Popup.margins,
right: Constants.Popup.margins
)
row.heightAnchor.constraint(equalToConstant: row.bounds.height).isActive = true
row.addArrangedSubview(chart)
}
}
if id == "GPU temperature" {
circle.setValue(value)
circle.setText(temperature(value))
circle.toolTip = "\(localizedString(id)): \(temperature(value))"
chart.suffix = UnitTemperature.current.symbol
if self.temperatureChart == nil {
self.temperatureChart = chart
}
} else if id == "GPU utilization" {
circle.setValue(value)
circle.setText("\(Int(value*100))%")
circle.toolTip = "\(localizedString(id)): \(Int(value*100))%"
if self.utilizationChart == nil {
self.utilizationChart = chart
}
} else if id == "Render utilization" {
circle.setValue(value)
circle.setText("\(Int(value*100))%")
circle.toolTip = "\(localizedString(id)): \(Int(value*100))%"
if self.renderUtilizationChart == nil {
self.renderUtilizationChart = chart
}
} else if id == "Tiler utilization" {
circle.setValue(value)
circle.setText("\(Int(value*100))%")
circle.toolTip = "\(localizedString(id)): \(Int(value*100))%"
if self.tilerUtilizationChart == nil {
self.tilerUtilizationChart = chart
}
}
}
public func update(_ gpu: GPU_Info) {
self.detailsView.update(gpu)
if self.window?.isVisible ?? false {
self.stateView?.layer?.backgroundColor = (gpu.state ? NSColor.systemGreen : NSColor.systemRed).cgColor
self.stateView?.toolTip = localizedString("GPU \(gpu.state ? "enabled" : "disabled")")
self.addStats(id: "GPU temperature", gpu.temperature)
self.addStats(id: "GPU utilization", gpu.utilization)
self.addStats(id: "Render utilization", gpu.renderUtilization)
self.addStats(id: "Tiler utilization", gpu.tilerUtilization)
}
if let value = gpu.temperature {
if let temp = Double(temperature(value).replacingOccurrences(of: "C", with: "").replacingOccurrences(of: "F", with: "").digits) {
self.temperatureChart?.addValue(temp/100)
} else {
self.temperatureChart?.addValue(value)
}
}
if let value = gpu.utilization {
self.utilizationChart?.addValue(value)
}
if let value = gpu.renderUtilization {
self.renderUtilizationChart?.addValue(value)
}
if let value = gpu.tilerUtilization {
self.tilerUtilizationChart?.addValue(value)
}
}
@objc private func showDetails() {
if let view = self.arrangedSubviews.first(where: { $0 is GPUDetails }) {
view.removeFromSuperview()
} else {
self.insertArrangedSubview(self.detailsView, at: 1)
}
self.setFrameSize(NSSize(
width: self.frame.width,
height: self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +)
))
self.sizeCallback()
}
}
private class GPUDetails: NSView {
private var status: NSTextField? = nil
private var fanSpeed: NSTextField? = nil
private var coreClock: NSTextField? = nil
private var memoryClock: NSTextField? = nil
private var temperature: NSTextField? = nil
private var utilization: NSTextField? = nil
private var renderUtilization: NSTextField? = nil
private var tilerUtilization: NSTextField? = nil
open override var intrinsicContentSize: CGSize {
return CGSize(width: self.bounds.width, height: self.bounds.height)
}
init(width: CGFloat, value: GPU_Info) {
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 0))
let grid: NSGridView = NSGridView(frame: NSRect(
x: Constants.Popup.margins, y: Constants.Popup.margins,
width: self.frame.width - (Constants.Popup.margins*2), height: 0
))
grid.yPlacement = .center
grid.xPlacement = .leading
grid.rowSpacing = 0
grid.columnSpacing = 0
var num: CGFloat = 2
if let value = value.vendor {
grid.addRow(with: keyValueRow("\(localizedString("Vendor")):", value))
num += 1
}
grid.addRow(with: keyValueRow("\(localizedString("Model")):", value.model))
if let value = value.cores {
let arr = keyValueRow("\(localizedString("Cores")):", "\(value)")
grid.addRow(with: arr)
num += 1
}
let state: String = value.state ? localizedString("Active") : localizedString("Non active")
let arr = keyValueRow("\(localizedString("Status")):", state)
self.status = arr.last
grid.addRow(with: arr)
if let value = value.fanSpeed {
let arr = keyValueRow("\(localizedString("Fan speed")):", "\(value)%")
self.fanSpeed = arr.last
grid.addRow(with: arr)
num += 1
}
if let value = value.coreClock {
let arr = keyValueRow("\(localizedString("Core clock")):", "\(value)MHz")
self.coreClock = arr.last
grid.addRow(with: arr)
num += 1
}
if let value = value.memoryClock {
let arr = keyValueRow("\(localizedString("Memory clock")):", "\(value)MHz")
self.memoryClock = arr.last
grid.addRow(with: arr)
num += 1
}
if let value = value.temperature {
let arr = keyValueRow("\(localizedString("Temperature")):", Kit.temperature(Double(value)))
self.temperature = arr.last
grid.addRow(with: arr)
num += 1
}
if let value = value.utilization {
let arr = keyValueRow("\(localizedString("Utilization")):", "\(Int(value*100))%")
self.utilization = arr.last
grid.addRow(with: arr)
num += 1
}
if let value = value.renderUtilization {
let arr = keyValueRow("\(localizedString("Render utilization")):", "\(Int(value*100))%")
self.renderUtilization = arr.last
grid.addRow(with: arr)
num += 1
}
if let value = value.tilerUtilization {
let arr = keyValueRow("\(localizedString("Tiler utilization")):", "\(Int(value*100))%")
self.tilerUtilization = arr.last
grid.addRow(with: arr)
num += 1
}
self.setFrameSize(NSSize(width: self.frame.width, height: (16 * num) + Constants.Popup.margins))
grid.setFrameSize(NSSize(width: grid.frame.width, height: self.frame.height - Constants.Popup.margins))
self.addSubview(grid)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func keyValueRow(_ key: String, _ value: String) -> [NSTextField] {
return [
LabelField(frame: NSRect(x: 0, y: 0, width: 0, height: 16), key),
ValueField(frame: NSRect(x: 0, y: 0, width: 0, height: 16), value)
]
}
public func update(_ gpu: GPU_Info) {
self.status?.stringValue = gpu.state ? localizedString("Active") : localizedString("Non active")
if let value = gpu.fanSpeed {
self.fanSpeed?.stringValue = "\(value)%"
}
if let value = gpu.coreClock {
self.coreClock?.stringValue = "\(value)MHz"
}
if let value = gpu.memoryClock {
self.memoryClock?.stringValue = "\(value)MHz"
}
if let value = gpu.temperature {
self.temperature?.stringValue = Kit.temperature(Double(value))
}
if let value = gpu.utilization {
self.utilization?.stringValue = "\(Int(value*100))%"
}
if let value = gpu.renderUtilization {
self.renderUtilization?.stringValue = "\(Int(value*100))%"
}
if let value = gpu.tilerUtilization {
self.tilerUtilization?.stringValue = "\(Int(value*100))%"
}
}
}
================================================
FILE: Modules/GPU/portal.swift
================================================
//
// portal.swift
// GPU
//
// Created by Serhiy Mytrovtsiy on 18/02/2023
// Using Swift 5.0
// Running on macOS 13.2
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
public class Portal: PortalWrapper {
private var circle: HalfCircleGraphView? = nil
private var usageField: NSTextField? = nil
private var renderField: NSTextField? = nil
private var tilerField: NSTextField? = nil
private var initialized: Bool = false
public override func load() {
let view = NSStackView()
view.orientation = .horizontal
view.distribution = .fillEqually
view.spacing = Constants.Popup.spacing*2
view.edgeInsets = NSEdgeInsets(
top: 0,
left: Constants.Popup.spacing*2,
bottom: 0,
right: Constants.Popup.spacing*2
)
let chartsView = self.charts()
let detailsView = self.details()
view.addArrangedSubview(chartsView)
view.addArrangedSubview(detailsView)
self.addArrangedSubview(view)
chartsView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
}
private func charts() -> NSView {
let view = NSStackView()
view.orientation = .vertical
view.distribution = .fillEqually
view.spacing = Constants.Popup.spacing*2
view.edgeInsets = NSEdgeInsets(
top: Constants.Popup.spacing*4,
left: Constants.Popup.spacing*4,
bottom: Constants.Popup.spacing*4,
right: Constants.Popup.spacing*4
)
let chart = HalfCircleGraphView()
chart.toolTip = localizedString("GPU usage")
view.addArrangedSubview(chart)
self.circle = chart
return view
}
private func details() -> NSView {
let view = NSStackView()
view.orientation = .vertical
view.distribution = .fillEqually
view.spacing = Constants.Popup.spacing*2
self.usageField = portalRow(view, title: "\(localizedString("Usage")):").1
self.renderField = portalRow(view, title: "\(localizedString("Render")):").1
self.tilerField = portalRow(view, title: "\(localizedString("Tiler")):").1
return view
}
internal func callback(_ value: GPU_Info) {
DispatchQueue.main.async(execute: {
if (self.window?.isVisible ?? false) || !self.initialized {
if let value = value.utilization {
self.usageField?.stringValue = "\(Int(value*100))%"
}
if let value = value.renderUtilization {
self.renderField?.stringValue = "\(Int(value*100))%"
}
if let value = value.tilerUtilization {
self.tilerField?.stringValue = "\(Int(value*100))%"
}
self.circle?.toolTip = "\(localizedString("GPU usage")): \(Int(value.utilization!*100))%"
self.circle?.setValue(value.utilization!)
self.circle?.setText("\(Int(value.utilization!*100))%")
self.initialized = true
}
})
}
}
================================================
FILE: Modules/GPU/reader.swift
================================================
//
// reader.swift
// GPU
//
// Created by Serhiy Mytrovtsiy on 17/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
public struct device {
public let vendor: String?
public let model: String
public let pci: String
public var used: Bool
}
let vendors: [Data: String] = [
Data.init([0x86, 0x80, 0x00, 0x00]): "Intel",
Data.init([0x02, 0x10, 0x00, 0x00]): "AMD"
]
internal class InfoReader: Reader {
private var gpus: GPUs = GPUs()
private var displays: [gpu_s] = []
private var devices: [device] = []
public override func setup() {
if let list = SystemKit.shared.device.info.gpu {
self.displays = list
}
guard let PCIdevices = fetchIOService("IOPCIDevice") else {
return
}
let devices = PCIdevices.filter{ $0.object(forKey: "IOName") as? String == "display" }
devices.forEach { (dict: NSDictionary) in
guard let deviceID = dict["device-id"] as? Data, let vendorID = dict["vendor-id"] as? Data else {
error("device-id or vendor-id not found", log: self.log)
return
}
let pci = "0x" + Data([deviceID[1], deviceID[0], vendorID[1], vendorID[0]]).map { String(format: "%02hhX", $0) }.joined().lowercased()
guard let modelData = dict["model"] as? Data, let modelName = String(data: modelData, encoding: .ascii) else {
error("GPU model not found", log: self.log)
return
}
let model = modelName.replacingOccurrences(of: "\0", with: "")
var vendor: String? = nil
if let v = vendors.first(where: { $0.key == vendorID }) {
vendor = v.value
}
self.devices.append(device(
vendor: vendor,
model: model,
pci: pci,
used: false
))
}
}
public override func read() {
guard let accelerators = fetchIOService(kIOAcceleratorClassName) else {
return
}
var devices = self.devices
for (index, accelerator) in accelerators.enumerated() {
guard let IOClass = accelerator.object(forKey: "IOClass") as? String else {
error("IOClass not found", log: self.log)
return
}
guard let stats = accelerator["PerformanceStatistics"] as? [String: Any] else {
error("PerformanceStatistics not found", log: self.log)
return
}
var id: String = ""
var vendor: String? = nil
var model: String = ""
var cores: Int? = nil
let accMatch = (accelerator["IOPCIMatch"] as? String ?? accelerator["IOPCIPrimaryMatch"] as? String ?? "").lowercased()
for (i, device) in devices.enumerated() {
if accMatch.range(of: device.pci) != nil && !device.used {
model = device.model
vendor = device.vendor
id = "\(model) #\(index)"
devices[i].used = true
break
}
}
let ioClass = IOClass.lowercased()
var predictModel = ""
var type: GPU_types = .unknown
let utilization: Int? = stats["Device Utilization %"] as? Int ?? stats["GPU Activity(%)"] as? Int ?? nil
let renderUtilization: Int? = stats["Renderer Utilization %"] as? Int ?? nil
let tilerUtilization: Int? = stats["Tiler Utilization %"] as? Int ?? nil
var temperature: Int? = stats["Temperature(C)"] as? Int ?? nil
let fanSpeed: Int? = stats["Fan Speed(%)"] as? Int ?? nil
let coreClock: Int? = stats["Core Clock(MHz)"] as? Int ?? nil
let memoryClock: Int? = stats["Memory Clock(MHz)"] as? Int ?? nil
if ioClass == "nvaccelerator" || ioClass.contains("nvidia") { // nvidia
predictModel = "Nvidia Graphics"
type = .discrete
} else if ioClass.contains("amd") { // amd
predictModel = "AMD Graphics"
type = .discrete
if temperature == nil || temperature == 0 {
if let tmp = SMC.shared.getValue("TGDD"), tmp != 128 {
temperature = Int(tmp)
}
}
} else if ioClass.contains("intel") { // intel
predictModel = "Intel Graphics"
type = .integrated
if temperature == nil || temperature == 0 {
if let tmp = SMC.shared.getValue("TCGC"), tmp != 128 {
temperature = Int(tmp)
}
}
} else if ioClass.contains("agx") { // apple
predictModel = stats["model"] as? String ?? "Apple Graphics"
if let display = self.displays.first(where: { $0.vendor == "sppci_vendor_Apple" }) {
if let name = display.name {
predictModel = name
}
if let num = display.cores {
cores = num
}
}
type = .integrated
} else {
predictModel = "Unknown"
type = .unknown
}
if model == "" {
model = predictModel
}
if let v = vendor {
model = model.removedRegexMatches(pattern: v, replaceWith: "").trimmingCharacters(in: .whitespacesAndNewlines)
}
if self.gpus.list.first(where: { $0.id == id }) == nil {
self.gpus.list.append(GPU_Info(
id: id,
type: type.rawValue,
IOClass: IOClass,
vendor: vendor,
model: model,
cores: cores
))
}
guard let idx = self.gpus.list.firstIndex(where: { $0.id == id }) else {
return
}
if let agcInfo = accelerator["AGCInfo"] as? [String: Int], let state = agcInfo["poweredOffByAGC"] {
self.gpus.list[idx].state = state == 0
}
if var value = utilization {
if value > 100 {
value = 100
}
self.gpus.list[idx].utilization = Double(value)/100
}
if var value = renderUtilization {
if value > 100 {
value = 100
}
self.gpus.list[idx].renderUtilization = Double(value)/100
}
if var value = tilerUtilization {
if value > 100 {
value = 100
}
self.gpus.list[idx].tilerUtilization = Double(value)/100
}
if let value = temperature {
self.gpus.list[idx].temperature = Double(value)
}
if let value = fanSpeed {
self.gpus.list[idx].fanSpeed = value
}
if let value = coreClock {
self.gpus.list[idx].coreClock = value
}
if let value = memoryClock {
self.gpus.list[idx].memoryClock = value
}
}
self.gpus.list.sort{ !$0.state && $1.state }
self.callback(self.gpus)
}
}
================================================
FILE: Modules/GPU/settings.swift
================================================
//
// settings.swift
// GPU
//
// Created by Serhiy Mytrovtsiy on 17/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class Settings: NSStackView, Settings_v {
private var updateIntervalValue: Int = 1
private var selectedGPU: String
private var showTypeValue: Bool = false
private let title: String
public var selectedGPUHandler: (String) -> Void = {_ in }
public var callback: (() -> Void) = {}
public var setInterval: ((_ value: Int) -> Void) = {_ in }
private var hyperthreadView: NSView? = nil
private var button: NSPopUpButton?
public init(_ module: ModuleType) {
self.title = module.stringValue
self.selectedGPU = Store.shared.string(key: "\(self.title)_gpu", defaultValue: "")
self.updateIntervalValue = Store.shared.int(key: "\(self.title)_updateInterval", defaultValue: self.updateIntervalValue)
self.showTypeValue = Store.shared.bool(key: "\(self.title)_showType", defaultValue: self.showTypeValue)
super.init(frame: NSRect(x: 0, y: 0, width: 0, height: 0))
self.wantsLayer = true
self.orientation = .vertical
self.distribution = .gravityAreas
self.spacing = Constants.Settings.margin
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func load(widgets: [widget_t]) {
self.subviews.forEach{ $0.removeFromSuperview() }
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Update interval"), component: selectView(
action: #selector(self.changeUpdateInterval),
items: ReaderUpdateIntervals,
selected: "\(self.updateIntervalValue)"
))
]))
#if arch(x86_64)
if !widgets.filter({ $0 == .mini }).isEmpty {
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Show GPU type"), component: switchView(
action: #selector(self.toggleShowType),
state: self.showTypeValue
))
]))
}
#endif
self.button = selectView(
action: #selector(self.handleSelection),
items: [],
selected: ""
)
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("GPU to show"), component: self.button!)
]))
}
internal func setList(_ gpus: GPUs) {
var list: [KeyValue_t] = [
KeyValue_t(key: "automatic", value: "Automatic"),
KeyValue_t(key: "separator", value: "separator")
]
gpus.active().forEach{ list.append(KeyValue_t(key: $0.model, value: $0.model)) }
DispatchQueue.main.async(execute: {
guard let button = self.button else { return }
if button.menu?.items.count != list.count {
let menu = NSMenu()
list.forEach { (item) in
if item.key.contains("separator") {
menu.addItem(NSMenuItem.separator())
} else {
let interfaceMenu = NSMenuItem(title: localizedString(item.value), action: nil, keyEquivalent: "")
interfaceMenu.representedObject = item.key
menu.addItem(interfaceMenu)
if self.selectedGPU == item.key {
interfaceMenu.state = .on
}
}
}
button.menu = menu
button.sizeToFit()
}
})
}
@objc private func changeUpdateInterval(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let value = Int(key) else { return }
self.updateIntervalValue = value
Store.shared.set(key: "\(self.title)_updateInterval", value: value)
self.setInterval(value)
}
@objc private func handleSelection(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.selectedGPU = key
Store.shared.set(key: "\(self.title)_gpu", value: key)
self.selectedGPUHandler(key)
}
@objc private func toggleShowType(_ sender: NSControl) {
self.showTypeValue = controlState(sender)
Store.shared.set(key: "\(self.title)_showType", value: self.showTypeValue)
self.callback()
}
}
================================================
FILE: Modules/GPU/widget.swift
================================================
//
// widget.swift
// GPU
//
// Created by Serhiy Mytrovtsiy on 17/07/2024
// Using Swift 5.0
// Running on macOS 14.5
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//
import SwiftUI
import WidgetKit
import Charts
import Kit
public struct GPU_entry: TimelineEntry {
public static let kind = "GPUWidget"
public static var snapshot: GPU_entry = GPU_entry(value: GPU_Info(id: "", type: "", IOClass: "", model: "", cores: nil, utilization: 0.11, render: 0.11, tiler: 0.11))
public var date: Date {
Calendar.current.date(byAdding: .second, value: 5, to: Date())!
}
public var value: GPU_Info? = nil
}
public struct Provider: TimelineProvider {
public typealias Entry = GPU_entry
private let userDefaults: UserDefaults? = UserDefaults(suiteName: "\(Bundle.main.object(forInfoDictionaryKey: "TeamId") as! String).eu.exelban.Stats.widgets")
public func placeholder(in context: Context) -> GPU_entry {
GPU_entry()
}
public func getSnapshot(in context: Context, completion: @escaping (GPU_entry) -> Void) {
completion(GPU_entry.snapshot)
}
public func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
self.userDefaults?.set(Date().timeIntervalSince1970, forKey: GPU_entry.kind)
var entry = GPU_entry()
if let raw = userDefaults?.data(forKey: "GPU@InfoReader"), let load = try? JSONDecoder().decode(GPU_Info.self, from: raw) {
entry.value = load
}
let entries: [GPU_entry] = [entry]
completion(Timeline(entries: entries, policy: .atEnd))
}
}
@available(macOS 14.0, *)
public struct GPUWidget: Widget {
var usedColor: Color = Color(nsColor: NSColor.systemBlue)
var freeColor: Color = Color(nsColor: NSColor.lightGray)
public init() {}
public var body: some WidgetConfiguration {
StaticConfiguration(kind: GPU_entry.kind, provider: Provider()) { entry in
VStack(spacing: 10) {
if let value = entry.value {
HStack {
Chart {
SectorMark(angle: .value(localizedString("Used"), value.utilization ?? 0), innerRadius: .ratio(0.8)).foregroundStyle(self.usedColor)
SectorMark(angle: .value(localizedString("Free"), 1-(value.utilization ?? 0)), innerRadius: .ratio(0.8)).foregroundStyle(self.freeColor)
}
.frame(maxWidth: .infinity, maxHeight: 84)
.chartLegend(.hidden)
.chartBackground { chartProxy in
GeometryReader { geometry in
if let anchor = chartProxy.plotFrame {
let frame = geometry[anchor]
Text("\(Int((value.utilization ?? 0)*100))%")
.font(.system(size: 14, weight: .regular))
.position(x: frame.midX, y: frame.midY-5)
Text("GPU")
.font(.system(size: 8, weight: .semibold))
.position(x: frame.midX, y: frame.midY+8)
}
}
}
}
VStack(spacing: 3) {
HStack {
Text(localizedString("Usage")).font(.system(size: 12, weight: .regular)).foregroundColor(.secondary)
Spacer()
Text("\(Int((value.utilization ?? 0)*100))%")
}
HStack {
Text(localizedString("Render")).font(.system(size: 12, weight: .regular)).foregroundColor(.secondary)
Spacer()
Text("\(Int((value.renderUtilization ?? 0)*100))%")
}
HStack {
Text(localizedString("Tiler")).font(.system(size: 12, weight: .regular)).foregroundColor(.secondary)
Spacer()
Text("\(Int((value.tilerUtilization ?? 0)*100))%")
}
}
} else {
Text("No data")
}
}
.containerBackground(for: .widget) {
Color.clear
}
}
.configurationDisplayName("GPU widget")
.description("Displays GPU stats")
.supportedFamilies([.systemSmall])
}
}
================================================
FILE: Modules/Net/Info.plist
================================================
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
1.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSHumanReadableCopyright
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
================================================
FILE: Modules/Net/config.plist
================================================
Name
Network
State
Symbol
network
Widgets
label
Default
Order
0
speed
Default
Symbols
Output
U
Input
D
Words
Output
Upload speed
Input
Download speed
Order
1
network_chart
Default
Order
2
Unsupported colors
utilization
pressure
system
state
Default
Order
3
Preview
Value
text
Default
Order
4
Preview
Value
192.168.0.1
Settings
popup
notifications
================================================
FILE: Modules/Net/main.swift
================================================
//
// main.swift
// Net
//
// Created by Serhiy Mytrovtsiy on 24/05/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
import SystemConfiguration
import WidgetKit
public enum Network_t: String, Codable {
case wifi
case ethernet
case bluetooth
case other
}
public struct Network_interface: Codable {
var status: Bool = false
var displayName: String = ""
var BSDName: String = ""
var address: String = ""
var transmitRate: Double = 0
}
public struct Network_addr: Codable {
var v4: String? = nil
var v6: String? = nil
var countryCode: String? = nil
}
public struct Network_wifi: Codable {
var countryCode: String? = nil
var ssid: String? = nil
var bssid: String? = nil
var RSSI: Int? = nil
var noise: Int? = nil
var standard: String? = nil
var mode: String? = nil
var security: String? = nil
var channel: String? = nil
var channelBand: String? = nil
var channelWidth: String? = nil
var channelNumber: String? = nil
mutating func reset() {
self.countryCode = nil
self.ssid = nil
self.RSSI = nil
self.noise = nil
self.standard = nil
self.mode = nil
self.security = nil
self.channel = nil
}
}
public struct Bandwidth: Codable {
var upload: Int64 = 0
var download: Int64 = 0
}
public struct Network_Usage: Codable, RemoteType {
var bandwidth: Bandwidth = Bandwidth()
var total: Bandwidth = Bandwidth()
var laddr: Network_addr = Network_addr() // local ip
var raddr: Network_addr = Network_addr() // remote ip
var dns: [String] = []
var interface: Network_interface? = nil
var connectionType: Network_t? = nil
var status: Bool = false
var wifiDetails: Network_wifi = Network_wifi()
mutating func reset() {
self.bandwidth = Bandwidth()
self.laddr = Network_addr()
self.raddr = Network_addr()
self.dns = []
self.interface = nil
self.connectionType = nil
self.wifiDetails.reset()
}
public func remote() -> Data? {
let addr = "\(self.laddr.v4 ?? ""),\(self.laddr.v6 ?? ""),\(self.raddr.v4 ?? ""),\(self.raddr.v6 ?? "")"
let string = "1,\(self.interface?.BSDName ?? ""),1,\(self.bandwidth.download),\(self.bandwidth.upload),\(addr)$"
return string.data(using: .utf8)
}
}
public struct Network_Connectivity: Codable {
var status: Bool = false
var latency: Double = 0
var jitter: Double = 0
}
public struct Network_Process: Codable, Process_p {
public var pid: Int
public var name: String
public var time: Date
public var download: Int
public var upload: Int
public var icon: NSImage {
get {
if let app = NSRunningApplication(processIdentifier: pid_t(self.pid)), let icon = app.icon {
return icon
}
return Constants.defaultProcessIcon
}
}
public init(pid: Int = 0, name: String = "", time: Date = Date(), download: Int = 0, upload: Int = 0) {
self.pid = pid
self.name = name
self.time = time
self.download = download
self.upload = upload
}
}
public class Network: Module {
private let popupView: Popup
private let settingsView: Settings
private let portalView: Portal
private let notificationsView: Notifications
private var usageReader: UsageReader? = nil
private var processReader: ProcessReader? = nil
private var connectivityReader: ConnectivityReader? = nil
private let ipUpdater = NSBackgroundActivityScheduler(identifier: "eu.exelban.Stats.Network.IP")
private let usageReseter = NSBackgroundActivityScheduler(identifier: "eu.exelban.Stats.Network.Usage")
private var widgetActivationThresholdState: Bool {
Store.shared.bool(key: "\(self.config.name)_widgetActivationThresholdState", defaultValue: false)
}
private var widgetActivationThreshold: Int {
Store.shared.int(key: "\(self.config.name)_widgetActivationThreshold", defaultValue: 0)
}
private var widgetActivationThresholdSize: SizeUnit {
SizeUnit.fromString(Store.shared.string(key: "\(self.name)_widgetActivationThresholdSize", defaultValue: SizeUnit.MB.key))
}
private var publicIPRefreshInterval: String {
Store.shared.string(key: "\(self.name)_publicIPRefreshInterval", defaultValue: "never")
}
private var textValue: String {
Store.shared.string(key: "\(self.name)_textWidgetValue", defaultValue: "$addr.public - $status")
}
private var systemWidgetsUpdatesState: Bool {
self.userDefaults?.bool(forKey: "systemWidgetsUpdates_state") ?? false
}
public init() {
self.settingsView = Settings(.network)
self.popupView = Popup(.network)
self.portalView = Portal(.network)
self.notificationsView = Notifications(.network)
super.init(
moduleType: .network,
popup: self.popupView,
settings: self.settingsView,
portal: self.portalView,
notifications: self.notificationsView
)
guard self.available else { return }
self.usageReader = UsageReader(.network) { [weak self] value in
self?.usageCallback(value)
}
self.processReader = ProcessReader(.network) { [weak self] value in
if let list = value {
self?.popupView.processCallback(list)
}
}
self.connectivityReader = ConnectivityReader(.network) { [weak self] value in
self?.connectivityCallback(value)
}
self.settingsView.callbackWhenUpdateNumberOfProcesses = {
self.popupView.numberOfProcessesUpdated()
DispatchQueue.global(qos: .background).async {
self.processReader?.read()
}
}
self.settingsView.callback = { [weak self] in
self?.usageReader?.getDetails()
self?.usageReader?.read()
}
self.settingsView.usageResetCallback = { [weak self] in
self?.setUsageReset()
}
self.settingsView.connectivityHostCallback = { [weak self] isDisabled in
if isDisabled {
self?.popupView.resetConnectivityView()
self?.connectivityCallback(Network_Connectivity(status: false))
}
}
self.settingsView.setInterval = { [weak self] value in
self?.connectivityReader?.setInterval(value)
}
self.settingsView.publicIPRefreshIntervalCallback = { [weak self] in
self?.setIPUpdater()
}
self.setReaders([self.usageReader, self.processReader, self.connectivityReader])
self.setIPUpdater()
self.setUsageReset()
}
public override func isAvailable() -> Bool {
var list: [String] = []
for interface in SCNetworkInterfaceCopyAll() as NSArray {
if let displayName = SCNetworkInterfaceGetLocalizedDisplayName(interface as! SCNetworkInterface) {
list.append(displayName as String)
}
}
return !list.isEmpty
}
private func usageCallback(_ raw: Network_Usage?) {
guard let value = raw, self.enabled else { return }
self.popupView.usageCallback(value)
self.portalView.usageCallback(value)
self.notificationsView.usageCallback(value)
var upload: Int64 = value.bandwidth.upload
var download: Int64 = value.bandwidth.download
if self.widgetActivationThresholdState {
upload = 0
download = 0
let threshold = self.widgetActivationThresholdSize.toBytes(self.widgetActivationThreshold)
if value.bandwidth.upload >= threshold || value.bandwidth.download >= threshold {
upload = value.bandwidth.upload
download = value.bandwidth.download
}
}
self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: SWidget) in
switch w.item {
case let widget as SpeedWidget: widget.setValue(input: download, output: upload)
case let widget as NetworkChart: widget.setValue(upload: Double(upload), download: Double(download))
case let widget as TextWidget:
var text = self.textValue
let pairs = TextWidget.parseText(text)
pairs.forEach { pair in
var replacement: String? = nil
switch pair.key {
case "$addr":
switch pair.value {
case "public": replacement = value.raddr.v4 ?? value.raddr.v6 ?? "-"
case "publicV4": replacement = value.raddr.v4 ?? "-"
case "publicV6": replacement = value.raddr.v6 ?? "-"
case "private": replacement = value.laddr.v4 ?? value.laddr.v6 ?? "-"
case "privateV4": replacement = value.laddr.v4 ?? "-"
case "privateV6": replacement = value.laddr.v6 ?? "-"
case "countryCode": replacement = value.raddr.countryCode ?? "-"
case "flag": replacement = value.raddr.countryCode != nil ? countryFlag(value.raddr.countryCode!) : "-"
default: return
}
case "$interface":
switch pair.value {
case "displayName": replacement = value.interface?.displayName ?? "-"
case "BSDName": replacement = value.interface?.BSDName ?? "-"
case "address": replacement = value.interface?.address ?? "-"
case "transmitRate": replacement = "\(value.interface?.transmitRate ?? 0)"
default: return
}
case "$wifi":
switch pair.value {
case "ssid": replacement = value.wifiDetails.ssid ?? "-"
case "bssid": replacement = value.wifiDetails.bssid ?? "-"
case "RSSI": replacement = "\(value.wifiDetails.RSSI ?? 0)"
case "noise": replacement = "\(value.wifiDetails.noise ?? 0)"
case "standard": replacement = value.wifiDetails.standard ?? "-"
case "mode": replacement = value.wifiDetails.mode ?? "-"
case "security": replacement = value.wifiDetails.security ?? "-"
case "channel": replacement = value.wifiDetails.channel ?? "-"
case "channelBand": replacement = value.wifiDetails.channelBand ?? "-"
case "channelWidth": replacement = value.wifiDetails.channelWidth ?? "-"
case "channelNumber": replacement = value.wifiDetails.channelNumber ?? "-"
default: return
}
case "$status":
replacement = localizedString(value.status ? "UP" : "DOWN")
case "$upload":
switch pair.value {
case "total": replacement = Units(bytes: value.total.upload).getReadableMemory()
default: replacement = Units(bytes: value.bandwidth.upload).getReadableMemory()
}
case "$download":
switch pair.value {
case "total": replacement = Units(bytes: value.total.download).getReadableMemory()
default: replacement = Units(bytes: value.bandwidth.download).getReadableMemory()
}
case "$type":
replacement = value.connectionType?.rawValue ?? "-"
case "$icmp":
guard let connectivity = self.connectivityReader?.value else { return }
switch pair.value {
case "status": replacement = localizedString(connectivity.status ? "UP" : "DOWN")
case "latency": replacement = "\(Int(connectivity.latency)) ms"
default: return
}
default: return
}
if let replacement {
let key = pair.value.isEmpty ? pair.key : "\(pair.key).\(pair.value)"
text = text.replacingOccurrences(of: key, with: replacement)
}
}
widget.setValue(text)
default: break
}
}
if self.systemWidgetsUpdatesState {
if isWidgetActive(self.userDefaults, [Network_entry.kind]), let blobData = try? JSONEncoder().encode(raw) {
self.userDefaults?.set(blobData, forKey: "Network@UsageReader")
}
WidgetCenter.shared.reloadTimelines(ofKind: Network_entry.kind)
}
}
private func connectivityCallback(_ raw: Network_Connectivity?) {
guard let value = raw, self.enabled else { return }
self.popupView.connectivityCallback(value)
self.notificationsView.connectivityCallback(value)
self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: SWidget) in
switch w.item {
case let widget as StateWidget: widget.setValue(value.status)
default: break
}
}
}
private func setIPUpdater() {
self.ipUpdater.invalidate()
switch self.publicIPRefreshInterval {
case "hour":
self.ipUpdater.interval = 60 * 60
case "12":
self.ipUpdater.interval = 60 * 60 * 12
case "24":
self.ipUpdater.interval = 60 * 60 * 24
default: return
}
self.ipUpdater.repeats = true
self.ipUpdater.schedule { (completion: @escaping NSBackgroundActivityScheduler.CompletionHandler) in
guard self.enabled && self.isAvailable() else { return }
debug("going to automatically refresh IP address...")
NotificationCenter.default.post(name: .refreshPublicIP, object: nil, userInfo: nil)
completion(NSBackgroundActivityScheduler.Result.finished)
}
}
private func setUsageReset() {
self.usageReseter.invalidate()
switch AppUpdateInterval(rawValue: Store.shared.string(key: "\(self.config.name)_usageReset", defaultValue: AppUpdateInterval.never.rawValue)) {
case .oncePerDay: self.usageReseter.interval = 60 * 60 * 24
case .oncePerWeek: self.usageReseter.interval = 60 * 60 * 24 * 7
case .oncePerMonth: self.usageReseter.interval = 60 * 60 * 24 * 30
case .atStart: NotificationCenter.default.post(name: .resetTotalNetworkUsage, object: nil, userInfo: nil)
case .never: return
default: return
}
self.usageReseter.repeats = true
self.usageReseter.schedule { (completion: @escaping NSBackgroundActivityScheduler.CompletionHandler) in
guard self.enabled && self.isAvailable() else {
return
}
debug("going to reset the usage...")
NotificationCenter.default.post(name: .resetTotalNetworkUsage, object: nil, userInfo: nil)
completion(NSBackgroundActivityScheduler.Result.finished)
}
}
}
================================================
FILE: Modules/Net/notifications.swift
================================================
//
// notifications.swift
// Net
//
// Created by Serhiy Mytrovtsiy on 25/01/2025
// Using Swift 6.0
// Running on macOS 15.1
//
// Copyright © 2025 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
class Notifications: NotificationsWrapper {
private let connectionID: String = "connection"
private let connectionThresholdID: String = "connection_threshold"
private let interfaceID: String = "interface"
private let localID: String = "localIP"
private let publicID: String = "publicIP"
private let wifiID: String = "wifi"
private var connectionState: Bool = false
private var connectionThreshold: Int = 2
private var interfaceState: Bool = false
private var localIPState: Bool = false
private var publicIPState: Bool = false
private var wifiState: Bool = false
private var connection: Bool?
private var connectionCount: Int = 0
private var connectionPrev: Bool?
private var interface: String?
private var localIP: String?
private var publicIP: String?
private var wifi: String?
private var localIPCount: Int = 0
private var localIPThreshold: Int = 3
private var publicIPCount: Int = 0
private var publicIPThreshold: Int = 3
private var connectionInit: Bool = false
private var interfaceInit: Bool = false
private var localIPInit: Bool = false
private var publicIPInit: Bool = false
private var wifiInit: Bool = false
public init(_ module: ModuleType) {
super.init(module, [self.connectionID, self.interfaceID, self.localID, self.publicID, self.wifiID])
self.connectionState = Store.shared.bool(key: "\(self.module)_notifications_connection_state", defaultValue: self.connectionState)
self.connectionThreshold = Store.shared.int(key: "\(self.module)_notifications_connection_threshold", defaultValue: self.connectionThreshold)
self.interfaceState = Store.shared.bool(key: "\(self.module)_notifications_interface_state", defaultValue: self.interfaceState)
self.localIPState = Store.shared.bool(key: "\(self.module)_notifications_localIP_state", defaultValue: self.localIPState)
self.publicIPState = Store.shared.bool(key: "\(self.module)_notifications_publicIP_state", defaultValue: self.publicIPState)
self.wifiState = Store.shared.bool(key: "\(self.module)_notifications_wifi_state", defaultValue: self.wifiState)
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Status"), component: PreferencesSwitch(
action: self.toggleConnectionState, state: self.connectionState, with: StepperInput(
self.connectionThreshold, range: NSRange(location: 1, length: 9), visibileUnit: false,
callback: self.changeWidgetConnectionThreshold
)
)),
PreferencesRow(localizedString("Network interface"), component: switchView(
action: #selector(self.toggleInterfaceState),
state: self.interfaceState
)),
PreferencesRow(localizedString("Local IP"), component: switchView(
action: #selector(self.toggleLocalIPState),
state: self.localIPState
)),
PreferencesRow(localizedString("Public IP"), component: switchView(
action: #selector(self.toggleNPublicIPState),
state: self.publicIPState
)),
PreferencesRow(localizedString("WiFi network"), component: switchView(
action: #selector(self.toggleWiFiState),
state: self.wifiState
))
]))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func usageCallback(_ value: Network_Usage) {
if !self.interfaceInit {
self.interface = value.interface?.BSDName
self.interfaceInit = true
}
if !self.localIPInit {
if let v4 = value.laddr.v4 {
self.localIP = v4
self.localIPInit = true
} else if let v6 = value.laddr.v6 {
self.localIP = v6
self.localIPInit = true
}
}
if !self.publicIPInit {
if let v4 = value.raddr.v4 {
self.publicIP = v4
self.publicIPInit = true
} else if let v6 = value.raddr.v6 {
self.publicIP = v6
self.publicIPInit = true
}
}
if !self.wifiInit {
self.wifi = value.wifiDetails.ssid
self.wifiInit = true
}
if self.interfaceState {
if value.interface?.BSDName != self.interface {
self.newNotification(id: self.interfaceID, title: localizedString("Network interface changed"), subtitle: nil)
}
self.interface = value.interface?.BSDName
}
if self.localIPState {
let addr = value.laddr.v4 ?? value.laddr.v6
if addr != self.localIP {
self.localIPCount += 1
if self.localIPCount >= self.localIPThreshold {
var subtitle = ""
if let prev = self.localIP {
subtitle = localizedString("Previous IP", prev)
}
if let new = addr {
if !subtitle.isEmpty {
subtitle += "\n"
}
subtitle += localizedString("New IP", new)
}
self.newNotification(id: self.localID, title: localizedString("Local IP changed"), subtitle: subtitle)
self.localIP = addr
self.localIPCount = 0
}
} else {
self.localIPCount = 0
}
}
if self.publicIPState {
let addr = value.raddr.v4 ?? value.raddr.v6
if addr != self.publicIP {
self.publicIPCount += 1
if self.publicIPCount >= self.publicIPThreshold {
var subtitle = ""
if let prev = self.publicIP {
subtitle = localizedString("Previous IP", prev)
}
if let new = addr {
if !subtitle.isEmpty {
subtitle += "\n"
}
subtitle += localizedString("New IP", new)
}
self.newNotification(id: self.publicID, title: localizedString("Public IP changed"), subtitle: subtitle)
self.publicIP = addr
self.publicIPCount = 0
}
} else {
self.publicIPCount = 0
}
}
if self.wifiState {
if value.wifiDetails.ssid != self.wifi {
self.newNotification(id: self.wifiID, title: localizedString("WiFi network changed"), subtitle: nil)
}
self.wifi = value.wifiDetails.ssid
}
}
internal func connectivityCallback(_ value: Network_Connectivity) {
guard self.connectionState else { return }
if self.connection == nil {
self.connection = value.status
return
}
if self.connection != value.status {
self.connectionCount += 1
} else {
self.connectionCount = 0
}
if self.connectionCount >= self.connectionThreshold {
let title: String = value.status ? localizedString("Internet connection established") : localizedString("Internet connection lost")
self.newNotification(id: self.connectionID, title: title, subtitle: nil)
self.connection = value.status
self.connectionCount = 0
}
}
@objc private func toggleConnectionState(_ sender: NSControl) {
self.connectionState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_connection_state", value: self.connectionState)
}
@objc private func changeWidgetConnectionThreshold(_ newValue: Int) {
self.connectionThreshold = newValue
Store.shared.set(key: "\(self.module)_notifications_connection_threshold", value: newValue)
}
@objc private func toggleInterfaceState(_ sender: NSControl) {
self.interfaceState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_interface_state", value: self.interfaceState)
}
@objc private func toggleLocalIPState(_ sender: NSControl) {
self.localIPState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_localIP_state", value: self.localIPState)
}
@objc private func toggleNPublicIPState(_ sender: NSControl) {
self.publicIPState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_publicIP_state", value: self.publicIPState)
}
@objc private func toggleWiFiState(_ sender: NSControl) {
self.wifiState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_wifi_state", value: self.wifiState)
}
}
================================================
FILE: Modules/Net/popup.swift
================================================
//
// popup.swift
// Net
//
// Created by Serhiy Mytrovtsiy on 24/05/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
// swiftlint:disable:next type_body_length
internal class Popup: PopupWrapper {
private var uploadContainerView: NSView? = nil
private var uploadView: NSView? = nil
private var uploadValue: Int64 = 0
private var uploadValueField: NSTextField? = nil
private var uploadUnitField: NSTextField? = nil
private var uploadStateView: ColorView? = nil
private var downloadContainerView: NSView? = nil
private var downloadView: NSView? = nil
private var downloadValue: Int64 = 0
private var downloadValueField: NSTextField? = nil
private var downloadUnitField: NSTextField? = nil
private var downloadStateView: ColorView? = nil
private var downloadColorView: NSView? = nil
private var uploadColorView: NSView? = nil
private var totalUploadLabel: LabelField? = nil
private var totalUploadField: ValueField? = nil
private var totalDownloadLabel: LabelField? = nil
private var totalDownloadField: ValueField? = nil
private var statusField: ValueField? = nil
private var connectivityField: ValueField? = nil
private var latencyField: ValueField? = nil
private var jitterField: ValueField? = nil
private var interfaceView: NSStackView? = nil
private var interfaceField: ValueField? = nil
private var interfaceStatusField: ValueField? = nil
private var macAddressField: ValueField? = nil
private var ssidField: ValueField? = nil
private var standardField: ValueField? = nil
private var channelField: ValueField? = nil
private var ssidView: NSView? = nil
private var interfaceDetailsState: Bool = false
private var standardView: NSView? = nil
private var channelView: NSView? = nil
private var interfaceSpeedView: NSView? = nil
private var interfaceSpeedField: ValueField? = nil
private var dnsServersView: NSView? = nil
private var dnsServersField: ValueField? = nil
private var addressView: NSStackView? = nil
private var localIPField: ValueField? = nil
private var publicIPv4Field: ValueField? = nil
private var publicIPv6Field: ValueField? = nil
private var publicIPv4View: NSView? = nil
private var publicIPv6View: NSView? = nil
private var publicIPState: Bool = true
private var processesView: NSView? = nil
private var processes: ProcessesView? = nil
private var chart: NetworkChartView? = nil
private var reverseOrderState: Bool = false
private var chartHistory: Int = 180
private var chartScale: Scale = .none
private var chartFixedScale: Int = 12
private var chartFixedScaleSize: SizeUnit = .MB
private var chartPrefSection: PreferencesSection? = nil
private var connectivityChart: GridChartView? = nil
private var initialized: Bool = false
private var processesInitialized: Bool = false
private var connectionInitialized: Bool = false
private var lastReset: Date = Date()
private var latency: [Double] = []
private var jitter: [Double] = []
private var base: DataSizeBase {
DataSizeBase(rawValue: Store.shared.string(key: "\(self.title)_base", defaultValue: "byte")) ?? .byte
}
private var numberOfProcesses: Int {
Store.shared.int(key: "\(self.title)_processes", defaultValue: 8)
}
private var processesHeight: CGFloat {
(22*CGFloat(self.numberOfProcesses)) + (self.numberOfProcesses == 0 ? 0 : Constants.Popup.separatorHeight + 22)
}
private var downloadColorState: SColor = .secondBlue
private var downloadColor: NSColor {
var value = NSColor.systemBlue
if let color = self.downloadColorState.additional as? NSColor {
value = color
}
return value
}
private var uploadColorState: SColor = .secondRed
private var uploadColor: NSColor {
var value = NSColor.systemRed
if let color = self.uploadColorState.additional as? NSColor {
value = color
}
return value
}
public init(_ module: ModuleType) {
super.init(module, frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
self.spacing = 0
self.orientation = .vertical
self.downloadColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_downloadColor", defaultValue: self.downloadColorState.key))
self.uploadColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_uploadColor", defaultValue: self.uploadColorState.key))
self.reverseOrderState = Store.shared.bool(key: "\(self.title)_reverseOrder", defaultValue: self.reverseOrderState)
self.chartHistory = Store.shared.int(key: "\(self.title)_chartHistory", defaultValue: self.chartHistory)
self.chartScale = Scale.fromString(Store.shared.string(key: "\(self.title)_chartScale", defaultValue: self.chartScale.key))
self.chartFixedScale = Store.shared.int(key: "\(self.title)_chartFixedScale", defaultValue: self.chartFixedScale)
self.chartFixedScaleSize = SizeUnit.fromString(Store.shared.string(key: "\(self.title)_chartFixedScaleSize", defaultValue: self.chartFixedScaleSize.key))
self.publicIPState = Store.shared.bool(key: "\(self.title)_publicIP", defaultValue: self.publicIPState)
self.interfaceDetailsState = Store.shared.bool(key: "\(self.title)_interfaceDetails", defaultValue: self.interfaceDetailsState)
self.addArrangedSubview(self.initDashboard())
self.addArrangedSubview(self.initChart())
self.addArrangedSubview(self.initConnectivityChart())
self.addArrangedSubview(self.initDetails())
self.addArrangedSubview(self.initInterface())
self.addArrangedSubview(self.initAddress())
self.addArrangedSubview(self.initProcesses())
if !self.publicIPState {
self.addressView?.removeFromSuperview()
}
self.recalculateHeight()
NotificationCenter.default.addObserver(self, selector: #selector(self.resetTotalNetworkUsageCallback), name: .resetTotalNetworkUsage, object: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
NotificationCenter.default.removeObserver(self, name: .resetTotalNetworkUsage, object: nil)
}
private func recalculateHeight() {
var h: CGFloat = 0
self.arrangedSubviews.forEach { v in
if let v = v as? NSStackView {
h += v.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +)
} else {
h += v.bounds.height
}
}
if self.frame.size.height != h {
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.sizeCallback?(self.frame.size)
}
}
// MARK: - views
private func initDashboard() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 90))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let leftPart: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width / 2, height: view.frame.height))
let downloadFields = self.topValueView(leftPart, title: localizedString("Downloading"), color: self.downloadColor)
self.downloadContainerView = leftPart
self.downloadView = downloadFields.0
self.downloadValueField = downloadFields.1
self.downloadUnitField = downloadFields.2
self.downloadStateView = downloadFields.3
let rightPart: NSView = NSView(frame: NSRect(x: view.frame.width / 2, y: 0, width: view.frame.width / 2, height: view.frame.height))
let uploadFields = self.topValueView(rightPart, title: localizedString("Uploading"), color: self.uploadColor)
self.uploadContainerView = rightPart
self.uploadView = uploadFields.0
self.uploadValueField = uploadFields.1
self.uploadUnitField = uploadFields.2
self.uploadStateView = uploadFields.3
view.addSubview(leftPart)
view.addSubview(rightPart)
return view
}
private func initChart() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 90 + Constants.Popup.separatorHeight))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let separator = separatorView(localizedString("Usage history"), origin: NSPoint(x: 0, y: 90), width: self.frame.width)
let container: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: separator.frame.origin.y))
container.wantsLayer = true
container.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.1).cgColor
container.layer?.cornerRadius = 3
let chart = NetworkChartView(
frame: NSRect(x: 0, y: 1, width: container.frame.width, height: container.frame.height - 2),
num: self.chartHistory, reversedOrder: self.reverseOrderState, outColor: self.uploadColor, inColor: self.downloadColor,
scale: self.chartScale,
fixedScale: Double(self.chartFixedScaleSize.toBytes(self.chartFixedScale))
)
chart.base = self.base
container.addSubview(chart)
self.chart = chart
view.addSubview(separator)
view.addSubview(container)
return view
}
private func initConnectivityChart() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 30 + Constants.Popup.separatorHeight))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let separator = separatorView(localizedString("Connectivity history"), origin: NSPoint(x: 0, y: 30), width: self.frame.width)
let container: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: separator.frame.origin.y))
container.wantsLayer = true
container.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.1).cgColor
container.layer?.cornerRadius = 3
let chart = GridChartView(frame: NSRect(x: 0, y: 1, width: container.frame.width, height: container.frame.height - 2), grid: (30, 3))
container.addSubview(chart)
self.connectivityChart = chart
view.addSubview(separator)
view.addSubview(container)
return view
}
private func initDetails() -> NSView {
let view = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
view.orientation = .vertical
view.spacing = 0
let row: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: Constants.Popup.separatorHeight))
row.heightAnchor.constraint(equalToConstant: Constants.Popup.separatorHeight).isActive = true
let button = NSButtonWithPadding()
button.frame = CGRect(x: view.frame.width - 18, y: 6, width: 18, height: 18)
button.bezelStyle = .regularSquare
button.isBordered = false
button.imageScaling = NSImageScaling.scaleAxesIndependently
button.contentTintColor = .lightGray
button.action = #selector(self.resetTotalNetworkUsage)
button.target = self
button.toolTip = localizedString("Reset")
button.image = Bundle(for: Module.self).image(forResource: "refresh")!
row.addSubview(separatorView(localizedString("Details"), width: self.frame.width))
row.addSubview(button)
view.addArrangedSubview(row)
let totalUpload = popupWithColorRow(view, color: self.uploadColor, title: "\(localizedString("Total upload")):", value: "0")
let totalDownload = popupWithColorRow(view, color: self.downloadColor, title: "\(localizedString("Total download")):", value: "0")
self.uploadColorView = totalUpload.0
self.totalUploadLabel = totalUpload.1
self.totalUploadField = totalUpload.2
self.downloadColorView = totalDownload.0
self.totalDownloadLabel = totalDownload.1
self.totalDownloadField = totalDownload.2
self.statusField = popupRow(view, title: "\(localizedString("Status")):", value: localizedString("Unknown")).1
self.connectivityField = popupRow(view, title: "\(localizedString("Internet connection")):", value: localizedString("Unknown")).1
self.latencyField = popupRow(view, title: "\(localizedString("Latency")):", value: "0 ms").1
self.jitterField = popupRow(view, title: "\(localizedString("Jitter")):", value: "0 ms").1
return view
}
private func initInterface() -> NSView {
let view = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
view.orientation = .vertical
view.spacing = 0
let row: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: Constants.Popup.separatorHeight))
row.heightAnchor.constraint(equalToConstant: Constants.Popup.separatorHeight).isActive = true
let button = NSButtonWithPadding()
button.frame = CGRect(x: view.frame.width - 18, y: 6, width: 18, height: 18)
button.bezelStyle = .regularSquare
button.isBordered = false
button.imageScaling = NSImageScaling.scaleAxesIndependently
button.contentTintColor = .lightGray
button.action = #selector(self.toggleInterfaceDetails)
button.target = self
button.toolTip = localizedString("Details")
button.image = Bundle(for: Module.self).image(forResource: "tune")!
row.addSubview(separatorView(localizedString("Interface"), width: self.frame.width))
row.addSubview(button)
view.addArrangedSubview(row)
self.interfaceField = popupRow(view, title: "\(localizedString("Interface")):", value: localizedString("Unknown")).1
self.interfaceStatusField = popupRow(view, title: "\(localizedString("Status")):", value: localizedString("Unknown")).1
self.macAddressField = popupRow(view, title: "\(localizedString("Physical address")):", value: localizedString("Unknown")).1
self.macAddressField?.isSelectable = true
let ssid = popupRow(view, title: "\(localizedString("Network")):", value: localizedString("Unknown"))
let standard = popupRow(view, title: "\(localizedString("Standard")):", value: localizedString("Unavailable"))
let channel = popupRow(view, title: "\(localizedString("Channel")):", value: localizedString("Unavailable"))
let speed = popupRow(view, title: "\(localizedString("Speed")):", value: localizedString("Unknown"))
self.ssidField = ssid.1
self.standardField = standard.1
self.channelField = channel.1
self.interfaceSpeedField = speed.1
self.ssidView = ssid.2
self.standardView = standard.2
self.channelView = channel.2
self.interfaceSpeedView = speed.2
if !self.interfaceDetailsState {
self.standardView?.removeFromSuperview()
self.channelView?.removeFromSuperview()
self.interfaceSpeedView?.removeFromSuperview()
}
self.interfaceView = view
return view
}
private func initAddress() -> NSView {
let view = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
view.orientation = .vertical
view.spacing = 0
let row: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: Constants.Popup.separatorHeight))
row.heightAnchor.constraint(equalToConstant: Constants.Popup.separatorHeight).isActive = true
let button = NSButtonWithPadding()
button.frame = CGRect(x: view.frame.width - 18, y: 6, width: 18, height: 18)
button.bezelStyle = .regularSquare
button.isBordered = false
button.imageScaling = NSImageScaling.scaleAxesIndependently
button.contentTintColor = .lightGray
button.action = #selector(self.refreshPublicIP)
button.target = self
button.toolTip = localizedString("Refresh")
button.image = Bundle(for: Module.self).image(forResource: "refresh")!
row.addSubview(separatorView(localizedString("Address"), width: self.frame.width))
row.addSubview(button)
view.addArrangedSubview(row)
self.localIPField = popupRow(view, title: "\(localizedString("Local IP")):", value: localizedString("Unknown")).1
let ipV4 = popupRow(view, title: "\(localizedString("Public IP")):", value: localizedString("Unknown"))
let ipV6 = popupRow(view, title: "\(localizedString("Public IP")):", value: localizedString("Unknown"))
self.publicIPv4Field = ipV4.1
self.publicIPv6Field = ipV6.1
self.publicIPv4View = ipV4.2
self.publicIPv6View = ipV6.2
self.localIPField?.isSelectable = true
self.publicIPv4Field?.isSelectable = true
self.publicIPv6Field?.isSelectable = true
if let valueView = self.publicIPv6Field {
valueView.font = NSFont.systemFont(ofSize: 7, weight: .semibold)
valueView.setFrameOrigin(NSPoint(x: valueView.frame.origin.x, y: -1))
}
ipV4.2.removeFromSuperview()
ipV6.2.removeFromSuperview()
self.addressView = view
return view
}
private func initProcesses() -> NSView {
if self.numberOfProcesses == 0 {
let v = NSView()
self.processesView = v
return v
}
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.processesHeight))
let separator = separatorView(localizedString("Top processes"), origin: NSPoint(x: 0, y: self.processesHeight-Constants.Popup.separatorHeight), width: self.frame.width)
let container: ProcessesView = ProcessesView(
frame: NSRect(x: 0, y: 0, width: self.frame.width, height: separator.frame.origin.y),
values: [(localizedString("Downloading"), self.downloadColor), (localizedString("Uploading"), self.uploadColor)],
n: self.numberOfProcesses
)
self.processes = container
view.addSubview(separator)
view.addSubview(container)
self.processesView = view
return view
}
// MARK: - callbacks
public func numberOfProcessesUpdated() {
if self.processes?.count == self.numberOfProcesses { return }
DispatchQueue.main.async(execute: {
self.processesView?.removeFromSuperview()
self.processesView = nil
self.processes = nil
self.addArrangedSubview(self.initProcesses())
self.processesInitialized = false
self.recalculateHeight()
})
}
public func usageCallback(_ value: Network_Usage) {
DispatchQueue.main.async(execute: {
if (self.window?.isVisible ?? false) || !self.initialized {
var resized = false
self.uploadValue = value.bandwidth.upload
self.downloadValue = value.bandwidth.download
self.setUploadDownloadFields()
self.totalUploadField?.stringValue = Units(bytes: value.total.upload).getReadableMemory()
self.totalDownloadField?.stringValue = Units(bytes: value.total.download).getReadableMemory()
let form = DateComponentsFormatter()
form.maximumUnitCount = 2
form.unitsStyle = .full
form.allowedUnits = [.day, .hour, .minute]
if let duration = form.string(from: self.lastReset, to: Date()) {
self.totalUploadLabel?.toolTip = localizedString("Last reset", duration)
self.totalDownloadLabel?.toolTip = localizedString("Last reset", duration)
}
if let interface = value.interface {
self.interfaceField?.stringValue = "\(interface.displayName) (\(interface.BSDName)"
if let cc = value.wifiDetails.countryCode {
self.interfaceField?.stringValue += ", \(cc)"
}
self.interfaceField?.stringValue += ")"
self.interfaceStatusField?.stringValue = localizedString(interface.status ? "UP" : "DOWN")
self.macAddressField?.stringValue = interface.address
self.interfaceSpeedField?.stringValue = "\(Int(interface.transmitRate.rounded()))baseT"
} else {
self.interfaceField?.stringValue = localizedString("Unknown")
self.interfaceStatusField?.stringValue = localizedString("Unknown")
self.macAddressField?.stringValue = localizedString("Unknown")
self.interfaceSpeedField?.stringValue = localizedString("Unknown")
}
if value.connectionType == .wifi {
if let view = self.ssidView, view.superview == nil && value.wifiDetails.ssid != nil {
self.interfaceView?.addArrangedSubview(view)
resized = true
}
if self.interfaceDetailsState, let view = self.standardView, view.superview == nil && value.wifiDetails.standard != nil {
self.interfaceView?.addArrangedSubview(view)
resized = true
}
if self.interfaceDetailsState, let view = self.channelView, view.superview == nil && value.wifiDetails.channel != nil {
self.interfaceView?.addArrangedSubview(view)
resized = true
}
self.ssidField?.stringValue = value.wifiDetails.ssid ?? localizedString("Unknown")
if let v = value.wifiDetails.RSSI {
self.ssidField?.stringValue += " (\(v))"
}
self.standardField?.stringValue = value.wifiDetails.standard ?? localizedString("Unknown")
self.channelField?.stringValue = value.wifiDetails.channel ?? localizedString("Unknown")
var rssi = localizedString("Unknown")
if let v = value.wifiDetails.RSSI {
rssi = "\(v) dBm"
}
var noise = localizedString("Unknown")
if let v = value.wifiDetails.noise {
noise = "\(v) dBm"
}
let number = value.wifiDetails.channelNumber ?? localizedString("Unknown")
let band = value.wifiDetails.channelBand ?? localizedString("Unknown")
let width = value.wifiDetails.channelWidth ?? localizedString("Unknown")
self.channelField?.toolTip = "RSSI: \(rssi)\nNoise: \(noise)\nChannel number: \(number)\nChannel band: \(band)\nChannel width: \(width)\n"
} else {
if self.ssidView?.superview != nil {
self.ssidField?.stringValue = localizedString("Unavailable")
self.ssidView?.removeFromSuperview()
resized = true
}
if self.standardField?.superview != nil {
self.standardField?.stringValue = localizedString("Unavailable")
self.standardView?.removeFromSuperview()
resized = true
}
if self.channelView?.superview != nil {
self.channelField?.stringValue = localizedString("Unavailable")
self.channelView?.removeFromSuperview()
resized = true
}
}
var privateIP = localizedString("Unknown")
if let v4 = value.laddr.v4, !v4.isEmpty {
privateIP = v4
} else if let v6 = value.laddr.v6, !v6.isEmpty {
privateIP = v6
}
if self.localIPField?.stringValue != privateIP {
self.localIPField?.stringValue = privateIP
}
if let view = self.publicIPv4View {
if let addr = value.raddr.v4 {
if view.superview == nil {
self.addressView?.addArrangedSubview(view)
resized = true
}
var ip = addr
if let cc = value.raddr.countryCode, !cc.isEmpty {
if let flag = countryFlag(cc) {
ip += " \(flag)"
} else {
ip += " (\(cc))"
}
self.publicIPv4Field?.toolTip = cc
}
if self.publicIPv4Field?.stringValue != ip {
self.publicIPv4Field?.stringValue = ip
}
} else if view.superview != nil {
view.removeFromSuperview()
resized = true
self.publicIPv4Field?.stringValue = localizedString("Unknown")
}
}
if let view = self.publicIPv6View {
if let addr = value.raddr.v6 {
if view.superview == nil {
self.addressView?.addArrangedSubview(view)
resized = true
}
var ip = addr
if let cc = value.raddr.countryCode {
if let flag = countryFlag(cc) {
ip += " \(flag)"
} else {
ip += " (\(cc))"
}
self.publicIPv6Field?.toolTip = cc
}
if self.publicIPv6Field?.stringValue != ip {
self.publicIPv6Field?.stringValue = ip
}
} else if view.superview != nil {
view.removeFromSuperview()
resized = true
self.publicIPv6Field?.stringValue = localizedString("Unknown")
}
}
if self.interfaceDetailsState {
if !value.dns.isEmpty {
let servers = value.dns.joined(separator: "\n")
if self.dnsServersField == nil || value.dns.count != self.dnsServersField?.stringValue.split(separator: "\n").count {
if let view = self.dnsServersView {
view.removeFromSuperview()
}
let view = popupRow(self.interfaceView, title: "\(localizedString("DNS Server")):", value: servers, multiline: true)
self.dnsServersField = view.1
self.dnsServersView = view.2
self.dnsServersField?.isSelectable = true
}
if self.dnsServersField?.stringValue != servers {
self.dnsServersField?.stringValue = servers
}
resized = true
} else if let view = self.dnsServersView {
view.removeFromSuperview()
resized = true
}
}
self.statusField?.stringValue = localizedString(value.status ? "UP" : "DOWN")
if resized {
self.recalculateHeight()
}
self.initialized = true
}
if let chart = self.chart {
if chart.base != self.base {
chart.base = self.base
}
chart.addValue(upload: Double(value.bandwidth.upload), download: Double(value.bandwidth.download))
}
})
}
public func connectivityCallback(_ value: Network_Connectivity?) {
if self.latency.count >= 90 {
self.latency.remove(at: 0)
}
self.latency.append(value?.latency ?? 0)
if self.jitter.count >= 90 {
self.jitter.remove(at: 0)
}
self.jitter.append(value?.jitter ?? 0)
DispatchQueue.main.async(execute: {
if (self.window?.isVisible ?? false) || !self.connectionInitialized {
var text = "Unknown"
var latency = localizedString("Unknown")
var jitter = localizedString("Unknown")
if let v = value {
text = v.status ? "UP" : "DOWN"
if v.status && !self.latency.isEmpty {
latency = "\((self.latency.reduce(0, +) / Double(self.latency.count)).rounded(toPlaces: 2)) ms"
}
if v.status && !self.jitter.isEmpty {
jitter = "\((self.jitter.reduce(0, +) / Double(self.jitter.count)).rounded(toPlaces: 2)) ms"
}
}
self.latencyField?.stringValue = latency
self.jitterField?.stringValue = jitter
self.connectivityField?.stringValue = localizedString(text)
self.connectionInitialized = true
}
if let value, let chart = self.connectivityChart {
chart.addValue(value.status)
}
})
}
public func processCallback(_ list: [Network_Process]) {
DispatchQueue.main.async(execute: {
if !(self.window?.isVisible ?? false) && self.processesInitialized {
return
}
let list = list.map{ $0 }
if list.count != self.processes?.count { self.processes?.clear() }
for i in 0.. NSView? {
let view = SettingsContainerView()
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Keyboard shortcut"), component: KeyboardShartcutView(
callback: self.setKeyboardShortcut,
value: self.keyboardShortcut
))
]))
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Color of download"), component: selectView(
action: #selector(self.toggleDownloadColor),
items: SColor.allColors,
selected: self.downloadColorState.key
)),
PreferencesRow(localizedString("Color of upload"), component: selectView(
action: #selector(self.toggleUploadColor),
items: SColor.allColors,
selected: self.uploadColorState.key
))
]))
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Reverse order"), component: switchView(
action: #selector(self.toggleReverseOrder),
state: self.reverseOrderState
))
]))
self.chartPrefSection = PreferencesSection([
PreferencesRow(localizedString("Chart history"), component: selectView(
action: #selector(self.togglechartHistory),
items: LineChartHistory,
selected: "\(self.chartHistory)"
)),
PreferencesRow(localizedString("Main chart scaling"), component: selectView(
action: #selector(self.toggleChartScale),
items: Scale.allCases,
selected: self.chartScale.key
)),
PreferencesRow(localizedString("Scale value"), component: StepperInput(
self.chartFixedScale, range: NSRange(location: 1, length: 1023),
unit: self.chartFixedScaleSize.key, units: SizeUnit.allCases,
callback: self.toggleFixedScale, unitCallback: self.toggleFixedScaleSize
))
])
view.addArrangedSubview(self.chartPrefSection!)
self.chartPrefSection?.setRowVisibility(2, newState: self.chartScale == .fixed)
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Public IP"), component: switchView(
action: #selector(self.togglePublicIP),
state: self.publicIPState
))
]))
return view
}
@objc private func toggleUploadColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.uploadColorState = newValue
Store.shared.set(key: "\(self.title)_uploadColor", value: key)
if let color = newValue.additional as? NSColor {
self.processes?.setColor(1, color)
self.uploadColorView?.layer?.backgroundColor = color.cgColor
self.uploadStateView?.setColor(color)
self.chart?.setColors(out: color)
}
}
@objc private func toggleDownloadColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.downloadColorState = newValue
Store.shared.set(key: "\(self.title)_downloadColor", value: key)
if let color = newValue.additional as? NSColor {
self.processes?.setColor(0, color)
self.downloadColorView?.layer?.backgroundColor = color.cgColor
self.downloadStateView?.setColor(color)
self.chart?.setColors(in: color)
}
}
@objc private func toggleReverseOrder(_ sender: NSControl) {
self.reverseOrderState = controlState(sender)
self.chart?.setReverseOrder(self.reverseOrderState)
Store.shared.set(key: "\(self.title)_reverseOrder", value: self.reverseOrderState)
self.display()
}
@objc private func togglechartHistory(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let value = Int(key) else { return }
self.chartHistory = value
Store.shared.set(key: "\(self.title)_chartHistory", value: value)
self.chart?.reinit(self.chartHistory)
}
@objc private func toggleChartScale(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let value = Scale.allCases.first(where: { $0.key == key }) else { return }
self.chartScale = value
self.chart?.setScale(self.chartScale, Double(self.chartFixedScaleSize.toBytes(self.chartFixedScale)))
self.chartPrefSection?.setRowVisibility(2, newState: self.chartScale == .fixed)
Store.shared.set(key: "\(self.title)_chartScale", value: key)
self.display()
}
@objc private func togglePublicIP(_ sender: NSControl) {
self.publicIPState = controlState(sender)
Store.shared.set(key: "\(self.title)_publicIP", value: self.publicIPState)
DispatchQueue.main.async(execute: {
if !self.publicIPState {
self.addressView?.removeFromSuperview()
} else if let view = self.addressView {
self.insertArrangedSubview(view, at: 4)
}
self.recalculateHeight()
})
}
@objc private func toggleFixedScale(_ newValue: Int) {
self.chart?.setScale(self.chartScale, Double(self.chartFixedScaleSize.toBytes(newValue)))
Store.shared.set(key: "\(self.title)_chartFixedScale", value: newValue)
}
private func toggleFixedScaleSize(_ newValue: KeyValue_p) {
guard let newUnit = newValue as? SizeUnit else { return }
self.chartFixedScaleSize = newUnit
Store.shared.set(key: "\(self.title)_chartFixedScaleSize", value: self.chartFixedScaleSize.key)
self.display()
}
@objc private func toggleInterfaceDetails() {
self.interfaceDetailsState = !self.interfaceDetailsState
Store.shared.set(key: "\(self.title)_interfaceDetails", value: self.interfaceDetailsState)
if !self.interfaceDetailsState {
self.standardView?.removeFromSuperview()
self.channelView?.removeFromSuperview()
self.interfaceSpeedView?.removeFromSuperview()
self.dnsServersView?.removeFromSuperview()
} else {
if let view = self.standardView, view.superview == nil && self.standardField?.stringValue != localizedString("Unavailable") {
self.interfaceView?.addArrangedSubview(view)
}
if let view = self.channelView, view.superview == nil && self.channelField?.stringValue != localizedString("Unavailable") {
self.interfaceView?.addArrangedSubview(view)
}
if let view = self.interfaceSpeedView, view.superview == nil {
self.interfaceView?.addArrangedSubview(view)
}
if let view = self.dnsServersView, view.superview == nil {
self.interfaceView?.addArrangedSubview(view)
}
}
self.recalculateHeight()
}
// MARK: - helpers
private func topValueView(_ view: NSView, title: String, color: NSColor) -> (NSView, NSTextField, NSTextField, ColorView) {
let topHeight: CGFloat = 30
let titleHeight: CGFloat = 15
view.setAccessibilityElement(true)
view.toolTip = title
let valueWidth = "0".widthOfString(usingFont: .systemFont(ofSize: 26, weight: .light)) + 5
let unitWidth = "KB/s".widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light)) + 5
let topPartWidth = valueWidth + unitWidth
let topView: NSView = NSView(frame: NSRect(
x: (view.frame.width-topPartWidth)/2,
y: (view.frame.height - topHeight - titleHeight)/2 + titleHeight,
width: topPartWidth,
height: topHeight
))
let valueField = LabelField(frame: NSRect(x: 0, y: 0, width: valueWidth, height: 30), "0")
valueField.font = NSFont.systemFont(ofSize: 26, weight: .light)
valueField.textColor = .textColor
valueField.alignment = .right
let unitField = LabelField(frame: NSRect(x: valueField.frame.width, y: 4, width: unitWidth, height: 15), "KB/s")
unitField.font = NSFont.systemFont(ofSize: 13, weight: .light)
unitField.textColor = .labelColor
unitField.alignment = .left
let titleWidth: CGFloat = title.widthOfString(usingFont: NSFont.systemFont(ofSize: 12, weight: .regular))+8
let iconSize: CGFloat = 12
let bottomWidth: CGFloat = titleWidth+iconSize
let bottomView: NSView = NSView(frame: NSRect(
x: (view.frame.width-bottomWidth)/2,
y: topView.frame.origin.y - titleHeight,
width: bottomWidth,
height: titleHeight
))
let colorBlock: ColorView = ColorView(frame: NSRect(x: 0, y: 1, width: iconSize, height: iconSize), color: color, radius: 4)
let titleField = LabelField(frame: NSRect(x: iconSize, y: 0, width: titleWidth, height: titleHeight), title)
titleField.alignment = .center
topView.addSubview(valueField)
topView.addSubview(unitField)
bottomView.addSubview(colorBlock)
bottomView.addSubview(titleField)
view.addSubview(topView)
view.addSubview(bottomView)
return (topView, valueField, unitField, colorBlock)
}
private func setUploadDownloadFields() {
let upload = Units(bytes: self.uploadValue).getReadableTuple(base: self.base)
let download = Units(bytes: self.downloadValue).getReadableTuple(base: self.base)
self.uploadContainerView?.toolTip = "\(localizedString("Uploading")): \(upload.0)\(upload.1)"
self.downloadContainerView?.toolTip = "\(localizedString("Downloading")): \(download.0)\(download.1)"
var valueWidth = "\(upload.0)".widthOfString(usingFont: .systemFont(ofSize: 26, weight: .light)) + 5
var unitWidth = upload.1.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light)) + 5
var topPartWidth = valueWidth + unitWidth
self.uploadView?.setFrameSize(NSSize(width: topPartWidth, height: self.uploadView!.frame.height))
self.uploadView?.setFrameOrigin(NSPoint(x: ((self.frame.width/2)-topPartWidth)/2, y: self.uploadView!.frame.origin.y))
self.uploadValueField?.setFrameSize(NSSize(width: valueWidth, height: self.uploadValueField!.frame.height))
self.uploadValueField?.stringValue = "\(upload.0)"
self.uploadUnitField?.setFrameSize(NSSize(width: unitWidth, height: self.uploadUnitField!.frame.height))
self.uploadUnitField?.setFrameOrigin(NSPoint(x: self.uploadValueField!.frame.width, y: self.uploadUnitField!.frame.origin.y))
self.uploadUnitField?.stringValue = upload.1
valueWidth = "\(download.0)".widthOfString(usingFont: .systemFont(ofSize: 26, weight: .light)) + 5
unitWidth = download.1.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light)) + 5
topPartWidth = valueWidth + unitWidth
self.downloadView?.setFrameSize(NSSize(width: topPartWidth, height: self.downloadView!.frame.height))
self.downloadView?.setFrameOrigin(NSPoint(x: ((self.frame.width/2)-topPartWidth)/2, y: self.downloadView!.frame.origin.y))
self.downloadValueField?.setFrameSize(NSSize(width: valueWidth, height: self.downloadValueField!.frame.height))
self.downloadValueField?.stringValue = "\(download.0)"
self.downloadUnitField?.setFrameSize(NSSize(width: unitWidth, height: self.downloadUnitField!.frame.height))
self.downloadUnitField?.setFrameOrigin(NSPoint(x: self.downloadValueField!.frame.width, y: self.downloadUnitField!.frame.origin.y))
self.downloadUnitField?.stringValue = download.1
self.uploadStateView?.setState(self.uploadValue != 0)
self.downloadStateView?.setState(self.downloadValue != 0)
}
@objc private func refreshPublicIP() {
NotificationCenter.default.post(name: .refreshPublicIP, object: nil, userInfo: nil)
self.localIPField?.stringValue = localizedString("Updating...")
self.publicIPv4Field?.stringValue = localizedString("Updating...")
self.publicIPv6Field?.stringValue = localizedString("Updating...")
}
@objc private func resetTotalNetworkUsage() {
NotificationCenter.default.post(name: .resetTotalNetworkUsage, object: nil, userInfo: nil)
self.totalUploadField?.stringValue = Units(bytes: 0).getReadableMemory()
self.totalDownloadField?.stringValue = Units(bytes: 0).getReadableMemory()
self.lastReset = Date()
}
@objc private func resetTotalNetworkUsageCallback() {
self.lastReset = Date()
}
}
================================================
FILE: Modules/Net/portal.swift
================================================
//
// portal.swift
// Net
//
// Created by Serhiy Mytrovtsiy on 18/02/2023
// Using Swift 5.0
// Running on macOS 13.2
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
public class Portal: PortalWrapper {
private var chart: NetworkChartView? = nil
private var publicIPField: NSTextField? = nil
private var publicIPView: NSView? = nil
private var localIPField: NSTextField? = nil
private var localIPView: NSView? = nil
private var base: DataSizeBase {
DataSizeBase(rawValue: Store.shared.string(key: "\(self.name)_base", defaultValue: "byte")) ?? .byte
}
private var reverseOrderState: Bool {
Store.shared.bool(key: "\(self.name)_reverseOrder", defaultValue: false)
}
private var chartScale: Scale {
Scale.fromString(Store.shared.string(key: "\(self.name)_chartScale", defaultValue: Scale.none.key))
}
private var chartFixedScale: Int {
Store.shared.int(key: "\(self.name)_chartFixedScale", defaultValue: 12)
}
private var chartFixedScaleSize: SizeUnit {
SizeUnit.fromString(Store.shared.string(key: "\(self.name)_chartFixedScaleSize", defaultValue: SizeUnit.MB.key))
}
private var publicIPState: Bool {
Store.shared.bool(key: "\(self.name)_publicIP", defaultValue: true)
}
private var downloadColor: NSColor {
let v = SColor.fromString(Store.shared.string(key: "\(self.name)_downloadColor", defaultValue: SColor.secondBlue.key))
var value = NSColor.systemBlue
if let color = v.additional as? NSColor {
value = color
}
return value
}
private var uploadColor: NSColor {
let v = SColor.fromString(Store.shared.string(key: "\(self.name)_uploadColor", defaultValue: SColor.secondRed.key))
var value = NSColor.systemRed
if let color = v.additional as? NSColor {
value = color
}
return value
}
private var initialized: Bool = false
public override func load() {
let view = NSStackView()
view.orientation = .vertical
view.distribution = .fill
view.spacing = Constants.Popup.spacing*2
view.edgeInsets = NSEdgeInsets(
top: 0,
left: Constants.Popup.spacing*2,
bottom: 0,
right: Constants.Popup.spacing*2
)
let container: NSView = NSView(frame: CGRect(x: 0, y: 0, width: self.frame.width - (Constants.Popup.spacing*8), height: 68))
container.wantsLayer = true
container.layer?.cornerRadius = 3
let chart = NetworkChartView(
frame: CGRect(x: 0, y: 0, width: self.frame.width - (Constants.Popup.spacing*8), height: 68),
num: 120,
reversedOrder: self.reverseOrderState,
outColor: self.uploadColor,
inColor: self.downloadColor,
scale: self.chartScale,
fixedScale: Double(self.chartFixedScaleSize.toBytes(self.chartFixedScale))
)
chart.base = self.base
container.addSubview(chart)
self.chart = chart
view.addArrangedSubview(container)
let publicIP = portalRow(view, title: "\(localizedString("Public IP")):", value: localizedString("Unknown"), isSelectable: true)
self.publicIPField = publicIP.1
self.publicIPView = publicIP.2
self.publicIPView?.isHidden = !self.publicIPState
self.publicIPView?.heightAnchor.constraint(equalToConstant: 16).isActive = true
let localIP = portalRow(view, title: "\(localizedString("Local IP")):", value: localizedString("Unknown"), isSelectable: true)
self.localIPField = localIP.1
self.localIPView = localIP.2
self.localIPView?.isHidden = self.publicIPState
self.localIPView?.heightAnchor.constraint(equalToConstant: 16).isActive = true
self.addArrangedSubview(view)
}
public func usageCallback(_ value: Network_Usage) {
DispatchQueue.main.async(execute: {
if let chart = self.chart {
if chart.base != self.base {
chart.base = self.base
}
chart.addValue(upload: Double(value.bandwidth.upload), download: Double(value.bandwidth.download))
chart.setScale(self.chartScale, Double(self.chartFixedScaleSize.toBytes(self.chartFixedScale)))
chart.setColors(in: self.downloadColor, out: self.uploadColor)
}
if self.publicIPState, let view = self.publicIPView, view.isHidden {
self.publicIPView?.isHidden = false
self.localIPView?.isHidden = true
} else if !self.publicIPState, let view = self.publicIPView, !view.isHidden {
self.publicIPView?.isHidden = true
self.localIPView?.isHidden = false
}
if let view = self.publicIPField, view.stringValue != value.raddr.v4 {
if let addr = value.raddr.v4 {
view.stringValue = (value.wifiDetails.countryCode != nil) ? "\(addr) (\(value.wifiDetails.countryCode!))" : addr
} else {
view.stringValue = localizedString("Unknown")
}
if let addr = value.raddr.v6 {
view.toolTip = "\("\(localizedString("v6")):") \(addr)"
} else {
view.toolTip = "\("\(localizedString("v6")):") \(localizedString("Unknown"))"
}
}
var privateIP = localizedString("Unknown")
if let v4 = value.laddr.v4, !v4.isEmpty {
privateIP = v4
} else if let v6 = value.laddr.v6, !v6.isEmpty {
privateIP = v6
}
if self.localIPField?.stringValue != privateIP {
self.localIPField?.stringValue = privateIP
}
})
}
}
================================================
FILE: Modules/Net/readers.swift
================================================
//
// readers.swift
// Net
//
// Created by Serhiy Mytrovtsiy on 24/05/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
import SystemConfiguration
import CoreWLAN
struct ipResponse: Decodable {
var ip: String
var country: String
var cc: String
}
// swiftlint:disable control_statement
extension CWPHYMode: @retroactive CustomStringConvertible {
public var description: String {
switch(self) {
case .mode11a: return "802.11a"
case .mode11ac: return "802.11ac"
case .mode11b: return "802.11b"
case .mode11g: return "802.11g"
case .mode11n: return "802.11n"
case .mode11ax: return "802.11ax"
case .modeNone: return "none"
@unknown default: return "unknown"
}
}
}
extension CWInterfaceMode: @retroactive CustomStringConvertible {
public var description: String {
switch(self) {
case .hostAP: return "AP"
case .IBSS: return "Adhoc"
case .station: return "Station"
case .none: return "none"
@unknown default: return "unknown"
}
}
}
extension CWSecurity: @retroactive CustomStringConvertible {
public var description: String {
switch(self) {
case .none: return "none"
case .WEP: return "WEP"
case .wpaPersonal: return "WPA Personal"
case .wpaPersonalMixed: return "WPA Personal Mixed"
case .wpa2Personal: return "WPA2 Personal"
case .personal: return "Personal"
case .dynamicWEP: return "Dynamic WEP"
case .wpaEnterprise: return "WPA Enterprise"
case .wpaEnterpriseMixed: return "WPA Enterprise Mixed"
case .wpa2Enterprise: return "WPA2 Enterprise"
case .enterprise: return "Enterprise"
case .unknown: return "unknown"
case .wpa3Personal: return "WPA3 Personal"
case .wpa3Enterprise: return "WPA3 Enterprise"
case .wpa3Transition: return "WPA3 Transition"
default: return "unknown"
}
}
}
extension CWChannelBand: @retroactive CustomStringConvertible {
public var description: String {
switch(self) {
case .band2GHz: return "2 GHz"
case .band5GHz: return "5 GHz"
case .band6GHz: return "6 GHz"
case .bandUnknown: return "unknown"
@unknown default: return "unknown"
}
}
}
extension CWChannelWidth: @retroactive CustomStringConvertible {
public var description: String {
switch(self) {
case .width20MHz: return "20 MHz"
case .width40MHz: return "40 MHz"
case .width80MHz: return "80 MHz"
case .width160MHz: return "160 MHz"
case .widthUnknown: return "unknown"
@unknown default: return "unknown"
}
}
}
// swiftlint:enable control_statement
extension CWChannel {
override public var description: String {
return "\(channelNumber) (\(channelBand), \(channelWidth))"
}
}
internal class UsageReader: Reader, CWEventDelegate {
private var reachability: Reachability = Reachability(start: true)
private let variablesQueue = DispatchQueue(label: "eu.exelban.NetworkUsageReader")
private var _usage: Network_Usage = Network_Usage()
public var usage: Network_Usage {
get { self.variablesQueue.sync { self._usage } }
set { self.variablesQueue.sync { self._usage = newValue } }
}
private var primaryInterface: String {
get {
if let global = SCDynamicStoreCopyValue(nil, "State:/Network/Global/IPv4" as CFString), let name = global["PrimaryInterface"] as? String {
return name
}
return ""
}
}
private var interfaceID: String {
get { Store.shared.string(key: "Network_interface", defaultValue: self.primaryInterface) }
set { Store.shared.set(key: "Network_interface", value: newValue) }
}
private var reader: String {
get { Store.shared.string(key: "Network_reader", defaultValue: "interface") }
}
private var vpnConnection: Bool {
if let settings = CFNetworkCopySystemProxySettings()?.takeRetainedValue() as? [String: Any], let scopes = settings["__SCOPED__"] as? [String: Any] {
return !scopes.filter({ $0.key.contains("tap") || $0.key.contains("tun") || $0.key.contains("ppp") || $0.key.contains("ipsec") || $0.key.contains("ipsec0")}).isEmpty
}
return false
}
private var VPNMode: Bool {
get { Store.shared.bool(key: "Network_VPNMode", defaultValue: false) }
}
private var publicIPState: Bool {
get { Store.shared.bool(key: "Network_publicIP", defaultValue: true) }
}
private let wifiClient = CWWiFiClient.shared()
private var lastDetailsReadTS: Date = .distantPast
public override func setup() {
self.reachability.reachable = {
if self.active {
self.getPublicIP()
self.getDetails()
self.getWiFiDetails()
}
}
self.reachability.unreachable = {
if self.active {
self.getWiFiDetails()
self.usage.reset()
self.callback(self.usage)
}
}
NotificationCenter.default.addObserver(self, selector: #selector(refreshPublicIP), name: .refreshPublicIP, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(resetTotalNetworkUsage), name: .resetTotalNetworkUsage, object: nil)
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 1) {
if self.active {
self.getPublicIP()
self.getDetails()
}
}
if let usage = self.value {
self.usage = usage
self.usage.bandwidth = Bandwidth()
}
self.wifiClient.delegate = self
self.startListeningForWifiEvents()
}
public override func terminate() {
self.reachability.stop()
self.stopListeningForWifiEvents()
}
public override func read() {
self.getDetails()
let current: Bandwidth = self.reader == "interface" ? self.readInterfaceBandwidth() : self.readProcessBandwidth()
// allows to reset the value to 0 when first read
if self.usage.bandwidth.upload != 0 {
self.usage.bandwidth.upload = current.upload - self.usage.bandwidth.upload
}
if self.usage.bandwidth.download != 0 {
self.usage.bandwidth.download = current.download - self.usage.bandwidth.download
}
self.usage.bandwidth.upload = max(self.usage.bandwidth.upload, 0) // prevent negative upload value
self.usage.bandwidth.download = max(self.usage.bandwidth.download, 0) // prevent negative download value
self.usage.total.upload += self.usage.bandwidth.upload
self.usage.total.download += self.usage.bandwidth.download
self.usage.status = self.reachability.isReachable
if self.vpnConnection && self.VPNMode {
self.usage.bandwidth.upload /= 2
self.usage.bandwidth.download /= 2
}
self.callback(self.usage)
self.usage.bandwidth.upload = current.upload
self.usage.bandwidth.download = current.download
}
private func readInterfaceBandwidth() -> Bandwidth {
var interfaceAddresses: UnsafeMutablePointer? = nil
var totalUpload: Int64 = 0
var totalDownload: Int64 = 0
guard getifaddrs(&interfaceAddresses) == 0 else {
return Bandwidth()
}
var pointer = interfaceAddresses
while pointer != nil {
defer { pointer = pointer?.pointee.ifa_next }
guard let pointer = pointer else { break }
if String(cString: pointer.pointee.ifa_name) != self.interfaceID {
continue
}
self.usage.interface?.status = (pointer.pointee.ifa_flags & UInt32(IFF_UP)) != 0
if let raw = pointer.pointee.ifa_data {
let dataPtr = raw.assumingMemoryBound(to: if_data.self)
let ifData = dataPtr.pointee
let baud = UInt64(ifData.ifi_baudrate)
if baud > 0 {
self.usage.interface?.transmitRate = Double(baud) / 1_000_000.0
}
}
self.getLocalIP(pointer)
if let info = self.getBytesInfo(pointer) {
totalUpload += info.upload
totalDownload += info.download
}
}
freeifaddrs(interfaceAddresses)
return Bandwidth(upload: totalUpload, download: totalDownload)
}
private func readProcessBandwidth() -> Bandwidth {
let task = Process()
task.launchPath = "/usr/bin/nettop"
task.arguments = ["-P", "-L", "1", "-n", "-k", "time,interface,state,rx_dupe,rx_ooo,re-tx,rtt_avg,rcvsize,tx_win,tc_class,tc_mgt,cc_algo,P,C,R,W,arch"]
task.environment = [
"NSUnbufferedIO": "YES",
"LC_ALL": "en_US.UTF-8"
]
let inputPipe = Pipe()
let outputPipe = Pipe()
let errorPipe = Pipe()
defer {
inputPipe.fileHandleForWriting.closeFile()
outputPipe.fileHandleForReading.closeFile()
errorPipe.fileHandleForReading.closeFile()
}
task.standardInput = inputPipe
task.standardOutput = outputPipe
task.standardError = errorPipe
do {
try task.run()
} catch let err {
error("read bandwidth from processes: \(err)", log: self.log)
return Bandwidth()
}
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: outputData, encoding: .utf8)
_ = String(data: errorData, encoding: .utf8)
guard let output, !output.isEmpty else { return Bandwidth() }
var totalUpload: Int64 = 0
var totalDownload: Int64 = 0
var firstLine = false
output.enumerateLines { (line, _) in
if !firstLine {
firstLine = true
return
}
let parsedLine = line.split(separator: ",")
guard parsedLine.count >= 3 else {
return
}
if let download = Int64(parsedLine[1]) {
totalDownload += download
}
if let upload = Int64(parsedLine[2]) {
totalUpload += upload
}
}
return Bandwidth(upload: totalUpload, download: totalDownload)
}
public func getDetails() {
guard self.interfaceID != "" else { return }
let now = Date()
if now.timeIntervalSince(self.lastDetailsReadTS) < 15 { return }
for interface in SCNetworkInterfaceCopyAll() as NSArray {
if let bsdName = SCNetworkInterfaceGetBSDName(interface as! SCNetworkInterface), bsdName as String == self.interfaceID,
let type = SCNetworkInterfaceGetInterfaceType(interface as! SCNetworkInterface),
let displayName = SCNetworkInterfaceGetLocalizedDisplayName(interface as! SCNetworkInterface),
let address = SCNetworkInterfaceGetHardwareAddressString(interface as! SCNetworkInterface) {
self.usage.interface = Network_interface(displayName: displayName as String, BSDName: bsdName as String, address: address as String)
switch type {
case kSCNetworkInterfaceTypeEthernet:
self.usage.connectionType = .ethernet
case kSCNetworkInterfaceTypeIEEE80211, kSCNetworkInterfaceTypeWWAN:
self.usage.connectionType = .wifi
case kSCNetworkInterfaceTypeBluetooth:
self.usage.connectionType = .bluetooth
default:
self.usage.connectionType = .other
}
}
}
if let prefs = SCPreferencesCreate(nil, "Stats" as CFString, nil), let services = SCNetworkServiceCopyAll(prefs) as? [SCNetworkService] {
for service in services {
if let interface = SCNetworkServiceGetInterface(service), let name = SCNetworkInterfaceGetBSDName(interface), name as String == self.interfaceID,
let serviceID = SCNetworkServiceGetServiceID(service) {
let key = "State:/Network/Service/\(serviceID)/DNS" as CFString
if let settings = SCDynamicStoreCopyValue(nil, key) as? [String: Any] {
self.usage.dns = settings["ServerAddresses"] as? [String] ?? []
}
}
}
}
guard self.usage.interface != nil else { return }
if self.usage.wifiDetails.ssid != nil && (self.usage.wifiDetails.ssid == "" || self.usage.wifiDetails.ssid == "") {
self.usage.wifiDetails.ssid = nil
}
if self.usage.connectionType == .wifi && self.usage.wifiDetails.ssid == nil || self.usage.wifiDetails.ssid == "" {
self.getWiFiDetails()
}
self.lastDetailsReadTS = Date()
}
private func getWiFiDetails() {
if let interface = CWWiFiClient.shared().interface(withName: self.interfaceID) {
if let ssid = interface.ssid() {
self.usage.wifiDetails.ssid = ssid
} else if let cfg = interface.configuration(),
let set = (cfg.value(forKey: "networkProfiles") as? NSOrderedSet),
let first = set.firstObject as? CWNetworkProfile,
let raw = first.ssid, !raw.isEmpty {
self.usage.wifiDetails.ssid = raw.replacingOccurrences(of: "’", with: "'").replacingOccurrences(of: "‘", with: "'").trimmingCharacters(in: .whitespacesAndNewlines)
}
if let bssid = interface.bssid() {
self.usage.wifiDetails.bssid = bssid
}
if let cc = interface.countryCode() {
self.usage.wifiDetails.countryCode = cc
}
self.usage.wifiDetails.RSSI = interface.rssiValue()
self.usage.wifiDetails.noise = interface.noiseMeasurement()
self.usage.wifiDetails.standard = interface.activePHYMode().description
self.usage.wifiDetails.mode = interface.interfaceMode().description
self.usage.wifiDetails.security = interface.security().description
if let ch = interface.wlanChannel() {
self.usage.wifiDetails.channel = ch.description
self.usage.wifiDetails.channelBand = ch.channelBand.description
self.usage.wifiDetails.channelWidth = ch.channelWidth.description
self.usage.wifiDetails.channelNumber = ch.channelNumber.description
}
}
if self.usage.wifiDetails.ssid == nil || self.usage.wifiDetails.ssid == "" {
guard let res = process(path: "/usr/sbin/system_profiler", arguments: ["SPAirPortDataType", "-json"]) else {
return
}
do {
if let json = try JSONSerialization.jsonObject(with: Data(res.utf8), options: []) as? [String: Any] {
if let arr = json["SPAirPortDataType"] as? [[String: Any]],
let airport = arr.first(where: { $0["spairport_airport_interfaces"] != nil }),
let interfaces = airport["spairport_airport_interfaces"] as? [[String: Any]],
let interface = interfaces.first(where: { $0["_name"] as? String == self.interfaceID }),
let obj = interface["spairport_current_network_information"] as? [String: Any] {
self.usage.wifiDetails.ssid = obj["_name"] as? String
self.usage.wifiDetails.countryCode = obj["spairport_network_country_code"] as? String
self.usage.wifiDetails.standard = obj["spairport_network_phymode"] as? String
}
}
} catch let err as NSError {
error("error to parse system_profiler SPAirPortDataType: \(err.localizedDescription)")
return
}
}
}
private func getLocalIP(_ pointer: UnsafeMutablePointer) {
var addr = pointer.pointee.ifa_addr.pointee
guard addr.sa_family == UInt8(AF_INET) || addr.sa_family == UInt8(AF_INET6) else { return}
var ip = [CChar](repeating: 0, count: Int(NI_MAXHOST))
getnameinfo(&addr, socklen_t(addr.sa_len), &ip, socklen_t(ip.count), nil, socklen_t(0), NI_NUMERICHOST)
let ipStr = String(cString: ip)
if addr.sa_family == UInt8(AF_INET) && !ipStr.isEmpty {
self.usage.laddr.v4 = ipStr
} else if addr.sa_family == UInt8(AF_INET6) && !ipStr.isEmpty {
self.usage.laddr.v6 = ipStr
}
}
private func getPublicIP() {
guard self.publicIPState else { return }
struct Addr_s: Decodable {
let ipv4: String?
let ipv6: String?
let country: String?
}
DispatchQueue.global(qos: .userInitiated).async {
let response = syncShell("curl -s -4 https://api.mac-stats.com/ip")
if !response.isEmpty, let data = response.data(using: .utf8),
let addr = try? JSONDecoder().decode(Addr_s.self, from: data) {
if let ip = addr.ipv4, self.isIPv4(ip) {
self.usage.raddr.v4 = ip
}
if let countryCode = addr.country {
self.usage.raddr.countryCode = countryCode
}
}
}
DispatchQueue.global(qos: .userInitiated).async {
let response = syncShell("curl -s -6 https://api.mac-stats.com/ip")
if !response.isEmpty, let data = response.data(using: .utf8),
let addr = try? JSONDecoder().decode(Addr_s.self, from: data) {
if let ip = addr.ipv6, !self.isIPv4(ip) {
self.usage.raddr.v6 = ip
}
if let countryCode = addr.country {
self.usage.raddr.countryCode = countryCode
}
}
}
}
private func getBytesInfo(_ pointer: UnsafeMutablePointer) -> (upload: Int64, download: Int64)? {
let addr = pointer.pointee.ifa_addr.pointee
guard addr.sa_family == UInt8(AF_LINK) else {
return nil
}
let data: UnsafeMutablePointer? = unsafeBitCast(pointer.pointee.ifa_data, to: UnsafeMutablePointer.self)
return (upload: Int64(data?.pointee.ifi_obytes ?? 0), download: Int64(data?.pointee.ifi_ibytes ?? 0))
}
private func isIPv4(_ ip: String) -> Bool {
let arr = ip.split(separator: ".").compactMap{ Int($0) }
return arr.count == 4 && arr.filter{ $0 >= 0 && $0 < 256}.count == 4
}
@objc func refreshPublicIP() {
self.usage.raddr.v4 = nil
self.usage.raddr.v6 = nil
DispatchQueue.global(qos: .background).async {
self.getPublicIP()
}
}
@objc func resetTotalNetworkUsage() {
self.usage.total = Bandwidth()
self.save(self.usage)
}
private func startListeningForWifiEvents() {
do {
try self.wifiClient.startMonitoringEvent(with: .ssidDidChange)
} catch let err as NSError {
error("failed to start monitoring Wi-Fi events: \(err.localizedDescription)")
}
}
private func stopListeningForWifiEvents() {
do {
try self.wifiClient.stopMonitoringEvent(with: .ssidDidChange)
} catch let err as NSError {
error("failed to stop monitoring Wi-Fi events: \(err.localizedDescription)")
}
}
public func ssidDidChangeForWiFiInterface(withName interfaceName: String) {
self.getWiFiDetails()
}
private func isInterfaceUp(_ ifName: String) -> Bool {
var addrs: UnsafeMutablePointer? = nil
guard getifaddrs(&addrs) == 0, let first = addrs else { return false }
defer { freeifaddrs(addrs) }
var ptr = first
while true {
let name = String(cString: ptr.pointee.ifa_name)
if name == ifName {
return (ptr.pointee.ifa_flags & UInt32(IFF_UP)) != 0
}
if let next = ptr.pointee.ifa_next {
ptr = next
} else {
break
}
}
return false
}
}
public class ProcessReader: Reader<[Network_Process]> {
private let title: String = "Network"
private var previous: [Network_Process] = []
private var numberOfProcesses: Int {
get {
return Store.shared.int(key: "\(self.title)_processes", defaultValue: 8)
}
}
public override func setup() {
self.popup = true
}
public override func read() {
if self.numberOfProcesses == 0 {
return
}
let task = Process()
task.launchPath = "/usr/bin/nettop"
task.arguments = ["-P", "-L", "1", "-n", "-k", "time,interface,state,rx_dupe,rx_ooo,re-tx,rtt_avg,rcvsize,tx_win,tc_class,tc_mgt,cc_algo,P,C,R,W,arch"]
task.environment = [
"NSUnbufferedIO": "YES",
"LC_ALL": "en_US.UTF-8"
]
let inputPipe = Pipe()
let outputPipe = Pipe()
let errorPipe = Pipe()
defer {
inputPipe.fileHandleForWriting.closeFile()
outputPipe.fileHandleForReading.closeFile()
errorPipe.fileHandleForReading.closeFile()
}
task.standardInput = inputPipe
task.standardOutput = outputPipe
task.standardError = errorPipe
do {
try task.run()
} catch let error {
print(error)
return
}
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: outputData, encoding: .utf8)
_ = String(data: errorData, encoding: .utf8)
guard let output, !output.isEmpty else { return }
var list: [Network_Process] = []
var firstLine = false
output.enumerateLines { (line, _) in
if !firstLine {
firstLine = true
return
}
let parsedLine = line.split(separator: ",")
guard parsedLine.count >= 3 else {
return
}
var process = Network_Process()
process.time = Date()
let nameArray = parsedLine[0].split(separator: ".")
if let pid = nameArray.last {
process.pid = Int(pid) ?? 0
}
if let app = NSRunningApplication(processIdentifier: pid_t(process.pid) ) {
process.name = app.localizedName ?? nameArray.dropLast().joined(separator: ".")
} else {
process.name = nameArray.dropLast().joined(separator: ".")
}
if process.name == "" {
process.name = "\(process.pid)"
}
if let download = Int(parsedLine[1]) {
process.download = download
}
if let upload = Int(parsedLine[2]) {
process.upload = upload
}
list.append(process)
}
var processes: [Network_Process] = []
if self.previous.isEmpty {
self.previous = list
processes = list
} else {
self.previous.forEach { (pp: Network_Process) in
if let i = list.firstIndex(where: { $0.pid == pp.pid }) {
let p = list[i]
var download = p.download - pp.download
var upload = p.upload - pp.upload
let time = download == 0 && upload == 0 ? pp.time : Date()
list[i].time = time
if download < 0 {
download = 0
}
if upload < 0 {
upload = 0
}
processes.append(Network_Process(pid: p.pid, name: p.name, time: time, download: download, upload: upload))
}
}
self.previous = list
}
processes.sort {
let firstMax = max($0.download, $0.upload)
let secondMax = max($1.download, $1.upload)
let firstMin = min($0.download, $0.upload)
let secondMin = min($1.download, $1.upload)
if firstMax == secondMax && firstMin == secondMin { // download and upload values are the same, sort by time
return $0.time < $1.time
} else if firstMax == secondMax && firstMin != secondMin { // max values are the same, min not. Sort by min values
return firstMin < secondMin
}
return firstMax < secondMax // max values are not the same, sort by max value
}
self.callback(processes.suffix(self.numberOfProcesses).reversed())
}
}
internal class ConnectivityReaderWrapper {
weak var reader: ConnectivityReader?
init(_ reader: ConnectivityReader) {
self.reader = reader
}
}
// inspired by https://github.com/samiyr/SwiftyPing
internal class ConnectivityReader: Reader {
private let variablesQueue = DispatchQueue(label: "eu.exelban.ConnectivityReaderQueue")
private let identifier = UInt16.random(in: 0.. Bool {
guard data.count >= MemoryLayout.size + MemoryLayout.size,
let headerOffset = icmpHeaderOffset(of: data) else { return false }
let payloadSize = data.count - headerOffset - MemoryLayout.size
let icmpHeader = data.withUnsafeBytes({ $0.load(fromByteOffset: headerOffset, as: ICMPHeader.self) })
let payload = data.subdata(in: (data.count - payloadSize).. Data? {
var header = ICMPHeader(
type: 8,
code: 0,
checksum: 0,
identifier: CFSwapInt16HostToBig(self.identifier),
sequenceNumber: CFSwapInt16HostToBig(0),
payload: self.fingerprint.uuid
)
let delta = MemoryLayout.size - MemoryLayout.size
var additional = [UInt8]()
if delta > 0 {
additional = (0...size) + Data(additional)
}
private func computeChecksum(header: ICMPHeader, additionalPayload: [UInt8]) -> UInt16? {
let typecode = Data([header.type, header.code]).withUnsafeBytes { $0.load(as: UInt16.self) }
var sum = UInt64(typecode) + UInt64(header.identifier) + UInt64(header.sequenceNumber)
let payload = convert(payload: header.payload) + additionalPayload
guard payload.count % 2 == 0 else { return nil }
var i = 0
while i < payload.count {
guard payload.indices.contains(i + 1) else { return nil }
sum += Data([payload[i], payload[i + 1]]).withUnsafeBytes { UInt64($0.load(as: UInt16.self)) }
i += 2
}
while sum >> 16 != 0 {
sum = (sum & 0xffff) + (sum >> 16)
}
guard sum < UInt16.max else { return nil }
return ~UInt16(sum)
}
private func convert(payload: uuid_t) -> [UInt8] {
let p = payload
return [p.0, p.1, p.2, p.3, p.4, p.5, p.6, p.7, p.8, p.9, p.10, p.11, p.12, p.13, p.14, p.15].map { UInt8($0) }
}
private func icmpHeaderOffset(of packet: Data) -> Int? {
if packet.count >= MemoryLayout.size + MemoryLayout.size {
let ipHeader = packet.withUnsafeBytes({ $0.load(as: IPHeader.self) })
if ipHeader.versionAndHeaderLength & 0xF0 == 0x40 && ipHeader.protocol == IPPROTO_ICMP {
let headerLength = Int(ipHeader.versionAndHeaderLength) & 0x0F * MemoryLayout.size
if packet.count >= headerLength + MemoryLayout.size {
return headerLength
}
}
}
return nil
}
private func openConn() {
let info = ConnectivityReaderWrapper(self)
let unmanagedSocketInfo = Unmanaged.passRetained(info)
var context = CFSocketContext(version: 0, info: unmanagedSocketInfo.toOpaque(), retain: nil, release: nil, copyDescription: nil)
self.socket = CFSocketCreate(kCFAllocatorDefault, AF_INET, SOCK_DGRAM, IPPROTO_ICMP, CFSocketCallBackType.dataCallBack.rawValue, { _, callBackType, _, data, info in
guard let info = info, let data = data else { return }
if (callBackType as CFSocketCallBackType) == CFSocketCallBackType.dataCallBack {
let cfdata = Unmanaged.fromOpaque(data).takeUnretainedValue()
let wrapper = Unmanaged.fromOpaque(info).takeUnretainedValue()
wrapper.reader?.socketCallback(data: cfdata as Data)
}
}, &context)
let handle = CFSocketGetNative(self.socket)
var value: Int32 = 1
let err = setsockopt(handle, SOL_SOCKET, SO_NOSIGPIPE, &value, socklen_t(MemoryLayout.size(ofValue: value)))
guard err == 0 else { return }
self.socketSource = CFSocketCreateRunLoopSource(nil, self.socket, 0)
CFRunLoopAddSource(CFRunLoopGetMain(), self.socketSource, .commonModes)
}
private func closeConn() {
if let source = self.socketSource {
CFRunLoopSourceInvalidate(source)
self.socketSource = nil
}
if let socket = self.socket {
CFSocketInvalidate(socket)
self.socket = nil
}
self.timeoutTimer?.invalidate()
self.timeoutTimer = nil
}
private func resolve() -> Data? {
self.lastHost = self.ICMPHost
var streamError = CFStreamError()
let cfhost = CFHostCreateWithName(nil, self.ICMPHost as CFString).takeRetainedValue()
let status = CFHostStartInfoResolution(cfhost, .addresses, &streamError)
guard status else { return nil }
var success: DarwinBoolean = false
guard let addresses = CFHostGetAddressing(cfhost, &success)?.takeUnretainedValue() as? [Data] else {
return nil
}
var data: Data?
for address in addresses {
let addrin = address.socketAddress
if address.count >= MemoryLayout.size && addrin.sa_family == UInt8(AF_INET) {
data = address
break
}
}
guard let data = data, !data.isEmpty else { return nil }
return data
}
}
================================================
FILE: Modules/Net/settings.swift
================================================
//
// settings.swift
// Net
//
// Created by Serhiy Mytrovtsiy on 06/07/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
import SystemConfiguration
var textWidgetHelp = """
Description
You can use a combination of any of the variables. There is only one limitation: there must be a space between each variable.
Examples:
- $addr.public - $status
- $addr.public - $wifi.ssid - $status
Available variables
- $addr.public: Public IP address.
- $addr.publicV4: Public IPv4 address.
- $addr.publicV6: Public IPv6 address.
- $addr.private: Private/local IP address.
- $addr.privateV4: Private/local IPv4 address.
- $addr.privateV6: Private/local IPv6 address.
- $addr.countryCode: Country code based on the public IP address.
- $addr.flag: Emoji flag based on the country code.
- $interface.displayName: Network interface name.
- $interface.BSDName: BSD name of the network interface.
- $interface.address: MAC address of the network interface.
- $wifi.ssid: Wi-Fi network name.
- $wifi.bssid: MAC address of the Wi-Fi access point (BSSID).
- $wifi.RSSI: Signal strength of the Wi-Fi network (RSSI).
- $wifi.noise: Noise level of the Wi-Fi network.
- $wifi.transmitRate: Transmit rate (connection speed) of the Wi-Fi network.
- $wifi.standard: Wi-Fi standard (e.g., 802.11a/b/g/n/ac).
- $wifi.mode: Operating mode of the Wi-Fi (e.g., infrastructure, adhoc).
- $wifi.security: Type of security used by the Wi-Fi network.
- $wifi.channel: Wi-Fi channel being used.
- $wifi.channelBand: Frequency band of the Wi-Fi channel (e.g., 2.4 GHz, 5 GHz).
- $wifi.channelWidth: Channel width used in MHz.
- $wifi.channelNumber: Channel number used by the Wi-Fi network.
- $status: Status of the network connection. "UP" if active, "DOWN" if inactive.
- $upload.total: Total amount of data uploaded over the connection.
- $upload: Current upload bandwidth used.
- $download.total: Total amount of data downloaded over the connection.
- $download: Current download bandwidth used.
- $type: Type of network connection (e.g., Ethernet, Wi-Fi, Cellular).
- $icmp.status: ICMP status.
- $icmp.latency: ICMP latency.
"""
internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate {
private var numberOfProcesses: Int = 8
private var readerType: String = "interface"
private var usageReset: String = AppUpdateInterval.never.rawValue
private var VPNModeState: Bool = false
private var widgetActivationThresholdState: Bool = false
private var widgetActivationThreshold: Int = 0
private var widgetActivationThresholdSize: SizeUnit = .MB
private var connectivityICMPHost: String = "1.1.1.1"
private var connectivityHTTPHost: String = "https://google.com"
private var updateConnectivityIntervalValue: Int = 1
private var connectivityMode: ConnectivityReader.ConnectivityMode = .icmp
private var publicIPState: Bool = true
private var publicIPRefreshInterval: String = "never"
private var baseValue: String = "byte"
private var textValue: String = "$addr.public - $status"
public var callback: (() -> Void) = {}
public var callbackWhenUpdateNumberOfProcesses: (() -> Void) = {}
public var usageResetCallback: (() -> Void) = {}
public var connectivityHostCallback: ((_ newState: Bool) -> Void) = { _ in }
public var setInterval: ((_ value: Int) -> Void) = {_ in }
public var publicIPRefreshIntervalCallback: (() -> Void) = {}
private let title: String
private var sliderView: NSView? = nil
private var section: PreferencesSection? = nil
private var widgetThresholdSection: PreferencesSection? = nil
private let textWidgetHelpPanel: HelpHUD = HelpHUD(textWidgetHelp)
private var list: [Network_interface] = []
private var vpnConnection: Bool {
if let settings = CFNetworkCopySystemProxySettings()?.takeRetainedValue() as? [String: Any], let scopes = settings["__SCOPED__"] as? [String: Any] {
return !scopes.filter({ $0.key.contains("tap") || $0.key.contains("tun") || $0.key.contains("ppp") || $0.key.contains("ipsec") || $0.key.contains("ipsec0")}).isEmpty
}
return false
}
private var connectivityHostField: NSTextField? = nil
public init(_ module: ModuleType) {
self.title = module.stringValue
self.numberOfProcesses = Store.shared.int(key: "\(self.title)_processes", defaultValue: self.numberOfProcesses)
self.readerType = Store.shared.string(key: "\(self.title)_reader", defaultValue: self.readerType)
self.usageReset = Store.shared.string(key: "\(self.title)_usageReset", defaultValue: self.usageReset)
self.VPNModeState = Store.shared.bool(key: "\(self.title)_VPNMode", defaultValue: self.VPNModeState)
self.widgetActivationThresholdState = Store.shared.bool(key: "\(self.title)_widgetActivationThresholdState", defaultValue: self.widgetActivationThresholdState)
self.widgetActivationThreshold = Store.shared.int(key: "\(self.title)_widgetActivationThreshold", defaultValue: self.widgetActivationThreshold)
self.widgetActivationThresholdSize = SizeUnit.fromString(Store.shared.string(key: "\(self.title)_widgetActivationThresholdSize", defaultValue: self.widgetActivationThresholdSize.key))
self.connectivityICMPHost = Store.shared.string(key: "\(self.title)_ICMPHost", defaultValue: self.connectivityICMPHost)
self.connectivityHTTPHost = Store.shared.string(key: "\(self.title)_HTTPHost", defaultValue: self.connectivityHTTPHost)
self.updateConnectivityIntervalValue = Store.shared.int(key: "\(self.title)_updateICMPInterval", defaultValue: self.updateConnectivityIntervalValue)
self.connectivityMode = ConnectivityReader.ConnectivityMode(rawValue: Store.shared.string(key: "\(self.title)_connectivityMode", defaultValue: "icmp")) ?? .icmp
self.publicIPState = Store.shared.bool(key: "\(self.title)_publicIP", defaultValue: self.publicIPState)
self.publicIPRefreshInterval = Store.shared.string(key: "\(self.title)_publicIPRefreshInterval", defaultValue: self.publicIPRefreshInterval)
self.baseValue = Store.shared.string(key: "\(self.title)_base", defaultValue: self.baseValue)
self.textValue = Store.shared.string(key: "\(self.title)_textWidgetValue", defaultValue: self.textValue)
super.init(frame: NSRect.zero)
self.orientation = .vertical
self.spacing = Constants.Settings.margin
for interface in SCNetworkInterfaceCopyAll() as NSArray {
if let bsdName = SCNetworkInterfaceGetBSDName(interface as! SCNetworkInterface),
let displayName = SCNetworkInterfaceGetLocalizedDisplayName(interface as! SCNetworkInterface) {
self.list.append(Network_interface(displayName: displayName as String, BSDName: bsdName as String))
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func load(widgets: [widget_t]) {
self.subviews.forEach{ $0.removeFromSuperview() }
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Number of top processes"), component: selectView(
action: #selector(self.changeNumberOfProcesses),
items: NumbersOfProcesses.map{ KeyValue_t(key: "\($0)", value: "\($0)") },
selected: "\(self.numberOfProcesses)"
))
]))
let interfaces = selectView(
action: #selector(self.handleSelection),
items: [],
selected: ""
)
let selectedInterface = Store.shared.string(key: "\(self.title)_interface", defaultValue: "")
let menu = NSMenu()
let autodetection = NSMenuItem(title: localizedString("Autodetection"), action: nil, keyEquivalent: "")
autodetection.identifier = NSUserInterfaceItemIdentifier(rawValue: "autodetection")
autodetection.tag = 128
menu.addItem(autodetection)
menu.addItem(NSMenuItem.separator())
self.list.forEach { (interface: Network_interface) in
let interfaceMenu = NSMenuItem(title: "\(interface.displayName) (\(interface.BSDName))", action: nil, keyEquivalent: "")
interfaceMenu.identifier = NSUserInterfaceItemIdentifier(rawValue: interface.BSDName)
menu.addItem(interfaceMenu)
if selectedInterface != "" && selectedInterface == interface.BSDName {
interfaceMenu.state = .on
}
}
interfaces.menu = menu
interfaces.sizeToFit()
if selectedInterface == "" {
interfaces.selectItem(withTag: 128)
}
var prefs: [PreferencesRow] = [
PreferencesRow(localizedString("Reader type"), component: selectView(
action: #selector(self.changeReaderType),
items: NetworkReaders,
selected: self.readerType
)),
PreferencesRow(localizedString("Network interface"), component: interfaces),
PreferencesRow(localizedString("Base"), component: selectView(
action: #selector(self.toggleBase),
items: SpeedBase,
selected: self.baseValue
)),
PreferencesRow(localizedString("Reset data usage"), component: selectView(
action: #selector(self.toggleUsageReset),
items: AppUpdateIntervals.filter({ $0.key != "Silent" }),
selected: self.usageReset
)),
PreferencesRow(localizedString("Public IP"), component: switchView(
action: #selector(self.togglePublicIPState),
state: self.publicIPState
)),
PreferencesRow(localizedString("Auto-refresh public IP address"), component: selectView(
action: #selector(self.toggleRefreshIPInterval),
items: PublicIPAddressRefreshIntervals,
selected: self.publicIPRefreshInterval
))
]
if self.vpnConnection {
prefs.append(PreferencesRow(localizedString("VPN mode"), component: switchView(
action: #selector(self.toggleVPNMode),
state: self.VPNModeState
)))
}
let section = PreferencesSection(prefs)
section.setRowVisibility(1, newState: self.readerType == "interface")
section.setRowVisibility(5, newState: self.publicIPState)
self.addArrangedSubview(section)
self.section = section
self.widgetThresholdSection = PreferencesSection([
PreferencesRow(localizedString("Widget activation threshold"), component: PreferencesSwitch(
action: self.toggleWidgetActivationThreshold, state: self.widgetActivationThresholdState, with: StepperInput(
self.widgetActivationThreshold, range: NSRange(location: 1, length: 1023),
unit: self.widgetActivationThresholdSize.key, units: SizeUnit.allCases,
callback: self.changeWidgetActivationThreshold, unitCallback: self.toggleWidgetActivationThresholdSize
)
))
])
self.addArrangedSubview(self.widgetThresholdSection!)
self.widgetThresholdSection?.setRowVisibility(1, newState: self.widgetActivationThresholdState)
var connectivityHost = self.connectivityICMPHost
if self.connectivityMode == .http {
connectivityHost = self.connectivityHTTPHost
}
let ICMPField = self.inputField(id: "ICMP", value: connectivityHost, placeholder: localizedString("Leave empty to disable the check"))
self.connectivityHostField = ICMPField
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Reader type"), component: selectView(
action: #selector(self.changeConnectivityMode),
items: [
KeyValue_t(key: "icmp", value: "ICMP"),
KeyValue_t(key: "http", value: "HTTP")
],
selected: self.connectivityMode.rawValue
)),
PreferencesRow(localizedString("Connectivity host"), component: ICMPField) {
NSWorkspace.shared.open(URL(string: "https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol")!)
},
PreferencesRow(localizedString("Update interval"), component: selectView(
action: #selector(self.changeICMPUpdateInterval),
items: ReaderUpdateIntervals,
selected: "\(self.updateConnectivityIntervalValue)"
))
]))
if widgets.contains(where: { $0 == .text }) {
let textField = self.inputField(id: "text", value: self.textValue, placeholder: localizedString("This will be visible in the text widget"))
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Text widget value"), component: textField) { [weak self] in
self?.textWidgetHelpPanel.show()
}
]))
}
}
private func inputField(id: String, value: String, placeholder: String) -> NSTextField {
let field: NSTextField = NSTextField()
field.identifier = NSUserInterfaceItemIdentifier(id)
field.widthAnchor.constraint(equalToConstant: 250).isActive = true
field.font = NSFont.systemFont(ofSize: 12, weight: .regular)
field.textColor = .textColor
field.isEditable = true
field.isSelectable = true
field.usesSingleLineMode = true
field.maximumNumberOfLines = 1
field.focusRingType = .none
field.stringValue = value
field.delegate = self
field.placeholderString = placeholder
return field
}
@objc private func handleSelection(_ sender: NSPopUpButton) {
guard let item = sender.selectedItem, let id = item.identifier?.rawValue else { return }
if id == "autodetection" {
Store.shared.remove("\(self.title)_interface")
} else {
if let bsdName = item.identifier?.rawValue {
Store.shared.set(key: "\(self.title)_interface", value: bsdName)
}
}
self.callback()
}
@objc private func changeNumberOfProcesses(_ sender: NSMenuItem) {
if let value = Int(sender.title) {
self.numberOfProcesses = value
Store.shared.set(key: "\(self.title)_processes", value: value)
self.callbackWhenUpdateNumberOfProcesses()
}
}
@objc private func changeReaderType(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.readerType = key
Store.shared.set(key: "\(self.title)_reader", value: key)
self.section?.setRowVisibility(1, newState: self.readerType == "interface")
NotificationCenter.default.post(name: .resetTotalNetworkUsage, object: nil, userInfo: nil)
}
@objc private func toggleUsageReset(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.usageReset = key
Store.shared.set(key: "\(self.title)_usageReset", value: key)
self.usageResetCallback()
}
@objc func toggleVPNMode(_ sender: NSControl) {
self.VPNModeState = controlState(sender)
Store.shared.set(key: "\(self.title)_VPNMode", value: self.VPNModeState)
}
@objc func toggleWidgetActivationThreshold(_ sender: NSControl) {
self.widgetActivationThresholdState = controlState(sender)
Store.shared.set(key: "\(self.title)_widgetActivationThresholdState", value: self.widgetActivationThresholdState)
self.widgetThresholdSection?.setRowVisibility(1, newState: self.widgetActivationThresholdState)
}
@objc private func changeWidgetActivationThreshold(_ newValue: Int) {
self.widgetActivationThreshold = newValue
Store.shared.set(key: "\(self.title)_widgetActivationThreshold", value: newValue)
}
private func toggleWidgetActivationThresholdSize(_ newValue: KeyValue_p) {
guard let newUnit = newValue as? SizeUnit else { return }
self.widgetActivationThresholdSize = newUnit
Store.shared.set(key: "\(self.title)_widgetActivationThresholdSize", value: self.widgetActivationThresholdSize.key)
self.display()
}
func controlTextDidChange(_ notification: Notification) {
if let field = notification.object as? NSTextField {
if field.identifier == NSUserInterfaceItemIdentifier("ICMP") {
if self.connectivityMode == .http {
self.connectivityHTTPHost = field.stringValue
Store.shared.set(key: "\(self.title)_HTTPHost", value: self.connectivityHTTPHost)
self.connectivityHostCallback(self.connectivityHTTPHost.isEmpty)
} else {
self.connectivityICMPHost = field.stringValue
Store.shared.set(key: "\(self.title)_ICMPHost", value: self.connectivityICMPHost)
self.connectivityHostCallback(self.connectivityICMPHost.isEmpty)
}
} else if field.identifier == NSUserInterfaceItemIdentifier("text") {
self.textValue = field.stringValue
Store.shared.set(key: "\(self.title)_textWidgetValue", value: self.textValue)
}
}
}
@objc private func changeICMPUpdateInterval(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let value = Int(key) else { return }
self.updateConnectivityIntervalValue = value
Store.shared.set(key: "\(self.title)_updateICMPInterval", value: value)
self.setInterval(value)
}
@objc func togglePublicIPState(_ sender: NSControl) {
self.publicIPState = controlState(sender)
Store.shared.set(key: "\(self.title)_publicIP", value: self.publicIPState)
self.section?.setRowVisibility(5, newState: self.publicIPState)
}
@objc private func toggleRefreshIPInterval(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.publicIPRefreshInterval = key
Store.shared.set(key: "\(self.title)_publicIPRefreshInterval", value: self.publicIPRefreshInterval)
self.publicIPRefreshIntervalCallback()
}
@objc private func toggleBase(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.baseValue = key
Store.shared.set(key: "\(self.title)_base", value: self.baseValue)
}
@objc private func changeConnectivityMode(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.connectivityMode = ConnectivityReader.ConnectivityMode(rawValue: key) ?? .icmp
Store.shared.set(key: "\(self.title)_connectivityMode", value: self.connectivityMode.rawValue)
self.connectivityHostField?.stringValue = self.connectivityICMPHost
if self.connectivityMode == .http {
self.connectivityHostField?.stringValue = self.connectivityHTTPHost
}
}
}
================================================
FILE: Modules/Net/widget.swift
================================================
//
// widget.swift
// Net
//
// Created by Serhiy Mytrovtsiy on 30/07/2024
// Using Swift 5.0
// Running on macOS 14.5
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//
import SwiftUI
import WidgetKit
import Charts
import Kit
public struct Network_entry: TimelineEntry {
public static let kind = "NetworkWidget"
public static var snapshot: Network_entry = Network_entry(value: Network_Usage(
bandwidth: Bandwidth(upload: 1_238_400, download: 18_732_000),
raddr: Network_addr(v4: "192.168.0.1"),
interface: Network_interface(displayName: "Stats"),
status: true
))
public var date: Date {
Calendar.current.date(byAdding: .second, value: 5, to: Date())!
}
public var value: Network_Usage? = nil
}
public struct Provider: TimelineProvider {
public typealias Entry = Network_entry
private let userDefaults: UserDefaults? = UserDefaults(suiteName: "\(Bundle.main.object(forInfoDictionaryKey: "TeamId") as! String).eu.exelban.Stats.widgets")
public func placeholder(in context: Context) -> Network_entry {
Network_entry()
}
public func getSnapshot(in context: Context, completion: @escaping (Network_entry) -> Void) {
completion(Network_entry.snapshot)
}
public func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
self.userDefaults?.set(Date().timeIntervalSince1970, forKey: Network_entry.kind)
var entry = Network_entry()
if let raw = userDefaults?.data(forKey: "Network@UsageReader"), let load = try? JSONDecoder().decode(Network_Usage.self, from: raw) {
entry.value = load
}
let entries: [Network_entry] = [entry]
completion(Timeline(entries: entries, policy: .atEnd))
}
}
@available(macOS 14.0, *)
public struct NetworkWidget: Widget {
private var downloadColor: Color = Color(nsColor: NSColor.systemBlue)
private var uploadColor: Color = Color(nsColor: NSColor.systemRed)
public init() {}
public var body: some WidgetConfiguration {
StaticConfiguration(kind: Network_entry.kind, provider: Provider()) { entry in
VStack(spacing: 10) {
if let value = entry.value {
VStack {
HStack {
VStack {
VStack(spacing: 0) {
Text(Units(bytes: value.bandwidth.download).getReadableTuple().0).font(.system(size: 24, weight: .regular))
Text(Units(bytes: value.bandwidth.download).getReadableTuple().1).font(.system(size: 10, weight: .regular))
}
Text("Download").font(.system(size: 12, weight: .regular)).foregroundColor(.gray)
}.frame(maxWidth: .infinity)
VStack {
VStack(spacing: 0) {
Text(Units(bytes: value.bandwidth.upload).getReadableTuple().0).font(.system(size: 24, weight: .regular))
Text(Units(bytes: value.bandwidth.upload).getReadableTuple().1).font(.system(size: 10, weight: .regular))
}
Text("Upload").font(.system(size: 12, weight: .regular)).foregroundColor(.gray)
}.frame(maxWidth: .infinity)
}
.frame(maxHeight: .infinity)
VStack(spacing: 3) {
HStack {
Text("Status").font(.system(size: 12, weight: .regular)).foregroundColor(.secondary)
Spacer()
Text(value.status ? "UP" : "DOWN")
}
if let interface = value.interface {
HStack {
Text("Interface").font(.system(size: 12, weight: .regular)).foregroundColor(.secondary)
Spacer()
Text(value.wifiDetails.ssid ?? interface.displayName)
}
}
HStack {
Text("IP").font(.system(size: 12, weight: .regular)).foregroundColor(.secondary)
Spacer()
if let raddr = value.raddr.v6 {
Text(raddr)
.font(.system(size: 8))
.multilineTextAlignment(.trailing)
.frame(maxWidth: .infinity, alignment: .center)
} else if let raddr = value.raddr.v4 {
Text(raddr)
} else {
Text("Unknown")
}
}
}
}
} else {
Text("No data")
}
}
.containerBackground(for: .widget) {
Color.clear
}
}
.configurationDisplayName("Network widget")
.description("Displays network stats")
.supportedFamilies([.systemSmall])
}
}
================================================
FILE: Modules/RAM/Info.plist
================================================
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
1.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSHumanReadableCopyright
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
================================================
FILE: Modules/RAM/config.plist
================================================
Name
RAM
State
Symbol
memorychip
Widgets
label
Default
Order
0
mini
Default
Preview
Value
0.58
Order
1
line_chart
Default
Color
systemAccent
Order
2
bar_chart
Default
Label
Box
Color
systemAccent
Preview
Label
Box
Color
Value
0.48
Order
3
Unsupported colors
cluster
pie_chart
Default
Order
4
memory
Default
Preview
Value
7.75 GB, 8.25 GB
Order
5
tachometer
Default
Order
6
text
Default
Order
7
Settings
popup
notifications
================================================
FILE: Modules/RAM/main.swift
================================================
//
// main.swift
// Memory
//
// Created by Serhiy Mytrovtsiy on 12/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
import WidgetKit
public struct RAM_Usage: Codable, RemoteType {
var total: Double
var used: Double
var free: Double
var active: Double
var inactive: Double
var wired: Double
var compressed: Double
var app: Double
var cache: Double
var swap: Swap
var pressure: Pressure
var swapins: Int64
var swapouts: Int64
public var usage: Double {
get { Double((self.total - self.free) / self.total) }
}
public func remote() -> Data? {
let string = "\(self.total),\(self.used),\(self.pressure.level),\(self.swap.used)$"
return string.data(using: .utf8)
}
}
public struct Swap: Codable {
var total: Double
var used: Double
var free: Double
}
public struct Pressure: Codable {
let level: Int
let value: RAMPressure
}
public class RAM: Module {
private let popupView: Popup
private let settingsView: Settings
private let portalView: Portal
private let notificationsView: Notifications
private var usageReader: UsageReader? = nil
private var processReader: ProcessReader? = nil
private var splitValueState: Bool {
return Store.shared.bool(key: "\(self.config.name)_splitValue", defaultValue: false)
}
private var appColor: NSColor {
let color = SColor.secondBlue
let key = Store.shared.string(key: "\(self.config.name)_appColor", defaultValue: color.key)
if let c = SColor.fromString(key).additional as? NSColor {
return c
}
return color.additional as! NSColor
}
private var wiredColor: NSColor {
let color = SColor.secondOrange
let key = Store.shared.string(key: "\(self.config.name)_wiredColor", defaultValue: color.key)
if let c = SColor.fromString(key).additional as? NSColor {
return c
}
return color.additional as! NSColor
}
private var compressedColor: NSColor {
let color = SColor.pink
let key = Store.shared.string(key: "\(self.config.name)_compressedColor", defaultValue: color.key)
if let c = SColor.fromString(key).additional as? NSColor {
return c
}
return color.additional as! NSColor
}
private var textValue: String {
Store.shared.string(key: "\(self.name)_textWidgetValue", defaultValue: "$mem.used/$mem.total ($pressure.value)")
}
private var systemWidgetsUpdatesState: Bool {
self.userDefaults?.bool(forKey: "systemWidgetsUpdates_state") ?? false
}
public init() {
self.settingsView = Settings(.RAM)
self.popupView = Popup(.RAM)
self.portalView = Portal(.RAM)
self.notificationsView = Notifications(.RAM)
super.init(
moduleType: .RAM,
popup: self.popupView,
settings: self.settingsView,
portal: self.portalView,
notifications: self.notificationsView
)
guard self.available else { return }
self.settingsView.callback = { [weak self] in
self?.usageReader?.read()
}
self.settingsView.setInterval = { [weak self] value in
self?.processReader?.read()
self?.usageReader?.setInterval(value)
}
self.settingsView.setTopInterval = { [weak self] value in
self?.processReader?.setInterval(value)
}
self.usageReader = UsageReader(.RAM) { [weak self] value in
self?.loadCallback(value)
}
self.processReader = ProcessReader(.RAM) { [weak self] value in
if let list = value {
self?.popupView.processCallback(list)
}
}
self.settingsView.callbackWhenUpdateNumberOfProcesses = { [weak self] in
self?.popupView.numberOfProcessesUpdated()
DispatchQueue.global(qos: .background).async {
self?.processReader?.read()
}
}
self.setReaders([self.usageReader, self.processReader])
}
private func loadCallback(_ raw: RAM_Usage?) {
guard let value = raw, self.enabled else { return }
self.popupView.loadCallback(value)
self.portalView.callback(value)
self.notificationsView.loadCallback(value)
let total: Double = value.total == 0 ? 1 : value.total
self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: SWidget) in
switch w.item {
case let widget as Mini:
widget.setValue(value.usage)
widget.setPressure(value.pressure.value)
case let widget as LineChart:
widget.setValue(value.usage)
widget.setPressure(value.pressure.value)
case let widget as BarChart:
if self.splitValueState {
widget.setValue([[
ColorValue(value.app/total, color: self.appColor),
ColorValue(value.wired/total, color: self.wiredColor),
ColorValue(value.compressed/total, color: self.compressedColor)
]])
} else {
widget.setValue([[ColorValue(value.usage)]])
widget.setColorZones((0.8, 0.95))
widget.setPressure(value.pressure.value)
}
case let widget as PieChart:
widget.setValue([
circle_segment(value: value.app/total, color: self.appColor),
circle_segment(value: value.wired/total, color: self.wiredColor),
circle_segment(value: value.compressed/total, color: self.compressedColor)
])
case let widget as MemoryWidget:
let free = Units(bytes: Int64(value.free)).getReadableMemory(style: .memory)
let used = Units(bytes: Int64(value.used)).getReadableMemory(style: .memory)
widget.setValue((free, used), usedPercentage: value.usage)
widget.setPressure(value.pressure.value)
case let widget as Tachometer:
widget.setValue([
circle_segment(value: value.app/total, color: self.appColor),
circle_segment(value: value.wired/total, color: self.wiredColor),
circle_segment(value: value.compressed/total, color: self.compressedColor)
])
case let widget as TextWidget:
var text = "\(self.textValue)"
let pairs = TextWidget.parseText(text)
pairs.forEach { pair in
var replacement: String? = nil
switch pair.key {
case "$mem":
switch pair.value {
case "total": replacement = Units(bytes: Int64(value.total)).getReadableMemory(style: .memory)
case "used": replacement = Units(bytes: Int64(value.used)).getReadableMemory(style: .memory)
case "free": replacement = Units(bytes: Int64(value.free)).getReadableMemory(style: .memory)
case "active": replacement = Units(bytes: Int64(value.active)).getReadableMemory(style: .memory)
case "inactive": replacement = Units(bytes: Int64(value.inactive)).getReadableMemory(style: .memory)
case "wired": replacement = Units(bytes: Int64(value.wired)).getReadableMemory(style: .memory)
case "compressed": replacement = Units(bytes: Int64(value.compressed)).getReadableMemory(style: .memory)
case "app": replacement = Units(bytes: Int64(value.app)).getReadableMemory(style: .memory)
case "cache": replacement = Units(bytes: Int64(value.cache)).getReadableMemory(style: .memory)
case "swapins": replacement = "\(value.swapins)"
case "swapouts": replacement = "\(value.swapouts)"
default: return
}
case "$swap":
switch pair.value {
case "total": replacement = Units(bytes: Int64(value.swap.total)).getReadableMemory(style: .memory)
case "used": replacement = Units(bytes: Int64(value.swap.used)).getReadableMemory(style: .memory)
case "free": replacement = Units(bytes: Int64(value.swap.free)).getReadableMemory(style: .memory)
default: return
}
case "$pressure":
switch pair.value {
case "level": replacement = "\(value.pressure.level)"
case "value": replacement = value.pressure.value.rawValue
default: return
}
default: return
}
if let replacement {
let key = pair.value.isEmpty ? pair.key : "\(pair.key).\(pair.value)"
text = text.replacingOccurrences(of: key, with: replacement)
}
}
widget.setValue(text)
default: break
}
}
if self.systemWidgetsUpdatesState {
if isWidgetActive(self.userDefaults, [RAM_entry.kind, "UnitedWidget"]), let blobData = try? JSONEncoder().encode(value) {
self.userDefaults?.set(blobData, forKey: "RAM@UsageReader")
}
WidgetCenter.shared.reloadTimelines(ofKind: RAM_entry.kind)
WidgetCenter.shared.reloadTimelines(ofKind: "UnitedWidget")
}
}
}
================================================
FILE: Modules/RAM/notifications.swift
================================================
//
// notifications.swift
// RAM
//
// Created by Serhiy Mytrovtsiy on 05/12/2023
// Using Swift 5.0
// Running on macOS 14.1
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal let memoryPressureLevels: [KeyValue_t] = [
KeyValue_t(key: "warning", value: "Warning", additional: DispatchSource.MemoryPressureEvent.warning),
KeyValue_t(key: "critical", value: "Critical", additional: DispatchSource.MemoryPressureEvent.critical)
]
class Notifications: NotificationsWrapper {
private let totalID: String = "totalUsage"
private let freeID: String = "free"
private let pressureID: String = "pressure"
private let swapID: String = "swap"
private var totalState: Bool = false
private var freeState: Bool = false
private var pressureState: Bool = false
private var swapState: Bool = false
private var total: Int = 75
private var free: Int = 75
private var pressure: String = ""
private var swap: Int = 1
private var swapUnit: SizeUnit = .GB
public init(_ module: ModuleType) {
super.init(module, [self.totalID, self.freeID, self.pressureID, self.swapID])
if Store.shared.exist(key: "\(self.module)_notifications_totalUsage") {
let value = Store.shared.string(key: "\(self.module)_notifications_totalUsage", defaultValue: "")
if let v = Double(value) {
Store.shared.set(key: "\(self.module)_notifications_total_state", value: true)
Store.shared.set(key: "\(self.module)_notifications_total_value", value: Int(v*100))
Store.shared.remove("\(self.module)_notifications_totalUsage")
}
}
if Store.shared.exist(key: "\(self.module)_notifications_free") {
let value = Store.shared.string(key: "\(self.module)_notifications_free", defaultValue: "")
if let v = Double(value) {
Store.shared.set(key: "\(self.module)_notifications_free_state", value: true)
Store.shared.set(key: "\(self.module)_notifications_free_value", value: Int(v*100))
Store.shared.remove("\(self.module)_notifications_free")
}
}
if Store.shared.exist(key: "\(self.module)_notifications_pressure") {
let value = Store.shared.string(key: "\(self.module)_notifications_pressure", defaultValue: "")
if value != "" {
Store.shared.set(key: "\(self.module)_notifications_pressure_state", value: true)
Store.shared.set(key: "\(self.module)_notifications_pressure_value", value: value)
Store.shared.remove("\(self.module)_notifications_pressure")
}
}
if Store.shared.exist(key: "\(self.module)_notifications_swap") {
let value = Store.shared.string(key: "\(self.module)_notifications_swap", defaultValue: "")
if value != "" {
Store.shared.set(key: "\(self.module)_notifications_swap_state", value: true)
Store.shared.remove("\(self.module)_notifications_swap")
}
}
self.totalState = Store.shared.bool(key: "\(self.module)_notifications_total_state", defaultValue: self.totalState)
self.total = Store.shared.int(key: "\(self.module)_notifications_total_value", defaultValue: self.total)
self.freeState = Store.shared.bool(key: "\(self.module)_notifications_free_state", defaultValue: self.freeState)
self.free = Store.shared.int(key: "\(self.module)_notifications_free_value", defaultValue: self.free)
self.pressureState = Store.shared.bool(key: "\(self.module)_notifications_pressure_state", defaultValue: self.pressureState)
self.pressure = Store.shared.string(key: "\(self.module)_notifications_pressure_value", defaultValue: self.pressure)
self.swapState = Store.shared.bool(key: "\(self.module)_notifications_swap_state", defaultValue: self.swapState)
self.swap = Store.shared.int(key: "\(self.module)_notifications_swap_value", defaultValue: self.swap)
self.swapUnit = SizeUnit.fromString(Store.shared.string(key: "\(self.module)_notifications_swap_unit", defaultValue: self.swapUnit.key))
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Usage"), component: PreferencesSwitch(
action: self.toggleTotal, state: self.totalState, with: StepperInput(self.total, callback: self.changeTotal)
)),
PreferencesRow(localizedString("Free memory (less than)"), component: PreferencesSwitch(
action: self.toggleFree, state: self.freeState, with: StepperInput(self.free, callback: self.changeFree)
))
]))
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Memory pressure"), component: PreferencesSwitch(
action: self.togglePressure, state: self.pressureState,
with: selectView(action: #selector(self.changePressure), items: memoryPressureLevels, selected: self.pressure)
))
]))
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Swap size"), component: PreferencesSwitch(
action: self.toggleSwap, state: self.swapState, with: StepperInput(
self.swap, range: NSRange(location: 1, length: 1023), unit: self.swapUnit.key, units: SizeUnit.allCases,
callback: self.changeSwap, unitCallback: self.changeSwapUnit
)
))
]))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func loadCallback(_ value: RAM_Usage) {
let title = localizedString("RAM utilization threshold")
if self.totalState {
let subtitle = localizedString("RAM utilization is", "\(Int((value.usage)*100))%")
self.checkDouble(id: self.totalID, value: value.usage, threshold: Double(self.total)/100, title: title, subtitle: subtitle)
}
if self.freeState {
let free = value.free / value.total
let subtitle = localizedString("Free RAM is", "\(Int((free)*100))%")
self.checkDouble(id: self.freeID, value: free, threshold: Double(self.free)/100, title: title, subtitle: subtitle, less: true)
}
if self.pressureState, self.pressure != "", let thresholdPair = memoryPressureLevels.first(where: {$0.key == self.pressure}) {
if let threshold = thresholdPair.additional as? DispatchSource.MemoryPressureEvent {
self.checkDouble(
id: self.pressureID,
value: Double(value.pressure.level),
threshold: Double(threshold.rawValue),
title: title,
subtitle: "\(localizedString("Memory pressure")): \(localizedString(thresholdPair.value))"
)
}
}
if self.swapState {
let value = Units(bytes: Int64(value.swap.used))
let subtitle = "\(localizedString("Swap size")): \(value.getReadableMemory())"
self.checkDouble(id: self.swapID, value: value.toUnit(self.swapUnit), threshold: Double(self.swap), title: title, subtitle: subtitle)
}
}
@objc private func toggleTotal(_ sender: NSControl) {
self.totalState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_total_state", value: self.totalState)
}
@objc private func changeTotal(_ newValue: Int) {
self.total = newValue
Store.shared.set(key: "\(self.module)_notifications_total_value", value: self.total)
}
@objc private func toggleFree(_ sender: NSControl) {
self.freeState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_free_state", value: self.freeState)
}
@objc private func changeFree(_ newValue: Int) {
self.free = newValue
Store.shared.set(key: "\(self.module)_notifications_free_value", value: self.free)
}
@objc private func togglePressure(_ sender: NSControl) {
self.pressureState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_pressure_state", value: self.pressureState)
}
@objc private func changePressure(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.pressure = key
Store.shared.set(key: "\(self.module)_notifications_pressure_value", value: self.pressure)
}
@objc private func toggleSwap(_ sender: NSControl) {
self.swapState = controlState(sender)
Store.shared.set(key: "\(self.module)_notifications_swap_state", value: self.swapState)
}
@objc private func changeSwap(_ newValue: Int) {
self.swap = newValue
Store.shared.set(key: "\(self.module)_notifications_swap_value", value: self.swap)
}
private func changeSwapUnit(_ newValue: KeyValue_p) {
guard let newUnit = newValue as? SizeUnit else { return }
self.swapUnit = newUnit
Store.shared.set(key: "\(self.module)_notifications_swap_unit", value: self.swapUnit.key)
}
}
================================================
FILE: Modules/RAM/popup.swift
================================================
//
// popup.swift
// Memory
//
// Created by Serhiy Mytrovtsiy on 18/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class Popup: PopupWrapper {
private var grid: NSGridView? = nil
private let dashboardHeight: CGFloat = 90
private let chartHeight: CGFloat = 90 + Constants.Popup.separatorHeight
private let detailsHeight: CGFloat = (22*6) + Constants.Popup.separatorHeight
private let processHeight: CGFloat = 22
private var usedField: NSTextField? = nil
private var freeField: NSTextField? = nil
private var appField: NSTextField? = nil
private var inactiveField: NSTextField? = nil
private var wiredField: NSTextField? = nil
private var compressedField: NSTextField? = nil
private var swapField: NSTextField? = nil
private var appColorView: NSView? = nil
private var wiredColorView: NSView? = nil
private var compressedColorView: NSView? = nil
private var freeColorView: NSView? = nil
private var sliderView: NSView? = nil
private var chart: LineChartView? = nil
private var circle: PieChartView? = nil
private var level: PressureView? = nil
private var initialized: Bool = false
private var processesInitialized: Bool = false
private var processes: ProcessesView? = nil
private var numberOfProcesses: Int {
Store.shared.int(key: "\(self.title)_processes", defaultValue: 8)
}
private var processesHeight: CGFloat {
(self.processHeight*CGFloat(self.numberOfProcesses)) + (self.numberOfProcesses == 0 ? 0 : Constants.Popup.separatorHeight + 22)
}
private var lineChartHistory: Int = 180
private var lineChartScale: Scale = .none
private var lineChartFixedScale: Double = 1
private var chartPrefSection: PreferencesSection? = nil
private var appColorState: SColor = .secondBlue
private var appColor: NSColor { self.appColorState.additional as? NSColor ?? NSColor.systemRed }
private var wiredColorState: SColor = .secondOrange
private var wiredColor: NSColor { self.wiredColorState.additional as? NSColor ?? NSColor.systemBlue }
private var compressedColorState: SColor = .pink
private var compressedColor: NSColor { self.compressedColorState.additional as? NSColor ?? NSColor.lightGray }
private var freeColorState: SColor = .lightGray
private var freeColor: NSColor { self.freeColorState.additional as? NSColor ?? NSColor.systemBlue }
private var chartColorState: SColor = .systemAccent
private var chartColor: NSColor { self.chartColorState.additional as? NSColor ?? NSColor.systemBlue }
public init(_ module: ModuleType) {
super.init(module, frame: NSRect(
x: 0,
y: 0,
width: Constants.Popup.width,
height: dashboardHeight + chartHeight + detailsHeight
))
self.setFrameSize(NSSize(width: self.frame.width, height: self.frame.height+self.processesHeight))
self.appColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_appColor", defaultValue: self.appColorState.key))
self.wiredColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_wiredColor", defaultValue: self.wiredColorState.key))
self.compressedColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_compressedColor", defaultValue: self.compressedColorState.key))
self.freeColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_freeColor", defaultValue: self.freeColorState.key))
self.chartColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_chartColor", defaultValue: self.chartColorState.key))
self.lineChartHistory = Store.shared.int(key: "\(self.title)_lineChartHistory", defaultValue: self.lineChartHistory)
self.lineChartScale = Scale.fromString(Store.shared.string(key: "\(self.title)_lineChartScale", defaultValue: self.lineChartScale.key))
self.lineChartFixedScale = Double(Store.shared.int(key: "\(self.title)_lineChartFixedScale", defaultValue: 100)) / 100
let gridView: NSGridView = NSGridView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height))
gridView.rowSpacing = 0
gridView.yPlacement = .fill
gridView.addRow(with: [self.initDashboard()])
gridView.addRow(with: [self.initChart()])
gridView.addRow(with: [self.initDetails()])
gridView.addRow(with: [self.initProcesses()])
gridView.row(at: 0).height = self.dashboardHeight
gridView.row(at: 1).height = self.chartHeight
gridView.row(at: 2).height = self.detailsHeight
self.addSubview(gridView)
self.grid = gridView
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func updateLayer() {
self.chart?.display()
}
public override func disappear() {
self.processes?.setLock(false)
}
public func numberOfProcessesUpdated() {
if self.processes?.count == self.numberOfProcesses { return }
DispatchQueue.main.async(execute: {
let h: CGFloat = self.dashboardHeight + self.chartHeight + self.detailsHeight + self.processesHeight
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.grid?.setFrameSize(NSSize(width: self.frame.width, height: h))
self.grid?.row(at: 3).cell(at: 0).contentView?.removeFromSuperview()
self.processes = nil
self.grid?.removeRow(at: 3)
self.grid?.addRow(with: [self.initProcesses()])
self.processesInitialized = false
self.sizeCallback?(self.frame.size)
})
}
private func initDashboard() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: self.frame.height - self.dashboardHeight, width: self.frame.width, height: self.dashboardHeight))
let container: NSView = NSView(frame: NSRect(x: 0, y: 10, width: view.frame.width, height: self.dashboardHeight-20))
self.circle = PieChartView(frame: NSRect(
x: (container.frame.width - container.frame.height)/2,
y: 0,
width: container.frame.height,
height: container.frame.height
), segments: [], drawValue: true)
self.circle!.toolTip = localizedString("Memory usage")
container.addSubview(self.circle!)
let centralWidth: CGFloat = self.dashboardHeight-20
let sideWidth: CGFloat = (view.frame.width - centralWidth - (Constants.Popup.margins*2))/2
self.level = PressureView(frame: NSRect(x: (sideWidth - 60)/2, y: 10, width: 60, height: 50))
self.level!.toolTip = localizedString("Memory pressure")
view.addSubview(self.level!)
view.addSubview(container)
return view
}
private func initChart() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.chartHeight))
let separator = separatorView(localizedString("Usage history"), origin: NSPoint(x: 0, y: self.chartHeight-Constants.Popup.separatorHeight), width: self.frame.width)
let container: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: separator.frame.origin.y))
container.wantsLayer = true
container.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.1).cgColor
container.layer?.cornerRadius = 3
let chartFrame = NSRect(x: 1, y: 0, width: view.frame.width, height: container.frame.height)
self.chart = LineChartView(frame: chartFrame, num: self.lineChartHistory, scale: self.lineChartScale, fixedScale: self.lineChartFixedScale)
self.chart?.color = self.chartColor
container.addSubview(self.chart!)
view.addSubview(separator)
view.addSubview(container)
return view
}
private func initDetails() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.detailsHeight))
let separator = separatorView(localizedString("Details"), origin: NSPoint(x: 0, y: self.detailsHeight-Constants.Popup.separatorHeight), width: self.frame.width)
let container: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: separator.frame.origin.y))
container.orientation = .vertical
container.spacing = 0
self.usedField = popupRow(container, title: "\(localizedString("Used")):", value: "").1
(self.appColorView, _, self.appField) = popupWithColorRow(container, color: self.appColor, title: "\(localizedString("App")):", value: "")
(self.wiredColorView, _, self.wiredField) = popupWithColorRow(container, color: self.wiredColor, title: "\(localizedString("Wired")):", value: "")
(self.compressedColorView, _, self.compressedField) = popupWithColorRow(container, color: self.compressedColor, title: "\(localizedString("Compressed")):", value: "")
(self.freeColorView, _, self.freeField) = popupWithColorRow(container, color: self.freeColor.withAlphaComponent(0.5), title: "\(localizedString("Free")):", value: "")
self.swapField = popupRow(container, title: "\(localizedString("Swap")):", value: "").1
view.addSubview(separator)
view.addSubview(container)
return view
}
private func initProcesses() -> NSView {
if self.numberOfProcesses == 0 { return NSView() }
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.processesHeight))
let separator = separatorView(localizedString("Top processes"), origin: NSPoint(x: 0, y: self.processesHeight-Constants.Popup.separatorHeight), width: self.frame.width)
let container: ProcessesView = ProcessesView(
frame: NSRect(x: 0, y: 0, width: self.frame.width, height: separator.frame.origin.y),
values: [(localizedString("Usage"), nil)],
n: self.numberOfProcesses
)
self.processes = container
view.addSubview(separator)
view.addSubview(container)
return view
}
private func addFirstRow(mView: NSView, y: CGFloat, title: String, value: String) -> NSTextField {
let rowView: NSView = NSView(frame: NSRect(x: 0, y: y, width: mView.frame.width, height: 16))
let labelWidth = title.widthOfString(usingFont: .systemFont(ofSize: 10, weight: .light)) + 4
let labelView: NSTextField = TextView(frame: NSRect(x: 0, y: 1.5, width: labelWidth, height: 13))
labelView.stringValue = title
labelView.alignment = .natural
labelView.font = NSFont.systemFont(ofSize: 10, weight: .light)
let valueView: NSTextField = TextView(frame: NSRect(x: labelWidth, y: 1, width: mView.frame.width - labelWidth, height: 14))
valueView.stringValue = value
valueView.alignment = .right
valueView.font = NSFont.systemFont(ofSize: 11, weight: .medium)
rowView.addSubview(labelView)
rowView.addSubview(valueView)
mView.addSubview(rowView)
return valueView
}
public func loadCallback(_ value: RAM_Usage) {
DispatchQueue.main.async(execute: {
if (self.window?.isVisible ?? false) || !self.initialized {
self.appField?.stringValue = Units(bytes: Int64(value.app)).getReadableMemory(style: .memory)
self.inactiveField?.stringValue = Units(bytes: Int64(value.inactive)).getReadableMemory(style: .memory)
self.wiredField?.stringValue = Units(bytes: Int64(value.wired)).getReadableMemory(style: .memory)
self.compressedField?.stringValue = Units(bytes: Int64(value.compressed)).getReadableMemory(style: .memory)
self.swapField?.stringValue = Units(bytes: Int64(value.swap.used)).getReadableMemory(style: .memory)
self.usedField?.stringValue = Units(bytes: Int64(value.used)).getReadableMemory(style: .memory)
self.freeField?.stringValue = Units(bytes: Int64(value.free)).getReadableMemory(style: .memory)
self.circle?.toolTip = "\(localizedString("Memory usage")): \(Int(value.usage*100))%"
self.circle?.setValue(value.usage)
self.circle?.setSegments([
circle_segment(value: value.app/value.total, color: self.appColor),
circle_segment(value: value.wired/value.total, color: self.wiredColor),
circle_segment(value: value.compressed/value.total, color: self.compressedColor)
])
self.circle?.setNonActiveSegmentColor(self.freeColor)
self.level?.setValue(value.pressure)
self.level?.toolTip = "\(localizedString("Memory pressure")): \(value.pressure.value.rawValue)"
self.initialized = true
}
self.chart?.addValue(value.usage)
})
}
public func processCallback(_ list: [TopProcess]) {
DispatchQueue.main.async(execute: {
if !(self.window?.isVisible ?? false) && self.processesInitialized {
return
}
let list = list.map { $0 }
if list.count != self.processes?.count { self.processes?.clear() }
for i in 0.. NSView? {
let view = SettingsContainerView()
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Keyboard shortcut"), component: KeyboardShartcutView(
callback: self.setKeyboardShortcut,
value: self.keyboardShortcut
))
]))
view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("App color"), component: selectView(
action: #selector(toggleAppColor),
items: SColor.allColors,
selected: self.appColorState.key
)),
PreferencesRow(localizedString("Wired color"), component: selectView(
action: #selector(toggleWiredColor),
items: SColor.allColors,
selected: self.wiredColorState.key
)),
PreferencesRow(localizedString("Compressed color"), component: selectView(
action: #selector(toggleCompressedColor),
items: SColor.allColors,
selected: self.compressedColorState.key
)),
PreferencesRow(localizedString("Free color"), component: selectView(
action: #selector(toggleFreeColor),
items: SColor.allColors,
selected: self.freeColorState.key
))
]))
self.sliderView = sliderView(
action: #selector(self.toggleLineChartFixedScale),
value: Int(self.lineChartFixedScale * 100),
initialValue: "\(Int(self.lineChartFixedScale * 100)) %"
)
self.chartPrefSection = PreferencesSection([
PreferencesRow(localizedString("Chart color"), component: selectView(
action: #selector(self.toggleChartColor),
items: SColor.allColors,
selected: self.chartColorState.key
)),
PreferencesRow(localizedString("Chart history"), component: selectView(
action: #selector(self.toggleLineChartHistory),
items: LineChartHistory,
selected: "\(self.lineChartHistory)"
)),
PreferencesRow(localizedString("Main chart scaling"), component: selectView(
action: #selector(self.toggleLineChartScale),
items: Scale.allCases,
selected: self.lineChartScale.key
)),
PreferencesRow(localizedString("Scale value"), component: self.sliderView!)
])
self.chartPrefSection?.setRowVisibility(3, newState: self.lineChartScale == .fixed)
view.addArrangedSubview(self.chartPrefSection!)
return view
}
@objc private func toggleAppColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.appColorState = newValue
Store.shared.set(key: "\(self.title)_appColor", value: key)
if let color = newValue.additional as? NSColor {
self.appColorView?.layer?.backgroundColor = color.cgColor
}
}
@objc private func toggleWiredColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.wiredColorState = newValue
Store.shared.set(key: "\(self.title)_wiredColor", value: key)
if let color = newValue.additional as? NSColor {
self.wiredColorView?.layer?.backgroundColor = color.cgColor
}
}
@objc private func toggleCompressedColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.compressedColorState = newValue
Store.shared.set(key: "\(self.title)_compressedColor", value: key)
if let color = newValue.additional as? NSColor {
self.compressedColorView?.layer?.backgroundColor = color.cgColor
}
}
@objc private func toggleFreeColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.freeColorState = newValue
Store.shared.set(key: "\(self.title)_freeColor", value: key)
if let color = newValue.additional as? NSColor {
self.freeColorView?.layer?.backgroundColor = color.cgColor
}
}
@objc private func toggleChartColor(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let newValue = SColor.allColors.first(where: { $0.key == key }) else {
return
}
self.chartColorState = newValue
Store.shared.set(key: "\(self.title)_chartColor", value: key)
if let color = newValue.additional as? NSColor {
self.chart?.color = color
}
}
@objc private func toggleLineChartHistory(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let value = Int(key) else { return }
self.lineChartHistory = value
Store.shared.set(key: "\(self.title)_lineChartHistory", value: value)
self.chart?.reinit(self.lineChartHistory)
}
@objc private func toggleLineChartScale(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String,
let value = Scale.allCases.first(where: { $0.key == key }) else { return }
self.chartPrefSection?.setRowVisibility(3, newState: value == .fixed)
self.lineChartScale = value
self.chart?.setScale(self.lineChartScale, fixedScale: self.lineChartFixedScale)
Store.shared.set(key: "\(self.title)_lineChartScale", value: key)
self.display()
}
@objc private func toggleLineChartFixedScale(_ sender: NSSlider) {
let value = Int(sender.doubleValue)
if let field = self.sliderView?.subviews.first(where: { $0 is NSTextField }), let view = field as? NSTextField {
view.stringValue = "\(value) %"
}
self.lineChartFixedScale = sender.doubleValue / 100
self.chart?.setScale(self.lineChartScale, fixedScale: self.lineChartFixedScale)
Store.shared.set(key: "\(self.title)_lineChartFixedScale", value: value)
}
}
public class PressureView: NSView {
private let segments: [circle_segment] = [
circle_segment(value: 1/3, color: NSColor.systemGreen),
circle_segment(value: 1/3, color: NSColor.systemYellow),
circle_segment(value: 1/3, color: NSColor.systemRed)
]
private var value: Pressure = Pressure(level: 1, value: .normal)
public override init(frame: NSRect) {
super.init(frame: frame)
self.setAccessibilityElement(true)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func draw(_ rect: CGRect) {
let arcWidth: CGFloat = 7.0
let centerPoint = CGPoint(x: self.frame.width/2, y: self.frame.height/2)
let radius = (min(self.frame.width, self.frame.height) - arcWidth) / 2
guard let context = NSGraphicsContext.current?.cgContext else { return }
context.setShouldAntialias(true)
context.setLineWidth(arcWidth)
context.setLineCap(.round)
let startAngle: CGFloat = -(1/4)*CGFloat.pi
let endCircle: CGFloat = (7/4)*CGFloat.pi - (1/4)*CGFloat.pi
var previousAngle = startAngle
context.saveGState()
context.translateBy(x: self.frame.width, y: 0)
context.scaleBy(x: -1, y: 1)
for segment in self.segments {
let currentAngle: CGFloat = previousAngle + (CGFloat(segment.value) * endCircle)
context.setStrokeColor(segment.color.cgColor)
context.addArc(center: centerPoint, radius: radius, startAngle: previousAngle, endAngle: currentAngle, clockwise: false)
context.strokePath()
previousAngle = currentAngle
}
context.restoreGState()
let needleEndSize: CGFloat = 2
let needlePath = NSBezierPath()
switch self.value.value {
case .normal:
needlePath.move(to: CGPoint(x: self.bounds.width * 0.15, y: self.bounds.width * 0.40))
needlePath.line(to: CGPoint(x: self.bounds.width/2, y: self.bounds.height/2 - needleEndSize))
needlePath.line(to: CGPoint(x: self.bounds.width/2, y: self.bounds.height/2 + needleEndSize))
case .warning:
needlePath.move(to: CGPoint(x: self.bounds.width/2, y: self.bounds.width * 0.85))
needlePath.line(to: CGPoint(x: self.bounds.width/2 - needleEndSize, y: self.bounds.height/2))
needlePath.line(to: CGPoint(x: self.bounds.width/2 + needleEndSize, y: self.bounds.height/2))
case .critical:
needlePath.move(to: CGPoint(x: self.bounds.width * 0.85, y: self.bounds.width * 0.40))
needlePath.line(to: CGPoint(x: self.bounds.width/2, y: self.bounds.height/2 - needleEndSize))
needlePath.line(to: CGPoint(x: self.bounds.width/2, y: self.bounds.height/2 + needleEndSize))
}
needlePath.close()
let needleCirclePath = NSBezierPath(
roundedRect: NSRect(x: self.bounds.width/2-needleEndSize, y: self.bounds.height/2-needleEndSize, width: needleEndSize*2, height: needleEndSize*2),
xRadius: needleEndSize*2,
yRadius: needleEndSize*2
)
needleCirclePath.close()
NSColor.systemBlue.setFill()
needlePath.fill()
needleCirclePath.fill()
let stringAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .regular),
NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor,
NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
]
let rect = CGRect(x: (self.frame.width-6)/2, y: (self.frame.height-26)/2, width: 6, height: 12)
let str = NSAttributedString.init(string: "\(self.value.level)", attributes: stringAttributes)
str.draw(with: rect)
}
public func setValue(_ newValue: Pressure) {
self.value = newValue
if self.window?.isVisible ?? true {
self.display()
}
}
}
================================================
FILE: Modules/RAM/portal.swift
================================================
//
// portal.swift
// RAM
//
// Created by Serhiy Mytrovtsiy on 17/02/2023
// Using Swift 5.0
// Running on macOS 13.2
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
public class Portal: PortalWrapper {
private var circle: PieChartView? = nil
private var usedField: NSTextField? = nil
private var freeField: NSTextField? = nil
private var swapField: NSTextField? = nil
private var pressureLevelField: NSTextField? = nil
private var initialized: Bool = false
private var appColorState: SColor = .secondBlue
private var appColor: NSColor { self.appColorState.additional as? NSColor ?? NSColor.systemRed }
private var wiredColorState: SColor = .secondOrange
private var wiredColor: NSColor { self.wiredColorState.additional as? NSColor ?? NSColor.systemBlue }
private var compressedColorState: SColor = .pink
private var compressedColor: NSColor { self.compressedColorState.additional as? NSColor ?? NSColor.lightGray }
private var freeColorState: SColor = .lightGray
private var freeColor: NSColor { self.freeColorState.additional as? NSColor ?? NSColor.systemBlue }
public override func load() {
self.loadColors()
let view = NSStackView()
view.orientation = .horizontal
view.distribution = .fillEqually
view.spacing = Constants.Popup.spacing*2
view.edgeInsets = NSEdgeInsets(
top: 0,
left: Constants.Popup.spacing*2,
bottom: 0,
right: Constants.Popup.spacing*2
)
let chartsView = self.charts()
let detailsView = self.details()
view.addArrangedSubview(chartsView)
view.addArrangedSubview(detailsView)
self.addArrangedSubview(view)
chartsView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
}
public func loadColors() {
self.appColorState = SColor.fromString(Store.shared.string(key: "\(self.name)_appColor", defaultValue: self.appColorState.key))
self.wiredColorState = SColor.fromString(Store.shared.string(key: "\(self.name)_wiredColor", defaultValue: self.wiredColorState.key))
self.compressedColorState = SColor.fromString(Store.shared.string(key: "\(self.name)_compressedColor", defaultValue: self.compressedColorState.key))
self.freeColorState = SColor.fromString(Store.shared.string(key: "\(self.name)_freeColor", defaultValue: self.freeColorState.key))
}
private func charts() -> NSView {
let view = NSStackView()
view.orientation = .vertical
view.distribution = .fillEqually
view.spacing = Constants.Popup.spacing*2
view.edgeInsets = NSEdgeInsets(
top: Constants.Popup.spacing*4,
left: Constants.Popup.spacing*4,
bottom: Constants.Popup.spacing*4,
right: Constants.Popup.spacing*4
)
let chart = PieChartView(frame: NSRect.zero, segments: [], drawValue: true)
chart.toolTip = localizedString("Memory usage")
view.addArrangedSubview(chart)
self.circle = chart
return view
}
private func details() -> NSView {
let view = NSStackView()
view.orientation = .vertical
view.distribution = .fillEqually
view.spacing = Constants.Popup.spacing*2
self.usedField = portalRow(view, title: "\(localizedString("Used")):").1
self.freeField = portalRow(view, title: "\(localizedString("Free")):").1
self.swapField = portalRow(view, title: "\(localizedString("Swap")):").1
self.pressureLevelField = portalRow(view, title: "\(localizedString("Memory pressure")):").1
return view
}
internal func callback(_ value: RAM_Usage) {
DispatchQueue.main.async(execute: {
if (self.window?.isVisible ?? false) || !self.initialized {
self.usedField?.stringValue = Units(bytes: Int64(value.used)).getReadableMemory(style: .memory)
self.freeField?.stringValue = Units(bytes: Int64(value.free)).getReadableMemory(style: .memory)
self.swapField?.stringValue = Units(bytes: Int64(value.swap.used)).getReadableMemory(style: .memory)
self.pressureLevelField?.stringValue = value.pressure.value.rawValue
self.usedField?.toolTip = "\(Int(value.usage.rounded(toPlaces: 2) * 100))%"
self.freeField?.toolTip = "\(Int((1-value.usage).rounded(toPlaces: 2) * 100))%"
if let level = memoryPressureLevels.first(where: { $0.additional as? RAMPressure == value.pressure.value }) {
self.pressureLevelField?.toolTip = localizedString(level.value)
}
self.circle?.toolTip = "\(localizedString("Memory usage")): \(Int(value.usage*100))%"
self.circle?.setValue(value.usage)
self.circle?.setSegments([
circle_segment(value: value.app/value.total, color: self.appColor),
circle_segment(value: value.wired/value.total, color: self.wiredColor),
circle_segment(value: value.compressed/value.total, color: self.compressedColor)
])
self.circle?.setNonActiveSegmentColor(self.freeColor)
self.initialized = true
}
})
}
}
================================================
FILE: Modules/RAM/readers.swift
================================================
//
// readers.swift
// Memory
//
// Created by Serhiy Mytrovtsiy on 12/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class UsageReader: Reader {
public var totalSize: Double = 0
public override func setup() {
var stats = host_basic_info()
var count = UInt32(MemoryLayout.size / MemoryLayout.size)
let kerr: kern_return_t = withUnsafeMutablePointer(to: &stats) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
host_info(mach_host_self(), HOST_BASIC_INFO, $0, &count)
}
}
if kerr == KERN_SUCCESS {
self.totalSize = Double(stats.max_mem)
return
}
self.totalSize = 0
error("host_info(): \(String(cString: mach_error_string(kerr), encoding: String.Encoding.ascii) ?? "unknown error")", log: self.log)
}
public override func read() {
var stats = vm_statistics64()
var count = UInt32(MemoryLayout.size / MemoryLayout.size)
let result: kern_return_t = withUnsafeMutablePointer(to: &stats) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
host_statistics64(mach_host_self(), HOST_VM_INFO64, $0, &count)
}
}
if result == KERN_SUCCESS {
let active = Double(stats.active_count) * Double(vm_page_size)
let speculative = Double(stats.speculative_count) * Double(vm_page_size)
let inactive = Double(stats.inactive_count) * Double(vm_page_size)
let wired = Double(stats.wire_count) * Double(vm_page_size)
let compressed = Double(stats.compressor_page_count) * Double(vm_page_size)
let purgeable = Double(stats.purgeable_count) * Double(vm_page_size)
let external = Double(stats.external_page_count) * Double(vm_page_size)
let swapins = Int64(stats.swapins)
let swapouts = Int64(stats.swapouts)
let used = active + inactive + speculative + wired + compressed - purgeable - external
let free = self.totalSize - used
var intSize: size_t = MemoryLayout.size
var pressureLevel: Int = 0
sysctlbyname("kern.memorystatus_vm_pressure_level", &pressureLevel, &intSize, nil, 0)
var pressureValue: RAMPressure
switch pressureLevel {
case 2: pressureValue = .warning
case 4: pressureValue = .critical
default: pressureValue = .normal
}
var stringSize: size_t = MemoryLayout.size
var swap: xsw_usage = xsw_usage()
sysctlbyname("vm.swapusage", &swap, &stringSize, nil, 0)
self.callback(RAM_Usage(
total: self.totalSize,
used: used,
free: free,
active: active,
inactive: inactive,
wired: wired,
compressed: compressed,
app: used - wired - compressed,
cache: purgeable + external,
swap: Swap(
total: Double(swap.xsu_total),
used: Double(swap.xsu_used),
free: Double(swap.xsu_avail)
),
pressure: Pressure(level: pressureLevel, value: pressureValue),
swapins: swapins,
swapouts: swapouts
))
return
}
error("host_statistics64(): \(String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error")", log: self.log)
}
}
public class ProcessReader: Reader<[TopProcess]> {
private let title: String = "RAM"
private var numberOfProcesses: Int {
get { Store.shared.int(key: "\(self.title)_processes", defaultValue: 8) }
}
private var combinedProcesses: Bool{
get { Store.shared.bool(key: "\(self.title)_combinedProcesses", defaultValue: false) }
}
private typealias dynGetResponsiblePidFuncType = @convention(c) (CInt) -> CInt
public override func setup() {
self.popup = true
self.setInterval(Store.shared.int(key: "\(self.title)_updateTopInterval", defaultValue: 1))
}
public override func read() {
if self.numberOfProcesses == 0 {
return
}
let task = Process()
task.launchPath = "/usr/bin/top"
if self.combinedProcesses {
task.arguments = ["-l", "1", "-o", "mem", "-stats", "pid,command,mem"]
} else {
task.arguments = ["-l", "1", "-o", "mem", "-n", "\(self.numberOfProcesses)", "-stats", "pid,command,mem"]
}
let outputPipe = Pipe()
let errorPipe = Pipe()
defer {
outputPipe.fileHandleForReading.closeFile()
errorPipe.fileHandleForReading.closeFile()
}
task.standardOutput = outputPipe
task.standardError = errorPipe
do {
try task.run()
} catch let err {
error("top(): \(err.localizedDescription)", log: self.log)
return
}
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: outputData, encoding: .utf8)
_ = String(data: errorData, encoding: .utf8)
guard let output, !output.isEmpty else { return }
var processes: [TopProcess] = []
output.enumerateLines { (line, _) in
if line.matches("^\\d+\\** +.* +\\d+[A-Z]*\\+?\\-? *$") {
processes.append(ProcessReader.parseProcess(line))
}
}
if !self.combinedProcesses {
self.callback(processes)
return
}
var processGroups: [String: [TopProcess]] = [:]
for process in processes {
let responsiblePid = ProcessReader.getResponsiblePid(process.pid)
let groupKey = "\(responsiblePid)"
if processGroups[groupKey] != nil {
processGroups[groupKey]!.append(process)
} else {
processGroups[groupKey] = [process]
}
}
var result: [TopProcess] = []
for (_, processes) in processGroups {
let totalUsage = processes.reduce(0) { $0 + $1.usage }
let firstProcess = processes.first!
let name: String
if let app = NSRunningApplication(processIdentifier: pid_t(ProcessReader.getResponsiblePid(firstProcess.pid))),
let appName = app.localizedName {
name = appName
} else {
name = firstProcess.name
}
result.append(TopProcess(
pid: ProcessReader.getResponsiblePid(firstProcess.pid),
name: name,
usage: totalUsage
))
}
result.sort { $0.usage > $1.usage }
self.callback(Array(result.prefix(self.numberOfProcesses)))
}
private static let dynGetResponsiblePidFunc: UnsafeMutableRawPointer? = {
let result = dlsym(UnsafeMutableRawPointer(bitPattern: -1), "responsibility_get_pid_responsible_for_pid")
if result == nil {
error("Error loading responsibility_get_pid_responsible_for_pid")
}
return result
}()
static func getResponsiblePid(_ childPid: Int) -> Int {
guard ProcessReader.dynGetResponsiblePidFunc != nil else {
return childPid
}
let responsiblePid = unsafeBitCast(ProcessReader.dynGetResponsiblePidFunc, to: dynGetResponsiblePidFuncType.self)(CInt(childPid))
guard responsiblePid != -1 else {
return childPid
}
return Int(responsiblePid)
}
static public func parseProcess(_ raw: String) -> TopProcess {
var str = raw.trimmingCharacters(in: .whitespaces)
let pidString = str.find(pattern: "^\\d+")
if let range = str.range(of: pidString) {
str = str.replacingCharacters(in: range, with: "")
}
var arr = str.split(separator: " ")
if arr.first == "*" {
arr.removeFirst()
}
var usageString = str.suffix(6)
if let lastElement = arr.last {
usageString = lastElement
arr.removeLast()
}
var command = arr.joined(separator: " ")
.replacingOccurrences(of: pidString, with: "")
.trimmingCharacters(in: .whitespaces)
if let regex = try? NSRegularExpression(pattern: " (\\+|\\-)*$", options: .caseInsensitive) {
command = regex.stringByReplacingMatches(in: command, options: [], range: NSRange(location: 0, length: command.count), withTemplate: "")
}
let pid = Int(pidString.filter("01234567890.".contains)) ?? 0
var usage = Double(usageString.filter("01234567890.".contains)) ?? 0
if usageString.last == "G" {
usage *= 1024 // apply gigabyte multiplier
} else if usageString.last == "K" {
usage /= 1024 // apply kilobyte divider
} else if usageString.last == "M" && usageString.count == 5 {
usage /= 1024
usage *= 1000
}
var name: String = command
if let app = NSRunningApplication(processIdentifier: pid_t(pid)), let n = app.localizedName {
name = n
}
if command.contains("com.apple.Virtua") && name.contains("Docker") {
name = "Docker"
}
return TopProcess(pid: pid, name: name, usage: usage * Double(1000 * 1000))
}
}
================================================
FILE: Modules/RAM/settings.swift
================================================
//
// settings.swift
// Memory
//
// Created by Serhiy Mytrovtsiy on 11/07/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
var textWidgetHelp = """
Description
You can use a combination of any of the variables.
Examples:
- $mem.used/$mem.total ($pressure.value)
- Pressure: $pressure.value
- Free: $mem.free
Available variables
- $mem.total: Total RAM memory.
- $mem.used: Used RAM memory.
- $mem.free: Free RAM memory.
- $mem.active: Active RAM memory.
- $mem.inactive: Inactive RAM memory.
- $mem.wired: Wired RAM memory.
- $mem.compressed: Compressed RAM memory.
- $mem.app: Used RAM memory by applications.
- $mem.cache: Cached RAM memory.
- $mem.swapins: The number of memory pages loaded in from virtual memory to physical memory.
- $mem.swapouts: The number of memory pages swapped out to physical memory from virtual memory.
- $swap.total: Total swap memory.
- $swap.used: Used swap memory.
- $swap.free: Free swap memory.
- $pressure.value: Pressure value (normal, warning, critical).
- $pressure.level: Pressure level (1, 2, 4).
"""
internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate {
private var updateIntervalValue: Int = 1
private var updateTopIntervalValue: Int = 1
private var numberOfProcesses: Int = 8
private var splitValueState: Bool = false
private var notificationLevel: String = "Disabled"
private var textValue: String = "$mem.used/$mem.total ($pressure.value)"
private var combinedProcessesState: Bool = false
private let title: String
public var callback: (() -> Void) = {}
public var callbackWhenUpdateNumberOfProcesses: (() -> Void) = {}
public var setInterval: ((_ value: Int) -> Void) = {_ in }
public var setTopInterval: ((_ value: Int) -> Void) = {_ in }
private let textWidgetHelpPanel: HelpHUD = HelpHUD(textWidgetHelp)
public init(_ module: ModuleType) {
self.title = module.stringValue
self.updateIntervalValue = Store.shared.int(key: "\(self.title)_updateInterval", defaultValue: self.updateIntervalValue)
self.updateTopIntervalValue = Store.shared.int(key: "\(self.title)_updateTopInterval", defaultValue: self.updateTopIntervalValue)
self.numberOfProcesses = Store.shared.int(key: "\(self.title)_processes", defaultValue: self.numberOfProcesses)
self.splitValueState = Store.shared.bool(key: "\(self.title)_splitValue", defaultValue: self.splitValueState)
self.notificationLevel = Store.shared.string(key: "\(self.title)_notificationLevel", defaultValue: self.notificationLevel)
self.textValue = Store.shared.string(key: "\(self.title)_textWidgetValue", defaultValue: self.textValue)
self.combinedProcessesState = Store.shared.bool(key: "\(self.title)_combinedProcesses", defaultValue: self.combinedProcessesState)
super.init(frame: NSRect.zero)
self.orientation = .vertical
self.distribution = .gravityAreas
self.spacing = Constants.Settings.margin
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func load(widgets: [widget_t]) {
self.subviews.forEach{ $0.removeFromSuperview() }
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Update interval"), component: selectView(
action: #selector(self.changeUpdateInterval),
items: ReaderUpdateIntervals,
selected: "\(self.updateIntervalValue)"
)),
PreferencesRow(localizedString("Update interval for top processes"), component: selectView(
action: #selector(self.changeUpdateTopInterval),
items: ReaderUpdateIntervals,
selected: "\(self.updateTopIntervalValue)"
))
]))
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Combined processes"), component: switchView(
action: #selector(toggleCombinedProcesses),
state: self.combinedProcessesState
)),
PreferencesRow(localizedString("Number of top processes"), component: selectView(
action: #selector(changeNumberOfProcesses),
items: NumbersOfProcesses.map{ KeyValue_t(key: "\($0)", value: "\($0)") },
selected: "\(self.numberOfProcesses)"
))
]))
if !widgets.filter({ $0 == .barChart }).isEmpty {
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Split the value (App/Wired/Compressed)"), component: switchView(
action: #selector(toggleSplitValue),
state: self.splitValueState
))
]))
}
if widgets.contains(where: { $0 == .text }) {
let textField = self.inputField(id: "text", value: self.textValue, placeholder: localizedString("This will be visible in the text widget"))
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Text widget value"), component: textField) { [weak self] in
self?.textWidgetHelpPanel.show()
}
]))
}
}
private func inputField(id: String, value: String, placeholder: String) -> NSView {
let field: NSTextField = NSTextField()
field.identifier = NSUserInterfaceItemIdentifier(id)
field.widthAnchor.constraint(equalToConstant: 250).isActive = true
field.font = NSFont.systemFont(ofSize: 12, weight: .regular)
field.textColor = .textColor
field.isEditable = true
field.isSelectable = true
field.usesSingleLineMode = true
field.maximumNumberOfLines = 1
field.focusRingType = .none
field.stringValue = value
field.delegate = self
field.placeholderString = placeholder
return field
}
@objc private func changeUpdateInterval(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let value = Int(key) else { return }
self.updateIntervalValue = value
Store.shared.set(key: "\(self.title)_updateInterval", value: value)
self.setInterval(value)
}
@objc private func changeUpdateTopInterval(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let value = Int(key) else { return }
self.updateTopIntervalValue = value
Store.shared.set(key: "\(self.title)_updateTopInterval", value: value)
self.setTopInterval(value)
}
@objc private func changeNumberOfProcesses(_ sender: NSMenuItem) {
if let value = Int(sender.title) {
self.numberOfProcesses = value
Store.shared.set(key: "\(self.title)_processes", value: value)
self.callbackWhenUpdateNumberOfProcesses()
}
}
@objc private func toggleSplitValue(_ sender: NSControl) {
self.splitValueState = controlState(sender)
Store.shared.set(key: "\(self.title)_splitValue", value: self.splitValueState)
self.callback()
}
@objc private func toggleCombinedProcesses(_ sender: NSControl) {
self.combinedProcessesState = controlState(sender)
Store.shared.set(key: "\(self.title)_combinedProcesses", value: self.combinedProcessesState)
self.callback()
}
func controlTextDidChange(_ notification: Notification) {
if let field = notification.object as? NSTextField {
if field.identifier == NSUserInterfaceItemIdentifier("text") {
self.textValue = field.stringValue
Store.shared.set(key: "\(self.title)_textWidgetValue", value: self.textValue)
}
}
}
}
================================================
FILE: Modules/RAM/widget.swift
================================================
//
// widget.swift
// RAM
//
// Created by Serhiy Mytrovtsiy on 03/07/2024
// Using Swift 5.0
// Running on macOS 14.5
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//
import SwiftUI
import WidgetKit
import Charts
import Kit
public struct RAM_entry: TimelineEntry {
public static let kind = "RAMWidget"
public static var snapshot: RAM_entry = RAM_entry(value:
RAM_Usage(
total: 34359738368.0,
used: 18993741824.0,
free: 15365996544.0,
active: 14518042624.0,
inactive: 13899530240.0,
wired: 2209333248.0,
compressed: 414629888.0,
app: 16369778688.0,
cache: 12575948800.0,
swap: Swap(total: 0, used: 0, free: 0),
pressure: Pressure(level: 1, value: .normal),
swapins: 14,
swapouts: 16
)
)
public var date: Date {
Calendar.current.date(byAdding: .second, value: 5, to: Date())!
}
public var value: RAM_Usage? = nil
}
public struct Provider: TimelineProvider {
public typealias Entry = RAM_entry
private let userDefaults: UserDefaults? = UserDefaults(suiteName: "\(Bundle.main.object(forInfoDictionaryKey: "TeamId") as! String).eu.exelban.Stats.widgets")
public func placeholder(in context: Context) -> RAM_entry {
RAM_entry()
}
public func getSnapshot(in context: Context, completion: @escaping (RAM_entry) -> Void) {
completion(RAM_entry.snapshot)
}
public func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
self.userDefaults?.set(Date().timeIntervalSince1970, forKey: RAM_entry.kind)
var entry = RAM_entry()
if let raw = userDefaults?.data(forKey: "RAM@UsageReader"), let load = try? JSONDecoder().decode(RAM_Usage.self, from: raw) {
entry.value = load
}
let entries: [RAM_entry] = [entry]
completion(Timeline(entries: entries, policy: .atEnd))
}
}
@available(macOS 14.0, *)
public struct RAMWidget: Widget {
var usedColor: Color = Color(nsColor: NSColor.systemBlue)
var freeColor: Color = Color(nsColor: NSColor.lightGray)
public init() {}
public var body: some WidgetConfiguration {
StaticConfiguration(kind: RAM_entry.kind, provider: Provider()) { entry in
VStack(spacing: 10) {
if let value = entry.value {
HStack {
Chart {
SectorMark(angle: .value(localizedString("Used"), value.used/value.total), innerRadius: .ratio(0.8)).foregroundStyle(self.usedColor)
SectorMark(angle: .value(localizedString("Free"), 1-(value.used/value.total)), innerRadius: .ratio(0.8)).foregroundStyle(self.freeColor)
}
.frame(maxWidth: .infinity, maxHeight: 84)
.chartLegend(.hidden)
.chartBackground { chartProxy in
GeometryReader { geometry in
if let anchor = chartProxy.plotFrame {
let frame = geometry[anchor]
Text("\(Int((value.used/value.total)*100))%")
.font(.system(size: 14, weight: .regular))
.position(x: frame.midX, y: frame.midY-5)
Text("RAM")
.font(.system(size: 8, weight: .semibold))
.position(x: frame.midX, y: frame.midY+8)
}
}
}
}
VStack(spacing: 3) {
HStack {
Rectangle().fill(self.usedColor).frame(width: 12, height: 12).cornerRadius(2)
Text(localizedString("Used")).font(.system(size: 12, weight: .regular)).foregroundColor(.secondary)
Spacer()
Text(Units(bytes: Int64(value.used)).getReadableMemory(style: .memory))
}
HStack {
Rectangle().fill(self.freeColor).frame(width: 12, height: 12).cornerRadius(2)
Text(localizedString("Free")).font(.system(size: 12, weight: .regular)).foregroundColor(.secondary)
Spacer()
Text(Units(bytes: Int64(value.free)).getReadableMemory(style: .memory))
}
HStack {
Text(localizedString("Pressure level")).font(.system(size: 12, weight: .regular)).foregroundColor(.secondary)
Spacer()
Text("\(value.pressure.level)")
}
}
} else {
Text("No data")
}
}
.containerBackground(for: .widget) {
Color.clear
}
}
.configurationDisplayName("RAM widget")
.description("Displays RAM stats")
.supportedFamilies([.systemSmall])
}
}
================================================
FILE: Modules/Sensors/Info.plist
================================================
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
1.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSHumanReadableCopyright
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
================================================
FILE: Modules/Sensors/bridge.h
================================================
//
// bridge.h
// Stats
//
// Created by Serhiy Mytrovtsiy on 30/03/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
// Based on https://github.com/yujitach/MenuMeters/blob/master/hardware_reader/applesilicon_hardware_reader.m
//
#include
#include
typedef struct __IOHIDEvent *IOHIDEventRef;
typedef struct __IOHIDServiceClient *IOHIDServiceClientRef;
typedef struct IOReportSubscriptionRef* IOReportSubscriptionRef;
#ifdef __LP64__
typedef double IOHIDFloat;
#else
typedef float IOHIDFloat;
#endif
#define IOHIDEventFieldBase(type) (type << 16)
#define kIOHIDEventTypeTemperature 15
#define kIOHIDEventTypePower 25
IOHIDEventSystemClientRef IOHIDEventSystemClientCreate(CFAllocatorRef allocator);
int IOHIDEventSystemClientSetMatching(IOHIDEventSystemClientRef client, CFDictionaryRef match);
IOHIDEventRef IOHIDServiceClientCopyEvent(IOHIDServiceClientRef, int64_t , int32_t, int64_t);
CFTypeRef IOHIDServiceClientCopyProperty(IOHIDServiceClientRef service, CFStringRef property);
IOHIDFloat IOHIDEventGetFloatValue(IOHIDEventRef event, int32_t field);
NSDictionary*AppleSiliconSensors(int page, int usage, int32_t type);
CFDictionaryRef IOReportCopyChannelsInGroup(CFStringRef a, CFStringRef b, uint64_t c, uint64_t d, uint64_t e);
void IOReportMergeChannels(CFDictionaryRef a, CFDictionaryRef b, CFTypeRef null);
IOReportSubscriptionRef IOReportCreateSubscription(void* a, CFMutableDictionaryRef b, CFMutableDictionaryRef* c, uint64_t d, CFTypeRef e);
CFDictionaryRef IOReportCreateSamples(IOReportSubscriptionRef a, CFMutableDictionaryRef b, CFTypeRef c);
CFDictionaryRef IOReportCreateSamplesDelta(CFDictionaryRef a, CFDictionaryRef b, CFTypeRef c);
CFStringRef IOReportChannelGetGroup(CFDictionaryRef a);
CFStringRef IOReportChannelGetSubGroup(CFDictionaryRef a);
CFStringRef IOReportChannelGetChannelName(CFDictionaryRef a);
CFStringRef IOReportChannelGetUnitLabel(CFDictionaryRef a);
int32_t IOReportStateGetCount(CFDictionaryRef a);
CFStringRef IOReportStateGetNameForIndex(CFDictionaryRef a, int32_t b);
int64_t IOReportStateGetResidency(CFDictionaryRef a, int32_t b);
int64_t IOReportSimpleGetIntegerValue(CFDictionaryRef a, int32_t b);
================================================
FILE: Modules/Sensors/config.plist
================================================
Name
Sensors
State
Symbol
fanblades.fill
AlternativeSymbol
thermometer.sun
Widgets
label
Default
Order
0
mini
Default
Title
Sensor
Preview
Value
0.12
Unsupported colors
pressure
Order
1
sensors
Default
Preview
Values
38°,41°
Order
2
bar_chart
Default
Title
FAN
Preview
Value
0.36,0.3
Color
Unsupported colors
pressure
cluster
Order
3
Settings
popup
notifications
================================================
FILE: Modules/Sensors/main.swift
================================================
//
// main.swift
// Sensors
//
// Created by Serhiy Mytrovtsiy on 17/06/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
public class Sensors: Module {
private var sensorsReader: SensorsReader?
private let popupView: Popup
private let settingsView: Settings
private let portalView: Portal
private let notificationsView: Notifications
private var fanValueState: FanValue {
FanValue(rawValue: Store.shared.string(key: "\(self.config.name)_fanValue", defaultValue: "percentage")) ?? .percentage
}
private var selectedSensor: String
public init() {
self.settingsView = Settings(.sensors)
self.popupView = Popup()
self.portalView = Portal(.sensors)
self.notificationsView = Notifications(.sensors)
self.selectedSensor = Store.shared.string(key: "\(ModuleType.sensors.stringValue)_sensor", defaultValue: "Average System Total")
super.init(
moduleType: .sensors,
popup: self.popupView,
settings: self.settingsView,
portal: self.portalView,
notifications: self.notificationsView
)
guard self.available else { return }
self.sensorsReader = SensorsReader { [weak self] value in
self?.usageCallback(value)
}
self.settingsView.setList(self.sensorsReader?.list.sensors)
self.popupView.setup(self.sensorsReader?.list.sensors)
self.portalView.setup(self.sensorsReader?.list.sensors)
self.notificationsView.setup(self.sensorsReader?.list.sensors)
self.settingsView.callback = { [weak self] in
self?.sensorsReader?.read()
}
self.settingsView.setInterval = { [weak self] value in
self?.sensorsReader?.setInterval(value)
}
self.settingsView.HIDcallback = { [weak self] in
DispatchQueue.global(qos: .background).async {
self?.sensorsReader?.HIDCallback()
DispatchQueue.main.async {
self?.popupView.setup(self?.sensorsReader?.list.sensors)
self?.portalView.setup(self?.sensorsReader?.list.sensors)
self?.settingsView.setList(self?.sensorsReader?.list.sensors)
self?.notificationsView.setup(self?.sensorsReader?.list.sensors)
}
}
}
self.settingsView.unknownCallback = { [weak self] in
DispatchQueue.global(qos: .background).async {
self?.sensorsReader?.unknownCallback()
DispatchQueue.main.async {
self?.popupView.setup(self?.sensorsReader?.list.sensors)
self?.portalView.setup(self?.sensorsReader?.list.sensors)
self?.settingsView.setList(self?.sensorsReader?.list.sensors)
self?.notificationsView.setup(self?.sensorsReader?.list.sensors)
}
}
}
self.selectedSensor = Store.shared.string(key: "\(ModuleType.sensors.stringValue)_sensor", defaultValue: self.selectedSensor)
self.settingsView.selectedHandler = { [weak self] value in
self?.selectedSensor = value
self?.sensorsReader?.read()
}
self.setReaders([self.sensorsReader])
}
public override func willTerminate() {
guard SMCHelper.shared.isActive(), let reader = self.sensorsReader else { return }
reader.list.sensors.filter({ $0 is Fan }).forEach { (s: Sensor_p) in
if let f = s as? Fan, let mode = f.customMode {
if !mode.isAutomatic {
SMCHelper.shared.setFanMode(f.id, mode: FanMode.automatic.rawValue)
}
}
}
}
private func usageCallback(_ raw: Sensors_List?) {
guard let value = raw, self.enabled else { return }
self.popupView.usageCallback(value.sensors)
self.portalView.usageCallback(value.sensors)
self.notificationsView.usageCallback(value.sensors)
let activeWidgets = self.menuBar.widgets.filter{ $0.isActive }
self.sensorsReader?.sleepMode(state: activeWidgets.contains(where: {$0.item is Label}) && activeWidgets.count == 1)
activeWidgets.forEach { (w: SWidget) in
switch w.item {
case let widget as Mini:
if let active = value.sensors.first(where: { $0.key == self.selectedSensor }) {
var value: Double = active.localValue/100
var unit: String = active.miniUnit
if let fan = active as? Fan, self.fanValueState == .percentage {
value = Double(fan.percentage)/100
unit = "%"
}
if value > 999 {
unit = ""
}
widget.setValue(value)
widget.setSuffix(unit)
}
case let widget as StackWidget:
var list: [Stack_t] = []
value.sensors.forEach { (s: Sensor_p) in
if s.state {
var value = s.formattedMiniValue
if let f = s as? Fan {
if self.fanValueState == .percentage {
value = "\(f.percentage)%"
}
}
list.append(Stack_t(key: s.key, value: value))
}
}
widget.setValues(list)
case let widget as BarChart:
var flatList: [[ColorValue]] = []
value.sensors.filter{ $0 is Fan }.forEach { (s: Sensor_p) in
if s.state, let f = s as? Fan {
flatList.append([ColorValue(((f.value*100)/f.maxSpeed)/100)])
}
}
widget.setValue(flatList)
default: break
}
}
}
}
================================================
FILE: Modules/Sensors/notifications.swift
================================================
//
// notifications.swift
// Sensors
//
// Created by Serhiy Mytrovtsiy on 05/12/2023
// Using Swift 5.0
// Running on macOS 14.1
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
class Notifications: NotificationsWrapper {
private var unknownSensorsState: Bool {
Store.shared.bool(key: "Sensors_unknown", defaultValue: false)
}
private var temperatureLevels: [KeyValue_t] = [
KeyValue_t(key: "", value: "Disabled")
]
private let temperatureList: [String] = ["30", "35", "40", "45", "50", "55", "60", "65", "70", "75", "80", "85", "90", "96", "100", "105", "110"]
public init(_ module: ModuleType) {
super.init(module)
for p in self.temperatureList {
if let v = Double(p) {
self.temperatureLevels.append(KeyValue_t(key: p, value: temperature(v)))
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func setup(_ values: [Sensor_p]? = nil) {
guard var values = values else { return }
values = values.filter({ $0.type == .fan || $0.type == .temperature })
if !self.unknownSensorsState {
values = values.filter({ $0.group != .unknown })
}
self.subviews.forEach({ $0.removeFromSuperview() })
self.initIDs(values.map{$0.key})
var types: [SensorType] = []
values.forEach { (s: Sensor_p) in
if !types.contains(s.type) {
types.append(s.type)
}
}
types.forEach { (typ: SensorType) in
let filtered = values.filter{ $0.type == typ }
var groups: [SensorGroup] = []
filtered.forEach { (s: Sensor_p) in
if !groups.contains(s.group) {
groups.append(s.group)
}
}
let section = PreferencesSection(label: localizedString(typ.rawValue))
groups.forEach { (group: SensorGroup) in
filtered.filter{ $0.group == group }.forEach { (s: Sensor_p) in
let btn = selectView(
action: #selector(self.changeSensorNotificaion),
items: s.type == .temperature ? temperatureLevels : notificationLevels,
selected: s.notificationThreshold
)
btn.identifier = NSUserInterfaceItemIdentifier(rawValue: s.key)
section.add(PreferencesRow(localizedString(s.name), component: btn))
}
}
self.addArrangedSubview(section)
}
}
internal func usageCallback(_ values: [Sensor_p]) {
let sensors = values.filter({ !$0.notificationThreshold.isEmpty })
let title = localizedString("Sensor threshold")
for s in sensors {
if let threshold = Double(s.notificationThreshold) {
let subtitle = localizedString("\(localizedString(s.name)): \(s.formattedPopupValue)")
self.checkDouble(id: s.key, value: s.value, threshold: threshold, title: title, subtitle: subtitle)
}
}
}
@objc private func changeSensorNotificaion(_ sender: NSMenuItem) {
guard let id = sender.identifier, let key = sender.representedObject as? String else { return }
Store.shared.set(key: "sensor_\(id.rawValue)_notification", value: key)
}
}
================================================
FILE: Modules/Sensors/popup.swift
================================================
//
// popup.swift
// Sensors
//
// Created by Serhiy Mytrovtsiy on 22/06/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
private struct Sensor_t: KeyValue_p {
let key: String
let name: String?
var value: String
var additional: Any?
var index: Int {
get { Store.shared.int(key: "sensors_\(self.key)_index", defaultValue: -1) }
set { Store.shared.set(key: "sensors_\(self.key)_index", value: newValue) }
}
init(key: String, value: String, name: String? = nil) {
self.key = key
self.value = value
self.name = name
}
}
internal class Popup: PopupWrapper {
private var list: [String: NSView] = [:]
private var unknownSensorsState: Bool { Store.shared.bool(key: "Sensors_unknown", defaultValue: false) }
private var fanValueState: FanValue = .percentage
private var sensors: [Sensor_p] = []
private let settingsView: NSStackView = NSStackView()
private var fanControlState: Bool {
get { Store.shared.bool(key: "Sensors_fanControl", defaultValue: true) }
set { Store.shared.set(key: "Sensors_fanControl", value: newValue) }
}
public init() {
super.init(ModuleType.sensors, frame: NSRect( x: 0, y: 0, width: Constants.Popup.width, height: 0))
self.fanValueState = FanValue(rawValue: Store.shared.string(key: "Sensors_popup_fanValue", defaultValue: self.fanValueState.rawValue)) ?? .percentage
self.orientation = .vertical
self.spacing = 0
self.translatesAutoresizingMaskIntoConstraints = false
self.settingsView.orientation = .vertical
self.settingsView.spacing = Constants.Settings.margin
self.settingsView.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Keyboard shortcut"), component: KeyboardShartcutView(
callback: self.setKeyboardShortcut,
value: self.keyboardShortcut
))
]))
self.settingsView.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Fan value"), component: selectView(
action: #selector(self.toggleFanValue),
items: FanValues,
selected: self.fanValueState.rawValue
))
]))
#if arch(arm64)
NotificationCenter.default.addObserver(self, selector: #selector(self.checkFanModesAndResetFtst), name: .checkFanModes, object: nil)
#endif
}
deinit {
NotificationCenter.default.removeObserver(self)
}
#if arch(arm64)
@objc private func checkFanModesAndResetFtst() {
let fanViews = self.list.values.compactMap { $0 as? FanView }
guard !fanViews.isEmpty else { return }
guard fanViews.allSatisfy({ $0.fan.mode.isAutomatic }) else { return }
SMCHelper.shared.resetFanControl()
}
#endif
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func setup(_ values: [Sensor_p]? = nil, reload: Bool = false) {
guard let values = reload ? self.sensors : values else { return }
let fans = values.filter({ $0.type == .fan && $0.popupState })
var sensors = values
if !self.unknownSensorsState {
sensors = sensors.filter({ $0.group != .unknown })
}
self.subviews.forEach({ $0.removeFromSuperview() })
if !reload {
self.settingsView.subviews.filter({ $0.identifier == NSUserInterfaceItemIdentifier("sensor") }).forEach { v in
v.removeFromSuperview()
}
}
if !fans.isEmpty {
self.addArrangedSubview(self.fansSeparatorView())
let container = NSStackView()
container.orientation = .vertical
container.spacing = Constants.Popup.spacing
fans.forEach { (f: Sensor_p) in
if let fan = f as? Fan {
if f.isComputed {
let sensor = SensorView(fan, width: self.frame.width, toggleable: false) {}
self.list[fan.key] = sensor
container.addArrangedSubview(sensor)
} else {
let view = FanView(fan, width: self.frame.width) { [weak self] in
let h = container.arrangedSubviews.map({ $0.bounds.height + container.spacing }).reduce(0, +) - container.spacing
if container.frame.size.height != h && h >= 0 {
container.setFrameSize(NSSize(width: container.frame.width, height: h))
}
self?.recalculateHeight()
}
self.list[fan.key] = view
container.addArrangedSubview(view)
}
}
}
let h = container.arrangedSubviews.map({ $0.bounds.height + container.spacing }).reduce(0, +) - container.spacing
if container.frame.size.height != h {
container.setFrameSize(NSSize(width: container.frame.width, height: h))
}
self.addArrangedSubview(container)
}
var types: [SensorType] = []
sensors.forEach { (s: Sensor_p) in
if !types.contains(s.type) {
types.append(s.type)
}
}
types.forEach { (typ: SensorType) in
var filtered = sensors.filter{ $0.type == typ }
var groups: [SensorGroup] = []
filtered.forEach { (s: Sensor_p) in
if !groups.contains(s.group) {
groups.append(s.group)
}
}
if !reload {
let section = PreferencesSection(label: localizedString(typ.rawValue))
section.identifier = NSUserInterfaceItemIdentifier("sensor")
groups.forEach { (group: SensorGroup) in
filtered.filter{ $0.group == group }.forEach { (s: Sensor_p) in
let btn = switchView(
action: #selector(self.toggleSensor),
state: s.popupState
)
btn.identifier = NSUserInterfaceItemIdentifier(rawValue: s.key)
section.add(PreferencesRow(localizedString(s.name), component: btn))
}
}
self.settingsView.addArrangedSubview(section)
}
if typ == .fan { return }
filtered = filtered.filter{ $0.popupState }
if filtered.isEmpty { return }
self.addArrangedSubview(separatorView(localizedString(typ.rawValue), width: self.frame.width))
groups.forEach { (group: SensorGroup) in
filtered.filter{ $0.group == group }.forEach { (s: Sensor_p) in
let sensor = SensorView(s, width: self.frame.width) { [weak self] in
self?.recalculateHeight()
}
self.addArrangedSubview(sensor)
self.list[s.key] = sensor
}
}
}
if !reload {
self.sensors = values
}
self.recalculateHeight()
}
internal func usageCallback(_ values: [Sensor_p]) {
DispatchQueue.main.async(execute: {
values.filter({ $0 is Sensor }).forEach { (s: Sensor_p) in
if let sensor = self.list[s.key] as? SensorView {
sensor.addHistoryPoint(s)
}
}
if self.window?.isVisible ?? false {
values.forEach { (s: Sensor_p) in
switch self.list[s.key] {
case let fan as FanView:
if let f = s as? Fan {
fan.update(f)
}
case let sensor as SensorView:
sensor.update(s)
case .none, .some:
break
}
}
}
})
}
private func recalculateHeight() {
let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - self.spacing
if self.frame.size.height != h {
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.sizeCallback?(self.frame.size)
}
}
// MARK: - Settings
public override func settings() -> NSView? {
self.settingsView
}
@objc private func toggleFanValue(_ sender: NSMenuItem) {
if let key = sender.representedObject as? String, let value = FanValue(rawValue: key) {
self.fanValueState = value
Store.shared.set(key: "Sensors_popup_fanValue", value: self.fanValueState.rawValue)
}
}
// MARK: helpers
private func fansSeparatorView() -> NSView {
let view: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 26))
view.widthAnchor.constraint(equalToConstant: view.frame.width).isActive = true
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
view.orientation = .horizontal
view.spacing = 0
view.distribution = .fillEqually
view.alignment = .top
let labelView: NSTextField = TextView()
labelView.stringValue = localizedString("Fans")
labelView.alignment = .center
labelView.textColor = .secondaryLabelColor
labelView.font = NSFont.systemFont(ofSize: 12, weight: .medium)
let btnContainer = NSView()
let button = NSButton()
button.frame = CGRect(x: (self.frame.width/3)-20, y: 10, width: 15, height: 15)
button.bezelStyle = .regularSquare
button.isBordered = false
button.imageScaling = NSImageScaling.scaleAxesIndependently
button.contentTintColor = .lightGray
button.action = #selector(self.toggleFanControl)
button.target = self
button.toolTip = localizedString("Control")
button.image = Bundle(for: Module.self).image(forResource: "tune")!
btnContainer.addSubview(button)
view.addArrangedSubview(NSView())
view.addArrangedSubview(labelView)
view.addArrangedSubview(btnContainer)
return view
}
@objc private func toggleSensor(_ sender: NSControl) {
guard let id = sender.identifier else { return }
Store.shared.set(key: "sensor_\(id.rawValue)_popup", value: controlState(sender))
self.setup(reload: true)
}
@objc private func toggleFanControl() {
self.fanControlState = !self.fanControlState
NotificationCenter.default.post(name: .toggleFanControl, object: nil, userInfo: ["state": self.fanControlState])
}
}
// MARK: - Sensor view
internal class SensorView: NSStackView {
public var sizeCallback: (() -> Void)
private var valueView: ValueSensorView!
private var chartView: ChartSensorView!
private var openned: Bool = false
private var fanValueState: FanValue {
FanValue(rawValue: Store.shared.string(key: "Sensors_popup_fanValue", defaultValue: FanValue.percentage.rawValue)) ?? .percentage
}
public init(_ sensor: Sensor_p, width: CGFloat, toggleable: Bool = true, callback: @escaping (() -> Void)) {
self.sizeCallback = callback
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 22))
self.orientation = .vertical
self.distribution = .fillProportionally
self.spacing = 0
self.valueView = ValueSensorView(sensor, width: width, toggleable: toggleable, callback: { [weak self] in
self?.open()
})
self.chartView = ChartSensorView(width: width, suffix: sensor.unit)
self.addArrangedSubview(self.valueView)
NSLayoutConstraint.activate([
self.widthAnchor.constraint(equalToConstant: self.bounds.width)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(_ sensor: Sensor_p) {
var value = sensor.formattedPopupValue
if let fan = sensor as? Fan {
value = self.fanValueState == .percentage ? "\(fan.percentage)%" : fan.formattedValue
}
self.valueView.update(value)
}
public func addHistoryPoint(_ sensor: Sensor_p) {
self.chartView.update(sensor.localValue, sensor.unit)
}
private func open() {
if self.openned {
self.chartView.removeFromSuperview()
} else {
self.addArrangedSubview(self.chartView)
}
self.openned = !self.openned
let h = self.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +)
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.sizeCallback()
}
}
internal class ValueSensorView: NSStackView {
public var callback: (() -> Void)
private var labelView: LabelField = {
let view = LabelField(frame: NSRect.zero)
view.cell?.truncatesLastVisibleLine = true
return view
}()
private var valueView: ValueField = ValueField(frame: NSRect.zero)
private let isToggleable: Bool
public init(_ sensor: Sensor_p, width: CGFloat, toggleable: Bool = true, callback: @escaping (() -> Void)) {
self.callback = callback
self.isToggleable = toggleable
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 22))
self.wantsLayer = true
self.orientation = .horizontal
self.distribution = .fillProportionally
self.spacing = 0
self.layer?.cornerRadius = 3
self.labelView.stringValue = sensor.name
self.labelView.toolTip = sensor.key
self.valueView.stringValue = sensor.formattedValue
self.addArrangedSubview(self.labelView)
self.addArrangedSubview(self.valueView)
if self.isToggleable {
self.addTrackingArea(NSTrackingArea(
rect: NSRect(x: 0, y: 0, width: self.frame.width, height: 22),
options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp],
owner: self,
userInfo: nil
))
}
NSLayoutConstraint.activate([
self.labelView.heightAnchor.constraint(equalToConstant: 16),
self.widthAnchor.constraint(equalToConstant: self.bounds.width),
self.heightAnchor.constraint(equalToConstant: self.bounds.height)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(_ value: String) {
self.valueView.stringValue = value
}
override func mouseDown(with theEvent: NSEvent) {
guard self.isToggleable else { return }
self.callback()
}
public override func mouseEntered(with: NSEvent) {
guard self.isToggleable else { return }
self.layer?.backgroundColor = .init(gray: 0.01, alpha: 0.05)
}
public override func mouseExited(with: NSEvent) {
guard self.isToggleable else { return }
self.layer?.backgroundColor = .none
}
}
internal class ChartSensorView: NSStackView {
private var chart: LineChartView? = nil
public init(width: CGFloat, suffix: String) {
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 60))
self.wantsLayer = true
self.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.1).cgColor
self.orientation = .horizontal
self.distribution = .fillProportionally
self.spacing = 0
self.layer?.cornerRadius = 3
self.chart = LineChartView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height), num: 120, scale: .linear)
self.chart?.suffix = suffix
if let view = self.chart {
self.addArrangedSubview(view)
}
NSLayoutConstraint.activate([
self.widthAnchor.constraint(equalToConstant: self.bounds.width),
self.heightAnchor.constraint(equalToConstant: self.bounds.height)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(_ value: Double, _ suffix: String) {
if self.chart?.suffix != suffix {
self.chart?.suffix = suffix
}
self.chart?.addValue(value/100)
}
}
// MARK: - Fan view
internal class FanView: NSStackView {
public var sizeCallback: (() -> Void)
internal var fan: Fan
private var ready: Bool = false
private var helperView: NSView? = nil
private var controlView: NSView? = nil
private var buttonsView: NSView? = nil
private var valueField: NSTextField? = nil
private var sliderValueField: NSTextField? = nil
private var slider: NSSlider? = nil
private var modeButtons: ModeButtons? = nil
private var debouncer: DispatchWorkItem? = nil
private var barView: BarChartView? = nil
private var minBtn: NSButton? = nil
private var maxBtn: NSButton? = nil
private var speedState: Bool {
Store.shared.bool(key: "Sensors_speed", defaultValue: false)
}
private var syncState: Bool {
Store.shared.bool(key: "Sensors_fansSync", defaultValue: false)
}
private var speed: Double {
get {
if let v = self.fan.customSpeed, self.speedState {
return Double(v)
}
return self.fan.value
}
}
private var resetModeAfterSleep: Bool = false
private var controlState: Bool
private var fanValue: FanValue {
FanValue(rawValue: Store.shared.string(key: "Sensors_popup_fanValue", defaultValue: FanValue.percentage.rawValue)) ?? .percentage
}
private var horizontalMargin: CGFloat {
self.edgeInsets.top + self.edgeInsets.bottom + (self.spacing*CGFloat(self.arrangedSubviews.count))
}
private var willSleepMode: FanMode? = nil // fan mode before sleep
private var willSleepSpeed: Int? = nil // fan speed before sleep
public init(_ fan: Fan, width: CGFloat, callback: @escaping (() -> Void)) {
self.fan = fan
self.sizeCallback = callback
self.controlState = Store.shared.bool(key: "Sensors_fanControl", defaultValue: true)
let inset: CGFloat = 5
super.init(frame: NSRect(x: 0, y: 0, width: width - (inset*2), height: 0))
self.helperView = self.noHelper()
self.controlView = self.control()
self.buttonsView = self.mode()
self.orientation = .vertical
self.alignment = .centerX
self.distribution = .fillProportionally
self.spacing = 1
self.edgeInsets = NSEdgeInsets(top: inset, left: inset, bottom: inset, right: inset)
self.wantsLayer = true
self.layer?.cornerRadius = 2
self.nameAndSpeed()
self.setupControls()
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(self.wakeListener), name: NSWorkspace.didWakeNotification, object: nil)
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(self.sleepListener), name: NSWorkspace.willSleepNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.syncFanSpeed), name: .syncFansControl, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.changeHelperState), name: .fanHelperState, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.controlCallback), name: .toggleFanControl, object: nil)
if let fanMode = self.fan.customMode, self.speedState && fanMode != FanMode.automatic {
SMCHelper.shared.setFanMode(fan.id, mode: fanMode.rawValue)
self.modeButtons?.setMode(FanMode(rawValue: fanMode.rawValue) ?? .automatic)
self.setSpeed(value: Int(self.speed), then: {
DispatchQueue.main.async {
self.sliderValueField?.textColor = .systemBlue
}
})
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
NSWorkspace.shared.notificationCenter.removeObserver(self)
NotificationCenter.default.removeObserver(self, name: .syncFansControl, object: nil)
NotificationCenter.default.removeObserver(self, name: .fanHelperState, object: nil)
NotificationCenter.default.removeObserver(self, name: .toggleSettings, object: nil)
}
override func updateLayer() {
self.layer?.backgroundColor = (isDarkMode ? NSColor(red: 17/255, green: 17/255, blue: 17/255, alpha: 0.25) : NSColor(red: 245/255, green: 245/255, blue: 245/255, alpha: 1)).cgColor
}
private func nameAndSpeed() {
let row: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 16))
row.widthAnchor.constraint(equalToConstant: self.frame.width).isActive = true
row.heightAnchor.constraint(equalToConstant: row.bounds.height).isActive = true
row.orientation = .horizontal
row.distribution = .fillEqually
row.spacing = 0
let nameField: NSTextField = TextView()
nameField.stringValue = self.fan.name
nameField.toolTip = self.fan.key
nameField.cell?.truncatesLastVisibleLine = true
let value = self.fan.value
let valueField: NSTextField = TextView()
valueField.font = NSFont.systemFont(ofSize: 13, weight: .regular)
valueField.alignment = .right
valueField.stringValue = self.fanValue == .percentage ? "\(self.fan.percentage)%" : self.fan.formattedValue
valueField.toolTip = "\(value)"
let bar = BarChartView(frame: NSRect(x: 0, y: 0, width: 80, height: 8), horizontal: true)
bar.widthAnchor.constraint(equalToConstant: 80).isActive = true
bar.heightAnchor.constraint(equalToConstant: 8).isActive = true
let percentage = self.fan.percentage < 0 ? 0 : self.fan.percentage
bar.setValue(ColorValue(Double(percentage) / 100))
row.addArrangedSubview(nameField)
row.addArrangedSubview(bar)
row.addArrangedSubview(valueField)
self.valueField = valueField
self.barView = bar
self.addArrangedSubview(row)
}
private func noHelper() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 30))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let container = NSStackView(frame: NSRect(x: 0, y: 4, width: view.frame.width, height: view.frame.height - 8))
container.wantsLayer = true
container.layer?.cornerRadius = 3
container.layer?.borderWidth = 1
container.layer?.borderColor = NSColor.lightGray.cgColor
container.orientation = .horizontal
container.alignment = .centerY
container.distribution = .fillProportionally
container.spacing = 0
let button: NSButton = NSButton(title: localizedString("Install fan helper"), target: nil, action: #selector(self.installHelper))
button.isBordered = false
button.target = self
container.addArrangedSubview(button)
view.addSubview(container)
return view
}
private func mode() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 30))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let buttons = ModeButtons(frame: NSRect(
x: 0,
y: 4,
width: view.frame.width,
height: view.frame.height - 8
), mode: self.fan.mode)
buttons.callback = { [weak self] (mode: FanMode) in
if let fan = self?.fan, mode == .automatic || fan.mode != mode {
self?.fan.mode = mode
self?.fan.customMode = mode
SMCHelper.shared.setFanMode(fan.id, mode: mode.rawValue)
}
self?.toggleControlView(mode == .forced)
}
buttons.off = { [weak self] in
if let fan = self?.fan {
if self?.fan.mode != .forced {
self?.fan.mode = .forced
SMCHelper.shared.setFanMode(fan.id, mode: FanMode.forced.rawValue)
}
SMCHelper.shared.setFanSpeed(fan.id, speed: 0)
self?.fan.customSpeed = 0
}
self?.toggleControlView(false)
}
buttons.turbo = { [weak self] in
if let fan = self?.fan {
if self?.fan.mode != .forced {
self?.fan.mode = .forced
SMCHelper.shared.setFanMode(fan.id, mode: FanMode.forced.rawValue)
}
SMCHelper.shared.setFanSpeed(fan.id, speed: Int(fan.maxSpeed))
self?.fan.customSpeed = Int(fan.maxSpeed)
}
self?.toggleControlView(false)
}
view.addSubview(buttons)
self.modeButtons = buttons
return view
}
private func control() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 46))
view.identifier = NSUserInterfaceItemIdentifier(rawValue: "control")
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let controls: NSStackView = NSStackView(frame: NSRect(x: 0, y: 14, width: view.frame.width, height: 30))
controls.orientation = .horizontal
controls.spacing = 0
let slider: NSSlider = NSSlider(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 26))
slider.minValue = self.fan.minSpeed
slider.maxValue = self.fan.maxSpeed
slider.doubleValue = self.speed
slider.isContinuous = true
slider.action = #selector(self.sliderCallback)
slider.target = self
let levels: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 16))
let minBtn: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: 50, height: levels.frame.height))
minBtn.title = "\(Int(self.fan.minSpeed))"
minBtn.toolTip = localizedString("Min")
minBtn.setButtonType(.toggle)
minBtn.isBordered = false
minBtn.target = self
minBtn.state = .off
minBtn.action = #selector(self.setMin)
minBtn.wantsLayer = true
minBtn.layer?.cornerRadius = 3
minBtn.layer?.borderWidth = 1
minBtn.layer?.borderColor = NSColor.lightGray.cgColor
let valueField: NSTextField = TextView(frame: NSRect(x: 80, y: 0, width: levels.frame.width - 160, height: levels.frame.height))
valueField.font = NSFont.systemFont(ofSize: 11, weight: .light)
valueField.textColor = .secondaryLabelColor
valueField.alignment = .center
let maxBtn: NSButton = NSButton(frame: NSRect(x: levels.frame.width - 50, y: 0, width: 50, height: levels.frame.height))
maxBtn.title = "\(Int(self.fan.maxSpeed))"
maxBtn.toolTip = localizedString("Max")
maxBtn.setButtonType(.toggle)
maxBtn.isBordered = false
maxBtn.target = self
maxBtn.state = .off
maxBtn.wantsLayer = true
maxBtn.action = #selector(self.setMax)
maxBtn.layer?.cornerRadius = 3
maxBtn.layer?.borderWidth = 1
maxBtn.layer?.borderColor = NSColor.lightGray.cgColor
controls.addArrangedSubview(slider)
levels.addSubview(minBtn)
levels.addSubview(valueField)
levels.addSubview(maxBtn)
view.addSubview(controls)
view.addSubview(levels)
self.slider = slider
self.sliderValueField = valueField
self.minBtn = minBtn
self.maxBtn = maxBtn
return view
}
private func toggleControlView(_ state: Bool) {
guard let view = self.controlView else {
return
}
if state {
self.slider?.doubleValue = self.speed
if self.speedState {
self.setSpeed(value: Int(self.speed), then: {
DispatchQueue.main.async {
self.sliderValueField?.textColor = .systemBlue
}
})
}
self.addArrangedSubview(view)
} else {
self.sliderValueField?.stringValue = ""
self.sliderValueField?.textColor = .secondaryLabelColor
self.minBtn?.state = .off
self.maxBtn?.state = .off
view.removeFromSuperview()
}
let h = self.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +)
self.setFrameSize(NSSize(width: self.frame.width, height: h + self.horizontalMargin))
self.sizeCallback()
}
private func setSpeed(value: Int, then: @escaping () -> Void = {}) {
self.sliderValueField?.stringValue = "\(value) RPM"
self.sliderValueField?.textColor = .secondaryLabelColor
self.fan.customSpeed = value
self.debouncer?.cancel()
let task = DispatchWorkItem { [weak self] in
DispatchQueue.global(qos: .userInteractive).async { [weak self] in
if let id = self?.fan.id {
SMCHelper.shared.setFanSpeed(id, speed: value)
}
then()
}
}
self.debouncer = task
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3, execute: task)
}
@objc private func sliderCallback(_ sender: NSSlider) {
var value = sender.doubleValue
if value > self.fan.maxSpeed {
value = self.fan.maxSpeed
} else if value < self.fan.minSpeed {
value = self.fan.minSpeed
}
self.minBtn?.state = .off
self.maxBtn?.state = .off
self.setSpeed(value: Int(value), then: {
DispatchQueue.main.async {
self.slider?.intValue = Int32(value)
self.sliderValueField?.textColor = .systemBlue
}
})
if sender.tag != 4 {
if self.fan.minSpeed != 0 && self.fan.maxSpeed != 0 && self.fan.maxSpeed != self.fan.minSpeed {
let percentage = Int((100*(value-self.fan.minSpeed))/(self.fan.maxSpeed - self.fan.minSpeed))
NotificationCenter.default.post(name: .syncFansControl, object: nil, userInfo: ["percentage": percentage])
} else {
NotificationCenter.default.post(name: .syncFansControl, object: nil, userInfo: ["speed": Int(value)])
}
}
}
@objc func setMin(_ sender: NSButton) {
self.slider?.doubleValue = self.fan.minSpeed
self.maxBtn?.state = .off
self.setSpeed(value: Int(self.fan.minSpeed))
NotificationCenter.default.post(name: .syncFansControl, object: nil, userInfo: ["speed": Int(self.fan.minSpeed)])
}
@objc func setMax(_ sender: NSButton) {
self.slider?.doubleValue = self.fan.maxSpeed
self.minBtn?.state = .off
self.setSpeed(value: Int(self.fan.maxSpeed))
NotificationCenter.default.post(name: .syncFansControl, object: nil, userInfo: ["speed": Int(self.fan.maxSpeed)])
}
@objc private func wakeListener(aNotification: NSNotification) {
self.resetModeAfterSleep = true
if self.speedState {
if let mode = self.willSleepMode, let speed = self.willSleepSpeed {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
SMCHelper.shared.setFanMode(self.fan.id, mode: mode.rawValue)
self.modeButtons?.setMode(mode)
if !mode.isAutomatic {
self.setSpeed(value: speed, then: {
DispatchQueue.main.async {
self.sliderValueField?.textColor = .systemBlue
}
})
}
}
}
self.willSleepMode = nil
self.willSleepSpeed = nil
}
if let value = self.fan.customSpeed, !self.fan.mode.isAutomatic {
self.setSpeed(value: value, then: {
DispatchQueue.main.async {
self.sliderValueField?.textColor = .systemBlue
}
})
}
}
@objc private func sleepListener(aNotification: NSNotification) {
guard SMCHelper.shared.isActive(), let mode = self.fan.customMode, !mode.isAutomatic else { return }
self.willSleepMode = mode
self.willSleepSpeed = self.fan.customSpeed
SMCHelper.shared.setFanMode(fan.id, mode: FanMode.automatic.rawValue)
self.modeButtons?.setMode(.automatic)
}
@objc private func syncFanSpeed(_ notification: Notification) {
guard self.syncState else { return }
var speed = notification.userInfo?["speed"] as? Int
if let percentage = notification.userInfo?["percentage"] as? Int {
speed = ((Int(self.fan.maxSpeed - self.fan.minSpeed)*percentage)/100) + Int(self.fan.minSpeed)
}
guard let speed, self.fan.customSpeed != speed else { return }
let slider = NSSlider()
slider.tag = 4
slider.maxValue = 30000
slider.intValue = Int32(speed)
self.sliderCallback(slider)
}
public func update(_ value: Fan) {
DispatchQueue.main.async(execute: {
if (self.window?.isVisible ?? false) || !self.ready {
self.fan.value = value.value
var newValue = ""
if value.value != 1 {
if self.fan.maxSpeed == 1 || self.fan.maxSpeed == 0 {
newValue = "\(Int(value.value)) RPM"
} else {
newValue = self.fanValue == .percentage ? "\(value.percentage)%" : value.formattedValue
}
}
self.valueField?.stringValue = newValue
self.valueField?.toolTip = value.formattedValue
if let v = self.barView {
let percentage = value.percentage < 0 ? 0 : value.percentage
v.setValue(ColorValue(Double(percentage) / 100))
}
if self.resetModeAfterSleep && !value.mode.isAutomatic {
if self.sliderValueField?.stringValue != "" && self.slider?.doubleValue != value.value {
self.slider?.doubleValue = value.value
self.sliderValueField?.stringValue = ""
}
self.modeButtons?.setMode(.forced)
self.resetModeAfterSleep = false
}
self.ready = true
}
})
}
@objc private func installHelper(_ sender: NSButton) {
SMCHelper.shared.install { status in
NotificationCenter.default.post(name: .fanHelperState, object: nil, userInfo: ["state": status])
}
}
private func setupControls(_ isInstalled: Bool? = nil) {
let helperState = isInstalled ?? SMCHelper.shared.isInstalled
if !self.controlState {
self.helperView?.removeFromSuperview()
self.controlView?.removeFromSuperview()
self.buttonsView?.removeFromSuperview()
} else {
if helperState {
self.helperView?.removeFromSuperview()
if self.fan.maxSpeed != self.fan.minSpeed, let v = self.buttonsView {
self.addArrangedSubview(v)
}
if self.fan.mode == .forced, let v = self.controlView {
self.addArrangedSubview(v)
}
} else {
self.buttonsView?.removeFromSuperview()
self.controlView?.removeFromSuperview()
if let v = self.helperView {
self.addArrangedSubview(v)
}
}
}
let h = self.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +)
self.setFrameSize(NSSize(width: self.frame.width, height: h + self.horizontalMargin))
self.sizeCallback()
}
@objc private func changeHelperState(_ notification: Notification) {
guard let state = notification.userInfo?["state"] as? Bool else { return }
self.setupControls(state)
}
@objc private func controlCallback(_ notification: Notification) {
guard let state = notification.userInfo?["state"] as? Bool else { return }
self.controlState = state
self.setupControls()
}
}
private class ModeButtons: NSStackView {
public var callback: (FanMode) -> Void = {_ in }
public var turbo: () -> Void = {}
public var off: () -> Void = {}
private var fansSyncState: Bool {
Store.shared.bool(key: "Sensors_fansSync", defaultValue: false)
}
private var offBtn: NSButton
private var autoBtn: NSButton = NSButton(title: localizedString("Automatic"), target: nil, action: #selector(autoMode))
private var manualBtn: NSButton = NSButton(title: localizedString("Manual"), target: nil, action: #selector(manualMode))
private var turboBtn: NSButton
public init(frame: NSRect, mode: FanMode) {
var turboIcon: NSImage = NSImage(named: NSImage.Name("ac_unit"))!
var offIcon: NSImage = NSImage(named: NSImage.Name("ac_unit"))!
if #available(macOS 12.0, *) {
turboIcon = iconFromSymbol(name: "snowflake", scale: .large)
offIcon = iconFromSymbol(name: "fanblades.slash", scale: .medium)
}
self.offBtn = NSButton(image: offIcon, target: nil, action: #selector(offMode))
self.turboBtn = NSButton(image: turboIcon, target: nil, action: #selector(turboMode))
super.init(frame: frame)
self.orientation = .horizontal
self.alignment = .centerY
self.distribution = .fillProportionally
self.spacing = 0
self.wantsLayer = true
self.layer?.cornerRadius = 3
self.layer?.borderWidth = 1
self.layer?.borderColor = NSColor.lightGray.cgColor
let modes: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height))
modes.orientation = .horizontal
modes.alignment = .centerY
modes.distribution = .fillEqually
self.autoBtn.setButtonType(.toggle)
self.autoBtn.isBordered = false
self.autoBtn.target = self
self.autoBtn.state = mode.isAutomatic ? .on : .off
self.manualBtn.setButtonType(.toggle)
self.manualBtn.isBordered = false
self.manualBtn.target = self
self.manualBtn.state = mode == .forced ? .on : .off
modes.addArrangedSubview(self.autoBtn)
modes.addArrangedSubview(self.manualBtn)
self.offBtn.setButtonType(.toggle)
self.offBtn.isBordered = false
self.offBtn.target = self
self.turboBtn.setButtonType(.toggle)
self.turboBtn.isBordered = false
self.turboBtn.target = self
NSLayoutConstraint.activate([
self.offBtn.widthAnchor.constraint(equalToConstant: 26),
self.offBtn.heightAnchor.constraint(equalToConstant: self.frame.height),
self.turboBtn.widthAnchor.constraint(equalToConstant: 26),
self.turboBtn.heightAnchor.constraint(equalToConstant: self.frame.height),
modes.heightAnchor.constraint(equalToConstant: self.frame.height)
])
self.addArrangedSubview(modes)
self.addArrangedSubview(self.offBtn)
self.addArrangedSubview(self.turboBtn)
NotificationCenter.default.addObserver(self, selector: #selector(syncFanMode), name: .syncFansControl, object: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc private func autoMode(_ sender: NSButton) {
if sender.state.rawValue == 0 {
self.autoBtn.state = .on
return
}
self.manualBtn.state = .off
self.offBtn.state = .off
self.turboBtn.state = .off
self.callback(.automatic)
NotificationCenter.default.post(name: .syncFansControl, object: nil, userInfo: ["mode": "automatic"])
NotificationCenter.default.post(name: .checkFanModes, object: nil)
}
@objc private func manualMode(_ sender: NSButton) {
if sender.state.rawValue == 0 {
self.manualBtn.state = .on
return
}
self.autoBtn.state = .off
self.offBtn.state = .off
self.turboBtn.state = .off
self.callback(.forced)
NotificationCenter.default.post(name: .syncFansControl, object: nil, userInfo: ["mode": "forced"])
}
@objc private func offMode(_ sender: NSButton) {
if sender.state.rawValue == 0 {
self.offBtn.state = .on
return
}
if !Store.shared.bool(key: "Sensors_turnOffFanAlert", defaultValue: false) {
let alert = NSAlert()
alert.messageText = localizedString("Turn off fan")
alert.informativeText = localizedString("You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?")
alert.showsSuppressionButton = true
alert.addButton(withTitle: localizedString("Turn off"))
alert.addButton(withTitle: localizedString("Cancel"))
if alert.runModal() == .alertFirstButtonReturn {
if let suppressionButton = alert.suppressionButton, suppressionButton.state == .on {
Store.shared.set(key: "Sensors_turnOffFanAlert", value: true)
}
self.toggleOffMode(sender)
} else {
self.offBtn.state = .off
}
} else {
self.toggleOffMode(sender)
}
}
private func toggleOffMode(_ sender: NSButton) {
self.manualBtn.state = .off
self.autoBtn.state = .off
self.offBtn.state = .on
self.turboBtn.state = .off
self.off()
if sender.tag != 4 {
NotificationCenter.default.post(name: .syncFansControl, object: nil, userInfo: ["mode": "off"])
}
}
@objc private func turboMode(_ sender: NSButton) {
if sender.state.rawValue == 0 {
self.turboBtn.state = .on
return
}
self.manualBtn.state = .off
self.autoBtn.state = .off
self.offBtn.state = .off
self.turboBtn.state = .on
self.turbo()
if sender.tag != 4 {
NotificationCenter.default.post(name: .syncFansControl, object: nil, userInfo: ["mode": "turbo"])
}
}
@objc private func syncFanMode(_ notification: Notification) {
guard let mode = notification.userInfo?["mode"] as? String, self.fansSyncState else {
return
}
if mode == "automatic" {
self.setMode(.automatic)
} else if mode == "forced" {
self.setMode(.forced)
} else if mode == "off" {
let btn = NSButton()
btn.state = .on
btn.tag = 4
self.offMode(btn)
} else if mode == "turbo" {
let btn = NSButton()
btn.state = .on
btn.tag = 4
self.turboMode(btn)
}
}
public func setMode(_ mode: FanMode) {
if mode.isAutomatic {
self.autoBtn.state = .on
self.manualBtn.state = .off
self.offBtn.state = .off
self.turboBtn.state = .off
self.callback(.automatic)
} else if mode == .forced {
self.manualBtn.state = .on
self.autoBtn.state = .off
self.offBtn.state = .off
self.turboBtn.state = .off
self.callback(.forced)
}
}
}
================================================
FILE: Modules/Sensors/portal.swift
================================================
//
// portal.swift
// Sensors
//
// Created by Serhiy Mytrovtsiy on 14/01/2024
// Using Swift 5.0
// Running on macOS 14.3
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//
import AppKit
import Kit
public class Portal: NSStackView, Portal_p {
public var name: String
private var initialized: Bool = false
private var container: ScrollableStackView = ScrollableStackView()
private var list: [String: NSView] = [:]
private var unknownSensorsState: Bool {
Store.shared.bool(key: "Sensors_unknown", defaultValue: false)
}
init(_ name: ModuleType) {
self.name = name.stringValue
super.init(frame: NSRect( x: 0, y: 0, width: Constants.Popup.width, height: Constants.Popup.portalHeight))
self.wantsLayer = true
self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
self.layer?.cornerRadius = 3
self.orientation = .vertical
self.distribution = .fillEqually
self.widthAnchor.constraint(equalToConstant: Constants.Popup.width).isActive = true
self.spacing = Constants.Popup.spacing
self.edgeInsets = NSEdgeInsets(
top: Constants.Popup.spacing*2,
left: Constants.Popup.spacing*2,
bottom: Constants.Popup.spacing*2,
right: Constants.Popup.spacing
)
self.container.stackView.spacing = 0
self.addArrangedSubview(PortalHeader(self.name))
self.addArrangedSubview(self.container)
self.heightAnchor.constraint(equalToConstant: Constants.Popup.portalHeight).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func updateLayer() {
self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
}
public func setup(_ values: [Sensor_p]? = nil) {
guard var list = values else { return }
list = list.filter{ $0.popupState }
if !self.unknownSensorsState {
list = list.filter({ $0.group != .unknown })
}
if !self.list.isEmpty {
self.container.stackView.subviews.forEach({ $0.removeFromSuperview() })
self.list = [:]
}
var width: CGFloat = self.frame.width - self.edgeInsets.left - self.edgeInsets.right
if list.count >= 4 {
width -= self.container.scrollWidth ?? Constants.Popup.margins
}
list.forEach { s in
let v = ValueSensorView(s, width: width, callback: {})
self.container.stackView.addArrangedSubview(v)
self.list[s.key] = v
}
}
public func usageCallback(_ values: [Sensor_p]) {
DispatchQueue.main.async(execute: {
if self.window?.isVisible ?? false {
values.forEach { (s: Sensor_p) in
if let v = self.list[s.key] as? ValueSensorView {
v.update(s.formattedPopupValue)
}
}
}
})
}
}
================================================
FILE: Modules/Sensors/reader.m
================================================
//
// reader.m
// Sensors
//
// Created by Serhiy Mytrovtsiy on 06/05/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
#import
#import "bridge.h"
NSDictionary*AppleSiliconSensors(int32_t page, int32_t usage, int32_t type) {
NSDictionary* dictionary = @{@"PrimaryUsagePage":@(page),@"PrimaryUsage":@(usage)};
IOHIDEventSystemClientRef system = IOHIDEventSystemClientCreate(kCFAllocatorDefault);
IOHIDEventSystemClientSetMatching(system, (__bridge CFDictionaryRef)dictionary);
CFArrayRef services = IOHIDEventSystemClientCopyServices(system);
if (services == nil) {
return nil;
}
NSMutableDictionary*dict = [NSMutableDictionary dictionary];
for (int i = 0; i < CFArrayGetCount(services); i++) {
IOHIDServiceClientRef service = (IOHIDServiceClientRef)CFArrayGetValueAtIndex(services, i);
NSString* name = CFBridgingRelease(IOHIDServiceClientCopyProperty(service, CFSTR("Product")));
IOHIDEventRef event = IOHIDServiceClientCopyEvent(service, type, 0, 0);
if (event == nil) {
continue;
}
if (name && event) {
double value = IOHIDEventGetFloatValue(event, IOHIDEventFieldBase(type));
dict[name]=@(value);
}
CFRelease(event);
}
CFRelease(services);
CFRelease(system);
return dict;
}
================================================
FILE: Modules/Sensors/readers.swift
================================================
//
// readers.swift
// Sensors
//
// Created by Serhiy Mytrovtsiy on 17/06/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class SensorsReader: Reader {
static let HIDtypes: [SensorType] = [.temperature, .voltage]
internal var list: Sensors_List = Sensors_List()
private var lastRead: Date = Date()
private let firstRead: Date = Date()
private var lastIOSensorsRead: Date? = nil
private var HIDState: Bool {
Store.shared.bool(key: "Sensors_hid", defaultValue: false)
}
private var unknownSensorsState: Bool
private var channels: CFMutableDictionary? = nil
private var subscription: IOReportSubscriptionRef? = nil
private var powers: (CPU: Double, GPU: Double, ANE: Double, RAM: Double, PCI: Double) = (0.0, 0.0, 0.0, 0.0, 0.0)
init(callback: @escaping (T?) -> Void = {_ in }) {
self.unknownSensorsState = Store.shared.bool(key: "Sensors_unknown", defaultValue: false)
super.init(.sensors, callback: callback)
self.channels = self.getChannels()
var dict: Unmanaged?
self.subscription = IOReportCreateSubscription(nil, self.channels, &dict, 0, nil)
dict?.release()
self.list.sensors = self.sensors()
}
private func sensors() -> [Sensor_p] {
var available: [String] = SMC.shared.getAllKeys()
var list: [Sensor_p] = []
var sensorsList = SensorsList
if let platform = SystemKit.shared.device.platform {
sensorsList = sensorsList.filter({ $0.platforms.contains(platform) })
}
if let count = SMC.shared.getValue("FNum") {
list += self.loadFans(Int(count))
}
available = available.filter({ (key: String) -> Bool in
switch key.prefix(1) {
case "T", "V", "P", "I": return true
default: return false
}
})
sensorsList.forEach { (s: Sensor) in
if let idx = available.firstIndex(where: { $0 == s.key }) {
list.append(s)
available.remove(at: idx)
}
}
sensorsList.filter{ $0.key.contains("%") }.forEach { (s: Sensor) in
var index = 1
for i in 0..<10 {
let key = s.key.replacingOccurrences(of: "%", with: "\(i)")
if let idx = available.firstIndex(where: { $0 == key }) {
var sensor = s.copy()
sensor.key = key
sensor.name = s.name.replacingOccurrences(of: "%", with: "\(index)")
list.append(sensor)
available.remove(at: idx)
index += 1
}
}
}
available.forEach { (key: String) in
var type: SensorType? = nil
switch key.prefix(1) {
case "T": type = .temperature
case "V": type = .voltage
case "P": type = .power
case "I": type = .current
default: type = nil
}
if let t = type {
list.append(Sensor(key: key, name: key, group: .unknown, type: t, platforms: []))
}
}
for sensor in list {
if let newValue = SMC.shared.getValue(sensor.key) {
if let idx = list.firstIndex(where: { $0.key == sensor.key }) {
list[idx].value = newValue
}
}
}
var results: [Sensor_p] = []
results += list.filter({ (s: Sensor_p) -> Bool in
if s.type == .temperature && (s.value == 0 || s.value > 110) {
return false
} else if s.type == .current && s.value > 100 {
return false
}
return true
})
#if arch(arm64)
if self.HIDState {
results += self.initHIDSensors()
}
results += self.initIOSensors()
#endif
results += self.initCalculatedSensors(results)
return results
}
public override func read() {
for i in self.list.sensors.indices {
guard self.list.sensors[i].group != .hid && !self.list.sensors[i].isComputed else { continue }
if !self.unknownSensorsState && self.list.sensors[i].group == .unknown { continue }
var newValue = SMC.shared.getValue(self.list.sensors[i].key) ?? 0
if self.list.sensors[i].type == .temperature && self.list.sensors[i].group == .CPU &&
(newValue < 10 || newValue > 120) { // fix for m2 broken sensors
newValue = self.list.sensors[i].value
}
self.list.sensors[i].value = newValue
}
var cpuSensors = self.list.sensors.filter({ $0.group == .CPU && $0.type == .temperature && $0.average }).map{ $0.value }
var gpuSensors = self.list.sensors.filter({ $0.group == .GPU && $0.type == .temperature && $0.average }).map{ $0.value }
let fanSensors = self.list.sensors.filter({ $0.type == .fan && !$0.isComputed })
#if arch(arm64)
if self.HIDState {
for typ in SensorsReader.HIDtypes {
let (page, usage, type) = self.m1Preset(type: typ)
AppleSiliconSensors(page, usage, type).forEach { (key, value) in
guard let key = key as? String, let value = value as? Double, value < 300 && value >= 0 else {
return
}
if let idx = self.list.sensors.firstIndex(where: { $0.group == .hid && $0.key == key }) {
self.list.sensors[idx].value = value
}
}
}
cpuSensors += self.list.sensors.filter({ $0.key.hasPrefix("pACC MTR Temp") || $0.key.hasPrefix("eACC MTR Temp") }).map{ $0.value }
gpuSensors += self.list.sensors.filter({ $0.key.hasPrefix("GPU MTR Temp") }).map{ $0.value }
let socSensors = self.list.sensors.filter({ $0.key.hasPrefix("SOC MTR Temp") }).map{ $0.value }
if !socSensors.isEmpty {
if let idx = self.list.sensors.firstIndex(where: { $0.key == "Average SOC" }) {
self.list.sensors[idx].value = socSensors.reduce(0, +) / Double(socSensors.count)
}
if let max = socSensors.max() {
if let idx = self.list.sensors.firstIndex(where: { $0.key == "Hottest SOC" }) {
self.list.sensors[idx].value = max
}
}
}
}
if let (cpu, gpu, ane, ram, pci) = self.IOSensors() {
if let idx = self.list.sensors.firstIndex(where: { $0.key == "CPU Power" }) {
self.list.sensors[idx].value = cpu
}
if let idx = self.list.sensors.firstIndex(where: { $0.key == "GPU Power" }) {
self.list.sensors[idx].value = gpu
}
if let idx = self.list.sensors.firstIndex(where: { $0.key == "ANE Power" }) {
self.list.sensors[idx].value = ane
}
if let idx = self.list.sensors.firstIndex(where: { $0.key == "RAM Power" }) {
self.list.sensors[idx].value = ram
}
if let idx = self.list.sensors.firstIndex(where: { $0.key == "PCI Power" }) {
self.list.sensors[idx].value = pci
}
}
#endif
if !cpuSensors.isEmpty {
if let idx = self.list.sensors.firstIndex(where: { $0.key == "Average CPU" }) {
self.list.sensors[idx].value = cpuSensors.reduce(0, +) / Double(cpuSensors.count)
}
if let max = cpuSensors.max() {
if let idx = self.list.sensors.firstIndex(where: { $0.key == "Hottest CPU" }) {
self.list.sensors[idx].value = max
}
}
}
if !gpuSensors.isEmpty {
if let idx = self.list.sensors.firstIndex(where: { $0.key == "Average GPU" }) {
self.list.sensors[idx].value = gpuSensors.reduce(0, +) / Double(gpuSensors.count)
}
if let max = gpuSensors.max() {
if let idx = self.list.sensors.firstIndex(where: { $0.key == "Hottest GPU" }) {
self.list.sensors[idx].value = max
}
}
}
if !fanSensors.isEmpty && fanSensors.count > 1 {
if let f = fanSensors.max(by: { $0.value < $1.value }) as? Fan {
if let idx = self.list.sensors.firstIndex(where: { $0.key == "Fastest fan" }) {
if var fan = self.list.sensors[idx] as? Fan {
fan.value = f.value
fan.minSpeed = f.minSpeed
fan.maxSpeed = f.maxSpeed
self.list.sensors[idx] = fan
}
}
}
}
if let PSTRSensor = self.list.sensors.first(where: { $0.key == "PSTR"}), PSTRSensor.value > 0 {
let sinceLastRead = Date().timeIntervalSince(self.lastRead)
let sinceFirstRead = Date().timeIntervalSince(self.firstRead)
if let totalIdx = self.list.sensors.firstIndex(where: {$0.key == "Total System Consumption"}), sinceLastRead > 0 {
self.list.sensors[totalIdx].value += PSTRSensor.value * sinceLastRead / 3600
if let avgIdx = self.list.sensors.firstIndex(where: {$0.key == "Average System Total"}), sinceFirstRead > 0 {
self.list.sensors[avgIdx].value = self.list.sensors[totalIdx].value * 3600 / sinceFirstRead
}
}
self.lastRead = Date()
}
// cut off low dc in voltage
if let idx = self.list.sensors.firstIndex(where: { $0.key == "VD0R" }), self.list.sensors[idx].value < 0.4 {
self.list.sensors[idx].value = 0
}
// cut off low dc in current
if let idx = self.list.sensors.firstIndex(where: { $0.key == "ID0R" }), self.list.sensors[idx].value < 0.05 {
self.list.sensors[idx].value = 0
}
self.callback(self.list)
}
private func initCalculatedSensors(_ sensors: [Sensor_p]) -> [Sensor_p] {
var list: [Sensor_p] = []
var cpuSensors = sensors.filter({ $0.group == .CPU && $0.type == .temperature && $0.average }).map{ $0.value }
var gpuSensors = sensors.filter({ $0.group == .GPU && $0.type == .temperature && $0.average }).map{ $0.value }
#if arch(arm64)
if self.HIDState {
cpuSensors += sensors.filter({ $0.key.hasPrefix("pACC MTR Temp") || $0.key.hasPrefix("eACC MTR Temp") }).map{ $0.value }
gpuSensors += sensors.filter({ $0.key.hasPrefix("GPU MTR Temp") }).map{ $0.value }
}
#endif
let fanSensors = sensors.filter({ $0.type == .fan && !$0.isComputed })
if !cpuSensors.isEmpty {
let value = cpuSensors.reduce(0, +) / Double(cpuSensors.count)
list.append(Sensor(key: "Average CPU", name: "Average CPU", value: value, group: .CPU, type: .temperature, platforms: Platform.all, isComputed: true))
if let max = cpuSensors.max() {
list.append(Sensor(key: "Hottest CPU", name: "Hottest CPU", value: max, group: .CPU, type: .temperature, platforms: Platform.all, isComputed: true))
}
}
if !gpuSensors.isEmpty {
let value = gpuSensors.reduce(0, +) / Double(gpuSensors.count)
list.append(Sensor(key: "Average GPU", name: "Average GPU", value: value, group: .GPU, type: .temperature, platforms: Platform.all, isComputed: true))
if let max = gpuSensors.max() {
list.append(Sensor(key: "Hottest GPU", name: "Hottest GPU", value: max, group: .GPU, type: .temperature, platforms: Platform.all, isComputed: true))
}
}
if !fanSensors.isEmpty && fanSensors.count > 1 {
if let f = fanSensors.max(by: { $0.value < $1.value }) as? Fan {
list.append(Fan(id: -1, key: "Fastest fan", name: "Fastest fan", minSpeed: f.minSpeed, maxSpeed: f.maxSpeed, value: f.value, mode: .automatic, isComputed: true))
}
}
// Init total power since launched, only if Total Power sensor is available
if sensors.contains(where: { $0.key == "PSTR"}) {
list.append(Sensor(key: "Total System Consumption", name: "Total System Consumption", value: 0, group: .sensor, type: .energy, platforms: Platform.all, isComputed: true))
list.append(Sensor(key: "Average System Total", name: "Average System Total", value: 0, group: .sensor, type: .power, platforms: Platform.all, isComputed: true))
}
return list.filter({ (s: Sensor_p) -> Bool in
switch s.type {
case .temperature:
return s.value < 110 && s.value >= 0
case .voltage:
return s.value < 300 && s.value >= 0
case .current:
return s.value < 100 && s.value >= 0
default: return true
}
}).sorted { $0.key.lowercased() < $1.key.lowercased() }
}
public func unknownCallback() {
self.unknownSensorsState = Store.shared.bool(key: "Sensors_unknown", defaultValue: false)
}
}
// MARK: - Fans
extension SensorsReader {
private func loadFans(_ count: Int) -> [Sensor_p] {
debug("Found \(Int(count)) fans", log: self.log)
var list: [Fan] = []
for i in 0.. FanMode {
#if arch(arm64)
// Apple Silicon: Read F%dMd directly
// Mode values: 0 = auto, 1 = manual, 3 = system (treated as auto for UI)
let modeValue = Int(SMC.shared.getValue("F\(id)Md") ?? 0)
return modeValue == 1 ? .forced : .automatic
#else
// Legacy Intel: Use FS! bitmask
// Bitmask: 0 = all auto, 1 = fan 0 forced, 2 = fan 1 forced, 3 = both forced
let fansMode: Int = Int(SMC.shared.getValue("FS! ") ?? 0)
var mode: FanMode = .automatic
if fansMode == 0 {
mode = .automatic
} else if fansMode == 3 {
mode = .forced
} else if fansMode == 1 && id == 0 {
mode = .forced
} else if fansMode == 2 && id == 1 {
mode = .forced
}
return mode
#endif
}
}
// MARK: - HID sensors
extension SensorsReader {
private func m1Preset(type: SensorType) -> (Int32, Int32, Int32) {
var page: Int32 = 0
var usage: Int32 = 0
var eventType: Int32 = kIOHIDEventTypeTemperature
// usagePage:
// kHIDPage_AppleVendor = 0xff00,
// kHIDPage_AppleVendorTemperatureSensor = 0xff05,
// kHIDPage_AppleVendorPowerSensor = 0xff08,
// kHIDPage_GenericDesktop
//
// usage:
// kHIDUsage_AppleVendor_TemperatureSensor = 0x0005,
// kHIDUsage_AppleVendorPowerSensor_Current = 0x0002,
// kHIDUsage_AppleVendorPowerSensor_Voltage = 0x0003,
// kHIDUsage_GD_Keyboard
//
switch type {
case .temperature:
page = 0xff00
usage = 0x0005
eventType = kIOHIDEventTypeTemperature
case .current:
page = 0xff08
usage = 0x0002
eventType = kIOHIDEventTypePower
case .voltage:
page = 0xff08
usage = 0x0003
eventType = kIOHIDEventTypePower
case .power, .energy, .fan: break
}
return (page, usage, eventType)
}
private func initHIDSensors() -> [Sensor] {
var list: [Sensor] = []
for typ in SensorsReader.HIDtypes {
let (page, usage, type) = self.m1Preset(type: typ)
if let sensors = AppleSiliconSensors(page, usage, type) {
sensors.forEach { (key, value) in
guard let key = key as? String, let value = value as? Double else {
return
}
var name: String = key
HIDSensorsList.forEach { (s: Sensor) in
if s.key.contains("%") {
var index = 1
for i in 0..<64 {
if s.key.replacingOccurrences(of: "%", with: "\(i)") == key {
name = s.name.replacingOccurrences(of: "%", with: "\(index)")
}
index += 1
}
} else if s.key == key {
name = s.name
}
}
list.append(Sensor(
key: key,
name: name,
value: value,
group: .hid,
type: typ,
platforms: Platform.all
))
}
}
}
let socSensors = list.filter({ $0.key.hasPrefix("SOC MTR Temp") }).map{ $0.value }
if !socSensors.isEmpty {
let value = socSensors.reduce(0, +) / Double(socSensors.count)
list.append(Sensor(key: "Average SOC", name: "Average SOC", value: value, group: .hid, type: .temperature, platforms: Platform.all))
if let max = socSensors.max() {
list.append(Sensor(key: "Hottest SOC", name: "Hottest SOC", value: max, group: .hid, type: .temperature, platforms: Platform.all))
}
}
return list.filter({ (s: Sensor_p) -> Bool in
switch s.type {
case .temperature:
return s.value < 110 && s.value >= 0
case .voltage:
return s.value < 300 && s.value >= 0
case .current:
return s.value < 100 && s.value >= 0
default: return true
}
}).sorted { $0.key.lowercased() < $1.key.lowercased() }
}
public func HIDCallback() {
if self.HIDState {
self.list.sensors += self.initHIDSensors()
} else {
self.list.sensors = self.list.sensors.filter({ $0.group != .hid })
}
}
}
// MARK: - Apple Silicon power sensors
extension SensorsReader {
private func getChannels() -> CFMutableDictionary? {
let channelNames: [(String, String?)] = [("Energy Model", nil)]
var channels: [CFDictionary] = []
for (gname, sname) in channelNames {
let channel = IOReportCopyChannelsInGroup(gname as CFString?, sname as CFString?, 0, 0, 0)
guard let channel = channel?.takeRetainedValue() else { continue }
channels.append(channel)
}
let chan = channels[0]
for i in 1.. [Sensor] {
guard let (cpu, gpu, ane, ram, pci) = self.IOSensors() else { return [] }
return [
Sensor(key: "CPU Power", name: "CPU Power", value: cpu, group: .CPU, type: .power, platforms: Platform.apple, isComputed: true),
Sensor(key: "GPU Power", name: "GPU Power", value: gpu, group: .GPU, type: .power, platforms: Platform.apple, isComputed: true),
Sensor(key: "ANE Power", name: "ANE Power", value: ane, group: .system, type: .power, platforms: Platform.apple, isComputed: true),
Sensor(key: "RAM Power", name: "RAM Power", value: ram, group: .system, type: .power, platforms: Platform.apple, isComputed: true),
Sensor(key: "PCI Power", name: "PCI Power", value: pci, group: .system, type: .power, platforms: Platform.apple, isComputed: true)
]
}
private static func appleSiliconPower(currentEnergy: Double, previousEnergy: Double, elapsed: TimeInterval) -> Double {
guard elapsed > 0 else { return 0 }
return (currentEnergy - previousEnergy) / elapsed
}
private func IOSensors() -> (Double, Double, Double, Double, Double)? {
guard let reportSample = IOReportCreateSamples(self.subscription, self.channels, nil)?.takeRetainedValue(),
let dict = reportSample as? [String: Any] else {
return nil
}
let items = dict["IOReportChannels"] as! CFArray
let now = Date()
let prevCPU = self.powers.CPU
let prevGPU = self.powers.GPU
let prevANE = self.powers.ANE
let prevRAM = self.powers.RAM
let prevPCI = self.powers.PCI
for i in 0.. Void) = {}
public var HIDcallback: (() -> Void) = {}
public var unknownCallback: (() -> Void) = {}
public var setInterval: ((_ value: Int) -> Void) = {_ in }
public var selectedHandler: (String) -> Void = {_ in }
private let title: String
private var button: NSPopUpButton?
private var list: [Sensor_p] = []
private var sensorsPrefs: PreferencesSection?
private var selectedSensor: String = "Average System Total"
public init(_ module: ModuleType) {
self.title = module.stringValue
self.hidState = SystemKit.shared.device.platform == .m1 ? true : false
super.init(frame: NSRect.zero)
self.orientation = .vertical
self.spacing = Constants.Settings.margin
self.updateIntervalValue = Store.shared.int(key: "\(self.title)_updateInterval", defaultValue: self.updateIntervalValue)
self.hidState = Store.shared.bool(key: "\(self.title)_hid", defaultValue: self.hidState)
self.fanSpeedState = Store.shared.bool(key: "\(self.title)_speed", defaultValue: self.fanSpeedState)
self.fansSyncState = Store.shared.bool(key: "\(self.title)_fansSync", defaultValue: self.fansSyncState)
self.unknownSensorsState = Store.shared.bool(key: "\(self.title)_unknown", defaultValue: self.unknownSensorsState)
self.fanValueState = FanValue(rawValue: Store.shared.string(key: "\(self.title)_fanValue", defaultValue: self.fanValueState.rawValue)) ?? .percentage
self.selectedSensor = Store.shared.string(key: "\(self.title)_sensor", defaultValue: self.selectedSensor)
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Update interval"), component: selectView(
action: #selector(self.changeUpdateInterval),
items: ReaderUpdateIntervals,
selected: "\(self.updateIntervalValue)"
))
]))
self.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Fan value"), component: selectView(
action: #selector(self.toggleFanValue),
items: FanValues,
selected: self.fanValueState.rawValue
)),
PreferencesRow(localizedString("Save the fan speed"), component: switchView(
action: #selector(self.toggleSpeedState),
state: self.fanSpeedState
)),
PreferencesRow(localizedString("Synchronize fan's control"), component: switchView(
action: #selector(self.toggleFansSync),
state: self.fansSyncState
))
]))
var sensorsRows: [PreferencesRow] = [
PreferencesRow(localizedString("Show unknown sensors"), component: switchView(
action: #selector(self.toggleuUnknownSensors),
state: self.unknownSensorsState
))
]
if isARM {
sensorsRows.append(PreferencesRow(localizedString("HID sensors"), component: switchView(
action: #selector(self.toggleHID),
state: self.hidState
)))
}
sensorsRows.append(PreferencesRow(localizedString("Sensor to show"), id: "active_sensor", component: selectView(
action: #selector(self.handleSelection),
items: [],
selected: self.selectedSensor)
))
let sensorsPrefs = PreferencesSection(sensorsRows)
self.sensorsPrefs = sensorsPrefs
self.addArrangedSubview(sensorsPrefs)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func load(widgets: [widget_t]) {
var sensors = self.list
guard !sensors.isEmpty else {
return
}
if !self.unknownSensorsState {
sensors = sensors.filter({ $0.group != .unknown })
}
self.subviews.filter({ $0.identifier == NSUserInterfaceItemIdentifier("sensor") }).forEach { v in
v.removeFromSuperview()
}
var types: [SensorType] = []
sensors.forEach { (s: Sensor_p) in
if !types.contains(s.type) {
types.append(s.type)
}
}
var buttonList: [KeyValue_t] = []
types.forEach { (typ: SensorType) in
let section = PreferencesSection(label: localizedString(typ.rawValue))
section.identifier = NSUserInterfaceItemIdentifier("sensor")
let filtered = sensors.filter{ $0.type == typ }
var groups: [SensorGroup] = []
filtered.forEach { (s: Sensor_p) in
if !groups.contains(s.group) {
groups.append(s.group)
}
}
groups.forEach { (group: SensorGroup) in
filtered.filter{ $0.group == group }.forEach { (s: Sensor_p) in
let btn = switchView(
action: #selector(self.toggleSensor),
state: s.state
)
btn.identifier = NSUserInterfaceItemIdentifier(rawValue: s.key)
section.add(PreferencesRow(localizedString(s.name), component: btn))
buttonList.append(KeyValue_t(key: s.key, value: "\(localizedString(typ.rawValue)) - \(s.name)"))
}
}
self.addArrangedSubview(section)
}
if let row = self.sensorsPrefs?.findRow("active_sensor") {
if !widgets.isEmpty {
self.sensorsPrefs?.setRowVisibility(row, newState: widgets.contains(where: { $0 == .mini }))
}
row.replaceComponent(with: selectView(
action: #selector(self.handleSelection),
items: buttonList,
selected: self.selectedSensor
))
}
}
public func setList(_ list: [Sensor_p]?) {
guard let list else { return }
self.list = self.unknownSensorsState ? list : list.filter({ $0.group != .unknown })
self.load(widgets: [])
}
@objc private func toggleSensor(_ sender: NSControl) {
guard let id = sender.identifier else { return }
Store.shared.set(key: "sensor_\(id.rawValue)", value: controlState(sender))
self.callback()
}
@objc private func changeUpdateInterval(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String, let value = Int(key) else { return }
self.updateIntervalValue = value
Store.shared.set(key: "\(self.title)_updateInterval", value: value)
self.setInterval(value)
}
@objc private func toggleSpeedState(_ sender: NSControl) {
self.fanSpeedState = controlState(sender)
Store.shared.set(key: "\(self.title)_speed", value: self.fanSpeedState)
self.callback()
}
@objc private func toggleHID(_ sender: NSControl) {
self.hidState = controlState(sender)
Store.shared.set(key: "\(self.title)_hid", value: self.hidState)
self.HIDcallback()
}
@objc private func toggleFansSync(_ sender: NSControl) {
self.fansSyncState = controlState(sender)
Store.shared.set(key: "\(self.title)_fansSync", value: self.fansSyncState)
}
@objc private func toggleuUnknownSensors(_ sender: NSControl) {
self.unknownSensorsState = controlState(sender)
Store.shared.set(key: "\(self.title)_unknown", value: self.unknownSensorsState)
self.unknownCallback()
}
@objc private func toggleFanValue(_ sender: NSMenuItem) {
if let key = sender.representedObject as? String, let value = FanValue(rawValue: key) {
self.fanValueState = value
Store.shared.set(key: "\(self.title)_fanValue", value: self.fanValueState.rawValue)
self.callback()
}
}
@objc private func handleSelection(_ sender: NSPopUpButton) {
guard let item = sender.selectedItem, let id = item.representedObject as? String else { return }
self.selectedSensor = id
Store.shared.set(key: "\(self.title)_sensor", value: self.selectedSensor)
self.selectedHandler(self.selectedSensor)
}
}
================================================
FILE: Modules/Sensors/values.swift
================================================
//
// values.swift
// Sensors
//
// Created by Serhiy Mytrovtsiy on 17/06/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Kit
import Cocoa
public enum SensorGroup: String, Codable {
case CPU = "CPU"
case GPU = "GPU"
case system = "Systems"
case sensor = "Sensors"
case hid = "HID"
case unknown = "Unknown"
}
public enum SensorType: String, Codable {
case temperature = "Temperature"
case voltage = "Voltage"
case current = "Current"
case power = "Power"
case energy = "Energy"
case fan = "Fans"
}
public protocol Sensor_p {
var key: String { get }
var name: String { get }
var value: Double { get set }
var state: Bool { get }
var popupState: Bool { get }
var notificationThreshold: String { get }
var group: SensorGroup { get }
var type: SensorType { get }
var platforms: [Platform] { get }
var isComputed: Bool { get }
var average: Bool { get }
var localValue: Double { get }
var unit: String { get }
var miniUnit: String { get }
var formattedValue: String { get }
var formattedMiniValue: String { get }
var formattedPopupValue: String { get }
}
public class Sensors_List: Codable {
private var queue: DispatchQueue = DispatchQueue(label: "eu.exelban.Stats.Sensors.SynchronizedArray", attributes: .concurrent)
private var list: [Sensor_p] = []
public var sensors: [Sensor_p] {
get {
self.queue.sync{ self.list }
}
set {
self.queue.async(flags: .barrier) {
self.list = newValue
}
}
}
private enum CodingKeys: String, CodingKey {
case sensors
}
public init() {}
public func encode(to encoder: Encoder) throws {
let wrappers = sensors.map { Sensor_w($0) }
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(wrappers, forKey: .sensors)
}
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let wrappers = try container.decode([Sensor_w].self, forKey: .sensors)
self.sensors = wrappers.map { $0.sensor }
}
}
public struct Sensor_w: Codable {
let sensor: Sensor_p
private enum CodingKeys: String, CodingKey {
case base, payload
}
private enum Typ: Int, Codable {
case sensor
case fan
}
init(_ sensor: Sensor_p) {
self.sensor = sensor
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let base = try container.decode(Typ.self, forKey: .base)
switch base {
case .sensor: self.sensor = try container.decode(Sensor.self, forKey: .payload)
case .fan: self.sensor = try container.decode(Fan.self, forKey: .payload)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch sensor {
case let payload as Sensor:
try container.encode(Typ.sensor, forKey: .base)
try container.encode(payload, forKey: .payload)
case let payload as Fan:
try container.encode(Typ.fan, forKey: .base)
try container.encode(payload, forKey: .payload)
default: break
}
}
}
public struct Sensor: Sensor_p, Codable {
public var key: String
public var name: String
public var value: Double = 0
public var group: SensorGroup
public var type: SensorType
public var platforms: [Platform]
public var isComputed: Bool = false
public var average: Bool = false
public var unit: String {
switch self.type {
case .temperature:
return UnitTemperature.current.symbol
case .voltage:
return "V"
case .power:
return "W"
case .energy:
return "Wh"
case .current:
return "A"
case .fan:
return "RPM"
}
}
public var miniUnit: String {
switch self.type {
case .temperature:
return "°"
case .voltage:
return "V"
case .power:
return "W"
case .energy:
return "Wh"
case .current:
return "A"
default:
return ""
}
}
public var formattedValue: String {
switch self.type {
case .temperature:
return temperature(value)
case .voltage:
let val = value >= 100 ? "\(Int(value))" : String(format: "%.3f", value)
return "\(val)\(unit)"
case .power, .energy:
let val = value >= 100 ? "\(Int(value))" : String(format: "%.2f", value)
return "\(val)\(unit)"
case .current:
let val = value >= 100 ? "\(Int(value))" : String(format: "%.2f", value)
return "\(val)\(unit)"
case .fan:
return "\(Int(value)) \(unit)"
}
}
public var formattedPopupValue: String {
switch self.type {
case .temperature:
return temperature(value, fractionDigits: 1)
case .voltage:
let val = value >= 100 ? "\(Int(value))" : String(format: "%.3f", value)
return "\(val)\(unit)"
case .power, .energy:
let val = value >= 100 ? "\(Int(value))" : String(format: "%.2f", value)
return "\(val)\(unit)"
case .current:
let val = value >= 100 ? "\(Int(value))" : String(format: "%.2f", value)
return "\(val)\(unit)"
case .fan:
return "\(Int(value)) \(unit)"
}
}
public var formattedMiniValue: String {
switch self.type {
case .temperature:
return temperature(value).replacingOccurrences(of: "C", with: "").replacingOccurrences(of: "F", with: "")
case .voltage, .power, .energy, .current:
let val = value >= 9.95 ? "\(Int(round(value)))" : String(format: "%.1f", value)
return "\(val)\(unit)"
case .fan:
return "\(Int(value))"
}
}
public var localValue: Double {
if self.type == .temperature {
return Double(self.formattedMiniValue.digits) ?? self.value
}
return self.value
}
public var state: Bool {
Store.shared.bool(key: "sensor_\(self.key)", defaultValue: false)
}
public var popupState: Bool {
Store.shared.bool(key: "sensor_\(self.key)_popup", defaultValue: true)
}
public var notificationThreshold: String {
Store.shared.string(key: "sensor_\(self.key)_notification", defaultValue: "")
}
public func copy() -> Sensor {
Sensor(
key: self.key,
name: self.name,
group: self.group,
type: self.type,
platforms: self.platforms,
isComputed: self.isComputed,
average: self.average
)
}
}
public struct Fan: Sensor_p, Codable {
public let id: Int
public var key: String
public var name: String
public var minSpeed: Double
public var maxSpeed: Double
public var value: Double
public var mode: FanMode
public var percentage: Int {
if self.value != 0 && self.maxSpeed != 0 && self.value != 1 && self.maxSpeed != 1 {
return (100*Int(self.value)) / Int(self.maxSpeed)
}
return 0
}
public var group: SensorGroup = .sensor
public var type: SensorType = .fan
public var platforms: [Platform] = Platform.all
public var isIntelOnly: Bool = false
public var isComputed: Bool = false
public var average: Bool = false
public var unit: String = "RPM"
public var miniUnit: String = ""
public var formattedValue: String {
"\(Int(self.value)) RPM"
}
public var formattedMiniValue: String {
"\(Int(self.value))"
}
public var formattedPopupValue: String {
"\(Int(self.value)) RPM"
}
public var localValue: Double {
return self.value
}
public var state: Bool {
Store.shared.bool(key: "sensor_\(self.key)", defaultValue: false)
}
public var popupState: Bool {
Store.shared.bool(key: "sensor_\(self.key)_popup", defaultValue: true)
}
public var notificationThreshold: String {
Store.shared.string(key: "sensor_\(self.key)_notification", defaultValue: "")
}
public var customSpeed: Int? {
get {
if !Store.shared.exist(key: "fan_\(self.id)_speed") {
return nil
}
return Store.shared.int(key: "fan_\(self.id)_speed", defaultValue: Int(self.minSpeed))
}
set {
if let value = newValue {
Store.shared.set(key: "fan_\(self.id)_speed", value: value)
} else {
Store.shared.remove("fan_\(self.id)_speed")
}
}
}
public var customMode: FanMode? {
get {
if !Store.shared.exist(key: "fan_\(self.id)_mode") {
return nil
}
let value = Store.shared.int(key: "fan_\(self.id)_mode", defaultValue: FanMode.automatic.rawValue)
return FanMode(rawValue: value)
}
set {
if let value = newValue {
Store.shared.set(key: "fan_\(self.id)_mode", value: value.rawValue)
} else {
Store.shared.remove("fan_\(self.id)_mode")
}
}
}
}
// List of keys: https://github.com/acidanthera/VirtualSMC/blob/master/Docs/SMCSensorKeys.txt
internal let SensorsList: [Sensor] = [
// Temperature
Sensor(key: "TA%P", name: "Ambient %", group: .sensor, type: .temperature, platforms: Platform.all),
Sensor(key: "Th%H", name: "Heatpipe %", group: .sensor, type: .temperature, platforms: [.intel]),
Sensor(key: "TZ%C", name: "Thermal zone %", group: .sensor, type: .temperature, platforms: Platform.all),
Sensor(key: "TC0D", name: "CPU diode", group: .CPU, type: .temperature, platforms: Platform.all),
Sensor(key: "TC0E", name: "CPU diode virtual", group: .CPU, type: .temperature, platforms: Platform.all),
Sensor(key: "TC0F", name: "CPU diode filtered", group: .CPU, type: .temperature, platforms: Platform.all),
Sensor(key: "TC0H", name: "CPU heatsink", group: .CPU, type: .temperature, platforms: Platform.all),
Sensor(key: "TC0P", name: "CPU proximity", group: .CPU, type: .temperature, platforms: Platform.all),
Sensor(key: "TCAD", name: "CPU package", group: .CPU, type: .temperature, platforms: Platform.all),
Sensor(key: "TC%c", name: "CPU core %", group: .CPU, type: .temperature, platforms: Platform.all, average: true),
Sensor(key: "TC%C", name: "CPU Core %", group: .CPU, type: .temperature, platforms: Platform.all, average: true),
Sensor(key: "TCGC", name: "GPU Intel Graphics", group: .GPU, type: .temperature, platforms: Platform.all),
Sensor(key: "TG0D", name: "GPU diode", group: .GPU, type: .temperature, platforms: Platform.all),
Sensor(key: "TGDD", name: "GPU AMD Radeon", group: .GPU, type: .temperature, platforms: Platform.all),
Sensor(key: "TG0H", name: "GPU heatsink", group: .GPU, type: .temperature, platforms: Platform.all),
Sensor(key: "TG0P", name: "GPU proximity", group: .GPU, type: .temperature, platforms: Platform.all),
Sensor(key: "Tm0P", name: "Mainboard", group: .system, type: .temperature, platforms: Platform.all),
Sensor(key: "Tp0P", name: "Powerboard", group: .system, type: .temperature, platforms: [.intel]),
Sensor(key: "TB1T", name: "Battery", group: .system, type: .temperature, platforms: [.intel]),
Sensor(key: "TW0P", name: "Airport", group: .system, type: .temperature, platforms: Platform.all),
Sensor(key: "TL0P", name: "Display", group: .system, type: .temperature, platforms: Platform.all),
Sensor(key: "TI%P", name: "Thunderbolt %", group: .system, type: .temperature, platforms: Platform.all),
Sensor(key: "TH%A", name: "Disk % (A)", group: .system, type: .temperature, platforms: Platform.all),
Sensor(key: "TH%B", name: "Disk % (B)", group: .system, type: .temperature, platforms: Platform.all),
Sensor(key: "TH%C", name: "Disk % (C)", group: .system, type: .temperature, platforms: Platform.all),
Sensor(key: "TTLD", name: "Thunderbolt left", group: .system, type: .temperature, platforms: Platform.all),
Sensor(key: "TTRD", name: "Thunderbolt right", group: .system, type: .temperature, platforms: Platform.all),
Sensor(key: "TN0D", name: "Northbridge diode", group: .system, type: .temperature, platforms: Platform.all),
Sensor(key: "TN0H", name: "Northbridge heatsink", group: .system, type: .temperature, platforms: Platform.all),
Sensor(key: "TN0P", name: "Northbridge proximity", group: .system, type: .temperature, platforms: Platform.all),
// Apple Silicon
Sensor(key: "Tp09", name: "CPU efficiency core 1", group: .CPU, type: .temperature, platforms: Platform.m1Gen, average: true),
Sensor(key: "Tp0T", name: "CPU efficiency core 2", group: .CPU, type: .temperature, platforms: Platform.m1Gen, average: true),
Sensor(key: "Tp01", name: "CPU performance core 1", group: .CPU, type: .temperature, platforms: Platform.m1Gen, average: true),
Sensor(key: "Tp05", name: "CPU performance core 2", group: .CPU, type: .temperature, platforms: Platform.m1Gen, average: true),
Sensor(key: "Tp0D", name: "CPU performance core 3", group: .CPU, type: .temperature, platforms: Platform.m1Gen, average: true),
Sensor(key: "Tp0H", name: "CPU performance core 4", group: .CPU, type: .temperature, platforms: Platform.m1Gen, average: true),
Sensor(key: "Tp0L", name: "CPU performance core 5", group: .CPU, type: .temperature, platforms: Platform.m1Gen, average: true),
Sensor(key: "Tp0P", name: "CPU performance core 6", group: .CPU, type: .temperature, platforms: Platform.m1Gen, average: true),
Sensor(key: "Tp0X", name: "CPU performance core 7", group: .CPU, type: .temperature, platforms: Platform.m1Gen, average: true),
Sensor(key: "Tp0b", name: "CPU performance core 8", group: .CPU, type: .temperature, platforms: Platform.m1Gen, average: true),
Sensor(key: "Tg05", name: "GPU 1", group: .GPU, type: .temperature, platforms: Platform.m1Gen, average: true),
Sensor(key: "Tg0D", name: "GPU 2", group: .GPU, type: .temperature, platforms: Platform.m1Gen, average: true),
Sensor(key: "Tg0L", name: "GPU 3", group: .GPU, type: .temperature, platforms: Platform.m1Gen, average: true),
Sensor(key: "Tg0T", name: "GPU 4", group: .GPU, type: .temperature, platforms: Platform.m1Gen, average: true),
Sensor(key: "Tm02", name: "Memory 1", group: .sensor, type: .temperature, platforms: Platform.m1Gen),
Sensor(key: "Tm06", name: "Memory 2", group: .sensor, type: .temperature, platforms: Platform.m1Gen),
Sensor(key: "Tm08", name: "Memory 3", group: .sensor, type: .temperature, platforms: Platform.m1Gen),
Sensor(key: "Tm09", name: "Memory 4", group: .sensor, type: .temperature, platforms: Platform.m1Gen),
// M2
Sensor(key: "Tp1h", name: "CPU efficiency core 1", group: .CPU, type: .temperature, platforms: Platform.m2Gen, average: true),
Sensor(key: "Tp1t", name: "CPU efficiency core 2", group: .CPU, type: .temperature, platforms: Platform.m2Gen, average: true),
Sensor(key: "Tp1p", name: "CPU efficiency core 3", group: .CPU, type: .temperature, platforms: Platform.m2Gen, average: true),
Sensor(key: "Tp1l", name: "CPU efficiency core 4", group: .CPU, type: .temperature, platforms: Platform.m2Gen, average: true),
Sensor(key: "Tp01", name: "CPU performance core 1", group: .CPU, type: .temperature, platforms: Platform.m2Gen, average: true),
Sensor(key: "Tp05", name: "CPU performance core 2", group: .CPU, type: .temperature, platforms: Platform.m2Gen, average: true),
Sensor(key: "Tp09", name: "CPU performance core 3", group: .CPU, type: .temperature, platforms: Platform.m2Gen, average: true),
Sensor(key: "Tp0D", name: "CPU performance core 4", group: .CPU, type: .temperature, platforms: Platform.m2Gen, average: true),
Sensor(key: "Tp0X", name: "CPU performance core 5", group: .CPU, type: .temperature, platforms: Platform.m2Gen, average: true),
Sensor(key: "Tp0b", name: "CPU performance core 6", group: .CPU, type: .temperature, platforms: Platform.m2Gen, average: true),
Sensor(key: "Tp0f", name: "CPU performance core 7", group: .CPU, type: .temperature, platforms: Platform.m2Gen, average: true),
Sensor(key: "Tp0j", name: "CPU performance core 8", group: .CPU, type: .temperature, platforms: Platform.m2Gen, average: true),
Sensor(key: "Tg0f", name: "GPU 1", group: .GPU, type: .temperature, platforms: Platform.m2Gen, average: true),
Sensor(key: "Tg0j", name: "GPU 2", group: .GPU, type: .temperature, platforms: Platform.m2Gen, average: true),
// M3
Sensor(key: "Te05", name: "CPU efficiency core 1", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Te0L", name: "CPU efficiency core 2", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Te0P", name: "CPU efficiency core 3", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Te0S", name: "CPU efficiency core 4", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf04", name: "CPU performance core 1", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf09", name: "CPU performance core 2", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf0A", name: "CPU performance core 3", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf0B", name: "CPU performance core 4", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf0D", name: "CPU performance core 5", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf0E", name: "CPU performance core 6", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf44", name: "CPU performance core 7", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf49", name: "CPU performance core 8", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf4A", name: "CPU performance core 9", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf4B", name: "CPU performance core 10", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf4D", name: "CPU performance core 11", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf4E", name: "CPU performance core 12", group: .CPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf14", name: "GPU 1", group: .GPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf18", name: "GPU 2", group: .GPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf19", name: "GPU 3", group: .GPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf1A", name: "GPU 4", group: .GPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf24", name: "GPU 5", group: .GPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf28", name: "GPU 6", group: .GPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf29", name: "GPU 7", group: .GPU, type: .temperature, platforms: Platform.m3Gen, average: true),
Sensor(key: "Tf2A", name: "GPU 8", group: .GPU, type: .temperature, platforms: Platform.m3Gen, average: true),
// M4
Sensor(key: "Te05", name: "CPU efficiency core 1", group: .CPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Te0S", name: "CPU efficiency core 2", group: .CPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Te09", name: "CPU efficiency core 3", group: .CPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Te0H", name: "CPU efficiency core 4", group: .CPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Tp01", name: "CPU performance core 1", group: .CPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Tp05", name: "CPU performance core 2", group: .CPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Tp09", name: "CPU performance core 3", group: .CPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Tp0D", name: "CPU performance core 4", group: .CPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Tp0V", name: "CPU performance core 5", group: .CPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Tp0Y", name: "CPU performance core 6", group: .CPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Tp0b", name: "CPU performance core 7", group: .CPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Tp0e", name: "CPU performance core 8", group: .CPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Tg0G", name: "GPU 1", group: .GPU, type: .temperature, platforms: [.m4], average: true),
Sensor(key: "Tg0H", name: "GPU 2", group: .GPU, type: .temperature, platforms: [.m4], average: true),
Sensor(key: "Tg1U", name: "GPU 1", group: .GPU, type: .temperature, platforms: [.m4Pro, .m4Max, .m4Ultra], average: true),
Sensor(key: "Tg1k", name: "GPU 2", group: .GPU, type: .temperature, platforms: [.m4Pro, .m4Max, .m4Ultra], average: true),
Sensor(key: "Tg0K", name: "GPU 3", group: .GPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Tg0L", name: "GPU 4", group: .GPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Tg0d", name: "GPU 5", group: .GPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Tg0e", name: "GPU 6", group: .GPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Tg0j", name: "GPU 7", group: .GPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Tg0k", name: "GPU 8", group: .GPU, type: .temperature, platforms: Platform.m4Gen, average: true),
Sensor(key: "Tm0p", name: "Memory Proximity 1", group: .sensor, type: .temperature, platforms: Platform.m4Gen),
Sensor(key: "Tm1p", name: "Memory Proximity 2", group: .sensor, type: .temperature, platforms: Platform.m4Gen),
Sensor(key: "Tm2p", name: "Memory Proximity 3", group: .sensor, type: .temperature, platforms: Platform.m4Gen),
// M5
Sensor(key: "Tp00", name: "CPU super core 1", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp04", name: "CPU super core 2", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp08", name: "CPU super core 3", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp0C", name: "CPU super core 4", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp0G", name: "CPU super core 5", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp0K", name: "CPU super core 6", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp0O", name: "CPU performance core 1", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp0R", name: "CPU performance core 2", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp0U", name: "CPU performance core 3", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp0X", name: "CPU performance core 4", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp0a", name: "CPU performance core 5", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp0d", name: "CPU performance core 6", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp0g", name: "CPU performance core 7", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp0j", name: "CPU performance core 8", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp0m", name: "CPU performance core 9", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp0p", name: "CPU performance core 10", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp0u", name: "CPU performance core 11", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tp0y", name: "CPU performance core 12", group: .CPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tg0U", name: "GPU 1", group: .GPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tg0X", name: "GPU 2", group: .GPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tg0d", name: "GPU 3", group: .GPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tg0g", name: "GPU 4", group: .GPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tg0j", name: "GPU 5", group: .GPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tg1Y", name: "GPU 6", group: .GPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tg1c", name: "GPU 7", group: .GPU, type: .temperature, platforms: Platform.m5Gen, average: true),
Sensor(key: "Tg1g", name: "GPU 8", group: .GPU, type: .temperature, platforms: Platform.m5Gen, average: true),
// Apple Silicon
Sensor(key: "TaLP", name: "Airflow left", group: .sensor, type: .temperature, platforms: Platform.apple),
Sensor(key: "TaRF", name: "Airflow right", group: .sensor, type: .temperature, platforms: Platform.apple),
Sensor(key: "TH0x", name: "NAND", group: .system, type: .temperature, platforms: Platform.apple),
Sensor(key: "TB1T", name: "Battery 1", group: .system, type: .temperature, platforms: Platform.apple),
Sensor(key: "TB2T", name: "Battery 2", group: .system, type: .temperature, platforms: Platform.apple),
Sensor(key: "TW0P", name: "Airport", group: .system, type: .temperature, platforms: Platform.apple),
// Voltage
Sensor(key: "VCAC", name: "CPU IA", group: .CPU, type: .voltage, platforms: Platform.all),
Sensor(key: "VCSC", name: "CPU System Agent", group: .CPU, type: .voltage, platforms: Platform.all),
Sensor(key: "VC%C", name: "CPU Core %", group: .CPU, type: .voltage, platforms: Platform.all),
Sensor(key: "VCTC", name: "GPU Intel Graphics", group: .GPU, type: .voltage, platforms: Platform.all),
Sensor(key: "VG0C", name: "GPU", group: .GPU, type: .voltage, platforms: Platform.all),
Sensor(key: "VM0R", name: "Memory", group: .system, type: .voltage, platforms: Platform.all),
Sensor(key: "Vb0R", name: "CMOS", group: .system, type: .voltage, platforms: Platform.all),
Sensor(key: "VD0R", name: "DC In", group: .sensor, type: .voltage, platforms: Platform.all),
Sensor(key: "VP0R", name: "12V rail", group: .sensor, type: .voltage, platforms: Platform.all),
Sensor(key: "Vp0C", name: "12V vcc", group: .sensor, type: .voltage, platforms: Platform.all),
Sensor(key: "VV2S", name: "3V", group: .sensor, type: .voltage, platforms: Platform.all),
Sensor(key: "VR3R", name: "3.3V", group: .sensor, type: .voltage, platforms: Platform.all),
Sensor(key: "VV1S", name: "5V", group: .sensor, type: .voltage, platforms: Platform.all),
Sensor(key: "VV9S", name: "12V", group: .sensor, type: .voltage, platforms: Platform.all),
Sensor(key: "VeES", name: "PCI 12V", group: .sensor, type: .voltage, platforms: Platform.all),
// Current
Sensor(key: "IC0R", name: "CPU High side", group: .sensor, type: .current, platforms: Platform.all),
Sensor(key: "IG0R", name: "GPU High side", group: .sensor, type: .current, platforms: Platform.all),
Sensor(key: "ID0R", name: "DC In", group: .sensor, type: .current, platforms: Platform.all),
Sensor(key: "IBAC", name: "Battery", group: .sensor, type: .current, platforms: Platform.all),
Sensor(key: "IDBR", name: "Brightness", group: .sensor, type: .current, platforms: Platform.all),
Sensor(key: "IU1R", name: "Thunderbolt Left", group: .sensor, type: .current, platforms: Platform.all),
Sensor(key: "IU2R", name: "Thunderbolt Right", group: .sensor, type: .current, platforms: Platform.all),
// Power
Sensor(key: "PC0C", name: "CPU Core", group: .CPU, type: .power, platforms: Platform.all),
Sensor(key: "PCAM", name: "CPU Core (IMON)", group: .CPU, type: .power, platforms: Platform.all),
Sensor(key: "PCPC", name: "CPU Package", group: .CPU, type: .power, platforms: Platform.all),
Sensor(key: "PCTR", name: "CPU Total", group: .CPU, type: .power, platforms: Platform.all),
Sensor(key: "PCPT", name: "CPU Package total", group: .CPU, type: .power, platforms: Platform.all),
Sensor(key: "PCPR", name: "CPU Package total (SMC)", group: .CPU, type: .power, platforms: Platform.all),
Sensor(key: "PC0R", name: "CPU Computing high side", group: .CPU, type: .power, platforms: Platform.all),
Sensor(key: "PC0G", name: "CPU GFX", group: .CPU, type: .power, platforms: Platform.all),
Sensor(key: "PCEC", name: "CPU VccEDRAM", group: .CPU, type: .power, platforms: Platform.all),
Sensor(key: "PCPG", name: "GPU Intel Graphics", group: .GPU, type: .power, platforms: Platform.all),
Sensor(key: "PG0C", name: "GPU", group: .GPU, type: .power, platforms: Platform.all),
Sensor(key: "PG0R", name: "GPU 1", group: .GPU, type: .power, platforms: Platform.all),
Sensor(key: "PG1R", name: "GPU 2", group: .GPU, type: .power, platforms: Platform.all),
Sensor(key: "PCGC", name: "Intel GPU", group: .GPU, type: .power, platforms: Platform.all),
Sensor(key: "PCGM", name: "Intel GPU (IMON)", group: .GPU, type: .power, platforms: Platform.all),
Sensor(key: "PC3C", name: "RAM", group: .sensor, type: .power, platforms: Platform.all),
Sensor(key: "PPBR", name: "Battery", group: .sensor, type: .power, platforms: Platform.all),
Sensor(key: "PDTR", name: "DC In", group: .sensor, type: .power, platforms: Platform.all),
Sensor(key: "PMTR", name: "Memory Total", group: .sensor, type: .power, platforms: Platform.all),
Sensor(key: "PSTR", name: "System Total", group: .sensor, type: .power, platforms: Platform.all),
Sensor(key: "PU1R", name: "Thunderbolt Left", group: .sensor, type: .power, platforms: Platform.all),
Sensor(key: "PU2R", name: "Thunderbolt Right", group: .sensor, type: .power, platforms: Platform.all),
Sensor(key: "PDBR", name: "Power Delivery Brightness", group: .sensor, type: .power, platforms: [.m1, .m1Pro, .m1Max, .m1Ultra, .m4, .m4Pro, .m4Max, .m4Ultra])
]
internal let HIDSensorsList: [Sensor] = [
Sensor(key: "pACC MTR Temp Sensor%", name: "CPU performance core %", group: .CPU, type: .temperature, platforms: Platform.all),
Sensor(key: "eACC MTR Temp Sensor%", name: "CPU efficiency core %", group: .CPU, type: .temperature, platforms: Platform.all),
Sensor(key: "GPU MTR Temp Sensor%", name: "GPU core %", group: .GPU, type: .temperature, platforms: Platform.all),
Sensor(key: "SOC MTR Temp Sensor%", name: "SOC core %", group: .sensor, type: .temperature, platforms: Platform.all),
Sensor(key: "ANE MTR Temp Sensor%", name: "Neural engine %", group: .sensor, type: .temperature, platforms: Platform.all),
Sensor(key: "ISP MTR Temp Sensor%", name: "Image Signal Processor %", group: .sensor, type: .temperature, platforms: Platform.all),
Sensor(key: "PMGR SOC Die Temp Sensor%", name: "Power manager die %", group: .sensor, type: .temperature, platforms: Platform.all),
Sensor(key: "PMU tdev%", name: "Power management unit dev %", group: .sensor, type: .temperature, platforms: Platform.all),
Sensor(key: "PMU tdie%", name: "Power management unit die %", group: .sensor, type: .temperature, platforms: Platform.all),
Sensor(key: "gas gauge battery", name: "Battery", group: .sensor, type: .temperature, platforms: Platform.all),
Sensor(key: "NAND CH% temp", name: "Disk %s", group: .GPU, type: .temperature, platforms: Platform.all)
]
================================================
FILE: README.md
================================================
# Stats

[](https://github.com/exelban/stats/releases)
[](https://github.com/exelban/stats/releases)
macOS system monitor in your menu bar
## Installation
### Manual
You can download the latest version [here](https://github.com/exelban/stats/releases/latest/download/Stats.dmg).
This will download a file called `Stats.dmg`. Open it and move the app to the application folder.
### Homebrew
To install it using Homebrew, open the Terminal app and type:
```bash
brew install stats
```
### Legacy version
Legacy version for older systems could be found [here](https://mac-stats.com/downloads).
## Requirements
Stats is supported on the released macOS version starting from macOS 11.15 (Big Sur).
## Features
Stats is an application that allows you to monitor your macOS system.
- CPU utilization
- GPU utilization
- Memory usage
- Disk utilization
- Network usage
- Battery level
- Fan's control (not maintained)
- Sensors information (Temperature/Voltage/Power)
- Bluetooth devices
- Multiple time zone clock
## FAQs
### How do you change the order of the menu bar icons?
macOS decides the order of the menu bar items not `Stats` - it may change after the first reboot after installing Stats.
To change the order of any menu bar icon - macOS Mojave (version 10.14) and up.
1. Hold down ⌘ (command key).
2. Drag the icon to the desired position on the menu bar.
3. Release ⌘ (command key)
### How to reduce energy impact or CPU usage of Stats?
Stats tries to be efficient as it's possible. But reading some data periodically is not a cheap task. Each module has its own "price". So, if you want to reduce energy impact from the Stats you need to disable some Stats modules. The most inefficient modules are Sensors and Bluetooth. Disabling these modules could reduce CPU usage and power efficiency by up to 50% in some cases.
### Fan control
Fan control is in legacy mode. It does not receive any updates or fixes. It's not dropped from the app just because in the old Macs it works pretty acceptable. I'm open to accepting fixed or improvements (via PR) for this feature in case someone would like to help with that. But have no option and time to provide support for this feature.
### Sensors show incorrect CPU/GPU core count
CPU/GPU sensors are simply thermal zones (sensors) on the CPU/GPU. They have no relation to the number of cores or specific cores.
For example, a CPU is typically divided into two clusters: efficiency and performance. Each cluster contains multiple temperature sensors, and Stats simply displays these sensors. However, "CPU Efficient Core 1" does not represent the temperature of a single efficient core—it only indicates one of the temperature sensors within the efficiency core cluster.
Additionally, with each new SoC, Apple changes the sensor keys. As a result, it takes time to determine which SMC values correspond to the appropriate sensors. If anyone knows how to accurately match the sensors for Apple Silicon, please contact me.
### App crash – what to do?
First, ensure that you are using the latest version of Stats. There is a high chance that a fix preventing the crash has already been released. If you are already running the latest version, check the open issues. Only if none of the existing issues address your problem should you open a new issue.
### Why my issue was closed without any response?
Most probably because it's a duplicated issue and there is an answer to the question, report, or proposition. Please use a search by closed issues to get an answer.
So, if your issue was closed without any response, most probably it already has a response.
### External API
Stats uses some external APIs, such as:
- https://api.mac-stats.com – For update checks and retrieving the public IP address
- https://api.github.com – Fallback for update checks
Both of these APIs are used to check for updates. Additionally, an external request is required to obtain the public IP address. I do not want to use any third-party providers for retrieving the public IP address, so I use my own server for this purpose.
If you have concerns about these requests, you have a few options:
- propose a PR that allows these features to work without an external server
- block both of these servers using any network filtering app (if you're reading this, you're likely using something like Little Snitch, so you can easily do this). In this case do not expect to receive any updates or see your public IP in the network module.
## Supported languages
- English
- Polski
- Українська
- Русский
- 中文 (简体) (thanks to [chenguokai](https://github.com/chenguokai), [Tai-Zhou](https://github.com/Tai-Zhou), and [Jerry](https://github.com/Jerry23011))
- Türkçe (thanks to [yusufozgul](https://github.com/yusufozgul) and [setanarut](https://github.com/setanarut))
- 한국어 (thanks to [escapeanaemia](https://github.com/escapeanaemia) and [iamhslee](https://github.com/iamhslee))
- German (thanks to [natterstefan](https://github.com/natterstefan) and [aneitel](https://github.com/aneitel))
- 中文 (繁體) (thanks to [iamch15542](https://github.com/iamch15542) and [jrthsr700tmax](https://github.com/jrthsr700tmax))
- Spanish (thanks to [jcconca](https://github.com/jcconca))
- Vietnamese (thanks to [HXD.VN](https://github.com/xuandung38))
- French (thanks to [RomainLt](https://github.com/RomainLt))
- Italian (thanks to [gmcinalli](https://github.com/gmcinalli))
- Portuguese (Brazil) (thanks to [marcelochaves95](https://github.com/marcelochaves95) and [pedroserigatto](https://github.com/pedroserigatto))
- Norwegian Bokmål (thanks to [rubjo](https://github.com/rubjo))
- 日本語 (thanks to [treastrain](https://github.com/treastrain))
- Portuguese (Portugal) (thanks to [AdamModus](https://github.com/AdamModus))
- Czech (thanks to [mpl75](https://github.com/mpl75))
- Magyar (thanks to [moriczr](https://github.com/moriczr))
- Bulgarian (thanks to [zbrox](https://github.com/zbrox))
- Romanian (thanks to [razluta](https://github.com/razluta))
- Dutch (thanks to [ngohungphuc](https://github.com/ngohungphuc))
- Hrvatski (thanks to [milotype](https://github.com/milotype))
- Danish (thanks to [casperes1996](https://github.com/casperes1996) and [aleksanderbl29](https://github.com/aleksanderbl29))
- Catalan (thanks to [davidalonso](https://github.com/davidalonso))
- Indonesian (thanks to [yooody](https://github.com/yooody))
- Hebrew (thanks to [BadSugar](https://github.com/BadSugar))
- Slovenian (thanks to [zigapovhe](https://github.com/zigapovhe))
- Greek (thanks to [sudoxcess](https://github.com/sudoxcess) and [vaionicle](https://github.com/vaionicle))
- Persian (thanks to [ShawnAlisson](https://github.com/ShawnAlisson))
- Slovenský (thanks to [martinbernat](https://github.com/martinbernat))
- Thai (thanks to [apiphoomchu](https://github.com/apiphoomchu))
- Estonian (thanks to [postylem](https://github.com/postylem))
- Hindi (thanks to [patiljignesh](https://github.com/patiljignesh))
- Finnish (thanks to [eightscrow](https://github.com/eightscrow))
You can help by adding a new language or improving the existing translation.
## License
[MIT License](https://github.com/exelban/stats/blob/master/LICENSE)
================================================
FILE: SMC/Helper/Info.plist
================================================
CFBundleIdentifier
eu.exelban.Stats.SMC.Helper
CFBundleName
eu.exelban.Stats.SMC.Helper
CFBundleShortVersionString
1.1.0
CFBundleVersion
3
CFBundleInfoDictionaryVersion
6.0
SMAuthorizedClients
anchor apple generic and identifier "eu.exelban.Stats" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = RP2S87B72W)
================================================
FILE: SMC/Helper/Launchd.plist
================================================
Label
eu.exelban.Stats.SMC.Helper
MachServices
eu.exelban.Stats.SMC.Helper
================================================
FILE: SMC/Helper/main.swift
================================================
//
// main.swift
// Helper
//
// Created by Serhiy Mytrovtsiy on 17/11/2022
// Using Swift 5.0
// Running on macOS 13.0
//
// Copyright © 2022 Serhiy Mytrovtsiy. All rights reserved.
//
import Foundation
let helper = Helper()
helper.run()
class Helper: NSObject, NSXPCListenerDelegate, HelperProtocol {
private let listener: NSXPCListener
private let smcQueue = DispatchQueue(label: "eu.exelban.Stats.SMC.Helper.smcQueue")
private var connections = [NSXPCConnection]()
private var shouldQuit = false
private var shouldQuitCheckInterval = 1.0
private var smc: String? = nil
override init() {
self.listener = NSXPCListener(machServiceName: "eu.exelban.Stats.SMC.Helper")
super.init()
self.listener.delegate = self
}
public func run() {
let args = CommandLine.arguments.dropFirst()
if !args.isEmpty && args.first == "uninstall" {
NSLog("detected uninstall command")
if let val = args.last, let pid: pid_t = Int32(val) {
while kill(pid, 0) == 0 {
usleep(50000)
}
}
self.uninstallHelper()
exit(0)
}
self.listener.resume()
while !self.shouldQuit {
RunLoop.current.run(until: Date(timeIntervalSinceNow: self.shouldQuitCheckInterval))
}
}
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
do {
let isValid = try CodesignCheck.codeSigningMatches(pid: newConnection.processIdentifier)
if !isValid {
NSLog("invalid connection, dropping")
return false
}
} catch {
NSLog("error checking code signing: \(error)")
return false
}
newConnection.exportedInterface = NSXPCInterface(with: HelperProtocol.self)
newConnection.exportedObject = self
newConnection.invalidationHandler = {
if let connectionIndex = self.connections.firstIndex(of: newConnection) {
self.connections.remove(at: connectionIndex)
}
if self.connections.isEmpty {
self.shouldQuit = true
}
}
self.connections.append(newConnection)
newConnection.resume()
return true
}
private func uninstallHelper() {
let process = Process()
process.launchPath = "/bin/launchctl"
process.qualityOfService = QualityOfService.userInitiated
process.arguments = ["unload", "/Library/LaunchDaemons/eu.exelban.Stats.SMC.Helper.plist"]
process.launch()
process.waitUntilExit()
if process.terminationStatus != .zero {
NSLog("termination code: \(process.terminationStatus)")
}
NSLog("unloaded from launchctl")
do {
try FileManager.default.removeItem(at: URL(fileURLWithPath: "/Library/LaunchDaemons/eu.exelban.Stats.SMC.Helper.plist"))
} catch let err {
NSLog("plist deletion: \(err)")
}
NSLog("property list deleted")
do {
try FileManager.default.removeItem(at: URL(fileURLWithPath: "/Library/PrivilegedHelperTools/eu.exelban.Stats.SMC.Helper"))
} catch let err {
NSLog("helper deletion: \(err)")
}
NSLog("smc helper deleted")
}
}
extension Helper {
func version(completion: (String) -> Void) {
completion(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0")
}
func setSMCPath(_ path: String) {
self.smc = path
}
func setFanMode(id: Int, mode: Int, completion: (String?) -> Void) {
smcQueue.sync {
guard let smc = self.smc else {
completion("missing smc tool")
return
}
let result = syncShell("\(smc) fan \(id) -m \(mode)")
if let error = result.error, !error.isEmpty {
NSLog("error set fan mode: \(error)")
completion(nil)
return
}
completion(result.output)
}
}
func setFanSpeed(id: Int, value: Int, completion: (String?) -> Void) {
smcQueue.sync {
guard let smc = self.smc else {
completion("missing smc tool")
return
}
let result = syncShell("\(smc) fan \(id) -v \(value)")
if let error = result.error, !error.isEmpty {
NSLog("error set fan speed: \(error)")
completion(nil)
return
}
completion(result.output)
}
}
func resetFanControl(completion: (String?) -> Void) {
smcQueue.sync {
guard let smc = self.smc else {
completion("missing smc tool")
return
}
let result = syncShell("\(smc) reset")
if let error = result.error, !error.isEmpty {
NSLog("error reset fan control: \(error)")
completion(nil)
return
}
completion(result.output)
}
}
func powermetrics(_ samplers: [String], completion: @escaping (String?) -> Void) {
let result = syncShell("powermetrics -n 1 -s \(samplers.joined(separator: ",")) --sample-rate 1000")
if let error = result.error, !error.isEmpty {
NSLog("error call powermetrics: \(error)")
completion(nil)
return
}
completion(result.output)
}
public func syncShell(_ args: String) -> (output: String?, error: String?) {
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", args]
let outputPipe = Pipe()
let errorPipe = Pipe()
defer {
outputPipe.fileHandleForReading.closeFile()
errorPipe.fileHandleForReading.closeFile()
}
task.standardOutput = outputPipe
task.standardError = errorPipe
do {
try task.run()
} catch let err {
return (nil, "syncShell: \(err.localizedDescription)")
}
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: outputData, encoding: .utf8)
let error = String(data: errorData, encoding: .utf8)
return (output, error)
}
func uninstall() {
let process = Process()
process.launchPath = "/Library/PrivilegedHelperTools/eu.exelban.Stats.SMC.Helper"
process.qualityOfService = QualityOfService.userInitiated
process.arguments = ["uninstall", String(getpid())]
process.launch()
exit(0)
}
}
// https://github.com/duanefields/VirtualKVM/blob/master/VirtualKVM/CodesignCheck.swift
let kSecCSDefaultFlags = 0
enum CodesignCheckError: Error {
case message(String)
}
struct CodesignCheck {
public static func codeSigningMatches(pid: pid_t) throws -> Bool {
return try self.codeSigningCertificatesForSelf() == self.codeSigningCertificates(forPID: pid)
}
private static func codeSigningCertificatesForSelf() throws -> [SecCertificate] {
guard let secStaticCode = try secStaticCodeSelf() else { return [] }
return try codeSigningCertificates(forStaticCode: secStaticCode)
}
private static func codeSigningCertificates(forPID pid: pid_t) throws -> [SecCertificate] {
guard let secStaticCode = try secStaticCode(forPID: pid) else { return [] }
return try codeSigningCertificates(forStaticCode: secStaticCode)
}
private static func executeSecFunction(_ secFunction: () -> (OSStatus) ) throws {
let osStatus = secFunction()
guard osStatus == errSecSuccess else {
throw CodesignCheckError.message(String(describing: SecCopyErrorMessageString(osStatus, nil)))
}
}
private static func secStaticCodeSelf() throws -> SecStaticCode? {
var secCodeSelf: SecCode?
try executeSecFunction { SecCodeCopySelf(SecCSFlags(rawValue: 0), &secCodeSelf) }
guard let secCode = secCodeSelf else {
throw CodesignCheckError.message("SecCode returned empty from SecCodeCopySelf")
}
return try secStaticCode(forSecCode: secCode)
}
private static func secStaticCode(forPID pid: pid_t) throws -> SecStaticCode? {
var secCodePID: SecCode?
try executeSecFunction { SecCodeCopyGuestWithAttributes(nil, [kSecGuestAttributePid: pid] as CFDictionary, [], &secCodePID) }
guard let secCode = secCodePID else {
throw CodesignCheckError.message("SecCode returned empty from SecCodeCopyGuestWithAttributes")
}
return try secStaticCode(forSecCode: secCode)
}
private static func secStaticCode(forSecCode secCode: SecCode) throws -> SecStaticCode? {
var secStaticCodeCopy: SecStaticCode?
try executeSecFunction { SecCodeCopyStaticCode(secCode, [], &secStaticCodeCopy) }
guard let secStaticCode = secStaticCodeCopy else {
throw CodesignCheckError.message("SecStaticCode returned empty from SecCodeCopyStaticCode")
}
return secStaticCode
}
private static func isValid(secStaticCode: SecStaticCode) throws {
try executeSecFunction { SecStaticCodeCheckValidity(secStaticCode, SecCSFlags(rawValue: kSecCSDoNotValidateResources | kSecCSCheckNestedCode), nil) }
}
private static func secCodeInfo(forStaticCode secStaticCode: SecStaticCode) throws -> [String: Any]? {
try isValid(secStaticCode: secStaticCode)
var secCodeInfoCFDict: CFDictionary?
try executeSecFunction { SecCodeCopySigningInformation(secStaticCode, SecCSFlags(rawValue: kSecCSSigningInformation), &secCodeInfoCFDict) }
guard let secCodeInfo = secCodeInfoCFDict as? [String: Any] else {
throw CodesignCheckError.message("CFDictionary returned empty from SecCodeCopySigningInformation")
}
return secCodeInfo
}
private static func codeSigningCertificates(forStaticCode secStaticCode: SecStaticCode) throws -> [SecCertificate] {
guard
let secCodeInfo = try secCodeInfo(forStaticCode: secStaticCode),
let secCertificates = secCodeInfo[kSecCodeInfoCertificates as String] as? [SecCertificate] else { return [] }
return secCertificates
}
}
================================================
FILE: SMC/Helper/protocol.swift
================================================
//
// protocol.swift
// Helper
//
// Created by Serhiy Mytrovtsiy on 17/11/2022
// Using Swift 5.0
// Running on macOS 13.0
//
// Copyright © 2022 Serhiy Mytrovtsiy. All rights reserved.
//
import Foundation
@objc public protocol HelperProtocol {
func version(completion: @escaping (String) -> Void)
func setSMCPath(_ path: String)
func setFanMode(id: Int, mode: Int, completion: @escaping (String?) -> Void)
func setFanSpeed(id: Int, value: Int, completion: @escaping (String?) -> Void)
func resetFanControl(completion: @escaping (String?) -> Void)
func powermetrics(_ samplers: [String], completion: @escaping (String?) -> Void)
func uninstall()
}
================================================
FILE: SMC/Makefile
================================================
.PHONY: build
.SILENT: build
build:
rm -rf ./build
xcodebuild \
-project ../Stats.xcodeproj \
-scheme SMC \
-destination 'platform=OS X,arch=x86_64' \
-configuration Release archive \
-archivePath ./build/smc.xcarchive \
SKIP_INSTALL=NO
cp ./build/smc.xcarchive/Products/usr/local/bin/smc ./
rm -rf ./build
================================================
FILE: SMC/main.swift
================================================
//
// main.swift
// SMC
//
// Created by Serhiy Mytrovtsiy on 25/05/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
import Foundation
enum CMDType: String {
case list
case set
case fan
case fans
case reset
case help
case unknown
init(value: String) {
switch value {
case "list": self = .list
case "set": self = .set
case "fan": self = .fan
case "fans": self = .fans
case "reset": self = .reset
case "help": self = .help
default: self = .unknown
}
}
}
enum FlagsType: String {
case temperature = "T"
case voltage = "V"
case power = "P"
case fans = "F"
case all
init(value: String) {
switch value {
case "-t": self = .temperature
case "-v": self = .voltage
case "-p": self = .power
case "-f": self = .fans
default: self = .all
}
}
}
func main() {
var args = CommandLine.arguments.dropFirst()
let cmd = CMDType(value: args.first ?? "")
args = args.dropFirst()
switch cmd {
case .list:
var keys = SMC.shared.getAllKeys()
args.forEach { (arg: String) in
let flag = FlagsType(value: arg)
if flag != .all {
keys = keys.filter{ $0.hasPrefix(flag.rawValue)}
}
}
print("[INFO]: found \(keys.count) keys\n")
keys.forEach { (key: String) in
let value = SMC.shared.getValue(key)
print("[\(key)] ", value ?? 0)
}
case .set:
guard let keyIndex = args.firstIndex(where: { $0 == "-k" }),
let valueIndex = args.firstIndex(where: { $0 == "-v" }),
args.indices.contains(keyIndex+1),
args.indices.contains(valueIndex+1) else {
return
}
let key = args[keyIndex+1]
if key.count != 4 {
print("[ERROR]: key must contain 4 characters!")
return
}
guard let value = Int(args[valueIndex+1]) else {
print("[ERROR]: wrong value passed!")
return
}
let result = SMC.shared.write(key, value)
if result != kIOReturnSuccess {
print("[ERROR]: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
print("[INFO]: set \(value) on \(key)")
case .fan:
guard let idString = args.first, let id = Int(idString) else {
print("[ERROR]: missing fan id")
return
}
var help: Bool = true
if let index = args.firstIndex(where: { $0 == "-v" }), args.indices.contains(index+1), let value = Int(args[index+1]) {
SMC.shared.setFanSpeed(id, speed: value)
help = false
}
if let index = args.firstIndex(where: { $0 == "-m" }), args.indices.contains(index+1),
let raw = Int(args[index+1]), let mode = FanMode.init(rawValue: raw) {
SMC.shared.setFanMode(id, mode: mode)
help = false
}
guard help else { return }
print("Available Flags:")
print(" -m change the fan mode: 0 - automatic, 1 - manual")
print(" -v change the fan speed")
case .fans:
guard let count = SMC.shared.getValue("FNum") else {
print("FNum not found")
return
}
print("Number of fans: \(count)\n")
for i in 0.. String {
return String(describing: UnicodeScalar(self >> 24 & 0xff)!) +
String(describing: UnicodeScalar(self >> 16 & 0xff)!) +
String(describing: UnicodeScalar(self >> 8 & 0xff)!) +
String(describing: UnicodeScalar(self & 0xff)!)
}
}
extension UInt16 {
init(bytes: (UInt8, UInt8)) {
self = UInt16(bytes.0) << 8 | UInt16(bytes.1)
}
}
extension UInt32 {
init(bytes: (UInt8, UInt8, UInt8, UInt8)) {
self = UInt32(bytes.0) << 24 | UInt32(bytes.1) << 16 | UInt32(bytes.2) << 8 | UInt32(bytes.3)
}
}
extension Int {
init(fromFPE2 bytes: (UInt8, UInt8)) {
self = (Int(bytes.0) << 6) + (Int(bytes.1) >> 2)
}
}
extension Float {
init?(_ bytes: [UInt8]) {
self = bytes.withUnsafeBytes {
return $0.load(fromByteOffset: 0, as: Self.self)
}
}
var bytes: [UInt8] {
withUnsafeBytes(of: self, Array.init)
}
}
public class SMC {
public static let shared = SMC()
private var conn: io_connect_t = 0
public init() {
var result: kern_return_t
var iterator: io_iterator_t = 0
let device: io_object_t
let matchingDictionary: CFMutableDictionary = IOServiceMatching("AppleSMC")
result = IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDictionary, &iterator)
if result != kIOReturnSuccess {
print("Error IOServiceGetMatchingServices(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
device = IOIteratorNext(iterator)
IOObjectRelease(iterator)
if device == 0 {
print("Error IOIteratorNext(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
result = IOServiceOpen(device, mach_task_self_, 0, &conn)
IOObjectRelease(device)
if result != kIOReturnSuccess {
print("Error IOServiceOpen(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
}
deinit {
let result = self.close()
if result != kIOReturnSuccess {
print("error close smc connection: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
}
}
public func close() -> kern_return_t {
return IOServiceClose(conn)
}
public func getValue(_ key: String) -> Double? {
var result: kern_return_t = 0
var val: SMCVal_t = SMCVal_t(key)
result = read(&val)
if result != kIOReturnSuccess {
print("Error read(\(key)): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
}
if val.dataSize > 0 {
if val.bytes.first(where: { $0 != 0 }) == nil && val.key != "FS! " && val.key != "F0Md" && val.key != "F1Md" {
return nil
}
switch val.dataType {
case SMCDataType.UI8.rawValue:
return Double(val.bytes[0])
case SMCDataType.UI16.rawValue:
return Double(UInt16(bytes: (val.bytes[0], val.bytes[1])))
case SMCDataType.UI32.rawValue:
return Double(UInt32(bytes: (val.bytes[0], val.bytes[1], val.bytes[2], val.bytes[3])))
case SMCDataType.SP1E.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 16384)
case SMCDataType.SP3C.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 4096)
case SMCDataType.SP4B.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 2048)
case SMCDataType.SP5A.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 1024)
case SMCDataType.SP69.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 512)
case SMCDataType.SP78.rawValue:
let intValue: Double = Double(Int(val.bytes[0]) * 256 + Int(val.bytes[1]))
return Double(intValue / 256)
case SMCDataType.SP87.rawValue:
let intValue: Double = Double(Int(val.bytes[0]) * 256 + Int(val.bytes[1]))
return Double(intValue / 128)
case SMCDataType.SP96.rawValue:
let intValue: Double = Double(Int(val.bytes[0]) * 256 + Int(val.bytes[1]))
return Double(intValue / 64)
case SMCDataType.SPA5.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 32)
case SMCDataType.SPB4.rawValue:
let intValue: Double = Double(Int(val.bytes[0]) * 256 + Int(val.bytes[1]))
return Double(intValue / 16)
case SMCDataType.SPF0.rawValue:
let intValue: Double = Double(Int(val.bytes[0]) * 256 + Int(val.bytes[1]))
return intValue
case SMCDataType.FLT.rawValue:
let value: Float? = Float(val.bytes)
if value != nil {
return Double(value!)
}
return nil
case SMCDataType.FPE2.rawValue:
return Double(Int(fromFPE2: (val.bytes[0], val.bytes[1])))
default:
return nil
}
}
return nil
}
public func getStringValue(_ key: String) -> String? {
var result: kern_return_t = 0
var val: SMCVal_t = SMCVal_t(key)
result = read(&val)
if result != kIOReturnSuccess {
print("Error read(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
}
if val.dataSize > 0 {
if val.bytes.first(where: { $0 != 0}) == nil {
return nil
}
switch val.dataType {
case SMCDataType.FDS.rawValue:
let c1 = String(UnicodeScalar(val.bytes[4]))
let c2 = String(UnicodeScalar(val.bytes[5]))
let c3 = String(UnicodeScalar(val.bytes[6]))
let c4 = String(UnicodeScalar(val.bytes[7]))
let c5 = String(UnicodeScalar(val.bytes[8]))
let c6 = String(UnicodeScalar(val.bytes[9]))
let c7 = String(UnicodeScalar(val.bytes[10]))
let c8 = String(UnicodeScalar(val.bytes[11]))
let c9 = String(UnicodeScalar(val.bytes[12]))
let c10 = String(UnicodeScalar(val.bytes[13]))
let c11 = String(UnicodeScalar(val.bytes[14]))
let c12 = String(UnicodeScalar(val.bytes[15]))
return (c1 + c2 + c3 + c4 + c5 + c6 + c7 + c8 + c9 + c10 + c11 + c12).trimmingCharacters(in: .whitespaces)
default:
print("unsupported data type \(val.dataType) for key: \(key)")
return nil
}
}
return nil
}
public func getAllKeys() -> [String] {
var list: [String] = []
let keysNum: Double? = self.getValue("#KEY")
if keysNum == nil {
print("ERROR no keys count found")
return list
}
var result: kern_return_t = 0
var input: SMCKeyData_t = SMCKeyData_t()
var output: SMCKeyData_t = SMCKeyData_t()
for i in 0...Int(keysNum!) {
input = SMCKeyData_t()
output = SMCKeyData_t()
input.data8 = SMCKeys.readIndex.rawValue
input.data32 = UInt32(i)
result = call(SMCKeys.kernelIndex.rawValue, input: &input, output: &output)
if result != kIOReturnSuccess {
continue
}
list.append(output.key.toString())
}
return list
}
public func write(_ key: String, _ newValue: Int) -> kern_return_t {
var value = SMCVal_t(key)
value.dataSize = 2
value.bytes = [UInt8(newValue >> 6), UInt8((newValue << 2) ^ ((newValue >> 6) << 8)), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0)]
return write(value)
}
// MARK: - fans
public func setFanMode(_ id: Int, mode: FanMode) {
#if arch(arm64)
if mode == .forced {
if !unlockFanControl(fanId: id) { return }
} else {
let modeKey = "F\(id)Md"
let targetKey = "F\(id)Tg"
if self.getValue(modeKey) != nil {
var modeVal = SMCVal_t(modeKey)
let readResult = read(&modeVal)
guard readResult == kIOReturnSuccess else {
print(smcError("read", key: modeKey, result: readResult))
return
}
if modeVal.bytes[0] != 0 {
modeVal.bytes[0] = 0
if !writeWithRetry(modeVal) { return }
}
}
var targetValue = SMCVal_t(targetKey)
let result = read(&targetValue)
guard result == kIOReturnSuccess else {
print(smcError("read", key: targetKey, result: result))
return
}
let bytes = Float(0).bytes
targetValue.bytes[0] = bytes[0]
targetValue.bytes[1] = bytes[1]
targetValue.bytes[2] = bytes[2]
targetValue.bytes[3] = bytes[3]
if !writeWithRetry(targetValue) { return }
}
#else
// Intel
if self.getValue("F\(id)Md") != nil {
var result: kern_return_t = 0
var value = SMCVal_t("F\(id)Md")
result = read(&value)
if result != kIOReturnSuccess {
print("Error read fan mode: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
value.bytes = [UInt8(mode.rawValue), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0)]
result = write(value)
if result != kIOReturnSuccess {
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
}
let fansMode = Int(self.getValue("FS! ") ?? 0)
var newMode: UInt8 = 0
if fansMode == 0 && id == 0 && mode == .forced {
newMode = 1
} else if fansMode == 0 && id == 1 && mode == .forced {
newMode = 2
} else if fansMode == 1 && id == 0 && mode == .automatic {
newMode = 0
} else if fansMode == 1 && id == 1 && mode == .forced {
newMode = 3
} else if fansMode == 2 && id == 1 && mode == .automatic {
newMode = 0
} else if fansMode == 2 && id == 0 && mode == .forced {
newMode = 3
} else if fansMode == 3 && id == 0 && mode == .automatic {
newMode = 2
} else if fansMode == 3 && id == 1 && mode == .automatic {
newMode = 1
}
if fansMode == newMode {
return
}
var result: kern_return_t = 0
var value = SMCVal_t("FS! ")
result = read(&value)
if result != kIOReturnSuccess {
print("Error read fan mode: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
value.bytes = [0, newMode, UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0)]
result = write(value)
if result != kIOReturnSuccess {
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
#endif
}
public func setFanSpeed(_ id: Int, speed: Int) {
if let maxSpeed = self.getValue("F\(id)Mx"),
speed > Int(maxSpeed) {
return setFanSpeed(id, speed: Int(maxSpeed))
}
#if arch(arm64)
var modeVal = SMCVal_t("F\(id)Md")
let modeResult = read(&modeVal)
guard modeResult == kIOReturnSuccess else {
print("Error read fan mode: " + (String(cString: mach_error_string(modeResult), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
if modeVal.bytes[0] != 1 {
if !unlockFanControl(fanId: id) { return }
}
#endif
var result: kern_return_t = 0
var value = SMCVal_t("F\(id)Tg")
result = read(&value)
if result != kIOReturnSuccess {
print("Error read fan value: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
if value.dataType == "flt " {
let bytes = Float(speed).bytes
value.bytes[0] = bytes[0]
value.bytes[1] = bytes[1]
value.bytes[2] = bytes[2]
value.bytes[3] = bytes[3]
} else if value.dataType == "fpe2" {
value.bytes[0] = UInt8(speed >> 6)
value.bytes[1] = UInt8((speed << 2) ^ ((speed >> 6) << 8))
value.bytes[2] = UInt8(0)
value.bytes[3] = UInt8(0)
}
#if arch(arm64)
if !writeWithRetry(value) {
return
}
#else
result = write(value)
if result != kIOReturnSuccess {
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
#endif
}
public func resetFans() {
var value = SMCVal_t("FS! ")
value.dataSize = 2
let result = write(value)
if result != kIOReturnSuccess {
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
}
}
// MARK: - Apple Silicon Fan Control
#if arch(arm64)
/// Format SMC error for logging with context
private func smcError(_ operation: String, key: String, result: kern_return_t) -> String {
let errorDesc = String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"
return "[\(key)] \(operation) failed: \(errorDesc) (0x\(String(result, radix: 16)))"
}
private func writeWithRetry(_ value: SMCVal_t, maxAttempts: Int = 10, delayMicros: UInt32 = 50_000) -> Bool {
let mutableValue = value
var lastResult: kern_return_t = kIOReturnSuccess
for attempt in 0.. Bool {
var ftstCheck = SMCVal_t("Ftst")
let ftstResult = read(&ftstCheck)
guard ftstResult == kIOReturnSuccess else {
print(smcError("read", key: "Ftst", result: ftstResult))
return false
}
let ftstActive = ftstCheck.bytes[0] == 1
if ftstActive {
return retryModeWrite(fanId: fanId, maxAttempts: 20)
}
// Try direct write first (works on M1 without Ftst)
let modeKey = "F\(fanId)Md"
var modeVal = SMCVal_t(modeKey)
let modeRead = read(&modeVal)
guard modeRead == kIOReturnSuccess else {
print(smcError("read", key: modeKey, result: modeRead))
return false
}
modeVal.bytes[0] = 1
if write(modeVal) == kIOReturnSuccess {
return true
}
// Direct failed; fall back to Ftst unlock
var ftstVal = SMCVal_t("Ftst")
let ftstRead = read(&ftstVal)
guard ftstRead == kIOReturnSuccess else {
print(smcError("read", key: "Ftst", result: ftstRead))
return false
}
ftstVal.bytes[0] = 1
if !writeWithRetry(ftstVal, maxAttempts: 100) {
return false
}
// Wait for thermalmonitord to yield control
usleep(3_000_000)
return retryModeWrite(fanId: fanId, maxAttempts: 300)
}
private func retryModeWrite(fanId: Int, maxAttempts: Int) -> Bool {
let modeKey = "F\(fanId)Md"
var modeVal = SMCVal_t(modeKey)
let result = read(&modeVal)
guard result == kIOReturnSuccess else {
print(smcError("read", key: modeKey, result: result))
return false
}
modeVal.bytes[0] = 1
return writeWithRetry(modeVal, maxAttempts: maxAttempts, delayMicros: 100_000)
}
public func resetFanControl() -> Bool {
var value = SMCVal_t("Ftst")
let result = read(&value)
guard result == kIOReturnSuccess else {
print(smcError("read", key: "Ftst", result: result))
return false
}
if value.bytes[0] == 0 { return true }
value.bytes[0] = 0
return writeWithRetry(value)
}
#endif
// MARK: - internal functions
private func read(_ value: UnsafeMutablePointer) -> kern_return_t {
var result: kern_return_t = 0
var input = SMCKeyData_t()
var output = SMCKeyData_t()
input.key = FourCharCode(fromString: value.pointee.key)
input.data8 = SMCKeys.readKeyInfo.rawValue
result = call(SMCKeys.kernelIndex.rawValue, input: &input, output: &output)
if result != kIOReturnSuccess {
return result
}
value.pointee.dataSize = UInt32(output.keyInfo.dataSize)
value.pointee.dataType = output.keyInfo.dataType.toString()
input.keyInfo.dataSize = output.keyInfo.dataSize
input.data8 = SMCKeys.readBytes.rawValue
result = call(SMCKeys.kernelIndex.rawValue, input: &input, output: &output)
if result != kIOReturnSuccess {
return result
}
memcpy(&value.pointee.bytes, &output.bytes, Int(value.pointee.dataSize))
return kIOReturnSuccess
}
private func write(_ value: SMCVal_t) -> kern_return_t {
var input = SMCKeyData_t()
var output = SMCKeyData_t()
input.key = FourCharCode(fromString: value.key)
input.data8 = SMCKeys.writeBytes.rawValue
input.keyInfo.dataSize = IOByteCount32(value.dataSize)
input.bytes = (value.bytes[0], value.bytes[1], value.bytes[2], value.bytes[3], value.bytes[4], value.bytes[5],
value.bytes[6], value.bytes[7], value.bytes[8], value.bytes[9], value.bytes[10], value.bytes[11],
value.bytes[12], value.bytes[13], value.bytes[14], value.bytes[15], value.bytes[16], value.bytes[17],
value.bytes[18], value.bytes[19], value.bytes[20], value.bytes[21], value.bytes[22], value.bytes[23],
value.bytes[24], value.bytes[25], value.bytes[26], value.bytes[27], value.bytes[28], value.bytes[29],
value.bytes[30], value.bytes[31])
let result = self.call(SMCKeys.kernelIndex.rawValue, input: &input, output: &output)
if result != kIOReturnSuccess {
return result
}
// IOKit can return kIOReturnSuccess but SMC firmware may still reject the write.
// Check SMC-level result code (0x00 = success, non-zero = error)
if output.result != 0x00 {
return kIOReturnError
}
return kIOReturnSuccess
}
private func call(_ index: UInt8, input: inout SMCKeyData_t, output: inout SMCKeyData_t) -> kern_return_t {
let inputSize = MemoryLayout.stride
var outputSize = MemoryLayout.stride
return IOConnectCallStructMethod(conn, UInt32(index), &input, inputSize, &output, &outputSize)
}
}
================================================
FILE: Stats/AppDelegate.swift
================================================
//
// AppDelegate.swift
// Stats
//
// Created by Serhiy Mytrovtsiy on 28.05.2019.
// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
import UserNotifications
import CPU
import RAM
import Disk
import Net
import Battery
import Sensors
import GPU
import Bluetooth
import Clock
let updater = Updater(github: "exelban/stats", url: "https://api.mac-stats.com/release/latest")
var modules: [Module] = [
CPU(),
GPU(),
RAM(),
Disk(),
Sensors(),
Network(),
Battery(),
Bluetooth(),
Clock()
]
@main
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
internal let settingsWindow: SettingsWindow = SettingsWindow()
internal let updateWindow: UpdateWindow = UpdateWindow()
internal let setupWindow: SetupWindow = SetupWindow()
internal let supportWindow: SupportWindow = SupportWindow()
internal let updateActivity = NSBackgroundActivityScheduler(identifier: "eu.exelban.Stats.updateCheck")
internal let supportActivity = NSBackgroundActivityScheduler(identifier: "eu.exelban.Stats.support")
internal var clickInNotification: Bool = false
internal var menuBarItem: NSStatusItem? = nil
internal var combinedView: CombinedView = CombinedView()
internal var pauseState: Bool {
Store.shared.bool(key: "pause", defaultValue: false)
}
private var startTS: Date?
static func main() {
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.run()
}
func applicationDidFinishLaunching(_ aNotification: Notification) {
let startingPoint = Date()
self.parseArguments()
self.parseVersion()
SMCHelper.shared.checkForUpdate()
self.setup {
modules.reversed().forEach{ $0.mount() }
self.settingsWindow.setModules()
}
self.defaultValues()
self.icon()
NotificationCenter.default.addObserver(self, selector: #selector(listenForAppPause), name: .pause, object: nil)
NSEvent.addGlobalMonitorForEvents(matching: [.keyDown, .flagsChanged]) { [weak self] event in
self?.handleKeyEvent(event)
}
NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .flagsChanged]) { [weak self] event in
self?.handleKeyEvent(event)
return event
}
info("Stats started in \((startingPoint.timeIntervalSinceNow * -1).rounded(toPlaces: 4)) seconds")
self.startTS = Date()
}
func applicationWillTerminate(_ aNotification: Notification) {
modules.forEach{ $0.terminate() }
Remote.shared.terminate()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
if self.clickInNotification {
self.clickInNotification = false
return true
}
guard let startTS = self.startTS, Date().timeIntervalSince(startTS) > 2 else { return false }
if flag {
self.settingsWindow.makeKeyAndOrderFront(self)
} else {
self.settingsWindow.setIsVisible(true)
}
return true
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
self.clickInNotification = true
if let uri = response.notification.request.content.userInfo["url"] as? String {
debug("Downloading new version of app...")
if let url = URL(string: uri) {
updater.download(url, completion: { path in
updater.install(path: path) { error in
if let error {
showAlert("Error update Stats", error, .critical)
}
}
})
}
}
completionHandler()
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"filename" : "icon_16x16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "icon_16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "icon_32x32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "icon_32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "icon_128x128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "icon_256x256 1.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "icon_256x256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "icon_512x512 1.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "icon_512x512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "icon_512x512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/ac_unit.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "outline_ac_unit_black_18pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "outline_ac_unit_black_18pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "outline_ac_unit_black_18pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/apps.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "baseline_apps_white_24pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "baseline_apps_white_24pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "baseline_apps_white_24pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/bug.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "baseline_bug_report_white_24pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "baseline_bug_report_white_24pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "baseline_bug_report_white_24pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/devices/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/devices/imac.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "imac.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/devices/imacPro.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "imacPro.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/devices/macMini.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "macMini.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/devices/macMini2020.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "macMini2020.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/devices/macMini2024.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "macMini2024.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/devices/macPro.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "macPro.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/devices/macPro2019.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "mac pro 2019.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/devices/macStudio.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "macStudio.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/devices/macbookAir.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "macbookAir.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/devices/macbookAir4thGen.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "macbookAir.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/devices/macbookNeo.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "macbookNeo.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/devices/macbookPro.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "macbookPro.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/devices/macbookPro5thGen.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "macbookPro.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/donate.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "donate@1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "donate@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "donate@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/high-battery.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "high-battery.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/low-battery.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "low-battery.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/pause.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "outline_pause_white_24pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "outline_pause_white_24pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "outline_pause_white_24pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/power.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "baseline_power_settings_new_white_24pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "baseline_power_settings_new_white_24pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "baseline_power_settings_new_white_24pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/record.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "baseline_radio_button_checked_black_20pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "baseline_radio_button_checked_black_20pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "baseline_radio_button_checked_black_20pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/resume.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "baseline_play_arrow_white_24pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "baseline_play_arrow_white_24pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "baseline_play_arrow_white_24pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/settings.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "baseline_settings_white_24pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "baseline_settings_white_24pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "baseline_settings_white_24pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/stop.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "baseline_stop_circle_black_20pt_1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "baseline_stop_circle_black_20pt_2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "baseline_stop_circle_black_20pt_3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/support/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/support/github.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "github.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/support/ko-fi.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "ko-fi.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/support/patreon.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "patreon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Assets.xcassets/support/paypal.imageset/Contents.json
================================================
{
"images" : [
{
"filename" : "paypal.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Stats/Supporting Files/Info.plist
================================================
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
APPL
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
758
Description
Simple macOS system monitor in your menu bar
LSApplicationCategoryType
public.app-category.utilities
LSMinimumSystemVersion
$(MACOSX_DEPLOYMENT_TARGET)
LSUIElement
NSAppTransportSecurity
NSAllowsArbitraryLoads
NSBluetoothAlwaysUsageDescription
This permission allows obtaining battery level of Bluetooth devices
NSHumanReadableCopyright
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
NSPrincipalClass
NSApplication
NSUserNotificationAlertStyle
alert
SMPrivilegedExecutables
eu.exelban.Stats.SMC.Helper
anchor apple generic and identifier "eu.exelban.Stats.SMC.Helper" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = RP2S87B72W)
TeamId
RP2S87B72W
================================================
FILE: Stats/Supporting Files/Stats/en.xcloc/Localized Contents/en.xliff
================================================
LaunchAtLogin
LaunchAtLogin
Bundle name
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright (human-readable)
ModuleKit
ModuleKit
Bundle name
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright (human-readable)
Battery
Battery
Bundle name
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright (human-readable)
CPU
CPU
Bundle name
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright (human-readable)
Disk
Disk
Bundle name
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright (human-readable)
GPU
GPU
Bundle name
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright (human-readable)
Memory
Memory
Bundle name
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright (human-readable)
Net
Net
Bundle name
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright (human-readable)
Sensors
Sensors
Bundle name
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright (human-readable)
Stats
Stats
Bundle name
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright (human-readable)
StatsKit
StatsKit
Bundle name
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
Copyright (human-readable)
================================================
FILE: Stats/Supporting Files/Stats/en.xcloc/Source Contents/LaunchAtLogin/en.lproj/InfoPlist.strings
================================================
/* Bundle name */
"CFBundleName" = "LaunchAtLogin";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.";
================================================
FILE: Stats/Supporting Files/Stats/en.xcloc/Source Contents/ModuleKit/Supporting Files/en.lproj/InfoPlist.strings
================================================
/* Bundle name */
"CFBundleName" = "ModuleKit";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.";
================================================
FILE: Stats/Supporting Files/Stats/en.xcloc/Source Contents/Modules/Battery/en.lproj/InfoPlist.strings
================================================
/* Bundle name */
"CFBundleName" = "Battery";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.";
================================================
FILE: Stats/Supporting Files/Stats/en.xcloc/Source Contents/Modules/CPU/en.lproj/InfoPlist.strings
================================================
/* Bundle name */
"CFBundleName" = "CPU";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.";
================================================
FILE: Stats/Supporting Files/Stats/en.xcloc/Source Contents/Modules/Disk/en.lproj/InfoPlist.strings
================================================
/* Bundle name */
"CFBundleName" = "Disk";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.";
================================================
FILE: Stats/Supporting Files/Stats/en.xcloc/Source Contents/Modules/GPU/en.lproj/InfoPlist.strings
================================================
/* Bundle name */
"CFBundleName" = "GPU";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.";
================================================
FILE: Stats/Supporting Files/Stats/en.xcloc/Source Contents/Modules/Memory/en.lproj/InfoPlist.strings
================================================
/* Bundle name */
"CFBundleName" = "Memory";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.";
================================================
FILE: Stats/Supporting Files/Stats/en.xcloc/Source Contents/Modules/Net/en.lproj/InfoPlist.strings
================================================
/* Bundle name */
"CFBundleName" = "Net";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.";
================================================
FILE: Stats/Supporting Files/Stats/en.xcloc/Source Contents/Modules/Sensors/en.lproj/InfoPlist.strings
================================================
/* Bundle name */
"CFBundleName" = "Sensors";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.";
================================================
FILE: Stats/Supporting Files/Stats/en.xcloc/Source Contents/Stats/Supporting Files/en.lproj/InfoPlist.strings
================================================
/* Bundle name */
"CFBundleName" = "Stats";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.";
================================================
FILE: Stats/Supporting Files/Stats/en.xcloc/Source Contents/Stats/Supporting Files/en.lproj/Localizable.strings
================================================
/*
Localizable.strings
Stats
Created by Samuel Grant on 27/08/2020.
Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
*/
================================================
FILE: Stats/Supporting Files/Stats/en.xcloc/Source Contents/StatsKit/en.lproj/InfoPlist.strings
================================================
/* Bundle name */
"CFBundleName" = "StatsKit";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.";
================================================
FILE: Stats/Supporting Files/Stats/en.xcloc/contents.json
================================================
{
"developmentRegion" : "en",
"targetLocale" : "en",
"toolInfo" : {
"toolBuildNumber" : "11E708",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "11.6"
},
"version" : "1.0"
}
================================================
FILE: Stats/Supporting Files/Stats.entitlements
================================================
com.apple.security.cs.disable-library-validation
com.apple.security.application-groups
$(TeamIdentifierPrefix)eu.exelban.Stats.widgets
================================================
FILE: Stats/Supporting Files/ar.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by postylem on 2023/07/26.
// Using Swift 5.0.
// Running on macOS 13.4.
//
// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "المعالج";
"Open CPU settings" = "إفتح إعدادات المعالج";
"GPU" = "كرت الشاشة";
"Open GPU settings" = "إفتح إعدادات كرت الشاشة";
"RAM" = "الرام";
"Open RAM settings" = "إفتح إعدادات الرام";
"Disk" = "القرص";
"Open Disk settings" = "فتح إعدادات القرص";
"Sensors" = "الحساسات";
"Open Sensors settings" = "فتح إعدادات الحساسات";
"Network" = "الشبكة";
"Open Network settings" = "فتح إعدادات الشبكة";
"Battery" = "البطارية";
"Open Battery settings" = "فتح إعدادات البطارية";
"Bluetooth" = "بلوتوث";
"Open Bluetooth settings" = "فتح إعدادات البلوتوث";
"Clock" = "الساعة";
"Open Clock settings" = "فتح إعدادات الساعة";
// Words
"Unknown" = "غير معروف";
"Version" = "الإصدار";
"Processor" = "المعالج";
"Memory" = "الرام";
"Graphics" = "الرسومات";
"Close" = "إغلاق";
"Download" = "تحميل";
"Install" = "تثبيت";
"Cancel" = "إلغاء";
"Unavailable" = "غير متوفر";
"Yes" = "نعم";
"No" = "لا";
"Automatic" = "تلقائي";
"Manual" = "يدوي";
"None" = "لا شيء";
"Dots" = "نقاط";
"Arrows" = "سهم";
"Characters" = "حروف";
"Short" = "قصير";
"Long" = "طويل";
"Statistics" = "إحصائيات";
"Max" = "الأقصى";
"Min" = "الأدنى";
"Reset" = "إعادة";
"Alignment" = "المحاذاة";
"Left alignment" = "يسار";
"Center alignment" = "وسط";
"Right alignment" = "يمين";
"Dashboard" = "لوحة التحكم";
"Enabled" = "مفعّل";
"Disabled" = "معطل";
"Silent" = "صامت";
"Units" = "وحدات";
"Fans" = "مراوح";
"Scaling" = "تحجيم";
"Linear" = "خطي";
"Square" = "مربع";
"Cube" = "مكعب";
"Logarithmic" = "لوغاريتمي";
"Fixed scale" = "مقاس ثابت";
"Cores" = "نوى";
"Settings" = "الإعدادات";
"Name" = "الاسم";
"Format" = "التنسيق";
"Turn off" = "إيقاف";
"Normal" = "طبيعي"; // translategemma:4b
"Warning" = "تحذير";
"Critical" = "حرج";
"Usage" = "الاستخدام";
"2 minutes" = "2 دقائق";
"3 minutes" = "3 دقائق";
"10 minutes" = "10 دقائق";
"Import" = "استيراد";
"Export" = "تصدير";
"Separator" = "فاصل"; // translategemma:4b
"Read" = "اقرأ"; // translategemma:4b
"Write" = "اكتب"; // translategemma:4b
"Frequency" = "التردد"; // translategemma:4b
"Save" = "حفظ"; // translategemma:4b
"Run" = "تشغيل"; // translategemma:4b
"Stop" = "توقف"; // translategemma:4b
"Uninstall" = "إلغاء التثبيت"; // translategemma:4b
"1 sec" = "1 ثانية"; // translategemma:4b
"2 sec" = "2 ثانية"; // translategemma:4b
"3 sec" = "3 ثانية"; // translategemma:4b
"5 sec" = "5 ثانية"; // translategemma:4b
"10 sec" = "10 ثانية"; // translategemma:4b
"15 sec" = "15 ثانية"; // translategemma:4b
"30 sec" = "30 ثانية"; // translategemma:4b
"60 sec" = "60 ثانية"; // translategemma:4b
// Setup
"Stats Setup" = "إعدادات الإحصاءات";
"Previous" = "السابق";
"Previous page" = "الصفحة السابقة";
"Next" = "التالي";
"Next page" = "الصفحة التالية";
"Finish" = "إنهاء";
"Finish setup" = "إنهاء الإعداد";
"Welcome to Stats" = "مرحبًا بك في الإحصاءات";
"welcome_message" = "شكرًا لاستخدام الإحصاءات، أداة مراقبة نظام macOS المجانية ومفتوحة المصدر لشريط القوائم الخاص بك.";
"Start the application automatically when starting your Mac" = "بدء تشغيل التطبيق تلقائيًا عند بدء تشغيل Mac الخاص بك";
"Do not start the application automatically when starting your Mac" = "عدم بدء تشغيل التطبيق تلقائيًا عند بدء تشغيل Mac الخاص بك";
"Do everything silently in the background (recommended)" = "قم بعمل كل شيء بصمت في الخلفية (موصى به)";
"Check for a new version on startup" = "التحقق من وجود إصدار جديد عند التشغيل";
"Check for a new version every day (once a day)" = "التحقق من وجود إصدار جديد كل يوم (مرة واحدة في اليوم)";
"Check for a new version every week (once a week)" = "التحقق من وجود إصدار جديد كل أسبوع (مرة واحدة في الأسبوع)";
"Check for a new version every month (once a month)" = "التحقق من وجود إصدار جديد كل شهر (مرة واحدة في الشهر)";
"Never check for updates (not recommended)" = "عدم التحقق من وجود تحديثات (غير موصى به)";
"Anonymous telemetry for better development decisions" = "معلومات تشخيصية مجهولة لاتخاذ قرارات تطوير أفضل";
"Share anonymous telemetry data" = "مشاركة بيانات التشخيص المجهولة";
"Do not share anonymous telemetry data" = "عدم مشاركة بيانات التشخيص المجهولة";
"The configuration is completed" = "تم الانتهاء من التكوين";
"finish_setup_message" = "كل شيء مُعدّ، الإحصاءات هي أداة مفتوحة المصدر، مجانية وستظل دائمًا كذلك. إذا كنت تستمتع بها يمكنك دعم المشروع، دائمًا محل تقدير!";
// Alerts
"New version available" = "إصدار جديد متاح";
"Click to install the new version of Stats" = "انقر لتثبيت الإصدار الجديد من الإحصاءات";
"Successfully updated" = "تم التحديث بنجاح";
"Stats was updated to v" = "تم تحديث الإحصاءات إلى الإصدار v%0";
"Reset settings text" = "سيتم إعادة ضبط جميع إعدادات التطبيق وسيتم إعادة تشغيل التطبيق. هل تريد المتابعة؟";
"Support text" = "شكرًا لك على استخدامك ل Stats! \n\nيستغرق الحفاظ على هذا المشروع مفتوح المصدر وتحسينه وقتًا وموارد. دعمكم يساعدنا على الاستمرار في توفير تطبيق مجاني وموثوق للجميع. كل جزء صغير يساعدنا!";
// Settings
"Open Activity Monitor" = "فتح مراقب النشاط";
"Report a bug" = "الإبلاغ عن خطأ";
"Support the application" = "دعم التطبيق";
"Close application" = "إغلاق التطبيق";
"Open application settings" = "فتح إعدادات التطبيق";
"Open dashboard" = "فتح لوحة التحكم";
"No notifications available in this module" = "لا توجد إشعارات متاحة في هذه الوحدة";
"Open Calendar" = "فتح التقويم"; // translategemma:4b
"Toggle the module" = "تشغيل/إيقاف الوحدة."; // translategemma:4b
// Application settings
"Update application" = "تحديث التطبيق";
"Check for updates" = "التحقق من وجود تحديثات";
"At start" = "عند البدء";
"Once per day" = "مرة يوميًا";
"Once per week" = "مرة أسبوعيًا";
"Once per month" = "مرة شهريًا";
"Never" = "أبدًا";
"Check for update" = "التحقق من وجود تحديث";
"Show icon in dock" = "إظهار الرمز في القائمة";
"Start at login" = "بدء التشغيل عند تسجيل الدخول";
"Build number" = "رقم الإصدار";
"Import settings" = "استيراد الإعدادات"; // translategemma:4b
"Export settings" = "إعدادات التصدير"; // translategemma:4b
"Reset settings" = "إعادة ضبط الإعدادات";
"Pause the Stats" = "إيقاف الإحصاءات";
"Resume the Stats" = "استئناف الإحصاءات";
"Combined modules" = "الوحدات المجتمعة";
"Combined details" = "تفاصيل مجمعة"; // translategemma:4b
"Spacing" = "التباعد";
"Share anonymous telemetry" = "مشاركة بيانات التشخيص المجهولة";
"Choose file" = "اختر الملف"; // translategemma:4b
"Stress tests" = "اختبارات تحمل الضغط"; // translategemma:4b
// Dashboard
"Serial number" = "الرقم التسلسلي";
"Model identifier" = "معرف النموذج"; // translategemma:4b
"Production year" = "سنة الإنتاج"; // translategemma:4b
"Uptime" = "وقت التشغيل";
"Number of cores" = "%0 نوى";
"Number of threads" = "%0 خيوط";
"Number of e-cores" = "%0 نوى كفاءة";
"Number of p-cores" = "%0 نوى أداء";
"Disks" = "الأقراص"; // translategemma:4b
"Display" = "عرض"; // translategemma:4b
// Update
"The latest version of Stats installed" = "تم تثبيت أحدث إصدار من الإحصاءات";
"Downloading..." = "جار التحميل...";
"Current version: " = "الإصدار الحالي: ";
"Latest version: " = "الإصدار الأحدث: ";
// Widgets
"Color" = "لون";
"Label" = "تسمية";
"Box" = "مربع";
"Frame" = "إطار";
"Value" = "قيمة";
"Colorize" = "تلوين";
"Colorize value" = "تلوين القيمة";
"Additional information" = "معلومات إضافية";
"Reverse values order" = "عكس ترتيب القيم";
"Base" = "قاعدة";
"Display mode" = "وضع العرض";
"One row" = "صف واحد";
"Two rows" = "صفين";
"Mini widget" = "مصغر";
"Line chart widget" = "مخطط خطي";
"Bar chart widget" = "مخطط شريطي";
"Pie chart widget" = "مخطط دائري";
"Network chart widget" = "مخطط شبكة";
"Speed widget" = "عداد سرعة";
"Battery widget" = "عداد بطارية";
"Stack widget" = "عداد تراكمي";
"Memory widget" = "عداد ذاكرة";
"Static width" = "عرض ثابت";
"Tachometer widget" = "عداد سرعة";
"State widget" = "عداد حالة";
"Text widget" = "عنصري نص"; // translategemma:4b
"Battery details widget" = "وحدة معلومات البطارية"; // translategemma:4b
"Show symbols" = "إظهار الرموز";
"Label widget" = "تسمية";
"Number of reads in the chart" = "عدد القراءات في المخطط";
"Color of download" = "لون التنزيل";
"Color of upload" = "لون الرفع";
"Monospaced font" = "خط ذو تساوي المسافات";
"Reverse order" = "ترتيب عكسي";
"Chart history" = "مدة المخطط";
"Default color" = "القيمة الافتراضية"; // translategemma:4b
"Transparent when no activity" = "شفاف عندما لا يوجد نشاط"; // translategemma:4b
"Constant color" = "ثابت"; // translategemma:4b
// Module Kit
"Open module settings" = "فتح إعدادات الوحدة";
"Select widget" = "اختيار %0 عنصر";
"Open widget settings" = "فتح إعدادات العنصر";
"Update interval" = "فاصل التحديث";
"Usage history" = "سجل الاستخدام";
"Details" = "التفاصيل";
"Top processes" = "أعلى العمليات";
"Pictogram" = "رمز";
"Module" = "وحدة";
"Widgets" = "عناصر";
"Popup" = "قائمة منبثقة";
"Notifications" = "إشعارات";
"Merge widgets" = "دمج العناصر";
"No available widgets to configure" = "لا توجد عناصر متاحة للتكوين";
"No options to configure for the popup in this module" = "لا توجد خيارات للتكوين للقائمة المنبثقة في هذه الوحدة";
"Process" = "عملية";
"Kill process" = "إيقاف العملية";
"Keyboard shortcut" = "اختصار لوحة المفاتيح"; // translategemma:4b
"Listening..." = "الاستماع..."; // translategemma:4b
// Modules
"Number of top processes" = "عدد أعلى العمليات";
"Update interval for top processes" = "فاصل التحديث لأعلى العمليات";
"Notification level" = "مستوى الإشعار";
"Chart color" = "لون المخطط";
"Main chart scaling" = "تحجيم المخطط الرئيسي";
"Scale value" = "قيمة المقياس";
"Text widget value" = "قيمة عنصر واجهة النص"; // translategemma:4b
// CPU
"CPU usage" = "استخدام المعالج";
"CPU temperature" = "حرارة المعالج";
"CPU frequency" = "تردد المعالج";
"System" = "النظام";
"User" = "المستخدم";
"Idle" = "الخمول";
"Show usage per core" = "عرض الاستخدام لكل نواة";
"Show hyper-threading cores" = "عرض نوى التناوب الهجين";
"Split the value (System/User)" = "تقسيم القيمة (نظام/مستخدم)";
"Scheduler limit" = "حد المجدول";
"Speed limit" = "حد السرعة";
"Average load" = "متوسط الحمل";
"1 minute" = "1 دقيقة";
"5 minutes" = "5 دقائق";
"15 minutes" = "15 دقيقة";
"CPU usage threshold" = "عتبة استخدام المعالج";
"CPU usage is" = "استخدام المعالج %0";
"Efficiency cores" = "نوى كفاءة";
"Performance cores" = "نوى أداء";
"System color" = "لون النظام";
"User color" = "لون المستخدم";
"Idle color" = "لون الخمول";
"Cluster grouping" = "تجميع المجموعات";
"Efficiency cores color" = "لون نوى الكفاءة";
"Performance cores color" = "لون نوى الأداء";
"Total load" = "إجمالي الحمل";
"System load" = "حمل النظام";
"User load" = "حمل المستخدم";
"Efficiency cores load" = "حمل نوى الكفاءة";
"Performance cores load" = "حمل نوى الأداء";
"All cores" = "جميع النواة"; // translategemma:4b
// GPU
"GPU to show" = "عرض GPU";
"Show GPU type" = "عرض نوع GPU";
"GPU enabled" = "GPU مُمكّن";
"GPU disabled" = "GPU معطل";
"GPU temperature" = "حرارة GPU";
"GPU utilization" = "استخدام GPU";
"Vendor" = "البائع";
"Model" = "النموذج";
"Status" = "الحالة";
"Active" = "نشط";
"Non active" = "غير نشط";
"Fan speed" = "سرعة المروحة";
"Core clock" = "تردد النواة";
"Memory clock" = "تردد الذاكرة";
"Utilization" = "استخدام";
"Render utilization" = "استخدام الرندر";
"Tiler utilization" = "استخدام اللوح";
"GPU usage threshold" = "عتبة استخدام GPU";
"GPU usage is" = "استخدام GPU %0";
// RAM
"Memory usage" = "استخدام الذاكرة";
"Memory pressure" = "ضغط الذاكرة";
"Total" = "الإجمالي";
"Used" = "المستخدمة";
"App" = "التطبيق";
"Wired" = "المسلكة";
"Compressed" = "المضغوطة";
"Free" = "المتبقية";
"Swap" = "التبادل";
"Split the value (App/Wired/Compressed)" = "تقسيم القيمة (تطبيق/مسلكة/مضغوطة)";
"RAM utilization threshold" = "عتبة استخدام الذاكرة";
"RAM utilization is" = "استخدام الذاكرة %0";
"App color" = "لون التطبيق";
"Wired color" = "لون المسلكة";
"Compressed color" = "لون المضغوطة";
"Free color" = "لون الذاكرة المتبقية";
"Free memory (less than)" = "الذاكرة المتبقية (أقل من)";
"Swap size" = "حجم التبادل";
"Free RAM is" = "الذاكرة العشوائية المتبقية هي %0";
// Disk
"Show removable disks" = "عرض الأقراص القابلة للإزالة";
"Used disk memory" = "%0 من %1 مستخدمة";
"Free disk memory" = "%0 من %1 متبقية";
"Disk to show" = "القرص للعرض";
"Open disk" = "فتح القرص";
"Switch view" = "تبديل العرض";
"Disk utilization threshold" = "عتبة استخدام القرص";
"Disk utilization is" = "استخدام القرص %0";
"Read color" = "لون القراءة";
"Write color" = "لون الكتابة";
"Disk usage" = "استخدام القرص";
"Total read" = "إجمالي القراءة";
"Total written" = "إجمالي الكتابة";
"Write speed" = "اكتب"; // translategemma:4b
"Read speed" = "اقرأ"; // translategemma:4b
"Drives" = "محركات"; // translategemma:4b
"SMART data" = "بيانات ذكية"; // translategemma:4b
// Sensors
"Temperature unit" = "وحدة الحرارة";
"Celsius" = "درجة مئوية";
"Fahrenheit" = "فهرنهايت";
"Save the fan speed" = "حفظ سرعة المروحة";
"Fan" = "مروحة";
"HID sensors" = "مستشعرات HID";
"Synchronize fan's control" = "مزامنة تحكم المروحة";
"Current" = "الحالي";
"Energy" = "الطاقة";
"Show unknown sensors" = "عرض المستشعرات غير المعروفة";
"Install fan helper" = "تثبيت مساعد المروحة";
"Uninstall fan helper" = "إلغاء تثبيت مساعد المروحة";
"Fan value" = "قيمة المروحة";
"Turn off fan" = "إيقاف تشغيل المروحة";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "أنت على وشك إيقاف تشغيل المروحة. هذا ليس إجراءً مُوصى به يمكن أن يتسبب في تلف جهازك، هل تريد بالتأكيد القيام بذلك؟";
"Sensor threshold" = "عتبة المستشعر";
"Left fan" = "المروحة اليسرى";
"Right fan" = "المروحة اليمنى";
"Fastest fan" = "أسرع مروحة";
"Sensor to show" = "جهاز إشارة"; // translategemma:4b
// Network
"Uploading" = "تحميل";
"Downloading" = "تنزيل";
"Public IP" = "عنوان IP العام";
"Local IP" = "عنوان IP المحلي";
"Interface" = "واجهة";
"Physical address" = "العنوان الفيزيائي";
"Refresh" = "تحديث";
"Click to copy public IP address" = "انقر لنسخ عنوان IP العام";
"Click to copy local IP address" = "انقر لنسخ عنوان IP المحلي";
"Click to copy wifi name" = "انقر لنسخ اسم الواي فاي";
"Click to copy mac address" = "انقر لنسخ عنوان MAC";
"No connection" = "لا يوجد اتصال";
"Network interface" = "واجهة الشبكة";
"Total download" = "إجمالي التنزيل";
"Total upload" = "إجمالي الرفع";
"Reader type" = "نوع القارئ";
"Interface based" = "بناءً على الواجهة";
"Processes based" = "بناءً على العمليات";
"Reset data usage" = "إعادة تعيين استخدام البيانات";
"VPN mode" = "وضع VPN";
"Standard" = "قياسي";
"Security" = "أمان";
"Channel" = "قناة";
"Common scale" = "مقياس شائع";
"Autodetection" = "الكشف التلقائي";
"Widget activation threshold" = "عتبة تنشيط الودجة";
"Internet connection" = "اتصال بالإنترنت";
"Active state color" = "لون الحالة النشطة";
"Nonactive state color" = "لون الحالة غير النشطة";
"Connectivity host (ICMP)" = "مضيف الاتصال (ICMP)";
"Leave empty to disable the check" = "اتركه فارغًا لتعطيل الفحص";
"Connectivity history" = "سجل الاتصال";
"Auto-refresh public IP address" = "تحديث تلقائي لعنوان IP العام";
"Every hour" = "كل ساعة";
"Every 12 hours" = "كل 12 ساعة";
"Every 24 hours" = "كل 24 ساعة";
"Network activity" = "نشاط الشبكة";
"Last reset" = "آخر إعادة تعيين منذ %0";
"Latency" = "تأخير";
"Upload speed" = "تحميل"; // translategemma:4b
"Download speed" = "تنزيل"; // translategemma:4b
"Address" = "العنوان"; // translategemma:4b
"WiFi network" = "شبكة Wi-Fi"; // translategemma:4b
"Local IP changed" = "تغير عنوان IP المحلي"; // translategemma:4b
"Public IP changed" = "تغير عنوان IP العام."; // translategemma:4b
"Previous IP" = "عنوان IP السابق: %0"; // translategemma:4b
"New IP" = "عنوان IP الجديد: %0"; // translategemma:4b
"Internet connection lost" = "فقدان الاتصال بالإنترنت"; // translategemma:4b
"Internet connection established" = "تم إنشاء اتصال بالإنترنت"; // translategemma:4b
// Battery
"Level" = "المستوى";
"Source" = "المصدر";
"AC Power" = "تيار متردد";
"Battery Power" = "بطارية";
"Time" = "الوقت";
"Health" = "الصحة";
"Amperage" = "التيار";
"Voltage" = "الجهد";
"Cycles" = "الدورات";
"Temperature" = "درجة الحرارة";
"Power adapter" = "محول الطاقة";
"Power" = "الطاقة";
"Is charging" = "يتم الشحن";
"Time to discharge" = "الوقت للتفريغ";
"Time to charge" = "الوقت للشحن";
"Calculating" = "جاري الحساب";
"Fully charged" = "مشحون بالكامل";
"Not connected" = "غير متصل";
"Low level notification" = "إشعار بالمستوى المنخفض";
"High level notification" = "إشعار بالمستوى العالي";
"Low battery" = "البطارية منخفضة";
"High battery" = "البطارية مرتفعة";
"Battery remaining" = "البطارية تبقى %0%";
"Battery remaining to full charge" = "البطارية تبقى %0% حتى الشحن الكامل";
"Percentage" = "النسبة المئوية";
"Percentage and time" = "النسبة المئوية والوقت";
"Time and percentage" = "الوقت والنسبة المئوية";
"Time format" = "تنسيق الوقت";
"Hide additional information when full" = "إخفاء المعلومات الإضافية عندما تكون ممتلئة";
"Last charge" = "آخر شحن";
"Capacity" = "السعة";
"current / maximum / designed" = "الحالي / الأقصى / المصمم";
"Low power mode" = "وضع الطاقة المنخفضة";
"Percentage inside the icon" = "النسبة المئوية داخل الرمز";
"Colorize battery" = "تلوين البطارية";
"Charging current" = "تيار الشحن"; // translategemma:4b
"Charging Voltage" = "جهد الشحن"; // translategemma:4b
"Charger state inside the battery" = "حالة الشاحن داخل البطارية"; // translategemma:4b
// Bluetooth
"Battery to show" = "البطارية المراد عرضها";
"No Bluetooth devices are available" = "لا توجد أجهزة بلوتوث متاحة";
// Clock
"Time zone" = "المنطقة الزمنية";
"Local" = "محلي";
"Calendar" = "تقويم"; // translategemma:4b
"Show week numbers" = "عرض أرقام الأسابيع"; // translategemma:4b
"Local time" = "الوقت المحلي"; // translategemma:4b
"Add new clock" = "أضف ساعة جديدة"; // translategemma:4b
"Delete selected clock" = "حذف الساعة المحددة"; // translategemma:4b
"Help with datetime format" = "مساعدة في تنسيق التاريخ والوقت"; // translategemma:4b
// Colors
"Based on utilization" = "بناءً على الاستخدام";
"Based on pressure" = "بناءً على الضغط";
"Based on cluster" = "بناءً على التجمع";
"System accent" = "لون النظام";
"Monochrome accent" = "لون مونوكروم";
"Clear" = "واضح";
"White" = "أبيض";
"Black" = "أسود";
"Gray" = "رمادي";
"Second gray" = "رمادي ثانوي";
"Dark gray" = "رمادي داكن";
"Light gray" = "رمادي فاتح";
"Red" = "أحمر";
"Second red" = "أحمر ثانوي";
"Green" = "أخضر";
"Second green" = "أخضر ثانوي";
"Blue" = "أزرق";
"Second blue" = "أزرق ثانوي";
"Yellow" = "أصفر";
"Second yellow" = "أصفر ثانوي";
"Orange" = "برتقالي";
"Second orange" = "برتقالي ثانوي";
"Purple" = "أرجواني";
"Second purple" = "أرجواني ثانوي";
"Brown" = "بني";
"Second brown" = "بني ثانوي";
"Cyan" = "سماوي";
"Magenta" = "قرمزي";
"Pink" = "زهري";
"Teal" = "أزرق مخضر";
"Indigo" = "نيلي";
================================================
FILE: Stats/Supporting Files/bg.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Rostislav Raykov on 31/01/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "Процесор";
"Open CPU settings" = "Настройки на процесора";
"GPU" = "Графична карта";
"Open GPU settings" = "Настройки на графичната карта";
"RAM" = "RAM";
"Open RAM settings" = "Настройки на RAM";
"Disk" = "Диск";
"Open Disk settings" = "Настройки на диска";
"Sensors" = "Сензори";
"Open Sensors settings" = "Настройки на сензорите";
"Network" = "Мрежа";
"Open Network settings" = "Настройки на мрежата";
"Battery" = "Батерия";
"Open Battery settings" = "Настройки на мрежата";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Настройки на bluetooth";
"Clock" = "Часовник"; // translategemma:4b
"Open Clock settings" = "Отворете настройките на часовника"; // translategemma:4b
// Words
"Unknown" = "Непознато";
"Version" = "Версия";
"Processor" = "Процесор";
"Memory" = "Памет";
"Graphics" = "Графика";
"Close" = "Затваряне";
"Download" = "Изтегляне";
"Install" = "Инсталиране";
"Cancel" = "Отказ";
"Unavailable" = "Недостъпно";
"Yes" = "Да";
"No" = "Не";
"Automatic" = "Автоматично";
"Manual" = "Ръчно";
"None" = "Без";
"Dots" = "Точки";
"Arrows" = "Стрелки";
"Characters" = "Символ";
"Short" = "Кратко";
"Long" = "Дълго";
"Statistics" = "Статистика";
"Max" = "Макс.";
"Min" = "Мин.";
"Reset" = "Зануляване";
"Alignment" = "Подравняване";
"Left alignment" = "Ляво";
"Center alignment" = "Център";
"Right alignment" = "Дясно";
"Dashboard" = "Табло";
"Enabled" = "Включено";
"Disabled" = "Изключено";
"Silent" = "Тихо";
"Units" = "Единици";
"Fans" = "Вентилатори";
"Scaling" = "Скалиране";
"Linear" = "Линейно";
"Square" = "Квадратно";
"Cube" = "Кубично";
"Logarithmic" = "Логаритмично";
"Fixed scale" = "Решено"; // translategemma:4b
"Cores" = "Ядра";
"Settings" = "Настройки"; // translategemma:4b
"Name" = "Име"; // translategemma:4b
"Format" = "Формат"; // translategemma:4b
"Turn off" = "Изключете"; // translategemma:4b
"Normal" = "Нормален"; // translategemma:4b
"Warning" = "Внимание"; // translategemma:4b
"Critical" = "Критичен"; // translategemma:4b
"Usage" = "Използване"; // translategemma:4b
"2 minutes" = "2 минути"; // translategemma:4b
"3 minutes" = "3 минути"; // translategemma:4b
"10 minutes" = "10 минути"; // translategemma:4b
"Import" = "Импорт"; // translategemma:4b
"Export" = "Експортиране"; // translategemma:4b
"Separator" = "Разделител"; // translategemma:4b
"Read" = "Прочетете"; // translategemma:4b
"Write" = "Напишете"; // translategemma:4b
"Frequency" = "Честота"; // translategemma:4b
"Save" = "Запазване"; // translategemma:4b
"Run" = "Стартирай"; // translategemma:4b
"Stop" = "Спрете"; // translategemma:4b
"Uninstall" = "Деинсталиране"; // translategemma:4b
"1 sec" = "1 секунда"; // translategemma:4b
"2 sec" = "2 секунди"; // translategemma:4b
"3 sec" = "3 секунди"; // translategemma:4b
"5 sec" = "5 секунди"; // translategemma:4b
"10 sec" = "10 секунди"; // translategemma:4b
"15 sec" = "15 секунди"; // translategemma:4b
"30 sec" = "30 секунди"; // translategemma:4b
"60 sec" = "60 секунди"; // translategemma:4b
// Setup
"Stats Setup" = "Настройки на Stats";
"Previous" = "Предишно";
"Previous page" = "Предишна страница";
"Next" = "Следващо";
"Next page" = "Следваща страница";
"Finish" = "Приключване";
"Finish setup" = "Приключване на настройването";
"Welcome to Stats" = "Добре дошли в Stats";
"welcome_message" = "Благодарим Ви, че ползвате Stats, безплатен системен монитор за macOS с отворен код.";
"Start the application automatically when starting your Mac" = "Стартирай приложението автоматично при стартиране на вашия Mac";
"Do not start the application automatically when starting your Mac" = "Не стартирай приложението автоматично при стартиране на вашия Mac";
"Do everything silently in the background (recommended)" = "Прави всичко тихо във фонов режим (препоръчително)";
"Check for a new version on startup" = "Проверяване за нова версия при стартиране";
"Check for a new version every day (once a day)" = "Проверяване за нова версия всеки ден (веднъж на ден)";
"Check for a new version every week (once a week)" = "Проверяване за нова версия всяка седмица (веднъж на седмица)";
"Check for a new version every month (once a month)" = "Проверяване за нова версия всеки месец (веднъж на месец)";
"Never check for updates (not recommended)" = "Без проверяване за нови версии (не се препоръчва)";
"Anonymous telemetry for better development decisions" = "Анонимни данни за телеметрия за по-добри решения при разработване"; // translategemma:4b
"Share anonymous telemetry data" = "Споделяне на анонимни данни за телеметрия"; // translategemma:4b
"Do not share anonymous telemetry data" = "Не споделяйте анонимни данни за телеметрия."; // translategemma:4b
"The configuration is completed" = "Конфигурацията е завършена";
"finish_setup_message" = "Всичко е настроено! \n Stats е приложение с отворен код, безплатно е и винаги ще бъде. \n Ако ви допада, може да подкрепите проекта, ще сме много благодарни!";
// Alerts
"New version available" = "Налична е нова версия";
"Click to install the new version of Stats" = "Натиснете, за да инсталирате новата версия на Stats";
"Successfully updated" = "Успешно обновяване";
"Stats was updated to v" = "Stats е обновена до версия v%0";
"Reset settings text" = "Всички настройки на приложението ще бъдат занулени и приложението ще се рестартира. Сигурни ли сте, че искате това?";
"Support text" = "Благодарим ви, че използвате Статистики!\n\n Поддържането и подобряването на този проект с отворен код изисква време и ресурси. Вашата подкрепа ни помага да продължим да предоставяме безплатно и надеждно приложение за всички.\n\nАко намирате Stats за полезно, моля, помислете дали да не дадете своя принос. Всяка малка част помага!";
// Settings
"Open Activity Monitor" = "Отваряне на монитора на активностите";
"Report a bug" = "Докладване на проблем";
"Support the application" = "Подкрепа";
"Close application" = "Затваряне на приложението";
"Open application settings" = "Отваряне на настройките на приложението";
"Open dashboard" = "Отваряне на таблото";
"No notifications available in this module" = "Няма налични известия в този модул"; // translategemma:4b
"Open Calendar" = "Отвори календара"; // translategemma:4b
"Toggle the module" = "Активирайте/деактивирайте модула"; // translategemma:4b
// Application settings
"Update application" = "Обновяване на приложението";
"Check for updates" = "Проверка за обновления";
"At start" = "При стартиране";
"Once per day" = "Веднъж на ден";
"Once per week" = "Веднъж на седмица";
"Once per month" = "Веднъж на месец";
"Never" = "Никога";
"Check for update" = "Проверка за обновления";
"Show icon in dock" = "Показване на икона в Dock";
"Start at login" = "Автоматично стартиране";
"Build number" = "Номер на версията"; // translategemma:4b
"Import settings" = "Импортиране на настройки"; // translategemma:4b
"Export settings" = "Настройки за експортиране"; // translategemma:4b
"Reset settings" = "Зануляване на настройките";
"Pause the Stats" = "Паузиране на Stats";
"Resume the Stats" = "Продължаване на Stats";
"Combined modules" = "Комбинирани модули"; // translategemma:4b
"Combined details" = "Комбинирани данни"; // translategemma:4b
"Spacing" = "Разстояние между елементи"; // translategemma:4b
"Share anonymous telemetry" = "Споделяне на анонимни данни за телеметрия"; // translategemma:4b
"Choose file" = "Изберете файл"; // translategemma:4b
"Stress tests" = "Тестове за измерване на напрежението"; // translategemma:4b
// Dashboard
"Serial number" = "Сериен номер";
"Model identifier" = "Идентификатор на модела"; // translategemma:4b
"Production year" = "Година на производство"; // translategemma:4b
"Uptime" = "Време от последно рестартиране";
"Number of cores" = "%0 ядра";
"Number of threads" = "%0 нишки";
"Number of e-cores" = "%0 ефикасни ядра";
"Number of p-cores" = "%0 производителни ядра";
"Disks" = "Твърди дискове"; // translategemma:4b
"Display" = "Дисплей"; // translategemma:4b
// Update
"The latest version of Stats installed" = "Имате най-новата версия на Stats";
"Downloading..." = "Изтегляне...";
"Current version: " = "Текуща версия: ";
"Latest version: " = "Последна версия: ";
// Widgets
"Color" = "Цвят";
"Label" = "Етикет";
"Box" = "Непрозрачен фон";
"Frame" = "Рамка";
"Value" = "Стойност";
"Colorize" = "Оцветяване";
"Colorize value" = "Стойност на оцветяване";
"Additional information" = "Допълнителна информация";
"Reverse values order" = "Обръщане на подредбата на стойностите";
"Base" = "Основа";
"Display mode" = "Режим на преглед";
"One row" = "Един ред";
"Two rows" = "Два реда";
"Mini widget" = "Мини";
"Line chart widget" = "Линейна диаграма";
"Bar chart widget" = "Стълбовидна диаграма";
"Pie chart widget" = "Кръгова диаграма";
"Network chart widget" = "Мрежова диаграма";
"Speed widget" = "Скорост";
"Battery widget" = "Батерия";
"Stack widget" = "Стек"; // translategemma:4b
"Memory widget" = "Памет";
"Static width" = "Постоянна ширина";
"Tachometer widget" = "Тахометър";
"State widget" = "Състояние на елемента"; // translategemma:4b
"Text widget" = "Текстово поле"; // translategemma:4b
"Battery details widget" = "Widget за информация за батерията"; // translategemma:4b
"Show symbols" = "Показване на символи";
"Label widget" = "Етикет";
"Number of reads in the chart" = "Брой прочитания в графиката";
"Color of download" = "Цвят на свалянето";
"Color of upload" = "Цвят на качването";
"Monospaced font" = "Шрифт с еднаква ширина на символите"; // translategemma:4b
"Reverse order" = "Обратен ред"; // translategemma:4b
"Chart history" = "История на графика"; // translategemma:4b
"Default color" = "Стойности по подразбиране"; // translategemma:4b
"Transparent when no activity" = "Прозрачен, когато няма активност"; // translategemma:4b
"Constant color" = "Постоянен"; // translategemma:4b
// Module Kit
"Open module settings" = "Отваряне на настройките на модула";
"Select widget" = "Избор на уиджет %0";
"Open widget settings" = "Настройки на уиджета";
"Update interval" = "Интервал на опресняване";
"Usage history" = "История на използването";
"Details" = "Подробности";
"Top processes" = "Топ процеси";
"Pictogram" = "Пиктограма";
"Module" = "Модул";
"Widgets" = "Джаджи";
"Popup" = "Изскачащ прозорец";
"Notifications" = "Известия";
"Merge widgets" = "Сливане на уиджети";
"No available widgets to configure" = "Няма налични уиджети за конфигуриране";
"No options to configure for the popup in this module" = "Няма опции за конфигуриране за изскачащият прозорец в този модул";
"Process" = "Процес"; // translategemma:4b
"Kill process" = "Завършете процеса"; // translategemma:4b
"Keyboard shortcut" = "Натискане на комбинация от клавиши"; // translategemma:4b
"Listening..." = "Слушам..."; // translategemma:4b
// Modules
"Number of top processes" = "Брой топ процеси";
"Update interval for top processes" = "Интервал на опресняване на топ процесите";
"Notification level" = "Ниво на известия";
"Chart color" = "Цвят на графиката";
"Main chart scaling" = "Основно мащабиране на графиката"; // translategemma:4b
"Scale value" = "Стойност на мащаба"; // translategemma:4b
"Text widget value" = "Стойност на текстовото поле"; // translategemma:4b
// CPU
"CPU usage" = "Употреба на процесора";
"CPU temperature" = "Температура на процесора";
"CPU frequency" = "Честота на процесора";
"System" = "Система";
"User" = "Потребител";
"Idle" = "В покой";
"Show usage per core" = "Показване на употреба по ядро";
"Show hyper-threading cores" = "Показване на hyper-threading ядра";
"Split the value (System/User)" = "Разделяне на стойността (Система/Потребител)";
"Scheduler limit" = "Ограничение на планировката"; // translategemma:4b
"Speed limit" = "Ограничение на скоростта";
"Average load" = "Средно натоварване";
"1 minute" = "1 минута";
"5 minutes" = "5 минути";
"15 minutes" = "15 минути";
"CPU usage threshold" = "Праг за използване на процесора"; // translategemma:4b
"CPU usage is" = "Употребата на процесорът е %0";
"Efficiency cores" = "Ефикасни ядра";
"Performance cores" = "Производителни ядра";
"System color" = "Системен цвят";
"User color" = "Потребителски цвят";
"Idle color" = "Цвят при неактивност";
"Cluster grouping" = "Групиране на кластери"; // translategemma:4b
"Efficiency cores color" = "Цветове на ядрата за ефективност"; // translategemma:4b
"Performance cores color" = "Цвят на ядрата за производителност"; // translategemma:4b
"Total load" = "Обща консумация"; // translategemma:4b
"System load" = "Натоварване на системата"; // translategemma:4b
"User load" = "Натоварване на потребителите"; // translategemma:4b
"Efficiency cores load" = "Ядрото за ефективност се зарежда"; // translategemma:4b
"Performance cores load" = "Зареждане на ядрата за производителност"; // translategemma:4b
"All cores" = "Всички ядра"; // translategemma:4b
// GPU
"GPU to show" = "Показване на графична карта";
"Show GPU type" = "Показване на вид на графична карта";
"GPU enabled" = "Включена графична карта";
"GPU disabled" = "Изключена графична карта";
"GPU temperature" = "Температура на графичната карта";
"GPU utilization" = "Утилизация на графичната карта";
"Vendor" = "Производител";
"Model" = "Модел";
"Status" = "Статус";
"Active" = "Активна";
"Non active" = "Неактивна";
"Fan speed" = "Скорост на вентилатора";
"Core clock" = "Тактова честота на ядрото";
"Memory clock" = "Тактова честота на паметта";
"Utilization" = "Утилизация";
"Render utilization" = "Използване на ресурсите при рендиране"; // translategemma:4b
"Tiler utilization" = "Използване на плочки"; // translategemma:4b
"GPU usage threshold" = "Праг за използване на графичния процесор"; // translategemma:4b
"GPU usage is" = "Употребата на грфичната карта е %0";
// RAM
"Memory usage" = "Заета памет";
"Memory pressure" = "Натоварване на паметта";
"Total" = "Общо";
"Used" = "Заета";
"App" = "Приложения";
"Wired" = "С проводник"; // translategemma:4b
"Compressed" = "Компересирана";
"Free" = "Свободна";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Разделете стойността (Приложения/Wired/Компересирана)";
"RAM utilization threshold" = "Праг на утилизацията на паметта";
"RAM utilization is" = "Утилизацията на паметта е %0";
"App color" = "Цвят на приложението"; // translategemma:4b
"Wired color" = "Цвят с проводник"; // translategemma:4b
"Compressed color" = "Цвят на компресираната памет";
"Free color" = "Цвят на свободната памет";
"Free memory (less than)" = "Безплатно памет (по-малко от)"; // translategemma:4b
"Swap size" = "Размер на размяната памет"; // translategemma:4b
"Free RAM is" = "Наличната RAM е %0"; // translategemma:4b
// Disk
"Show removable disks" = "Показване на преносими дискове";
"Used disk memory" = "Заето %0 от %1";
"Free disk memory" = "Свободно %0 от %1";
"Disk to show" = "Показване на диск";
"Open disk" = "Отваряне на диск";
"Switch view" = "Преключване на изглед";
"Disk utilization threshold" = "Праг на утилизацията на диска";
"Disk utilization is" = "Утилизацията на диска е %0";
"Read color" = "Разгледайте цветовете"; // translategemma:4b
"Write color" = "Напишете цвят"; // translategemma:4b
"Disk usage" = "Използвано дисково пространство"; // translategemma:4b
"Total read" = "Общо прочетено"; // translategemma:4b
"Total written" = "Общо написан"; // translategemma:4b
"Write speed" = "Напишете"; // translategemma:4b
"Read speed" = "Прочетете"; // translategemma:4b
"Drives" = "Драйвери"; // translategemma:4b
"SMART data" = "Данни от SMART"; // translategemma:4b
// Sensors
"Temperature unit" = "Температурна единица";
"Celsius" = "Целзиус";
"Fahrenheit" = "Фаренхайт";
"Save the fan speed" = "Запазете скоростта на вентилатора";
"Fan" = "Вентилатор";
"HID sensors" = "Датчици за измерване на влажността"; // translategemma:4b
"Synchronize fan's control" = "Синхронизирайте контрола на вентилатора"; // translategemma:4b
"Current" = "Текущо";
"Energy" = "Енергия";
"Show unknown sensors" = "Показване на непознатите сензори";
"Install fan helper" = "Инсталирайте помощник за вентилатора"; // translategemma:4b
"Uninstall fan helper" = "Премахнете приложението за управление на вентилаторите."; // translategemma:4b
"Fan value" = "Стойност на фен"; // translategemma:4b
"Turn off fan" = "Изключете вентилатора"; // translategemma:4b
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Ще изключите вентилатора. Това не е препоръчителна стъпка, която може да повреди вашия Mac. Наистина ли искате да го направите?"; // translategemma:4b
"Sensor threshold" = "Праг на сензора"; // translategemma:4b
"Left fan" = "Отляво"; // translategemma:4b
"Right fan" = "Правилно"; // translategemma:4b
"Fastest fan" = "Най-бързо"; // translategemma:4b
"Sensor to show" = "Датчик за показване"; // translategemma:4b
// Network
"Uploading" = "Качване";
"Downloading" = "Изтегляне";
"Public IP" = "Публично IP";
"Local IP" = "Локално IP";
"Interface" = "Интерфейс";
"Physical address" = "Физически адрес";
"Refresh" = "Опресняване";
"Click to copy public IP address" = "Натиснете, за да копирате публичния IP адрес";
"Click to copy local IP address" = "Натиснете, за да копирате локалния IP адрес";
"Click to copy wifi name" = "Натиснете, за да копирате името на WiFi мреждата";
"Click to copy mac address" = "Натиснете, за да копирате mac адреса";
"No connection" = "Няма връзка";
"Network interface" = "Мрежов интерфейс";
"Total download" = "Общо изтеглено";
"Total upload" = "Общо качено";
"Reader type" = "Тип четец";
"Interface based" = "На база интерфейс";
"Processes based" = "На база процес";
"Reset data usage" = "Зануляване";
"VPN mode" = "VPN режим";
"Standard" = "Стандарт"; // translategemma:4b
"Security" = "Сигурност"; // translategemma:4b
"Channel" = "Канал";
"Common scale" = "Общ мащаб"; // translategemma:4b
"Autodetection" = "Автоматично засичане";
"Widget activation threshold" = "Праг за активиране на компонента"; // translategemma:4b
"Internet connection" = "Интернет връзка";
"Active state color" = "Цвят на активното състояние";
"Nonactive state color" = "Цвят на неактивното състояние";
"Connectivity host (ICMP)" = "Хост за свързаност (ICMP)"; // translategemma:4b
"Leave empty to disable the check" = "Оставете празно, за да изключите проверката";
"Connectivity history" = "История на връзките"; // translategemma:4b
"Auto-refresh public IP address" = "Автоматично обновяване на публичния IP адрес"; // translategemma:4b
"Every hour" = "На всеки час"; // translategemma:4b
"Every 12 hours" = "Всеки 12 часа"; // translategemma:4b
"Every 24 hours" = "Всеки 24 часа"; // translategemma:4b
"Network activity" = "Активност в мрежата"; // translategemma:4b
"Last reset" = "Последната актуализация беше преди %0 дни."; // translategemma:4b
"Latency" = "Забавяне"; // translategemma:4b
"Upload speed" = "Качване"; // translategemma:4b
"Download speed" = "Изтегляне"; // translategemma:4b
"Address" = "Адрес"; // translategemma:4b
"WiFi network" = "Мрежа безжичен интернет"; // translategemma:4b
"Local IP changed" = "Локалният IP адрес е променен."; // translategemma:4b
"Public IP changed" = "Публичният IP адрес е променен."; // translategemma:4b
"Previous IP" = "Предишна IP: %0"; // translategemma:4b
"New IP" = "Нов IP: %0"; // translategemma:4b
"Internet connection lost" = "Прекъснатото интернет връзка"; // translategemma:4b
"Internet connection established" = "Установена е връзка към интернет"; // translategemma:4b
// Battery
"Level" = "Ниво";
"Source" = "Източник";
"AC Power" = "Зарядно";
"Battery Power" = "Батерия";
"Time" = "Време";
"Health" = "Здраве";
"Amperage" = "Сила на тока";
"Voltage" = "Волтаж";
"Cycles" = "Цикли";
"Temperature" = "Температура";
"Power adapter" = "Зарядно";
"Power" = "Мощност";
"Is charging" = "Зарежда се";
"Time to discharge" = "Време до пълно разреждане";
"Time to charge" = "Време до пълен заряд";
"Calculating" = "Пресмята се";
"Fully charged" = "Пълен заряд";
"Not connected" = "Не е свързано";
"Low level notification" = "Известие за ниско ниво на батерията";
"High level notification" = "Известие за високо ниво на батерията";
"Low battery" = "Ниско ниво на батерията";
"High battery" = "Високо ниво на батерията";
"Battery remaining" = "Оставащи %0%";
"Battery remaining to full charge" = "%0% до пълен заряд";
"Percentage" = "Процент";
"Percentage and time" = "Процент и време";
"Time and percentage" = "Време и процент";
"Time format" = "Формат на времето";
"Hide additional information when full" = "Скриване на допълнителна информация при пълен заряд";
"Last charge" = "Последно зареждане";
"Capacity" = "Капацитет";
"current / maximum / designed" = "текущ / максимален / проектиран"; // translategemma:4b
"Low power mode" = "Режим за пестене на енергията";
"Percentage inside the icon" = "Процент в иконата";
"Colorize battery" = "Оцветете батерията"; // translategemma:4b
"Charging current" = "Текущ на ток за зареждане"; // translategemma:4b
"Charging Voltage" = "Напрежение за зареждане"; // translategemma:4b
"Charger state inside the battery" = "Състояние на зареждането вътре в батерията"; // translategemma:4b
// Bluetooth
"Battery to show" = "Батерия за показване";
"No Bluetooth devices are available" = "Няма налични Bluetooth устройства";
// Clock
"Time zone" = "Часова зона"; // translategemma:4b
"Local" = "Местен"; // translategemma:4b
"Calendar" = "Календар"; // translategemma:4b
"Show week numbers" = "Показвайте номера на седмиците"; // translategemma:4b
"Local time" = "Местен час"; // translategemma:4b
"Add new clock" = "Добавете нов часовник"; // translategemma:4b
"Delete selected clock" = "Изтрийте избраното време"; // translategemma:4b
"Help with datetime format" = "Помощ при форматиране на дати и часове"; // translategemma:4b
// Colors
"Based on utilization" = "На база утилизация";
"Based on pressure" = "На база натоварване";
"Based on cluster" = "Въз основа на кластеризация"; // translategemma:4b
"System accent" = "Системен акцент";
"Monochrome accent" = "Едноцветен акцент";
"Clear" = "Прозрачно";
"White" = "Бяло";
"Black" = "Черно";
"Gray" = "Сиво";
"Second gray" = "Второ сиво";
"Dark gray" = "Тъмно сиво";
"Light gray" = "Светло сиво";
"Red" = "Червено";
"Second red" = "Второ червено";
"Green" = "Зелено";
"Second green" = "Второ зелено";
"Blue" = "Синьо";
"Second blue" = "Второ синьо";
"Yellow" = "Жълто";
"Second yellow" = "Второ жълто";
"Orange" = "Оранжево";
"Second orange" = "Второ оранжево";
"Purple" = "Виолетово";
"Second purple" = "Второ виолетово";
"Brown" = "Кафяво";
"Second brown" = "Второ кафяво";
"Cyan" = "Циан";
"Magenta" = "Магента";
"Pink" = "Розово";
"Teal" = "Изумруд";
"Indigo" = "Индиго";
================================================
FILE: Stats/Supporting Files/ca.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by David Alonso (davidalonso) on 25/11/2021.
// Using Swift 5.0.
// Running on macOS 12.0.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "Obre la configuració de CPU";
"GPU" = "GPU";
"Open GPU settings" = "Obre la configuració de GPU";
"RAM" = "RAM";
"Open RAM settings" = "Obre la configuració de RAM";
"Disk" = "Disc";
"Open Disk settings" = "Obre la configuració de disc";
"Sensors" = "Sensores"; // translategemma:4b
"Open Sensors settings" = "Obre la configuració de sensors";
"Network" = "Xarxa";
"Open Network settings" = "Obre la configuració de xarxa";
"Battery" = "Bateria";
"Open Battery settings" = "Obre la configuració de bateria";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Obre la configuració de bluetooth";
"Clock" = "Rellotge";
"Open Clock settings" = "Obre la configuració de rellotge";
// Words
"Unknown" = "Desconegut";
"Version" = "Versió";
"Processor" = "Processador";
"Memory" = "Memòria";
"Graphics" = "Gràfics";
"Close" = "Tanca";
"Download" = "Baixa";
"Install" = "Instal·la";
"Cancel" = "Cancel·la";
"Unavailable" = "No disponible";
"Yes" = "Sí";
"No" = "No";
"Automatic" = "Automàtic";
"Manual" = "Manual";
"None" = "Cap";
"Dots" = "Punts";
"Arrows" = "Fletxes";
"Characters" = "Lletres";
"Short" = "Curt";
"Long" = "Llarg";
"Statistics" = "Estadístiques";
"Max" = "Màx";
"Min" = "Mín";
"Reset" = "Reinicialitza";
"Alignment" = "Alineació";
"Left alignment" = "Esquerra";
"Center alignment" = "Centre";
"Right alignment" = "Dreta";
"Dashboard" = "Panell";
"Enabled" = "Activat";
"Disabled" = "Desactivat";
"Silent" = "Silenci";
"Units" = "Unitats";
"Fans" = "Ventiladors";
"Scaling" = "Escalant";
"Linear" = "Liniar";
"Square" = "Quadrat";
"Cube" = "Cub";
"Logarithmic" = "Logarítmic";
"Fixed scale" = "Escala fixe";
"Cores" = "Nuclis";
"Settings" = "Configuració";
"Name" = "Nom";
"Format" = "Format";
"Turn off" = "Apagar";
"Normal" = "Normal";
"Warning" = "Alerta";
"Critical" = "Crític";
"Usage" = "Ús";
"2 minutes" = "2 minuts";
"3 minutes" = "3 minuts";
"10 minutes" = "10 minuts";
"Import" = "Importar";
"Export" = "Exportar";
"Separator" = "Separador"; // translategemma:4b
"Read" = "Llegeix"; // translategemma:4b
"Write" = "Escriure"; // translategemma:4b
"Frequency" = "Frecència"; // translategemma:4b
"Save" = "Guarda"; // translategemma:4b
"Run" = "Executar"; // translategemma:4b
"Stop" = "Atura"; // translategemma:4b
"Uninstall" = "Desinstal·lar"; // translategemma:4b
"1 sec" = "1 segon"; // translategemma:4b
"2 sec" = "2 segons"; // translategemma:4b
"3 sec" = "3 segons"; // translategemma:4b
"5 sec" = "5 segons"; // translategemma:4b
"10 sec" = "10 segons"; // translategemma:4b
"15 sec" = "15 segons"; // translategemma:4b
"30 sec" = "30 segons"; // translategemma:4b
"60 sec" = "60 segons"; // translategemma:4b
// Setup
"Stats Setup" = "Configuració de Stats";
"Previous" = "Previ";
"Previous page" = "Pàgina prèvia";
"Next" = "Següent";
"Next page" = "pàgina següent";
"Finish" = "Finalitzar";
"Finish setup" = "Finalitzar configuració";
"Welcome to Stats" = "Benvingut/Benvinguda a Stats";
"welcome_message" = "Gràcies per fer servir Stats, una aplicació de monitorització per a la teva barra de menús de macOS de codi lliure.";
"Start the application automatically when starting your Mac" = "Iniciar automàticament l'aplicació al iniciar el teu Mac";
"Do not start the application automatically when starting your Mac" = "No iniciar automàticament l'aplicació al iniciar el teu Mac";
"Do everything silently in the background (recommended)" = "Fer tot silenciosament al radere (recomanat)";
"Check for a new version on startup" = "Comprovar noves versions al iniciar";
"Check for a new version every day (once a day)" = "Comprovar noves versions cada dia (una vegada al dia)";
"Check for a new version every week (once a week)" = "Comprovar noves versions cada setmana (una vegada a la setmana)";
"Check for a new version every month (once a month)" = "Comprovar noves versions cada mes (una vegada al mes)";
"Never check for updates (not recommended)" = "No comprovar mai noves versions (no recomanat)";
"Anonymous telemetry for better development decisions" = "Telemetria anònima per proporcionar millors decisions pels desenvolupadors";
"Share anonymous telemetry data" = "Compartir telemetria anònima";
"Do not share anonymous telemetry data" = "No compartir telemetria anònima";
"The configuration is completed" = "La configuració ha estat finalitzada"; // translategemma:4b
"finish_setup_message" = "Tot està configurat! \n Stats és una eina de codi lliure, és gratuït i sempre ho serà. \n Si el gaudeixes pots contribuir al projecte, sempre és benvingut!";
// Alerts
"New version available" = "Nova versió disponible";
"Click to install the new version of Stats" = "Clica per a instal·lar la nova versió d'Stats";
"Successfully updated" = "Actualització completada amb èxit";
"Stats was updated to v" = "Stats s'ha actualitzat a v%0";
"Reset settings text" = "Tota la configuració actual serà esborrada i l'aplicació es reiniciarà. Estàs segur/segura de voler fer això?";
"Support text" = "Gràcies per utilitzar Stats!\n\n Mantenir i millorar aquest projecte de codi obert requereix temps i recursos. El vostre suport ens ajuda a continuar oferint una aplicació gratuïta i fiable per a tothom.\n\nSi trobeu útils les estadístiques, penseu a fer una contribució. Cada petita mica ajuda!";
// Settings
"Open Activity Monitor" = "Obre el Visor d'Activitat";
"Report a bug" = "Reporta un problema";
"Support the application" = "Suporta l'aplicació";
"Close application" = "Tanca l'aplicació";
"Open application settings" = "Obre la configuració de l'aplicació";
"Open dashboard" = "Obre el panell";
"No notifications available in this module" = "No hi ha notificaciones disponibles en aquest mòdul."; // translategemma:4b
"Open Calendar" = "Obrir calendari"; // translategemma:4b
"Toggle the module" = "Activa/desactiva el mòdul"; // translategemma:4b
// Application settings
"Update application" = "Actualitza l'aplicació";
"Check for updates" = "Busca actualitzacions";
"At start" = "A l'inici";
"Once per day" = "Un cop al dia";
"Once per week" = "Un cop per setmana";
"Once per month" = "Un cop al mes";
"Never" = "Mai";
"Check for update" = "Busca actualitzacions";
"Show icon in dock" = "Mostra l'icona al Dock";
"Start at login" = "Arranca al iniciar sessió";
"Build number" = "Número de Build";
"Import settings" = "Importa configuració";
"Export settings" = "Exporta configuració";
"Reset settings" = "Reinicia configuració";
"Pause the Stats" = "Pausa Stats";
"Resume the Stats" = "Continua Stats";
"Combined modules" = "Mòduls combinats";
"Combined details" = "Detalls combinats";
"Spacing" = "Espaiat";
"Share anonymous telemetry" = "Comparteix telemetria anònima";
"Choose file" = "Seleccionar fitxer"; // translategemma:4b
"Stress tests" = "Tests de resistència"; // translategemma:4b
// Dashboard
"Serial number" = "Número de sèrie";
"Model identifier" = "Identificador de model";
"Production year" = "Any de producció";
"Uptime" = "Temps d'activitat";
"Number of cores" = "%0 nuclis";
"Number of threads" = "%0 fils";
"Number of e-cores" = "%0 nuclis d'eficiència";
"Number of p-cores" = "%0 nuclis de rendiment";
"Disks" = "Discs"; // translategemma:4b
"Display" = "Pantalla"; // translategemma:4b
// Update
"The latest version of Stats installed" = "L'última versió d'Stats està instal·lada";
"Downloading..." = "Baixant...";
"Current version: " = "Versió instal·lada: ";
"Latest version: " = "Última versió: ";
// Widgets
"Color" = "Color";
"Label" = "Etiqueta";
"Box" = "Caixa";
"Frame" = "Marc";
"Value" = "Valor";
"Colorize" = "Acoloreix";
"Colorize value" = "Valor de l'acoloriment";
"Additional information" = "Informació addicional";
"Reverse values order" = "Invertir l'ordre dels valors";
"Base" = "Base";
"Display mode" = "Mode de visualització";
"One row" = "Una fila";
"Two rows" = "Dues files";
"Mini widget" = "Mini widget";
"Line chart widget" = "Gràfic de línies";
"Bar chart widget" = "Gràfic de barres";
"Pie chart widget" = "Gràfic circular";
"Network chart widget" = "Gràfic de xarxa";
"Speed widget" = "Velocitat";
"Battery widget" = "Bateria";
"Stack widget" = "Pila"; // translategemma:4b
"Memory widget" = "Memòria";
"Static width" = "Ample fixe";
"Tachometer widget" = "Tacòmetre";
"State widget" = "Widget d'estat"; // translategemma:4b
"Text widget" = "Element de text"; // translategemma:4b
"Battery details widget" = "Widget de detalls de la bateria"; // translategemma:4b
"Show symbols" = "Mostra els símbols";
"Label widget" = "Etiqueta";
"Number of reads in the chart" = "Nombre de lectures al gràfic";
"Color of download" = "Color de la descàrrega";
"Color of upload" = "Color de càrrega";
"Monospaced font" = "Tipus de lletra monoespaiat";
"Reverse order" = "Canvia l'ordre";
"Chart history" = "Historial de gràfics";
"Default color" = "Color per determinat";
"Transparent when no activity" = "Transparent quan no hi hagi activitat";
"Constant color" = "Color constant";
// Module Kit
"Open module settings" = "Obre la configuració del mòdul";
"Select widget" = "Selecciona el widget %0";
"Open widget settings" = "Obre la configuració del widget";
"Update interval" = "Freqüència de refresc";
"Usage history" = "Historial d'ús";
"Details" = "Detalls";
"Top processes" = "Processos principals";
"Pictogram" = "Pictograma";
"Module" = "Mòdul";
"Widgets" = "Ginys";
"Popup" = "Finestra emergència"; // translategemma:4b
"Notifications" = "Notificacions";
"Merge widgets" = "Combina ginys";
"No available widgets to configure" = "No hi ha widgets disponibles per configurar";
"No options to configure for the popup in this module" = "No hi ha opcions per configurar per a la finestra emergent en aquest mòdul";
"Process" = "Procés";
"Kill process" = "Tanca procés";
"Keyboard shortcut" = "Atall de teclat"; // translategemma:4b
"Listening..." = "Escoltant..."; // translategemma:4b
// Modules
"Number of top processes" = "Nombre de processos principals";
"Update interval for top processes" = "Freqüència de refresc per als processos principals";
"Notification level" = "Nivell de notificació";
"Chart color" = "Color del gràfic";
"Main chart scaling" = "Escala del gràfic principal";
"Scale value" = "Valor d'escala";
"Text widget value" = "Valor del widget de text"; // translategemma:4b
// CPU
"CPU usage" = "Ús de la CPU";
"CPU temperature" = "Temperatura de la CPU";
"CPU frequency" = "Freqüència de la CPU";
"System" = "Sistema";
"User" = "Usuari";
"Idle" = "Inactiu";
"Show usage per core" = "Mostra l'ús per nucli";
"Show hyper-threading cores" = "Mostra els nuclis amb hyper-threading";
"Split the value (System/User)" = "Divideix el valor (Sistema/Usuari)";
"Scheduler limit" = "Límit del planificador";
"Speed limit" = "Límit de velocitat";
"Average load" = "Càrrega mitja";
"1 minute" = "1 minut";
"5 minutes" = "5 minuts";
"15 minutes" = "15 minuts";
"CPU usage threshold" = "Límit d'ús de la CPU";
"CPU usage is" = "L'ús de la CPU és %0";
"Efficiency cores" = "Nuclis d'eficiència";
"Performance cores" = "Nuclis de rendiment";
"System color" = "Color del sistema";
"User color" = "Color de l'usuari";
"Idle color" = "Sense color";
"Cluster grouping" = "Agrupament en cluster";
"Efficiency cores color" = "Color dels nuclis d'eficiència";
"Performance cores color" = "Color dels nuclis de rendiment";
"Total load" = "Càrrega total";
"System load" = "Càrrega del sistema";
"User load" = "Càrrega de l'usuari";
"Efficiency cores load" = "Càrrega dels nuclis d'eficiència";
"Performance cores load" = "Càrrega dels nuclis de rendiment";
"All cores" = "Totes les nuclis"; // translategemma:4b
// GPU
"GPU to show" = "GPU a mostrar";
"Show GPU type" = "Mostra el tipus de GPU";
"GPU enabled" = "GPU activada";
"GPU disabled" = "GPU desactivada";
"GPU temperature" = "Temperatura de la GPU";
"GPU utilization" = "Ús de la GPU";
"Vendor" = "Fabricant";
"Model" = "Model";
"Status" = "Estat";
"Active" = "Activa";
"Non active" = "Inactiva";
"Fan speed" = "Velocitat del ventilador";
"Core clock" = "Rellotge del nucli";
"Memory clock" = "Rellotge de la memòria";
"Utilization" = "Ús";
"Render utilization" = "Utilització del Render";
"Tiler utilization" = "Utilització del Tiler";
"GPU usage threshold" = "Límit d'ús de la GPU";
"GPU usage is" = "L'ús de la GPU és %0";
// RAM
"Memory usage" = "Ús de la memòria";
"Memory pressure" = "Pressió de la memòria";
"Total" = "Total";
"Used" = "Usada";
"App" = "Aplicació";
"Wired" = "Física";
"Compressed" = "Comprimida";
"Free" = "Lliure";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Dividir el valor (Aplicació/Física/Comprimida)";
"RAM utilization threshold" = "Límit d'ús de la RAM";
"RAM utilization is" = "L'ús de la RAM és %0";
"App color" = "Color de l'aplicació";
"Wired color" = "Color del Wired";
"Compressed color" = "Color de la comprimida";
"Free color" = "Color de la lliure";
"Free memory (less than)" = "Memòria lliure (menys de)";
"Swap size" = "Tamany del Swap";
"Free RAM is" = "La RAM lliure és %0";
// Disk
"Show removable disks" = "Mostra els discs extraïbles";
"Used disk memory" = "En ús %0 de %1";
"Free disk memory" = "Lliure %0 de %1";
"Disk to show" = "Disc a mostrar";
"Open disk" = "Disc obert";
"Switch view" = "Canviar vista";
"Disk utilization threshold" = "Límit d'ús de la utilització del disc";
"Disk utilization is" = "La utilització del disc és %0";
"Read color" = "Color per a la lectura";
"Write color" = "Color per a l'escriptura'";
"Disk usage" = "Ús del disc";
"Total read" = "Total llegit";
"Total written" = "Total escrit";
"Write speed" = "Velocitat de escriptura";
"Read speed" = "Velocitat de lectura";
"Drives" = "Discos"; // translategemma:4b
"SMART data" = "Dades SMART"; // translategemma:4b
// Sensors
"Temperature unit" = "Unitat de temperatura";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Guarda la velocitat del ventilador";
"Fan" = "Ventilador";
"HID sensors" = "Sensors HID";
"Synchronize fan's control" = "Sincronitza el control del ventilador";
"Current" = "Corrent";
"Energy" = "Energia";
"Show unknown sensors" = "Mostra sensors desconeguts";
"Install fan helper" = "Instal·la l'ajuda del ventilador";
"Uninstall fan helper" = "Desinstal·la l'ajuda del ventilador";
"Fan value" = "Valor del ventilador";
"Turn off fan" = "Apaga el ventilador";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Estàs a punt d'apagar el ventilador. No està recomanat i pot espatllar el teu mac, estàs segur/segura que vols fer això?";
"Sensor threshold" = "Límit del sensor";
"Left fan" = "Ventilador esquerre";
"Right fan" = "Ventilador dret";
"Fastest fan" = "Ventilador més ràpid";
"Sensor to show" = "Sensor per mostrar"; // translategemma:4b
// Network
"Uploading" = "Carregant";
"Downloading" = "Baixant";
"Public IP" = "IP Pública";
"Local IP" = "IP Local";
"Interface" = "Interfície";
"Physical address" = "Direcció física";
"Refresh" = "Actualitza";
"Click to copy public IP address" = "Clica per a copiar la direcció IP pública";
"Click to copy local IP address" = "Clica per a copiar la direcció IP local";
"Click to copy wifi name" = "Clica per a copiar el nom de la xarxa WiFi";
"Click to copy mac address" = "Clica per a copiar la direcció MAC";
"No connection" = "Sense connexió";
"Network interface" = "Interfície de xarxa";
"Total download" = "Baixada total";
"Total upload" = "Càrrega total";
"Reader type" = "Tipus de lector";
"Interface based" = "Interfície";
"Processes based" = "Procés";
"Reset data usage" = "Reinicia l'ús de dades";
"VPN mode" = "Mode VPN";
"Standard" = "Estàndar";
"Security" = "Seguretat";
"Channel" = "Canal";
"Common scale" = "Escala comuna";
"Autodetection" = "Autodetecció";
"Widget activation threshold" = "Límit d'activació del giny";
"Internet connection" = "Connexió a internet";
"Active state color" = "Color per l'estat actiu";
"Nonactive state color" = "Color per l'estat inactiu";
"Connectivity host (ICMP)" = "Connectivitat host (ICMP)";
"Leave empty to disable the check" = "Deixa en blanc per desactivar el check";
"Connectivity history" = "Historial de connectivitat";
"Auto-refresh public IP address" = "Refresca automàticament l'adreça pública IP";
"Every hour" = "Cada hora";
"Every 12 hours" = "Cada 12 hores";
"Every 24 hours" = "Cada 24 hores";
"Network activity" = "Activitat de la xarxa";
"Last reset" = "L'últim reinici va ser fa %0";
"Latency" = "Latència";
"Upload speed" = "Velocitat de pujada";
"Download speed" = "Velocitat de baixada";
"Address" = "Adreça"; // translategemma:4b
"WiFi network" = "Xarxa Wi-Fi"; // translategemma:4b
"Local IP changed" = "La direcció IP local s'ha modificat"; // translategemma:4b
"Public IP changed" = "La direcció IP pública ha canviat."; // translategemma:4b
"Previous IP" = "Adreça IP anterior: %0"; // translategemma:4b
"New IP" = "Nova IP: %0"; // translategemma:4b
"Internet connection lost" = "S'ha perdut la connexió a Internet"; // translategemma:4b
"Internet connection established" = "Connexió a Internet establerta"; // translategemma:4b
// Battery
"Level" = "Nivell";
"Source" = "Font";
"AC Power" = "Alimentació";
"Battery Power" = "Alimentat per bateria";
"Time" = "Temps";
"Health" = "Salut";
"Amperage" = "Amperatge";
"Voltage" = "Voltatge";
"Cycles" = "Nombre de cicles";
"Temperature" = "Temperatura";
"Power adapter" = "Adaptador de corrent";
"Power" = "Corrent";
"Is charging" = "Està carregant";
"Time to discharge" = "Temps fins la descàrrega completa";
"Time to charge" = "Temps fins la càrrega completa";
"Calculating" = "Calculant";
"Fully charged" = "Completament carregada";
"Not connected" = "No connectat";
"Low level notification" = "Notificació de nivell baix";
"High level notification" = "Notificació de nivell alt";
"Low battery" = "Bateria baixa";
"High battery" = "Bateria alta";
"Battery remaining" = "%0% restant";
"Battery remaining to full charge" = "%0% fins la càrrega completa";
"Percentage" = "Percentatge";
"Percentage and time" = "Percentatge i temps";
"Time and percentage" = "Temps i percentatge";
"Time format" = "Format de temps";
"Hide additional information when full" = "Oculta informació addicional quan la bateria estigui carregada";
"Last charge" = "Última càrrega";
"Capacity" = "Capacitat";
"current / maximum / designed" = "actual / màxima / dissenyada";
"Low power mode" = "Mode de baix consum";
"Percentage inside the icon" = "Percentatge dins de la icona";
"Colorize battery" = "Acoloreix la bateria";
"Charging current" = "Corrent de càrrega";
"Charging Voltage" = "Voltatge de càrrega";
"Charger state inside the battery" = "Estat del carregador dins de la bateria"; // translategemma:4b
// Bluetooth
"Battery to show" = "Bateria a mostrar";
"No Bluetooth devices are available" = "No hi han dispositius Bluetooth disponibles";
// Clock
"Time zone" = "Zona horària";
"Local" = "Local";
"Calendar" = "Calendari";
"Show week numbers" = "Mostrar els números de la setmana"; // translategemma:4b
"Local time" = "Hora local";
"Add new clock" = "Afegir un nou rellotge"; // translategemma:4b
"Delete selected clock" = "Eliminar l'horari seleccionat"; // translategemma:4b
"Help with datetime format" = "Ajuda amb el format de data i hora"; // translategemma:4b
// Colors
"Based on utilization" = "Basat en l'utilització";
"Based on pressure" = "Basat en la pressió";
"Based on cluster" = "Basat en el cluster";
"System accent" = "Accent del sistema";
"Monochrome accent" = "Accent monocromàtic";
"Clear" = "Clar";
"White" = "Blanc";
"Black" = "Negre";
"Gray" = "Gris";
"Second gray" = "Segon gris";
"Dark gray" = "Gris fosc";
"Light gray" = "Gris clar";
"Red" = "Vermell";
"Second red" = "Segon vermell";
"Green" = "Verd";
"Second green" = "Segon verd";
"Blue" = "Blau";
"Second blue" = "Segon blau";
"Yellow" = "Groc";
"Second yellow" = "Segon groc";
"Orange" = "Taronja";
"Second orange" = "Segon taronja";
"Purple" = "Morat";
"Second purple" = "Segon morat";
"Brown" = "Marró";
"Second brown" = "Segon marró";
"Cyan" = "Cian";
"Magenta" = "Magenta";
"Pink" = "Rosa";
"Teal" = "Verd-blau";
"Indigo" = "Indi";
================================================
FILE: Stats/Supporting Files/cs.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by @mpl75 on 11/01/2021.
//
//
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "Procesor"; // translategemma:4b
"Open CPU settings" = "Otevřít nastavení CPU";
"GPU" = "Grafická karta"; // translategemma:4b
"Open GPU settings" = "Otevřít nastavení GPU";
"RAM" = "RAM";
"Open RAM settings" = "Otevřít nastavení RAM";
"Disk" = "Disk";
"Open Disk settings" = "Otevřít nastavení disku";
"Sensors" = "Senzory";
"Open Sensors settings" = "Otevřít nastavení senzorů";
"Network" = "Síť";
"Open Network settings" = "Otevřít nastavení sítě";
"Battery" = "Baterie";
"Open Battery settings" = "Otevřít nastavení baterie";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Otevřít nastavení bluetooth";
"Clock" = "Hodiny"; // translategemma:4b
"Open Clock settings" = "Otevřít nastavení času"; // translategemma:4b
// Words
"Unknown" = "Neznámý";
"Version" = "Verze";
"Processor" = "Procesor";
"Memory" = "Paměť";
"Graphics" = "Grafika";
"Close" = "Zavřít";
"Download" = "Stáhnout";
"Install" = "Instalovat";
"Cancel" = "Zrušit";
"Unavailable" = "Nedostupné";
"Yes" = "Ano";
"No" = "Ne";
"Automatic" = "Automaticky";
"Manual" = "Manuálně";
"None" = "Žádný";
"Dots" = "Tečky";
"Arrows" = "Šipky";
"Characters" = "Znak";
"Short" = "Krátký";
"Long" = "Dlouhý";
"Statistics" = "Statistiky";
"Max" = "Max";
"Min" = "Min";
"Reset" = "Reset";
"Alignment" = "Zarovnání";
"Left alignment" = "Zarovnat doleva";
"Center alignment" = "Zarovnat na střed";
"Right alignment" = "Zarovnat doprava";
"Dashboard" = "Přehled"; // translategemma:4b
"Enabled" = "Povoleno";
"Disabled" = "Zakázáno";
"Silent" = "Tichý";
"Units" = "Jednotky";
"Fans" = "Ventilátory";
"Scaling" = "Měřítko";
"Linear" = "Lineární";
"Square" = "Čtvercový";
"Cube" = "Kubický";
"Logarithmic" = "Logaritmický";
"Fixed scale" = "Opraveno"; // translategemma:4b
"Cores" = "Jádra";
"Settings" = "Nastavení";
"Name" = "Jméno"; // translategemma:4b
"Format" = "Formát"; // translategemma:4b
"Turn off" = "Vypnout"; // translategemma:4b
"Normal" = "Normální"; // translategemma:4b
"Warning" = "Varování"; // translategemma:4b
"Critical" = "Kritický"; // translategemma:4b
"Usage" = "Použití"; // translategemma:4b
"2 minutes" = "2 minuty"; // translategemma:4b
"3 minutes" = "3 minuty"; // translategemma:4b
"10 minutes" = "10 minut"; // translategemma:4b
"Import" = "Import";
"Export" = "Export";
"Separator" = "Oddělovač"; // translategemma:4b
"Read" = "Přečtěte"; // translategemma:4b
"Write" = "Napište"; // translategemma:4b
"Frequency" = "Frekvence"; // translategemma:4b
"Save" = "Uložit"; // translategemma:4b
"Run" = "Spustit"; // translategemma:4b
"Stop" = "Zastav"; // translategemma:4b
"Uninstall" = "Odinstalovat"; // translategemma:4b
"1 sec" = "1 sekunda"; // translategemma:4b
"2 sec" = "2 sekundy"; // translategemma:4b
"3 sec" = "3 sekundy"; // translategemma:4b
"5 sec" = "5 sekund"; // translategemma:4b
"10 sec" = "10 sekund"; // translategemma:4b
"15 sec" = "15 sekund"; // translategemma:4b
"30 sec" = "30 sekund"; // translategemma:4b
"60 sec" = "60 sekund"; // translategemma:4b
// Setup
"Stats Setup" = "Nastavení Stats";
"Previous" = "Zpět";
"Previous page" = "Předchozí strana";
"Next" = "Dále";
"Next page" = "Další strana";
"Finish" = "Dokončit";
"Finish setup" = "Dokončit nastavení";
"Welcome to Stats" = "Vítejte ve Stats";
"welcome_message" = "Děkujeme, že používáte Stats, bezplatný open source monitor systému macOS pro nástrojovou lištu.";
"Start the application automatically when starting your Mac" = "Automatické spuštění aplikace při startu počítače Mac";
"Do not start the application automatically when starting your Mac" = "Nespouštějte aplikaci automaticky při spuštění Macu";
"Do everything silently in the background (recommended)" = "Provádět vše tiše na pozadí (doporučeno)";
"Check for a new version on startup" = "Kontrola nové verze při spuštění";
"Check for a new version every day (once a day)" = "Kontrola nové verze každý den (jednou denně)";
"Check for a new version every week (once a week)" = "Kontrola nové verze každý týden (jednou týdně)";
"Check for a new version every month (once a month)" = "Kontrola nové verze každý měsíc (jednou měsíčně)";
"Never check for updates (not recommended)" = "Nikdy nekontrolovat aktualizace (nedoporučuje se)";
"Anonymous telemetry for better development decisions" = "Anonymizovaná telemetrie pro lepší rozhodování při vývoji"; // translategemma:4b
"Share anonymous telemetry data" = "Sdílet anonymizovaná telemetrická data"; // translategemma:4b
"Do not share anonymous telemetry data" = "Neposkytujte anonymizovaná telemetrická data."; // translategemma:4b
"The configuration is completed" = "Konfigurace je dokončena";
"finish_setup_message" = "Vše je připraveno! \n Statistiky jsou open source nástroj, který je zdarma a vždy bude. \n Pokud se vám líbí, můžete projekt podpořit, vždy se to cení!";
// Alerts
"New version available" = "Nová verze k dispozici";
"Click to install the new version of Stats" = "Klikněte pro instalaci nové verze Stats";
"Successfully updated" = "Úspěšně aktualizováno";
"Stats was updated to v" = "Stats byly aktualizovány na v%0";
"Reset settings text" = "Všechna nastavení aplikace budou resetována a aplikace bude restartována. Jste si jisti, že to chcete udělat?";
"Support text" = "Děkujeme, že používáte Stats!\n\n Udržování a zlepšování tohoto open-source projektu vyžaduje čas a zdroje. Vaše podpora nám pomáhá nadále poskytovat bezplatnou a spolehlivou aplikaci pro každého.\n\nPokud vám Stats pomáhají, zvažte prosím možnost přispět. Každý malý kousek pomůže!";
// Settings
"Open Activity Monitor" = "Otevřít Monitor aktivity";
"Report a bug" = "Nahlásit chybu";
"Support the application" = "Podpořte aplikaci";
"Close application" = "Zavřít aplikaci";
"Open application settings" = "Otevřít nastavení aplikace";
"Open dashboard" = "Otevřít dashboard";
"No notifications available in this module" = "V tomto modulu nejsou k dispozici žádné upozornění."; // translategemma:4b
"Open Calendar" = "Otevřít kalendář"; // translategemma:4b
"Toggle the module" = "Povolte modul"; // translategemma:4b
// Application settings
"Update application" = "Aktualizovat aplikaci";
"Check for updates" = "Zkontrolovat aktualizace";
"At start" = "Při startu";
"Once per day" = "Jednou denně";
"Once per week" = "Jednou týdně";
"Once per month" = "Jednou měsíčně";
"Never" = "Nikdy";
"Check for update" = "Zkontrolovat aktualizace";
"Show icon in dock" = "Zobrazovat ikonu v Docku";
"Start at login" = "Spustit po přihlášení";
"Build number" = "Číslo sestavení";
"Import settings" = "Nastavení importu"; // translategemma:4b
"Export settings" = "Nastavení exportu"; // translategemma:4b
"Reset settings" = "Obnovit nastavení";
"Pause the Stats" = "Pozastavit Stats";
"Resume the Stats" = "Obnovit Stats";
"Combined modules" = "Kombinace modulů";
"Combined details" = "Souhrn údajů"; // translategemma:4b
"Spacing" = "Odsazení";
"Share anonymous telemetry" = "Sdílejte anonymizovaná telemetrická data"; // translategemma:4b
"Choose file" = "Vyberte soubor"; // translategemma:4b
"Stress tests" = "Testy zatížení"; // translategemma:4b
// Dashboard
"Serial number" = "Sériové číslo";
"Model identifier" = "Identifikátor modelu"; // translategemma:4b
"Production year" = "Rok výroby"; // translategemma:4b
"Uptime" = "Spuštěno";
"Number of cores" = "%0 jader";
"Number of threads" = "%0 vláken";
"Number of e-cores" = "%0 efektivních jader";
"Number of p-cores" = "%0 výkonných jader";
"Disks" = "Disky"; // translategemma:4b
"Display" = "Zobrazení"; // translategemma:4b
// Update
"The latest version of Stats installed" = "Byla nainstalována nejnovější verze Stats";
"Downloading..." = "Stahování...";
"Current version: " = "Aktuální verze: ";
"Latest version: " = "Nejnovější verze: ";
// Widgets
"Color" = "Barva";
"Label" = "Popisek";
"Box" = "Výplň";
"Frame" = "Okraj";
"Value" = "Hodnota";
"Colorize" = "Obarvit";
"Colorize value" = "Obarvit hodnotu";
"Additional information" = "Další informace";
"Reverse values order" = "Obrátit pořadí hodnot";
"Base" = "Základ";
"Display mode" = "Režim zobrazení";
"One row" = "Jeden řádek";
"Two rows" = "Dva řádky";
"Mini widget" = "Mini";
"Line chart widget" = "Čárový graf";
"Bar chart widget" = "Sloupcový graf";
"Pie chart widget" = "Koláčový graf";
"Network chart widget" = "Síťový graf";
"Speed widget" = "Rychlost";
"Battery widget" = "Baterie";
"Stack widget" = "Stav"; // translategemma:4b
"Memory widget" = "Paměť";
"Static width" = "Statická šířka";
"Tachometer widget" = "Tachometr";
"State widget" = "Stav";
"Text widget" = "Prvek pro zobrazení textu"; // translategemma:4b
"Battery details widget" = "Widget s informacemi o baterii"; // translategemma:4b
"Show symbols" = "Zobrazit symboly";
"Label widget" = "Popisek";
"Number of reads in the chart" = "Počet čtení v grafu";
"Color of download" = "Barva stahování";
"Color of upload" = "Barva nahrávání";
"Monospaced font" = "Font s pevnou šířkou znaků"; // translategemma:4b
"Reverse order" = "Způsob obrácený"; // translategemma:4b
"Chart history" = "Historie grafu"; // translategemma:4b
"Default color" = "Výchozí"; // translategemma:4b
"Transparent when no activity" = "Průhledný, když není žádná aktivita"; // translategemma:4b
"Constant color" = "Konstantní"; // translategemma:4b
// Module Kit
"Open module settings" = "Otevřít nastavení modulu";
"Select widget" = "Vyber widget %0";
"Open widget settings" = "Otevřít nastavení widgetu";
"Update interval" = "Interval aktualizace";
"Usage history" = "Historie použití";
"Details" = "Detaily";
"Top processes" = "První procesy";
"Pictogram" = "Piktogram";
"Module" = "Modul";
"Widgets" = "Widgety";
"Popup" = "Vyskakovat";
"Notifications" = "Oznámení";
"Merge widgets" = "Sloučit widgety";
"No available widgets to configure" = "Žádné dostupné widgety ke konfiguraci";
"No options to configure for the popup in this module" = "Žádné dostupné konfigurace vyskakovacího okna tohoto modulu";
"Process" = "Proces"; // translategemma:4b
"Kill process" = "Zastavit proces"; // translategemma:4b
"Keyboard shortcut" = "Klávesové zkratky"; // translategemma:4b
"Listening..." = "Poslouchám..."; // translategemma:4b
// Modules
"Number of top processes" = "Počet prvních procesů";
"Update interval for top processes" = "Interval aktualizace pro první procesy";
"Notification level" = "Úroveň oznámení";
"Chart color" = "Barva grafu";
"Main chart scaling" = "Hlavní škálování grafu"; // translategemma:4b
"Scale value" = "Hodnota měřítka"; // translategemma:4b
"Text widget value" = "Hodnota widgetu textu"; // translategemma:4b
// CPU
"CPU usage" = "Použití CPU";
"CPU temperature" = "Teplota CPU";
"CPU frequency" = "Frekvence CPU";
"System" = "Systém";
"User" = "Uživatel";
"Idle" = "Nečinný";
"Show usage per core" = "Zobrazit použití po jádrech";
"Show hyper-threading cores" = "Zobrazit jádra hyper-threading";
"Split the value (System/User)" = "Rozdělit hodnotu (Systém/Uživatel)";
"Scheduler limit" = "Omezení plánovače";
"Speed limit" = "Omezení rychlosti";
"Average load" = "Průměrné zatížení";
"1 minute" = "1 minuta";
"5 minutes" = "5 minut";
"15 minutes" = "15 minut";
"CPU usage threshold" = "Prahová hodnota využití CPU";
"CPU usage is" = "Využití procesoru je %0";
"Efficiency cores" = "Efektivní jádra";
"Performance cores" = "Výkonná jádra";
"System color" = "Systémová barva";
"User color" = "Barva uživatele";
"Idle color" = "Barva nečinnosti";
"Cluster grouping" = "Seskupení klastrů";
"Efficiency cores color" = "Efektivní jádra – barva"; // translategemma:4b
"Performance cores color" = "Barva výkonových jader"; // translategemma:4b
"Total load" = "Celková zátěž"; // translategemma:4b
"System load" = "Zátěž systému"; // translategemma:4b
"User load" = "Zátěž uživatele"; // translategemma:4b
"Efficiency cores load" = "Procesory pro efektivní zpracování jsou načítány."; // translategemma:4b
"Performance cores load" = "Načítání výkonových jader"; // translategemma:4b
"All cores" = "Všechny jádra"; // translategemma:4b
// GPU
"GPU to show" = "GPU k zobrazení";
"Show GPU type" = "Zobrazit typ GPU";
"GPU enabled" = "GPU aktivováno";
"GPU disabled" = "GPU deaktivováno";
"GPU temperature" = "Teplota GPU";
"GPU utilization" = "Využití GPU";
"Vendor" = "Prodejce";
"Model" = "Model";
"Status" = "Stav"; // translategemma:4b
"Active" = "Aktivní";
"Non active" = "Neaktivní";
"Fan speed" = "Rychlost ventilátoru";
"Core clock" = "Takt jádra";
"Memory clock" = "Takt pamětí";
"Utilization" = "Využití";
"Render utilization" = "Využití vykreslování";
"Tiler utilization" = "Využití tilingu";
"GPU usage threshold" = "Prahová hodnota využití GPU";
"GPU usage is" = "Využití GPU je %0";
// RAM
"Memory usage" = "Využití paměti";
"Memory pressure" = "Zatížení paměti";
"Total" = "Celkem";
"Used" = "Využitá";
"App" = "Aplikační";
"Wired" = "Pevná";
"Compressed" = "Komprese";
"Free" = "Volná";
"Swap" = "Odkládací";
"Split the value (App/Wired/Compressed)" = "Rozdělit hodnotu (Aplikační/Pevná/Komprese)";
"RAM utilization threshold" = "Prahová hodnota využití paměti";
"RAM utilization is" = "Využití paměti je %0";
"App color" = "Barva aplikační paměti";
"Wired color" = "Barva pevné paměti";
"Compressed color" = "Barva kompresované paměti";
"Free color" = "Barva volné paměti";
"Free memory (less than)" = "Volná paměť (méně než)"; // translategemma:4b
"Swap size" = "Velikost paměti pro výměnu"; // translategemma:4b
"Free RAM is" = "Volná RAM je %0"; // translategemma:4b
// Disk
"Show removable disks" = "Zobrazovat vyměnitelné disky";
"Used disk memory" = "Využito %0 z %1";
"Free disk memory" = "Volno %0 z %1";
"Disk to show" = "Zobrazit disk";
"Open disk" = "Otevřít disk";
"Switch view" = "Přepnout pohled";
"Disk utilization threshold" = "Prahová hodnota využití disku";
"Disk utilization is" = "Využití disku je %0";
"Read color" = "Přečtěte si informace o barvách"; // translategemma:4b
"Write color" = "Napište barvu"; // translategemma:4b
"Disk usage" = "Použití disku"; // translategemma:4b
"Total read" = "Celkový počet přečtených dat"; // translategemma:4b
"Total written" = "Celkem napsáno"; // translategemma:4b
"Write speed" = "Napište"; // translategemma:4b
"Read speed" = "Přečtěte"; // translategemma:4b
"Drives" = "Řídící jednotky"; // translategemma:4b
"SMART data" = "Data SMART"; // translategemma:4b
// Sensors
"Temperature unit" = "Jednotka teploty";
"Celsius" = "Celcius"; // translategemma:4b
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Uložit rychlost ventilátoru";
"Fan" = "Ventilátor";
"HID sensors" = "HID senzory";
"Synchronize fan's control" = "Synchronizace ovládání ventilátoru";
"Current" = "Proud";
"Energy" = "Energie";
"Show unknown sensors" = "Zobrazit neznáme senzory";
"Install fan helper" = "Nainstalovat pomocnou aplikaci ovládání ventilátorů";
"Uninstall fan helper" = "Odinstalovat pomocnou aplikaci ventilátorů";
"Fan value" = "Hodnota ventilátoru";
"Turn off fan" = "Vypněte ventilátor"; // translategemma:4b
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Budete vypínat ventilátor. Toto není doporučená akce, která může poškodit váš Mac. Jste si jisti, že to chcete udělat?"; // translategemma:4b
"Sensor threshold" = "Hranice senzoru"; // translategemma:4b
"Left fan" = "Vlevo"; // translategemma:4b
"Right fan" = "Správně"; // translategemma:4b
"Fastest fan" = "Nejrychlejší"; // translategemma:4b
"Sensor to show" = "Senzor zobrazuje"; // translategemma:4b
// Network
"Uploading" = "Nahrávání";
"Downloading" = "Stahování";
"Public IP" = "Veřejná IP";
"Local IP" = "Místní IP";
"Interface" = "Rozhraní";
"Physical address" = "Fyzická adresa";
"Refresh" = "Obnovit";
"Click to copy public IP address" = "Klikněte pro zkopírování veřejné IP adresy";
"Click to copy local IP address" = "Klikněte pro zkopírování místní IP adresy";
"Click to copy wifi name" = "Klikněte pro zkopírování názvu wifi";
"Click to copy mac address" = "Klikněte pro zkopírování MAC adresy";
"No connection" = "Žádné připojení";
"Network interface" = "Síťové rozhraní";
"Total download" = "Celkem staženo";
"Total upload" = "Celkem nahráno";
"Reader type" = "Typ přehledu";
"Interface based" = "Podle rozhraní";
"Processes based" = "Podle procesů";
"Reset data usage" = "Resetovat využití dat";
"VPN mode" = "VPN režim";
"Standard" = "Standard";
"Security" = "Zabezpečení";
"Channel" = "Kanál";
"Common scale" = "Společná stupnice";
"Autodetection" = "Automatická detekce";
"Widget activation threshold" = "Práh aktivace widgetu";
"Internet connection" = "Připojení k internetu";
"Active state color" = "Barva aktivního stavu";
"Nonactive state color" = "Barva neaktivního stavu";
"Connectivity host (ICMP)" = "Hostitel připojení (ICMP)";
"Leave empty to disable the check" = "Pro vypnutí kontroly ponechte prázdné pole";
"Connectivity history" = "Historie připojení"; // translategemma:4b
"Auto-refresh public IP address" = "Automatické obnovení veřejné IP adresy"; // translategemma:4b
"Every hour" = "Každou hodinu"; // translategemma:4b
"Every 12 hours" = "Každých 12 hodin"; // translategemma:4b
"Every 24 hours" = "Každých 24 hodin"; // translategemma:4b
"Network activity" = "Aktivita sítě"; // translategemma:4b
"Last reset" = "Poslední reset před %0 dny"; // translategemma:4b
"Latency" = "Latence"; // translategemma:4b
"Upload speed" = "Nahrazení"; // translategemma:4b
"Download speed" = "Stáhnout"; // translategemma:4b
"Address" = "Adresa"; // translategemma:4b
"WiFi network" = "Síť Wi-Fi"; // translategemma:4b
"Local IP changed" = "Lokální IP adresa se změnila."; // translategemma:4b
"Public IP changed" = "Veřejná IP adresa se změnila."; // translategemma:4b
"Previous IP" = "Předchozí IP: %0"; // translategemma:4b
"New IP" = "Nová IP adresa: %0"; // translategemma:4b
"Internet connection lost" = "Přerušeno připojení k internetu"; // translategemma:4b
"Internet connection established" = "Připojení k internetu zřízeno"; // translategemma:4b
// Battery
"Level" = "Úroveň";
"Source" = "Zdroj";
"AC Power" = "Napájení ze sítě";
"Battery Power" = "Napájení z baterie";
"Time" = "Čas";
"Health" = "Zdraví";
"Amperage" = "Proud";
"Voltage" = "Napětí";
"Cycles" = "Počet cyklů";
"Temperature" = "Teplota";
"Power adapter" = "Síťový adaptér";
"Power" = "Výkon";
"Is charging" = "Nabíjí se";
"Time to discharge" = "Čas do vybití";
"Time to charge" = "Čas do nabití";
"Calculating" = "Počítání";
"Fully charged" = "Plně nabito";
"Not connected" = "Nepřipojeno";
"Low level notification" = "Upozornění při nízké úrovni";
"High level notification" = "Upozornění při vysoké úrovni";
"Low battery" = "Nízká úroveň baterie";
"High battery" = "Vysoká úroveň baterie";
"Battery remaining" = "Zbývá %0 %";
"Battery remaining to full charge" = "%0% do úplného nabití";
"Percentage" = "Procenta";
"Percentage and time" = "Procenta a čas";
"Time and percentage" = "Čas a procenta";
"Time format" = "Formát času";
"Hide additional information when full" = "Skrýt další informace při plném nabití";
"Last charge" = "Poslední nabití";
"Capacity" = "Kapacita";
"current / maximum / designed" = "current / maximum / výrobní";
"Low power mode" = "Režim nízké spotřeby";
"Percentage inside the icon" = "Procento uvnitř ikony";
"Colorize battery" = "Zbarvě baterii"; // translategemma:4b
"Charging current" = "Proudnost nabíjení"; // translategemma:4b
"Charging Voltage" = "Napěťová hodnota pro nabíjení"; // translategemma:4b
"Charger state inside the battery" = "Stav nabíjení uvnitř baterie"; // translategemma:4b
// Bluetooth
"Battery to show" = "Baterie k zobrazení";
"No Bluetooth devices are available" = "Žádná bluetooth zařízení nejsou k dispozici";
// Clock
"Time zone" = "Časová zóna"; // translategemma:4b
"Local" = "Místní"; // translategemma:4b
"Calendar" = "Kalendář"; // translategemma:4b
"Show week numbers" = "Zobrazte čísla týdnů"; // translategemma:4b
"Local time" = "Místní čas"; // translategemma:4b
"Add new clock" = "Přidejte nové hodiny"; // translategemma:4b
"Delete selected clock" = "Smaž vybrané hodiny"; // translategemma:4b
"Help with datetime format" = "Pomoc s formátováním dat a času"; // translategemma:4b
// Colors
"Based on utilization" = "Na základě využití";
"Based on pressure" = "Na základě tlaku";
"Based on cluster" = "Na základě skupiny"; // translategemma:4b
"System accent" = "Systém";
"Monochrome accent" = "Černobílá";
"Clear" = "Průhledná";
"White" = "Bílá";
"Black" = "Černá";
"Gray" = "Šedá";
"Second gray" = "Alternativní šedá";
"Dark gray" = "Tmavá šedá";
"Light gray" = "Světlá šedá";
"Red" = "Červená";
"Second red" = "Alternativní červená";
"Green" = "Zelená";
"Second green" = "Alternativní zelená";
"Blue" = "Modrá";
"Second blue" = "Alternativní modrá";
"Yellow" = "Žlutá";
"Second yellow" = "Alternativní žlutá";
"Orange" = "Oranžová";
"Second orange" = "Alternativní oranžová";
"Purple" = "Nachová";
"Second purple" = "Alternativní nachová";
"Brown" = "Hnědá";
"Second brown" = "Alternativní hnědá";
"Cyan" = "Tyrkysová";
"Magenta" = "Purpurová";
"Pink" = "Růžová";
"Teal" = "Šedozelená";
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/da.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Casper Sørensen (casperes1996) on 18/11/2021, updated by Aleksander Bang-Larsen (aleksanderbl29) on 11/06/2023, 28/06/2023 and 25/08/2023.
// Using Swift 5.0.
// Running on macOS 13.4.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "Åben indstillinger for CPU";
"GPU" = "GPU";
"Open GPU settings" = "Åben indstillinger for GPU";
"RAM" = "RAM";
"Open RAM settings" = "Åben indstillinger for RAM";
"Disk" = "Disk";
"Open Disk settings" = "Åben indstillinger for disk";
"Sensors" = "Sensorer";
"Open Sensors settings" = "Åben indstillinger for sensorer";
"Network" = "Netværk";
"Open Network settings" = "Åben indstillinger for netværk";
"Battery" = "Batteri";
"Open Battery settings" = "Åben indstillinger for batteri";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Åben indstillinger for bluetooth";
"Clock" = "Ur";
"Open Clock settings" = "Åben indstillinger for ur";
// Words
"Unknown" = "Ukendt";
"Version" = "Version";
"Processor" = "Processor";
"Memory" = "Hukommelse";
"Graphics" = "Grafik";
"Close" = "Luk";
"Download" = "Download";
"Install" = "Installér";
"Cancel" = "Annuller";
"Unavailable" = "Utilgængelig";
"Yes" = "Ja";
"No" = "Nej";
"Automatic" = "Automatisk";
"Manual" = "Manuelt"; // As in opposed to automatic, not a manual. A manual would be translated as manual, same as English.
"None" = "Ingen";
"Dots" = "Prikker";
"Arrows" = "Pile";
"Characters" = "Tegn";
"Short" = "Kort";
"Long" = "Lang";
"Statistics" = "Statistik";
"Max" = "Max";
"Min" = "Mit"; // translategemma:4b
"Reset" = "Nulstil";
"Alignment" = "Opstilling"; //As in text alignment
"Left alignment" = "Venstrejusteret";
"Center alignment" = "Midterjusteret";
"Right alignment" = "Højrejusteret";
"Dashboard" = "Instrumentbræt";
"Enabled" = "Aktiveret";
"Disabled" = "Deaktiveret";
"Silent" = "Stille";
"Units" = "Enheder";
"Fans" = "Blæsere";
"Scaling" = "Skalering";
"Linear" = "Lineær";
"Square" = "Kvadrat"; // translategemma:4b
"Cube" = "Kube"; // translategemma:4b
"Logarithmic" = "Logaritmisk";
"Fixed scale" = "Løst"; // translategemma:4b
"Cores" = "Kerner";
"Settings" = "Indstillinger";
"Name" = "Navn";
"Format" = "Format";
"Turn off" = "Sluk";
"Normal" = "Normal";
"Warning" = "Advarsel"; // translategemma:4b
"Critical" = "Afgørende"; // translategemma:4b
"Usage" = "Brug"; // translategemma:4b
"2 minutes" = "2 minutter"; // translategemma:4b
"3 minutes" = "3 minutter"; // translategemma:4b
"10 minutes" = "10 minutter"; // translategemma:4b
"Import" = "Importer"; // translategemma:4b
"Export" = "Eksport"; // translategemma:4b
"Separator" = "Adskiller"; // translategemma:4b
"Read" = "Læs"; // translategemma:4b
"Write" = "Skriv"; // translategemma:4b
"Frequency" = "Frekvens"; // translategemma:4b
"Save" = "Gem"; // translategemma:4b
"Run" = "Kør"; // translategemma:4b
"Stop" = "Stop";
"Uninstall" = "Afinstallér"; // translategemma:4b
"1 sec" = "1 sekund"; // translategemma:4b
"2 sec" = "2 sek"; // translategemma:4b
"3 sec" = "3 sek"; // translategemma:4b
"5 sec" = "5 sekunder"; // translategemma:4b
"10 sec" = "10 sek"; // translategemma:4b
"15 sec" = "15 sek"; // translategemma:4b
"30 sec" = "30 sek"; // translategemma:4b
"60 sec" = "60 sek."; // translategemma:4b
// Setup
"Stats Setup" = "Stats opsætning";
"Previous" = "Forrige";
"Previous page" = "Forrige side";
"Next" = "Næste";
"Next page" = "Næste side";
"Finish" = "Afslut";
"Finish setup" = "Afslut opsætning";
"Welcome to Stats" = "Velkommen til Stats";
"welcome_message" = "Tak for, at du bruger Stats, det gratis, open source, program til at overvåge din Mac fra menulinjen.";
"Start the application automatically when starting your Mac" = "Start programmet automatisk når din Mac tænder";
"Do not start the application automatically when starting your Mac" = "Start ikke automatisk programmet når din Mac tænder";
"Do everything silently in the background (recommended)" = "Foretag alle handlinger i baggrunden (anbefalet)";
"Check for a new version on startup" = "Søg efter ny opdatering ved programstart";
"Check for a new version every day (once a day)" = "Søg efter ny opdatering hver dag (en gang dagligt)";
"Check for a new version every week (once a week)" = "Søg efter ny opdatering hver uge (en gang om ugen)";
"Check for a new version every month (once a month)" = "Søg efter ny opdatering hver måned (en gang hver måned)";
"Never check for updates (not recommended)" = "Søg aldrig efter nye opdateringer (ikke anbefalet)";
"Anonymous telemetry for better development decisions" = "Anonym brugsdata til at forbedre udviklingen af programmet";
"Share anonymous telemetry data" = "Del anonym data om brugen af programmet";
"Do not share anonymous telemetry data" = "Del ikke anonym data om brugen af programmet";
"The configuration is completed" = "Opsætning gennemført!";
"finish_setup_message" = "Alting er opsat! \n Stats er et open source værktøj, det er gratis og vil altid være det. \n Hvis du nyder Stats er du meget velkommen til at støtte projektet!";
// Alerts
"New version available" = "Ny udgave tilgængelig";
"Click to install the new version of Stats" = "Klik for at installere den nye udgave af Stats";
"Successfully updated" = "Opdatering succesfuld";
"Stats was updated to v" = "Stats er blevet opdateret til v%0";
"Reset settings text" = "Alle programmets indstillinger vil blive nulstillet og programmet genstartes. Er du sikker på du vil fortsætte?";
"Support text" = "Tak, fordi du bruger Stats!\n\n Det tager tid og ressourcer at vedligeholde og forbedre dette open source-projekt. Din støtte hjælper os med at fortsætte med at levere en gratis og pålidelig applikation til alle.\n\nHvis du finder Stats nyttigt, kan du overveje at give et bidrag. Hver eneste lille smule hjælper!";
// Settings
"Open Activity Monitor" = "Åben Aktivitetsmonitor";
"Report a bug" = "Rapportér en fejl";
"Support the application" = "Støt programmets udvikling";
"Close application" = "Luk programmet";
"Open application settings" = "Åben applikationsindstillinger";
"Open dashboard" = "Åben instrumentbræt";
"No notifications available in this module" = "Ingen notifikationer er tilgængelige i dette modul"; // translategemma:4b
"Open Calendar" = "Åbn kalender"; // translategemma:4b
"Toggle the module" = "Aktiver/deaktiver modulet"; // translategemma:4b
// Application settings
"Update application" = "Opdatér applikation";
"Check for updates" = "Søg efter opdateringer";
"At start" = "Ved opstart";
"Once per day" = "Én gang om dagen";
"Once per week" = "Én gang om ugen";
"Once per month" = "Én gang om måneden";
"Never" = "Aldrig";
"Check for update" = "Søg efter opdatering";
"Show icon in dock" = "Vis ikon i Dock";
"Start at login" = "Start ved login";
"Build number" = "Byggeversion"; // translategemma:4b
"Import settings" = "Importindstillinger"; // translategemma:4b
"Export settings" = "Eksportindstillinger"; // translategemma:4b
"Reset settings" = "Nulstil indstillinger";
"Pause the Stats" = "Sæt Stats på pause";
"Resume the Stats" = "Genoptag Stats";
"Combined modules" = "Kombinerede moduler";
"Combined details" = "Sammenfattede oplysninger"; // translategemma:4b
"Spacing" = "Mellemrum";
"Share anonymous telemetry" = "Del anonym brugsdata";
"Choose file" = "Vælg fil"; // translategemma:4b
"Stress tests" = "Belastningstest"; // translategemma:4b
// Dashboard
"Serial number" = "Serienummer";
"Model identifier" = "Model-identifikator"; // translategemma:4b
"Production year" = "Produktionsår"; // translategemma:4b
"Uptime" = "Oppetid";
"Number of cores" = "%0 kerner";
"Number of threads" = "%0 tråde";
"Number of e-cores" = "%0 efficiency kerner";
"Number of p-cores" = "%0 performance kerner";
"Disks" = "Skiver"; // translategemma:4b
"Display" = "Skærm"; // translategemma:4b
// Update
"The latest version of Stats installed" = "Den nyeste udgave af Stats er installeret";
"Downloading..." = "Downloader...";
"Current version: " = "Nuværende version: ";
"Latest version: " = "Nyeste version: ";
// Widgets
"Color" = "Farve";
"Label" = "Tekstmærke";
"Box" = "Boks";
"Frame" = "Ramme";
"Value" = "Værdi";
"Colorize" = "Farve";
"Colorize value" = "Farve værdi";
"Additional information" = "Yderligere information";
"Reverse values order" = "Omvend værdi-rækkefølge";
"Base" = "Grundlæggende"; // translategemma:4b
"Display mode" = "Visningstilstand";
"One row" = "Én række";
"Two rows" = "Two rækker";
"Mini widget" = "Mini";
"Line chart widget" = "Linjegraf";
"Bar chart widget" = "Bjælegraf";
"Pie chart widget" = "Cirkeldiagram";
"Network chart widget" = "Netværksgraf";
"Speed widget" = "Hastighed";
"Battery widget" = "Batteri";
"Stack widget" = "Stablet";
"Memory widget" = "Hukommelse";
"Static width" = "Statisk bredde";
"Tachometer widget" = "Omdrejningstæller";
"State widget" = "Status";
"Text widget" = "Tekstwidget"; // translategemma:4b
"Battery details widget" = "Widget til at vise batteriets detaljer"; // translategemma:4b
"Show symbols" = "Vis symboler";
"Label widget" = "Etikett"; // translategemma:4b
"Number of reads in the chart" = "Antal læsninger i grafen";
"Color of download" = "Download farve";
"Color of upload" = "Upload farve";
"Monospaced font" = "Monospaced skrifttype";
"Reverse order" = "Omvend rækkefølge"; // translategemma:4b
"Chart history" = "Diagramhistorik"; // translategemma:4b
"Default color" = "Standard"; // translategemma:4b
"Transparent when no activity" = "Gennemsigtig, når der ikke er nogen aktivitet"; // translategemma:4b
"Constant color" = "Konstant"; // translategemma:4b
// Module Kit
"Open module settings" = "Åben indstillinger for moduler";
"Select widget" = "Vælg %0 widget";
"Open widget settings" = "Åben indstillinger for widgets";
"Update interval" = "Opdateringsinterval";
"Usage history" = "Brugshistorik";
"Details" = "Detaljer";
"Top processes" = "Top processer";
"Pictogram" = "Piktogram";
"Module" = "Modul";
"Widgets" = "Widget"; // translategemma:4b
"Popup" = "Pop op";
"Notifications" = "Meddelelser";
"Merge widgets" = "Sammenlæg widgets";
"No available widgets to configure" = "Ingen widgets tilgængelig for konfiguration";
"No options to configure for the popup in this module" = "Det er ikke muligt at konfigurere popup'en i dette modul";
"Process" = "Proces"; // translategemma:4b
"Kill process" = "Afslut proces"; // translategemma:4b
"Keyboard shortcut" = "Tastaturgenvej"; // translategemma:4b
"Listening..." = "Lytter..."; // translategemma:4b
// Modules
"Number of top processes" = "Antal topprocesser";
"Update interval for top processes" = "Opdateringsinterval for topprocesser";
"Notification level" = "Notifikationsniveau";
"Chart color" = "Grafens farve";
"Main chart scaling" = "Primær diagramskala"; // translategemma:4b
"Scale value" = "Skala værdi"; // translategemma:4b
"Text widget value" = "Værdien for tekstwidget"; // translategemma:4b
// CPU
"CPU usage" = "CPU brug";
"CPU temperature" = "CPU temperatur";
"CPU frequency" = "CPU frekvens";
"System" = "System";
"User" = "Bruger";
"Idle" = "Ikke i brug";
"Show usage per core" = "Vis brug per kærne";
"Show hyper-threading cores" = "Vis hyper-threading kærner";
"Split the value (System/User)" = "Opdel værdier (System/Bruger)";
"Scheduler limit" = "Schedulerbegrænsning";
"Speed limit" = "Hastighedsbegræsning";
"Average load" = "Gennemsnitlig belastning";
"1 minute" = "1 minut";
"5 minutes" = "5 minuter";
"15 minutes" = "15 minuter";
"CPU usage threshold" = "Grænseværdi for CPU brug";
"CPU usage is" = "CPU brug er %0";
"Efficiency cores" = "Efficiency kerner";
"Performance cores" = "Performance kerner";
"System color" = "Systemets farve";
"User color" = "Bruger farve";
"Idle color" = "'Ikke i brug' farve";
"Cluster grouping" = "Grupper efter type";
"Efficiency cores color" = "Efficiency kerner farve";
"Performance cores color" = "Performance kerner farve";
"Total load" = "Samlet belastning"; // translategemma:4b
"System load" = "Systembelastning"; // translategemma:4b
"User load" = "Brugerbelastning"; // translategemma:4b
"Efficiency cores load" = "Effektivitetskerner indlæses"; // translategemma:4b
"Performance cores load" = "Ydestykkerne indlæses"; // translategemma:4b
"All cores" = "Alle kerner"; // translategemma:4b
// GPU
"GPU to show" = "GPU der vises";
"Show GPU type" = "Vis GPU type";
"GPU enabled" = "GPU aktiveret";
"GPU disabled" = "GPU deaktiveret";
"GPU temperature" = "GPU temperatur";
"GPU utilization" = "GPU brugsniveau";
"Vendor" = "Leverandør";
"Model" = "Model";
"Status" = "Status";
"Active" = "Aktiv";
"Non active" = "Ikke aktiv";
"Fan speed" = "Blæserhastighed";
"Core clock" = "Kærnefrekvens";
"Memory clock" = "Hukommelsesfrekvens";
"Utilization" = "Brugsniveau";
"Render utilization" = "Brug af rendering";
"Tiler utilization" = "Brug af tiler";
"GPU usage threshold" = "Grænseværdi for GPU brug";
"GPU usage is" = "GPU brug er %0";
// RAM
"Memory usage" = "Hukommelse i brug";
"Memory pressure" = "Hukommelsebelastning";
"Total" = "Samlet"; // translategemma:4b
"Used" = "Brugt";
"App" = "App";
"Wired" = "Kabllet"; // translategemma:4b
"Compressed" = "Komprimeret";
"Free" = "Ledig";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Opdel værdierne (App/Wired/Komprimeret)";
"RAM utilization threshold" = "Grænseværdi for RAM udnyttelse";
"RAM utilization is" = "RAM udnyttelse er %0";
"App color" = "App farve";
"Wired color" = "Brugt farve";
"Compressed color" = "Komprimeret farve";
"Free color" = "Fri farve";
"Free memory (less than)" = "Gratis hukommelse (mindre end)"; // translategemma:4b
"Swap size" = "Udvekslingsstørrelse"; // translategemma:4b
"Free RAM is" = "Ledig RAM er %0"; // translategemma:4b
// Disk
"Show removable disks" = "Vis eksterne diske";
"Used disk memory" = "%0 af %1 i brug";
"Free disk memory" = "%0 af %1 ledig";
"Disk to show" = "Disk der vises";
"Open disk" = "Åben disk";
"Switch view" = "Skift visning";
"Disk utilization threshold" = "Grænseværdi for diskbrug";
"Disk utilization is" = "Disk brug er %0";
"Read color" = "Read farve";
"Write color" = "Write farve";
"Disk usage" = "Disk brug";
"Total read" = "Samlet læsetid"; // translategemma:4b
"Total written" = "Samlet skriftligt"; // translategemma:4b
"Write speed" = "Skriv"; // translategemma:4b
"Read speed" = "Læs"; // translategemma:4b
"Drives" = "Drivkraft"; // translategemma:4b
"SMART data" = "SMART-data"; // translategemma:4b
// Sensors
"Temperature unit" = "Temperaturenhed";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Gem blæserhastighed";
"Fan" = "Blæser";
"HID sensors" = "HID sensorer";
"Synchronize fan's control" = "Synkroniser blæserkontrol";
"Current" = "Strøm";
"Energy" = "Energi";
"Show unknown sensors" = "Vis ukendte sensorer";
"Install fan helper" = "Tillad blæserkontrol";
"Uninstall fan helper" = "Afinstaller blæserkontrol";
"Fan value" = "Blæserværdi";
"Turn off fan" = "Slå blæseren fra"; // translategemma:4b
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Du er ved at slukke for blæseren. Dette er ikke en anbefalet handling, da det kan skade din Mac. Er du sikker på du vil fortsætte?";
"Sensor threshold" = "Sensorgrænse"; // translategemma:4b
"Left fan" = "Venstre"; // translategemma:4b
"Right fan" = "Korrekt"; // translategemma:4b
"Fastest fan" = "Hurtigst"; // translategemma:4b
"Sensor to show" = "Sensor til at vise"; // translategemma:4b
// Network
"Uploading" = "Upload";
"Downloading" = "Download";
"Public IP" = "Offentlig IP";
"Local IP" = "Lokal IP";
"Interface" = "Grænseflade"; // translategemma:4b
"Physical address" = "Fysisk adresse";
"Refresh" = "Genopfrisk";
"Click to copy public IP address" = "Klik for at kopiere offentlig IP";
"Click to copy local IP address" = "Klik for at kopiere lokal IP";
"Click to copy wifi name" = "Klik for at kopiere wifi-navn";
"Click to copy mac address" = "Klik for at kopiere mac-adresse";
"No connection" = "Ingen forbindelse";
"Network interface" = "Netværksinterface";
"Total download" = "Download i alt";
"Total upload" = "Upload i alt";
"Reader type" = "Læsertype";
"Interface based" = "Interface baseret";
"Processes based" = "Process baseret";
"Reset data usage" = "Nulstil databrug";
"VPN mode" = "VPN-tilstand";
"Standard" = "Standard";
"Security" = "Sikkerhed";
"Channel" = "Kanal";
"Common scale" = "Ens skalering";
"Autodetection" = "Automatisk";
"Widget activation threshold" = "Grænseværdi for aktivering af widget";
"Internet connection" = "Internetforbindelse";
"Active state color" = "Aktiv farve";
"Nonactive state color" = "Inaktiv farve";
"Connectivity host (ICMP)" = "Host for forbindelse (ICMP)"; // translategemma:4b
"Leave empty to disable the check" = "Lad feltet være tom for at deaktivere tjekket";
"Connectivity history" = "Forbindelseshistorik";
"Auto-refresh public IP address" = "Automatisk opdatering af offentlig IP adresse";
"Every hour" = "Hver time";
"Every 12 hours" = "Hver 12 timer";
"Every 24 hours" = "Hver 24 timer";
"Network activity" = "Netværksaktivitet";
"Last reset" = "Sidste nulstilling %0 siden";
"Latency" = "Forsinkelse"; // translategemma:4b
"Upload speed" = "Upload";
"Download speed" = "Download";
"Address" = "Adresse"; // translategemma:4b
"WiFi network" = "WiFi-netværk"; // translategemma:4b
"Local IP changed" = "Det lokale IP-adresse har ændret sig"; // translategemma:4b
"Public IP changed" = "Den offentlige IP-adresse er ændret"; // translategemma:4b
"Previous IP" = "Tidligere IP-adresse: %0"; // translategemma:4b
"New IP" = "Ny IP-adresse: %0"; // translategemma:4b
"Internet connection lost" = "Internetforbindelsen er mistet"; // translategemma:4b
"Internet connection established" = "Internetforbindelse etableret"; // translategemma:4b
// Battery
"Level" = "Niveau";
"Source" = "Kilde";
"AC Power" = "Strøm fra AC";
"Battery Power" = "Strøm fra batteri";
"Time" = "Tid";
"Health" = "Helbred";
"Amperage" = "Ampere";
"Voltage" = "Volt";
"Cycles" = "Cykler";
"Temperature" = "Temperatur";
"Power adapter" = "Strømforsyning";
"Power" = "Strøm";
"Is charging" = "Oplader";
"Time to discharge" = "Tid til afladning";
"Time to charge" = "Tid til opladning";
"Calculating" = "Udregner";
"Fully charged" = "Fuldt opladt";
"Not connected" = "Ikke forbundet";
"Low level notification" = "Notifikation om lavt niveau";
"High level notification" = "Notifikation om højt niveau";
"Low battery" = "Lavt batteri";
"High battery" = "Højt batteri";
"Battery remaining" = "%0% Tilbage";
"Battery remaining to full charge" = "%0% Indtil fuldt opladt";
"Percentage" = "Procent";
"Percentage and time" = "Procent og tid";
"Time and percentage" = "Tid og procent";
"Time format" = "Tidsformat";
"Hide additional information when full" = "Skjul yderligere information når fuldt opladt";
"Last charge" = "Sidst opladt";
"Capacity" = "Kapacitet";
"current / maximum / designed" = "nuværende / maksimal / designet"; // translategemma:4b
"Low power mode" = "Energibesparende funktion";
"Percentage inside the icon" = "Procenttegn i ikonet";
"Colorize battery" = "Farvelæg batteri";
"Charging current" = "Lade-strøm"; // translategemma:4b
"Charging Voltage" = "Opladningsspænding"; // translategemma:4b
"Charger state inside the battery" = "Tilstand af opladningssystemet inde i batteriet"; // translategemma:4b
// Bluetooth
"Battery to show" = "Batteri der vises";
"No Bluetooth devices are available" = "Ingen tilgængelige Bluetoothenheder";
// Clock
"Time zone" = "Tidszone";
"Local" = "Lokalt";
"Calendar" = "Kalender"; // translategemma:4b
"Show week numbers" = "Vis ugenummer"; // translategemma:4b
"Local time" = "Lokal tid"; // translategemma:4b
"Add new clock" = "Tilføj et nyt ur"; // translategemma:4b
"Delete selected clock" = "Slet det valgte ur"; // translategemma:4b
"Help with datetime format" = "Hjælp med at formatere dato og tid"; // translategemma:4b
// Colors
"Based on utilization" = "Baseret på brug";
"Based on pressure" = "Baseret på belastning";
"Based on cluster" = "Baseret på gruppen"; // translategemma:4b
"System accent" = "Systemets tone"; // translategemma:4b
"Monochrome accent" = "Monochromatisk accent";
"Clear" = "Gennemsigtig";
"White" = "Hvid";
"Black" = "Sort";
"Gray" = "Grå";
"Second gray" = "Sekundær grå";
"Dark gray" = "Mørk grå";
"Light gray" = "Lys grå";
"Red" = "Rød";
"Second red" = "Sekundær rød";
"Green" = "Grøn";
"Second green" = "Sekundær grøn";
"Blue" = "Blå";
"Second blue" = "Sekundær blå";
"Yellow" = "Gul";
"Second yellow" = "Sekundær gul";
"Orange" = "Orange";
"Second orange" = "Sekundær orange";
"Purple" = "Lilla";
"Second purple" = "Sekundær lilla";
"Brown" = "Brun";
"Second brown" = "Sekundær brun";
"Cyan" = "Cyan";
"Magenta" = "Magenta";
"Pink" = "Pink";
"Teal" = "Blågrøn";
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/de.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "CPU-Einstellungen öffnen";
"GPU" = "GPU";
"Open GPU settings" = "GPU-Einstellungen öffnen";
"RAM" = "RAM";
"Open RAM settings" = "RAM-Einstellungen öffnen";
"Disk" = "Festplatte";
"Open Disk settings" = "Festplatten-Einstellungen öffnen";
"Sensors" = "Sensoren";
"Open Sensors settings" = "Sensor-Einstellungen öffnen";
"Network" = "Netzwerk";
"Open Network settings" = "Netzwerk-Einstellungen öffnen";
"Battery" = "Batterie";
"Open Battery settings" = "Batterie-Einstellungen öffnen";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Bluetooth-Einstellungen öffnen";
"Clock" = "Uhr";
"Open Clock settings" = "Uhr-Einstellungen öffnen";
// Words
"Unknown" = "Unbekannt";
"Version" = "Version";
"Processor" = "Prozessor";
"Memory" = "Speicher";
"Graphics" = "Grafik";
"Close" = "Schließen";
"Download" = "Herunterladen";
"Install" = "Installieren";
"Cancel" = "Abbrechen";
"Unavailable" = "Nicht verfügbar";
"Yes" = "Ja";
"No" = "Nein";
"Automatic" = "Automatisch";
"Manual" = "Manuell";
"None" = "Keine";
"Dots" = "Punkte";
"Arrows" = "Pfeile";
"Characters" = "Zeichen";
"Short" = "Kurz";
"Long" = "Lang";
"Statistics" = "Statistiken";
"Max" = "Max";
"Min" = "Min";
"Reset" = "Zurücksetzen";
"Alignment" = "Ausrichtung";
"Left alignment" = "Links";
"Center alignment" = "Zentriert";
"Right alignment" = "Rechts";
"Dashboard" = "Dashboard";
"Enabled" = "Aktiviert";
"Disabled" = "Deaktiviert";
"Silent" = "Lautlos";
"Units" = "Einheiten";
"Fans" = "Lüfter";
"Scaling" = "Skalierung";
"Linear" = "Linear";
"Square" = "Quadratisch";
"Cube" = "Kubisch";
"Logarithmic" = "Logarithmisch";
"Fixed scale" = "Fest";
"Cores" = "Kerne";
"Settings" = "Einstellungen";
"Name" = "Name";
"Format" = "Format";
"Turn off" = "Ausschalten";
"Normal" = "Normal";
"Warning" = "Warnung";
"Critical" = "Kritisch";
"Usage" = "Auslastung";
"2 minutes" = "2 Minuten";
"3 minutes" = "3 Minuten";
"10 minutes" = "10 Minuten";
"Import" = "Importieren";
"Export" = "Exportieren";
"Separator" = "Trennzeichen";
"Read" = "Lesen";
"Write" = "Schreiben";
"Frequency" = "Frequenz";
"Save" = "Speichern";
"Run" = "Ausführen";
"Stop" = "Stoppen";
"Uninstall" = "Deinstallieren";
"1 sec" = "1 Sek.";
"2 sec" = "2 Sek.";
"3 sec" = "3 Sek.";
"5 sec" = "5 Sek.";
"10 sec" = "10 Sek.";
"15 sec" = "15 Sek.";
"30 sec" = "30 Sek.";
"60 sec" = "60 Sek.";
// Setup
"Stats Setup" = "Stats einrichten";
"Previous" = "Zurück";
"Previous page" = "Vorherige Seite";
"Next" = "Weiter";
"Next page" = "Nächste Seite";
"Finish" = "Fertig";
"Finish setup" = "Einrichtung abschließen";
"Welcome to Stats" = "Willkommen bei Stats";
"welcome_message" = "Vielen Dank, dass Sie Stats verwenden – einen kostenlosen Open-Source-Systemmonitor für Ihre macOS-Menüleiste.";
"Start the application automatically when starting your Mac" = "Die App automatisch beim Starten Ihres Mac starten";
"Do not start the application automatically when starting your Mac" = "Die App nicht automatisch beim Starten Ihres Mac starten";
"Do everything silently in the background (recommended)" = "Alles lautlos im Hintergrund ausführen (empfohlen)";
"Check for a new version on startup" = "Beim Start nach einer neuen Version suchen";
"Check for a new version every day (once a day)" = "Täglich nach einer neuen Version suchen (einmal pro Tag)";
"Check for a new version every week (once a week)" = "Wöchentlich nach einer neuen Version suchen (einmal pro Woche)";
"Check for a new version every month (once a month)" = "Monatlich nach einer neuen Version suchen (einmal pro Monat)";
"Never check for updates (not recommended)" = "Nie nach Updates suchen (nicht empfohlen)";
"Anonymous telemetry for better development decisions" = "Anonyme Telemetrie für bessere Entwicklungsentscheidungen";
"Share anonymous telemetry data" = "Anonyme Telemetriedaten teilen";
"Do not share anonymous telemetry data" = "Keine anonymen Telemetriedaten teilen";
"The configuration is completed" = "Die Einrichtung ist abgeschlossen";
"finish_setup_message" = "Alles ist eingerichtet! \n Stats ist ein Open-Source-Tool, das kostenlos ist und immer kostenlos bleiben wird. \n Wenn es Ihnen gefällt, können Sie das Projekt unterstützen – das wird immer geschätzt!";
// Alerts
"New version available" = "Neue Version verfügbar";
"Click to install the new version of Stats" = "Klicken, um die neue Version von Stats zu installieren";
"Successfully updated" = "Erfolgreich aktualisiert";
"Stats was updated to v" = "Stats wurde auf v%0 aktualisiert";
"Reset settings text" = "Alle Einstellungen der App werden zurückgesetzt und die App wird neu gestartet. Sind Sie sicher, dass Sie das tun möchten?";
"Support text" = "Vielen Dank, dass Sie Stats verwenden!\n\n Die Pflege und Verbesserung dieses Open-Source-Projekts erfordert Zeit und Ressourcen. Ihre Unterstützung hilft uns, weiterhin eine kostenlose und zuverlässige App für alle bereitzustellen.\n\nWenn Sie Stats hilfreich finden, erwägen Sie bitte einen Beitrag. Jede Kleinigkeit hilft!";
// Settings
"Open Activity Monitor" = "Aktivitätsanzeige öffnen";
"Report a bug" = "Fehler melden";
"Support the application" = "Die App unterstützen";
"Close application" = "App schließen";
"Open application settings" = "App-Einstellungen öffnen";
"Open dashboard" = "Dashboard öffnen";
"No notifications available in this module" = "In diesem Modul sind keine Benachrichtigungen verfügbar";
"Open Calendar" = "Kalender öffnen";
"Toggle the module" = "Modul umschalten";
// Application settings
"Update application" = "App aktualisieren";
"Check for updates" = "Nach Updates suchen";
"At start" = "Beim Start";
"Once per day" = "Einmal pro Tag";
"Once per week" = "Einmal pro Woche";
"Once per month" = "Einmal pro Monat";
"Never" = "Nie";
"Check for update" = "Nach Update suchen";
"Show icon in dock" = "Symbol im Dock anzeigen";
"Start at login" = "Bei Anmeldung starten";
"Build number" = "Build-Nummer";
"Import settings" = "Einstellungen importieren";
"Export settings" = "Einstellungen exportieren";
"Reset settings" = "Einstellungen zurücksetzen";
"Pause the Stats" = "Stats pausieren";
"Resume the Stats" = "Stats fortsetzen";
"Combined modules" = "Kombinierte Module";
"Combined details" = "Kombinierte Details";
"Spacing" = "Abstand";
"Share anonymous telemetry" = "Anonyme Telemetrie teilen";
"Choose file" = "Datei auswählen";
"Stress tests" = "Stresstests";
// Dashboard
"Serial number" = "Seriennummer";
"Model identifier" = "Modellkennung";
"Production year" = "Produktionsjahr";
"Uptime" = "Laufzeit";
"Number of cores" = "%0 Kerne";
"Number of threads" = "%0 Threads";
"Number of e-cores" = "%0 Effizienzkerne";
"Number of p-cores" = "%0 Leistungskerne";
"Disks" = "Festplatten";
"Display" = "Bildschirm";
// Update
"The latest version of Stats installed" = "Die neueste Version von Stats ist installiert";
"Downloading..." = "Wird heruntergeladen …";
"Current version: " = "Aktuelle Version: ";
"Latest version: " = "Neueste Version: ";
// Widgets
"Color" = "Farbe";
"Label" = "Beschriftung";
"Box" = "Karton"; // translategemma:4b
"Frame" = "Rahmen";
"Value" = "Wert";
"Colorize" = "Einfärben";
"Colorize value" = "Wert einfärben";
"Additional information" = "Zusätzliche Informationen";
"Reverse values order" = "Wertereihenfolge umkehren";
"Base" = "Basis";
"Display mode" = "Anzeigemodus";
"One row" = "Eine Zeile";
"Two rows" = "Zwei Zeilen";
"Mini widget" = "Mini";
"Line chart widget" = "Liniendiagramm";
"Bar chart widget" = "Balkendiagramm";
"Pie chart widget" = "Kreisdiagramm";
"Network chart widget" = "Netzwerkdiagramm";
"Speed widget" = "Geschwindigkeit";
"Battery widget" = "Batterie";
"Stack widget" = "Stapel";
"Memory widget" = "Speicher";
"Static width" = "Feste Breite";
"Tachometer widget" = "Drehzahlmesser"; // translategemma:4b
"State widget" = "Status-Widget";
"Text widget" = "Text-Widget";
"Battery details widget" = "Batteriedetails-Widget";
"Show symbols" = "Symbole anzeigen";
"Label widget" = "Beschriftung";
"Number of reads in the chart" = "Anzahl der Messwerte im Diagramm";
"Color of download" = "Farbe für Download";
"Color of upload" = "Farbe für Upload";
"Monospaced font" = "Nichtproportionale Schrift";
"Reverse order" = "Umgekehrte Reihenfolge";
"Chart history" = "Diagrammverlauf";
"Default color" = "Standard";
"Transparent when no activity" = "Transparent bei Inaktivität";
"Constant color" = "Konstant";
// Module Kit
"Open module settings" = "Modul-Einstellungen öffnen";
"Select widget" = "%0-Widget auswählen";
"Open widget settings" = "Widget-Einstellungen öffnen";
"Update interval" = "Aktualisierungsintervall";
"Usage history" = "Nutzungsverlauf";
"Details" = "Details";
"Top processes" = "Top-Prozesse";
"Pictogram" = "Piktogramm";
"Module" = "Modul";
"Widgets" = "Widgets";
"Popup" = "Pop-up"; // translategemma:4b
"Notifications" = "Benachrichtigungen";
"Merge widgets" = "Widgets zusammenführen";
"No available widgets to configure" = "Keine Widgets zum Konfigurieren verfügbar";
"No options to configure for the popup in this module" = "Keine Optionen für das Popup in diesem Modul verfügbar";
"Process" = "Prozess";
"Kill process" = "Prozess beenden";
"Keyboard shortcut" = "Tastaturkurzbefehl";
"Listening..." = "Wartet …";
// Modules
"Number of top processes" = "Anzahl der Top-Prozesse";
"Update interval for top processes" = "Aktualisierungsintervall für Top-Prozesse";
"Notification level" = "Benachrichtigungsstufe";
"Chart color" = "Diagrammfarbe";
"Main chart scaling" = "Hauptdiagramm-Skalierung";
"Scale value" = "Skalierungswert";
"Text widget value" = "Text-Widget-Wert";
// CPU
"CPU usage" = "CPU-Auslastung";
"CPU temperature" = "CPU-Temperatur";
"CPU frequency" = "CPU-Frequenz";
"System" = "System";
"User" = "Benutzer";
"Idle" = "Leerlauf";
"Show usage per core" = "Auslastung pro Kern anzeigen";
"Show hyper-threading cores" = "Hyper-Threading-Kerne anzeigen";
"Split the value (System/User)" = "Wert aufteilen (System/Benutzer)";
"Scheduler limit" = "Scheduler-Limit";
"Speed limit" = "Geschwindigkeitslimit";
"Average load" = "Durchschnittliche Last";
"1 minute" = "1 Minute";
"5 minutes" = "5 Minuten";
"15 minutes" = "15 Minuten";
"CPU usage threshold" = "CPU-Auslastungsschwelle";
"CPU usage is" = "CPU-Auslastung ist %0";
"Efficiency cores" = "Effizienzkerne";
"Performance cores" = "Leistungskerne";
"System color" = "Systemfarbe";
"User color" = "Benutzerfarbe";
"Idle color" = "Leerlauffarbe";
"Cluster grouping" = "Cluster-Gruppierung";
"Efficiency cores color" = "Effizienzkerne-Farbe";
"Performance cores color" = "Leistungskerne-Farbe";
"Total load" = "Gesamtlast";
"System load" = "Systemlast";
"User load" = "Benutzerlast";
"Efficiency cores load" = "Effizienzkerne-Last";
"Performance cores load" = "Leistungskerne-Last";
"All cores" = "Alle Kerne";
// GPU
"GPU to show" = "Anzuzeigende GPU";
"Show GPU type" = "GPU-Typ anzeigen";
"GPU enabled" = "GPU aktiviert";
"GPU disabled" = "GPU deaktiviert";
"GPU temperature" = "GPU-Temperatur";
"GPU utilization" = "GPU-Auslastung";
"Vendor" = "Hersteller";
"Model" = "Modell";
"Status" = "Status";
"Active" = "Aktiv";
"Non active" = "Inaktiv";
"Fan speed" = "Lüftergeschwindigkeit";
"Core clock" = "Kerntakt";
"Memory clock" = "Speichertakt";
"Utilization" = "Auslastung";
"Render utilization" = "Render-Auslastung";
"Tiler utilization" = "Tiler-Auslastung";
"GPU usage threshold" = "GPU-Auslastungsschwelle";
"GPU usage is" = "GPU-Auslastung ist %0";
// RAM
"Memory usage" = "Speicherauslastung";
"Memory pressure" = "Speicherdruck";
"Total" = "Gesamt";
"Used" = "Belegt";
"App" = "App";
"Wired" = "Reserviert";
"Compressed" = "Komprimiert";
"Free" = "Frei";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Wert aufteilen (App/Reserviert/Komprimiert)";
"RAM utilization threshold" = "RAM-Auslastungsschwelle";
"RAM utilization is" = "RAM-Auslastung ist %0";
"App color" = "App-Farbe";
"Wired color" = "Reserviert-Farbe";
"Compressed color" = "Komprimiert-Farbe";
"Free color" = "Frei-Farbe";
"Free memory (less than)" = "Freier Speicher (weniger als)";
"Swap size" = "Swap-Größe";
"Free RAM is" = "Freier RAM ist %0";
// Disk
"Show removable disks" = "Wechseldatenträger anzeigen";
"Used disk memory" = "%0 von %1 belegt";
"Free disk memory" = "%0 von %1 frei";
"Disk to show" = "Anzuzeigende Festplatte";
"Open disk" = "Festplatte öffnen";
"Switch view" = "Ansicht wechseln";
"Disk utilization threshold" = "Festplatten-Auslastungsschwelle";
"Disk utilization is" = "Festplatten-Auslastung ist %0";
"Read color" = "Lesen-Farbe";
"Write color" = "Schreiben-Farbe";
"Disk usage" = "Festplattennutzung";
"Total read" = "Gelesen gesamt";
"Total written" = "Geschrieben gesamt";
"Write speed" = "Schreiben";
"Read speed" = "Lesen";
"Drives" = "Laufwerke";
"SMART data" = "SMART-Daten";
// Sensors
"Temperature unit" = "Temperatureinheit";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Lüftergeschwindigkeit speichern";
"Fan" = "Lüfter";
"HID sensors" = "HID-Sensoren";
"Synchronize fan's control" = "Lüftersteuerung synchronisieren";
"Current" = "Strom";
"Energy" = "Energie";
"Show unknown sensors" = "Unbekannte Sensoren anzeigen";
"Install fan helper" = "Lüfter-Hilfsprogramm installieren";
"Uninstall fan helper" = "Lüfter-Hilfsprogramm deinstallieren";
"Fan value" = "Lüfterwert";
"Turn off fan" = "Lüfter ausschalten";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Sie sind dabei, den Lüfter auszuschalten. Dies ist keine empfohlene Aktion und kann Ihren Mac beschädigen. Sind Sie sicher, dass Sie das tun möchten?";
"Sensor threshold" = "Sensorschwelle";
"Left fan" = "Links";
"Right fan" = "Rechts";
"Fastest fan" = "Schnellster";
"Sensor to show" = "Anzuzeigender Sensor";
// Network
"Uploading" = "Hochladen"; // translategemma:4b
"Downloading" = "Herunterladen"; // translategemma:4b
"Public IP" = "Öffentliche IP";
"Local IP" = "Lokale IP";
"Interface" = "Schnittstelle";
"Physical address" = "Physische Adresse";
"Refresh" = "Aktualisieren";
"Click to copy public IP address" = "Klicken, um die öffentliche IP-Adresse zu kopieren";
"Click to copy local IP address" = "Klicken, um die lokale IP-Adresse zu kopieren";
"Click to copy wifi name" = "Klicken, um den WLAN-Namen zu kopieren";
"Click to copy mac address" = "Klicken, um die MAC-Adresse zu kopieren";
"No connection" = "Keine Verbindung";
"Network interface" = "Netzwerkschnittstelle";
"Total download" = "Download gesamt";
"Total upload" = "Upload gesamt";
"Reader type" = "Lesertyp";
"Interface based" = "Schnittstellenbasiert";
"Processes based" = "Prozessbasiert";
"Reset data usage" = "Datennutzung zurücksetzen";
"VPN mode" = "VPN-Modus";
"Standard" = "Standard";
"Security" = "Sicherheit";
"Channel" = "Kanal";
"Common scale" = "Gemeinsame Skala";
"Autodetection" = "Automatische Erkennung";
"Widget activation threshold" = "Widget-Aktivierungsschwelle";
"Internet connection" = "Internetverbindung";
"Active state color" = "Farbe für aktiven Zustand";
"Nonactive state color" = "Farbe für inaktiven Zustand";
"Connectivity host (ICMP)" = "Verbindungshost (ICMP)";
"Leave empty to disable the check" = "Leer lassen, um die Prüfung zu deaktivieren";
"Connectivity history" = "Verbindungsverlauf";
"Auto-refresh public IP address" = "Öffentliche IP-Adresse automatisch aktualisieren";
"Every hour" = "Jede Stunde";
"Every 12 hours" = "Alle 12 Stunden";
"Every 24 hours" = "Alle 24 Stunden";
"Network activity" = "Netzwerkaktivität";
"Last reset" = "Letzte Zurücksetzung vor %0";
"Latency" = "Latenz";
"Upload speed" = "Hochladen"; // translategemma:4b
"Download speed" = "Herunterladen"; // translategemma:4b
"Address" = "Adresse";
"WiFi network" = "WLAN-Netzwerk";
"Local IP changed" = "Lokale IP hat sich geändert";
"Public IP changed" = "Öffentliche IP hat sich geändert";
"Previous IP" = "Vorherige IP: %0";
"New IP" = "Neue IP: %0";
"Internet connection lost" = "Internetverbindung unterbrochen";
"Internet connection established" = "Internetverbindung hergestellt";
// Battery
"Level" = "Ladezustand";
"Source" = "Quelle";
"AC Power" = "Netzteil";
"Battery Power" = "Batteriebetrieb";
"Time" = "Zeit";
"Health" = "Zustand";
"Amperage" = "Stromstärke";
"Voltage" = "Spannung";
"Cycles" = "Ladezyklen";
"Temperature" = "Temperatur";
"Power adapter" = "Netzteil";
"Power" = "Leistung";
"Is charging" = "Wird geladen";
"Time to discharge" = "Zeit bis zur Entladung";
"Time to charge" = "Zeit bis zur Aufladung";
"Calculating" = "Berechnung läuft";
"Fully charged" = "Vollständig geladen";
"Not connected" = "Nicht verbunden";
"Low level notification" = "Benachrichtigung bei niedrigem Ladezustand";
"High level notification" = "Benachrichtigung bei hohem Ladezustand";
"Low battery" = "Niedriger Ladezustand";
"High battery" = "Hoher Ladezustand";
"Battery remaining" = "%0 % verbleibend";
"Battery remaining to full charge" = "%0 % bis zur vollständigen Ladung";
"Percentage" = "Prozent";
"Percentage and time" = "Prozent und Zeit";
"Time and percentage" = "Zeit und Prozent";
"Time format" = "Zeitformat";
"Hide additional information when full" = "Zusätzliche Informationen bei voller Ladung ausblenden";
"Last charge" = "Letzte Ladung";
"Capacity" = "Kapazität";
"current / maximum / designed" = "aktuell / maximal / geplant";
"Low power mode" = "Stromsparmodus";
"Percentage inside the icon" = "Prozentanzeige im Symbol";
"Colorize battery" = "Batterie einfärben";
"Charging current" = "Ladestrom";
"Charging Voltage" = "Ladespannung";
"Charger state inside the battery" = "Ladegerätestatus im Batteriesymbol";
// Bluetooth
"Battery to show" = "Anzuzeigende Batterie";
"No Bluetooth devices are available" = "Keine Bluetooth-Geräte verfügbar";
// Clock
"Time zone" = "Zeitzone";
"Local" = "Lokal";
"Calendar" = "Kalender";
"Show week numbers" = "Zeige die Wochennummern"; // translategemma:4b
"Local time" = "Ortszeit";
"Add new clock" = "Neue Uhr hinzufügen";
"Delete selected clock" = "Ausgewählte Uhr löschen";
"Help with datetime format" = "Hilfe zum Datums-/Zeitformat";
// Colors
"Based on utilization" = "Nach Auslastung";
"Based on pressure" = "Nach Druck";
"Based on cluster" = "Nach Cluster";
"System accent" = "Systemakzent";
"Monochrome accent" = "Monochromer Akzent";
"Clear" = "Transparent";
"White" = "Weiß";
"Black" = "Schwarz";
"Gray" = "Grau";
"Second gray" = "Zweites Grau";
"Dark gray" = "Dunkelgrau";
"Light gray" = "Hellgrau";
"Red" = "Rot";
"Second red" = "Zweites Rot";
"Green" = "Grün";
"Second green" = "Zweites Grün";
"Blue" = "Blau";
"Second blue" = "Zweites Blau";
"Yellow" = "Gelb";
"Second yellow" = "Zweites Gelb";
"Orange" = "Orange";
"Second orange" = "Zweites Orange";
"Purple" = "Lila";
"Second purple" = "Zweites Lila";
"Brown" = "Braun";
"Second brown" = "Zweites Braun";
"Cyan" = "Cyan";
"Magenta" = "Magenta";
"Pink" = "Rosa";
"Teal" = "Blaugrün";
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/el.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "Επεξεργαστής"; // translategemma:4b
"Open CPU settings" = "Άνοιγμα ρυθμίσεων CPU";
"GPU" = "Κάρτα γραφικών"; // translategemma:4b
"Open GPU settings" = "Άνοιγμα ρυθμίσεων GPU";
"RAM" = "Μνήμη RAM"; // translategemma:4b
"Open RAM settings" = "Άνοιγμα ρυθμίσεων RAM";
"Disk" = "Δίσκος";
"Open Disk settings" = "Άνοιγμα ρυθμίσεων Δίσκου";
"Sensors" = "Αισθητήρες";
"Open Sensors settings" = "Άνοιγμα ρυθμίσεων Αισθητήρων";
"Network" = "Δίκτυο";
"Open Network settings" = "Άνοιγμα ρυθμίσεων Δικτύου";
"Battery" = "Μπαταρία";
"Open Battery settings" = "Άνοιγμα ρυθμίσεων Μπαταρίας";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Άνοιγμα ρυθμίσεων Bluetooth";
"Clock" = "Ωρά"; // translategemma:4b
"Open Clock settings" = "Άνοιγμα ρυθμίσεων ρολογιού"; // translategemma:4b
// Words
"Unknown" = "Άγνωστο";
"Version" = "Έκδοση";
"Processor" = "Επεξεργαστής";
"Memory" = "Μνήμη";
"Graphics" = "Γραφικά";
"Close" = "Κλειστό";
"Download" = "Λήψη";
"Install" = "Εγκατάσταση";
"Cancel" = "Ακύρωση";
"Unavailable" = "Μη Διαθέσιμο";
"Yes" = "Ναι";
"No" = "Οχι";
"Automatic" = "Αυτόματα";
"Manual" = "Χειροκίνητα";
"None" = "Κανένα";
"Dots" = "Τελείες";
"Arrows" = "Βέλη";
"Characters" = "Χαρακτήρες";
"Short" = "Σύντομο";
"Long" = "Μεγάλη";
"Statistics" = "Στατιστικά";
"Max" = "Μέγιστο";
"Min" = "Ελάχιστο";
"Reset" = "Επαναφορά";
"Alignment" = "Στοίχιση";
"Left alignment" = "Αριστερή";
"Center alignment" = "Κεντρική";
"Right alignment" = "Δεξιά";
"Dashboard" = "Ταμπλό";
"Enabled" = "Ενεργοποιημένο"; // translategemma:4b
"Disabled" = "Απενεργοποιημένο";
"Silent" = "Σίγαση";
"Units" = "Μονάδες";
"Fans" = "Ανεμιστήρες";
"Scaling" = "Κλίμακα";
"Linear" = "Γραμμικό";
"Square" = "Τετραγωνικό";
"Cube" = "Κυβικό";
"Logarithmic" = "Λογαριθμικό";
"Fixed scale" = "Διορθώθηκε"; // translategemma:4b
"Cores" = "Πυρήνες";
"Settings" = "Ρυθμίσεις";
"Name" = "Όνομα"; // translategemma:4b
"Format" = "Μορφή"; // translategemma:4b
"Turn off" = "Απενεργοποίηση"; // translategemma:4b
"Normal" = "Κανονικό"; // translategemma:4b
"Warning" = "Προειδοποίηση"; // translategemma:4b
"Critical" = "Κρίσιμο"; // translategemma:4b
"Usage" = "Χρήση"; // translategemma:4b
"2 minutes" = "2 λεπτά"; // translategemma:4b
"3 minutes" = "3 λεπτά"; // translategemma:4b
"10 minutes" = "10 λεπτά"; // translategemma:4b
"Import" = "Εισαγωγή"; // translategemma:4b
"Export" = "Εξαγωγή"; // translategemma:4b
"Separator" = "Διαχωριστής"; // translategemma:4b
"Read" = "Διαβάστε"; // translategemma:4b
"Write" = "Γράψτε"; // translategemma:4b
"Frequency" = "Συχνότητα"; // translategemma:4b
"Save" = "Αποθήκευση"; // translategemma:4b
"Run" = "Εκτελέστε"; // translategemma:4b
"Stop" = "Σταματήστε"; // translategemma:4b
"Uninstall" = "Ακύρωση εγκατάστασης"; // translategemma:4b
"1 sec" = "1 δευτερόλεπτο"; // translategemma:4b
"2 sec" = "2 δευτερόλεπτα"; // translategemma:4b
"3 sec" = "3 δευτερόλεπτα"; // translategemma:4b
"5 sec" = "5 δευτερόλεπτα"; // translategemma:4b
"10 sec" = "10 δευτερόλεπτα"; // translategemma:4b
"15 sec" = "15 δευτερόλεπτα"; // translategemma:4b
"30 sec" = "30 δευτερόλεπτα"; // translategemma:4b
"60 sec" = "60 δευτερόλεπτα"; // translategemma:4b
// Setup
"Stats Setup" = "Εγκατάσταση Stats";
"Previous" = "Προηγούμενο";
"Previous page" = "Προηγούμενη σελίδα";
"Next" = "Επόμενο";
"Next page" = "Επόμενη σελίδα";
"Finish" = "Τέλος";
"Finish setup" = "Τέλος εγκαταστασης";
"Welcome to Stats" = "Καλώς ήρθες στο Stats";
"welcome_message" = "Σας ευχαριστούμε που χρησιμοποιείτε το Stats, ένα δωρεάν, ανοιχτού κώδικα πρόγραμμα παρακολούθησης συστήματος για το macOS, το οποίο μπορείτε να χρησιμοποιήσετε στην γραμμή εργασιών σας."; // translategemma:4b
"Start the application automatically when starting your Mac" = "Ξεκινήστε την εφαρμογή αυτόματα κατά την εκκίνηση του Mac σας."; // translategemma:4b
"Do not start the application automatically when starting your Mac" = "Μην ξεκινάτε την εφαρμογή αυτόματα όταν ξεκινάτε τον Mac σας."; // translategemma:4b
"Do everything silently in the background (recommended)" = "Κάντε τα πάντα αθόρυβα στο παρασκήνιο (συνιστάται)"; // translategemma:4b
"Check for a new version on startup" = "Ελέγξτε για μια νέα έκδοση κατά την εκκίνηση."; // translategemma:4b
"Check for a new version every day (once a day)" = "Ελέγξτε για μια νέα έκδοση καθημερινά (μία φορά την ημέρα)"; // translategemma:4b
"Check for a new version every week (once a week)" = "Ελέγξτε για μια νέα έκδοση κάθε εβδομάδα (μία φορά την εβδομάδα)"; // translategemma:4b
"Check for a new version every month (once a month)" = "Ελέγξτε για μια νέα έκδοση κάθε μήνα (μία φορά το μήνα)"; // translategemma:4b
"Never check for updates (not recommended)" = "Μην ελέγχετε για ενημερώσεις (δεν συνιστάται)"; // translategemma:4b
"Anonymous telemetry for better development decisions" = "Ανωνυμοποιημένη συλλογή δεδομένων για καλύτερες αποφάσεις ανάπτυξης"; // translategemma:4b
"Share anonymous telemetry data" = "Μοιραστείτε ανώνυμα δεδομένα τηλεμετρίας"; // translategemma:4b
"Do not share anonymous telemetry data" = "Μην μοιράζεστε δεδομένα τηλεμετρίας που έχουν παραχωρηθεί ανώνυμα."; // translategemma:4b
"The configuration is completed" = "Η διαμόρφωση έχει ολοκληρωθεί"; // translategemma:4b
"finish_setup_message" = "Όλα είναι έτοιμα!"; // translategemma:4b
// Alerts
"New version available" = "Νέα έκδοση διαθέσιμη";
"Click to install the new version of Stats" = "Πάτησε εδώ για την εγκατάσταση της νέας έκδοσης του Stats";
"Successfully updated" = "Επιτυχής ενημέρωση";
"Stats was updated to v" = "Το Stats ενημερώθηκε στην ε%0";
"Reset settings text" = "Όλες οι ρυθμίσεις της εφαρμογής θα αρχικοποιηθούν και η εφαρμογή θα επανεκκινηθεί. Είστε σίγουροι;";
"Support text" = "Σας ευχαριστούμε για τη χρήση των Stats!\n\n Η συντήρηση και η βελτίωση αυτού του έργου ανοιχτού κώδικα απαιτεί χρόνο και πόρους. Η υποστήριξή σας μας βοηθά να συνεχίσουμε να παρέχουμε μια δωρεάν και αξιόπιστη εφαρμογή για όλους.\n\nΕάν βρίσκετε το Stats χρήσιμο, παρακαλούμε σκεφτείτε να συνεισφέρετε. Κάθε μικρό ποσό βοηθάει!";
// Settings
"Open Activity Monitor" = "Άνοιγμα Παρακολούθησης Δραστηριότητας";
"Report a bug" = "Αναφορά Προβλήματος";
"Support the application" = "Στηρίξτε την εφαρμογή";
"Close application" = "Κλείσιμο εφαρμογής";
"Open application settings" = "Άνοιγμα ρυθμίσεων εφαρμογής";
"Open dashboard" = "Άνοιγμα ταμπλό";
"No notifications available in this module" = "Δεν υπάρχουν διαθέσιμες ειδοποιήσεις σε αυτό το τμήμα."; // translategemma:4b
"Open Calendar" = "Άνοιγμα Ημερολογίου"; // translategemma:4b
"Toggle the module" = "Ενεργοποιήστε/Απενεργοποιήστε το module"; // translategemma:4b
// Application settings
"Update application" = "Ενημέρωση Εφαρμογής";
"Check for updates" = "Έλεγχος για ενημερώσεις";
"At start" = "Κατά την έναρξη";
"Once per day" = "Μια φορά την ημέρα";
"Once per week" = "Μια φορά την εβδομάδα";
"Once per month" = "Μια φορά το μήνα";
"Never" = "Ποτέ";
"Check for update" = "Έλεγχος για ενημέρωση";
"Show icon in dock" = "Εμφάνιση εικονιδίου στο dock";
"Start at login" = "Εκκίνηση κατά τη σύνδεση";
"Build number" = "Αριθμός έκδοσης";
"Import settings" = "Εισαγωγή ρυθμίσεων"; // translategemma:4b
"Export settings" = "Ρυθμίσεις εξαγωγής"; // translategemma:4b
"Reset settings" = "Επαναφορά ρυθμίσεων";
"Pause the Stats" = "Παύση του Stats";
"Resume the Stats" = "Ξαναρχίζω το Stats";
"Combined modules" = "Συνένωση ενοτήτων";
"Combined details" = "Συνοπτικές πληροφορίες"; // translategemma:4b
"Spacing" = "Κενό";
"Share anonymous telemetry" = "Μοιραστείτε ανώνυμα δεδομένα τηλεμετρίας"; // translategemma:4b
"Choose file" = "Επιλέξτε αρχείο"; // translategemma:4b
"Stress tests" = "Έλεγχοι αντοχής"; // translategemma:4b
// Dashboard
"Serial number" = "Σειριακός αριθμός";
"Model identifier" = "Αναγνώρισμα μοντέλου"; // translategemma:4b
"Production year" = "Έτος παραγωγής"; // translategemma:4b
"Uptime" = "Χρόνος Δραστηριότητας";
"Number of cores" = "%0 πυρήνες";
"Number of threads" = "%0 θέματα"; // translategemma:4b
"Number of e-cores" = "%0 πυρήνες απόδοσης";
"Number of p-cores" = "%0 πυρήνες επίδοσης";
"Disks" = "Δίσκοι"; // translategemma:4b
"Display" = "Οθόνη"; // translategemma:4b
// Update
"The latest version of Stats installed" = "Η τελευταία έκδοση του Stats είναι εγκατεστημένη";
"Downloading..." = "Λήψη...";
"Current version: " = "Τρέχουσα έκδοση: ";
"Latest version: " = "Τελευταία έκδοση: ";
// Widgets
"Color" = "Χρώμα";
"Label" = "Ετικέτα";
"Box" = "Κουτί";
"Frame" = "Περίγραμμα";
"Value" = "Τιμή";
"Colorize" = "Χρωματισμός";
"Colorize value" = "Χρωματισμός Τιμής";
"Additional information" = "Επιπρόσθετες πληροφορίες";
"Reverse values order" = "Αντιστροφή σειράς τιμών";
"Base" = "Βάση";
"Display mode" = "Λειτουργία Προβολής";
"One row" = "Μια σειρά";
"Two rows" = "Δυο σειρές";
"Mini widget" = "Μίνι";
"Line chart widget" = "Γραμμικό διάγραμμα";
"Bar chart widget" = "Ραβδόγραμμα";
"Pie chart widget" = "Διάγραμμα πίτας";
"Network chart widget" = "Διάγραμμα δικτύου";
"Speed widget" = "Ταχύτητα";
"Battery widget" = "Μπαταρία";
"Stack widget" = "Στοίβα"; // translategemma:4b
"Memory widget" = "Μνήμη";
"Static width" = "Σταθερό πλάτος";
"Tachometer widget" = "Ταχύμετρο";
"State widget" = "Στικ του κράτους"; // translategemma:4b
"Text widget" = "Widget κειμένου"; // translategemma:4b
"Battery details widget" = "Widget με πληροφορίες για την μπαταρία"; // translategemma:4b
"Show symbols" = "Εμφάνιση συμβόλων";
"Label widget" = "Ετικέτα";
"Number of reads in the chart" = "Αριθμός αναγνώσεων του διαγράμματος";
"Color of download" = "Χρώμα της λήψης";
"Color of upload" = "Χρώμα της μεταφόρτωσης";
"Monospaced font" = "Μονόχωρη γραμματοσειρά";
"Reverse order" = "Αντίστροφη σειρά"; // translategemma:4b
"Chart history" = "Ιστορικό γραφήματος"; // translategemma:4b
"Default color" = "Προεπιλεγμένο"; // translategemma:4b
"Transparent when no activity" = "Διαφανές όταν δεν υπάρχει κάποια δραστηριότητα"; // translategemma:4b
"Constant color" = "Σταθερός"; // translategemma:4b
// Module Kit
"Open module settings" = "Άνοιγμα ρυθμίσεων ενότητας";
"Select widget" = "Επιλογή %0 widget";
"Open widget settings" = "Άνοιγμα ρυθμίσεων widget";
"Update interval" = "Ενημέρωση μεσοδιαστήματος";
"Usage history" = "Ιστορικό χρήσης";
"Details" = "Λεπτομέρειες";
"Top processes" = "Κορυφαίες διεργασίες";
"Pictogram" = "Εικονόγραμμα";
"Module" = "Μονάδα μέτρησης";
"Widgets" = "Widget"; // translategemma:4b
"Popup" = "Αναδυόμενο παράθυρο";
"Notifications" = "Ειδοποιήσεις";
"Merge widgets" = "Συγχώνευση γραφικών στοιχείων";
"No available widgets to configure" = "Δεν υπάρχουν διαθέσιμα γραφικά στοιχεία για διαμόρφωση";
"No options to configure for the popup in this module" = "Δεν υπάρχουν επιλογές για διαμόρφωση για το αναδυόμενο παράθυρο σε αυτήν την ενότητα";
"Process" = "Διαδικασία"; // translategemma:4b
"Kill process" = "Τερματισμός διεργασίας"; // translategemma:4b
"Keyboard shortcut" = "Σύντομο συνδυασμό πληκτρολογίου"; // translategemma:4b
"Listening..." = "Ακούω..."; // translategemma:4b
// Modules
"Number of top processes" = "Αριθμός κορυφαίων διεργασιών";
"Update interval for top processes" = "Ενημέρωση μεσοδιαστήματος για τις κορυφαίες διεργασίες";
"Notification level" = "Επίπεδο Ειδοποίησης";
"Chart color" = "Χρώμα γραφήματος";
"Main chart scaling" = "Κύρια κλίμακα γραφήματος"; // translategemma:4b
"Scale value" = "Τιμή κλίμακας"; // translategemma:4b
"Text widget value" = "Τιμή στο πεδίο κειμένου"; // translategemma:4b
// CPU
"CPU usage" = "Χρήση CPU";
"CPU temperature" = "Θερμοκρασία CPU";
"CPU frequency" = "Συχνότητα CPU";
"System" = "Σύστημα";
"User" = "Χρήστης";
"Idle" = "Αδράνεια";
"Show usage per core" = "Εμφάνιση κατανάλωσης ανά επεξεργαστή";
"Show hyper-threading cores" = "Εμφάνιση των hyper-threading πυρήνων";
"Split the value (System/User)" = "Διαχωρισμός των τιμών (Συστήματος/Χρήστη)";
"Scheduler limit" = "Όριο χρονοπρογραμματιστή";
"Speed limit" = "Όριο ταχύτητας";
"Average load" = "Μέσο φορτίο";
"1 minute" = "1 λεπτό";
"5 minutes" = "5 λεπτά";
"15 minutes" = "15 λεπτά";
"CPU usage threshold" = "Όριο χρήσης CPU"; // translategemma:4b
"CPU usage is" = "Η χρήση της CPU είναι στο `%0"; // translategemma:4b
"Efficiency cores" = "Κύρια επεξεργαστικά κέντρα"; // translategemma:4b
"Performance cores" = "Επεξεργαστικές μονάδες"; // translategemma:4b
"System color" = "Χρώμα συστήματος"; // translategemma:4b
"User color" = "Χρώμα χρήστη"; // translategemma:4b
"Idle color" = "Χρώμα σε κατάσταση αναμονής"; // translategemma:4b
"Cluster grouping" = "Ομαδοποίηση"; // translategemma:4b
"Efficiency cores color" = "Χρώμα πυρήνων απόδοσης"; // translategemma:4b
"Performance cores color" = "Χρώμα των πυρήνων απόδοσης"; // translategemma:4b
"Total load" = "Συνολική χωρητικότητα"; // translategemma:4b
"System load" = "Φόρτος συστήματος"; // translategemma:4b
"User load" = "Φόρτος χρηστών"; // translategemma:4b
"Efficiency cores load" = "Ενα λειτουργικά κέντρα φορτώνουν"; // translategemma:4b
"Performance cores load" = "Φόρτωση των βασικών πυρήνων"; // translategemma:4b
"All cores" = "Όλα τα πυρήνες"; // translategemma:4b
// GPU
"GPU to show" = "GPU προς εμφάνιση";
"Show GPU type" = "Εμφάνιση τύπου GPU";
"GPU enabled" = "Ενεργοποιημένη GPU";
"GPU disabled" = "Απενεργοποιημένη GPU";
"GPU temperature" = "Θερμοκρασία GPU";
"GPU utilization" = "Χρήση GPU";
"Vendor" = "Προμηθευτής";
"Model" = "Μοντέλο";
"Status" = "Κατάσταση";
"Active" = "Ενεργή";
"Non active" = "Μη ενεργή";
"Fan speed" = "Ταχύτητα ανεμιστήρα";
"Core clock" = "Ρολόι επεξεργαστή";
"Memory clock" = "Ρολόι μνήμης";
"Utilization" = "Χρήση";
"Render utilization" = "Χρήση επεξεργαστικής ισχύος"; // translategemma:4b
"Tiler utilization" = "Χρήση πλακιδίων"; // translategemma:4b
"GPU usage threshold" = "Όριο χρήσης της GPU"; // translategemma:4b
"GPU usage is" = "Η χρήση της κάρτας γραφικών είναι στο `%0"; // translategemma:4b
// RAM
"Memory usage" = "Χρήση μνήμης";
"Memory pressure" = "Πίεση μνήμης";
"Total" = "Σύνολο";
"Used" = "Χρησιμοποιήθηκε";
"App" = "Εφαρμογή";
"Wired" = "Δεσμευμένη";
"Compressed" = "Συμπιεσμένη";
"Free" = "Ελεύθερη";
"Swap" = "Ανταλλαγή";
"Split the value (App/Wired/Compressed)" = "Χωρισμός της τιμής (Εφαρμογή/Δεσμευμένη/Συμπιεσμένη)";
"RAM utilization threshold" = "Όριο χρήσης μνήμης RAM"; // translategemma:4b
"RAM utilization is" = "Η χρήση της μνήμης RAM είναι στο `%0"; // translategemma:4b
"App color" = "Χρώμα Εφαρμογής";
"Wired color" = "Χρώμα Δεσμευμένης";
"Compressed color" = "Χρώμα Συμπίεσης";
"Free color" = "Χρώμα Ελεύθερου";
"Free memory (less than)" = "Δωρεάν μνήμη (λιγότερο από)"; // translategemma:4b
"Swap size" = "Μέγεθος εναλλαγής"; // translategemma:4b
"Free RAM is" = "Η διαθέσιμη μνήμη RAM είναι %0"; // translategemma:4b
// Disk
"Show removable disks" = "Εμφάνιση αφαιρούμενων δίσκων";
"Used disk memory" = "%0 απο %1 χρησιμοποιήθηκε";
"Free disk memory" = "%0 απο %1 ελεύθερη";
"Disk to show" = "Δίσκοι προς εμφάνιση";
"Open disk" = "Άνοιγμα δίσκου";
"Switch view" = "Αλλαγή προβολής";
"Disk utilization threshold" = "Όριο χρήσης δίσκου"; // translategemma:4b
"Disk utilization is" = "Ο χρόνος που χρησιμοποιείται στο δίσκο είναι το `%0"; // translategemma:4b
"Read color" = "Χρώμα ανάγνωσης";
"Write color" = "Χρώμα εγγραφής";
"Disk usage" = "Χρήση δίσκου"; // translategemma:4b
"Total read" = "Συνολική ανάγνωση"; // translategemma:4b
"Total written" = "Συνολική γραπτή"; // translategemma:4b
"Write speed" = "Γράψτε"; // translategemma:4b
"Read speed" = "Διαβάστε"; // translategemma:4b
"Drives" = "Δίσκοι"; // translategemma:4b
"SMART data" = "Δεδομένα SMART"; // translategemma:4b
// Sensors
"Temperature unit" = "Μονάδα θερμοκρασίας";
"Celsius" = "Κελσίου";
"Fahrenheit" = "Φαρενάιτ";
"Save the fan speed" = "Αποθήκευση ταχύτητας του ανεμιστήρα";
"Fan" = "Ανεμιστήρας";
"HID sensors" = "HID αισθητήρας";
"Synchronize fan's control" = "Συγχρονίστε τον έλεγχο του ανεμιστήρα"; // translategemma:4b
"Current" = "Τρέχουσα";
"Energy" = "Ενέργεια";
"Show unknown sensors" = "Εμφάνιση άγνωστους αισθητήρες";
"Install fan helper" = "Εγκατάσταση fan helper";
"Uninstall fan helper" = "Απεγκατάσταση fan helper";
"Fan value" = "Αξία ανεμιστήρα"; // translategemma:4b
"Turn off fan" = "Απενεργοποιήστε τον ανεμιστήρα"; // translategemma:4b
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Θα απενεργοποιήσετε τον ανεμιστήρα. Αυτή δεν είναι η συνιστώμενη ενέργεια και μπορεί να προκαλέσει ζημιά στον Mac σας. Είστε σίγουροι ότι θέλετε να το κάνετε;"; // translategemma:4b
"Sensor threshold" = "Όριο ανίχνευσης"; // translategemma:4b
"Left fan" = "Αριστερά"; // translategemma:4b
"Right fan" = "Σωστά"; // translategemma:4b
"Fastest fan" = "Ταχύτερος"; // translategemma:4b
"Sensor to show" = "Αισθητήρας για την εμφάνιση"; // translategemma:4b
// Network
"Uploading" = "Μεταφόρτωση";
"Downloading" = "Λήψη";
"Public IP" = "Δημόσια IP";
"Local IP" = "Τοπική IP";
"Interface" = "Διεπαφή";
"Physical address" = "Φυσική διεύθυνση";
"Refresh" = "Ανανέωση";
"Click to copy public IP address" = "Πατήστε για να αντιγράψετε την δημόσια διεύθυνση IP";
"Click to copy local IP address" = "Πατήστε για να αντιγράψετε την τοπική διεύθυνση IP";
"Click to copy wifi name" = "Πατήστε για να αντιγράψετε το όνομα του wifi";
"Click to copy mac address" = "Πατήστε για να αντιγράψετε την διεύθυνση mac";
"No connection" = "Χωρίς σύνδεση";
"Network interface" = "Διεπαφή δικτύου";
"Total download" = "Συνολική λήψη";
"Total upload" = "Συνολική μεταφόρτωση";
"Reader type" = "Τύπος ανάγνωσης";
"Interface based" = "Βάσει διεπαφής";
"Processes based" = "Βάσει διαδικασίας";
"Reset data usage" = "Επαναφορά δεδομένων χρήσης";
"VPN mode" = "Λειτουργία VPN";
"Standard" = "Κανονικός";
"Security" = "Ασφάλεια";
"Channel" = "Κανάλι";
"Common scale" = "Συνήθη κλίμακα"; // translategemma:4b
"Autodetection" = "Αυτόματη ανίχνευση"; // translategemma:4b
"Widget activation threshold" = "Όριο ενεργοποίησης widget"; // translategemma:4b
"Internet connection" = "Σύνδεση στο διαδίκτυο"; // translategemma:4b
"Active state color" = "Χρώμα κατά την ενεργή κατάσταση"; // translategemma:4b
"Nonactive state color" = "Χρώμα κατά την κατάσταση μη λειτουργίας"; // translategemma:4b
"Connectivity host (ICMP)" = "Υπολογιστής διασύνδεσης (ICMP)"; // translategemma:4b
"Leave empty to disable the check" = "Αφήστε το κενό για να απενεργοποιήσετε τον έλεγχο."; // translategemma:4b
"Connectivity history" = "Ιστορικό σύνδεσης"; // translategemma:4b
"Auto-refresh public IP address" = "Αυτόματη ανανέωση της δημόσιας διεύθυνσης IP"; // translategemma:4b
"Every hour" = "Κάθε ώρα"; // translategemma:4b
"Every 12 hours" = "Κάθε 12 ώρες"; // translategemma:4b
"Every 24 hours" = "Κάθε 24 ώρες"; // translategemma:4b
"Network activity" = "Δραστηριότητα δικτύου"; // translategemma:4b
"Last reset" = "Η τελευταία επαναφορά έγινε πριν %0 ημέρες"; // translategemma:4b
"Latency" = "Καθυστέρηση"; // translategemma:4b
"Upload speed" = "Ανέβασμα"; // translategemma:4b
"Download speed" = "Λήψη"; // translategemma:4b
"Address" = "Διεύθυνση"; // translategemma:4b
"WiFi network" = "Δίκτυο Wi-Fi"; // translategemma:4b
"Local IP changed" = "Η τοπική διεύθυνση IP έχει αλλάξει"; // translategemma:4b
"Public IP changed" = "Η δημόσια διεύθυνση IP έχει αλλάξει."; // translategemma:4b
"Previous IP" = "Προηγούμενη διεύθυνση IP: %0"; // translategemma:4b
"New IP" = "Νέα διεύθυνση IP: %0"; // translategemma:4b
"Internet connection lost" = "Απώλεια σύνδεσης στο διαδίκτυο"; // translategemma:4b
"Internet connection established" = "Εγκαταστάθηκε σύνδεση στο διαδίκτυο"; // translategemma:4b
// Battery
"Level" = "Επίπεδο";
"Source" = "Πηγή";
"AC Power" = "Εναλλασσόμενο Ρεύμα";
"Battery Power" = "Πηγή Μπαταρίας";
"Time" = "Χρόνος";
"Health" = "Υγεία";
"Amperage" = "Ένταση";
"Voltage" = "Τάση";
"Cycles" = "Κύκλοι";
"Temperature" = "Θερμοκρασία";
"Power adapter" = "Τροφοδοτικό"; // translategemma:4b
"Power" = "Ισχύς";
"Is charging" = "Σε φόρτιση";
"Time to discharge" = "Χρόνος αποφόρτισης";
"Time to charge" = "Χρόνος φόρτισης";
"Calculating" = "Υπολογίζεται";
"Fully charged" = "Πλήρως φορτισμένη";
"Not connected" = "Δεν συνδέθηκε";
"Low level notification" = "Ειδοποίηση χαμηλού επιπέδου μπαταρίας";
"High level notification" = "Ειδοποίηση υψηλού επιπέδου μπαταρίας";
"Low battery" = "Χαμηλή μπαταρία";
"High battery" = "Υψηλό επίπεδο μπαταρίας";
"Battery remaining" = "%0% απομένουν";
"Battery remaining to full charge" = "%0% για πλήρης φόρτιση";
"Percentage" = "Ποσοστό";
"Percentage and time" = "Ποσοστό και ώρα";
"Time and percentage" = "Ώρα και ποσοστό";
"Time format" = "Μορφή ώρας";
"Hide additional information when full" = "Απόκρυψη επιπρόσθετων πληροφοριών όταν γεμίσει";
"Last charge" = "Τελευταία φόρτιση";
"Capacity" = "Χωρητικότητα";
"current / maximum / designed" = "τρέχουσα / μέγιστη / σχεδιασμένη";
"Low power mode" = "Κατάστασης χαμηλής ενέργειας";
"Percentage inside the icon" = "Ποσοστό μέσα στο εικονίδιο";
"Colorize battery" = "Χρωματίστε την μπαταρία"; // translategemma:4b
"Charging current" = "Ρεύμα φόρτισης"; // translategemma:4b
"Charging Voltage" = "Τάση φόρτισης"; // translategemma:4b
"Charger state inside the battery" = "Κατάσταση φόρτισης μέσα στην μπαταρία"; // translategemma:4b
// Bluetooth
"Battery to show" = "Μπαταρία προς εμφάνιση";
"No Bluetooth devices are available" = "Καμία συσκευή Bluetooth δεν είναι διαθέσιμη";
// Clock
"Time zone" = "Ζώνη ώρας"; // translategemma:4b
"Local" = "Τοπικός"; // translategemma:4b
"Calendar" = "Ημερολόγιο"; // translategemma:4b
"Show week numbers" = "Εμφάνιση αριθμών εβδομάδων"; // translategemma:4b
"Local time" = "Τοπική ώρα"; // translategemma:4b
"Add new clock" = "Προσθέστε ένα νέο ρολόι"; // translategemma:4b
"Delete selected clock" = "Διαγραφή επιλεγμένου χρονοδιαγράμματος"; // translategemma:4b
"Help with datetime format" = "Βοήθεια με τη μορφοποίηση ημερομηνίας και ώρας"; // translategemma:4b
// Colors
"Based on utilization" = "Βάσει της χρήσης";
"Based on pressure" = "Βάσει πίεσης";
"Based on cluster" = "Βασισμένο σε ομαδοποίηση"; // translategemma:4b
"System accent" = "Τόνος συστήματος";
"Monochrome accent" = "Ασπρόμαυρος τόνος";
"Clear" = "Διάφανο";
"White" = "Άσπρο";
"Black" = "Μαύρο";
"Gray" = "Γκρί";
"Second gray" = "Δευτερεύον γκρι";
"Dark gray" = "Σκούρο γκρι";
"Light gray" = "Απαλό γκρι";
"Red" = "Κόκκινο";
"Second red" = "Δευτερεύον κόκκινο";
"Green" = "Πράσινο";
"Second green" = "Δευτερεύον πράσινο";
"Blue" = "Μπλε";
"Second blue" = "Δευτερεύον μπλε";
"Yellow" = "Κίτρινο";
"Second yellow" = "Δευτερεύον κίτρινο";
"Orange" = "Πορτοκαλί";
"Second orange" = "Δευτερεύον πορτοκαλί";
"Purple" = "Μώβ";
"Second purple" = "Δευτερεύον μώβ";
"Brown" = "Καφέ";
"Second brown" = "Δευτερεύον καφέ";
"Cyan" = "Κυανό";
"Magenta" = "Φούξια";
"Pink" = "Ροζ";
"Teal" = "Γαλαζοπράσινο";
"Indigo" = "Βαθύ μπλε";
================================================
FILE: Stats/Supporting Files/en-AU.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Will Georges on 15/12/2024.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "Open CPU settings";
"GPU" = "GPU";
"Open GPU settings" = "Open GPU settings";
"RAM" = "RAM";
"Open RAM settings" = "Open RAM settings";
"Disk" = "Disk";
"Open Disk settings" = "Open disk settings";
"Sensors" = "Sensors";
"Open Sensors settings" = "Open sensors settings";
"Network" = "Network";
"Open Network settings" = "Open network settings";
"Battery" = "Battery";
"Open Battery settings" = "Open battery settings";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Open bluetooth settings";
"Clock" = "Clock";
"Open Clock settings" = "Open clock settings";
// Words
"Unknown" = "Unknown";
"Version" = "Version";
"Processor" = "Processor";
"Memory" = "Memory";
"Graphics" = "Graphics";
"Close" = "Close";
"Download" = "Download";
"Install" = "Install";
"Cancel" = "Cancel";
"Unavailable" = "Unavailable";
"Yes" = "Yes";
"No" = "No";
"Automatic" = "Automatic";
"Manual" = "Manual";
"None" = "None";
"Dots" = "Dots";
"Arrows" = "Arrows";
"Characters" = "Character";
"Short" = "Short";
"Long" = "Long";
"Statistics" = "Statistics";
"Max" = "Max";
"Min" = "Min";
"Reset" = "Reset";
"Alignment" = "Alignment";
"Left alignment" = "Left";
"Center alignment" = "Center";
"Right alignment" = "Right";
"Dashboard" = "Dashboard";
"Enabled" = "Enabled";
"Disabled" = "Disabled";
"Silent" = "Silent";
"Units" = "Units";
"Fans" = "Fans";
"Scaling" = "Scaling";
"Linear" = "Linear";
"Square" = "Square";
"Cube" = "Cube";
"Logarithmic" = "Logarithmic";
"Fixed scale" = "Fixed";
"Cores" = "Cores";
"Settings" = "Settings";
"Name" = "Name";
"Format" = "Format";
"Turn off" = "Turn off";
"Normal" = "Normal";
"Warning" = "Warning";
"Critical" = "Critical";
"Usage" = "Usage";
"2 minutes" = "2 minutes";
"3 minutes" = "3 minutes";
"10 minutes" = "10 minutes";
"Import" = "Import";
"Export" = "Export";
"Separator" = "Separator";
"Read" = "Read";
"Write" = "Write";
"Frequency" = "Frequency";
"Save" = "Save";
"Run" = "Run";
"Stop" = "Stop";
"Uninstall" = "Uninstall";
"1 sec" = "1 sec";
"2 sec" = "2 sec";
"3 sec" = "3 sec";
"5 sec" = "5 sec";
"10 sec" = "10 sec";
"15 sec" = "15 sec";
"30 sec" = "30 sec";
"60 sec" = "60 sec";
// Setup
"Stats Setup" = "Stats Setup";
"Previous" = "Previous";
"Previous page" = "Previous page";
"Next" = "Next";
"Next page" = "Next page";
"Finish" = "Finish";
"Finish setup" = "Finish setup";
"Welcome to Stats" = "Welcome to Stats";
"welcome_message" = "Thanks for using Stats, a free open source macOS system monitor for your menu bar.";
"Start the application automatically when starting your Mac" = "Start the application automatically when starting your Mac";
"Do not start the application automatically when starting your Mac" = "Do not start the application automatically when starting your Mac";
"Do everything silently in the background (recommended)" = "Do everything silently in the background (recommended)";
"Check for a new version on startup" = "Check for a new version on startup";
"Check for a new version every day (once a day)" = "Check for a new version every day (once a day)";
"Check for a new version every week (once a week)" = "Check for a new version every week (once a week)";
"Check for a new version every month (once a month)" = "Check for a new version every month (once a month)";
"Never check for updates (not recommended)" = "Never check for updates (not recommended)";
"Anonymous telemetry for better development decisions" = "Anonymous telemetry for better development decisions";
"Share anonymous telemetry data" = "Share anonymous telemetry data";
"Do not share anonymous telemetry data" = "Do not share anonymous telemetry data";
"The configuration is completed" = "The configuration is completed";
"finish_setup_message" = "Everything is set up! \n The Stats is an open source tool, it's free and always will be. \n If you enjoy it you can support a project, it's always appreciated!";
// Alerts
"New version available" = "New version available";
"Click to install the new version of Stats" = "Click to install the new version of Stats";
"Successfully updated" = "Successfully updated";
"Stats was updated to v" = "Stats was updated to v%0";
"Reset settings text" = "All application settings will be reset and the application will be restarted. Are you sure you want to do this?";
"Support text" = "Thank you for using Stats!\n\n Maintaining and improving this open-source project takes time and resources. Your support helps us continue to provide a free and reliable application for everyone.\n\nIf you find Stats helpful, please consider making a contribution. Every little bit helps!";
// Settings
"Open Activity Monitor" = "Open Activity Monitor";
"Report a bug" = "Report a bug";
"Support the application" = "Support the application";
"Close application" = "Close application";
"Open application settings" = "Open application settings";
"Open dashboard" = "Open dashboard";
"No notifications available in this module" = "No notifications available in this module";
"Open Calendar" = "Open Calendar";
"Toggle the module" = "Toggle the module";
// Application settings
"Update application" = "Update application";
"Check for updates" = "Check for updates";
"At start" = "At start";
"Once per day" = "Once per day";
"Once per week" = "Once per week";
"Once per month" = "Once per month";
"Never" = "Never";
"Check for update" = "Check for update";
"Show icon in dock" = "Show icon in dock";
"Start at login" = "Start at login";
"Build number" = "Build number";
"Import settings" = "Import settings";
"Export settings" = "Export settings";
"Reset settings" = "Reset settings";
"Pause the Stats" = "Pause the Stats";
"Resume the Stats" = "Resume the Stats";
"Combined modules" = "Combined modules";
"Combined details" = "Combined details";
"Spacing" = "Spacing";
"Share anonymous telemetry" = "Share anonymous telemetry";
"Choose file" = "Choose file";
"Stress tests" = "Stress tests";
// Dashboard
"Serial number" = "Serial number";
"Model identifier" = "Model identifier";
"Production year" = "Production year";
"Uptime" = "Uptime";
"Number of cores" = "%0 cores";
"Number of threads" = "%0 threads";
"Number of e-cores" = "%0 efficiency cores";
"Number of p-cores" = "%0 performance cores";
"Disks" = "Disks";
"Display" = "Display";
// Update
"The latest version of Stats installed" = "The latest version of Stats is installed";
"Downloading..." = "Downloading...";
"Current version: " = "Current version: ";
"Latest version: " = "Latest version: ";
// Widgets
"Color" = "Colour";
"Label" = "Label";
"Box" = "Box";
"Frame" = "Frame";
"Value" = "Value";
"Colorize" = "Colourise";
"Colorize value" = "Colourise value";
"Additional information" = "Additional information";
"Reverse values order" = "Reverse values order";
"Base" = "Base";
"Display mode" = "View mode";
"One row" = "One row";
"Two rows" = "Two rows";
"Mini widget" = "Mini";
"Line chart widget" = "Line chart";
"Bar chart widget" = "Bar chart";
"Pie chart widget" = "Pie chart";
"Network chart widget" = "Network chart";
"Speed widget" = "Speed";
"Battery widget" = "Battery";
"Stack widget" = "Stack";
"Memory widget" = "Memory";
"Static width" = "Static width";
"Tachometer widget" = "Tachometer";
"State widget" = "State widget";
"Text widget" = "Text widget";
"Battery details widget" = "Battery details widget";
"Show symbols" = "Show symbols";
"Label widget" = "Label";
"Number of reads in the chart" = "Number of reads in the chart";
"Color of download" = "Colour of download";
"Color of upload" = "Colour of upload";
"Monospaced font" = "Monospaced font";
"Reverse order" = "Reverse order";
"Chart history" = "Chart history";
"Default color" = "Default";
"Transparent when no activity" = "Transparent when no activity";
"Constant color" = "Constant";
// Module Kit
"Open module settings" = "Open module settings";
"Select widget" = "Select %0 widget";
"Open widget settings" = "Open widget settings";
"Update interval" = "Update interval";
"Usage history" = "Usage history";
"Details" = "Details";
"Top processes" = "Top processes";
"Pictogram" = "Pictogram";
"Module" = "Module";
"Widgets" = "Widgets";
"Popup" = "Popup";
"Notifications" = "Notifications";
"Merge widgets" = "Merge widgets";
"No available widgets to configure" = "No available widgets to configure";
"No options to configure for the popup in this module" = "No options to configure for the popup in this module";
"Process" = "Process";
"Kill process" = "Kill process";
"Keyboard shortcut" = "Keyboard shortcut";
"Listening..." = "Listening...";
// Modules
"Number of top processes" = "Number of top processes";
"Update interval for top processes" = "Update interval for top processes";
"Notification level" = "Notification level";
"Chart color" = "Chart colour";
"Main chart scaling" = "Main chart scaling";
"Scale value" = "Scale value";
"Text widget value" = "Text widget value";
// CPU
"CPU usage" = "CPU usage";
"CPU temperature" = "CPU temperature";
"CPU frequency" = "CPU frequency";
"System" = "System";
"User" = "User";
"Idle" = "Idle";
"Show usage per core" = "Show usage per core";
"Show hyper-threading cores" = "Show hyper-threading cores";
"Split the value (System/User)" = "Split the value (System/User)";
"Scheduler limit" = "Scheduler limit";
"Speed limit" = "Speed limit";
"Average load" = "Average load";
"1 minute" = "1 minute";
"5 minutes" = "5 minutes";
"15 minutes" = "15 minutes";
"CPU usage threshold" = "CPU usage threshold";
"CPU usage is" = "CPU usage is %0";
"Efficiency cores" = "Efficiency cores";
"Performance cores" = "Performance cores";
"System color" = "System colour";
"User color" = "User colour";
"Idle color" = "Idle colour";
"Cluster grouping" = "Cluster grouping";
"Efficiency cores color" = "Efficiency cores colour";
"Performance cores color" = "Performance cores colour";
"Total load" = "Total load";
"System load" = "System load";
"User load" = "User load";
"Efficiency cores load" = "Efficiency cores load";
"Performance cores load" = "Performance cores load";
"All cores" = "All cores";
// GPU
"GPU to show" = "GPU to show";
"Show GPU type" = "Show GPU type";
"GPU enabled" = "GPU enabled";
"GPU disabled" = "GPU disabled";
"GPU temperature" = "GPU temperature";
"GPU utilization" = "GPU utilisation";
"Vendor" = "Vendor";
"Model" = "Model";
"Status" = "Status";
"Active" = "Active";
"Non active" = "Non active";
"Fan speed" = "Fan speed";
"Core clock" = "Core clock";
"Memory clock" = "Memory clock";
"Utilization" = "Utilisation";
"Render utilization" = "Render utilisation";
"Tiler utilization" = "Tiler utilisation";
"GPU usage threshold" = "GPU usage threshold";
"GPU usage is" = "GPU usage is %0";
// RAM
"Memory usage" = "Memory usage";
"Memory pressure" = "Memory pressure";
"Total" = "Total";
"Used" = "Used";
"App" = "App";
"Wired" = "Wired";
"Compressed" = "Compressed";
"Free" = "Free";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Split the value (App/Wired/Compressed)";
"RAM utilization threshold" = "RAM utilisation threshold";
"RAM utilization is" = "RAM utilisation is %0";
"App color" = "App colour";
"Wired color" = "Wired colour";
"Compressed color" = "Compressed colour";
"Free color" = "Free colour";
"Free memory (less than)" = "Free memory (less than)";
"Swap size" = "Swap size";
"Free RAM is" = "Free RAM is %0";
// Disk
"Show removable disks" = "Show removable disks";
"Used disk memory" = "%0 of %1 used";
"Free disk memory" = "%0 of %1 free";
"Disk to show" = "Disk to show";
"Open disk" = "Open disk";
"Switch view" = "Switch view";
"Disk utilization threshold" = "Disk utilisation threshold";
"Disk utilization is" = "Disk utilisation is %0";
"Read color" = "Read colour";
"Write color" = "Write colour";
"Disk usage" = "Disk usage";
"Total read" = "Total read";
"Total written" = "Total written";
"Write speed" = "Write";
"Read speed" = "Read";
"Drives" = "Drives";
"SMART data" = "SMART data";
// Sensors
"Temperature unit" = "Temperature unit";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Save the fan speed";
"Fan" = "Fan";
"HID sensors" = "HID sensors";
"Synchronize fan's control" = "Synchronise fan's control";
"Current" = "Current";
"Energy" = "Energy";
"Show unknown sensors" = "Show unknown sensors";
"Install fan helper" = "Install fan helper";
"Uninstall fan helper" = "Uninstall fan helper";
"Fan value" = "Fan value";
"Turn off fan" = "Turn off fan";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "You are going to turn off the fan. This is not recommended action that can damage your Mac, are you sure you want to do that?";
"Sensor threshold" = "Sensor threshold";
"Left fan" = "Left";
"Right fan" = "Right";
"Fastest fan" = "Fastest";
"Sensor to show" = "Sensor to show";
// Network
"Uploading" = "Upload";
"Downloading" = "Download";
"Public IP" = "Public IP";
"Local IP" = "Local IP";
"Interface" = "Interface";
"Physical address" = "Physical address";
"Refresh" = "Refresh";
"Click to copy public IP address" = "Click to copy public IP address";
"Click to copy local IP address" = "Click to copy local IP address";
"Click to copy wifi name" = "Click to copy WiFi name";
"Click to copy mac address" = "Click to copy MAC address";
"No connection" = "No connection";
"Network interface" = "Network interface";
"Total download" = "Total download";
"Total upload" = "Total upload";
"Reader type" = "Reader type";
"Interface based" = "Interface based";
"Processes based" = "Process based";
"Reset data usage" = "Reset data usage";
"VPN mode" = "VPN mode";
"Standard" = "Standard";
"Security" = "Security";
"Channel" = "Channel";
"Common scale" = "Common scale";
"Autodetection" = "Autodetection";
"Widget activation threshold" = "Widget activation threshold";
"Internet connection" = "Internet connection";
"Active state color" = "Active state colour";
"Nonactive state color" = "Nonactive state colour";
"Connectivity host (ICMP)" = "Connectivity host (ICMP)";
"Leave empty to disable the check" = "Leave empty to disable the check";
"Connectivity history" = "Connectivity history";
"Auto-refresh public IP address" = "Auto-refresh public IP address";
"Every hour" = "Every hour";
"Every 12 hours" = "Every 12 hours";
"Every 24 hours" = "Every 24 hours";
"Network activity" = "Network activity";
"Last reset" = "Last reset %0 ago";
"Latency" = "Latency";
"Upload speed" = "Upload";
"Download speed" = "Download";
"Address" = "Address";
"WiFi network" = "WiFi network";
"Local IP changed" = "Local IP has changed";
"Public IP changed" = "Public IP has changed";
"Previous IP" = "Previous IP: %0";
"New IP" = "New IP: %0";
"Internet connection lost" = "Internet connection lost";
"Internet connection established" = "Internet connection established";
// Battery
"Level" = "Level";
"Source" = "Source";
"AC Power" = "AC Power";
"Battery Power" = "Battery Power";
"Time" = "Time";
"Health" = "Health";
"Amperage" = "Amperage";
"Voltage" = "Voltage";
"Cycles" = "Cycles";
"Temperature" = "Temperature";
"Power adapter" = "Power adapter";
"Power" = "Power";
"Is charging" = "Is charging";
"Time to discharge" = "Time to discharge";
"Time to charge" = "Time to charge";
"Calculating" = "Calculating";
"Fully charged" = "Fully charged";
"Not connected" = "Not connected";
"Low level notification" = "Low level notification";
"High level notification" = "High level notification";
"Low battery" = "Low battery";
"High battery" = "High battery";
"Battery remaining" = "%0% remaining";
"Battery remaining to full charge" = "%0% to full charge";
"Percentage" = "Percentage";
"Percentage and time" = "Percentage and time";
"Time and percentage" = "Time and percentage";
"Time format" = "Time format";
"Hide additional information when full" = "Hide additional information when full";
"Last charge" = "Last charge";
"Capacity" = "Capacity";
"current / maximum / designed" = "current / maximum / designed";
"Low power mode" = "Low power mode";
"Percentage inside the icon" = "Percentage inside the icon";
"Colorize battery" = "Colourise battery";
"Charging current" = "Charging current";
"Charging Voltage" = "Charging voltage";
"Charger state inside the battery" = "Charger state inside the battery";
// Bluetooth
"Battery to show" = "Battery to show";
"No Bluetooth devices are available" = "No Bluetooth devices are available";
// Clock
"Time zone" = "Time zone";
"Local" = "Local";
"Calendar" = "Calendar";
"Show week numbers" = "Show week numbers";
"Local time" = "Local time";
"Add new clock" = "Add new clock";
"Delete selected clock" = "Delete selected clock";
"Help with datetime format" = "Help with datetime format";
// Colors
"Based on utilization" = "Based on utilisation";
"Based on pressure" = "Based on pressure";
"Based on cluster" = "Based on cluster";
"System accent" = "System accent";
"Monochrome accent" = "Monochrome accent";
"Clear" = "Clear";
"White" = "White";
"Black" = "Black";
"Gray" = "Grey";
"Second gray" = "Second grey";
"Dark gray" = "Dark grey";
"Light gray" = "Light grey";
"Red" = "Red";
"Second red" = "Second red";
"Green" = "Green";
"Second green" = "Second green";
"Blue" = "Blue";
"Second blue" = "Second blue";
"Yellow" = "Yellow";
"Second yellow" = "Second yellow";
"Orange" = "Orange";
"Second orange" = "Second orange";
"Purple" = "Purple";
"Second purple" = "Second purple";
"Brown" = "Brown";
"Second brown" = "Second brown";
"Cyan" = "Cyan";
"Magenta" = "Magenta";
"Pink" = "Pink";
"Teal" = "Teal";
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/en-GB.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Translated with <3 by Coopydood
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "Open CPU settings";
"GPU" = "GPU";
"Open GPU settings" = "Open GPU settings";
"RAM" = "RAM";
"Open RAM settings" = "Open RAM settings";
"Disk" = "Disk";
"Open Disk settings" = "Open disk settings";
"Sensors" = "Sensors";
"Open Sensors settings" = "Open sensors settings";
"Network" = "Network";
"Open Network settings" = "Open network settings";
"Battery" = "Battery";
"Open Battery settings" = "Open battery settings";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Open bluetooth settings";
"Clock" = "Clock";
"Open Clock settings" = "Open clock settings";
// Words
"Unknown" = "Unknown";
"Version" = "Version";
"Processor" = "Processor";
"Memory" = "Memory";
"Graphics" = "Graphics";
"Close" = "Close";
"Download" = "Download";
"Install" = "Install";
"Cancel" = "Cancel";
"Unavailable" = "Unavailable";
"Yes" = "Yes";
"No" = "No";
"Automatic" = "Automatic";
"Manual" = "Manual";
"None" = "None";
"Dots" = "Dots";
"Arrows" = "Arrows";
"Characters" = "Character";
"Short" = "Short";
"Long" = "Long";
"Statistics" = "Statistics";
"Max" = "Max";
"Min" = "Min";
"Reset" = "Reset";
"Alignment" = "Alignment";
"Left alignment" = "Left";
"Center alignment" = "Centre";
"Right alignment" = "Right";
"Dashboard" = "Dashboard";
"Enabled" = "Enabled";
"Disabled" = "Disabled";
"Silent" = "Silent";
"Units" = "Units";
"Fans" = "Fans";
"Scaling" = "Scaling";
"Linear" = "Linear";
"Square" = "Square";
"Cube" = "Cube";
"Logarithmic" = "Logarithmic";
"Fixed scale" = "Fixed";
"Cores" = "Cores";
"Settings" = "Settings";
"Name" = "Name";
"Format" = "Format";
"Turn off" = "Turn off";
"Normal" = "Normal";
"Warning" = "Warning";
"Critical" = "Critical";
"Usage" = "Usage";
"2 minutes" = "2 minutes";
"3 minutes" = "3 minutes";
"10 minutes" = "10 minutes";
"Import" = "Import";
"Export" = "Export";
"Separator" = "Separator";
"Read" = "Read";
"Write" = "Write";
"Frequency" = "Frequency";
"Save" = "Save";
"Run" = "Run";
"Stop" = "Stop";
"Uninstall" = "Uninstall";
"1 sec" = "1 sec";
"2 sec" = "2 sec";
"3 sec" = "3 sec";
"5 sec" = "5 sec";
"10 sec" = "10 sec";
"15 sec" = "15 sec";
"30 sec" = "30 sec";
"60 sec" = "60 sec";
// Setup
"Stats Setup" = "Stats Setup";
"Previous" = "Previous";
"Previous page" = "Previous page";
"Next" = "Next";
"Next page" = "Next page";
"Finish" = "Finish";
"Finish setup" = "Finish setup";
"Welcome to Stats" = "Welcome to Stats";
"welcome_message" = "Thanks for using Stats, a free open source macOS system monitor for your menu bar.";
"Start the application automatically when starting your Mac" = "Start the application automatically when starting your Mac";
"Do not start the application automatically when starting your Mac" = "Do not start the application automatically when starting your Mac";
"Do everything silently in the background (recommended)" = "Do everything silently in the background (recommended)";
"Check for a new version on startup" = "Check for a new version on startup";
"Check for a new version every day (once a day)" = "Check for a new version every day (once a day)";
"Check for a new version every week (once a week)" = "Check for a new version every week (once a week)";
"Check for a new version every month (once a month)" = "Check for a new version every month (once a month)";
"Never check for updates (not recommended)" = "Never check for updates (not recommended)";
"Anonymous telemetry for better development decisions" = "Anonymous telemetry for better development decisions";
"Share anonymous telemetry data" = "Share anonymous telemetry data";
"Do not share anonymous telemetry data" = "Do not share anonymous telemetry data";
"The configuration is completed" = "The configuration is completed";
"finish_setup_message" = "Everything is set up! \n The Stats is an open source tool, it's free and always will be. \n If you enjoy it you can support a project, it's always appreciated!";
// Alerts
"New version available" = "New version available";
"Click to install the new version of Stats" = "Click to install the new version of Stats";
"Successfully updated" = "Successfully updated";
"Stats was updated to v" = "Stats was updated to v%0";
"Reset settings text" = "All application settings will be reset and the application will be restarted. Are you sure you want to do this?";
"Support text" = "Thank you for using Stats!\n\n Maintaining and improving this open-source project takes time and resources. Your support helps us continue to provide a free and reliable application for everyone.\n\nIf you find Stats helpful, please consider making a contribution. Every little bit helps!";
// Settings
"Open Activity Monitor" = "Open Activity Monitor";
"Report a bug" = "Report a bug";
"Support the application" = "Support the application";
"Close application" = "Close application";
"Open application settings" = "Open application settings";
"Open dashboard" = "Open dashboard";
"No notifications available in this module" = "No notifications available in this module";
"Open Calendar" = "Open Calendar";
"Toggle the module" = "Toggle the module";
// Application settings
"Update application" = "Update application";
"Check for updates" = "Check for updates";
"At start" = "At start";
"Once per day" = "Once per day";
"Once per week" = "Once per week";
"Once per month" = "Once per month";
"Never" = "Never";
"Check for update" = "Check for update";
"Show icon in dock" = "Show icon in dock";
"Start at login" = "Start at login";
"Build number" = "Build number";
"Import settings" = "Import settings";
"Export settings" = "Export settings";
"Reset settings" = "Reset settings";
"Pause the Stats" = "Pause the Stats";
"Resume the Stats" = "Resume the Stats";
"Combined modules" = "Combined modules";
"Combined details" = "Combined details";
"Spacing" = "Spacing";
"Share anonymous telemetry" = "Share anonymous telemetry";
"Choose file" = "Choose file";
"Stress tests" = "Stress tests";
// Dashboard
"Serial number" = "Serial number";
"Model identifier" = "Model identifier";
"Production year" = "Production year";
"Uptime" = "Uptime";
"Number of cores" = "%0 cores";
"Number of threads" = "%0 threads";
"Number of e-cores" = "%0 efficiency cores";
"Number of p-cores" = "%0 performance cores";
"Disks" = "Disks";
"Display" = "Display";
// Update
"The latest version of Stats installed" = "The latest version of Stats is installed";
"Downloading..." = "Downloading...";
"Current version: " = "Current version: ";
"Latest version: " = "Latest version: ";
// Widgets
"Color" = "Colour";
"Label" = "Label";
"Box" = "Box";
"Frame" = "Frame";
"Value" = "Value";
"Colorize" = "Colourise";
"Colorize value" = "Colourise value";
"Additional information" = "Additional information";
"Reverse values order" = "Reverse values order";
"Base" = "Base";
"Display mode" = "View mode";
"One row" = "One row";
"Two rows" = "Two rows";
"Mini widget" = "Mini";
"Line chart widget" = "Line chart";
"Bar chart widget" = "Bar chart";
"Pie chart widget" = "Pie chart";
"Network chart widget" = "Network chart";
"Speed widget" = "Speed";
"Battery widget" = "Battery";
"Stack widget" = "Stack";
"Memory widget" = "Memory";
"Static width" = "Static width";
"Tachometer widget" = "Tachometer";
"State widget" = "State widget";
"Text widget" = "Text widget";
"Battery details widget" = "Battery details widget";
"Show symbols" = "Show symbols";
"Label widget" = "Label";
"Number of reads in the chart" = "Number of reads in the chart";
"Color of download" = "Colour of download";
"Color of upload" = "Colour of upload";
"Monospaced font" = "Monospaced font";
"Reverse order" = "Reverse order";
"Chart history" = "Chart history";
"Default color" = "Default";
"Transparent when no activity" = "Transparent when no activity";
"Constant color" = "Constant";
// Module Kit
"Open module settings" = "Open module settings";
"Select widget" = "Select %0 widget";
"Open widget settings" = "Open widget settings";
"Update interval" = "Update interval";
"Usage history" = "Usage history";
"Details" = "Details";
"Top processes" = "Top processes";
"Pictogram" = "Pictogram";
"Module" = "Module";
"Widgets" = "Widgets";
"Popup" = "Popup";
"Notifications" = "Notifications";
"Merge widgets" = "Merge widgets";
"No available widgets to configure" = "No available widgets to configure";
"No options to configure for the popup in this module" = "No options to configure for the popup in this module";
"Process" = "Process";
"Kill process" = "Kill process";
"Keyboard shortcut" = "Keyboard shortcut";
"Listening..." = "Listening...";
// Modules
"Number of top processes" = "Number of top processes";
"Update interval for top processes" = "Update interval for top processes";
"Notification level" = "Notification level";
"Chart color" = "Chart colour";
"Main chart scaling" = "Main chart scaling";
"Scale value" = "Scale value";
"Text widget value" = "Text widget value";
// CPU
"CPU usage" = "CPU usage";
"CPU temperature" = "CPU temperature";
"CPU frequency" = "CPU frequency";
"System" = "System";
"User" = "User";
"Idle" = "Idle";
"Show usage per core" = "Show usage per core";
"Show hyper-threading cores" = "Show hyper-threading cores";
"Split the value (System/User)" = "Split the value (System/User)";
"Scheduler limit" = "Scheduler limit";
"Speed limit" = "Speed limit";
"Average load" = "Average load";
"1 minute" = "1 minute";
"5 minutes" = "5 minutes";
"15 minutes" = "15 minutes";
"CPU usage threshold" = "CPU usage threshold";
"CPU usage is" = "CPU usage is %0";
"Efficiency cores" = "Efficiency cores";
"Performance cores" = "Performance cores";
"System color" = "System colour";
"User color" = "User colour";
"Idle color" = "Idle colour";
"Cluster grouping" = "Cluster grouping";
"Efficiency cores color" = "Efficiency cores colour";
"Performance cores color" = "Performance cores colour";
"Total load" = "Total load";
"System load" = "System load";
"User load" = "User load";
"Efficiency cores load" = "Efficiency cores load";
"Performance cores load" = "Performance cores load";
"All cores" = "All cores";
// GPU
"GPU to show" = "GPU to show";
"Show GPU type" = "Show GPU type";
"GPU enabled" = "GPU enabled";
"GPU disabled" = "GPU disabled";
"GPU temperature" = "GPU temperature";
"GPU utilization" = "GPU utilisation";
"Vendor" = "Vendor";
"Model" = "Model";
"Status" = "Status";
"Active" = "Active";
"Non active" = "Non active";
"Fan speed" = "Fan speed";
"Core clock" = "Core clock";
"Memory clock" = "Memory clock";
"Utilization" = "Utilisation";
"Render utilization" = "Render utilisation";
"Tiler utilization" = "Tiler utilisation";
"GPU usage threshold" = "GPU usage threshold";
"GPU usage is" = "GPU usage is %0";
// RAM
"Memory usage" = "Memory usage";
"Memory pressure" = "Memory pressure";
"Total" = "Total";
"Used" = "Used";
"App" = "App";
"Wired" = "Wired";
"Compressed" = "Compressed";
"Free" = "Free";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Split the value (App/Wired/Compressed)";
"RAM utilization threshold" = "RAM utilisation threshold";
"RAM utilization is" = "RAM utilisation is %0";
"App color" = "App colour";
"Wired color" = "Wired colour";
"Compressed color" = "Compressed colour";
"Free color" = "Free colour";
"Free memory (less than)" = "Free memory (less than)";
"Swap size" = "Swap size";
"Free RAM is" = "Free RAM is %0";
// Disk
"Show removable disks" = "Show removable disks";
"Used disk memory" = "%0 of %1 used";
"Free disk memory" = "%0 of %1 free";
"Disk to show" = "Disk to show";
"Open disk" = "Open disk";
"Switch view" = "Switch view";
"Disk utilization threshold" = "Disk utilisation threshold";
"Disk utilization is" = "Disk utilisation is %0";
"Read color" = "Read colour";
"Write color" = "Write colour";
"Disk usage" = "Disk usage";
"Total read" = "Total read";
"Total written" = "Total written";
"Write speed" = "Write";
"Read speed" = "Read";
"Drives" = "Drives";
"SMART data" = "SMART data";
// Sensors
"Temperature unit" = "Temperature unit";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Save the fan speed";
"Fan" = "Fan";
"HID sensors" = "HID sensors";
"Synchronize fan's control" = "Synchronise fan's control";
"Current" = "Current";
"Energy" = "Energy";
"Show unknown sensors" = "Show unknown sensors";
"Install fan helper" = "Install fan helper";
"Uninstall fan helper" = "Uninstall fan helper";
"Fan value" = "Fan value";
"Turn off fan" = "Turn off fan";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "You are going to turn off the fan. This is not recommended action that can damage your Mac, are you sure you want to do that?";
"Sensor threshold" = "Sensor threshold";
"Left fan" = "Left";
"Right fan" = "Right";
"Fastest fan" = "Fastest";
"Sensor to show" = "Sensor to show";
// Network
"Uploading" = "Upload";
"Downloading" = "Download";
"Public IP" = "Public IP";
"Local IP" = "Local IP";
"Interface" = "Interface";
"Physical address" = "Physical address";
"Refresh" = "Refresh";
"Click to copy public IP address" = "Click to copy public IP address";
"Click to copy local IP address" = "Click to copy local IP address";
"Click to copy wifi name" = "Click to copy Wi-Fi name";
"Click to copy mac address" = "Click to copy MAC address";
"No connection" = "No connection";
"Network interface" = "Network interface";
"Total download" = "Total download";
"Total upload" = "Total upload";
"Reader type" = "Reader type";
"Interface based" = "Interface based";
"Processes based" = "Process based";
"Reset data usage" = "Reset data usage";
"VPN mode" = "VPN mode";
"Standard" = "Standard";
"Security" = "Security";
"Channel" = "Channel";
"Common scale" = "Common scale";
"Autodetection" = "Autodetection";
"Widget activation threshold" = "Widget activation threshold";
"Internet connection" = "Internet connection";
"Active state color" = "Active state colour";
"Nonactive state color" = "Nonactive state colour";
"Connectivity host (ICMP)" = "Connectivity host (ICMP)";
"Leave empty to disable the check" = "Leave empty to disable the check";
"Connectivity history" = "Connectivity history";
"Auto-refresh public IP address" = "Auto-refresh public IP address";
"Every hour" = "Every hour";
"Every 12 hours" = "Every 12 hours";
"Every 24 hours" = "Every 24 hours";
"Network activity" = "Network activity";
"Last reset" = "Last reset %0 ago";
"Latency" = "Latency";
"Upload speed" = "Upload";
"Download speed" = "Download";
"Address" = "Address";
"WiFi network" = "WiFi network";
"Local IP changed" = "Local IP has changed";
"Public IP changed" = "Public IP has changed";
"Previous IP" = "Previous IP: %0";
"New IP" = "New IP: %0";
"Internet connection lost" = "Internet connection lost";
"Internet connection established" = "Internet connection established";
// Battery
"Level" = "Level";
"Source" = "Source";
"AC Power" = "AC Power";
"Battery Power" = "Battery Power";
"Time" = "Time";
"Health" = "Health";
"Amperage" = "Amperage";
"Voltage" = "Voltage";
"Cycles" = "Cycles";
"Temperature" = "Temperature";
"Power adapter" = "Power adapter";
"Power" = "Power";
"Is charging" = "Is charging";
"Time to discharge" = "Time to discharge";
"Time to charge" = "Time to charge";
"Calculating" = "Calculating";
"Fully charged" = "Fully charged";
"Not connected" = "Not connected";
"Low level notification" = "Low level notification";
"High level notification" = "High level notification";
"Low battery" = "Low battery";
"High battery" = "High battery";
"Battery remaining" = "%0% remaining";
"Battery remaining to full charge" = "%0% to full charge";
"Percentage" = "Percentage";
"Percentage and time" = "Percentage and time";
"Time and percentage" = "Time and percentage";
"Time format" = "Time format";
"Hide additional information when full" = "Hide additional information when full";
"Last charge" = "Last charge";
"Capacity" = "Capacity";
"current / maximum / designed" = "current / maximum / designed";
"Low power mode" = "Low power mode";
"Percentage inside the icon" = "Percentage inside the icon";
"Colorize battery" = "Colourise battery";
"Charging current" = "Charging current";
"Charging Voltage" = "Charging voltage";
"Charger state inside the battery" = "Charger state inside the battery";
// Bluetooth
"Battery to show" = "Battery to show";
"No Bluetooth devices are available" = "No Bluetooth devices are available";
// Clock
"Time zone" = "Time zone";
"Local" = "Local";
"Calendar" = "Calendar";
"Show week numbers" = "Show week numbers";
"Local time" = "Local time";
"Add new clock" = "Add new clock";
"Delete selected clock" = "Delete selected clock";
"Help with datetime format" = "Help with datetime format";
// Colors
"Based on utilization" = "Based on utilisation";
"Based on pressure" = "Based on pressure";
"Based on cluster" = "Based on cluster";
"System accent" = "System accent";
"Monochrome accent" = "Monochrome accent";
"Clear" = "Clear";
"White" = "White";
"Black" = "Black";
"Gray" = "Grey";
"Second gray" = "Second grey";
"Dark gray" = "Dark grey";
"Light gray" = "Light grey";
"Red" = "Red";
"Second red" = "Second red";
"Green" = "Green";
"Second green" = "Second green";
"Blue" = "Blue";
"Second blue" = "Second blue";
"Yellow" = "Yellow";
"Second yellow" = "Second yellow";
"Orange" = "Orange";
"Second orange" = "Second orange";
"Purple" = "Purple";
"Second purple" = "Second purple";
"Brown" = "Brown";
"Second brown" = "Second brown";
"Cyan" = "Cyan";
"Magenta" = "Magenta";
"Pink" = "Pink";
"Teal" = "Teal";
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/en.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "Open CPU settings";
"GPU" = "GPU";
"Open GPU settings" = "Open GPU settings";
"RAM" = "RAM";
"Open RAM settings" = "Open RAM settings";
"Disk" = "Disk";
"Open Disk settings" = "Open disk settings";
"Sensors" = "Sensors";
"Open Sensors settings" = "Open sensors settings";
"Network" = "Network";
"Open Network settings" = "Open network settings";
"Battery" = "Battery";
"Open Battery settings" = "Open battery settings";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Open bluetooth settings";
"Clock" = "Clock";
"Open Clock settings" = "Open clock settings";
// Words
"Unknown" = "Unknown";
"Version" = "Version";
"Processor" = "Processor";
"Memory" = "Memory";
"Graphics" = "Graphics";
"Close" = "Close";
"Download" = "Download";
"Install" = "Install";
"Cancel" = "Cancel";
"Unavailable" = "Unavailable";
"Yes" = "Yes";
"No" = "No";
"Automatic" = "Automatic";
"Manual" = "Manual";
"None" = "None";
"Dots" = "Dots";
"Arrows" = "Arrows";
"Characters" = "Character";
"Short" = "Short";
"Long" = "Long";
"Statistics" = "Statistics";
"Max" = "Max";
"Min" = "Min";
"Reset" = "Reset";
"Alignment" = "Alignment";
"Left alignment" = "Left";
"Center alignment" = "Center";
"Right alignment" = "Right";
"Dashboard" = "Dashboard";
"Enabled" = "Enabled";
"Disabled" = "Disabled";
"Silent" = "Silent";
"Units" = "Units";
"Fans" = "Fans";
"Scaling" = "Scaling";
"Linear" = "Linear";
"Square" = "Square";
"Cube" = "Cube";
"Logarithmic" = "Logarithmic";
"Fixed scale" = "Fixed";
"Cores" = "Cores";
"Settings" = "Settings";
"Name" = "Name";
"Format" = "Format";
"Turn off" = "Turn off";
"Normal" = "Normal";
"Warning" = "Warning";
"Critical" = "Critical";
"Usage" = "Usage";
"2 minutes" = "2 minutes";
"3 minutes" = "3 minutes";
"10 minutes" = "10 minutes";
"Import" = "Import";
"Export" = "Export";
"Separator" = "Separator";
"Read" = "Read";
"Write" = "Write";
"Frequency" = "Frequency";
"Save" = "Save";
"Run" = "Run";
"Stop" = "Stop";
"Uninstall" = "Uninstall";
"1 sec" = "1 sec";
"2 sec" = "2 sec";
"3 sec" = "3 sec";
"5 sec" = "5 sec";
"10 sec" = "10 sec";
"15 sec" = "15 sec";
"30 sec" = "30 sec";
"60 sec" = "60 sec";
// Setup
"Stats Setup" = "Stats Setup";
"Previous" = "Previous";
"Previous page" = "Previous page";
"Next" = "Next";
"Next page" = "Next page";
"Finish" = "Finish";
"Finish setup" = "Finish setup";
"Welcome to Stats" = "Welcome to Stats";
"welcome_message" = "Thanks for using Stats, a free open source macOS system monitor for your menu bar.";
"Start the application automatically when starting your Mac" = "Start the application automatically when starting your Mac";
"Do not start the application automatically when starting your Mac" = "Do not start the application automatically when starting your Mac";
"Do everything silently in the background (recommended)" = "Do everything silently in the background (recommended)";
"Check for a new version on startup" = "Check for a new version on startup";
"Check for a new version every day (once a day)" = "Check for a new version every day (once a day)";
"Check for a new version every week (once a week)" = "Check for a new version every week (once a week)";
"Check for a new version every month (once a month)" = "Check for a new version every month (once a month)";
"Never check for updates (not recommended)" = "Never check for updates (not recommended)";
"Anonymous telemetry for better development decisions" = "Anonymous telemetry for better development decisions";
"Share anonymous telemetry data" = "Share anonymous telemetry data";
"Do not share anonymous telemetry data" = "Do not share anonymous telemetry data";
"The configuration is completed" = "The configuration is completed";
"finish_setup_message" = "Everything is set up! \n The Stats is an open source tool, it's free and always will be. \n If you enjoy it you can support a project, it's always appreciated!";
// Alerts
"New version available" = "New version available";
"Click to install the new version of Stats" = "Click to install the new version of Stats";
"Successfully updated" = "Successfully updated";
"Stats was updated to v" = "Stats was updated to v%0";
"Reset settings text" = "All application settings will be reset and the application will be restarted. Are you sure you want to do this?";
"Support text" = "Thank you for using Stats!\n\n Maintaining and improving this open-source project takes time and resources. Your support helps us continue to provide a free and reliable application for everyone.\n\nIf you find Stats helpful, please consider making a contribution. Every little bit helps!";
// Settings
"Open Activity Monitor" = "Open Activity Monitor";
"Report a bug" = "Report a bug";
"Support the application" = "Support the application";
"Close application" = "Close application";
"Open application settings" = "Open application settings";
"Open dashboard" = "Open dashboard";
"No notifications available in this module" = "No notifications available in this module";
"Open Calendar" = "Open Calendar";
"Toggle the module" = "Toggle the module";
// Application settings
"Update application" = "Update application";
"Check for updates" = "Check for updates";
"At start" = "At start";
"Once per day" = "Once per day";
"Once per week" = "Once per week";
"Once per month" = "Once per month";
"Never" = "Never";
"Check for update" = "Check for update";
"Show icon in dock" = "Show icon in dock";
"Start at login" = "Start at login";
"Build number" = "Build number";
"Import settings" = "Import settings";
"Export settings" = "Export settings";
"Reset settings" = "Reset settings";
"Pause the Stats" = "Pause the Stats";
"Resume the Stats" = "Resume the Stats";
"Combined modules" = "Combined modules";
"Combined details" = "Combined details";
"Spacing" = "Spacing";
"Share anonymous telemetry" = "Share anonymous telemetry";
"Choose file" = "Choose file";
"Stress tests" = "Stress tests";
// Dashboard
"Serial number" = "Serial number";
"Model identifier" = "Model identifier";
"Production year" = "Production year";
"Uptime" = "Uptime";
"Number of cores" = "%0 cores";
"Number of threads" = "%0 threads";
"Number of e-cores" = "%0 efficiency cores";
"Number of p-cores" = "%0 performance cores";
"Disks" = "Disks";
"Display" = "Display";
// Update
"The latest version of Stats installed" = "The latest version of Stats is installed";
"Downloading..." = "Downloading...";
"Current version: " = "Current version: ";
"Latest version: " = "Latest version: ";
// Widgets
"Color" = "Color";
"Label" = "Label";
"Box" = "Box";
"Frame" = "Frame";
"Value" = "Value";
"Colorize" = "Colorize";
"Colorize value" = "Colorize value";
"Additional information" = "Additional information";
"Reverse values order" = "Reverse values order";
"Base" = "Base";
"Display mode" = "View mode";
"One row" = "One row";
"Two rows" = "Two rows";
"Mini widget" = "Mini";
"Line chart widget" = "Line chart";
"Bar chart widget" = "Bar chart";
"Pie chart widget" = "Pie chart";
"Network chart widget" = "Network chart";
"Speed widget" = "Speed";
"Battery widget" = "Battery";
"Stack widget" = "Stack";
"Memory widget" = "Memory";
"Static width" = "Static width";
"Tachometer widget" = "Tachometer";
"State widget" = "State widget";
"Text widget" = "Text widget";
"Battery details widget" = "Battery details widget";
"Show symbols" = "Show symbols";
"Label widget" = "Label";
"Number of reads in the chart" = "Number of reads in the chart";
"Color of download" = "Color of download";
"Color of upload" = "Color of upload";
"Monospaced font" = "Monospaced font";
"Reverse order" = "Reverse order";
"Chart history" = "Chart history";
"Default color" = "Default";
"Transparent when no activity" = "Transparent when no activity";
"Constant color" = "Constant";
// Module Kit
"Open module settings" = "Open module settings";
"Select widget" = "Select %0 widget";
"Open widget settings" = "Open widget settings";
"Update interval" = "Update interval";
"Usage history" = "Usage history";
"Details" = "Details";
"Top processes" = "Top processes";
"Pictogram" = "Pictogram";
"Module" = "Module";
"Widgets" = "Widgets";
"Popup" = "Popup";
"Notifications" = "Notifications";
"Merge widgets" = "Merge widgets";
"No available widgets to configure" = "No available widgets to configure";
"No options to configure for the popup in this module" = "No options to configure for the popup in this module";
"Process" = "Process";
"Kill process" = "Kill process";
"Keyboard shortcut" = "Keyboard shortcut";
"Listening..." = "Listening...";
// Modules
"Number of top processes" = "Number of top processes";
"Update interval for top processes" = "Update interval for top processes";
"Notification level" = "Notification level";
"Chart color" = "Chart color";
"Main chart scaling" = "Main chart scaling";
"Scale value" = "Scale value";
"Text widget value" = "Text widget value";
// CPU
"CPU usage" = "CPU usage";
"CPU temperature" = "CPU temperature";
"CPU frequency" = "CPU frequency";
"System" = "System";
"User" = "User";
"Idle" = "Idle";
"Show usage per core" = "Show usage per core";
"Show hyper-threading cores" = "Show hyper-threading cores";
"Split the value (System/User)" = "Split the value (System/User)";
"Scheduler limit" = "Scheduler limit";
"Speed limit" = "Speed limit";
"Average load" = "Average load";
"1 minute" = "1 minute";
"5 minutes" = "5 minutes";
"15 minutes" = "15 minutes";
"CPU usage threshold" = "CPU usage threshold";
"CPU usage is" = "CPU usage is %0";
"Efficiency cores" = "Efficiency cores";
"Performance cores" = "Performance cores";
"System color" = "System color";
"User color" = "User color";
"Idle color" = "Idle color";
"Cluster grouping" = "Cluster grouping";
"Efficiency cores color" = "Efficiency cores color";
"Performance cores color" = "Performance cores color";
"Total load" = "Total load";
"System load" = "System load";
"User load" = "User load";
"Efficiency cores load" = "Efficiency cores load";
"Performance cores load" = "Performance cores load";
"All cores" = "All cores";
// GPU
"GPU to show" = "GPU to show";
"Show GPU type" = "Show GPU type";
"GPU enabled" = "GPU enabled";
"GPU disabled" = "GPU disabled";
"GPU temperature" = "GPU temperature";
"GPU utilization" = "GPU utilization";
"Vendor" = "Vendor";
"Model" = "Model";
"Status" = "Status";
"Active" = "Active";
"Non active" = "Non active";
"Fan speed" = "Fan speed";
"Core clock" = "Core clock";
"Memory clock" = "Memory clock";
"Utilization" = "Utilization";
"Render utilization" = "Render utilization";
"Tiler utilization" = "Tiler utilization";
"GPU usage threshold" = "GPU usage threshold";
"GPU usage is" = "GPU usage is %0";
// RAM
"Memory usage" = "Memory usage";
"Memory pressure" = "Memory pressure";
"Total" = "Total";
"Used" = "Used";
"App" = "App";
"Wired" = "Wired";
"Compressed" = "Compressed";
"Free" = "Free";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Split the value (App/Wired/Compressed)";
"RAM utilization threshold" = "RAM utilization threshold";
"RAM utilization is" = "RAM utilization is %0";
"App color" = "App color";
"Wired color" = "Wired color";
"Compressed color" = "Compressed color";
"Free color" = "Free color";
"Free memory (less than)" = "Free memory (less than)";
"Swap size" = "Swap size";
"Free RAM is" = "Free RAM is %0";
// Disk
"Show removable disks" = "Show removable disks";
"Used disk memory" = "%0 of %1 used";
"Free disk memory" = "%0 of %1 free";
"Disk to show" = "Disk to show";
"Open disk" = "Open disk";
"Switch view" = "Switch view";
"Disk utilization threshold" = "Disk utilization threshold";
"Disk utilization is" = "Disk utilization is %0";
"Read color" = "Read color";
"Write color" = "Write color";
"Disk usage" = "Disk usage";
"Total read" = "Total read";
"Total written" = "Total written";
"Write speed" = "Write";
"Read speed" = "Read";
"Drives" = "Drives";
"SMART data" = "SMART data";
// Sensors
"Temperature unit" = "Temperature unit";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Save the fan speed";
"Fan" = "Fan";
"HID sensors" = "HID sensors";
"Synchronize fan's control" = "Synchronize fan's control";
"Current" = "Current";
"Energy" = "Energy";
"Show unknown sensors" = "Show unknown sensors";
"Install fan helper" = "Install fan helper";
"Uninstall fan helper" = "Uninstall fan helper";
"Fan value" = "Fan value";
"Turn off fan" = "Turn off fan";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?";
"Sensor threshold" = "Sensor threshold";
"Left fan" = "Left";
"Right fan" = "Right";
"Fastest fan" = "Fastest";
"Sensor to show" = "Sensor to show";
// Network
"Uploading" = "Upload";
"Downloading" = "Download";
"Public IP" = "Public IP";
"Local IP" = "Local IP";
"Interface" = "Interface";
"Physical address" = "Physical address";
"Refresh" = "Refresh";
"Click to copy public IP address" = "Click to copy public IP address";
"Click to copy local IP address" = "Click to copy local IP address";
"Click to copy wifi name" = "Click to copy wifi name";
"Click to copy mac address" = "Click to copy mac address";
"No connection" = "No connection";
"Network interface" = "Network interface";
"Total download" = "Total download";
"Total upload" = "Total upload";
"Reader type" = "Reader type";
"Interface based" = "Interface based";
"Processes based" = "Process based";
"Reset data usage" = "Reset data usage";
"VPN mode" = "VPN mode";
"Standard" = "Standard";
"Security" = "Security";
"Channel" = "Channel";
"Common scale" = "Common scale";
"Autodetection" = "Autodetection";
"Widget activation threshold" = "Widget activation threshold";
"Internet connection" = "Internet connection";
"Active state color" = "Active state color";
"Nonactive state color" = "Nonactive state color";
"Connectivity host (ICMP)" = "Connectivity host (ICMP)";
"Leave empty to disable the check" = "Leave empty to disable the check";
"Connectivity history" = "Connectivity history";
"Auto-refresh public IP address" = "Auto-refresh public IP address";
"Every hour" = "Every hour";
"Every 12 hours" = "Every 12 hours";
"Every 24 hours" = "Every 24 hours";
"Network activity" = "Network activity";
"Last reset" = "Last reset %0 ago";
"Latency" = "Latency";
"Upload speed" = "Upload";
"Download speed" = "Download";
"Address" = "Address";
"WiFi network" = "WiFi network";
"Local IP changed" = "Local IP has changed";
"Public IP changed" = "Public IP has changed";
"Previous IP" = "Previous IP: %0";
"New IP" = "New IP: %0";
"Internet connection lost" = "Internet connection lost";
"Internet connection established" = "Internet connection established";
// Battery
"Level" = "Level";
"Source" = "Source";
"AC Power" = "AC Power";
"Battery Power" = "Battery Power";
"Time" = "Time";
"Health" = "Health";
"Amperage" = "Amperage";
"Voltage" = "Voltage";
"Cycles" = "Cycles";
"Temperature" = "Temperature";
"Power adapter" = "Power adapter";
"Power" = "Power";
"Is charging" = "Is charging";
"Time to discharge" = "Time to discharge";
"Time to charge" = "Time to charge";
"Calculating" = "Calculating";
"Fully charged" = "Fully charged";
"Not connected" = "Not connected";
"Low level notification" = "Low level notification";
"High level notification" = "High level notification";
"Low battery" = "Low battery";
"High battery" = "High battery";
"Battery remaining" = "%0% remaining";
"Battery remaining to full charge" = "%0% to full charge";
"Percentage" = "Percentage";
"Percentage and time" = "Percentage and time";
"Time and percentage" = "Time and percentage";
"Time format" = "Time format";
"Hide additional information when full" = "Hide additional information when full";
"Last charge" = "Last charge";
"Capacity" = "Capacity";
"current / maximum / designed" = "current / maximum / designed";
"Low power mode" = "Low power mode";
"Percentage inside the icon" = "Percentage inside the icon";
"Colorize battery" = "Colorize battery";
"Charging current" = "Charging current";
"Charging Voltage" = "Charging voltage";
"Charger state inside the battery" = "Charger state inside the battery";
// Bluetooth
"Battery to show" = "Battery to show";
"No Bluetooth devices are available" = "No Bluetooth devices are available";
// Clock
"Time zone" = "Time zone";
"Local" = "Local";
"Calendar" = "Calendar";
"Show week numbers" = "Show week numbers";
"Local time" = "Local time";
"Add new clock" = "Add new clock";
"Delete selected clock" = "Delete selected clock";
"Help with datetime format" = "Help with datetime format";
// Colors
"Based on utilization" = "Based on utilization";
"Based on pressure" = "Based on pressure";
"Based on cluster" = "Based on cluster";
"System accent" = "System accent";
"Monochrome accent" = "Monochrome accent";
"Clear" = "Clear";
"White" = "White";
"Black" = "Black";
"Gray" = "Gray";
"Second gray" = "Second gray";
"Dark gray" = "Dark gray";
"Light gray" = "Light gray";
"Red" = "Red";
"Second red" = "Second red";
"Green" = "Green";
"Second green" = "Second green";
"Blue" = "Blue";
"Second blue" = "Second blue";
"Yellow" = "Yellow";
"Second yellow" = "Second yellow";
"Orange" = "Orange";
"Second orange" = "Second orange";
"Purple" = "Purple";
"Second purple" = "Second purple";
"Brown" = "Brown";
"Second brown" = "Second brown";
"Cyan" = "Cyan";
"Magenta" = "Magenta";
"Pink" = "Pink";
"Teal" = "Teal";
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/es.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by lucasaf04 on 03/12/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "Abrir ajustes de CPU";
"GPU" = "GPU";
"Open GPU settings" = "Abrir ajustes de GPU";
"RAM" = "RAM";
"Open RAM settings" = "Abrir ajustes de RAM";
"Disk" = "Disco";
"Open Disk settings" = "Abrir ajustes de disco";
"Sensors" = "Sensores";
"Open Sensors settings" = "Abrir ajustes de sensores";
"Network" = "Red";
"Open Network settings" = "Abrir ajustes de red";
"Battery" = "Batería";
"Open Battery settings" = "Abrir ajustes de batería";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Abrir ajustes de Bluetooth";
"Clock" = "Reloj";
"Open Clock settings" = "Abrir ajustes de reloj";
// Words
"Unknown" = "Desconocido";
"Version" = "Versión";
"Processor" = "Procesador";
"Memory" = "Memoria";
"Graphics" = "Gráficos";
"Close" = "Cerrar";
"Download" = "Descargar";
"Install" = "Instalar";
"Cancel" = "Cancelar";
"Unavailable" = "No disponible";
"Yes" = "Sí";
"No" = "No";
"Automatic" = "Automático";
"Manual" = "Manual";
"None" = "Ninguno";
"Dots" = "Puntos";
"Arrows" = "Flechas";
"Characters" = "Letras";
"Short" = "Corto";
"Long" = "Largo";
"Statistics" = "Estadísticas";
"Max" = "Máx";
"Min" = "Mín";
"Reset" = "Reiniciar";
"Alignment" = "Alineación";
"Left alignment" = "Izquierda";
"Center alignment" = "Centro";
"Right alignment" = "Derecha";
"Dashboard" = "Panel";
"Enabled" = "Activado";
"Disabled" = "Desactivado";
"Silent" = "Silencioso";
"Units" = "Unidades";
"Fans" = "Ventiladores";
"Scaling" = "Escala";
"Linear" = "Lineal"; // translategemma:4b
"Square" = "Cuadrática";
"Cube" = "Cúbica";
"Logarithmic" = "Logarítmica";
"Fixed scale" = "Escala fija";
"Cores" = "Núcleos";
"Settings" = "Ajustes";
"Name" = "Nombre";
"Format" = "Formato";
"Turn off" = "Apagar";
"Normal" = "Normal";
"Warning" = "Aviso";
"Critical" = "Crítico";
"Usage" = "Uso";
"2 minutes" = "2 minutos";
"3 minutes" = "3 minutos";
"10 minutes" = "10 minutos";
"Import" = "Importar";
"Export" = "Exportar";
"Separator" = "Separador";
"Read" = "Lectura";
"Write" = "Escritura";
"Frequency" = "Frecuencia";
"Save" = "Guardar"; // translategemma:4b
"Run" = "Ejecutar"; // translategemma:4b
"Stop" = "Detener"; // translategemma:4b
"Uninstall" = "Desinstalar"; // translategemma:4b
"1 sec" = "1 segundo"; // translategemma:4b
"2 sec" = "2 segundos"; // translategemma:4b
"3 sec" = "3 segundos"; // translategemma:4b
"5 sec" = "5 segundos"; // translategemma:4b
"10 sec" = "10 segundos"; // translategemma:4b
"15 sec" = "15 segundos"; // translategemma:4b
"30 sec" = "30 segundos"; // translategemma:4b
"60 sec" = "60 segundos"; // translategemma:4b
// Setup
"Stats Setup" = "Configuración de Stats";
"Previous" = "Anterior";
"Previous page" = "Página anterior";
"Next" = "Siguiente";
"Next page" = "Página siguiente";
"Finish" = "Finalizar";
"Finish setup" = "Configuración finalizada";
"Welcome to Stats" = "Bienvenido a Stats";
"welcome_message" = "Gracias por usar Stats, un monitor de sistema para su barra de menú gratuito de código libre para macOS.";
"Start the application automatically when starting your Mac" = "Iniciar la aplicación automáticamente al arrancar el Mac";
"Do not start the application automatically when starting your Mac" = "No iniciar la aplicación automáticamente al arrancar tu Mac";
"Do everything silently in the background (recommended)" = "Hacer todo silenciosamente en segundo plano (recomendado)";
"Check for a new version on startup" = "Comprobar si hay una versión nueva al iniciar la aplicación";
"Check for a new version every day (once a day)" = "Comprobar si hay una versión nueva todos los días (una vez al día)";
"Check for a new version every week (once a week)" = "Comprobar si hay una versión nueva todas las semanas (una vez a la semana)";
"Check for a new version every month (once a month)" = "Comprobar si hay una versión nueva todos los meses (una vez al mes)";
"Never check for updates (not recommended)" = "Nunca comprobar si hay una versión nueva (no es recomendado)";
"Anonymous telemetry for better development decisions" = "Telemetría anónima para mejorar las decisiones de desarrollo";
"Share anonymous telemetry data" = "Compartir datos anónimos de telemetría";
"Do not share anonymous telemetry data" = "No compartir datos anónimos de telemetría";
"The configuration is completed" = "La configuración ha sido completada";
"finish_setup_message" = "¡Todo ha sido configurado! \n Stats es una herramienta de código abierto, es gratis y siempre lo será. \n Si te gusta puedes apoyar el proyecto, ¡siempre es apreciado!";
// Alerts
"New version available" = "Nueva versión disponible";
"Click to install the new version of Stats" = "Haga clic para instalar la nueva versión de Stats";
"Successfully updated" = "Actualización completada con éxito";
"Stats was updated to v" = "Stats se ha actualizado a v%0";
"Reset settings text" = "Todos los ajustes de la aplicación serán restablecidos y la aplicación se reiniciará. ¿Seguro que quieres hacer esto?";
"Support text" = "Gracias por usar Stats. Mantener y mejorar este proyecto de código abierto requiere tiempo y recursos. Tu apoyo nos ayuda a seguir proporcionando una aplicación gratuita y fiable para todo el mundo.\nSi Stats te resulta útil, por favor, considera hacer una contribución. Todo ayuda.";
// Settings
"Open Activity Monitor" = "Abrir Monitor de Actividad";
"Report a bug" = "Reportar un error";
"Support the application" = "Apoya la aplicación";
"Close application" = "Cerrar la aplicación";
"Open application settings" = "Abrir la configuración de la aplicación";
"Open dashboard" = "Abrir panel";
"No notifications available in this module" = "No hay notificaciones disponibles para este módulo";
"Open Calendar" = "Abrir calendario"; // translategemma:4b
"Toggle the module" = "Activar/desactivar el módulo"; // translategemma:4b
// Application settings
"Update application" = "Actualizar la aplicación";
"Check for updates" = "Buscar actualizaciones";
"At start" = "Al iniciar el sistema";
"Once per day" = "Una vez al día";
"Once per week" = "Una vez por semana";
"Once per month" = "Una vez al mes";
"Never" = "Nunca";
"Check for update" = "Buscar actualizaciones";
"Show icon in dock" = "Muestra el icono en el dock";
"Start at login" = "Arrancar al iniciar sesión";
"Build number" = "Número de compilación";
"Import settings" = "Importar ajustes";
"Export settings" = "Exportar ajustes";
"Reset settings" = "Restablecer ajustes";
"Pause the Stats" = "Pausar Stats";
"Resume the Stats" = "Reanudar Stats";
"Combined modules" = "Combinar módulos";
"Combined details" = "Detalles combinados"; // translategemma:4b
"Spacing" = "Espaciado";
"Share anonymous telemetry" = "Compartir telemetría anónima";
"Choose file" = "Seleccionar archivo"; // translategemma:4b
"Stress tests" = "Pruebas de estrés"; // translategemma:4b
// Dashboard
"Serial number" = "Número de serie";
"Model identifier" = "Identificación del modelo";
"Production year" = "Año de fabricación";
"Uptime" = "Tiempo de actividad";
"Number of cores" = "Número núcleos";
"Number of threads" = "Número de hilos";
"Number of e-cores" = "Número de núcleos de eficiencia";
"Number of p-cores" = "Número de núcleos de rendimiento";
"Disks" = "Discos"; // translategemma:4b
"Display" = "Pantalla"; // translategemma:4b
// Update
"The latest version of Stats installed" = "La última versión de Stats está instalada";
"Downloading..." = "Descargando...";
"Current version: " = "Versión actual: ";
"Latest version: " = "Última versión: ";
// Widgets
"Color" = "Color";
"Label" = "Etiqueta";
"Box" = "Caja";
"Frame" = "Borde";
"Value" = "Valor";
"Colorize" = "Colorear";
"Colorize value" = "Colorear valor";
"Additional information" = "Información adicional";
"Reverse values order" = "Invertir el order de los valores";
"Base" = "Base";
"Display mode" = "Modo de visualización";
"One row" = "Una fila";
"Two rows" = "Dos filas";
"Mini widget" = "Mini";
"Line chart widget" = "Gráfico de líneas";
"Bar chart widget" = "Gráfico de barras";
"Pie chart widget" = "Gráfico circular";
"Network chart widget" = "Gráfico de red";
"Speed widget" = "Velocidad";
"Battery widget" = "Batería";
"Stack widget" = "Apilado";
"Memory widget" = "Memoria";
"Static width" = "Ancho fijo";
"Tachometer widget" = "Tacómetro";
"State widget" = "Estado";
"Text widget" = "Control de texto"; // translategemma:4b
"Battery details widget" = "Widget de detalles de la batería"; // translategemma:4b
"Show symbols" = "Mostrar símbolos";
"Label widget" = "Etiqueta";
"Number of reads in the chart" = "Número de lecturas en la gráfica";
"Color of download" = "Color de descarga";
"Color of upload" = "Color de subida";
"Monospaced font" = "Fuente monoespaciada";
"Reverse order" = "Orden inverso";
"Chart history" = "Historial del gráfico";
"Default color" = "Color por defecto";
"Transparent when no activity" = "Transparente cuando no hay actividad";
"Constant color" = "Color constante";
// Module Kit
"Open module settings" = "Abrir la configuración del módulo";
"Select widget" = "Seleccionar widget";
"Open widget settings" = "Abrir ajustes del widget";
"Update interval" = "Intervalo de actualización";
"Usage history" = "Historial de uso";
"Details" = "Detalles";
"Top processes" = "Procesos principales";
"Pictogram" = "Pictograma";
"Module" = "Módulo";
"Widgets" = "Widgets";
"Popup" = "Ventana emergente";
"Notifications" = "Notificaciones";
"Merge widgets" = "Unir widgets";
"No available widgets to configure" = "No hay widgets disponibles para configurar";
"No options to configure for the popup in this module" = "No hay opciones que configurar para la ventana emergente de este módulo";
"Process" = "Proceso";
"Kill process" = "Cierra el proceso";
"Keyboard shortcut" = "Atajo de teclado"; // translategemma:4b
"Listening..." = "Escuchando..."; // translategemma:4b
// Modules
"Number of top processes" = "Número de procesos principales";
"Update interval for top processes" = "Intervalo de actualización para los procesos principales";
"Notification level" = "Nivel de notificación";
"Chart color" = "Color de la gráfica";
"Main chart scaling" = "Escalado del gráfico principal";
"Scale value" = "Valor de la escala";
"Text widget value" = "Valor del widget de texto"; // translategemma:4b
// CPU
"CPU usage" = "Uso de la CPU";
"CPU temperature" = "Temperatura de la CPU";
"CPU frequency" = "Frecuencia de la CPU";
"System" = "Sistema";
"User" = "Usuario";
"Idle" = "Inactivo";
"Show usage per core" = "Mostrar uso por núcleo";
"Show hyper-threading cores" = "Mostrar los núcleos hyper-threading";
"Split the value (System/User)" = "Separar el valor (Sistema/Usuario)";
"Scheduler limit" = "Límite del planificador";
"Speed limit" = "Límite de velocidad";
"Average load" = "Carga promedio";
"1 minute" = "1 minuto";
"5 minutes" = "5 minutos";
"15 minutes" = "15 minutos";
"CPU usage threshold" = "Umbral de uso de la CPU";
"CPU usage is" = "El uso de la CPU es";
"Efficiency cores" = "Núcleos de eficiencia";
"Performance cores" = "Núcleos de rendimiento";
"System color" = "Color del sistema";
"User color" = "Color del usuario";
"Idle color" = "Color inactivo";
"Cluster grouping" = "Agrupación de clústeres";
"Efficiency cores color" = "Color de núcleos de eficiencia";
"Performance cores color" = "Color de núcleos de rendimiento";
"Total load" = "Carga total";
"System load" = "Carga de sistema";
"User load" = "Carga de usuario";
"Efficiency cores load" = "Carga de los núcleos de eficiencia";
"Performance cores load" = "Carga de los núcleos de rendimiento";
"All cores" = "Todos los núcleos";
// GPU
"GPU to show" = "GPU a mostrar";
"Show GPU type" = "Mostrar tipo de GPU";
"GPU enabled" = "GPU activada";
"GPU disabled" = "GPU desactivada";
"GPU temperature" = "Temperatura de la GPU";
"GPU utilization" = "Utilización de GPU";
"Vendor" = "Fabricante";
"Model" = "Modelo";
"Status" = "Estado";
"Active" = "Activo";
"Non active" = "Inactivo";
"Fan speed" = "Velocidad del ventilador";
"Core clock" = "Reloj del núcleo";
"Memory clock" = "Reloj de la memoria";
"Utilization" = "Utilización";
"Render utilization" = "Utilización de render";
"Tiler utilization" = "Utilización de tiler";
"GPU usage threshold" = "Umbral de uso de la GPU";
"GPU usage is" = "El uso de GPU es";
// RAM
"Memory usage" = "Uso de la memoria";
"Memory pressure" = "Presión de la memoria";
"Total" = "Total";
"Used" = "Usado";
"App" = "Aplicación"; // translategemma:4b
"Wired" = "Física";
"Compressed" = "Comprimida";
"Free" = "Libre";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Separar el valor (App/Física/Comprimida)";
"RAM utilization threshold" = "Umbral de utilización de RAM";
"RAM utilization is" = "La utilización de RAM es %0";
"App color" = "Color de app";
"Wired color" = "Color física";
"Compressed color" = "Color comprimida";
"Free color" = "Color libre";
"Free memory (less than)" = "Memoria libre (menos que)";
"Swap size" = "Tamaño de swap";
"Free RAM is" = "La RAM libre es %0";
// Disk
"Show removable disks" = "Mostrar los discos extraíbles";
"Used disk memory" = "Memoria de disco utilizada";
"Free disk memory" = "Memoria de disco libre";
"Disk to show" = "Disco a mostrar";
"Open disk" = "Abrir disco";
"Switch view" = "Cambiar vista";
"Disk utilization threshold" = "Umbral de utilización de disco";
"Disk utilization is" = "La utilización de disco es";
"Read color" = "Color de lectura";
"Write color" = "Color de escritura";
"Disk usage" = "Uso del disco";
"Total read" = "Total leído";
"Total written" = "Total escrito";
"Write speed" = "Velocidad de escritura";
"Read speed" = "Velocidad de lectura";
"Drives" = "Unidades";
"SMART data" = "Datos SMART"; // translategemma:4b
// Sensors
"Temperature unit" = "Unidad de temperatura";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Guardar la velocidad del ventilador";
"Fan" = "Ventilador";
"HID sensors" = "Sensores HID";
"Synchronize fan's control" = "Sincronizar control del ventilador";
"Current" = "Corriente";
"Energy" = "Energía";
"Show unknown sensors" = "Mostrar sensores desconocidos";
"Install fan helper" = "Instalar fan helper";
"Uninstall fan helper" = "Desinstalar fan helper";
"Fan value" = "Valor de ventilador";
"Turn off fan" = "Apagar ventilador";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Vas a apagar el ventilador. Esto puede dañar tu Mac y no se recomienda. ¿Seguro que quieres hacerlo?";
"Sensor threshold" = "Umbral del sensor";
"Left fan" = "Ventilador izquierdo";
"Right fan" = "Ventilador derecho";
"Fastest fan" = "Ventilador más veloz";
"Sensor to show" = "Sensor a mostrar";
// Network
"Uploading" = "Subida";
"Downloading" = "Descarga";
"Public IP" = "IP Pública";
"Local IP" = "IP Local";
"Interface" = "Interfaz";
"Physical address" = "Dirección física";
"Refresh" = "Actualizar";
"Click to copy public IP address" = "Haz clic para copiar la IP pública";
"Click to copy local IP address" = "Haz clic para copiar la IP local";
"Click to copy wifi name" = "Haz clic para copiar el nombre de la red Wi-Fi";
"Click to copy mac address" = "Haz clic para copiar la dirección MAC";
"No connection" = "Sin conexión";
"Network interface" = "Interfaz de red";
"Total download" = "Descarga total";
"Total upload" = "Subida total";
"Reader type" = "Tipo de lector";
"Interface based" = "Basado en interfaz";
"Processes based" = "Basado en procesos";
"Reset data usage" = "Restablecer el uso de datos";
"VPN mode" = "Modo VPN";
"Standard" = "Estándar";
"Security" = "Seguridad";
"Channel" = "Canal";
"Common scale" = "Escala común";
"Autodetection" = "Autodetección";
"Widget activation threshold" = "Umbral de activación del widget";
"Internet connection" = "Conexión a internet";
"Active state color" = "Color de estado activo";
"Nonactive state color" = "Color de estado inactivo";
"Connectivity host (ICMP)" = "Host de conectividad (ICMP)";
"Leave empty to disable the check" = "Dejar vacío para desactivar la verificación";
"Connectivity history" = "Historial de conectividad";
"Auto-refresh public IP address" = "Auto-actualizar IP pública";
"Every hour" = "Cada hora";
"Every 12 hours" = "Cada 12 horas";
"Every 24 hours" = "Cada 24 horas";
"Network activity" = "Actividad de red";
"Last reset" = "Último reinicio";
"Latency" = "Latencia";
"Upload speed" = "Velocidad de carga";
"Download speed" = "Velocidad de descarga";
"Address" = "Dirección"; // translategemma:4b
"WiFi network" = "Red Wi-Fi"; // translategemma:4b
"Local IP changed" = "La dirección IP local ha cambiado."; // translategemma:4b
"Public IP changed" = "La dirección IP pública ha cambiado."; // translategemma:4b
"Previous IP" = "Dirección IP anterior: %0"; // translategemma:4b
"New IP" = "Nueva dirección IP: %0"; // translategemma:4b
"Internet connection lost" = "Se ha perdido la conexión a Internet."; // translategemma:4b
"Internet connection established" = "Conexión a Internet establecida"; // translategemma:4b
// Battery
"Level" = "Nivel";
"Source" = "Fuente";
"AC Power" = "Adaptador de corriente";
"Battery Power" = "Energía de la batería";
"Time" = "Tiempo";
"Health" = "Salud";
"Amperage" = "Amperaje";
"Voltage" = "Voltaje";
"Cycles" = "Número de ciclos";
"Temperature" = "Temperatura";
"Power adapter" = "Adaptador de corriente";
"Power" = "Potencia";
"Is charging" = "Está cargando";
"Time to discharge" = "Agotado en";
"Time to charge" = "Cargado en";
"Calculating" = "Calculando";
"Fully charged" = "Completamente cargado";
"Not connected" = "No conectado";
"Low level notification" = "Notificación de nivel bajo";
"High level notification" = "Notificación de nivel alto";
"Low battery" = "Batería baja";
"High battery" = "Batería alta";
"Battery remaining" = "Batería restante";
"Battery remaining to full charge" = "Batería restante hasta carga completa";
"Percentage" = "Porcentaje";
"Percentage and time" = "Porcentaje y tiempo";
"Time and percentage" = "Tiempo y porcentaje";
"Time format" = "Formato de tiempo";
"Hide additional information when full" = "Ocultar información adicional cuando la batería está cargada";
"Last charge" = "Última carga";
"Capacity" = "Capacidad";
"current / maximum / designed" = "actual / máxima / teórica";
"Low power mode" = "Modo de bajo consumo";
"Percentage inside the icon" = "Porcentaje dentro del icono";
"Colorize battery" = "Colorear batería";
"Charging current" = "Corriente de carga";
"Charging Voltage" = "Voltaje de carga";
"Charger state inside the battery" = "Estado del cargador dentro de la batería";
// Bluetooth
"Battery to show" = "Batería a mostrar";
"No Bluetooth devices are available" = "No hay dipositivos Bluetooth disponibles";
// Clock
"Time zone" = "Zona horaria";
"Local" = "Local";
"Calendar" = "Calendario";
"Show week numbers" = "Mostrar los números de la semana"; // translategemma:4b
"Local time" = "Hora local";
"Add new clock" = "Añadir un nuevo reloj"; // translategemma:4b
"Delete selected clock" = "Eliminar reloj seleccionado"; // translategemma:4b
"Help with datetime format" = "Ayuda con el formato de fecha y hora"; // translategemma:4b
// Colors
"Based on utilization" = "Basado en utilización";
"Based on pressure" = "Basado en presión";
"Based on cluster" = "Basado en clúster";
"System accent" = "Acento del sistema";
"Monochrome accent" = "Acento monocromo";
"Clear" = "Claro";
"White" = "Blanco";
"Black" = "Negro";
"Gray" = "Gris";
"Second gray" = "Segundo gris";
"Dark gray" = "Gris oscuro";
"Light gray" = "Gris claro";
"Red" = "Rojo";
"Second red" = "Segundo rojo";
"Green" = "Verde";
"Second green" = "Segundo verde";
"Blue" = "Azul";
"Second blue" = "Segundo azul";
"Yellow" = "Amarillo";
"Second yellow" = "Segundo amarillo";
"Orange" = "Naranja";
"Second orange" = "Segundo naranja";
"Purple" = "Morado";
"Second purple" = "Segundo morado";
"Brown" = "Marrón";
"Second brown" = "Segundo marrón";
"Cyan" = "Cian";
"Magenta" = "Magenta";
"Pink" = "Rosa";
"Teal" = "Verde azulado";
"Indigo" = "Índigo";
================================================
FILE: Stats/Supporting Files/et.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by postylem on 2023/07/26.
// Using Swift 5.0.
// Running on macOS 13.4.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "Процессор"; // translategemma:4b
"Open CPU settings" = "Ava CPU seaded";
"GPU" = "Graafilise protsessori kaart"; // translategemma:4b
"Open GPU settings" = "Ava GPU seaded";
"RAM" = "Muutmälu";
"Open RAM settings" = "Ava muutmälu seaded";
"Disk" = "Ketas";
"Open Disk settings" = "Ava ketta seaded";
"Sensors" = "Andurid";
"Open Sensors settings" = "Ava andurite seaded";
"Network" = "Võrk";
"Open Network settings" = "Ava võrguseaded";
"Battery" = "Patarei";
"Open Battery settings" = "Ava patarei seaded";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Ava Bluetoothi seaded";
"Clock" = "Kell";
"Open Clock settings" = "Ava kella seaded";
// Words
"Unknown" = "Teadmatu";
"Version" = "Versioon";
"Processor" = "Protsessor";
"Memory" = "Mälu";
"Graphics" = "Graafika";
"Close" = "Sulge";
"Download" = "Laadi alla";
"Install" = "Paigalda";
"Cancel" = "Tühista";
"Unavailable" = "Pole saadaval";
"Yes" = "Ja";
"No" = "Ei";
"Automatic" = "Automaatne";
"Manual" = "Manuaal";
"None" = "Mitte ühtegi";
"Dots" = "Täpid";
"Arrows" = "Nooled";
"Characters" = "Märgid";
"Short" = "Lühike";
"Long" = "Pikk";
"Statistics" = "Statistika";
"Max" = "Maksimum";
"Min" = "Miinimum";
"Reset" = "Taasta";
"Alignment" = "Joondumine";
"Left alignment" = "Vasakpoolne joondus";
"Center alignment" = "Keskjoondus";
"Right alignment" = "Parempoolne joondus";
"Dashboard" = "Armatuurlaud";
"Enabled" = "Lubatud";
"Disabled" = "Keelatud";
"Silent" = "Hääletu";
"Units" = "Ühikud";
"Fans" = "Ventilaatorid";
"Scaling" = "Skaleerimine";
"Linear" = "Lineaar";
"Square" = "Ruut";
"Cube" = "Kuup";
"Logarithmic" = "Logaritmiline";
"Fixed scale" = "Lahendatud"; // translategemma:4b
"Cores" = "Tuumad";
"Settings" = "Seaded";
"Name" = "Nimi";
"Format" = "Vorming";
"Turn off" = "Lülita välja"; // translategemma:4b
"Normal" = "Normaalne"; // translategemma:4b
"Warning" = "Hoiatus"; // translategemma:4b
"Critical" = "Kriitiline"; // translategemma:4b
"Usage" = "Kasutus"; // translategemma:4b
"2 minutes" = "2 minutit"; // translategemma:4b
"3 minutes" = "3 minutit"; // translategemma:4b
"10 minutes" = "10 minutit"; // translategemma:4b
"Import" = "Impordi"; // translategemma:4b
"Export" = "Экспорт"; // translategemma:4b
"Separator" = "Eemaldaja"; // translategemma:4b
"Read" = "Loo"; // translategemma:4b
"Write" = "Kirjuta"; // translategemma:4b
"Frequency" = "Sagedus"; // translategemma:4b
"Save" = "Salvesta"; // translategemma:4b
"Run" = "Jooks"; // translategemma:4b
"Stop" = "Peatumine"; // translategemma:4b
"Uninstall" = "Kasutusest kõrvaldamine"; // translategemma:4b
"1 sec" = "1 sekund"; // translategemma:4b
"2 sec" = "2 sekund"; // translategemma:4b
"3 sec" = "3 sekundit"; // translategemma:4b
"5 sec" = "5 sekund"; // translategemma:4b
"10 sec" = "10 sekundit"; // translategemma:4b
"15 sec" = "15 sekundit"; // translategemma:4b
"30 sec" = "30 sekundit"; // translategemma:4b
"60 sec" = "60 sekund"; // translategemma:4b
// Setup
"Stats Setup" = "Stats Seadistamine";
"Previous" = "Eelmine";
"Previous page" = "Eelmine leht";
"Next" = "Järgmine";
"Next page" = "Järgmine leht";
"Finish" = "Lõpeta";
"Finish setup" = "Lõpeta seadistamine";
"Welcome to Stats" = "Tere tulemast Statsi";
"welcome_message" = "Täname, et kasutate Statsi, tasuta avatud lähtekoodiga macOS-i süsteemimonitor menüüriba jaoks.";
"Start the application automatically when starting your Mac" = "Käivita rakendus automaatselt Maci käivitamisel";
"Do not start the application automatically when starting your Mac" = "Ära käivita rakendus automaatselt Maci käivitamisel";
"Do everything silently in the background (recommended)" = "Tee kõike vaikselt taustal (soovitatav)";
"Check for a new version on startup" = "Kontrollige käivitamisel uut versiooni";
"Check for a new version every day (once a day)" = "Kontrolli uut versiooni iga päev (üks kord päevas)";
"Check for a new version every week (once a week)" = "Kontrolli uut versiooni iga nädal (üks kord nädalas)";
"Check for a new version every month (once a month)" = "Kontrolli uut versiooni iga kuu (üks kord kuus)";
"Never check for updates (not recommended)" = "Ära kunagi kontrolli värskendusi (pole soovitatav)";
"Anonymous telemetry for better development decisions" = "Anonüümne telemeetria paremate arendusotsuste tegemiseks";
"Share anonymous telemetry data" = "Jaga anonüümseid telemeetriaandmeid";
"Do not share anonymous telemetry data" = "Ära jaga anonüümseid telemeetriaandmeid";
"The configuration is completed" = "Seadistamine on lõpetatud";
"finish_setup_message" = "Kõik on paika pandud! \n Stats on avatud lähtekoodiga tööriist, see on tasuta ja jääb alati tasuta. \n Kui teile meeldib, saate projekti toetada, see on alati teretulnud!";
// Alerts
"New version available" = "Uus versioon saadaval";
"Click to install the new version of Stats" = "Klõpsa Statsi uue versiooni installimiseks";
"Successfully updated" = "Edukalt värskendatud";
"Stats was updated to v" = "Stats värskendati versioonile %0";
"Reset settings text" = "Kõik rakenduse seaded lähtestatakse ja rakendus taaskäivitatakse. Kas olete kindel, et soovite seda teha?";
"Support text" = "Täname teid statistika kasutamise eest!\n\n Selle avatud lähtekoodiga projekti hooldamine ja täiustamine nõuab aega ja ressursse. Teie toetus aitab meil jätkuvalt pakkuda kõigile tasuta ja usaldusväärset rakendust.\n\nJa kui Stats on teie arvates kasulik, siis palun kaaluge panuse andmist. Iga väike summa aitab!";
// Settings
"Open Activity Monitor" = "Ava aktiivsusmonitor";
"Report a bug" = "Teata veast";
"Support the application" = "Toetage rakendust";
"Close application" = "Sule rakendus";
"Open application settings" = "Ava rakenduse seaded";
"Open dashboard" = "Ava armatuurlaud";
"No notifications available in this module" = "Selles moodulis pole saadaval teavitusi."; // translategemma:4b
"Open Calendar" = "Ava kalender"; // translategemma:4b
"Toggle the module" = "Sisselülita moodul"; // translategemma:4b
// Application settings
"Update application" = "Uuenda rakendust";
"Check for updates" = "Otsi värskendusi";
"At start" = "Alguses";
"Once per day" = "Üks kord päevas";
"Once per week" = "Kord nädalas";
"Once per month" = "Üks kord kuus";
"Never" = "Mitte kunagi";
"Check for update" = "Otsi värskendusi";
"Show icon in dock" = "Kuva ikooni dokis";
"Start at login" = "Alusta sisselogimisest";
"Build number" = "Ehitamise number";
"Import settings" = "Impordi seaded"; // translategemma:4b
"Export settings" = "Экспортi seaded"; // translategemma:4b
"Reset settings" = "Lähtesta seaded";
"Pause the Stats" = "Peata Statsi";
"Resume the Stats" = "Jätka Statsi";
"Combined modules" = "Kombineeritud moodulid";
"Combined details" = "Ühendatud andmed"; // translategemma:4b
"Spacing" = "Vahekaugus";
"Share anonymous telemetry" = "Jaga anonüümset telemeetriat";
"Choose file" = "Vali fail"; // translategemma:4b
"Stress tests" = "Stressitestid"; // translategemma:4b
// Dashboard
"Serial number" = "Seerianumber";
"Model identifier" = "Mudelitunnistaja"; // translategemma:4b
"Production year" = "Tootmise aasta"; // translategemma:4b
"Uptime" = "Tööaeg";
"Number of cores" = "%0 tuumad";
"Number of threads" = "%0 lõimed";
"Number of e-cores" = "%0 tõhusatuumad";
"Number of p-cores" = "%0 jõudlustuumad";
"Disks" = "Plaadid"; // translategemma:4b
"Display" = "Ekraan"; // translategemma:4b
// Update
"The latest version of Stats installed" = "Uusim versioon on installitud";
"Downloading..." = "Allalaadimine...";
"Current version: " = "Praegune versioon: ";
"Latest version: " = "Uusim version: ";
// Widgets
"Color" = "Värv";
"Label" = "Silt";
"Box" = "Kast";
"Frame" = "Raam";
"Value" = "Väärtus";
"Colorize" = "Värvi";
"Colorize value" = "Värvi väärtus";
"Additional information" = "Lisateave";
"Reverse values order" = "Pööra väärtuste järjekorda vastupidiseks";
"Base" = "Alus";
"Display mode" = "Vaatamise režiim";
"One row" = "Üks rida";
"Two rows" = "Kaks read";
"Mini widget" = "Väike"; // translategemma:4b
"Line chart widget" = "Joonediagramm";
"Bar chart widget" = "Tulpdiagramm";
"Pie chart widget" = "Sektordiagramm";
"Network chart widget" = "Võrgudiagramm";
"Speed widget" = "Kiirus";
"Battery widget" = "Patarei";
"Stack widget" = "Põhikoht"; // translategemma:4b
"Memory widget" = "Mälu";
"Static width" = "Liikumatu laius";
"Tachometer widget" = "Tahhomeeter";
"State widget" = "Seisund";
"Text widget" = "Teksti atribuut"; // translategemma:4b
"Battery details widget" = "Aku üksikasjade vidžit"; // translategemma:4b
"Show symbols" = "Näita sümboleids";
"Label widget" = "Silt";
"Number of reads in the chart" = "Lugemiste arv diagrammis";
"Color of download" = "Allaladimise värv";
"Color of upload" = "Üleslaadimise värv";
"Monospaced font" = "Üheruumiline kirjastiil";
"Reverse order" = "Vastupidises järjekorras"; // translategemma:4b
"Chart history" = "Graafiku ajaloo"; // translategemma:4b
"Default color" = "Vaikimisi"; // translategemma:4b
"Transparent when no activity" = "Läbipaistev, kui pole tegevust."; // translategemma:4b
"Constant color" = "Järjekordne"; // translategemma:4b
// Module Kit
"Open module settings" = "Ava mooduli sätted";
"Select widget" = "Vali %0 vidin";
"Open widget settings" = "Ava vidina seaded";
"Update interval" = "Värskendamise intervall";
"Usage history" = "Kasutusajalugu";
"Details" = "Üksikasjad";
"Top processes" = "Tippprotsessid";
"Pictogram" = "Piktogramm";
"Module" = "Moodul";
"Widgets" = "Vidinad";
"Popup" = "Hüpik";
"Notifications" = "Teavitused";
"Merge widgets" = "Liida vidinad";
"No available widgets to configure" = "Vidinaid pole saadaval seadistamiseks";
"No options to configure for the popup in this module" = "Selles moodulis pole suvandeid konfigureerida hüpikakna jaoks";
"Process" = "Protsess"; // translategemma:4b
"Kill process" = "Lõpeta protsess"; // translategemma:4b
"Keyboard shortcut" = "Tahtekahtlik"; // translategemma:4b
"Listening..." = "Kuulamine..."; // translategemma:4b
// Modules
"Number of top processes" = "Tippprotsesside arv";
"Update interval for top processes" = "Tippprotsesside värskendusintervall";
"Notification level" = "Teavitustase";
"Chart color" = "Diagrammi värv";
"Main chart scaling" = "Peamise graafi skaala"; // translategemma:4b
"Scale value" = "Mõõtühiku väärtus"; // translategemma:4b
"Text widget value" = "Teksti atribuudi väärtus"; // translategemma:4b
// CPU
"CPU usage" = "CPU kasutus";
"CPU temperature" = "CPU temperatuur";
"CPU frequency" = "CPU sagedus";
"System" = "Süsteem";
"User" = "Kassutaja";
"Idle" = "Väljas"; // translategemma:4b
"Show usage per core" = "Kuva kasutust tuuma kohta";
"Show hyper-threading cores" = "Näita hüperlõime tuumad";
"Split the value (System/User)" = "Jagage väärtus (süsteem/kasutaja)";
"Scheduler limit" = "Planeerija limiit";
"Speed limit" = "Kiirus limiit";
"Average load" = "Keskmine koormus";
"1 minute" = "1 minut";
"5 minutes" = "5 minutit";
"15 minutes" = "15 minutit";
"CPU usage threshold" = "CPU kasutuslävi";
"CPU usage is" = "CPU kasutus on %0";
"Efficiency cores" = "Tõhusatuumad";
"Performance cores" = "Jõudlustuumad";
"System color" = "Süsteemi värv";
"User color" = "Kasutaja värv";
"Idle color" = "Tühikäik värv";
"Cluster grouping" = "Klastrite rühmitamine";
"Efficiency cores color" = "Tõhusatuumade värv";
"Performance cores color" = "Jõudlustuumade värv";
"Total load" = "Kokku koormat"; // translategemma:4b
"System load" = "Süsteemi koormus"; // translategemma:4b
"User load" = "Kasutajate koormus"; // translategemma:4b
"Efficiency cores load" = "Efektiivsed protsessorid laadivad"; // translategemma:4b
"Performance cores load" = "Protsessori üditöödel laadimise algus"; // translategemma:4b
"All cores" = "Kõik protsessorid"; // translategemma:4b
// GPU
"GPU to show" = "GPU, mida näidata";
"Show GPU type" = "Kuva GPU tüüp";
"GPU enabled" = "GPU lubatud";
"GPU disabled" = "GPU keelatud";
"GPU temperature" = "GPU temperatuur";
"GPU utilization" = "GPU kasutamine";
"Vendor" = "Müüja";
"Model" = "Mudel";
"Status" = "Olek";
"Active" = "Aktiivne";
"Non active" = "Mitteaktiivne";
"Fan speed" = "Ventilaatori kiirus";
"Core clock" = "Tuuma kell";
"Memory clock" = "Mälu kell";
"Utilization" = "Kasutus";
"Render utilization" = "Renderdamine kasutus";
"Tiler utilization" = "Plaatija kasutus";
"GPU usage threshold" = "GPU kasutuslävi";
"GPU usage is" = "GPU kasutus on %0";
// RAM
"Memory usage" = "Mälu kasutus";
"Memory pressure" = "Mälu surve";
"Total" = "Kokku";
"Used" = "Kasutatud";
"App" = "Rakendus";
"Wired" = "Juhtmega";
"Compressed" = "Tihendatud";
"Free" = "Vaba";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Jagage väärtus (rakendus/juhtmega/tihendatud)";
"RAM utilization threshold" = "Muutmälu kasutuslävi";
"RAM utilization is" = "Muutmälu kasutus on %0";
"App color" = "Rakenduse värv";
"Wired color" = "Juhtmega värv";
"Compressed color" = "Tihendatud värv";
"Free color" = "Vaba värv";
"Free memory (less than)" = "Vaba mälu (vähem kui)"; // translategemma:4b
"Swap size" = "Vahetuskuju"; // translategemma:4b
"Free RAM is" = "Vaba RAM on %0"; // translategemma:4b
// Disk
"Show removable disks" = "Kuva irdkettad";
"Used disk memory" = "Kuva irdkettad";
"Free disk memory" = "%0 %1-st vaba";
"Disk to show" = "Ketas, mida näidata";
"Open disk" = "Ava ketas";
"Switch view" = "Vaade vahetamine";
"Disk utilization threshold" = "Ketta kasutuslävi";
"Disk utilization is" = "Ketta kasutus on %0";
"Read color" = "Loe värvi";
"Write color" = "Kirjuta värv";
"Disk usage" = "Kettakasutus";
"Total read" = "Kokku lugemise arv"; // translategemma:4b
"Total written" = "Kokku kirjalikult"; // translategemma:4b
"Write speed" = "Kirjutada"; // translategemma:4b
"Read speed" = "Loe"; // translategemma:4b
"Drives" = "Juhtmed"; // translategemma:4b
"SMART data" = "SMART-andmed"; // translategemma:4b
// Sensors
"Temperature unit" = "Temperatuuri ühik";
"Celsius" = "Celsi"; // translategemma:4b
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Salvesta ventilaatori kiirus";
"Fan" = "Ventilaator";
"HID sensors" = "HID andurid";
"Synchronize fan's control" = "Sünkroniseeri ventilaatori juhtimine";
"Current" = "Vool";
"Energy" = "Energia";
"Show unknown sensors" = "Näita tundmatuid andureid";
"Install fan helper" = "Installi ventilaatoriabi";
"Uninstall fan helper" = "Desinstalli fänniabi";
"Fan value" = "Fänni väärtus";
"Turn off fan" = "Lülita ventilatsioon välja"; // translategemma:4b
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Te kavatstate lülitada ventilator välja. See ei ole soovitatav tegevus, mis võib kahjustada teie Mac-i. Kas olete kindel, et soovite seda teha?"; // translategemma:4b
"Sensor threshold" = "Sensori piirväärt"; // translategemma:4b
"Left fan" = "Vasakul"; // translategemma:4b
"Right fan" = "Õigesti"; // translategemma:4b
"Fastest fan" = "Kõige kiirem"; // translategemma:4b
"Sensor to show" = "Sensor, mis näitab"; // translategemma:4b
// Network
"Uploading" = "Üleslaadimine";
"Downloading" = "Allalaadimine";
"Public IP" = "Avalik IP";
"Local IP" = "Kohalik IP";
"Interface" = "Liides";
"Physical address" = "Füüsiline aadress";
"Refresh" = "Värskenda";
"Click to copy public IP address" = "Klõpsa avaliku IP-aadressi kopeerimiseks";
"Click to copy local IP address" = "Klõpsa kohaliku IP-aadressi kopeerimiseks";
"Click to copy wifi name" = "Klõpsa wifi nime kopeerimiseks";
"Click to copy mac address" = "MAC-aadressi kopeerimiseks klõpsake";
"No connection" = "Ühendus puudub";
"Network interface" = "Võrguliides";
"Total download" = "Allalaadimine kokku";
"Total upload" = "Üleslaadimine kokku";
"Reader type" = "Lugeja tüüp";
"Interface based" = "Liidesepõhine";
"Processes based" = "Protsessipõhine";
"Reset data usage" = "Lähtesta andmekasutus";
"VPN mode" = "VPN-režiim";
"Standard" = "Standard";
"Security" = "Turvalisus";
"Channel" = "Kanal";
"Common scale" = "Ühine skaala";
"Autodetection" = "Automaattuvastus";
"Widget activation threshold" = "Vidina aktiveerimise lävi";
"Internet connection" = "Interneti-ühendus";
"Active state color" = "Aktiivse oleku värv";
"Nonactive state color" = "Mitteaktiivse oleku värv";
"Connectivity host (ICMP)" = "Ühendushost (ICMP)";
"Leave empty to disable the check" = "Tšeki keelamiseks jätke tühjaks";
"Connectivity history" = "Ühenduse ajalugu";
"Auto-refresh public IP address" = "Avaliku IP-aadressi automaatne värskendamine";
"Every hour" = "Iga tund";
"Every 12 hours" = "Iga 12 tunni järel";
"Every 24 hours" = "Iga 24 tunni järel";
"Network activity" = "Võrgutegevus";
"Last reset" = "Viimane lähtestamine %0 tagasi";
"Latency" = "Laskumine"; // translategemma:4b
"Upload speed" = "Laadimine"; // translategemma:4b
"Download speed" = "Laadimine"; // translategemma:4b
"Address" = "Aadress"; // translategemma:4b
"WiFi network" = "WiFi võrk"; // translategemma:4b
"Local IP changed" = "Lokalne IP-aadress on muutunud"; // translategemma:4b
"Public IP changed" = "Avalik IP-aadress on muutunud"; // translategemma:4b
"Previous IP" = "Eelmine IP: %0"; // translategemma:4b
"New IP" = "Uus IP-aadress: %0"; // translategemma:4b
"Internet connection lost" = "Internet-ühendus katkestas."; // translategemma:4b
"Internet connection established" = "Internet-ühendus on toimimas"; // translategemma:4b
// Battery
"Level" = "Tase";
"Source" = "Allikas";
"AC Power" = "AC jõud"; // translategemma:4b
"Battery Power" = "Aku võimsus";
"Time" = "Aeg";
"Health" = "Tervis";
"Amperage" = "Amper"; // translategemma:4b
"Voltage" = "Pinge";
"Cycles" = "Tsüklid";
"Temperature" = "Temperatuur";
"Power adapter" = "Toiteadapter";
"Power" = "Toide";
"Is charging" = "Laadib";
"Time to discharge" = "Time to loss";
"Time to charge" = "Laadimise aeg";
"Calculating" = "Arvutamine";
"Fully charged" = "Täis laetud";
"Not connected" = "Pole ühendatud";
"Low level notification" = "Madala taseme teatis";
"High level notification" = "Kõrge taseme teatis";
"Low battery" = "Akud on tühi"; // translategemma:4b
"High battery" = "Kõrge aku";
"Battery remaining" = "%0% jäänud";
"Battery remaining to full charge" = "%0% täislaadimiseni";
"Percentage" = "Protsent";
"Percentage and time" = "Protsent ja aeg";
"Time and percentage" = "Aeg ja protsent";
"Time format" = "Ajavorming";
"Hide additional information when full" = "Peida lisateave, kui täis";
"Last charge" = "Viimane laadimine";
"Capacity" = "Mahutavus";
"current / maximum / designed" = "praegune / maksimaalne / kavandatud";
"Low power mode" = "Madala energiatarbega režiim";
"Percentage inside the icon" = "Protsent ikooni sees";
"Colorize battery" = "Värvi aku";
"Charging current" = "Laadimise vool"; // translategemma:4b
"Charging Voltage" = "Laadimise pinge"; // translategemma:4b
"Charger state inside the battery" = "Laadija staatus akus"; // translategemma:4b
// Bluetooth
"Battery to show" = "Patarei, mida näidata";
"No Bluetooth devices are available" = "Ükski Bluetooth-seade pole saadaval";
// Clock
"Time zone" = "Ajavöönd";
"Local" = "Kohalik";
"Calendar" = "Kalender"; // translategemma:4b
"Show week numbers" = "Näita nädalanumbreid"; // translategemma:4b
"Local time" = "Paikne aeg"; // translategemma:4b
"Add new clock" = "Lisage uus ajakella"; // translategemma:4b
"Delete selected clock" = "Kasuta valitud ajaskena"; // translategemma:4b
"Help with datetime format" = "Abi ajaveebi formaadi määramisel"; // translategemma:4b
// Colors
"Based on utilization" = "Põhineb kasutusel";
"Based on pressure" = "Põhineb rõhul";
"Based on cluster" = "Põhineb klastril";
"System accent" = "Süsteemi aktsent";
"Monochrome accent" = "Ühevärviline aktsent";
"Clear" = "Läbipaistev";
"White" = "Valge";
"Black" = "Must";
"Gray" = "Hall";
"Second gray" = "Teine hall";
"Dark gray" = "Tumehall";
"Light gray" = "Helehall";
"Red" = "Punane";
"Second red" = "Teine punane";
"Green" = "Roheline";
"Second green" = "Teine roheline";
"Blue" = "Sinine";
"Second blue" = "Teine sinine";
"Yellow" = "Kollane";
"Second yellow" = "Teine kollane";
"Orange" = "Oranž";
"Second orange" = "Teine oranž";
"Purple" = "Lilla";
"Second purple" = "Teine lilla";
"Brown" = "Pruun";
"Second brown" = "Teine pruun";
"Cyan" = "Tsüaan";
"Magenta" = "Magenta";
"Pink" = "Roosa";
"Teal" = "Sinakasroheline"; // ehk rohekassinine
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/fa.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Shayan Alizadeh on 17/11/2022.
// Using Swift 5.0.
// Running on macOS 13.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "سی پی یو";
"Open CPU settings" = "باز کردن تنظیمات سی پی یو";
"GPU" = "جی پی یو";
"Open GPU settings" = "باز کردن تنظیمات جی پی یو";
"RAM" = "رم";
"Open RAM settings" = "باز کردن تنظیمات رم";
"Disk" = "دیسک";
"Open Disk settings" = "باز کردن تنظیمات دیسک";
"Sensors" = "سنسور ها";
"Open Sensors settings" = "باز کردن تنظیمات سنسور ها";
"Network" = "شبکه";
"Open Network settings" = "باز کردن تنظیمات شبکه";
"Battery" = "باتری";
"Open Battery settings" = "باز کردن تنظیمات باتری";
"Bluetooth" = "بلوتوث";
"Open Bluetooth settings" = "باز کردن تنظیمات بلوتوث";
"Clock" = "ساعت"; // translategemma:4b
"Open Clock settings" = "باز کردن تنظیمات ساعت"; // translategemma:4b
// Words
"Unknown" = "نامعلوم";
"Version" = "نسخه";
"Processor" = "پردازنده";
"Memory" = "حافظه";
"Graphics" = "گرافیک ها";
"Close" = "بستن";
"Download" = "دانلود";
"Install" = "نصب";
"Cancel" = "لغو";
"Unavailable" = "غیر قابل دسترس";
"Yes" = "بله";
"No" = "خیر";
"Automatic" = "خودکار";
"Manual" = "دستی";
"None" = "هیچ کدام";
"Dots" = "نقطه ای";
"Arrows" = "فلش";
"Characters" = "کرکتر";
"Short" = "کوتاه";
"Long" = "بلند";
"Statistics" = "آمار";
"Max" = "حداکثر";
"Min" = "حداقل";
"Reset" = "ریست";
"Alignment" = "چینش";
"Left alignment" = "چپ";
"Center alignment" = "وسط";
"Right alignment" = "راست";
"Dashboard" = "داشبورد";
"Enabled" = "فعال";
"Disabled" = "غیرفعال";
"Silent" = "حالت سکوت";
"Units" = "واحد ها";
"Fans" = "فن ها";
"Scaling" = "مقیاس بندی";
"Linear" = "خطی";
"Square" = "مربع";
"Cube" = "مکعب";
"Logarithmic" = "لگاریتمی";
"Fixed scale" = "اصلاح شده"; // translategemma:4b
"Cores" = "هسته ها";
"Settings" = "تنظیمات"; // translategemma:4b
"Name" = "نام"; // translategemma:4b
"Format" = "فرمت"; // translategemma:4b
"Turn off" = "خاموش کردن"; // translategemma:4b
"Normal" = "نرمال"; // translategemma:4b
"Warning" = "هشدار"; // translategemma:4b
"Critical" = "فوری/ حیاتی"; // translategemma:4b
"Usage" = "نحوه استفاده"; // translategemma:4b
"2 minutes" = "۲ دقیقه"; // translategemma:4b
"3 minutes" = "3 دقیقه"; // translategemma:4b
"10 minutes" = "10 دقیقه"; // translategemma:4b
"Import" = "وارد کردن"; // translategemma:4b
"Export" = "صدور"; // translategemma:4b
"Separator" = "فاصلهگذار"; // translategemma:4b
"Read" = "خواندن"; // translategemma:4b
"Write" = "نوشتن"; // translategemma:4b
"Frequency" = "فرکانس"; // translategemma:4b
"Save" = "ذخیره"; // translategemma:4b
"Run" = "اجرا"; // translategemma:4b
"Stop" = "متوقف"; // translategemma:4b
"Uninstall" = "حذف"; // translategemma:4b
"1 sec" = "۱ ثانیه"; // translategemma:4b
"2 sec" = "2 ثانیه"; // translategemma:4b
"3 sec" = "3 ثانیه"; // translategemma:4b
"5 sec" = "5 ثانیه"; // translategemma:4b
"10 sec" = "10 ثانیه"; // translategemma:4b
"15 sec" = "15 ثانیه"; // translategemma:4b
"30 sec" = "30 ثانیه"; // translategemma:4b
"60 sec" = "60 ثانیه"; // translategemma:4b
// Setup
"Stats Setup" = "نصب Stats";
"Previous" = "قبلی";
"Previous page" = "صفحه قبل";
"Next" = "بعدی";
"Next page" = "صفحه بعد";
"Finish" = "پایان";
"Finish setup" = "پایان نصب";
"Welcome to Stats" = "به Stats خوش آمدید";
"welcome_message" = "ممنون از اینکه از Stats که یک اپ مانیتور کردن سیستم برای منو بار مکاواس هست استفاده می کنید.";
"Start the application automatically when starting your Mac" = "اجرای خودکار در زمان راهاندازی مک";
"Do not start the application automatically when starting your Mac" = "اجرا نشدن خودکار در زمان راهاندازی مک";
"Do everything silently in the background (recommended)" = "همهچیز در حالت سکوت در پسزمینه انجام شود (پیشنهاد شده)";
"Check for a new version on startup" = "بررسی برای نسخهی جدید در زمان راهاندازی مک";
"Check for a new version every day (once a day)" = "بررسی روزانه برای نسخه جدید (روزی یک بار)";
"Check for a new version every week (once a week)" = "بررسی هفتگی برای نسخه جدید (هفتهای یک بار)";
"Check for a new version every month (once a month)" = "بررسی ماهانه برای نسخه جدید (ماهی یک بار)";
"Never check for updates (not recommended)" = "هیچ وقت نسخهی جدید بررسی نشود (پیشنهاد نمیشود)";
"Anonymous telemetry for better development decisions" = "دادههای جمعآوری شده به صورت ناشناس برای تصمیمگیریهای بهتر در توسعه"; // translategemma:4b
"Share anonymous telemetry data" = "اشتراکگذاری دادههای ردیابی ناشناس"; // translategemma:4b
"Do not share anonymous telemetry data" = "از اشتراکگذاری دادههای جمعآوریشده به صورت ناشناس خودداری کنید."; // translategemma:4b
"The configuration is completed" = "پیکربندی کامل شد";
"finish_setup_message" = "همه چیز آماده است! \n Stats یک ابزار متن باز رایگان است و همیشه رایگان خواهد بود \n اگر از این پروژه راضی بودید میتوانید از ما حمایت کنید";
// Alerts
"New version available" = "نسخهی جدید در دسترس است";
"Click to install the new version of Stats" = "برای نصب نسخهی جدید Stats کلیک کنید";
"Successfully updated" = "با موفقیت بهروزرسانی شد";
"Stats was updated to v" = "Stats به نسخهی v%0 بروزسانی شد ";
"Reset settings text" = "تمام تنظیمات اپلیکیشن ریست خواهد شد و اپ دوباره راهاندازی خواهد شد، آیا از این کار مطمئنید؟";
"Support text" = "از اینکه از آمار استفاده می کنید متشکریم!\n\n نگهداری و بهبود این پروژه منبع باز زمان و منابع می طلبد. پشتیبانی شما به ما کمک میکند تا به ارائه یک برنامه رایگان و قابل اعتماد برای همه ادامه دهیم.\n\nاگر آمارها را مفید میدانید، لطفاً در نظر بگیرید. هر ذره کمک می کند!";
// Settings
"Open Activity Monitor" = "باز کردن نظارت فعالیتها";
"Report a bug" = "گزارش باگ";
"Support the application" = "حمایت از اپلیکیشن";
"Close application" = "بستن اپلیکیشن";
"Open application settings" = "بستن تنظیمات اپلیکیشن";
"Open dashboard" = "باز کردن داشبورد";
"No notifications available in this module" = "هیچ اعلان یا هشداری در این ماژول وجود ندارد."; // translategemma:4b
"Open Calendar" = "باز کردن تقویم"; // translategemma:4b
"Toggle the module" = "فعال/غیرفعال کردن ماژول"; // translategemma:4b
// Application settings
"Update application" = "بروزرسانی اپلیکیشن";
"Check for updates" = "بررسی برای نسخههای جدید";
"At start" = "در زمان شروع";
"Once per day" = "یک بار در روز";
"Once per week" = "یک بار در هفته";
"Once per month" = "یک بار در ماه";
"Never" = "هیچ وقت";
"Check for update" = "بررسی برای نسخهی جدید";
"Show icon in dock" = "نمایش آیکون در داک";
"Start at login" = "باز شدن در زمان لاگین";
"Build number" = "شماره ساخت";
"Import settings" = "وارد کردن تنظیمات"; // translategemma:4b
"Export settings" = "تنظیمات خروجی"; // translategemma:4b
"Reset settings" = "ریست تنظیمات";
"Pause the Stats" = "متوقف کردن Stats";
"Resume the Stats" = "ادامهی کار Stats";
"Combined modules" = "ماژولهای ترکیبی"; // translategemma:4b
"Combined details" = "جزئیات ترکیبی"; // translategemma:4b
"Spacing" = "فاصله"; // translategemma:4b
"Share anonymous telemetry" = "اشتراکگذاری دادههای ناشناس"; // translategemma:4b
"Choose file" = "فایل مورد نظر را انتخاب کنید"; // translategemma:4b
"Stress tests" = "آزمایشهای تحمل استرس"; // translategemma:4b
// Dashboard
"Serial number" = "سریال نامبر";
"Model identifier" = "شناسه مدل"; // translategemma:4b
"Production year" = "سال تولید"; // translategemma:4b
"Uptime" = "زمان روشن بودن";
"Number of cores" = "%0 هستهها";
"Number of threads" = "%0 تردها";
"Number of e-cores" = "%0 هستههای کممصرف";
"Number of p-cores" = "%0 هستههای پرقدرت";
"Disks" = "دیسکها"; // translategemma:4b
"Display" = "نمایش"; // translategemma:4b
// Update
"The latest version of Stats installed" = "آخرین نسخهی Stats نصب شده است";
"Downloading..." = "دانلود...";
"Current version: " = "نسخه فعلی: ";
"Latest version: " = "آخرین نسخه: ";
// Widgets
"Color" = "رنگ";
"Label" = "لیبل";
"Box" = "جعبه";
"Frame" = "فریم";
"Value" = "مقدار";
"Colorize" = "رنگی";
"Colorize value" = "مقدار رنگ";
"Additional information" = "اطلاعات بیشتر";
"Reverse values order" = "ترتیب برعکس";
"Base" = "پایه";
"Display mode" = "حالت نمایش";
"One row" = "یک ردیف";
"Two rows" = "دو ردیف";
"Mini widget" = "کوچک";
"Line chart widget" = "نمودار خطی";
"Bar chart widget" = "نمودار میلهای";
"Pie chart widget" = "نمودار دایرهای";
"Network chart widget" = "نمودار شبکه";
"Speed widget" = "سرعت";
"Battery widget" = "باتری";
"Stack widget" = "پشته"; // translategemma:4b
"Memory widget" = "حافظه";
"Static width" = "عرض ثابت";
"Tachometer widget" = "تاکومتر";
"State widget" = "ویجت حالت";
"Text widget" = "کد مربوط به ویجت متنی"; // translategemma:4b
"Battery details widget" = "ویجت اطلاعات باتری"; // translategemma:4b
"Show symbols" = "ویجت نمادها";
"Label widget" = "لیبل";
"Number of reads in the chart" = "تعداد خواندنها در نمودار";
"Color of download" = "رنگ دانلود";
"Color of upload" = "رنگ آپلود";
"Monospaced font" = "فونت با فاصله یکسان"; // translategemma:4b
"Reverse order" = "به ترتیب معکوس"; // translategemma:4b
"Chart history" = "تاریخچه نمودار"; // translategemma:4b
"Default color" = "پیشفرض"; // translategemma:4b
"Transparent when no activity" = "شفاف در زمان عدم فعالیت"; // translategemma:4b
"Constant color" = "ثابت"; // translategemma:4b
// Module Kit
"Open module settings" = "باز کردن تنظیمات ماژولها";
"Select widget" = "انتخاب %0 ویجت";
"Open widget settings" = "باز کردن تنظیمات ویجتها";
"Update interval" = "بروزرسانی زمانبندی";
"Usage history" = "تاریخچه مصرف";
"Details" = "جزئیات";
"Top processes" = "بیشترین پردازشها";
"Pictogram" = "پیکتوگرام";
"Module" = "مدول";
"Widgets" = "بزارک ها";
"Popup" = "پنجره بازشو";
"Notifications" = "اطلاعیه";
"Merge widgets" = "ادغام ویجتها";
"No available widgets to configure" = "ویجتی برای پیکربندی در دسترس نیست";
"No options to configure for the popup in this module" = "در این ماژول تنظیمی برای پاپآپ موجود نیست";
"Process" = "فرآیند"; // translategemma:4b
"Kill process" = "بستن فرآیند"; // translategemma:4b
"Keyboard shortcut" = "کلید میانبر"; // translategemma:4b
"Listening..." = "در حال گوش دادن..."; // translategemma:4b
// Modules
"Number of top processes" = "تعداد بالاترین پردازشها";
"Update interval for top processes" = " بروزرسانی زمانبندی برای بالاترین پردازشها";
"Notification level" = "سطح نوتیفیکیشن";
"Chart color" = "رنگ نمودار";
"Main chart scaling" = "مقیاسبندی نمودار اصلی"; // translategemma:4b
"Scale value" = "مقیاس مقدار"; // translategemma:4b
"Text widget value" = "مقدار عنصر `Text`"; // translategemma:4b
// CPU
"CPU usage" = "مصرف سی پی یو";
"CPU temperature" = "دمای سی پی یو";
"CPU frequency" = "فرکانس سی پی یو";
"System" = "سیستم";
"User" = "کاربر";
"Idle" = "بیکار";
"Show usage per core" = "نمایش مصرف براساس هسته";
"Show hyper-threading cores" = "نمایش هستههای هایپرتردینگ";
"Split the value (System/User)" = "جداسازی مقادیر (سیستم/کابر)";
"Scheduler limit" = "محدودیت برنامه";
"Speed limit" = "محدودیت سرعت";
"Average load" = "میانگین بار";
"1 minute" = "۱ دقیقه";
"5 minutes" = "۵ دقیقه";
"15 minutes" = "۱۵ دقیقه";
"CPU usage threshold" = "آستانه مصرف سی پی یو";
"CPU usage is" = "مصرف سی پی یو %0";
"Efficiency cores" = "هستههای کممصرف";
"Performance cores" = "هستههای پرقدرت";
"System color" = "رنگ سیستم";
"User color" = "رنگ کاربر";
"Idle color" = "رنگ بیکار";
"Cluster grouping" = "گروهبندی کلاستر";
"Efficiency cores color" = "رنگ هستههای بهینهسازی"; // translategemma:4b
"Performance cores color" = "رنگ هستههای عملکرد"; // translategemma:4b
"Total load" = "مجموع بار"; // translategemma:4b
"System load" = "بار سیستم"; // translategemma:4b
"User load" = "بار کاربر"; // translategemma:4b
"Efficiency cores load" = "بارگیری هستههای کارآمد"; // translategemma:4b
"Performance cores load" = "بارگذاری هستههای عملکرد"; // translategemma:4b
"All cores" = "تمام هستهها"; // translategemma:4b
// GPU
"GPU to show" = "نمایش جی پی یو";
"Show GPU type" = "نمایش نوع جی پی یو";
"GPU enabled" = "جی پی یو فعال";
"GPU disabled" = "جی پی یو غیرفعال";
"GPU temperature" = "دمای جی پی یو ";
"GPU utilization" = "مصرف جی پی یو";
"Vendor" = "وندر";
"Model" = "مدل";
"Status" = "وضعیت";
"Active" = "فعال";
"Non active" = "غیر فعال";
"Fan speed" = "سرعت فن";
"Core clock" = "کلاک هسته";
"Memory clock" = "کلاک حافظه";
"Utilization" = "استفاده";
"Render utilization" = "استفاده از رندر";
"Tiler utilization" = "استفاده از تایلر";
"GPU usage threshold" = "آستانه مصرف جی پی یو";
"GPU usage is" = "مصرف جی پی یو %0";
// RAM
"Memory usage" = "مصرف حافظه";
"Memory pressure" = "فشار حافظه";
"Total" = "کل";
"Used" = "استفاده شده";
"App" = "اپها";
"Wired" = "سیستمبندی";
"Compressed" = "فشرده شده";
"Free" = "آزاد";
"Swap" = "تبادل";
"Split the value (App/Wired/Compressed)" = "جداسازی مقادیر (اپ/سیستمبندی/فشرده شده)";
"RAM utilization threshold" = "آستانه مصرف رم";
"RAM utilization is" = "مصرف رم %0";
"App color" = "رنگ اپ";
"Wired color" = "رنگ سیستمبندی";
"Compressed color" = "رنگ فشرده شده";
"Free color" = "رنگ آزاد";
"Free memory (less than)" = "حافظه آزاد (کمتر از)"; // translategemma:4b
"Swap size" = "حجم حافظه فرعی"; // translategemma:4b
"Free RAM is" = "حافظه رم آزاد، %0 است."; // translategemma:4b
// Disk
"Show removable disks" = "نمایش حافظههای متصل شده";
"Used disk memory" = "%0 از %1 استفاده شده";
"Free disk memory" = "%0 از %1 آزاد است";
"Disk to show" = "نمایش دیسک";
"Open disk" = "باز کردن دیسک";
"Switch view" = "تغییر نمایش";
"Disk utilization threshold" = "آستانه مصرف دیسک";
"Disk utilization is" = "مصرف دیسک %0";
"Read color" = "رنگ را بخوان"; // translategemma:4b
"Write color" = "نوشتن رنگ"; // translategemma:4b
"Disk usage" = "مصرف فضای دیسک"; // translategemma:4b
"Total read" = "کل میزان خوانده شده"; // translategemma:4b
"Total written" = "مجموعاً نوشته شده"; // translategemma:4b
"Write speed" = "نوشتن"; // translategemma:4b
"Read speed" = "خواندن"; // translategemma:4b
"Drives" = "درایوها"; // translategemma:4b
"SMART data" = "دادههای هوشمند"; // translategemma:4b
// Sensors
"Temperature unit" = "واحد دما";
"Celsius" = "سلسیوس";
"Fahrenheit" = "فارنهایت";
"Save the fan speed" = "ذخیره سرعت فن";
"Fan" = "فن";
"HID sensors" = "سنسورهای HID";
"Synchronize fan's control" = "همگامسازی سرعت فنها";
"Current" = "جریان";
"Energy" = "انرژی";
"Show unknown sensors" = "نمایش سنسورهای ناشناخته";
"Install fan helper" = "نصب ابزار کمک فن"; // translategemma:4b
"Uninstall fan helper" = "حذف برنامه \"کمککننده فن"; // translategemma:4b
"Fan value" = "مقدار فن"; // translategemma:4b
"Turn off fan" = "فن را خاموش کنید"; // translategemma:4b
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "شما قصد دارید فن را خاموش کنید. این اقدام توصیه نمیشود و میتواند به کامپیوتر شما آسیب برساند. آیا مطمئن هستید که میخواهید این کار را انجام دهید؟"; // translategemma:4b
"Sensor threshold" = "آستانه سنسور"; // translategemma:4b
"Left fan" = "چپ"; // translategemma:4b
"Right fan" = "درست"; // translategemma:4b
"Fastest fan" = "سریعترین"; // translategemma:4b
"Sensor to show" = "نمایش سنسور"; // translategemma:4b
// Network
"Uploading" = "آپلود";
"Downloading" = "دانلود";
"Public IP" = "آی پی عمومی";
"Local IP" = "آی پی محلی";
"Interface" = "نوع اتصال";
"Physical address" = "آدرس فیزیکی";
"Refresh" = "بروزرسانی";
"Click to copy public IP address" = "برای کپی آدرس آی پی عمومی کلیک کنید";
"Click to copy local IP address" = "برای کپی آدرس آی پی محلی کلیک کنید";
"Click to copy wifi name" = "برای کپی نام وایفای کلیک کنید";
"Click to copy mac address" = "برای کپی مک آدرس کلیک کنید";
"No connection" = "اتصال قطع است";
"Network interface" = "نوع اتصال شبکه";
"Total download" = "کل دانلودها";
"Total upload" = "کل آپلودها";
"Reader type" = "حالت خواننده";
"Interface based" = "براساس نوع اتصال";
"Processes based" = "براساس پردازشها";
"Reset data usage" = "ریست مصرف دیتا";
"VPN mode" = "حالت وی پی ان";
"Standard" = "استاندارد";
"Security" = "امنیت";
"Channel" = "کانال";
"Common scale" = "مقیاس عادی";
"Autodetection" = "تشخیص خودکار";
"Widget activation threshold" = "آستانه فعالسازی ویجت";
"Internet connection" = "اتصال اینترنت";
"Active state color" = "رنگ حالت فعال";
"Nonactive state color" = "رنگ حالت غیر فعال";
"Connectivity host (ICMP)" = "میزبان اتصال (ICMP)";
"Leave empty to disable the check" = "خالی بگذارید تا بررسی غیرفعال شود";
"Connectivity history" = "تاریخچه اتصال"; // translategemma:4b
"Auto-refresh public IP address" = "بهطور خودکار، آدرس IP عمومی را بهروزرسانی کنید."; // translategemma:4b
"Every hour" = "هر ساعت"; // translategemma:4b
"Every 12 hours" = "هر ۱۲ ساعت"; // translategemma:4b
"Every 24 hours" = "هر 24 ساعت"; // translategemma:4b
"Network activity" = "فعالیت شبکه"; // translategemma:4b
"Last reset" = "آخرین بهروزرسانی %0 پیش از این"; // translategemma:4b
"Latency" = "تاخیر"; // translategemma:4b
"Upload speed" = "بارگذاری"; // translategemma:4b
"Download speed" = "دانلود"; // translategemma:4b
"Address" = "آدرس"; // translategemma:4b
"WiFi network" = "شبکه وایفای"; // translategemma:4b
"Local IP changed" = "آدرس IP محلی تغییر کرده است."; // translategemma:4b
"Public IP changed" = "آدرس IP عمومی تغییر کرده است."; // translategemma:4b
"Previous IP" = "آدرس IP قبلی: %0"; // translategemma:4b
"New IP" = "آدرس IP جدید: %0"; // translategemma:4b
"Internet connection lost" = "اتصال اینترنت قطع شد"; // translategemma:4b
"Internet connection established" = "اتصال اینترنت برقرار شد"; // translategemma:4b
// Battery
"Level" = "سطح";
"Source" = "منبع";
"AC Power" = "برق شهر";
"Battery Power" = "نیروی باتری";
"Time" = "زمان";
"Health" = "سلامت";
"Amperage" = "آمپر";
"Voltage" = "ولتاژ";
"Cycles" = "دوره";
"Temperature" = "دما";
"Power adapter" = "آداپتور برق";
"Power" = "نیرو";
"Is charging" = "در حال شارژ";
"Time to discharge" = "زمان خالی شدن شارژ";
"Time to charge" = "زمان شارژ";
"Calculating" = "محاسبه";
"Fully charged" = "شارژ کامل";
"Not connected" = "وصل نیست";
"Low level notification" = "نوتیفیکیشن سطح کم";
"High level notification" = "نوتیفیکشن سطح زیاد";
"Low battery" = "باتری کم";
"High battery" = "باتری زیاد";
"Battery remaining" = "%0% باقیمانده";
"Battery remaining to full charge" = "%0% تا شارژ کامل";
"Percentage" = "درصد";
"Percentage and time" = "درصد و زمان";
"Time and percentage" = "زمان و درصد";
"Time format" = "فرمت زمان";
"Hide additional information when full" = "پنهان کردن اطلاعات اضافه موقع شارژ کامل";
"Last charge" = "آخرین شارژ";
"Capacity" = "ظرفیت";
"current / maximum / designed" = "current / حداکثر / طراحی شده";
"Low power mode" = "حالت کم مصرف";
"Percentage inside the icon" = "درصد داخل آیکون";
"Colorize battery" = "رنگآمیزی باتری"; // translategemma:4b
"Charging current" = "جریان شارژ"; // translategemma:4b
"Charging Voltage" = "ولتاژ شارژ"; // translategemma:4b
"Charger state inside the battery" = "وضعیت شارژ در داخل باتری"; // translategemma:4b
// Bluetooth
"Battery to show" = "نشان دادن باتری";
"No Bluetooth devices are available" = "هیچ دیوایس بلوتوثی در دسترس نیست";
// Clock
"Time zone" = "محدوده زمانی"; // translategemma:4b
"Local" = "محلی"; // translategemma:4b
"Calendar" = "تقویم"; // translategemma:4b
"Show week numbers" = "نمایش شماره هفتهها"; // translategemma:4b
"Local time" = "زمان محلی"; // translategemma:4b
"Add new clock" = "اضافه کردن ساعت جدید"; // translategemma:4b
"Delete selected clock" = "حذف ساعت انتخابشده"; // translategemma:4b
"Help with datetime format" = "کمک در فرمت تاریخ و زمان"; // translategemma:4b
// Colors
"Based on utilization" = "بر اساس استفاده";
"Based on pressure" = "بر اساس فشار";
"Based on cluster" = "بر اساس کلاستر";
"System accent" = "اکنست سیستم";
"Monochrome accent" = "اکسنت تکرنگ";
"Clear" = "روشن";
"White" = "سفید";
"Black" = "مشکی";
"Gray" = "خاکستری";
"Second gray" = "خاکستری دوم";
"Dark gray" = "خاکستری تیره";
"Light gray" = "خاکستری روشن";
"Red" = "قرمز";
"Second red" = "قرمز دوم";
"Green" = "سبز";
"Second green" = "سبز دوم";
"Blue" = "آبی";
"Second blue" = "آبی دوم";
"Yellow" = "زرد";
"Second yellow" = "زرد دوم";
"Orange" = "نارنجی";
"Second orange" = "نارنجی دوم";
"Purple" = "بنفش";
"Second purple" = "بنفش دوم";
"Brown" = "قهوهای";
"Second brown" = "قهوهای دوم";
"Cyan" = "فیروزهای";
"Magenta" = "ارغوانی";
"Pink" = "صورتی";
"Teal" = "کله غازی";
"Indigo" = "نیلی";
================================================
FILE: Stats/Supporting Files/fi.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by eightscrow on 07/12/2024.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "Avaa CPU asetukset";
"GPU" = "Näytönohjain"; // translategemma:4b
"Open GPU settings" = "Avaa GPU asetukset";
"RAM" = "RAM";
"Open RAM settings" = "Avaa RAM asetukset";
"Disk" = "Levy";
"Open Disk settings" = "Avaa Levy asetukset";
"Sensors" = "Anturit";
"Open Sensors settings" = "Avaa Anturit asetukset";
"Network" = "Verkko";
"Open Network settings" = "Avaa Verkko asetukset";
"Battery" = "Akku";
"Open Battery settings" = "Avaa Akku asetukset";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Avaa Bluetooth asetukset";
"Clock" = "Kello";
"Open Clock settings" = "Avaa Kello asetukset";
// Words
"Unknown" = "Tuntematon";
"Version" = "Versio";
"Processor" = "Suoritin";
"Memory" = "Muisti";
"Graphics" = "Grafiikka";
"Close" = "Sulje";
"Download" = "Lataa";
"Install" = "Asenna";
"Cancel" = "Peruuta";
"Unavailable" = "Ei saatavilla";
"Yes" = "Kyllä";
"No" = "Ei";
"Automatic" = "Automaattinen";
"Manual" = "Manuaalinen";
"None" = "Ei mitään";
"Dots" = "Pisteet";
"Arrows" = "Nuolet";
"Characters" = "Merkit";
"Short" = "Lyhyt";
"Long" = "Pitkä";
"Statistics" = "Tilastot";
"Max" = "Maksimi";
"Min" = "Minimi";
"Reset" = "Palauta";
"Alignment" = "Tasaus";
"Left alignment" = "Vasen";
"Center alignment" = "Keskellä";
"Right alignment" = "Oikea";
"Dashboard" = "Kojelauta";
"Enabled" = "Käytössä";
"Disabled" = "Pois käytöstä";
"Silent" = "Hiljainen";
"Units" = "Yksiköt";
"Fans" = "Tuulettimet";
"Scaling" = "Skaalaus";
"Linear" = "Lineaarinen";
"Square" = "Neliö";
"Cube" = "Kuutio";
"Logarithmic" = "Logaritmi";
"Fixed scale" = "Kiinteä";
"Cores" = "Ytimet";
"Settings" = "Asetukset";
"Name" = "Nimi";
"Format" = "Muoto";
"Turn off" = "Sammuta";
"Normal" = "Normaali"; // translategemma:4b
"Warning" = "Varoitus";
"Critical" = "Kriittinen";
"Usage" = "Käyttö";
"2 minutes" = "2 minuuttia";
"3 minutes" = "3 minuuttia";
"10 minutes" = "10 minuuttia";
"Import" = "Tuo";
"Export" = "Vie";
"Separator" = "Erotin"; // translategemma:4b
"Read" = "Lue"; // translategemma:4b
"Write" = "Kirjoita"; // translategemma:4b
"Frequency" = "Taajuus"; // translategemma:4b
"Save" = "Tallenna"; // translategemma:4b
"Run" = "Juokse"; // translategemma:4b
"Stop" = "Pysäytä"; // translategemma:4b
"Uninstall" = "Poista asennus"; // translategemma:4b
"1 sec" = "1 sekunti"; // translategemma:4b
"2 sec" = "2 sekuntia"; // translategemma:4b
"3 sec" = "3 sekuntia"; // translategemma:4b
"5 sec" = "5 sekuntia"; // translategemma:4b
"10 sec" = "10 sekuntia"; // translategemma:4b
"15 sec" = "15 sekuntia"; // translategemma:4b
"30 sec" = "30 sekuntia"; // translategemma:4b
"60 sec" = "60 sekuntia"; // translategemma:4b
// Setup
"Stats Setup" = "Stats asennus";
"Previous" = "Edellinen";
"Previous page" = "Edellinen sivu";
"Next" = "Seuraava";
"Next page" = "Seuraava sivu";
"Finish" = "Valmis";
"Finish setup" = "Viimeistele asennus";
"Welcome to Stats" = "Tervetuloa Stats:iin";
"welcome_message" = "Kiitos, että käytät Statsia, ilmaista avoimen lähdekoodin macOS-järjestelmämonitoria valikkorivillesi.";
"Start the application automatically when starting your Mac" = "Käynnistä sovellus automaattisesti, kun käynnistät Macin";
"Do not start the application automatically when starting your Mac" = "Älä käynnistä sovellusta automaattisesti, kun käynnistät Macin";
"Do everything silently in the background (recommended)" = "Tee kaikki hiljaisesti taustalla (suositeltu)";
"Check for a new version on startup" = "Tarkista uusi versio käynnistyksen yhteydessä";
"Check for a new version every day (once a day)" = "Tarkista uusi versio päivittäin (kerran päivässä)";
"Check for a new version every week (once a week)" = "Tarkista uusi versio viikoittain (kerran viikossa)";
"Check for a new version every month (once a month)" = "Tarkista uusi versio kuukausittain (kerran kuukaudessa)";
"Never check for updates (not recommended)" = "Älä koskaan tarkista päivityksiä (ei suositeltu)";
"Anonymous telemetry for better development decisions" = "Anonyymi telemetria parempaa tuotekehitystä varten";
"Share anonymous telemetry data" = "Jaa anonyymit telemetriatiedot";
"Do not share anonymous telemetry data" = "Älä jaa anonyymejä telemetriatietoja";
"The configuration is completed" = "Määritys on valmis";
"finish_setup_message" = "Kaikki on valmista! \n Stats on avoimen lähdekoodin työkalu, se on ilmainen ja tulee aina olemaan. \n Jos pidät siitä, voit tukea projektia, mikä on aina arvostettua!";
// Alerts
"New version available" = "Uusi versio saatavilla";
"Click to install the new version of Stats" = "Klikkaa asentaaksesi uuden version Statsista";
"Successfully updated" = "Päivitys onnistui";
"Stats was updated to v" = "Stats päivitettiin versioon v%0";
"Reset settings text" = "Kaikki sovelluksen asetukset nollataan, ja sovellus käynnistetään uudelleen. Haluatko varmasti tehdä tämän?";
"Support text" = "Kiitos, että käytät Statsia!\n\n Tämän avoimen lähdekoodin projektin ylläpitäminen ja parantaminen vie aikaa ja resursseja. Tukesi auttaa meitä jatkossakin tarjoamaan ilmaisen ja luotettavan sovelluksen kaikille.\n\nJos Stats on mielestäsi hyödyllinen, harkitse lahjoituksen tekemistä. Jokainen pieni osa auttaa!";
// Settings
"Open Activity Monitor" = "Avaa Aktiviteettimonitori";
"Report a bug" = "Ilmoita virheestä";
"Support the application" = "Tue sovellusta";
"Close application" = "Sulje sovellus";
"Open application settings" = "Avaa sovelluksen asetukset";
"Open dashboard" = "Avaa kojelauta";
"No notifications available in this module" = "Ei ilmoituksia tässä moduulissa";
"Open Calendar" = "Avaa kalenteri"; // translategemma:4b
"Toggle the module" = "Ota moduuli käyttöön/poista käytöstä"; // translategemma:4b
// Application settings
"Update application" = "Päivitä sovellus";
"Check for updates" = "Tarkista päivitykset";
"At start" = "Käynnistyksessä";
"Once per day" = "Kerran päivässä";
"Once per week" = "Kerran viikossa";
"Once per month" = "Kerran kuukaudessa";
"Never" = "Ei koskaan";
"Check for update" = "Tarkista päivitys";
"Show icon in dock" = "Näytä kuvake telakassa";
"Start at login" = "Käynnistä kirjautumisen yhteydessä";
"Build number" = "Kokoonpanon numero";
"Import settings" = "Tuo asetukset";
"Export settings" = "Vie asetukset";
"Reset settings" = "Nollaa asetukset";
"Pause the Stats" = "Keskeytä Stats";
"Resume the Stats" = "Jatka Statsin käyttöä";
"Combined modules" = "Yhdistetyt moduulit";
"Combined details" = "Yhdistetyt tiedot";
"Spacing" = "Rivien väli"; // translategemma:4b
"Share anonymous telemetry" = "Jaa anonyymi telemetria";
"Choose file" = "Valitse tiedosto"; // translategemma:4b
"Stress tests" = "Stressitestit"; // translategemma:4b
// Dashboard
"Serial number" = "Sarjanumero";
"Model identifier" = "Mallin tunniste";
"Production year" = "Valmistusvuosi";
"Uptime" = "Käyttöaika";
"Number of cores" = "%0 ydintä";
"Number of threads" = "%0 säiettä";
"Number of e-cores" = "%0 e-ydintä";
"Number of p-cores" = "%0 p-ydintä";
"Disks" = "Levyt"; // translategemma:4b
"Display" = "Näyttö"; // translategemma:4b
// Update
"The latest version of Stats installed" = "Stats on päivitetty viimeisimpään versioon";
"Downloading..." = "Ladataan...";
"Current version: " = "Nykyinen versio: ";
"Latest version: " = "Viimeisin versio: ";
// Widgets
"Color" = "Väri";
"Label" = "Tunniste";
"Box" = "Laatikko";
"Frame" = "Kehys";
"Value" = "Arvo";
"Colorize" = "Värjää";
"Colorize value" = "Värjää arvo";
"Additional information" = "Lisätiedot";
"Reverse values order" = "Käänteinen arvojärjestys";
"Base" = "Perusta";
"Display mode" = "Näyttötila";
"One row" = "Yksi rivi";
"Two rows" = "Kaksi riviä";
"Mini widget" = "Pieni"; // translategemma:4b
"Line chart widget" = "Viivakaavio";
"Bar chart widget" = "Pylväskaavio";
"Pie chart widget" = "Ympyräkaavio";
"Network chart widget" = "Verkkokaavio";
"Speed widget" = "Nopeus";
"Battery widget" = "Akku";
"Stack widget" = "Pino";
"Memory widget" = "Muisti";
"Static width" = "Staattinen leveys";
"Tachometer widget" = "Tachometri";
"State widget" = "Tila";
"Text widget" = "Tekstikomponentti"; // translategemma:4b
"Battery details widget" = "Akun tiedot -widget"; // translategemma:4b
"Show symbols" = "Näytä symbolit";
"Label widget" = "Tunniste";
"Number of reads in the chart" = "Lukukertojen määrä kaaviossa";
"Color of download" = "Latauksen väri";
"Color of upload" = "Lähetyksen väri";
"Monospaced font" = "Kiinteäleveyksinen fontti";
"Reverse order" = "Käänteinen järjestys";
"Chart history" = "Kaavion historia";
"Default color" = "Oletusväri";
"Transparent when no activity" = "Läpinäkyvä, kun ei ole toimintaa";
"Constant color" = "Vakio väri";
// Module Kit
"Open module settings" = "Avaa moduulin asetukset";
"Select widget" = "Valitse %0 widget";
"Open widget settings" = "Avaa widgetin asetukset";
"Update interval" = "Päivitysväli";
"Usage history" = "Käytön historia";
"Details" = "Tiedot";
"Top processes" = "Suosituimmat prosessit";
"Pictogram" = "Piktogrammi";
"Module" = "Moduuli";
"Widgets" = "Widgetit";
"Popup" = "Ponnahdusikkuna";
"Notifications" = "Ilmoitukset";
"Merge widgets" = "Yhdistä widgetit";
"No available widgets to configure" = "Ei muokattavia widgettejä";
"No options to configure for the popup in this module" = "Ei muokattavia asetuksia tälle moduulille";
"Process" = "Prosessi";
"Kill process" = "Lopeta prosessi";
"Keyboard shortcut" = "Näppäimistön yhdistelmä"; // translategemma:4b
"Listening..." = "Kuuntelen…"; // translategemma:4b
// Modules
"Number of top processes" = "Suosittujen prosessien määrä";
"Update interval for top processes" = "Suosittujen prosessien päivitysväli";
"Notification level" = "Ilmoitustaso";
"Chart color" = "Kaavion väri";
"Main chart scaling" = "Pääkaavion skaalaus";
"Scale value" = "Skaalausarvo";
"Text widget value" = "Tekstikomponentin arvo"; // translategemma:4b
// CPU
"CPU usage" = "Suorittimen käyttö";
"CPU temperature" = "Suorittimen lämpötila";
"CPU frequency" = "Suorittimen taajuus";
"System" = "Järjestelmä";
"User" = "Käyttäjä";
"Idle" = "Lepotila";
"Show usage per core" = "Näytä käyttö ytimittäin";
"Show hyper-threading cores" = "Näytä hyper-threading-ytimet";
"Split the value (System/User)" = "Jaa arvo (Järjestelmä/Käyttäjä)";
"Scheduler limit" = "Ajastimen raja";
"Speed limit" = "Nopeusraja";
"Average load" = "Keskimääräinen kuorma";
"1 minute" = "1 minuutti";
"5 minutes" = "5 minuuttia";
"15 minutes" = "15 minuuttia";
"CPU usage threshold" = "Suorittimen käytön raja";
"CPU usage is" = "Suorittimen käyttö on %0";
"Efficiency cores" = "Tehokkuusytimet";
"Performance cores" = "Suorituskykyytimet";
"System color" = "Järjestelmän väri";
"User color" = "Käyttäjän väri";
"Idle color" = "Lepotilan väri";
"Cluster grouping" = "Ryhmien klusterointi";
"Efficiency cores color" = "E-ytimien väri";
"Performance cores color" = "P-ytimien väri";
"Total load" = "Kokonaiskuorma";
"System load" = "Järjestelmäkuorma";
"User load" = "Käyttäjäkuorma";
"Efficiency cores load" = "Tehokkuusytimien kuorma";
"Performance cores load" = "Suorituskykyytimien kuorma";
"All cores" = "Kaikki ytimet"; // translategemma:4b
// GPU
"GPU to show" = "Näytettävä GPU";
"Show GPU type" = "Näytä GPU-tyyppi";
"GPU enabled" = "GPU käytössä";
"GPU disabled" = "GPU pois käytöstä";
"GPU temperature" = "GPU:n lämpötila";
"GPU utilization" = "GPU:n käyttö";
"Vendor" = "Valmistaja";
"Model" = "Malli";
"Status" = "Tila";
"Active" = "Aktiivinen";
"Non active" = "Ei aktiivinen";
"Fan speed" = "Tuuletinnopeus";
"Core clock" = "Ytimen kellotaajuus";
"Memory clock" = "Muistin kellotaajuus";
"Utilization" = "Käyttöaste";
"Render utilization" = "Renderöinnin käyttöaste";
"Tiler utilization" = "Tilerin käyttöaste";
"GPU usage threshold" = "GPU:n käytön raja";
"GPU usage is" = "GPU:n käyttö on %0";
// RAM
"Memory usage" = "Muistin käyttö";
"Memory pressure" = "Muistipaine";
"Total" = "Kokonaismäärä";
"Used" = "Käytetty";
"App" = "Sovellus";
"Wired" = "Johdettu";
"Compressed" = "Pakatut";
"Free" = "Vapaa";
"Swap" = "Välimuisti";
"Split the value (App/Wired/Compressed)" = "Jaa arvo (Sovellus/Johdettu/Pakatut)";
"RAM utilization threshold" = "RAMin käytön raja";
"RAM utilization is" = "RAMin käyttö on %0";
"App color" = "Sovelluksen väri";
"Wired color" = "Johdettujen väri";
"Compressed color" = "Pakattujen väri";
"Free color" = "Vapaan väri";
"Free memory (less than)" = "Vapaa muisti (alle)";
"Swap size" = "Välimuistin koko";
"Free RAM is" = "Vapaa RAM on %0";
// Disk
"Show removable disks" = "Näytä irrotettavat levyt";
"Used disk memory" = "%0 / %1 käytössä";
"Free disk memory" = "%0 / %1 vapaana";
"Disk to show" = "Näytettävä levy";
"Open disk" = "Avaa levy";
"Switch view" = "Vaihda näkymää";
"Disk utilization threshold" = "Levyn käytön raja";
"Disk utilization is" = "Levyn käyttö on %0";
"Read color" = "Luvun väri";
"Write color" = "Kirjoituksen väri";
"Disk usage" = "Levyn käyttö";
"Total read" = "Luettu yhteensä";
"Total written" = "Kirjoitettu yhteensä";
"Write speed" = "Kirjoitusnopeus";
"Read speed" = "Lukunopeus";
"Drives" = "Voimansiirrot"; // translategemma:4b
"SMART data" = "SMART-data"; // translategemma:4b
// Sensors
"Temperature unit" = "Lämpötilan yksikkö";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Tallenna tuulettimen nopeus";
"Fan" = "Tuuletin";
"HID sensors" = "HID-anturit";
"Synchronize fan's control" = "Synkronoi tuulettimen hallinta";
"Current" = "Virta";
"Energy" = "Energia";
"Show unknown sensors" = "Näytä tuntemattomat anturit";
"Install fan helper" = "Asenna tuuletinavustaja";
"Uninstall fan helper" = "Poista tuuletinavustaja";
"Fan value" = "Tuulettimen arvo";
"Turn off fan" = "Sammuta tuuletin";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Olet sammuttamassa tuuletinta. Tämä ei ole suositeltavaa ja voi vahingoittaa Maciasi. Haluatko varmasti tehdä tämän?";
"Sensor threshold" = "Anturin raja";
"Left fan" = "Vasen tuuletin";
"Right fan" = "Oikea tuuletin";
"Fastest fan" = "Nopein tuuletin";
"Sensor to show" = "Anturi, joka näyttää"; // translategemma:4b
// Network
"Uploading" = "Lähetys";
"Downloading" = "Lataus";
"Public IP" = "Julkinen IP";
"Local IP" = "Paikallinen IP";
"Interface" = "Liitäntä";
"Physical address" = "Fyysinen osoite";
"Refresh" = "Päivitä";
"Click to copy public IP address" = "Kopioi julkinen IP-osoite napsauttamalla";
"Click to copy local IP address" = "Kopioi paikallinen IP-osoite napsauttamalla";
"Click to copy wifi name" = "Kopioi Wi-Fi-nimi napsauttamalla";
"Click to copy mac address" = "Kopioi MAC-osoite napsauttamalla";
"No connection" = "Ei yhteyttä";
"Network interface" = "Verkkoliitäntä";
"Total download" = "Ladattu yhteensä";
"Total upload" = "Lähetetty yhteensä";
"Reader type" = "Lukijan tyyppi";
"Interface based" = "Liitäntäpohjainen";
"Processes based" = "Prosessipohjainen";
"Reset data usage" = "Nollaa tiedonsiirto";
"VPN mode" = "VPN-tila";
"Standard" = "Standardi";
"Security" = "Turvallisuus";
"Channel" = "Kanava";
"Common scale" = "Yleinen skaala";
"Autodetection" = "Automaattinen tunnistus";
"Widget activation threshold" = "Widgetin aktivoinnin raja";
"Internet connection" = "Internet-yhteys";
"Active state color" = "Aktiivitilan väri";
"Nonactive state color" = "Ei-aktiivitilan väri";
"Connectivity host (ICMP)" = "Yhteyden isäntä (ICMP)";
"Leave empty to disable the check" = "Jätä tyhjäksi poistaaksesi tarkistuksen käytöstä";
"Connectivity history" = "Yhteyshistoria";
"Auto-refresh public IP address" = "Päivitä julkinen IP-osoite automaattisesti";
"Every hour" = "Joka tunti";
"Every 12 hours" = "Joka 12. tunti";
"Every 24 hours" = "Joka 24. tunti";
"Network activity" = "Verkkoaktiivisuus";
"Last reset" = "Viimeksi nollattu %0 sitten";
"Latency" = "Viive";
"Upload speed" = "Lähetysnopeus";
"Download speed" = "Latausnopeus";
"Address" = "Osoite"; // translategemma:4b
"WiFi network" = "Wi-Fi-verkko"; // translategemma:4b
"Local IP changed" = "Paikallinen IP-osoite on muuttunut"; // translategemma:4b
"Public IP changed" = "Julkinen IP-osoite on muuttunut"; // translategemma:4b
"Previous IP" = "Edellinen IP-osoite: %0"; // translategemma:4b
"New IP" = "Uusi IP-osoite: %0"; // translategemma:4b
"Internet connection lost" = "Internet-yhteys katkennut"; // translategemma:4b
"Internet connection established" = "Internet-yhteys muodostettu"; // translategemma:4b
// Battery
"Level" = "Taso";
"Source" = "Lähde";
"AC Power" = "Verkkovirta";
"Battery Power" = "Akkuvirta";
"Time" = "Aika";
"Health" = "Kunto";
"Amperage" = "Ampeerimäärä";
"Voltage" = "Jännite";
"Cycles" = "Sykliä";
"Temperature" = "Lämpötila";
"Power adapter" = "Virtalähde";
"Power" = "Teho";
"Is charging" = "Lataa";
"Time to discharge" = "Purkamisaika";
"Time to charge" = "Latausaika";
"Calculating" = "Lasketaan";
"Fully charged" = "Täyteen ladattu";
"Not connected" = "Ei kytketty";
"Low level notification" = "Matala tason ilmoitus";
"High level notification" = "Korkea tason ilmoitus";
"Low battery" = "Akku Vähissä";
"High battery" = "Akun varaus korkea";
"Battery remaining" = "%0% jäljellä";
"Battery remaining to full charge" = "%0% täyteen lataukseen";
"Percentage" = "Prosentti";
"Percentage and time" = "Prosentti ja aika";
"Time and percentage" = "Aika ja prosentti";
"Time format" = "Ajan muoto";
"Hide additional information when full" = "Piilota lisätiedot, kun täynnä";
"Last charge" = "Viimeinen lataus";
"Capacity" = "Kapasiteetti";
"current / maximum / designed" = "nykyinen / maksimi / suunniteltu";
"Low power mode" = "Virransäästötila";
"Percentage inside the icon" = "Prosentti kuvakkeen sisällä";
"Colorize battery" = "Värjää akku";
"Charging current" = "Latausvirta";
"Charging Voltage" = "Latausjännite";
"Charger state inside the battery" = "Latauksen tila akun sisällä"; // translategemma:4b
// Bluetooth
"Battery to show" = "Näytettävä akku";
"No Bluetooth devices are available" = "Bluetooth-laitteita ei ole saatavilla";
// Clock
"Time zone" = "Aikavyöhyke";
"Local" = "Paikallinen";
"Calendar" = "Kalenteri";
"Show week numbers" = "Näytä viikonumerot"; // translategemma:4b
"Local time" = "Paikallinen aika";
"Add new clock" = "Lisää uusi kello"; // translategemma:4b
"Delete selected clock" = "Poista valittu kellonaika"; // translategemma:4b
"Help with datetime format" = "Apua päivämäärän ja ajan muotoilussa"; // translategemma:4b
// Colors
"Based on utilization" = "Perustuu käyttöasteeseen";
"Based on pressure" = "Perustuu paineeseen";
"Based on cluster" = "Perustuu klusteriin";
"System accent" = "Järjestelmän korostus";
"Monochrome accent" = "Yksivärinen korostus";
"Clear" = "Kirkas";
"White" = "Valkoinen";
"Black" = "Musta";
"Gray" = "Harmaa";
"Second gray" = "Toinen harmaa";
"Dark gray" = "Tumma harmaa";
"Light gray" = "Vaalea harmaa";
"Red" = "Punainen";
"Second red" = "Toinen punainen";
"Green" = "Vihreä";
"Second green" = "Toinen vihreä";
"Blue" = "Sininen";
"Second blue" = "Toinen sininen";
"Yellow" = "Keltainen";
"Second yellow" = "Toinen keltainen";
"Orange" = "Oranssi";
"Second orange" = "Toinen oranssi";
"Purple" = "Violetti";
"Second purple" = "Toinen violetti";
"Brown" = "Ruskea";
"Second brown" = "Toinen ruskea";
"Cyan" = "Syaani";
"Magenta" = "Magenta";
"Pink" = "Vaaleanpunainen";
"Teal" = "Sinivihreä";
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/fr.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "Processeur"; // translategemma:4b
"Open CPU settings" = "Ouvrir les paramètres du CPU";
"GPU" = "GPU";
"Open GPU settings" = "Ouvrir les paramètres du GPU";
"RAM" = "RAM";
"Open RAM settings" = "Ouvrir les paramètres de la RAM";
"Disk" = "Disque";
"Open Disk settings" = "Ouvrir les paramètres du disque";
"Sensors" = "Capteurs";
"Open Sensors settings" = "Ouvrir les paramètres des capteurs";
"Network" = "Réseau";
"Open Network settings" = "Ouvrir les paramètres réseau";
"Battery" = "Batterie";
"Open Battery settings" = "Ouvrir les paramètres de la batterie";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Ouvrir les paramètres Bluetooth";
"Clock" = "Horloge";
"Open Clock settings" = "Ouvrir les paramètres de l'horloge";
// Words
"Unknown" = "Inconnu";
"Version" = "Version";
"Processor" = "Processeur";
"Memory" = "Mémoire";
"Graphics" = "Graphiques";
"Close" = "Fermer";
"Download" = "Télécharger";
"Install" = "Installer";
"Cancel" = "Annuler";
"Unavailable" = "Indisponible";
"Yes" = "Oui";
"No" = "Non";
"Automatic" = "Automatique";
"Manual" = "Manuel";
"None" = "Aucun";
"Dots" = "Points";
"Arrows" = "Flèches";
"Characters" = "Caractères";
"Short" = "Court";
"Long" = "Long";
"Statistics" = "Statistiques";
"Max" = "Max";
"Min" = "Min";
"Reset" = "Réinitialiser";
"Alignment" = "Alignement";
"Left alignment" = "Gauche";
"Center alignment" = "Centré";
"Right alignment" = "Droite";
"Dashboard" = "Tableau de bord";
"Enabled" = "Activé";
"Disabled" = "Désactivé";
"Silent" = "Silencieux";
"Units" = "Unités";
"Fans" = "Ventilateurs";
"Scaling" = "Échelle";
"Linear" = "Linéaire";
"Square" = "Carré";
"Cube" = "Cube";
"Logarithmic" = "Logarithmique";
"Fixed scale" = "Fixe";
"Cores" = "Cœurs";
"Settings" = "Paramètres";
"Name" = "Nom";
"Format" = "Format";
"Turn off" = "Éteindre";
"Normal" = "Normal";
"Warning" = "Avertissement";
"Critical" = "Critique";
"Usage" = "Utilisation";
"2 minutes" = "2 minutes";
"3 minutes" = "3 minutes";
"10 minutes" = "10 minutes";
"Import" = "Importer"; // translategemma:4b
"Export" = "Exporter"; // translategemma:4b
"Separator" = "Séparateur"; // translategemma:4b
"Read" = "Lire"; // translategemma:4b
"Write" = "Écrire"; // translategemma:4b
"Frequency" = "Fréquence"; // translategemma:4b
"Save" = "Enregistrer"; // translategemma:4b
"Run" = "Exécuter"; // translategemma:4b
"Stop" = "Arrêtez"; // translategemma:4b
"Uninstall" = "Supprimer"; // translategemma:4b
"1 sec" = "1 seconde"; // translategemma:4b
"2 sec" = "2 sec"; // translategemma:4b
"3 sec" = "3 secondes"; // translategemma:4b
"5 sec" = "5 secondes"; // translategemma:4b
"10 sec" = "10 secondes"; // translategemma:4b
"15 sec" = "15 secondes"; // translategemma:4b
"30 sec" = "30 secondes"; // translategemma:4b
"60 sec" = "60 secondes"; // translategemma:4b
// Setup
"Stats Setup" = "Configuration de Stats";
"Previous" = "Précédent";
"Previous page" = "Page précédente";
"Next" = "Suivant";
"Next page" = "Page suivante";
"Finish" = "Terminer";
"Finish setup" = "Terminer la configuration";
"Welcome to Stats" = "Bienvenue sur Stats";
"welcome_message" = "Merci d'utiliser Stats, un moniteur système open source gratuit pour la barre de menus de macOS.";
"Start the application automatically when starting your Mac" = "Démarrer l'application automatiquement lors du démarrage de votre Mac";
"Do not start the application automatically when starting your Mac" = "Ne pas démarrer l'application automatiquement lors du démarrage de votre Mac";
"Do everything silently in the background (recommended)" = "Effectuer toutes les opérations discrètement en arrière-plan (recommandé)";
"Check for a new version on startup" = "Vérifier s'il existe une nouvelle version au démarrage";
"Check for a new version every day (once a day)" = "Vérifier s'il existe une nouvelle version tous les jours (une fois par jour)";
"Check for a new version every week (once a week)" = "Vérifier s'il existe une nouvelle version toutes les semaines (une fois par semaine)";
"Check for a new version every month (once a month)" = "Vérifier s'il existe une nouvelle version tous les mois (une fois par mois)";
"Never check for updates (not recommended)" = "Ne jamais vérifier les mises à jour (non recommandé)";
"Anonymous telemetry for better development decisions" = "Télémétrie anonyme pour de meilleures décisions en matière de développement";
"Share anonymous telemetry data" = "Partager les données télémétriques anonymes";
"Do not share anonymous telemetry data" = "Ne pas partager les données télémétriques anonymes";
"The configuration is completed" = "La configuration est terminée";
"finish_setup_message" = "Tout est prêt ! \n Stats est un outil open source, gratuit et le restera toujours. \n Si vous appréciez ce projet, vous pouvez le soutenir, cela sera toujours apprécié !";
// Alerts
"New version available" = "Nouvelle version disponible";
"Click to install the new version of Stats" = "Cliquez pour installer la nouvelle version de Stats";
"Successfully updated" = "Mise à jour effectuée avec succès";
"Stats was updated to v" = "Stats a été mis à jour en version %0";
"Reset settings text" = "Tous les paramètres de l'application seront réinitialisés et l'application redémarrera. Êtes-vous sûr de vouloir continuer ?";
"Support text" = "Merci d'utiliser Stats!\nLa maintenance et l'amélioration de ce projet open-source nécessitent du temps et des ressources. Votre soutien nous aide à continuer à fournir une application gratuite et fiable pour tout le monde.\nSi vous trouvez Stats utile, veuillez envisager de faire une contribution. Chaque petit geste compte !";
// Settings
"Open Activity Monitor" = "Ouvrir le Moniteur d'activité";
"Report a bug" = "Signaler un bug";
"Support the application" = "Soutenir l'application";
"Close application" = "Fermer l'application";
"Open application settings" = "Ouvrir les paramètres de l'application";
"Open dashboard" = "Ouvrir le tableau de bord";
"No notifications available in this module" = "Aucune modification disponible pour ce module";
"Open Calendar" = "Ouvrir le calendrier"; // translategemma:4b
"Toggle the module" = "Activer/désactiver le module"; // translategemma:4b
// Application settings
"Update application" = "Mettre à jour l'application";
"Check for updates" = "Vérifier les mises à jour";
"At start" = "Au démarrage";
"Once per day" = "Une fois par jour";
"Once per week" = "Une fois par semaine";
"Once per month" = "Une fois par mois";
"Never" = "Jamais";
"Check for update" = "Vérifier les mises à jour";
"Show icon in dock" = "Afficher l'icône dans le Dock";
"Start at login" = "Démarrer à l'ouverture de la session";
"Build number" = "Numéro de version";
"Import settings" = "Importer les paramètres";
"Export settings" = "Exporter les paramètres";
"Reset settings" = "Réinitialiser les paramètres";
"Pause the Stats" = "Mettre Stats en pause";
"Resume the Stats" = "Reprendre Stats";
"Combined modules" = "Modules combinés";
"Combined details" = "Détails combinés";
"Spacing" = "Espacement";
"Share anonymous telemetry" = "Partager des données télémétriques anonymes";
"Choose file" = "Sélectionner un fichier"; // translategemma:4b
"Stress tests" = "Tests de charge"; // translategemma:4b
// Dashboard
"Serial number" = "Numéro de série";
"Model identifier" = "Identifiant du modèle";
"Production year" = "Année de production";
"Uptime" = "Temps de fonctionnement";
"Number of cores" = "%0 cœurs";
"Number of threads" = "%0 threads";
"Number of e-cores" = "%0 cœurs à haute efficacité énergétique";
"Number of p-cores" = "%0 cœurs de performance";
"Disks" = "Disques"; // translategemma:4b
"Display" = "Affichage"; // translategemma:4b
// Update
"The latest version of Stats installed" = "La dernière version de Stats est installée";
"Downloading..." = "Téléchargement en cours...";
"Current version: " = "Version actuelle : ";
"Latest version: " = "Dernière version : ";
// Widgets
"Color" = "Couleur";
"Label" = "Étiquette";
"Box" = "Boîte";
"Frame" = "Cadre";
"Value" = "Valeur";
"Colorize" = "Coloriser";
"Colorize value" = "Coloriser la valeur";
"Additional information" = "Informations supplémentaires";
"Reverse values order" = "Inverser l'ordre des valeurs";
"Base" = "Base";
"Display mode" = "Mode d'affichage";
"One row" = "Une seule ligne";
"Two rows" = "Deux lignes";
"Mini widget" = "Mini";
"Line chart widget" = "Graphique en ligne";
"Bar chart widget" = "Graphique en barres";
"Pie chart widget" = "Graphique circulaire";
"Network chart widget" = "Graphique réseau";
"Speed widget" = "Vitesse";
"Battery widget" = "Batterie";
"Stack widget" = "Pile"; // translategemma:4b
"Memory widget" = "Mémoire";
"Static width" = "Largeur fixe";
"Tachometer widget" = "Tachymètre";
"State widget" = "Widget d'état";
"Text widget" = "Widget de texte"; // translategemma:4b
"Battery details widget" = "Widget pour afficher les détails de la batterie"; // translategemma:4b
"Show symbols" = "Afficher les symboles";
"Label widget" = "Étiquette";
"Number of reads in the chart" = "Nombre de lectures sur le graphique";
"Color of download" = "Couleur du téléchargement";
"Color of upload" = "Couleur du téléversement";
"Monospaced font" = "Police à espacement fixe";
"Reverse order" = "Inverser l'ordre";
"Chart history" = "Historique du graphique";
"Default color" = "Par défaut";
"Transparent when no activity" = "Transparent en l'absence d'activité";
"Constant color" = "Constante";
// Module Kit
"Open module settings" = "Ouvrir les paramètres du module";
"Select widget" = "Sélectionner le widget %0";
"Open widget settings" = "Ouvrir les paramètres du widget";
"Update interval" = "Intervalle de mise à jour";
"Usage history" = "Historique d'utilisation";
"Details" = "Détails";
"Top processes" = "Processus principaux";
"Pictogram" = "Pictogramme";
"Module" = "Module";
"Widgets" = "Widgets";
"Popup" = "Fenêtre contextuelle"; // translategemma:4b
"Notifications" = "Notifications";
"Merge widgets" = "Fusionner les widgets";
"No available widgets to configure" = "Aucun widget disponible à configurer";
"No options to configure for the popup in this module" = "Aucune option à configurer pour la fenêtre contextuelle dans ce module";
"Process" = "Processus";
"Kill process" = "Interrompre le processus";
"Keyboard shortcut" = "Raccourci clavier"; // translategemma:4b
"Listening..." = "En écoute..."; // translategemma:4b
// Modules
"Number of top processes" = "Nombre de processus principaux";
"Update interval for top processes" = "Intervalle de mise à jour pour les processus principaux";
"Notification level" = "Niveau de notification";
"Chart color" = "Couleur du graphique";
"Main chart scaling" = "Échelle de la graphique principale"; // translategemma:4b
"Scale value" = "Valeur de l'échelle";
"Text widget value" = "Valeur du widget de texte"; // translategemma:4b
// CPU
"CPU usage" = "Utilisation du CPU";
"CPU temperature" = "Température du CPU";
"CPU frequency" = "Fréquence du CPU";
"System" = "Système";
"User" = "Utilisateur";
"Idle" = "Inactif";
"Show usage per core" = "Afficher l'utilisation par cœur";
"Show hyper-threading cores" = "Afficher les cœurs avec hyper-threading";
"Split the value (System/User)" = "Diviser la valeur (Système/Utilisateur)";
"Scheduler limit" = "Limite de planification";
"Speed limit" = "Limite de vitesse";
"Average load" = "Charge moyenne";
"1 minute" = "1 minute";
"5 minutes" = "5 minutes";
"15 minutes" = "15 minutes";
"CPU usage threshold" = "Seuil d'utilisation du CPU";
"CPU usage is" = "L'utilisation du CPU est de %0";
"Efficiency cores" = "Cœurs à haute efficacité énergétique";
"Performance cores" = "Cœurs de performance";
"System color" = "Couleur du système";
"User color" = "Couleur de l'utilisateur";
"Idle color" = "Couleur de l'inactivité";
"Cluster grouping" = "Regroupement par cluster";
"Efficiency cores color" = "Couleur des cœurs à haute efficacité énergétique";
"Performance cores color" = "Couleur des cœurs de performance";
"Total load" = "Charge totale";
"System load" = "Charge système";
"User load" = "Charge utilisateur";
"Efficiency cores load" = "Charge des cœurs à haute efficacité énergétique";
"Performance cores load" = "Charge des cœurs de performance";
"All cores" = "Tous les cœurs"; // translategemma:4b
// GPU
"GPU to show" = "GPU à afficher";
"Show GPU type" = "Afficher le type de GPU";
"GPU enabled" = "GPU activé";
"GPU disabled" = "GPU désactivé";
"GPU temperature" = "Température du GPU";
"GPU utilization" = "Utilisation du GPU";
"Vendor" = "Fournisseur";
"Model" = "Modèle";
"Status" = "Statut";
"Active" = "Actif";
"Non active" = "Inactif";
"Fan speed" = "Vitesse du ventilateur";
"Core clock" = "Fréquence du cœur";
"Memory clock" = "Fréquence de la mémoire";
"Utilization" = "Utilisation";
"Render utilization" = "Utilisation du rendu";
"Tiler utilization" = "Utilisation du tiler";
"GPU usage threshold" = "Seuil d'utilisation du GPU";
"GPU usage is" = "L'utilisation du GPU est de %0";
// RAM
"Memory usage" = "Utilisation de la mémoire";
"Memory pressure" = "Pression mémoire";
"Total" = "Total";
"Used" = "Utilisée";
"App" = "Application";
"Wired" = "Câblée";
"Compressed" = "Compressée";
"Free" = "Inactive";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Diviser la valeur (Application/Câblée/Compressée)";
"RAM utilization threshold" = "Seuil d'utilisation de la RAM";
"RAM utilization is" = "L'utilisation de la RAM est de %0";
"App color" = "Couleur des applications";
"Wired color" = "Couleur de la mémoire câblée";
"Compressed color" = "Couleur de la mémoire compressée";
"Free color" = "Couleur de la mémoire inactive";
"Free memory (less than)" = "Mémoire disponible (moins de)";
"Swap size" = "Taille du swap";
"Free RAM is" = "La RAM disponible est %0";
// Disk
"Show removable disks" = "Afficher les disques amovibles";
"Used disk memory" = "Espace disque utilisé : %0 sur %1";
"Free disk memory" = "Espace disque libre : %0 sur %1";
"Disk to show" = "Disque à afficher";
"Open disk" = "Ouvrir le disque";
"Switch view" = "Changer de vue";
"Disk utilization threshold" = "Seuil d'utilisation du disque";
"Disk utilization is" = "L'utilisation du disque est de %0";
"Read color" = "Couleur de lecture";
"Write color" = "Couleur d'écriture";
"Disk usage" = "Utilisation du disque";
"Total read" = "Lectures totales";
"Total written" = "Écritures totales";
"Write speed" = "Écritures";
"Read speed" = "Lectures";
"Drives" = "Galets"; // translategemma:4b
"SMART data" = "Données SMART"; // translategemma:4b
// Sensors
"Temperature unit" = "Unité de température";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Enregistrer la vitesse du ventilateur";
"Fan" = "Ventilateur";
"HID sensors" = "Capteurs HID";
"Synchronize fan's control" = "Synchroniser le contrôle des ventilateurs";
"Current" = "Courant";
"Energy" = "Énergie";
"Show unknown sensors" = "Afficher les capteurs inconnus";
"Install fan helper" = "Installer l'assistant des ventilateurs";
"Uninstall fan helper" = "Désinstaller l'assistant des ventilateurs";
"Fan value" = "Vitesse du ventilateur";
"Turn off fan" = "Éteindre les ventilateurs";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Vous vous apprêtez à éteindre les ventilateurs. Cette action n'est pas recommandée car elle peut endommager votre mac, êtes-vous sûr de vouloir faire cela ?";
"Sensor threshold" = "Seuil du capteur";
"Left fan" = "Gauche";
"Right fan" = "Droite";
"Fastest fan" = "Le plus rapide";
"Sensor to show" = "Afficher la valeur du capteur"; // translategemma:4b
// Network
"Uploading" = "Envoi";
"Downloading" = "Téléchargement";
"Public IP" = "IP publique";
"Local IP" = "IP locale";
"Interface" = "Interface";
"Physical address" = "Adresse physique";
"Refresh" = "Actualiser";
"Click to copy public IP address" = "Cliquez pour copier l'adresse IP publique";
"Click to copy local IP address" = "Cliquez pour copier l'adresse IP locale";
"Click to copy wifi name" = "Cliquez pour copier le nom du réseau Wi-Fi";
"Click to copy mac address" = "Cliquez pour copier l'adresse MAC";
"No connection" = "Aucune connexion";
"Network interface" = "Interface réseau";
"Total download" = "Total des téléchargements";
"Total upload" = "Total des envois";
"Reader type" = "Type de lecteur";
"Interface based" = "Basé sur l'interface";
"Processes based" = "Basé sur les processus";
"Reset data usage" = "Réinitialiser l'utilisation des données";
"VPN mode" = "Mode VPN";
"Standard" = "Norme"; // translategemma:4b
"Security" = "Sécurité";
"Channel" = "Canal";
"Common scale" = "Échelle commune";
"Autodetection" = "Détection automatique";
"Widget activation threshold" = "Seuil d'activation du widget";
"Internet connection" = "Connexion Internet";
"Active state color" = "Couleur de l'état actif";
"Nonactive state color" = "Couleur de l'état inactif";
"Connectivity host (ICMP)" = "Hôte de connectivité (ICMP)";
"Leave empty to disable the check" = "Laissez vide pour désactiver la vérification";
"Connectivity history" = "Historique de la connectivité";
"Auto-refresh public IP address" = "Actualiser automatiquement l'adresse IP publique";
"Every hour" = "Toutes les heures";
"Every 12 hours" = "Toutes les 12 heures";
"Every 24 hours" = "Toutes les 24 heures";
"Network activity" = "L'activité réseau";
"Last reset" = "Dernière réinitialisation il y a %0";
"Latency" = "Latence";
"Upload speed" = "Envoi";
"Download speed" = "Téléchargement";
"Address" = "Adresse"; // translategemma:4b
"WiFi network" = "Réseau WiFi"; // translategemma:4b
"Local IP changed" = "L'adresse IP locale a changé."; // translategemma:4b
"Public IP changed" = "L'adresse IP publique a été modifiée."; // translategemma:4b
"Previous IP" = "Adresse IP précédente : %0"; // translategemma:4b
"New IP" = "Nouvelle adresse IP : %0"; // translategemma:4b
"Internet connection lost" = "Perte de connexion Internet"; // translategemma:4b
"Internet connection established" = "Connexion Internet établie"; // translategemma:4b
// Battery
"Level" = "Niveau";
"Source" = "Source";
"AC Power" = "Alimentation secteur";
"Battery Power" = "Alimentation batterie";
"Time" = "Temps";
"Health" = "Santé";
"Amperage" = "Intensité";
"Voltage" = "Tension";
"Cycles" = "Nombre de cycles";
"Temperature" = "Température";
"Power adapter" = "Adaptateur secteur";
"Power" = "Courant";
"Is charging" = "En charge";
"Time to discharge" = "Temps restant";
"Time to charge" = "Temps de charge";
"Calculating" = "Calcul en cours";
"Fully charged" = "Complètement chargée";
"Not connected" = "Non connecté";
"Low level notification" = "Notification de niveau faible";
"High level notification" = "Notification de niveau élevé";
"Low battery" = "Batterie faible";
"High battery" = "Batterie élevée";
"Battery remaining" = "%0% restant";
"Battery remaining to full charge" = "%0% jusqu'à charge complète";
"Percentage" = "Pourcentage";
"Percentage and time" = "Pourcentage et temps";
"Time and percentage" = "Temps et pourcentage";
"Time format" = "Format de l'heure";
"Hide additional information when full" = "Masquer les informations supplémentaires lorsque la batterie est pleine";
"Last charge" = "Dernière charge";
"Capacity" = "Capacité";
"current / maximum / designed" = "actuelle / maximale / prévue";
"Low power mode" = "Mode d'économie d'énergie";
"Percentage inside the icon" = "Pourcentage à l'intérieur de l'icône";
"Colorize battery" = "Colorer la batterie";
"Charging current" = "Courant de charge";
"Charging Voltage" = "Tension de charge";
"Charger state inside the battery" = "État de charge à l'intérieur de la batterie"; // translategemma:4b
// Bluetooth
"Battery to show" = "Batterie à afficher";
"No Bluetooth devices are available" = "Aucun appareil Bluetooth n'est disponible";
// Clock
"Time zone" = "Fuseau horaire";
"Local" = "Local";
"Calendar" = "Calendrier";
"Show week numbers" = "Afficher les numéros de semaine"; // translategemma:4b
"Local time" = "Heure locale";
"Add new clock" = "Ajouter une nouvelle horloge"; // translategemma:4b
"Delete selected clock" = "Supprimer l'horloge sélectionnée"; // translategemma:4b
"Help with datetime format" = "Aide pour le format de date et d'heure"; // translategemma:4b
// Colors
"Based on utilization" = "Basé sur l'utilisation";
"Based on pressure" = "Basé sur la pression";
"Based on cluster" = "Basé sur le cluster";
"System accent" = "Couleur d'accentuation du système";
"Monochrome accent" = "Accentuation monochrome";
"Clear" = "Transparent";
"White" = "Blanc";
"Black" = "Noir";
"Gray" = "Gris";
"Second gray" = "Deuxième gris";
"Dark gray" = "Gris foncé";
"Light gray" = "Gris clair";
"Red" = "Rouge";
"Second red" = "Deuxième rouge";
"Green" = "Vert";
"Second green" = "Deuxième vert";
"Blue" = "Bleu";
"Second blue" = "Deuxième bleu";
"Yellow" = "Jaune";
"Second yellow" = "Deuxième jaune";
"Orange" = "Orange";
"Second orange" = "Deuxième orange";
"Purple" = "Violet";
"Second purple" = "Deuxième violet";
"Brown" = "Brun";
"Second brown" = "Deuxième brun";
"Cyan" = "Cyan";
"Magenta" = "Magenta";
"Pink" = "Rose";
"Teal" = "Sarcelle";
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/he.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "מעבד";
"Open CPU settings" = "פתח/י הגדרות מעבד";
"GPU" = "כרטיס גרפי"; // translategemma:4b
"Open GPU settings" = "פתח/י הגדרות גרפיות";
"RAM" = "זכרון";
"Open RAM settings" = "פתח/י הגדרות זכרון";
"Disk" = "דיסק";
"Open Disk settings" = "פתח/י הגדרות דיסק";
"Sensors" = "חיישנים";
"Open Sensors settings" = "פתח/י הגדרות חיישנים";
"Network" = "רשת";
"Open Network settings" = "פתח/י הגדרות רשת";
"Battery" = "בטרייה";
"Open Battery settings" = "פתח/י הגדרות בטרייה";
"Bluetooth" = "בלוטות׳";
"Open Bluetooth settings" = "פתח/י הגדרות בלוטות׳";
"Clock" = "שעון"; // translategemma:4b
"Open Clock settings" = "פתח הגדרות שעון"; // translategemma:4b
// Words
"Unknown" = "לא ידוע";
"Version" = "גרסא";
"Processor" = "מעבד";
"Memory" = "זכרון";
"Graphics" = "גרפיקה";
"Close" = "סגירה";
"Download" = "הורדה";
"Install" = "התקנה";
"Cancel" = "בטל";
"Unavailable" = "לא זמין";
"Yes" = "כן";
"No" = "לא";
"Automatic" = "אוטומטי";
"Manual" = "ידני";
"None" = "כלום";
"Dots" = "נקודות";
"Arrows" = "חצים";
"Characters" = "תווים";
"Short" = "קצר";
"Long" = "ארוך";
"Statistics" = "סטטיסטיקה";
"Max" = "מקסימום";
"Min" = "מינימום";
"Reset" = "ריסט";
"Alignment" = "יישור";
"Left alignment" = "שמאל";
"Center alignment" = "מרכז";
"Right alignment" = "ימין";
"Dashboard" = "דאשבורד";
"Enabled" = "הופעל"; // translategemma:4b
"Disabled" = "חסימה";
"Silent" = "שקט";
"Units" = "יחידות";
"Fans" = "מאווררים";
"Scaling" = "הגדלה"; // translategemma:4b
"Linear" = "ליניארי"; // translategemma:4b
"Square" = "ריבוע"; // translategemma:4b
"Cube" = "קוביה"; // translategemma:4b
"Logarithmic" = "לוגריתמי"; // translategemma:4b
"Fixed scale" = "תוקן"; // translategemma:4b
"Cores" = "ליבות"; // translategemma:4b
"Settings" = "הגדרות"; // translategemma:4b
"Name" = "שם"; // translategemma:4b
"Format" = "פורמט"; // translategemma:4b
"Turn off" = "כבה"; // translategemma:4b
"Normal" = "נורמלי"; // translategemma:4b
"Warning" = "אזהרה"; // translategemma:4b
"Critical" = "קריטי"; // translategemma:4b
"Usage" = "שימוש"; // translategemma:4b
"2 minutes" = "2 דקות"; // translategemma:4b
"3 minutes" = "3 דקות"; // translategemma:4b
"10 minutes" = "10 דקות"; // translategemma:4b
"Import" = "ייבוא"; // translategemma:4b
"Export" = "ייצוא"; // translategemma:4b
"Separator" = "מפריד"; // translategemma:4b
"Read" = "קרא"; // translategemma:4b
"Write" = "כתוב"; // translategemma:4b
"Frequency" = "תדירות"; // translategemma:4b
"Save" = "שמור"; // translategemma:4b
"Run" = "הפעל"; // translategemma:4b
"Stop" = "עצור"; // translategemma:4b
"Uninstall" = "הסר התקנה"; // translategemma:4b
"1 sec" = "1 שניה"; // translategemma:4b
"2 sec" = "2 שניות"; // translategemma:4b
"3 sec" = "3 שניות"; // translategemma:4b
"5 sec" = "5 שניות"; // translategemma:4b
"10 sec" = "10 שניות"; // translategemma:4b
"15 sec" = "15 שניות"; // translategemma:4b
"30 sec" = "30 שניות"; // translategemma:4b
"60 sec" = "60 שניות"; // translategemma:4b
// Setup
"Stats Setup" = "הגדרת סטטיסטיקות"; // translategemma:4b
"Previous" = "הקודם"; // translategemma:4b
"Previous page" = "עמוד קודם"; // translategemma:4b
"Next" = "הבא"; // translategemma:4b
"Next page" = "עמוד הבא"; // translategemma:4b
"Finish" = "סיום"; // translategemma:4b
"Finish setup" = "לסיים את ההגדרה"; // translategemma:4b
"Welcome to Stats" = "ברוכים הבאים ל-Stats"; // translategemma:4b
"welcome_message" = "תודה על השימוש ב-Stats, תוכנת ניטור מערכת macOS חינמית וקוד פתוח, עבור שורת המשימות שלכם."; // translategemma:4b
"Start the application automatically when starting your Mac" = "הפעל את האפליקציה באופן אוטומטי בעת הפעלת ה-Mac שלך."; // translategemma:4b
"Do not start the application automatically when starting your Mac" = "אל תפעילו את היישום באופן אוטומטי בעת הפעלת המחשב."; // translategemma:4b
"Do everything silently in the background (recommended)" = "בצעו את כל הפעולות בצורה שקטה ברקע (מומלץ)"; // translategemma:4b
"Check for a new version on startup" = "בדוק אם יש גרסה חדשה בעת ההפעלה."; // translategemma:4b
"Check for a new version every day (once a day)" = "בדוק אם יש גרסה חדשה מדי יום (פעם ביום)"; // translategemma:4b
"Check for a new version every week (once a week)" = "בדקו גרסה חדשה מדי שבוע (פעם בשבוע)"; // translategemma:4b
"Check for a new version every month (once a month)" = "בדקו גרסה חדשה מדי חודש (פעם בחודש)"; // translategemma:4b
"Never check for updates (not recommended)" = "לעולם אל לבד עדכונים (לא מומלץ)"; // translategemma:4b
"Anonymous telemetry for better development decisions" = "נתונים אנונימיים לניתוח, לקבלת החלטות פיתוח טובות יותר"; // translategemma:4b
"Share anonymous telemetry data" = "לשתף נתונים אנונימיים של ניטור"; // translategemma:4b
"Do not share anonymous telemetry data" = "אין לשתף נתונים אנונימיים של ניטור"; // translategemma:4b
"The configuration is completed" = "ההגדרה הושלמה"; // translategemma:4b
"finish_setup_message" = "הכל מוכן!"; // translategemma:4b
// Alerts
"New version available" = "גרסא חדשה זמינה";
"Click to install the new version of Stats" = "Stats לחץ כדי להתקין גרסא חדשה של";
"Successfully updated" = "עודכן בהצלחה";
"Stats was updated to v" = "v%0עודכן ל Stats";
"Reset settings text" = "?כל הגדרות התוכנה יתאפסו והתוכנה תופעל מחדש. האם אתם בטוחים שאתם רוצים לעשות את זה";
"Support text" = "תודה על השימוש בסטטיסטיקה!\n\n תחזוקה ושיפור של פרויקט קוד פתוח זה דורש זמן ומשאבים. התמיכה שלך עוזרת לנו להמשיך לספק אפליקציה חינמית ומהימנה לכולם.\n\nאם אתה מוצא שסטטיסטיקות מועילות, שקול לתרום. כל טיפה עוזרת!";
// Settings
"Open Activity Monitor" = "פתח/י את מעקב הפעילות";
"Report a bug" = "דווחו על באג";
"Support the application" = "תתמכו באפליקציה";
"Close application" = "סגירת אפליקציה";
"Open application settings" = "פתיחת הגדרות האפליקציה";
"Open dashboard" = "פתיחת דאשבורד";
"No notifications available in this module" = "אין התראות זמינות במודול זה"; // translategemma:4b
"Open Calendar" = "פתח קלנדר"; // translategemma:4b
"Toggle the module" = "הפעל/השבת את המודול"; // translategemma:4b
// Application settings
"Update application" = "עדכון אפליקציה";
"Check for updates" = "בדוק עדכונים";
"At start" = "בעלייה";
"Once per day" = "בכל יום";
"Once per week" = "בכל שבוע";
"Once per month" = "בכל חודש";
"Never" = "אף פעם";
"Check for update" = "בדיקת עדכון";
"Show icon in dock" = "הצג סמל בשורת התפריטים";
"Start at login" = "התחל בעלייה";
"Build number" = "מספר בילד";
"Import settings" = "הגדרות ייבוא"; // translategemma:4b
"Export settings" = "הגדרות ייצוא"; // translategemma:4b
"Reset settings" = "איפוס הגדרות";
"Pause the Stats" = "השהייה של הסטטיסטיקות"; // translategemma:4b
"Resume the Stats" = "חזור לנתונים"; // translategemma:4b
"Combined modules" = "מודולים משולבים"; // translategemma:4b
"Combined details" = "פרטים משולבים"; // translategemma:4b
"Spacing" = "הפרדה"; // translategemma:4b
"Share anonymous telemetry" = "שיתוף נתונים אנונימיים"; // translategemma:4b
"Choose file" = "בחר קובץ"; // translategemma:4b
"Stress tests" = "בדיקות עומס"; // translategemma:4b
// Dashboard
"Serial number" = "מספר סריאלי";
"Model identifier" = "מזהה המודל"; // translategemma:4b
"Production year" = "שנת ייצור"; // translategemma:4b
"Uptime" = "משך הזמן ללא השבתה";
"Number of cores" = "%0 ליבות";
"Number of threads" = "%0 טרדים";
"Number of e-cores" = "%0 ליבות ביצוע"; // translategemma:4b
"Number of p-cores" = "%0 ליבות ביצוע"; // translategemma:4b
"Disks" = "כוננים"; // translategemma:4b
"Display" = "תצוגה"; // translategemma:4b
// Update
"The latest version of Stats installed" = "מותקנת Stats הגרסא האחרונה של";
"Downloading..." = "...מוריד";
"Current version: " = " :גרסא נוכחית";
"Latest version: " = " :גרסא אחרונה";
// Widgets
"Color" = "צבע";
"Label" = "תווית";
"Box" = "קופסא";
"Frame" = "מסגרת";
"Value" = "ערך";
"Colorize" = "הוספת צבע";
"Colorize value" = "הוספת ערך לצבע";
"Additional information" = "מידע נוסף";
"Reverse values order" = "היפוך סדר הערכים";
"Base" = "בסיס";
"Display mode" = "מצב צפייה";
"One row" = "שורה אחת";
"Two rows" = "שתי שורות";
"Mini widget" = "מיני";
"Line chart widget" = "מגמת שורות";
"Bar chart widget" = "טבלת עמודות";
"Pie chart widget" = "טבלת עוגה";
"Network chart widget" = "טבלת רשת";
"Speed widget" = "מהירות";
"Battery widget" = "בטרייה";
"Stack widget" = "מחסן"; // translategemma:4b
"Memory widget" = "זכרון";
"Static width" = "אורך סטאטי";
"Tachometer widget" = "טכומטר";
"State widget" = "רכיב ממשלתי"; // translategemma:4b
"Text widget" = "רכיב טקסט"; // translategemma:4b
"Battery details widget" = "מדד פרטי סוללה"; // translategemma:4b
"Show symbols" = "הראה סמלים";
"Label widget" = "תווית";
"Number of reads in the chart" = "מספר קריאות במגמה";
"Color of download" = "צבע של הורדה";
"Color of upload" = "צבע של העלאה";
"Monospaced font" = "גופן בעל רווחים אחידים"; // translategemma:4b
"Reverse order" = "סדר הפוך"; // translategemma:4b
"Chart history" = "היסטוריית גרפים"; // translategemma:4b
"Default color" = "ברירת מחדל"; // translategemma:4b
"Transparent when no activity" = "שקוף כאשר אין פעילות"; // translategemma:4b
"Constant color" = "קבוע"; // translategemma:4b
// Module Kit
"Open module settings" = "פתח/י הגדרות מודל";
"Select widget" = "סמן %0 יישומון";
"Open widget settings" = "פתח/י הגדרות יישומון";
"Update interval" = "מרווח עדכון";
"Usage history" = "היסטוריית שימוש";
"Details" = "פרטים";
"Top processes" = "תהליכים מובילים";
"Pictogram" = "פיקטוגרמה";
"Module" = "מודול";
"Widgets" = "ווידג'טים";
"Popup" = "קופץ";
"Notifications" = "התראות";
"Merge widgets" = "מיזוג ווידג'טים";
"No available widgets to configure" = "אין ווידג'טים זמינים להגדרה";
"No options to configure for the popup in this module" = "אין אפשרויות להגדיר עבור החלון הקופץ במודול זה";
"Process" = "תהליך"; // translategemma:4b
"Kill process" = "הרס תהליך"; // translategemma:4b
"Keyboard shortcut" = "קיצור מקלדת"; // translategemma:4b
"Listening..." = "האזנה..."; // translategemma:4b
// Modules
"Number of top processes" = "מספר התהליכים המובילים";
"Update interval for top processes" = "מרווח עדכון עבור תהליכים מובילים";
"Notification level" = "רמת התראה";
"Chart color" = "צבע תרשים";
"Main chart scaling" = "הגדרת סולם הגרף הראשי"; // translategemma:4b
"Scale value" = "ערך סולם"; // translategemma:4b
"Text widget value" = "ערך של רכיב טקסט"; // translategemma:4b
// CPU
"CPU usage" = "שימוש המעבד";
"CPU temperature" = "טמפרטורת המעבד";
"CPU frequency" = "תדירות המעבד";
"System" = "מערכת";
"User" = "משתמש";
"Idle" = "מושהה";
"Show usage per core" = "הצג שימוש לכל ליבה";
"Show hyper-threading cores" = "הצג ליבות היפר-טרדינג";
"Split the value (System/User)" = "(מערכת/משתמש)פצלו את הערך";
"Scheduler limit" = "הגבלת תזמון";
"Speed limit" = "הגבלת מהירות";
"Average load" = "עומס ממוצע";
"1 minute" = "דקה";
"5 minutes" = "5 דקות";
"15 minutes" = "דקות 15";
"CPU usage threshold" = "סף השימוש במעבד"; // translategemma:4b
"CPU usage is" = "שימוש במעבד הוא `%0"; // translategemma:4b
"Efficiency cores" = "ליבות ביצועים"; // translategemma:4b
"Performance cores" = "ליבות ביצוע"; // translategemma:4b
"System color" = "צבע מערכת"; // translategemma:4b
"User color" = "צבע המשתמש"; // translategemma:4b
"Idle color" = "צבע כשאין פעילות"; // translategemma:4b
"Cluster grouping" = "קיבוץ"; // translategemma:4b
"Efficiency cores color" = "ליבות יעילות בצבע"; // translategemma:4b
"Performance cores color" = "צבע ליבות הביצועים"; // translategemma:4b
"Total load" = "סה\"כ עומס"; // translategemma:4b
"System load" = "עומס מערכת"; // translategemma:4b
"User load" = "עומס משתמש"; // translategemma:4b
"Efficiency cores load" = "טעינת ליבות ביצוע"; // translategemma:4b
"Performance cores load" = "ליבות הביצוע מתחילות לעבוד"; // translategemma:4b
"All cores" = "כל הליבות"; // translategemma:4b
// GPU
"GPU to show" = "להראות GPU";
"Show GPU type" = "GPUהראה את סוג ה";
"GPU enabled" = "מופעל GPU";
"GPU disabled" = "מושבת GPU";
"GPU temperature" = "GPU טמפרטורה של";
"GPU utilization" = "GPU ניצול";
"Vendor" = "ספק";
"Model" = "דגם";
"Status" = "מצב";
"Active" = "פעיל";
"Non active" = "לא פעיל";
"Fan speed" = "מהירות מאורר";
"Core clock" = "שעון ליבה";
"Memory clock" = "שעון זיכרון";
"Utilization" = "ניצול";
"Render utilization" = "ניצול רנדור";
"Tiler utilization" = "ניצול ריצוף";
"GPU usage threshold" = "סף השימוש בכרטיס הגרפי"; // translategemma:4b
"GPU usage is" = "שימוש בכרטיס גרפי: %0"; // translategemma:4b
// RAM
"Memory usage" = "שימוש בזכרון";
"Memory pressure" = "לחץ בזיכרון";
"Total" = "כולל";
"Used" = "בשימוש";
"App" = "אפליקציה";
"Wired" = "חוטי";
"Compressed" = "מכווץ";
"Free" = "משוחרר";
"Swap" = "החלפה";
"Split the value (App/Wired/Compressed)" = "(אפליקציה/חוטי/מכווץ) פצל/י את הערך";
"RAM utilization threshold" = "סף השימוש בזיכרון RAM"; // translategemma:4b
"RAM utilization is" = "שימוש בזיכרון RAM הוא `%0"; // translategemma:4b
"App color" = "צבע האפליקציה"; // translategemma:4b
"Wired color" = "צבע מקושר"; // translategemma:4b
"Compressed color" = "צבע דחוס"; // translategemma:4b
"Free color" = "צבע בחינם"; // translategemma:4b
"Free memory (less than)" = "זיכרון פנוי (פחות מ)"; // translategemma:4b
"Swap size" = "גודל זיכרון וירטואלי"; // translategemma:4b
"Free RAM is" = "זיכרון RAM פנוי הוא %0"; // translategemma:4b
// Disk
"Show removable disks" = "הראה דיסקים הניתנים להסרה";
"Used disk memory" = "0% מתוך %1 בשימוש";
"Free disk memory" = "0% מתוך %1 פנויים";
"Disk to show" = "דיסק להראות";
"Open disk" = "פתח/י דיסק";
"Switch view" = "החלף תצוגה";
"Disk utilization threshold" = "סף השימוש בדיסק"; // translategemma:4b
"Disk utilization is" = "שימוש בדיסק הוא %0"; // translategemma:4b
"Read color" = "קרא צבע"; // translategemma:4b
"Write color" = "כתוב בצבע"; // translategemma:4b
"Disk usage" = "שימוש בדיסק"; // translategemma:4b
"Total read" = "סך הכל קראו"; // translategemma:4b
"Total written" = "סה\"כ כתוב"; // translategemma:4b
"Write speed" = "כתוב"; // translategemma:4b
"Read speed" = "קרא"; // translategemma:4b
"Drives" = "דיסקים"; // translategemma:4b
"SMART data" = "נתונים חכמים"; // translategemma:4b
// Sensors
"Temperature unit" = "יחידת טמפרטורה";
"Celsius" = "צלסיוס";
"Fahrenheit" = "פרנהייט";
"Save the fan speed" = "שמירת מהירות המאורר";
"Fan" = "מאור"; // translategemma:4b
"HID sensors" = "HID חיישני";
"Synchronize fan's control" = "סנכרון שליטה במאוורר"; // translategemma:4b
"Current" = "הזמין הנוכחי"; // translategemma:4b
"Energy" = "אנרגיה"; // translategemma:4b
"Show unknown sensors" = "הצגת חיישנים לא ידועים"; // translategemma:4b
"Install fan helper" = "התקן את תוכנת העזרה לפעולת המאוורר"; // translategemma:4b
"Uninstall fan helper" = "הסר התקנה של תוכנת עזר למאוורר"; // translategemma:4b
"Fan value" = "ערך הצריכה"; // translategemma:4b
"Turn off fan" = "כבה את המאוורר"; // translategemma:4b
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "אתם עומדים לכבות את המאוורר. פעולה זו אינה מומלצת ויכולה לגרום נזק למחשב שלכם. האם אתם בטוחים שאתם רוצים לעשות זאת?"; // translategemma:4b
"Sensor threshold" = "סף חיישן"; // translategemma:4b
"Left fan" = "שמאל"; // translategemma:4b
"Right fan" = "נכון"; // translategemma:4b
"Fastest fan" = "המהיר ביותר"; // translategemma:4b
"Sensor to show" = "חיישן להצגה"; // translategemma:4b
// Network
"Uploading" = "העלאה";
"Downloading" = "הורדה";
"Public IP" = "כתובת רשת ציבורית";
"Local IP" = "כתובת רשת מקומית";
"Interface" = "ממשק";
"Physical address" = "כתובת פיזית";
"Refresh" = "רענן";
"Click to copy public IP address" = "לחץ כדי להעתיק כתובת רשת ציבורית";
"Click to copy local IP address" = "לחץ כדי להעתיק כתובת רשת מקומית";
"Click to copy wifi name" = "לחץ כדי להעתיק את שם האינטרנט האלחוטי";
"Click to copy mac address" = "MACלחץ כדי להעתיק את כתובת ה";
"No connection" = "אין חיבור";
"Network interface" = "ממשק רשת";
"Total download" = "הורדה כוללת";
"Total upload" = "העלאה כוללת";
"Reader type" = "מצב קריאה";
"Interface based" = "מבוסס ממשק";
"Processes based" = "מבוסס מעבד";
"Reset data usage" = "אפס את השימוש בנתונים";
"VPN mode" = "VPN מצב";
"Standard" = "סטנדרטי"; // translategemma:4b
"Security" = "אבטחה"; // translategemma:4b
"Channel" = "ערוץ"; // translategemma:4b
"Common scale" = "סולם סטנדרטי"; // translategemma:4b
"Autodetection" = "זיהוי אוטומטי"; // translategemma:4b
"Widget activation threshold" = "סף הפעלה של ווידג"; // translategemma:4b
"Internet connection" = "חיבור לאינטרנט"; // translategemma:4b
"Active state color" = "צבע המצב הפעיל"; // translategemma:4b
"Nonactive state color" = "צבע למצב לא פעיל"; // translategemma:4b
"Connectivity host (ICMP)" = "שרת קישוריות (ICMP)"; // translategemma:4b
"Leave empty to disable the check" = "השאירו ריק כדי לבטל את הבדיקה"; // translategemma:4b
"Connectivity history" = "היסטוריית חיבורים"; // translategemma:4b
"Auto-refresh public IP address" = "עדכון אוטומטי של כתובת ה-IP הציבורית"; // translategemma:4b
"Every hour" = "בכל שעה"; // translategemma:4b
"Every 12 hours" = "כל 12 שעות"; // translategemma:4b
"Every 24 hours" = "בכל 24 שעות"; // translategemma:4b
"Network activity" = "פעילות רשת"; // translategemma:4b
"Last reset" = "האיחוד האחרון בוצע לפני %0 ימים"; // translategemma:4b
"Latency" = "עיכוב"; // translategemma:4b
"Upload speed" = "העלה"; // translategemma:4b
"Download speed" = "הורדה"; // translategemma:4b
"Address" = "כתובת"; // translategemma:4b
"WiFi network" = "רשת Wi-Fi"; // translategemma:4b
"Local IP changed" = "כתובת ה-IP המקומית השתנתה"; // translategemma:4b
"Public IP changed" = "כתובת ה-IP הציבורית השתנתה"; // translategemma:4b
"Previous IP" = "כתובת IP קודמת: %0"; // translategemma:4b
"New IP" = "כתובת IP חדשה: %0"; // translategemma:4b
"Internet connection lost" = "ניתק התחברות לאינטרנט"; // translategemma:4b
"Internet connection established" = "חיבור לאינטרנט הושלם"; // translategemma:4b
// Battery
"Level" = "רמה";
"Source" = "מקור";
"AC Power" = "מטען";
"Battery Power" = "הספק בטרייה";
"Time" = "זמן";
"Health" = "בריאות";
"Amperage" = "זרם";
"Voltage" = "מתח";
"Cycles" = "מחזורים";
"Temperature" = "טמפרטורה";
"Power adapter" = "מטען";
"Power" = "מתח";
"Is charging" = "מטעין";
"Time to discharge" = "זמן לפריקה";
"Time to charge" = "זמן להטענה";
"Calculating" = "מחשב";
"Fully charged" = "טעון במלואה";
"Not connected" = "לא מחובר";
"Low level notification" = "התראה ברמה נמוכה";
"High level notification" = "התראה ברמה גבוהה";
"Low battery" = "בטרייה נמוכנה";
"High battery" = "בטרייה גבוהה";
"Battery remaining" = "נשאר %0%";
"Battery remaining to full charge" = "לטעינה מלאה %0%";
"Percentage" = "אחוז";
"Percentage and time" = "אחוז וזמן";
"Time and percentage" = "זמן ואחוז";
"Time format" = "פורמט זמן";
"Hide additional information when full" = "הסתר מידע נוסף כשהסוללה מלאה";
"Last charge" = "טעינה אחרונה";
"Capacity" = "קיבולת";
"current / maximum / designed" = "current / מקסימום / עיצוב";
"Low power mode" = "מצב צריכת חשמל נמוכה"; // translategemma:4b
"Percentage inside the icon" = "אחוז בתוך הסמל"; // translategemma:4b
"Colorize battery" = "צבע את הסוללה"; // translategemma:4b
"Charging current" = "זרם טעינה"; // translategemma:4b
"Charging Voltage" = "מתח טעינה"; // translategemma:4b
"Charger state inside the battery" = "מצב טעינה בתוך הסוללה"; // translategemma:4b
// Bluetooth
"Battery to show" = "סוללה להצגה";
"No Bluetooth devices are available" = "לא קיימים מכשירים בלוטות׳ זמינים";
// Clock
"Time zone" = "אזור זמן"; // translategemma:4b
"Local" = "מקומית"; // translategemma:4b
"Calendar" = "יומן"; // translategemma:4b
"Show week numbers" = "הצג מספרי שבועות"; // translategemma:4b
"Local time" = "זמן מקומי"; // translategemma:4b
"Add new clock" = "הוסף שעון חדש"; // translategemma:4b
"Delete selected clock" = "מחק את השעון הנבחר"; // translategemma:4b
"Help with datetime format" = "עזרה בפורמט תאריך ושעה"; // translategemma:4b
// Colors
"Based on utilization" = "מבוסס על ניצול";
"Based on pressure" = "מבוסס על לחץ";
"Based on cluster" = "בהתבסס על קבוצה"; // translategemma:4b
"System accent" = "הדגשת מערכת";
"Monochrome accent" = "הדגשת מונוכרום";
"Clear" = "נקה";
"White" = "לבן";
"Black" = "שחור";
"Gray" = "אפור";
"Second gray" = "אפור שני";
"Dark gray" = "אפור כהה";
"Light gray" = "אפור בהיר";
"Red" = "אדום";
"Second red" = "אדום שני";
"Green" = "ירוק";
"Second green" = "ירוק שני";
"Blue" = "כחול";
"Second blue" = "כחול שני";
"Yellow" = "צהוב";
"Second yellow" = "צהוב שני";
"Orange" = "כתום";
"Second orange" = "כתום שני";
"Purple" = "סגול";
"Second purple" = "סגול שני";
"Brown" = "חום";
"Second brown" = "חום שני";
"Cyan" = "ציאן";
"Magenta" = "מג'נטה";
"Pink" = "ורוד";
"Teal" = "ירוק-כחול";
"Indigo" = "אינדיגו";
================================================
FILE: Stats/Supporting Files/hi.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "सीपीयू";
"Open CPU settings" = "सीपीयू सेटिंग्स खोलें";
"GPU" = "जीपीयू";
"Open GPU settings" = "जीपीयू सेटिंग्स खोलें";
"RAM" = "रैम";
"Open RAM settings" = "रैम सेटिंग्स खोलें";
"Disk" = "डिस्क";
"Open Disk settings" = "डिस्क सेटिंग्स खोलें";
"Sensors" = "सेंसर";
"Open Sensors settings" = "सेंसर सेटिंग्स खोलें";
"Network" = "नेटवर्क";
"Open Network settings" = "नेटवर्क सेटिंग्स खोलें";
"Battery" = "बैटरी";
"Open Battery settings" = "बैटरी सेटिंग्स खोलें";
"Bluetooth" = "ब्लूटूथ";
"Open Bluetooth settings" = "ब्लूटूथ सेटिंग्स खोलें";
"Clock" = "घड़ी";
"Open Clock settings" = "घड़ी सेटिंग्स खोलें";
// Words
"Unknown" = "अज्ञात";
"Version" = "संस्करण";
"Processor" = "प्रोसेसर";
"Memory" = "मेमोरी";
"Graphics" = "ग्राफिक्स";
"Close" = "बंद";
"Download" = "डाउनलोड";
"Install" = "स्थापित करें";
"Cancel" = "रद्द करें";
"Unavailable" = "अनुपलब्ध";
"Yes" = "हाँ";
"No" = "नहीं";
"Automatic" = "स्वचालित";
"Manual" = "मैनुअल";
"None" = "कोई नहीं";
"Dots" = "डॉट्स";
"Arrows" = "तीर";
"Characters" = "चरित्र";
"Short" = "लघु";
"Long" = "लंबा";
"Statistics" = "सांख्यिकी";
"Max" = "अधिकतम";
"Min" = "मिन";
"Reset" = "रीसेट";
"Alignment" = "संरेखण";
"Left alignment" = "बाएं";
"Center alignment" = "केंद्र";
"Right alignment" = "सही";
"Dashboard" = "डैशबोर्ड";
"Enabled" = "सक्षम";
"Disabled" = "अक्षम";
"Silent" = "मौन";
"Units" = "इकाइयाँ";
"Fans" = "प्रशंसकों";
"Scaling" = "स्केलिंग";
"Linear" = "रैखिक";
"Square" = "वर्ग";
"Cube" = "क्यूब";
"Logarithmic" = "लघुगणकीय";
"Fixed scale" = "समाधान"; // translategemma:4b
"Cores" = "कोर";
"Settings" = "सेटिंग्स";
"Name" = "नाम";
"Format" = "प्रारूप";
"Turn off" = "बंद करें";
"Normal" = "सामान्य"; // translategemma:4b
"Warning" = "चेतावनी"; // translategemma:4b
"Critical" = "महत्वपूर्ण"; // translategemma:4b
"Usage" = "उपयोग"; // translategemma:4b
"2 minutes" = "2 मिनट"; // translategemma:4b
"3 minutes" = "3 मिनट"; // translategemma:4b
"10 minutes" = "10 मिनट"; // translategemma:4b
"Import" = "आयात"; // translategemma:4b
"Export" = "निर्यात"; // translategemma:4b
"Separator" = "विभाजक"; // translategemma:4b
"Read" = "पढ़ें"; // translategemma:4b
"Write" = "लिखें"; // translategemma:4b
"Frequency" = "आवृत्ति"; // translategemma:4b
"Save" = "जमा करें"; // translategemma:4b
"Run" = "चलाएं"; // translategemma:4b
"Stop" = "बंद करें"; // translategemma:4b
"Uninstall" = "अनइंस्टॉल"; // translategemma:4b
"1 sec" = "1 सेकंड"; // translategemma:4b
"2 sec" = "2 सेकंड"; // translategemma:4b
"3 sec" = "3 सेकंड"; // translategemma:4b
"5 sec" = "5 सेकंड"; // translategemma:4b
"10 sec" = "10 सेकंड"; // translategemma:4b
"15 sec" = "15 सेकंड"; // translategemma:4b
"30 sec" = "30 सेकंड"; // translategemma:4b
"60 sec" = "60 सेकंड"; // translategemma:4b
// Setup
"Stats Setup" = "आँकड़े सेटअप";
"Previous" = "पिछला";
"Previous page" = "पिछला पृष्ठ";
"Next" = "अगला";
"Next page" = "अगला पृष्ठ";
"Finish" = "समाप्त";
"Finish setup" = "सेटअप खत्म करें";
"Welcome to Stats" = "आँकड़ों में आपका स्वागत है";
"welcome_message" = "स्टैट्स का उपयोग करने के लिए धन्यवाद, आपके मेनू बार के लिए एक मुफ्त ओपन सोर्स मैकओएस सिस्टम मॉनिटर।";
"Start the application automatically when starting your Mac" = "अपना मैक शुरू करते समय एप्लिकेशन को स्वचालित रूप से शुरू करें";
"Do not start the application automatically when starting your Mac" = "अपना मैक शुरू करते समय एप्लिकेशन को स्वचालित रूप से शुरू न करें";
"Do everything silently in the background (recommended)" = "पृष्ठभूमि में चुपचाप सब कुछ करें (अनुशंसित)";
"Check for a new version on startup" = "स्टार्टअप पर एक नए संस्करण की जांच करें";
"Check for a new version every day (once a day)" = "हर दिन (दिन में एक बार) एक नए संस्करण की जांच करें";
"Check for a new version every week (once a week)" = "हर हफ्ते (सप्ताह में एक बार) एक नए संस्करण की जांच करें";
"Check for a new version every month (once a month)" = "हर महीने (महीने में एक बार) एक नए संस्करण की जांच करें";
"Never check for updates (not recommended)" = "अपडेट की जांच कभी न करें (अनुशंसित नहीं)";
"Anonymous telemetry for better development decisions" = "बेहतर विकास निर्णयों के लिए अनाम टेलीमेट्री";
"Share anonymous telemetry data" = "अनाम टेलीमेट्री डेटा साझा करें";
"Do not share anonymous telemetry data" = "अनाम टेलीमेट्री डेटा साझा न करें";
"The configuration is completed" = "कॉन्फ़िगरेशन पूरा हो गया है";
"finish_setup_message" = "सब कुछ सेट किया गया है! \n आँकड़े एक खुला स्रोत उपकरण है, यह मुफ़्त है और हमेशा रहेगा। \n यदि आप इसका आनंद लेते हैं तो आप एक परियोजना का समर्थन कर सकते हैं, यह हमेशा सराहना की जाती है!";
// Alerts
"New version available" = "नया संस्करण उपलब्ध है";
"Click to install the new version of Stats" = "आँकड़े के नए संस्करण को स्थापित करने के लिए क्लिक करें";
"Successfully updated" = "सफलतापूर्वक अपडेट किया गया";
"Stats was updated to v" = "आँकड़े v पर अपडेट किए गए थे";
"Reset settings text" = "Aसभी एप्लिकेशन सेटिंग्स रीसेट हो जाएंगी और एप्लिकेशन को पुनरारंभ किया जाएगा। क्या आप वाकई ऐसा करना चाहते हैं?";
"Support text" = "Stats का उपयोग करने के लिए धन्यवाद!\n\n इस ओपन-सोर्स प्रोजेक्ट को बनाए रखने और सुधारने में समय और संसाधन लगते हैं। आपका समर्थन हमें सभी के लिए एक निःशुल्क और विश्वसनीय एप्लिकेशन प्रदान करना जारी रखने में मदद करता है.\n\nयदि आपको Stats मददगार लगता है, तो कृपया योगदान देने पर विचार करें। हर छोटी-छोटी मदद मददगार होती है!";
// Settings
"Open Activity Monitor" = "ओपन एक्टिविटी मॉनिटर";
"Report a bug" = "बग की रिपोर्ट करें";
"Support the application" = "आवेदन का समर्थन करें";
"Close application" = "आवेदन बंद करें";
"Open application settings" = "ओपन एप्लिकेशन सेटिंग्स";
"Open dashboard" = "ओपन डैशबोर्ड";
"No notifications available in this module" = "इस मॉड्यूल में कोई सूचनाएं उपलब्ध नहीं हैं।"; // translategemma:4b
"Open Calendar" = "कैलेंडर खोलें"; // translategemma:4b
"Toggle the module" = "मॉड्यूल को चालू/बंद करें"; // translategemma:4b
// Application settings
"Update application" = "अद्यतन अनुप्रयोग";
"Check for updates" = "अपडेट की जाँच करें";
"At start" = "शुरुआत में";
"Once per day" = "प्रति दिन एक बार";
"Once per week" = "प्रति सप्ताह एक बार";
"Once per month" = "प्रति माह एक बार";
"Never" = "कभी नहीं";
"Check for update" = "अपडेट की जांच करें";
"Show icon in dock" = "डॉक में आइकन दिखाएं";
"Start at login" = "लॉगिन पर शुरू करें";
"Build number" = "बिल्ड नंबर";
"Import settings" = "आयात सेटिंग्स"; // translategemma:4b
"Export settings" = "निर्यात सेटिंग्स"; // translategemma:4b
"Reset settings" = "सेटिंग्स रीसेट करें";
"Pause the Stats" = "आँकड़ों को रोकें";
"Resume the Stats" = "आँकड़े फिर से शुरू करें";
"Combined modules" = "संयुक्त मॉड्यूल";
"Combined details" = "संयुक्त विवरण"; // translategemma:4b
"Spacing" = "स्पेसिंग";
"Share anonymous telemetry" = "अनाम टेलीमेट्री साझा करें";
"Choose file" = "फ़ाइल चुनें"; // translategemma:4b
"Stress tests" = "तनाव परीक्षण"; // translategemma:4b
// Dashboard
"Serial number" = "सीरियल नंबर";
"Model identifier" = "मॉडल पहचानकर्ता"; // translategemma:4b
"Production year" = "उत्पादन वर्ष"; // translategemma:4b
"Uptime" = "अपटाइम";
"Number of cores" = "%0 कोर";
"Number of threads" = "%0 थ्रेड्स";
"Number of e-cores" = "%0 दक्षता कोर";
"Number of p-cores" = "%0 प्रदर्शन कोर";
"Disks" = "डिस्क"; // translategemma:4b
"Display" = "प्रदर्शन"; // translategemma:4b
// Update
"The latest version of Stats installed" = "आँकड़े का नवीनतम संस्करण स्थापित है";
"Downloading..." = "डाउनलोडिंग ...";
"Current version: " = "वर्तमान संस्करण: ";
"Latest version: " = "नवीनतम संस्करण: ";
// Widgets
"Color" = "रंग";
"Label" = "लेबल";
"Box" = "बॉक्स";
"Frame" = "फ्रेम";
"Value" = "मूल्य";
"Colorize" = "रंगांकित";
"Colorize value" = "मूल्य को रंगीन करें";
"Additional information" = "अतिरिक्त जानकारी";
"Reverse values order" = "रिवर्स मान क्रम";
"Base" = "आधार";
"Display mode" = "दृश्य मोड";
"One row" = "एक पंक्ति";
"Two rows" = "दो पंक्तियाँ";
"Mini widget" = "मिनी";
"Line chart widget" = "लाइन चार्ट";
"Bar chart widget" = "बार चार्ट";
"Pie chart widget" = "पाई चार्ट";
"Network chart widget" = "नेटवर्क चार्ट";
"Speed widget" = "गति";
"Battery widget" = "बैटरी";
"Stack widget" = "स्टैक";
"Memory widget" = "मेमोरी";
"Static width" = "स्थैतिक चौड़ाई";
"Tachometer widget" = "टैकोमीटर";
"State widget" = "राज्य विजेट";
"Text widget" = "टेक्स्ट विजेट"; // translategemma:4b
"Battery details widget" = "बैटरी विवरण विजेट"; // translategemma:4b
"Show symbols" = "प्रतीक दिखाएँ";
"Label widget" = "लेबल";
"Number of reads in the chart" = "चार्ट में पढ़ने की संख्या";
"Color of download" = "डाउनलोड का रंग";
"Color of upload" = "अपलोड का रंग";
"Monospaced font" = "मोनोस्पेस्ड फ़ॉन्ट";
"Reverse order" = "उल्टे क्रम में"; // translategemma:4b
"Chart history" = "चार्ट का इतिहास"; // translategemma:4b
"Default color" = "डिफ़ॉल्ट"; // translategemma:4b
"Transparent when no activity" = "जब कोई गतिविधि नहीं होती तो पारदर्शी।"; // translategemma:4b
"Constant color" = "स्थिर"; // translategemma:4b
// Module Kit
"Open module settings" = "ओपन मॉड्यूल सेटिंग्स";
"Select widget" = "%0 विजेट का चयन करें";
"Open widget settings" = "विजेट सेटिंग्स खोलें";
"Update interval" = "अद्यतन अंतराल";
"Usage history" = "उपयोग इतिहास";
"Details" = "विवरण";
"Top processes" = "शीर्ष प्रक्रियाएं";
"Pictogram" = "पिक्टोग्राम";
"Module" = "मॉड्यूल";
"Widgets" = "विजेट";
"Popup" = "पॉपअप";
"Notifications" = "सूचनाएं";
"Merge widgets" = "मर्ज विजेट";
"No available widgets to configure" = "कॉन्फ़िगर करने के लिए कोई उपलब्ध विजेट नहीं";
"No options to configure for the popup in this module" = "इस मॉड्यूल में पॉपअप के लिए कॉन्फ़िगर करने के लिए कोई विकल्प नहीं";
"Process" = "प्रक्रिया"; // translategemma:4b
"Kill process" = "प्रक्रिया को समाप्त करें"; // translategemma:4b
"Keyboard shortcut" = "कीबोर्ड शॉर्टकट"; // translategemma:4b
"Listening..." = "सुन रहे हैं..."; // translategemma:4b
// Modules
"Number of top processes" = "शीर्ष प्रक्रियाओं की संख्या";
"Update interval for top processes" = "शीर्ष प्रक्रियाओं के लिए अंतराल अपडेट करें";
"Notification level" = "अधिसूचना स्तर";
"Chart color" = "चार्ट रंग";
"Main chart scaling" = "मुख्य चार्ट स्केल"; // translategemma:4b
"Scale value" = "माप मान"; // translategemma:4b
"Text widget value" = "टेक्स्ट विजेट का मान"; // translategemma:4b
// CPU
"CPU usage" = "सीपीयू उपयोग";
"CPU temperature" = "सीपीयू तापमान";
"CPU frequency" = "सीपीयू आवृत्ति";
"System" = "सिस्टम";
"User" = "उपयोगकर्ता";
"Idle" = "निष्क्रिय";
"Show usage per core" = "प्रति कोर उपयोग दिखाएं";
"Show hyper-threading cores" = "हाइपर-थ्रेडिंग कोर दिखाएं";
"Split the value (System/User)" = "मान (सिस्टम / उपयोगकर्ता) को विभाजित करें";
"Scheduler limit" = "शेड्यूलर सीमा";
"Speed limit" = "गति सीमा";
"Average load" = "औसत भार";
"1 minute" = "1 मिनट";
"5 minutes" = "5 मिनट";
"15 minutes" = "15 मिनट";
"CPU usage threshold" = "सीपीयू उपयोग सीमा";
"CPU usage is" = "सीपीयू उपयोग %0 है";
"Efficiency cores" = "दक्षता कोर";
"Performance cores" = "प्रदर्शन कोर";
"System color" = "सिस्टम रंग";
"User color" = "उपयोगकर्ता रंग";
"Idle color" = "निष्क्रिय रंग";
"Cluster grouping" = "क्लस्टर समूहीकरण";
"Efficiency cores color" = "दक्षता कोर रंग";
"Performance cores color" = "प्रदर्शन कोर रंग";
"Total load" = "कुल भार"; // translategemma:4b
"System load" = "सिस्टम का भार"; // translategemma:4b
"User load" = "उपयोगकर्ता भार"; // translategemma:4b
"Efficiency cores load" = "कार्यक्षमता कोर लोड"; // translategemma:4b
"Performance cores load" = "परफॉर्मेंस कोर लोड"; // translategemma:4b
"All cores" = "सभी कोर"; // translategemma:4b
// GPU
"GPU to show" = "दिखाने के लिए जीपीयू";
"Show GPU type" = "जीपीयू प्रकार दिखाएँ";
"GPU enabled" = "जीपीयू सक्षम";
"GPU disabled" = "जीपीयू अक्षम";
"GPU temperature" = "जीपीयू तापमान";
"GPU utilization" = "जीपीयू उपयोग";
"Vendor" = "विक्रेता";
"Model" = "मॉडल";
"Status" = "स्थिति";
"Active" = "सक्रिय";
"Non active" = "गैर सक्रिय";
"Fan speed" = "फैन स्पीड";
"Core clock" = "कोर घड़ी";
"Memory clock" = "मेमोरी क्लॉक";
"Utilization" = "उपयोग";
"Render utilization" = "उपयोग प्रस्तुत करें";
"Tiler utilization" = "टाइलर उपयोग";
"GPU usage threshold" = "जीपीयू उपयोग सीमा";
"GPU usage is" = "जीपीयू उपयोग %0 है";
// RAM
"Memory usage" = "मेमोरी उपयोग";
"Memory pressure" = "स्मृति दबाव";
"Total" = "कुल";
"Used" = "प्रयुक्त";
"App" = "ऐप";
"Wired" = "वायर्ड";
"Compressed" = "संपीड़ित";
"Free" = "मुक्त";
"Swap" = "स्वैप";
"Split the value (App/Wired/Compressed)" = "मान को विभाजित करें (ऐप/वायर्ड/संपीड़ित)";
"RAM utilization threshold" = "रैम उपयोग सीमा";
"RAM utilization is" = "रैम उपयोग %0 है";
"App color" = "ऐप रंग";
"Wired color" = "वायर्ड रंग";
"Compressed color" = "संपीड़ित रंग";
"Free color" = "मुक्त रंग";
"Free memory (less than)" = "मुफ्त मेमोरी (कम से कम)"; // translategemma:4b
"Swap size" = "स्wap का आकार"; // translategemma:4b
"Free RAM is" = "मुफ्त रैम %0 है"; // translategemma:4b
// Disk
"Show removable disks" = "हटाने योग्य डिस्क दिखाएँ";
"Used disk memory" = "%1 का %0 उपयोग किया गया";
"Free disk memory" = "%1 का %0 मुक्त";
"Disk to show" = "दिखाने के लिए डिस्क";
"Open disk" = "ओपन डिस्क";
"Switch view" = "स्विच व्यू";
"Disk utilization threshold" = "डिस्क उपयोग सीमा";
"Disk utilization is" = "डिस्क उपयोग %0 है";
"Read color" = "रंग पढ़ें";
"Write color" = "रंग लिखें";
"Disk usage" = "डिस्क उपयोग";
"Total read" = "कुल पढ़े गए"; // translategemma:4b
"Total written" = "कुल लिखित"; // translategemma:4b
"Write speed" = "लिखें"; // translategemma:4b
"Read speed" = "पढ़ें"; // translategemma:4b
"Drives" = "ड्राइव"; // translategemma:4b
"SMART data" = "स्मार्ट डेटा"; // translategemma:4b
// Sensors
"Temperature unit" = "तापमान इकाई";
"Celsius" = "सेल्सियस";
"Fahrenheit" = "सेल्सियस";
"Save the fan speed" = "पंखे की गति बचाएं";
"Fan" = "फैन";
"HID sensors" = "एचआईडी सेंसर";
"Synchronize fan's control" = "प्रशंसक के नियंत्रण को सिंक्रनाइज़ करें";
"Current" = "वर्तमान";
"Energy" = "ऊर्जा";
"Show unknown sensors" = "अज्ञात सेंसर दिखाएं";
"Install fan helper" = "फैन हेल्पर स्थापित करें";
"Uninstall fan helper" = "फैन हेल्पर की स्थापना रद्द करें";
"Fan value" = "फैन वैल्यू";
"Turn off fan" = "पंखा बंद करें";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "आप पंखे को बंद करने जा रहे हैं। यह अनुशंसित कार्रवाई नहीं है जो आपके मैक को नुकसान पहुंचा सकती है, क्या आप वाकई ऐसा करना चाहते हैं?";
"Sensor threshold" = "सेंसर की सीमा"; // translategemma:4b
"Left fan" = "बाएं"; // translategemma:4b
"Right fan" = "सही"; // translategemma:4b
"Fastest fan" = "सबसे तेज़"; // translategemma:4b
"Sensor to show" = "सेंसर को प्रदर्शित करना"; // translategemma:4b
// Network
"Uploading" = "अपलोड";
"Downloading" = "डाउनलोड";
"Public IP" = "सार्वजनिक आईपी";
"Local IP" = "स्थानीय आईपी";
"Interface" = "इंटरफ़ेस";
"Physical address" = "भौतिक पता";
"Refresh" = "ताज़ा करें";
"Click to copy public IP address" = "सार्वजनिक आईपी पते की प्रतिलिपि बनाने के लिए क्लिक करें";
"Click to copy local IP address" = "स्थानीय आईपी पते की प्रतिलिपि बनाने के लिए क्लिक करें";
"Click to copy wifi name" = "वाईफ़ाई नाम कॉपी करने के लिए क्लिक करें";
"Click to copy mac address" = "मैक पते की प्रतिलिपि बनाने के लिए क्लिक करें";
"No connection" = "कोई कनेक्शन नहीं";
"Network interface" = "नेटवर्क इंटरफ़ेस";
"Total download" = "कुल डाउनलोड";
"Total upload" = "कुल अपलोड";
"Reader type" = "रीडर प्रकार";
"Interface based" = "इंटरफ़ेस आधारित";
"Processes based" = "प्रक्रिया आधारित";
"Reset data usage" = "डेटा उपयोग रीसेट करें";
"VPN mode" = "वीपीएन मोड";
"Standard" = "मानक";
"Security" = "सुरक्षा";
"Channel" = "चैनल";
"Common scale" = "सामान्य पैमाने";
"Autodetection" = "ऑटोडिटेक्शन";
"Widget activation threshold" = "विजेट सक्रियण सीमा";
"Internet connection" = "इंटरनेट कनेक्शन";
"Active state color" = "सक्रिय राज्य रंग";
"Nonactive state color" = "नॉनएक्टिव स्टेट कलर";
"Connectivity host (ICMP)" = "कनेक्टिविटी होस्ट (आईसीएमपी)";
"Leave empty to disable the check" = "चेक को अक्षम करने के लिए खाली छोड़ दें";
"Connectivity history" = "कनेक्टिविटी इतिहास";
"Auto-refresh public IP address" = "ऑटो-रिफ्रेश सार्वजनिक आईपी पता";
"Every hour" = "हर घंटे";
"Every 12 hours" = "हर 12 घंटे";
"Every 24 hours" = "हर 24 घंटे";
"Network activity" = "नेटवर्क गतिविधि";
"Last reset" = "अंतिम रीसेट %0 पहले";
"Latency" = "विलंब"; // translategemma:4b
"Upload speed" = "अपलोड करें"; // translategemma:4b
"Download speed" = "डाउनलोड"; // translategemma:4b
"Address" = "पता"; // translategemma:4b
"WiFi network" = "वाईफाई नेटवर्क"; // translategemma:4b
"Local IP changed" = "स्थानीय आईपी बदल गया है"; // translategemma:4b
"Public IP changed" = "सार्वजनिक आईपी बदल गया है।"; // translategemma:4b
"Previous IP" = "पिछला आईपी पता: %0"; // translategemma:4b
"New IP" = "नया आईपी: %0"; // translategemma:4b
"Internet connection lost" = "इंटरनेट कनेक्शन खो गया"; // translategemma:4b
"Internet connection established" = "इंटरनेट कनेक्शन स्थापित"; // translategemma:4b
// Battery
"Level" = "स्तर";
"Source" = "स्रोत";
"AC Power" = "एसी पावर";
"Battery Power" = "बैटरी पावर";
"Time" = "समय";
"Health" = "स्वास्थ्य";
"Amperage" = "एम्पीयरेज";
"Voltage" = "वोल्टेज";
"Cycles" = "चक्र";
"Temperature" = "तापमान";
"Power adapter" = "पावर एडाप्टर";
"Power" = "शक्ति";
"Is charging" = "चार्ज कर रहा है";
"Time to discharge" = "डिस्चार्ज का समय";
"Time to charge" = "चार्ज करने का समय";
"Calculating" = "गणना";
"Fully charged" = "पूरी तरह से चार्ज";
"Not connected" = "कनेक्ट डे नहीं";
"Low level notification" = "निम्न स्तरीय अधिसूचना";
"High level notification" = "उच्च स्तरीय अधिसूचना";
"Low battery" = "कम बैटरी";
"High battery" = "उच्च बैटरी";
"Battery remaining" = "%0% शेष";
"Battery remaining to full charge" = "%0% से पूर्ण चार्ज";
"Percentage" = "प्रतिशत";
"Percentage and time" = "प्रतिशत और समय";
"Time and percentage" = "समय और प्रतिशत";
"Time format" = "समय प्रारूप";
"Hide additional information when full" = "पूर्ण होने पर अतिरिक्त जानकारी छिपाएं";
"Last charge" = "अंतिम शुल्क";
"Capacity" = "क्षमता";
"current / maximum / designed" = "वर्तमान / अधिकतम / डिजाइन";
"Low power mode" = "कम शक्ति मोड";
"Percentage inside the icon" = "आइकन के अंदर प्रतिशत";
"Colorize battery" = "रंगीन बैटरी";
"Charging current" = "चार्जिंग करंट"; // translategemma:4b
"Charging Voltage" = "चार्जिंग वोल्टेज"; // translategemma:4b
"Charger state inside the battery" = "बैटरी के अंदर चार्जर की स्थिति"; // translategemma:4b
// Bluetooth
"Battery to show" = "दिखाने के लिए बैटरी";
"No Bluetooth devices are available" = "कोई ब्लूटूथ डिवाइस उपलब्ध नहीं हैं";
// Clock
"Time zone" = "समय क्षेत्र";
"Local" = "स्थानीय";
"Calendar" = "कैलेंडर"; // translategemma:4b
"Show week numbers" = "सप्ताह की संख्याएँ दिखाएँ"; // translategemma:4b
"Local time" = "स्थानीय समय"; // translategemma:4b
"Add new clock" = "नया घड़ी जोड़ें"; // translategemma:4b
"Delete selected clock" = "चुना हुआ घड़ी हटाएँ"; // translategemma:4b
"Help with datetime format" = "datetime प्रारूप के साथ सहायता"; // translategemma:4b
// Colors
"Based on utilization" = "उपयोग के आधार पर";
"Based on pressure" = "दबाव के आधार पर";
"Based on cluster" = "क्लस्टर पर आधारित";
"System accent" = "सिस्टम उच्चारण";
"Monochrome accent" = "सिस्टम उच्चारण";
"Clear" = "स्पष्ट";
"White" = "सफेद";
"Black" = "काला";
"Gray" = "धूसर";
"Second gray" = "दूसरा धूसर";
"Dark gray" = "काला धूसर";
"Light gray" = "प्रकाश धूसर";
"Red" = "लाल";
"Second red" = "दूसरा लाल";
"Green" = "हरा";
"Second green" = "दूसरा हरा";
"Blue" = "नीला";
"Second blue" = "दूसरा नीला";
"Yellow" = "पीला";
"Second yellow" = "दूसरा पीला";
"Orange" = "नारंगी";
"Second orange" = "दूसरा नारंगी";
"Purple" = "बैंगनी";
"Second purple" = "दूसरा बैंगनी";
"Brown" = "भूरा";
"Second brown" = "दूसरा भूरा";
"Cyan" = "सियान";
"Magenta" = "मैजेंटा";
"Pink" = "गुलाबी";
"Teal" = "टील";
"Indigo" = "इंडिगो";
================================================
FILE: Stats/Supporting Files/hr.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "Procesor"; // translategemma:4b
"Open CPU settings" = "Otvori postavke procesora";
"GPU" = "GPU";
"Open GPU settings" = "Otvori postavke grafičkog procesora";
"RAM" = "RAM";
"Open RAM settings" = "Otvori postavke radne memorije";
"Disk" = "Disk";
"Open Disk settings" = "Otvori postavke diska";
"Sensors" = "Senzori";
"Open Sensors settings" = "Otvori postavke senzora";
"Network" = "Mreža";
"Open Network settings" = "Otvori postavke mreže";
"Battery" = "Baterija";
"Open Battery settings" = "Otvori postavke baterije";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Otvori postavke bluetootha";
"Clock" = "Sat"; // translategemma:4b
"Open Clock settings" = "Otvorite postavke sata"; // translategemma:4b
// Words
"Unknown" = "Nepoznato";
"Version" = "Verzija";
"Processor" = "Procesor";
"Memory" = "Memorija";
"Graphics" = "Grafičke kartice";
"Close" = "Zatvori";
"Download" = "Preuzmi";
"Install" = "Instaliraj";
"Cancel" = "Odustani";
"Unavailable" = "Nedostupno";
"Yes" = "Da";
"No" = "Ne";
"Automatic" = "Automatski";
"Manual" = "Ručno";
"None" = "Ništa";
"Dots" = "Točke";
"Arrows" = "Strelice";
"Characters" = "Znakovi";
"Short" = "Kratko";
"Long" = "Dugo";
"Statistics" = "Statistika";
"Max" = "Maks.";
"Min" = "Min.";
"Reset" = "Resetiraj";
"Alignment" = "Poravnanje";
"Left alignment" = "Lijevo";
"Center alignment" = "Centrirano";
"Right alignment" = "Desno";
"Dashboard" = "Pregledna ploča";
"Enabled" = "Aktivirano"; // translategemma:4b
"Disabled" = "Deaktivirano";
"Silent" = "Tiho";
"Units" = "Jedinice";
"Fans" = "Ventilatori";
"Scaling" = "Skaliranje"; // translategemma:4b
"Linear" = "Linearna"; // translategemma:4b
"Square" = "Kvadrat"; // translategemma:4b
"Cube" = "Kvadrat"; // translategemma:4b
"Logarithmic" = "Logaritmistički"; // translategemma:4b
"Fixed scale" = "Popravljeno"; // translategemma:4b
"Cores" = "Srdci"; // translategemma:4b
"Settings" = "Postavke"; // translategemma:4b
"Name" = "Ime"; // translategemma:4b
"Format" = "Format";
"Turn off" = "Isključite"; // translategemma:4b
"Normal" = "Normal";
"Warning" = "Upozorenje"; // translategemma:4b
"Critical" = "Kritičan"; // translategemma:4b
"Usage" = "Upotreba"; // translategemma:4b
"2 minutes" = "2 minute"; // translategemma:4b
"3 minutes" = "3 minute"; // translategemma:4b
"10 minutes" = "10 minuta"; // translategemma:4b
"Import" = "Uvoz"; // translategemma:4b
"Export" = "Eksport"; // translategemma:4b
"Separator" = "Razdjelnik"; // translategemma:4b
"Read" = "Pročitajte"; // translategemma:4b
"Write" = "Pišite"; // translategemma:4b
"Frequency" = "Frekvencija"; // translategemma:4b
"Save" = "Spremi"; // translategemma:4b
"Run" = "Pokrenite"; // translategemma:4b
"Stop" = "Stop";
"Uninstall" = "Deinstaliraj"; // translategemma:4b
"1 sec" = "1 sekunda"; // translategemma:4b
"2 sec" = "2 sekunde"; // translategemma:4b
"3 sec" = "3 sekunde"; // translategemma:4b
"5 sec" = "5 sekundi"; // translategemma:4b
"10 sec" = "10 sekundi"; // translategemma:4b
"15 sec" = "15 sekundi"; // translategemma:4b
"30 sec" = "30 sekundi"; // translategemma:4b
"60 sec" = "60 sekundi"; // translategemma:4b
// Setup
"Stats Setup" = "Postavljanje programa Stats";
"Previous" = "Prethodna";
"Previous page" = "Prethodna stranica";
"Next" = "Sljedeća";
"Next page" = "Sljedeća stranica";
"Finish" = "Završi";
"Finish setup" = "Završi postavljanje";
"Welcome to Stats" = "Stats dobrodošlica";
"welcome_message" = "Hvala što koristiš Stats, besplatni program otvorenog koda za praćenje macOS sustava u traci izbornika.";
"Start the application automatically when starting your Mac" = "Pokreni program automatski pri pokretanju računala";
"Do not start the application automatically when starting your Mac" = "Nemoj pokrenuti program automatski pri pokretanju računala";
"Do everything silently in the background (recommended)" = "Obavi sve tiho u pozadini (preporučuje se)";
"Check for a new version on startup" = "Traži nove verzije pri pokretanju programa";
"Check for a new version every day (once a day)" = "Traži nove verzije svaki dan (jednom dnevno)";
"Check for a new version every week (once a week)" = "Traži nove verzije svaki tjedan (jednom tjedno)";
"Check for a new version every month (once a month)" = "Traži nove verzije svaki mjesec (jednom mjesečno)";
"Never check for updates (not recommended)" = "Nikada nemoj tražiti nove verzije (ne preporučuje se)";
"Anonymous telemetry for better development decisions" = "Anonimni telemetri za bolja odluka u razvoju"; // translategemma:4b
"Share anonymous telemetry data" = "Podijelite anonimne podatke o telemetriji"; // translategemma:4b
"Do not share anonymous telemetry data" = "Nemojte dijeliti anonimna podatka o telemetriji."; // translategemma:4b
"The configuration is completed" = "Konfiguracija je završena";
"finish_setup_message" = "Sve je postavljeno! \n Stats je alat otvorenog koda, besplatan je i uvijek će takav biti. \n Ako ti se program sviđa, možeš podržati projekt. Zahvalni smo za svaku vrstu podrške!!";
// Alerts
"New version available" = "Dostupna je nova verzija";
"Click to install the new version of Stats" = "Pritisni za instaliranje nove verzije programa Stats";
"Successfully updated" = "Aktualiziranje je uspjelo";
"Stats was updated to v" = "Stats je aktualiziran na v%0";
"Reset settings text" = "Sve postavke programa će se resetirati i program će se ponovo pokrenuti. Stvarno to želiš učiniti?";
"Support text" = "Hvala vam što koristite Stats!\n\n Održavanje i poboljšanje ovog projekta otvorenog koda zahtijeva vrijeme i resurse. Vaša nam podrška pomaže da nastavimo pružati besplatnu i pouzdanu aplikaciju za sve.\n\nAko smatrate da je statistika korisna, razmislite o davanju doprinosa. Svako malo pomaže!";
// Settings
"Open Activity Monitor" = "Otvori program „Praćenje aktivnosti”";
"Report a bug" = "Prijavi grešku";
"Support the application" = "Podrži program";
"Close application" = "Zatvori program";
"Open application settings" = "Otvori postavke programa";
"Open dashboard" = "Otvori preglednu ploču";
"No notifications available in this module" = "Nema dostupnih obavještenja u ovom modulu"; // translategemma:4b
"Open Calendar" = "Otvoriti kalendar"; // translategemma:4b
"Toggle the module" = "Aktivirajte/deaktivirajte modul"; // translategemma:4b
// Application settings
"Update application" = "Aktualiziraj program";
"Check for updates" = "Traži nove verzije";
"At start" = "Prilikom pokretanja programa";
"Once per day" = "Jednom dnevno";
"Once per week" = "Jednom tjedno";
"Once per month" = "Jednom mjesečno";
"Never" = "Nikada";
"Check for update" = "Traži novu verziju";
"Show icon in dock" = "Prikaži ikonu u programskoj traci";
"Start at login" = "Pokreni nakon prijave";
"Build number" = "Broj izgradnje";
"Import settings" = "Postavke uvoza"; // translategemma:4b
"Export settings" = "Postavke za izvoz"; // translategemma:4b
"Reset settings" = "Resetiraj postavke";
"Pause the Stats" = "Zaustavite prikaz statistike"; // translategemma:4b
"Resume the Stats" = "Nastavite sa statistikom"; // translategemma:4b
"Combined modules" = "Kompatibilni moduli"; // translategemma:4b
"Combined details" = "Spojeni detalji"; // translategemma:4b
"Spacing" = "Razmak"; // translategemma:4b
"Share anonymous telemetry" = "Podijelite anonimne podatke o telemetriji"; // translategemma:4b
"Choose file" = "Odaberite datoteku"; // translategemma:4b
"Stress tests" = "Testiranje pod opterećenjem"; // translategemma:4b
// Dashboard
"Serial number" = "Serijski broj";
"Model identifier" = "Identifikator modela"; // translategemma:4b
"Production year" = "Godina proizvodnje"; // translategemma:4b
"Uptime" = "Vrijeme rada";
"Number of cores" = "Broj jezgri: %0";
"Number of threads" = "Broj komponenti procesa: %0";
"Number of e-cores" = "%0 učinkovitost jezgara"; // translategemma:4b
"Number of p-cores" = "%0 je broj jezgri za izvođenje."; // translategemma:4b
"Disks" = "Diskove"; // translategemma:4b
"Display" = "Prikaz"; // translategemma:4b
// Update
"The latest version of Stats installed" = "Instalirana je najnovija verzija programa Stats";
"Downloading..." = "Preuzimanje …";
"Current version: " = "Trenutačna verzija: ";
"Latest version: " = "Najnovija verzija: ";
// Widgets
"Color" = "Boja";
"Label" = "Oznaka";
"Box" = "Pozadina";
"Frame" = "Okvir";
"Value" = "Vrijednost";
"Colorize" = "Oboji";
"Colorize value" = "Oboji vrijednost";
"Additional information" = "Dodatne informacije";
"Reverse values order" = "Preokreni redoslijed vrijednosti";
"Base" = "Osnova";
"Display mode" = "Način prikaza";
"One row" = "Jednoredno";
"Two rows" = "Dvoredno";
"Mini widget" = "Mini";
"Line chart widget" = "Linijski dijagram";
"Bar chart widget" = "Stupčasti dijagram";
"Pie chart widget" = "Kružni dijagram";
"Network chart widget" = "Dijagram mreže";
"Speed widget" = "Brzina";
"Battery widget" = "Baterija";
"Stack widget" = "Struktura"; // translategemma:4b
"Memory widget" = "Memorija";
"Static width" = "Statična širina";
"Tachometer widget" = "Tahometar";
"State widget" = "Komponenta stanja"; // translategemma:4b
"Text widget" = "Komponenta za prikaz teksta"; // translategemma:4b
"Battery details widget" = "Widget s detaljima baterije"; // translategemma:4b
"Show symbols" = "Prikaži simbole";
"Label widget" = "Oznaka";
"Number of reads in the chart" = "Broj čitanja u dijagramu";
"Color of download" = "Boja primanja";
"Color of upload" = "Boja slanja";
"Monospaced font" = "Font s jednakim širinom znakova"; // translategemma:4b
"Reverse order" = "U obrnutom redosljedu"; // translategemma:4b
"Chart history" = "Povijest grafikona"; // translategemma:4b
"Default color" = "Predloženo"; // translategemma:4b
"Transparent when no activity" = "Prozirno kada nema aktivnosti"; // translategemma:4b
"Constant color" = "Konstanta"; // translategemma:4b
// Module Kit
"Open module settings" = "Otvori postavke modula";
"Select widget" = "Odaberi programčić %0";
"Open widget settings" = "Otvori postavke programčića";
"Update interval" = "Interval aktualiziranja";
"Usage history" = "Povijest korištenja";
"Details" = "Detalji";
"Top processes" = "Glavni procesi";
"Pictogram" = "Piktogram";
"Module" = "Modul";
"Widgets" = "Widgeti";
"Popup" = "Iskočiti";
"Notifications" = "Obavijesti";
"Merge widgets" = "Spoji programčiće";
"No available widgets to configure" = "Nema dostupnih widgeta za konfiguraciju";
"No options to configure for the popup in this module" = "Nema opcija za konfiguraciju za skočni prozor u ovom modulu";
"Process" = "Proces"; // translategemma:4b
"Kill process" = "Ubiti proces"; // translategemma:4b
"Keyboard shortcut" = "Kratka tipkarska naredba"; // translategemma:4b
"Listening..." = "Slušam..."; // translategemma:4b
// Modules
"Number of top processes" = "Broj glavnih procesa";
"Update interval for top processes" = "Interval aktualiziranja za glavne procese";
"Notification level" = "Razina obavještavanja";
"Chart color" = "Boja karte";
"Main chart scaling" = "Glavna skaliranje grafikona"; // translategemma:4b
"Scale value" = "Vrijednost skaliranja"; // translategemma:4b
"Text widget value" = "Vrijednost polja teksta"; // translategemma:4b
// CPU
"CPU usage" = "Korištenje procesora";
"CPU temperature" = "Temperatura procesora";
"CPU frequency" = "Frekvencija procesora";
"System" = "Sustav";
"User" = "Korisnik";
"Idle" = "Bez aktivnosti";
"Show usage per core" = "Prikaži korištenje procesora pojedinačno";
"Show hyper-threading cores" = "Prikaži hyper-threading procesore";
"Split the value (System/User)" = "Podijeli vrijednost (Sustav/Korisnik)";
"Scheduler limit" = "Ograničenje planera";
"Speed limit" = "Ograničenje brzine";
"Average load" = "Prosječno učitavanje";
"1 minute" = "1 minuta";
"5 minutes" = "5 minuta";
"15 minutes" = "15 minuta";
"CPU usage threshold" = "Prag korištenja procesora";
"CPU usage is" = "Korištenje procesora je %0";
"Efficiency cores" = "Srdobne jezgre"; // translategemma:4b
"Performance cores" = "Srdobne jezgre"; // translategemma:4b
"System color" = "Boja sustava"; // translategemma:4b
"User color" = "Boja korisnika"; // translategemma:4b
"Idle color" = "Boja u stanju mirovanja"; // translategemma:4b
"Cluster grouping" = "Grupiranje"; // translategemma:4b
"Efficiency cores color" = "Cores za učinkovitost, boje"; // translategemma:4b
"Performance cores color" = "Boja jezgara za performanse"; // translategemma:4b
"Total load" = "Ukupna optereženost"; // translategemma:4b
"System load" = "Nivo opterećenja sustava"; // translategemma:4b
"User load" = "Nivo korisničke upotrebe"; // translategemma:4b
"Efficiency cores load" = "Procesori za učinkovitost se iniciraju"; // translategemma:4b
"Performance cores load" = "Optimizirani jezgre se aktiviraju"; // translategemma:4b
"All cores" = "Sve jezgre"; // translategemma:4b
// GPU
"GPU to show" = "Grafički procesor";
"Show GPU type" = "Prikaži vrstu grafičkog procesora";
"GPU enabled" = "Grafički procesor je aktiviran";
"GPU disabled" = "Grafički procesor je deaktiviran";
"GPU temperature" = "Temperatura grafičkog procesora";
"GPU utilization" = "Korištenje grafičkog procesora";
"Vendor" = "Proizvođač";
"Model" = "Model";
"Status" = "Stanje";
"Active" = "Aktivno";
"Non active" = "Neaktivno";
"Fan speed" = "Brzina ventilatora";
"Core clock" = "Frekvencija procesora";
"Memory clock" = "Frekvencija memorije";
"Utilization" = "Korištenje";
"Render utilization" = "Korištenje iscrtavača";
"Tiler utilization" = "Korištenje popločivanja";
"GPU usage threshold" = "Prag korištenja grafičkog procesora";
"GPU usage is" = "Korištenje grafičkog procesora je %0";
// RAM
"Memory usage" = "Korištenje radne memorije";
"Memory pressure" = "Opterećenje radne memorije";
"Total" = "Ukupno";
"Used" = "Korišteno";
"App" = "Program";
"Wired" = "Rezidentno";
"Compressed" = "Komprimirano";
"Free" = "Slobodno";
"Swap" = "Virtualno";
"Split the value (App/Wired/Compressed)" = "Podijeli vrijednost (Program/Rezidentno/Komprimirano)";
"RAM utilization threshold" = "Prag korištenja radne memorije";
"RAM utilization is" = "Korištenje radne memorije je %0";
"App color" = "Boja aplikacije"; // translategemma:4b
"Wired color" = "Boja s kabelom"; // translategemma:4b
"Compressed color" = "Komprimirana boja"; // translategemma:4b
"Free color" = "Besplatna boja"; // translategemma:4b
"Free memory (less than)" = "Besplatna memorija (manja od)"; // translategemma:4b
"Swap size" = "Veličina za razmjenu"; // translategemma:4b
"Free RAM is" = "Besplatna količina RAM-a je %0"; // translategemma:4b
// Disk
"Show removable disks" = "Prikaži prijenosne diskove";
"Used disk memory" = "%0 od %1 korišteno";
"Free disk memory" = "%0 od %1 slobodno";
"Disk to show" = "Disk";
"Open disk" = "Otvori disk";
"Switch view" = "Zamijeni prikaz";
"Disk utilization threshold" = "Prag korištenja diska";
"Disk utilization is" = "Korištenje diska je %0";
"Read color" = "Pročitajte boje"; // translategemma:4b
"Write color" = "Napišite boju"; // translategemma:4b
"Disk usage" = "Korisna prostora"; // translategemma:4b
"Total read" = "Ukupno pročitano"; // translategemma:4b
"Total written" = "Ukupno pisano"; // translategemma:4b
"Write speed" = "Napiš"; // translategemma:4b
"Read speed" = "Pročitajte"; // translategemma:4b
"Drives" = "Priključci"; // translategemma:4b
"SMART data" = "Podaci u formatu SMART"; // translategemma:4b
// Sensors
"Temperature unit" = "Jedinica temperature";
"Celsius" = "Celzij";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Spremi brzinu ventilatora";
"Fan" = "Ventilator";
"HID sensors" = "Senzori HID uređaja";
"Synchronize fan's control" = "Sinkroniziraj upravljanje ventilatora";
"Current" = "Trenutna"; // translategemma:4b
"Energy" = "Energija"; // translategemma:4b
"Show unknown sensors" = "Pokažite nepoznate senzore"; // translategemma:4b
"Install fan helper" = "Instalirajte pomoćnik za ventilator"; // translategemma:4b
"Uninstall fan helper" = "Deinstalirajte pomoćnik za upravljanje ventilatorom"; // translategemma:4b
"Fan value" = "Vrijednost korisnika"; // translategemma:4b
"Turn off fan" = "Isključite ventilator"; // translategemma:4b
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Vi ćete isključiti ventilator. Ovo nije preporučena akcija koja može oštetiti vaš Mac. Jeste li sigurni da želite to učiniti?"; // translategemma:4b
"Sensor threshold" = "Granica senzora"; // translategemma:4b
"Left fan" = "Lijevo"; // translategemma:4b
"Right fan" = "U redu"; // translategemma:4b
"Fastest fan" = "Najbrže"; // translategemma:4b
"Sensor to show" = "Senzor prikazuje"; // translategemma:4b
// Network
"Uploading" = "Slanje";
"Downloading" = "Primanje";
"Public IP" = "Javni IP";
"Local IP" = "Lokalni IP";
"Interface" = "Sučelje";
"Physical address" = "Fizička adresa";
"Refresh" = "Osvježi";
"Click to copy public IP address" = "Pritisni za kopiranje javne IP adrese";
"Click to copy local IP address" = "Pritisni za kopiranje lokalne IP adrese";
"Click to copy wifi name" = "Pritisni za kopiranje imena wifi mreže";
"Click to copy mac address" = "Pritisni za kopiranje mac adrese";
"No connection" = "Ne postoji veza";
"Network interface" = "Sučelje mreže";
"Total download" = "Ukupno primljeno";
"Total upload" = "Ukupno poslano";
"Reader type" = "Vrsta čitača";
"Interface based" = "Na osnovi sučelja";
"Processes based" = "Na osnovi procesa";
"Reset data usage" = "Resetiraj korištenje podataka";
"VPN mode" = "Modus virtualne privatne mreže (VPN)";
"Standard" = "Standard";
"Security" = "Sigurnost";
"Channel" = "Kanal";
"Common scale" = "Uobičajena mjera"; // translategemma:4b
"Autodetection" = "Automatska detekcija"; // translategemma:4b
"Widget activation threshold" = "Granica za aktivaciju widgeta"; // translategemma:4b
"Internet connection" = "Povezivanje s internetom"; // translategemma:4b
"Active state color" = "Boja u aktivnom stanju"; // translategemma:4b
"Nonactive state color" = "Boja stanja u kojem se komponenta ne koristi"; // translategemma:4b
"Connectivity host (ICMP)" = "Host za konekciju (ICMP)"; // translategemma:4b
"Leave empty to disable the check" = "Ostavite praznim kako biste isključili provjeru."; // translategemma:4b
"Connectivity history" = "Historija konektivnosti"; // translategemma:4b
"Auto-refresh public IP address" = "Automatsko ažuriranje javne IP adrese"; // translategemma:4b
"Every hour" = "Svaki sat"; // translategemma:4b
"Every 12 hours" = "Svaki 12 sati"; // translategemma:4b
"Every 24 hours" = "Svaki dan"; // translategemma:4b
"Network activity" = "Aktivnost mreže"; // translategemma:4b
"Last reset" = "Posljednji reset prije `%0` dana"; // translategemma:4b
"Latency" = "Latencija"; // translategemma:4b
"Upload speed" = "Pošalj"; // translategemma:4b
"Download speed" = "Preuzmi"; // translategemma:4b
"Address" = "Adresa"; // translategemma:4b
"WiFi network" = "Wi-Fi mreža"; // translategemma:4b
"Local IP changed" = "Lokalni IP se promijenio"; // translategemma:4b
"Public IP changed" = "Публиčni IP se promijenio"; // translategemma:4b
"Previous IP" = "Prethodni IP: %0"; // translategemma:4b
"New IP" = "Nova IP adresa: %0"; // translategemma:4b
"Internet connection lost" = "Gubitak veze s internetom"; // translategemma:4b
"Internet connection established" = "Uspostavljena je veza s internetom"; // translategemma:4b
// Battery
"Level" = "Stanje";
"Source" = "Izvor";
"AC Power" = "Mrežno napajanje";
"Battery Power" = "Baterija";
"Time" = "Vrijeme";
"Health" = "Stanje";
"Amperage" = "Jakost";
"Voltage" = "Napon";
"Cycles" = "Broj ciklusa";
"Temperature" = "Temperatura";
"Power adapter" = "Adapter";
"Power" = "Snaga";
"Is charging" = "Puni se";
"Time to discharge" = "Vrijeme do pražnjenja";
"Time to charge" = "Vrijeme do napunjenosti";
"Calculating" = "Izračunava se";
"Fully charged" = "Napunjena";
"Not connected" = "Nije priključena";
"Low level notification" = "Obavijest za slabu bateriju";
"High level notification" = "Obavijest za napunjenu bateriju";
"Low battery" = "Baterija je slaba";
"High battery" = "Baterija je napunjena";
"Battery remaining" = "%0 % preostalo";
"Battery remaining to full charge" = "%0 % do napunjenosti";
"Percentage" = "Postotak";
"Percentage and time" = "Postotak i vrijeme";
"Time and percentage" = "Vrijeme i postotak";
"Time format" = "Format vremena";
"Hide additional information when full" = "Sakrij dodatne podatke kad je napunjena";
"Last charge" = "Zadnje punjenje";
"Capacity" = "Kapacitet";
"current / maximum / designed" = "current / maksimalni / projektirani";
"Low power mode" = "Mod rada s niskom potrošnjom"; // translategemma:4b
"Percentage inside the icon" = "Postotak unutar ikone"; // translategemma:4b
"Colorize battery" = "Boja bateriju"; // translategemma:4b
"Charging current" = "Trenutak punjenja"; // translategemma:4b
"Charging Voltage" = "Naponski napajanje"; // translategemma:4b
"Charger state inside the battery" = "Stanje punjenja unutar baterije"; // translategemma:4b
// Bluetooth
"Battery to show" = "Baterija";
"No Bluetooth devices are available" = "Nema dostupnih Bluetooth uređaja";
// Clock
"Time zone" = "Vremenska zona"; // translategemma:4b
"Local" = "Lokal"; // translategemma:4b
"Calendar" = "Kalendar"; // translategemma:4b
"Show week numbers" = "Pokažite brojeve tjedna"; // translategemma:4b
"Local time" = "Lokalno vrijeme"; // translategemma:4b
"Add new clock" = "Dodajte novi sat"; // translategemma:4b
"Delete selected clock" = "Izbrisati odabranu satnu jedinicu"; // translategemma:4b
"Help with datetime format" = "Pomoć s formatiranjem datuma i vremena"; // translategemma:4b
// Colors
"Based on utilization" = "Na osnovi korištenja";
"Based on pressure" = "Na osnovi opterećenja";
"Based on cluster" = "Na temelju klastera"; // translategemma:4b
"System accent" = "Boje sustava";
"Monochrome accent" = "Jednobojno";
"Clear" = "Bezbojno";
"White" = "Bijela";
"Black" = "Crna";
"Gray" = "Siva";
"Second gray" = "Druga siva";
"Dark gray" = "Tamnosiva";
"Light gray" = "Svjetlosiva";
"Red" = "Crvena";
"Second red" = "Druga crvena";
"Green" = "Zelena";
"Second green" = "Druga zelena";
"Blue" = "Plava";
"Second blue" = "Druga plava";
"Yellow" = "Žuta";
"Second yellow" = "Druga žuta";
"Orange" = "Narančasta";
"Second orange" = "Druga narančasta";
"Purple" = "Ljubičasta";
"Second purple" = "Druga ljubičasta";
"Brown" = "Smeđa";
"Second brown" = "Druga smeđa";
"Cyan" = "Cijan";
"Magenta" = "Magenta";
"Pink" = "Ružičasta";
"Teal" = "Tamnotirkizna";
"Indigo" = "Modra";
================================================
FILE: Stats/Supporting Files/hu.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by @moriczr on 22/01/2021, updated by András Oravecz (info@oandras.hu) on 06/01/2023
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "CPU beállítások megnyitása";
"GPU" = "Grafikus processzor"; // translategemma:4b
"Open GPU settings" = "GPU beállítások megnyitása";
"RAM" = "Memória";
"Open RAM settings" = "Memória beállítások megnyitása";
"Disk" = "Lemezek";
"Open Disk settings" = "Lemez beállítások megnyitása";
"Sensors" = "Érzékelők";
"Open Sensors settings" = "Érzékelő beállítások megnyitása";
"Network" = "Hálózat";
"Open Network settings" = "Hálózati beállítások megnyitása";
"Battery" = "Akkumulátor";
"Open Battery settings" = "Akkumulátor beállítások megnyitása";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Bluetooth beállítások megnyitása";
"Clock" = "Óra";
"Open Clock settings" = "Óra beállításainak megnyitása";
// Words
"Unknown" = "Ismeretlen";
"Version" = "Verzió";
"Processor" = "Processzor";
"Memory" = "Memória";
"Graphics" = "Grafika";
"Close" = "Bezár";
"Download" = "Letöltés";
"Install" = "Telepítés";
"Cancel" = "Mégsem";
"Unavailable" = "Nem elérhető";
"Yes" = "Igen";
"No" = "Nem";
"Automatic" = "Automatikus";
"Manual" = "Kézi";
"None" = "Egyik sem";
"Dots" = "Pontok";
"Arrows" = "Nyilak";
"Characters" = "Karakterek";
"Short" = "Rövid";
"Long" = "Hosszú";
"Statistics" = "Statisztikák";
"Max" = "Max";
"Min" = "Minimum"; // translategemma:4b
"Reset" = "Visszaállít";
"Alignment" = "Igazítás";
"Left alignment" = "Balra igazítás";
"Center alignment" = "Középre igazítás";
"Right alignment" = "Jobbra igazítás";
"Dashboard" = "Irányítópult";
"Enabled" = "Bekapcsolva";
"Disabled" = "Kikapcsolva";
"Silent" = "Néma";
"Units" = "Mértékegységek";
"Fans" = "Ventilátorok";
"Scaling" = "Méretezés";
"Linear" = "Lineáris";
"Square" = "Négyzet";
"Cube" = "Kocka";
"Logarithmic" = "Logaritmikus";
"Fixed scale" = "Javítva"; // translategemma:4b
"Cores" = "Magok";
"Settings" = "Beállítások";
"Name" = "Név";
"Format" = "Formátum";
"Turn off" = "Kikapcsolás";
"Normal" = "Normális"; // translategemma:4b
"Warning" = "Figyelmeztetés";
"Critical" = "Kritikus";
"Usage" = "Használat";
"2 minutes" = "2 perc"; // translategemma:4b
"3 minutes" = "3 perc"; // translategemma:4b
"10 minutes" = "10 perc"; // translategemma:4b
"Import" = "Import";
"Export" = "Kivonás"; // translategemma:4b
"Separator" = "Elválasztó"; // translategemma:4b
"Read" = "Olvassa el"; // translategemma:4b
"Write" = "Ír"; // translategemma:4b
"Frequency" = "Hajlamosaság"; // translategemma:4b
"Save" = "Mentés"; // translategemma:4b
"Run" = "Futtass"; // translategemma:4b
"Stop" = "Megállj"; // translategemma:4b
"Uninstall" = "Feltörlés"; // translategemma:4b
"1 sec" = "1 másodperc"; // translategemma:4b
"2 sec" = "2 másodperc"; // translategemma:4b
"3 sec" = "3 másodperc"; // translategemma:4b
"5 sec" = "5 másodperc"; // translategemma:4b
"10 sec" = "10 másodperc"; // translategemma:4b
"15 sec" = "15 másodperc"; // translategemma:4b
"30 sec" = "30 másodperc"; // translategemma:4b
"60 sec" = "60 másodperc"; // translategemma:4b
// Setup
"Stats Setup" = "A Stats beállítása";
"Previous" = "Előző";
"Previous page" = "Előző oldal";
"Next" = "Következő";
"Next page" = "Következő oldal";
"Finish" = "Befejezés";
"Finish setup" = "Beállítás befejezése";
"Welcome to Stats" = "Üdvözlünk a Stats programban";
"welcome_message" = "Köszönjük, hogy a Stats programot használod, ami egy ingyenes, nyílt forráskódú, macOS rendszer monitorozó alkalmazás a menüsorodba.";
"Start the application automatically when starting your Mac" = "Az alkalmazás indítása a rendszer indulásakor";
"Do not start the application automatically when starting your Mac" = "Ne indítsa az alkalmazást a rendszer indulásakor";
"Do everything silently in the background (recommended)" = "Végezzen mindent csendben a háttérben (ajánlott)";
"Check for a new version on startup" = "Új verzió keresése indításkor";
"Check for a new version every day (once a day)" = "Új verzió keresése minden nap (napi egyszer)";
"Check for a new version every week (once a week)" = "Új verzió keresése minden héten (heti egyszer)";
"Check for a new version every month (once a month)" = "Új verzió keresése minden hónapban (havi egyszer)";
"Never check for updates (not recommended)" = "Ne keressen frissítéseket (nem ajánlott)";
"Anonymous telemetry for better development decisions" = "Névtelen telemetria a jobb fejlesztői döntések érdekében";
"Share anonymous telemetry data" = "Névtelen telemetriai adatok megosztása";
"Do not share anonymous telemetry data" = "Ne osszon meg névtelen telemetriai adatokat";
"The configuration is completed" = "A konfiguráció befejezve";
"finish_setup_message" = "Minden beállítva!\nA Stats egy nyílt forráskódú eszköz, ami ingyenes és az is marad.\nHa hasznosnak találod, támogathatod a fejlesztését, mindig nagyra értékeljük!";
// Alerts
"New version available" = "Új verzió érhető el";
"Click to install the new version of Stats" = "Kattintson a Stats új verziójának telepítéséhez";
"Successfully updated" = "Sikeres frissítés";
"Stats was updated to v" = "A Stats sikeresen frissítve lett a(z) v%0 verzióra";
"Reset settings text" = "Minden alkalmazás beállítás alaphelyzetbe fog állni, és az alkalmazás újra fog indulni. Biztosan ezt szeretnéd?";
"Support text" = "Köszönjük, hogy használja a Stats-t!\n\n A nyílt forráskódú projekt fenntartása és fejlesztése időt és erőforrásokat igényel. Az Ön támogatása segít nekünk abban, hogy továbbra is ingyenes és megbízható alkalmazást nyújtsunk mindenkinek.\n\nHa hasznosnak találja a Stats-t, kérjük, fontolja meg, hogy hozzájáruljon. Minden kis aprócska összeg segít!";
// Settings
"Open Activity Monitor" = "Tevékenységfigyelő megnyitása";
"Report a bug" = "Probléma jelentése";
"Support the application" = "Támogasd az alkalmazás fejlesztését";
"Close application" = "Alkalmazás bezárása";
"Open application settings" = "Beállítások megnyitása";
"Open dashboard" = "Irányítópult megnyitása";
"No notifications available in this module" = "Nincsenek elérhető értesítések ebben a modulban";
"Open Calendar" = "Nyisd meg a naptárat"; // translategemma:4b
"Toggle the module" = "Engedje be/ki a modult"; // translategemma:4b
// Application settings
"Update application" = "Alkalmazás frissítése";
"Check for updates" = "Frissítések keresése";
"At start" = "Induláskor";
"Once per day" = "Naponta";
"Once per week" = "Hetente";
"Once per month" = "Havonta";
"Never" = "Soha";
"Check for update" = "Frissítés keresése";
"Show icon in dock" = "Ikon megjelenítése a Dockban";
"Start at login" = "Alkalmazás indítása bejelentkezéskor";
"Build number" = "Verziószám";
"Import settings" = "Import beállítások"; // translategemma:4b
"Export settings" = "Export beállítások"; // translategemma:4b
"Reset settings" = "Beállítások alaphelyzetbe állítása";
"Pause the Stats" = "Stats felfüggesztése";
"Resume the Stats" = "Stats folytatása";
"Combined modules" = "Egyesített modulok";
"Combined details" = "Összesített adatok"; // translategemma:4b
"Spacing" = "Helyköz";
"Share anonymous telemetry" = "Telemetria megosztása névtelenül";
"Choose file" = "Válasszon fájlt"; // translategemma:4b
"Stress tests" = "Feszültségtesztek"; // translategemma:4b
// Dashboard
"Serial number" = "Sorozatszám";
"Model identifier" = "Modell azonosító"; // translategemma:4b
"Production year" = "Gyártás éve"; // translategemma:4b
"Uptime" = "Indítás óta eltel idő";
"Number of cores" = "%0 mag";
"Number of threads" = "%0 szál";
"Number of e-cores" = "%0 energiatakarékos mag";
"Number of p-cores" = "%0 teljesítményre optimalizált mag";
"Disks" = "Merevlemezek"; // translategemma:4b
"Display" = "Megjelenítés"; // translategemma:4b
// Update
"The latest version of Stats installed" = "A Stats legújabb verziója van telepítve";
"Downloading..." = "Letöltés...";
"Current version: " = "Jelenlegi verzió: ";
"Latest version: " = "Legújabb verzió: ";
// Widgets
"Color" = "Szín";
"Label" = "Címke megjelenítése";
"Box" = "Háttér";
"Frame" = "Keret";
"Value" = "Érték megjelenítése";
"Colorize" = "Színezés";
"Colorize value" = "Érték színezése";
"Additional information" = "További információk megjelenítése";
"Reverse values order" = "Értékek sorrendjének megfordítása";
"Base" = "Mértékegység";
"Display mode" = "Megjelenítés mód";
"One row" = "Egy sor";
"Two rows" = "Két sor";
"Mini widget" = "Mini";
"Line chart widget" = "Vonaldiagram";
"Bar chart widget" = "Oszlopdiagram";
"Pie chart widget" = "Kördiagram";
"Network chart widget" = "Hálózati diagram";
"Speed widget" = "Sebesség";
"Battery widget" = "Akkumulátor";
"Stack widget" = "Szerkezet"; // translategemma:4b
"Memory widget" = "Memória";
"Static width" = "Fix szélesség";
"Tachometer widget" = "Fordulatszámmérő";
"State widget" = "Állapot";
"Text widget" = "Tekstbox"; // translategemma:4b
"Battery details widget" = "A akkumulátor részletei widget"; // translategemma:4b
"Show symbols" = "Szimbólumok mutatása";
"Label widget" = "Címke";
"Number of reads in the chart" = "Értékek száma a grafikonon";
"Color of download" = "Letöltés színe";
"Color of upload" = "Feltöltés színe";
"Monospaced font" = "Mono betűtípus";
"Reverse order" = "Fordított sorrend";
"Chart history" = "Grafikonok története"; // translategemma:4b
"Default color" = "Alapérték"; // translategemma:4b
"Transparent when no activity" = "Árnyalatless, ha nincs tevékenység"; // translategemma:4b
"Constant color" = "Konstans"; // translategemma:4b
// Module Kit
"Open module settings" = "Modul beállításainak megnyitása";
"Select widget" = "%0 modul kiválasztása";
"Open widget settings" = "Modul beállításainak megnyitása";
"Update interval" = "Frissítési időköz";
"Usage history" = "Használati előzmények";
"Details" = "Részletek";
"Top processes" = "Top folyamatok";
"Pictogram" = "Piktogram";
"Module" = "Modul";
"Widgets" = "Widgetek";
"Popup" = "Felugrik";
"Notifications" = "Értesítések";
"Merge widgets" = "Widgetek egyesítése";
"No available widgets to configure" = "Nincs elérhető widget a beállításhoz";
"No options to configure for the popup in this module" = "A modul lenyíló ablakához nem tartozik beállítás";
"Process" = "Folyamat"; // translategemma:4b
"Kill process" = "Átírd a folyamatot"; // translategemma:4b
"Keyboard shortcut" = "Billentyűparancs"; // translategemma:4b
"Listening..." = "Hallgat..."; // translategemma:4b
// Modules
"Number of top processes" = "Top folyamatok száma";
"Update interval for top processes" = "Top folyamatok frissítési időköze";
"Notification level" = "Értesítési szint";
"Chart color" = "Diagram színe";
"Main chart scaling" = "Fődiagram méretezése"; // translategemma:4b
"Scale value" = "Skálázási érték"; // translategemma:4b
"Text widget value" = "A szöveges widget értéke"; // translategemma:4b
// CPU
"CPU usage" = "CPU használat";
"CPU temperature" = "CPU hőmérséklet";
"CPU frequency" = "CPU frekvencia";
"System" = "Rendszer";
"User" = "Felhasználó";
"Idle" = "Tétlenség";
"Show usage per core" = "Terhelés megjelenítése magonként";
"Show hyper-threading cores" = "Hyper-threading magok mutatása";
"Split the value (System/User)" = "Ossza fel az értéket (Rendszer/Felhasználó)";
"Scheduler limit" = "Ütemező korlát";
"Speed limit" = "Sebesség korlát";
"Average load" = "Átlagos terheltség";
"1 minute" = "1 perc";
"5 minutes" = "5 perc";
"15 minutes" = "15 perc";
"CPU usage threshold" = "CPU használati küszöb";
"CPU usage is" = "%0 a CPU használat";
"Efficiency cores" = "Energiatakarékos magok";
"Performance cores" = "Teljesítményre optimalizált magok";
"System color" = "Rendszer színe";
"User color" = "Felhasználó színe";
"Idle color" = "Tétlenség színe";
"Cluster grouping" = "Csoportosítás";
"Efficiency cores color" = "Energiatakarékos magok színe";
"Performance cores color" = "Teljesítmény magok színe";
"Total load" = "Teljes terheltség";
"System load" = "Rendszer terheltség";
"User load" = "Felhasználó terheltség";
"Efficiency cores load" = "Energiatakarékos magok terheltsége";
"Performance cores load" = "Teljesítmény magok terheltsége";
"All cores" = "Minden mag"; // translategemma:4b
// GPU
"GPU to show" = "Megjelenítendő GPU";
"Show GPU type" = "GPU típus mutatása";
"GPU enabled" = "GPU bekapcsolva";
"GPU disabled" = "GPU kikapcsolva";
"GPU temperature" = "GPU hőmérséklet";
"GPU utilization" = "GPU terhelés";
"Vendor" = "Gyártó";
"Model" = "Modell";
"Status" = "Állapot";
"Active" = "Aktív";
"Non active" = "Inaktív";
"Fan speed" = "Ventilátor sebesség";
"Core clock" = "Mag órajel";
"Memory clock" = "Memória órajel";
"Utilization" = "Felhasználás";
"Render utilization" = "Render használat";
"Tiler utilization" = "Tiler használat";
"GPU usage threshold" = "GPU használati küszöb";
"GPU usage is" = "%0 a GPU használat";
// RAM
"Memory usage" = "Memóriahasználat";
"Memory pressure" = "Memóriaterhelés";
"Total" = "Teljes";
"Used" = "Felhasznált";
"App" = "Alkalmazás";
"Wired" = "Nem lapozható";
"Compressed" = "Tömörített";
"Free" = "Szabad";
"Swap" = "Csereterület";
"Split the value (App/Wired/Compressed)" = "Ossza fel az értéket (Alkalmazás/Nem lapozható/Tömörített)";
"RAM utilization threshold" = "RAM használati küszöb";
"RAM utilization is" = "%0 a RAM használat";
"App color" = "Alkalmazás színe";
"Wired color" = "Nem lapozható színe";
"Compressed color" = "Tömörített színe";
"Free color" = "Szabad színe";
"Free memory (less than)" = "Szabad memória (kevesebb mint)";
"Swap size" = "Swap mérete";
"Free RAM is" = "%0 a szabad RAM";
// Disk
"Show removable disks" = "Eltávolítható adattárolók megjelenítése";
"Used disk memory" = "%0 felhasznált %1-ból";
"Free disk memory" = "%0 szabad %1-ból";
"Disk to show" = "Megjelenítendő lemez";
"Open disk" = "Merevlemez megnyitása";
"Switch view" = "Nézet felcserélése";
"Disk utilization threshold" = "Lemez használati küszöb";
"Disk utilization is" = "%0 a lemez használat";
"Read color" = "Olvasás színe";
"Write color" = "Írás színe";
"Disk usage" = "Lemez használat";
"Total read" = "Teljes olvasott"; // translategemma:4b
"Total written" = "Teljes írásos"; // translategemma:4b
"Write speed" = "Ír"; // translategemma:4b
"Read speed" = "Olvassa el"; // translategemma:4b
"Drives" = "Üzemek"; // translategemma:4b
"SMART data" = "SMART adatok"; // translategemma:4b
// Sensors
"Temperature unit" = "Hőmérséklet mértékegysége";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Ventilátor sebesség mentése";
"Fan" = "Ventilátor";
"HID sensors" = "HID szenzorok";
"Synchronize fan's control" = "Ventilátor vezérlés szinkronizálása";
"Current" = "Áramerősség";
"Energy" = "Energia";
"Show unknown sensors" = "Ismeretlen szenzorok megjelenítése";
"Install fan helper" = "Ventilátor segítő telepítése";
"Uninstall fan helper" = "Ventilátor segítő törlése";
"Fan value" = "Ventilátor érték";
"Turn off fan" = "Ventilátor kikapcsolása";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "A ventilátor ki lesz kapcsolva. Ez a művelet károsíthatja a macjét, ezért nem ajánlott. Biztosan ki akarja kapcsolni a ventilátort?";
"Sensor threshold" = "Szenzor küszöb";
"Left fan" = "Bal ventilátor";
"Right fan" = "Jobb ventilátor";
"Fastest fan" = "Leggyorsabb ventilátor";
"Sensor to show" = "Érzékelő, amely..."; // translategemma:4b
// Network
"Uploading" = "Feltöltés";
"Downloading" = "Letöltés";
"Public IP" = "Publikus IP";
"Local IP" = "Helyi IP";
"Interface" = "Interfész";
"Physical address" = "Fizikai cím";
"Refresh" = "Frissítés";
"Click to copy public IP address" = "Kattints a publikus IP cím másolásához";
"Click to copy local IP address" = "Kattints a helyi IP cím másolásához";
"Click to copy wifi name" = "Kattints a Wi-Fi nevének másolásához";
"Click to copy mac address" = "Kattints a fizikai cím másolásához";
"No connection" = "Nincs kapcsolat";
"Network interface" = "Hálózati interfész kiválasztása";
"Total download" = "Fogadott adatok";
"Total upload" = "Küldött adatok";
"Reader type" = "Olvasó típusa";
"Interface based" = "Interfész alapú";
"Processes based" = "Folyamat alapú";
"Reset data usage" = "Adathasználati adatok alaphelyzetbe állítása";
"VPN mode" = "VPN mód";
"Standard" = "Szabvány";
"Security" = "Biztonság";
"Channel" = "Csatorna";
"Common scale" = "Azonos méretezés";
"Autodetection" = "Automatikus észlelés";
"Widget activation threshold" = "Widget aktivációs küszöbe";
"Internet connection" = "Internet kapcsolat";
"Active state color" = "Aktív állapot színe";
"Nonactive state color" = "Inaktív állapot színe";
"Connectivity host (ICMP)" = "Kapcsolati gazdagép (ICMP)";
"Leave empty to disable the check" = "Hagyd üresen az ellenőrzés kikapcsolásához";
"Connectivity history" = "Kapcsolati előzmények";
"Auto-refresh public IP address" = "Publikus IP cím automatikus frissítése";
"Every hour" = "Óránként";
"Every 12 hours" = "12 óránként";
"Every 24 hours" = "24 óránként";
"Network activity" = "Hálózati tevékenység";
"Last reset" = "Utolsó visszaállítás óta eltelt idő: %0";
"Latency" = "Válaszkazán"; // translategemma:4b
"Upload speed" = "Feltöltés"; // translategemma:4b
"Download speed" = "Letöltés"; // translategemma:4b
"Address" = "Cím"; // translategemma:4b
"WiFi network" = "WiFi hálózat"; // translategemma:4b
"Local IP changed" = "A helyi IP-cím megváltozott"; // translategemma:4b
"Public IP changed" = "A nyilvános IP-címet megváltoztatták"; // translategemma:4b
"Previous IP" = "Korábbi IP cím: %0"; // translategemma:4b
"New IP" = "Új IP cím: %0"; // translategemma:4b
"Internet connection lost" = "Az internetkapcsolat megszűnt"; // translategemma:4b
"Internet connection established" = "Az internetkapcsolat létrejött"; // translategemma:4b
// Battery
"Level" = "Töltöttségi szint";
"Source" = "Forrás";
"AC Power" = "Hálózati tápellátás";
"Battery Power" = "Akkumulátor tápellátás";
"Time" = "Időtartam";
"Health" = "Állapot";
"Amperage" = "Áramerősség";
"Voltage" = "Feszültség";
"Cycles" = "Ciklusok";
"Temperature" = "Hőmérséklet";
"Power adapter" = "Tápellátás";
"Power" = "Teljesítmény";
"Is charging" = "Jelenleg tölt";
"Time to discharge" = "Idő a lemerülésig";
"Time to charge" = "Idő a feltöltésig";
"Calculating" = "Kiszámítás";
"Fully charged" = "Teljesen feltöltve";
"Not connected" = "Nincs csatlakoztatva";
"Low level notification" = "Alacsony töltöttség értesítés";
"High level notification" = "Magas töltöttség értesítés";
"Low battery" = "Alacsony akkumulátor töltöttség";
"High battery" = "Magas akkumulátor töltöttség";
"Battery remaining" = "%0% töltöttség";
"Battery remaining to full charge" = "%0% hátra a teljes töltöttséghez";
"Percentage" = "Százalék";
"Percentage and time" = "Százalék és időtartam";
"Time and percentage" = "Időtartam és százalék";
"Time format" = "Időformátum";
"Hide additional information when full" = "További információk elrejtése, ha teljesen fel van töltve";
"Last charge" = "Utolsó töltés";
"Capacity" = "Kapacitás";
"current / maximum / designed" = "jelenlegi / maximum / gyári";
"Low power mode" = "Energia takarékos mód";
"Percentage inside the icon" = "Százalék megjelenítése az ikonban";
"Colorize battery" = "Akkumlátor színezése";
"Charging current" = "Töltési áram"; // translategemma:4b
"Charging Voltage" = "Töltési feszültség"; // translategemma:4b
"Charger state inside the battery" = "A töltő állapot a akkumulátorban"; // translategemma:4b
// Bluetooth
"Battery to show" = "Akkumulátor megjelenítése";
"No Bluetooth devices are available" = "Nincs elérhető Bluetooth eszköz";
// Clock
"Time zone" = "Időzóna";
"Local" = "Helyi";
"Calendar" = "Naptár"; // translategemma:4b
"Show week numbers" = "Mutassa meg a hetek számát"; // translategemma:4b
"Local time" = "Helyi idő"; // translategemma:4b
"Add new clock" = "Hozzon létre új órát"; // translategemma:4b
"Delete selected clock" = "Telenóra törlése"; // translategemma:4b
"Help with datetime format" = "Segítség a dátum és idő formázásában"; // translategemma:4b
// Colors
"Based on utilization" = "Használat függő";
"Based on pressure" = "Terhelés függő";
"Based on cluster" = "Csoport függő";
"System accent" = "Rendszer kiemelő színe";
"Monochrome accent" = "Monokróm kiemelő szín";
"Clear" = "Átlátszó";
"White" = "Fehér";
"Black" = "Fekete";
"Gray" = "Szürke";
"Second gray" = "Második szürke";
"Dark gray" = "Sötét szürke";
"Light gray" = "Világos szürke";
"Red" = "Piros";
"Second red" = "Második piros";
"Green" = "Zöld";
"Second green" = "Második zöld";
"Blue" = "Kék";
"Second blue" = "Második kék";
"Yellow" = "Citromsárga";
"Second yellow" = "Második citromsárga";
"Orange" = "Narancssárga";
"Second orange" = "Második narancssárga";
"Purple" = "Lila";
"Second purple" = "Második lila";
"Brown" = "Barna";
"Second brown" = "Második barna";
"Cyan" = "Cián";
"Magenta" = "Magenta";
"Pink" = "Rózsaszín";
"Teal" = "Zöldeskék";
"Indigo" = "Indigó";
================================================
FILE: Stats/Supporting Files/id.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "Buka pengaturan CPU";
"GPU" = "GPU";
"Open GPU settings" = "Buka pengaturan GPU";
"RAM" = "RAM";
"Open RAM settings" = "Buka pengaturan RAM";
"Disk" = "Hard disk"; // translategemma:4b
"Open Disk settings" = "Buka pengaturan Disk";
"Sensors" = "Sensor";
"Open Sensors settings" = "Buka pengaturan sensor";
"Network" = "Jaringan";
"Open Network settings" = "Buka pengaturan jaringan";
"Battery" = "Baterai";
"Open Battery settings" = "Buka pengaturan baterai";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Buka pengaturan bluetooth";
"Clock" = "Jam"; // translategemma:4b
"Open Clock settings" = "Buka pengaturan jam"; // translategemma:4b
// Words
"Unknown" = "Tidak dikenal";
"Version" = "Versi";
"Processor" = "Prosesor";
"Memory" = "Memori";
"Graphics" = "Grafis";
"Close" = "Tutup";
"Download" = "Unduh";
"Install" = "Pasang";
"Cancel" = "Batal";
"Unavailable" = "Tidak tersedia";
"Yes" = "Ya";
"No" = "Tidak";
"Automatic" = "Otomatis";
"Manual" = "Panduan"; // translategemma:4b
"None" = "Tidak ada";
"Dots" = "Titik";
"Arrows" = "Anak panah";
"Characters" = "Karakter";
"Short" = "Pendek";
"Long" = "Panjang";
"Statistics" = "Statistik";
"Max" = "Maks";
"Min" = "Minimal"; // translategemma:4b
"Reset" = "Atur ulang";
"Alignment" = "Penyelarasan"; // translategemma:4b
"Left alignment" = "Alignment Kiri";
"Center alignment" = "Alignment tengah";
"Right alignment" = "Alignment kanan";
"Dashboard" = "Dasbor";
"Enabled" = "Diaktifkan"; // translategemma:4b
"Disabled" = "Dinonaktifkan";
"Silent" = "Hening";
"Units" = "Unit";
"Fans" = "Kipas";
"Scaling" = "Skala"; // translategemma:4b
"Linear" = "Linear";
"Square" = "Perusahaan"; // translategemma:4b
"Cube" = "Kubus"; // translategemma:4b
"Logarithmic" = "Logaritmik"; // translategemma:4b
"Fixed scale" = "Sudah diperbaiki"; // translategemma:4b
"Cores" = "Inti"; // translategemma:4b
"Settings" = "Pengaturan"; // translategemma:4b
"Name" = "Nama"; // translategemma:4b
"Format" = "Format";
"Turn off" = "Matikan"; // translategemma:4b
"Normal" = "Normal";
"Warning" = "Peringatan"; // translategemma:4b
"Critical" = "Penting"; // translategemma:4b
"Usage" = "Penggunaan"; // translategemma:4b
"2 minutes" = "2 menit"; // translategemma:4b
"3 minutes" = "3 menit"; // translategemma:4b
"10 minutes" = "10 menit"; // translategemma:4b
"Import" = "Impor"; // translategemma:4b
"Export" = "Ekspor"; // translategemma:4b
"Separator" = "Pemisah"; // translategemma:4b
"Read" = "Baca"; // translategemma:4b
"Write" = "Tulis"; // translategemma:4b
"Frequency" = "Frekuensi"; // translategemma:4b
"Save" = "Simpan"; // translategemma:4b
"Run" = "Jalankan"; // translategemma:4b
"Stop" = "Berhenti"; // translategemma:4b
"Uninstall" = "Hapus"; // translategemma:4b
"1 sec" = "1 detik"; // translategemma:4b
"2 sec" = "2 detik"; // translategemma:4b
"3 sec" = "3 detik"; // translategemma:4b
"5 sec" = "5 detik"; // translategemma:4b
"10 sec" = "10 detik"; // translategemma:4b
"15 sec" = "15 detik"; // translategemma:4b
"30 sec" = "30 detik"; // translategemma:4b
"60 sec" = "60 detik"; // translategemma:4b
// Setup
"Stats Setup" = "Pengaturan Statistik"; // translategemma:4b
"Previous" = "Sebelumnya"; // translategemma:4b
"Previous page" = "Halaman sebelumnya"; // translategemma:4b
"Next" = "Selanjutnya"; // translategemma:4b
"Next page" = "Halaman berikutnya"; // translategemma:4b
"Finish" = "Selesai"; // translategemma:4b
"Finish setup" = "Selesaikan pengaturan"; // translategemma:4b
"Welcome to Stats" = "Selamat datang di Stats"; // translategemma:4b
"welcome_message" = "Terima kasih telah menggunakan Stats, sebuah pemantau sistem macOS sumber terbuka dan gratis untuk bilah menu Anda."; // translategemma:4b
"Start the application automatically when starting your Mac" = "Mulai aplikasi secara otomatis saat memulai Mac Anda"; // translategemma:4b
"Do not start the application automatically when starting your Mac" = "Jangan memulai aplikasi secara otomatis saat Anda menyalakan Mac Anda."; // translategemma:4b
"Do everything silently in the background (recommended)" = "Lakukan semuanya secara diam-diam di latar belakang (disarankan)"; // translategemma:4b
"Check for a new version on startup" = "Periksa versi terbaru saat memulai aplikasi."; // translategemma:4b
"Check for a new version every day (once a day)" = "Periksa versi terbaru setiap hari (sekali sehari)"; // translategemma:4b
"Check for a new version every week (once a week)" = "Periksa versi terbaru setiap minggu (sekali seminggu)"; // translategemma:4b
"Check for a new version every month (once a month)" = "Periksa versi terbaru setiap bulan (sekali dalam sebulan)"; // translategemma:4b
"Never check for updates (not recommended)" = "Jangan pernah memeriksa pembaruan (tidak disarankan)"; // translategemma:4b
"Anonymous telemetry for better development decisions" = "Data telemetry anonim untuk pengambilan keputusan pengembangan yang lebih baik"; // translategemma:4b
"Share anonymous telemetry data" = "Bagikan data telemetri anonim"; // translategemma:4b
"Do not share anonymous telemetry data" = "Jangan membagikan data telemetri tanpa identitas."; // translategemma:4b
"The configuration is completed" = "Konfigurasi telah selesai."; // translategemma:4b
"finish_setup_message" = "Semuanya sudah siap!"; // translategemma:4b
// Alerts
"New version available" = "Versi baru tersedia";
"Click to install the new version of Stats" = "Klik untuk memasang versi baru dari Stats";
"Successfully updated" = "Pembaruan berhasil";
"Stats was updated to v" = "Stats telah diperbarui v%0";
"Reset settings text" = "Semua pengaturan aplikasi akan diatur ulang, dan aplikasi akan mengulang kembali. Anda yakin untuk melakukan ini?";
"Support text" = "Terima kasih telah menggunakan Stats!\n\nMemelihara dan meningkatkan proyek sumber terbuka ini membutuhkan waktu dan sumber daya. Dukungan Anda membantu kami untuk terus menyediakan aplikasi yang gratis dan dapat diandalkan untuk semua orang.\n\nJika Anda merasa Stats bermanfaat, mohon pertimbangkan untuk memberikan kontribusi. Setiap hal kecil akan sangat membantu!";
// Settings
"Open Activity Monitor" = "Buka Monitor Aktivitas";
"Report a bug" = "Laporkan bug";
"Support the application" = "Dukung aplikasi ini";
"Close application" = "Tutup aplikasi";
"Open application settings" = "Buka pengaturan aplikasi";
"Open dashboard" = "Buka dasbor";
"No notifications available in this module" = "Tidak ada notifikasi yang tersedia dalam modul ini"; // translategemma:4b
"Open Calendar" = "Buka Kalender"; // translategemma:4b
"Toggle the module" = "Aktifkan/nonaktifkan modul"; // translategemma:4b
// Application settings
"Update application" = "Perbarui aplikasi";
"Check for updates" = "Periksa pembaruan";
"At start" = "Saat memulai";
"Once per day" = "Sekali setiap hari";
"Once per week" = "Sekali setiap minggu";
"Once per month" = "Sekali setiap bulan";
"Never" = "Jangan pernah";
"Check for update" = "Periksa pembaruan";
"Show icon in dock" = "Tampilkan ikon pada dok";
"Start at login" = "Mulai saat login";
"Build number" = "Nomor versi"; // translategemma:4b
"Import settings" = "Impor pengaturan"; // translategemma:4b
"Export settings" = "Pengaturan ekspor"; // translategemma:4b
"Reset settings" = "Atur ulang pengaturan";
"Pause the Stats" = "Jeda Statistik"; // translategemma:4b
"Resume the Stats" = "Kembalikan Statistik"; // translategemma:4b
"Combined modules" = "Modul yang digabungkan"; // translategemma:4b
"Combined details" = "Rincian gabungan"; // translategemma:4b
"Spacing" = "Jarak"; // translategemma:4b
"Share anonymous telemetry" = "Bagikan data telemetry tanpa mengungkapkan identitas"; // translategemma:4b
"Choose file" = "Pilih file"; // translategemma:4b
"Stress tests" = "Uji ketahanan"; // translategemma:4b
// Dashboard
"Serial number" = "Nomor serial";
"Model identifier" = "Pengidentifikasi model"; // translategemma:4b
"Production year" = "Tahun produksi"; // translategemma:4b
"Uptime" = "Waktu aktif";
"Number of cores" = "%0 core";
"Number of threads" = "%0 thread";
"Number of e-cores" = "%0 inti efisiensi"; // translategemma:4b
"Number of p-cores" = "%0 inti kinerja"; // translategemma:4b
"Disks" = "Disk"; // translategemma:4b
"Display" = "Tampilan"; // translategemma:4b
// Update
"The latest version of Stats installed" = "Versi terbaru dari Stats telah dipasang";
"Downloading..." = "Mengunduh...";
"Current version: " = "Versi sekarang: ";
"Latest version: " = "Versi terbaru: ";
// Widgets
"Color" = "Warna";
"Label" = "Label";
"Box" = "Kotak";
"Frame" = "Bingkai";
"Value" = "Nilai";
"Colorize" = "Warna"; // translategemma:4b
"Colorize value" = "Nilai pewarnaan"; // translategemma:4b
"Additional information" = "Informasi tambahan";
"Reverse values order" = "Balikkan posisi nilai";
"Base" = "Basis";
"Display mode" = "Mode tampilan";
"One row" = "Satu baris";
"Two rows" = "Dua baris";
"Mini widget" = "Kecil";
"Line chart widget" = "Diagram garis";
"Bar chart widget" = "Diagram batang";
"Pie chart widget" = "Diagram pie";
"Network chart widget" = "Diagram jaringan";
"Speed widget" = "Kecepatan";
"Battery widget" = "Baterai";
"Stack widget" = "Tumpukan"; // translategemma:4b
"Memory widget" = "Memori";
"Static width" = "Lebar statis";
"Tachometer widget" = "Takometer";
"State widget" = "Widget negara"; // translategemma:4b
"Text widget" = "Elemen widget teks"; // translategemma:4b
"Battery details widget" = "Widget detail baterai"; // translategemma:4b
"Show symbols" = "Tampilkan simbol";
"Label widget" = "Label";
"Number of reads in the chart" = "Jumlah yang dibaca pada diagram";
"Color of download" = "Warna pada unduhan";
"Color of upload" = "Warna pada unggahan";
"Monospaced font" = "Fon monospasi";
"Reverse order" = "Urutan terbalik"; // translategemma:4b
"Chart history" = "Sejarah grafik"; // translategemma:4b
"Default color" = "Nilai default"; // translategemma:4b
"Transparent when no activity" = "Transparan saat tidak ada aktivitas"; // translategemma:4b
"Constant color" = "Konstan"; // translategemma:4b
// Module Kit
"Open module settings" = "Buka pengaturan modul";
"Select widget" = "%0 widget dipilih";
"Open widget settings" = "Buka pengaturan widget";
"Update interval" = "Perbarui waktu jeda";
"Usage history" = "Riwayat penggunaan";
"Details" = "Rincian";
"Top processes" = "Proses teratas";
"Pictogram" = "Piktogram";
"Module" = "Modul";
"Widgets" = "Widget"; // translategemma:4b
"Popup" = "Muncul";
"Notifications" = "Notifikasi";
"Merge widgets" = "Gabungkan widget";
"No available widgets to configure" = "Tidak ada widget yang tersedia untuk dikonfigurasi";
"No options to configure for the popup in this module" = "Tidak ada opsi untuk mengonfigurasi popup di modul ini";
"Process" = "Proses"; // translategemma:4b
"Kill process" = "Hentikan proses"; // translategemma:4b
"Keyboard shortcut" = "Singkatan tombol"; // translategemma:4b
"Listening..." = "Sedang mendengarkan..."; // translategemma:4b
// Modules
"Number of top processes" = "Jumlah dari proses teratas";
"Update interval for top processes" = "Pembaruan waktu jeda dari proses teratas";
"Notification level" = "Tingkat pemberitahuan";
"Chart color" = "Warna bagan";
"Main chart scaling" = "Skala grafik utama"; // translategemma:4b
"Scale value" = "Nilai skala"; // translategemma:4b
"Text widget value" = "Nilai widget teks"; // translategemma:4b
// CPU
"CPU usage" = "Penggunaan CPU";
"CPU temperature" = "Suhu CPU";
"CPU frequency" = "Frekuensi CPU";
"System" = "Sistem";
"User" = "Pengguna";
"Idle" = "Tidak berjalan";
"Show usage per core" = "Tampilan penggunaan tiap core";
"Show hyper-threading cores" = "Tampilkan hyper-threading core";
"Split the value (System/User)" = "Pisahkan nilai (Sistem/User)";
"Scheduler limit" = "Batas penjadwal";
"Speed limit" = "Batas kecepatan";
"Average load" = "Beban rata-rata";
"1 minute" = "1 menit";
"5 minutes" = "5 menit";
"15 minutes" = "15 menit";
"CPU usage threshold" = "Batas penggunaan CPU"; // translategemma:4b
"CPU usage is" = "Penggunaan CPU adalah %0"; // translategemma:4b
"Efficiency cores" = "Inti efisiensi"; // translategemma:4b
"Performance cores" = "Inti kinerja"; // translategemma:4b
"System color" = "Warna sistem"; // translategemma:4b
"User color" = "Warna pengguna"; // translategemma:4b
"Idle color" = "Warna saat tidak digunakan"; // translategemma:4b
"Cluster grouping" = "Pengelompokan klaster"; // translategemma:4b
"Efficiency cores color" = "Inti warna"; // translategemma:4b
"Performance cores color" = "Warna inti kinerja"; // translategemma:4b
"Total load" = "Total beban"; // translategemma:4b
"System load" = "Beban sistem"; // translategemma:4b
"User load" = "Beban pengguna"; // translategemma:4b
"Efficiency cores load" = "Prosesor inti mulai bekerja"; // translategemma:4b
"Performance cores load" = "Prosesor inti memuat"; // translategemma:4b
"All cores" = "Semua inti"; // translategemma:4b
// GPU
"GPU to show" = "GPU yang ditampilkan";
"Show GPU type" = "Tampilkan jenis GPU";
"GPU enabled" = "GPU dinyalakan";
"GPU disabled" = "GPU dinonaktifkan";
"GPU temperature" = "Suhu GPU";
"GPU utilization" = "Pemanfaatan GPU";
"Vendor" = "Pemasok"; // translategemma:4b
"Model" = "Model";
"Status" = "Status";
"Active" = "Aktif";
"Non active" = "Tidak aktif";
"Fan speed" = "Kecepatan kipas";
"Core clock" = "Frekuensi inti"; // translategemma:4b
"Memory clock" = "Memori clock";
"Utilization" = "Pemanfaatan";
"Render utilization" = "Pemanfaatan render";
"Tiler utilization" = "Pemanfaatan tiler";
"GPU usage threshold" = "Batas penggunaan GPU"; // translategemma:4b
"GPU usage is" = "Penggunaan GPU adalah %0"; // translategemma:4b
// RAM
"Memory usage" = "Penggunaan memori";
"Memory pressure" = "Tekanan memori";
"Total" = "Total";
"Used" = "Digunakan";
"App" = "Aplikasi"; // translategemma:4b
"Wired" = "Berkelanjutan"; // translategemma:4b
"Compressed" = "Dikompres";
"Free" = "Kosong";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Pisahkan nilai (App/Wired/Compressed)";
"RAM utilization threshold" = "Batas penggunaan RAM"; // translategemma:4b
"RAM utilization is" = "Penggunaan RAM adalah %0"; // translategemma:4b
"App color" = "Warna aplikasi"; // translategemma:4b
"Wired color" = "Warna kabel"; // translategemma:4b
"Compressed color" = "Warna yang dikompres"; // translategemma:4b
"Free color" = "Warna gratis"; // translategemma:4b
"Free memory (less than)" = "Memori gratis (kurang dari)"; // translategemma:4b
"Swap size" = "Ukuran pertukaran"; // translategemma:4b
"Free RAM is" = "RAM gratis adalah %0"; // translategemma:4b
// Disk
"Show removable disks" = "Tampilkan disk yang dapat dilepas";
"Used disk memory" = "%0 dari %1 yang digunakan";
"Free disk memory" = "%0 dari %1 yang kosong";
"Disk to show" = "Disk yang ditampilkan";
"Open disk" = "Buka disk";
"Switch view" = "Alihkan tampilan";
"Disk utilization threshold" = "Batas penggunaan disk"; // translategemma:4b
"Disk utilization is" = "Penggunaan disk adalah %0"; // translategemma:4b
"Read color" = "Baca warna"; // translategemma:4b
"Write color" = "Tulis warna"; // translategemma:4b
"Disk usage" = "Penggunaan disk"; // translategemma:4b
"Total read" = "Total dibaca"; // translategemma:4b
"Total written" = "Total yang ditulis"; // translategemma:4b
"Write speed" = "Tulis"; // translategemma:4b
"Read speed" = "Baca"; // translategemma:4b
"Drives" = "Drive"; // translategemma:4b
"SMART data" = "Data SMART"; // translategemma:4b
// Sensors
"Temperature unit" = "Unit suhu";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Simpan kecepatan kipas";
"Fan" = "Kipas";
"HID sensors" = "Sensor HID";
"Synchronize fan's control" = "Sinkronkan kontrol kipas"; // translategemma:4b
"Current" = "Saat ini"; // translategemma:4b
"Energy" = "Energi"; // translategemma:4b
"Show unknown sensors" = "Tampilkan sensor yang tidak dikenal"; // translategemma:4b
"Install fan helper" = "Instal aplikasi pembantu kipas"; // translategemma:4b
"Uninstall fan helper" = "Hapus aplikasi pembantu kipas"; // translategemma:4b
"Fan value" = "Nilai penggemar"; // translategemma:4b
"Turn off fan" = "Matikan kipas"; // translategemma:4b
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Anda akan mematikan kipas. Tindakan ini tidak disarankan dan dapat merusak MacBook Anda. Apakah Anda yakin ingin melakukannya?"; // translategemma:4b
"Sensor threshold" = "Batas sensor"; // translategemma:4b
"Left fan" = "Kiri"; // translategemma:4b
"Right fan" = "Tepat"; // translategemma:4b
"Fastest fan" = "Tercepat"; // translategemma:4b
"Sensor to show" = "Sensor untuk menampilkan"; // translategemma:4b
// Network
"Uploading" = "Unggah";
"Downloading" = "Unduh";
"Public IP" = "IP Publik";
"Local IP" = "IP Lokal";
"Interface" = "Antarmuka";
"Physical address" = "Alamat fisik";
"Refresh" = "Segarkan";
"Click to copy public IP address" = "Klik untuk menyalin alamat IP publik";
"Click to copy local IP address" = "Klik untuk menyalin alamat IP lokal";
"Click to copy wifi name" = "Klik untuk menyalin nama wifi";
"Click to copy mac address" = "Klik untuk menyalin alamat mac";
"No connection" = "Tidak ada koneksi";
"Network interface" = "Antarmuka jaringan";
"Total download" = "Total diunduh";
"Total upload" = "Total diunggah";
"Reader type" = "Tipe pembaca";
"Interface based" = "Berdasarkan antarmuka";
"Processes based" = "Berdasarkan proses";
"Reset data usage" = "Atur ulang penggunaan data";
"VPN mode" = "Mode VPN";
"Standard" = "Standar"; // translategemma:4b
"Security" = "Keamanan"; // translategemma:4b
"Channel" = "Saluran"; // translategemma:4b
"Common scale" = "Skala umum"; // translategemma:4b
"Autodetection" = "Deteksi otomatis"; // translategemma:4b
"Widget activation threshold" = "Batas ambang aktivasi widget"; // translategemma:4b
"Internet connection" = "Koneksi internet"; // translategemma:4b
"Active state color" = "Warna pada keadaan aktif"; // translategemma:4b
"Nonactive state color" = "Warna untuk keadaan tidak aktif"; // translategemma:4b
"Connectivity host (ICMP)" = "Host koneksi (ICMP)"; // translategemma:4b
"Leave empty to disable the check" = "Kosongkan untuk menonaktifkan pemeriksaan"; // translategemma:4b
"Connectivity history" = "Riwayat koneksi"; // translategemma:4b
"Auto-refresh public IP address" = "Perbarui otomatis alamat IP publik"; // translategemma:4b
"Every hour" = "Setiap jam"; // translategemma:4b
"Every 12 hours" = "Setiap 12 jam"; // translategemma:4b
"Every 24 hours" = "Setiap 24 jam"; // translategemma:4b
"Network activity" = "Aktivitas jaringan"; // translategemma:4b
"Last reset" = "Terakhir di-reset %0 lalu"; // translategemma:4b
"Latency" = "Latensi"; // translategemma:4b
"Upload speed" = "Unggah"; // translategemma:4b
"Download speed" = "Unduh"; // translategemma:4b
"Address" = "Alamat"; // translategemma:4b
"WiFi network" = "Jaringan WiFi"; // translategemma:4b
"Local IP changed" = "Alamat IP lokal telah berubah"; // translategemma:4b
"Public IP changed" = "Alamat IP publik telah berubah"; // translategemma:4b
"Previous IP" = "Alamat IP sebelumnya: %0"; // translategemma:4b
"New IP" = "Alamat IP baru: %0"; // translategemma:4b
"Internet connection lost" = "Koneksi internet terputus"; // translategemma:4b
"Internet connection established" = "Koneksi internet telah terhubung"; // translategemma:4b
// Battery
"Level" = "Tingkat"; // translategemma:4b
"Source" = "Sumber";
"AC Power" = "Daya AC";
"Battery Power" = "Daya baterai";
"Time" = "Waktu";
"Health" = "Kesehatan";
"Amperage" = "Arus listrik";
"Voltage" = "Voltase";
"Cycles" = "Siklus";
"Temperature" = "Suhu";
"Power adapter" = "Adaptor daya";
"Power" = "Daya";
"Is charging" = "Sedang mengisi";
"Time to discharge" = "Waktu untuk melepas";
"Time to charge" = "Waktu untuk mengisi daya";
"Calculating" = "Menghitung";
"Fully charged" = "Terisi penuh";
"Not connected" = "Tidak terhubung";
"Low level notification" = "Notifikasi tingkat rendah";
"High level notification" = "Notifikasi tingkat tinggi";
"Low battery" = "Baterai lemah";
"High battery" = "Baterai tinggi";
"Battery remaining" = "Tersisa %0%";
"Battery remaining to full charge" = "%0% untuk mengisi penuh";
"Percentage" = "Persentase";
"Percentage and time" = "Persentase dan waktu";
"Time and percentage" = "Waktu dan persentase";
"Time format" = "Format waktu";
"Hide additional information when full" = "Sembunyikan informasi tambahan ketika penuh";
"Last charge" = "Terakhir diisi";
"Capacity" = "Kapasitas";
"current / maximum / designed" = "current / maksimal / dirancang";
"Low power mode" = "Mode hemat daya"; // translategemma:4b
"Percentage inside the icon" = "Persentase di dalam ikon"; // translategemma:4b
"Colorize battery" = "Warna-warnai baterai"; // translategemma:4b
"Charging current" = "Arus pengisian"; // translategemma:4b
"Charging Voltage" = "Tegangan pengisian"; // translategemma:4b
"Charger state inside the battery" = "Status pengisian daya di dalam baterai"; // translategemma:4b
// Bluetooth
"Battery to show" = "Baterai untuk ditampilkan";
"No Bluetooth devices are available" = "Tidak ada perangkat bluetooth yang tersedia";
// Clock
"Time zone" = "Zona waktu"; // translategemma:4b
"Local" = "Lokal"; // translategemma:4b
"Calendar" = "Kalender"; // translategemma:4b
"Show week numbers" = "Tampilkan nomor minggu"; // translategemma:4b
"Local time" = "Waktu setempat"; // translategemma:4b
"Add new clock" = "Tambahkan jam baru"; // translategemma:4b
"Delete selected clock" = "Hapus jam yang dipilih"; // translategemma:4b
"Help with datetime format" = "Bantuan dalam memformat tanggal dan waktu"; // translategemma:4b
// Colors
"Based on utilization" = "Berdasarkan pemanfaatan";
"Based on pressure" = "Berdasarkan tekanan";
"Based on cluster" = "Berdasarkan kelompok"; // translategemma:4b
"System accent" = "Aksen berdasarkan sistem";
"Monochrome accent" = "Akses monokrom";
"Clear" = "Bening";
"White" = "Putih";
"Black" = "Hitam";
"Gray" = "Abu-abu";
"Second gray" = "Abu-abu kedua";
"Dark gray" = "Abu-abu gelap";
"Light gray" = "Abu-abu terang";
"Red" = "Merah";
"Second red" = "Merah kedua";
"Green" = "Hijau";
"Second green" = "Hijau kedua";
"Blue" = "Biru";
"Second blue" = "Biru kedua";
"Yellow" = "Kuning";
"Second yellow" = "Kuning kedua";
"Orange" = "Oranye";
"Second orange" = "Oranye kedua";
"Purple" = "Ungu";
"Second purple" = "Ungu kedua";
"Brown" = "Coklat";
"Second brown" = "Coklat kedua";
"Cyan" = "Biru kehijauan"; // translategemma:4b
"Magenta" = "Magenta";
"Pink" = "Merah muda"; // translategemma:4b
"Teal" = "Teal";
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/it.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "Apri impostazioni CPU";
"GPU" = "GPU";
"Open GPU settings" = "Apri impostazioni GPU";
"RAM" = "RAM";
"Open RAM settings" = "Apri impostazioni RAM";
"Disk" = "Disco";
"Open Disk settings" = "Apri impostazioni Disco";
"Sensors" = "Sensori";
"Open Sensors settings" = "Apri impostazioni Sensori";
"Network" = "Rete";
"Open Network settings" = "Apri impostazioni Rete";
"Battery" = "Batteria";
"Open Battery settings" = "Apri impostazioni Batteria";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Apri impostazioni Bluetooth";
"Clock" = "Orologio";
"Open Clock settings" = "Apri impostazioni Orologio";
// Words
"Unknown" = "Sconosciuto";
"Version" = "Versione";
"Processor" = "Processore";
"Memory" = "Memoria";
"Graphics" = "Grafica";
"Close" = "Chiudi";
"Download" = "Scarica";
"Install" = "Installa";
"Cancel" = "Annulla";
"Unavailable" = "Non disponibile";
"Yes" = "Sì";
"No" = "No";
"Automatic" = "Automatico";
"Manual" = "Manuale";
"None" = "Nessuno";
"Dots" = "Punti";
"Arrows" = "Frecce";
"Characters" = "Caratteri";
"Short" = "Corto";
"Long" = "Lungo";
"Statistics" = "Statistiche";
"Max" = "Massimo"; // translategemma:4b
"Min" = "Min";
"Reset" = "Resetta";
"Alignment" = "Allineamento";
"Left alignment" = "Sinistra";
"Center alignment" = "Centro";
"Right alignment" = "Destra";
"Dashboard" = "Pannello di controllo"; // translategemma:4b
"Enabled" = "Abilitato";
"Disabled" = "Disabilitato";
"Silent" = "Silenzioso";
"Units" = "Unità";
"Fans" = "Ventole";
"Scaling" = "Ridimensionamento";
"Linear" = "Lineare";
"Square" = "Quadratico";
"Cube" = "Cubico";
"Logarithmic" = "Logaritmico";
"Fixed scale" = "Scala fissa";
"Cores" = "Core";
"Settings" = "Impostazioni";
"Name" = "Nome";
"Format" = "Formato";
"Turn off" = "Disattiva";
"Normal" = "Normale"; // translategemma:4b
"Warning" = "Avviso";
"Critical" = "Critico";
"Usage" = "Utilizzo";
"2 minutes" = "2 minuti";
"3 minutes" = "3 minuti";
"10 minutes" = "10 minuti";
"Import" = "Importa";
"Export" = "Esporta";
"Separator" = "Separatore";
"Read" = "Lettura";
"Write" = "Scrittura";
"Frequency" = "Frequenza";
"Save" = "Salva"; // translategemma:4b
"Run" = "Esegui"; // translategemma:4b
"Stop" = "Smetta"; // translategemma:4b
"Uninstall" = "Disinstallare"; // translategemma:4b
"1 sec" = "1 secondo"; // translategemma:4b
"2 sec" = "2 secondi"; // translategemma:4b
"3 sec" = "3 secondi"; // translategemma:4b
"5 sec" = "5 secondi"; // translategemma:4b
"10 sec" = "10 secondi"; // translategemma:4b
"15 sec" = "15 secondi"; // translategemma:4b
"30 sec" = "30 secondi"; // translategemma:4b
"60 sec" = "60 secondi"; // translategemma:4b
// Setup
"Stats Setup" = "Configurazione Stats";
"Previous" = "Precedente";
"Previous page" = "Pagina precedente";
"Next" = "Successivo";
"Next page" = "Pagina successiva";
"Finish" = "Completa";
"Finish setup" = "Completa configurazione";
"Welcome to Stats" = "Stats ti dà il benvenuto";
"welcome_message" = "Grazie per aver scelto Stats, un sistema di monitoraggio del sistema dalla menu bar gratuito e open source per macOS.";
"Start the application automatically when starting your Mac" = "Avvia l'applicazione automaticamente all'accensione del Mac";
"Do not start the application automatically when starting your Mac" = "Non avviare l'applicazione automaticamente all'accensione del Mac";
"Do everything silently in the background (recommended)" = "Gestisci autonomamente in background (consigliato)";
"Check for a new version on startup" = "Verifica la disponibilità di nuove versioni all'avvio";
"Check for a new version every day (once a day)" = "Verifica la disponibilità di nuove versioni ogni giorno (una volta al giorno)";
"Check for a new version every week (once a week)" = "Verifica la disponibilità di nuove versioni ogni settimana (una volta a settimana)";
"Check for a new version every month (once a month)" = "Verifica la disponibilità di nuove versioni ogni mese (una volta al mese)";
"Never check for updates (not recommended)" = "Non verificare mai la disponibilità di nuove versioni (sconsigliato)";
"Anonymous telemetry for better development decisions" = "Telemetria anonima per decisioni di sviluppo migliori";
"Share anonymous telemetry data" = "Condividi dati anonimi di telemetria";
"Do not share anonymous telemetry data" = "Non condividere dati anonimi di telemetria";
"The configuration is completed" = "Configurazione completata";
"finish_setup_message" = "È tutto pronto!\nStats è un software open source gratuito, e lo rimarrà per sempre.\nSe ti piace, dacci il tuo supporto: è sempre ben accetto!";
// Alerts
"New version available" = "Nuova versione disponibile";
"Click to install the new version of Stats" = "Clicca per installare la nuova versione di Stats";
"Successfully updated" = "Aggiornato con successo";
"Stats was updated to v" = "Stats è stato aggiornato alla v%0";
"Reset settings text" = "Tutte le impostazioni saranno resettate e l'applicazione sarà riavviata. Sei sicuro di volerlo fare?";
"Support text" = "Grazie per aver usato Stats!\n\nIl mantenimento e il miglioramento di questo progetto open-source richiede tempo e risorse. Il vostro sostegno ci aiuta a continuare a fornire un'applicazione gratuita e affidabile per tutti.\n\nSe trovate Stats utile, prendete in considerazione l'idea di dare un contributo. Ogni piccolo contributo è utile!";
// Settings
"Open Activity Monitor" = "Apri Monitoraggio Attività";
"Report a bug" = "Segnala un bug";
"Support the application" = "Supporta l'applicazione";
"Close application" = "Chiudi applicazione";
"Open application settings" = "Apri le impostazioni dell'applicazione";
"Open dashboard" = "Apri la dashboard";
"No notifications available in this module" = "Notifiche non disponibili per questo modulo";
"Open Calendar" = "Apri Calendario";
"Toggle the module" = "Attiva/disattiva il modulo"; // translategemma:4b
// Application settings
"Update application" = "Aggiorna applicazione";
"Check for updates" = "Controlla aggiornamenti";
"At start" = "All'avvio";
"Once per day" = "Una volta al giorno";
"Once per week" = "Una volta a settimana";
"Once per month" = "Una volta al mese";
"Never" = "Mai";
"Check for update" = "Verifica la disponibilità di nuove versioni";
"Show icon in dock" = "Mostra icona nel dock";
"Start at login" = "Avvia al login";
"Build number" = "Numero Build";
"Import settings" = "Importa impostazioni";
"Export settings" = "Esporta impostazioni";
"Reset settings" = "Resetta impostazioni";
"Pause the Stats" = "Metti in pausa Stats";
"Resume the Stats" = "Riprendi Stats";
"Combined modules" = "Moduli combinati";
"Combined details" = "Dettagli combinati";
"Spacing" = "Spazio";
"Share anonymous telemetry" = "Condividi telemetria anonima";
"Choose file" = "Seleziona file"; // translategemma:4b
"Stress tests" = "Test di stress"; // translategemma:4b
// Dashboard
"Serial number" = "Numero di serie";
"Model identifier" = "Modello";
"Production year" = "Anno di produzione";
"Uptime" = "Tempo di attività";
"Number of cores" = "%0 core";
"Number of threads" = "%0 thread";
"Number of e-cores" = "%0 efficiency core";
"Number of p-cores" = "%0 performance core";
"Disks" = "Dischi"; // translategemma:4b
"Display" = "Schermo"; // translategemma:4b
// Update
"The latest version of Stats installed" = "L'ultima versione di Stats è installata";
"Downloading..." = "Download in corso...";
"Current version: " = "Versione corrente: ";
"Latest version: " = "Ultima versione: ";
// Widgets
"Color" = "Colore";
"Label" = "Etichetta";
"Box" = "Scatola";
"Frame" = "Riquadro";
"Value" = "Valore";
"Colorize" = "Colora";
"Colorize value" = "Valore colorato";
"Additional information" = "Informazioni aggiuntive";
"Reverse values order" = "Inverti ordine valori";
"Base" = "Base";
"Display mode" = "Modalità di visualizzazione";
"One row" = "Una riga";
"Two rows" = "Due righe";
"Mini widget" = "Mini";
"Line chart widget" = "Grafico a linee";
"Bar chart widget" = "Grafico a barre";
"Pie chart widget" = "Grafico a torta";
"Network chart widget" = "Grafico rete";
"Speed widget" = "Velocità";
"Battery widget" = "Batteria";
"Stack widget" = "Stack";
"Memory widget" = "Memoria";
"Static width" = "Larghezza fissa";
"Tachometer widget" = "Tachimetro";
"State widget" = "Widget di stato";
"Text widget" = "Elemento di testo"; // translategemma:4b
"Battery details widget" = "Widget per dettagli della batteria"; // translategemma:4b
"Show symbols" = "Mostra simboli";
"Label widget" = "Etichetta widget";
"Number of reads in the chart" = "Numero di letture nel grafico";
"Color of download" = "Colore del download";
"Color of upload" = "Colore dell'upload";
"Monospaced font" = "Font a larghezza fissa";
"Reverse order" = "Inverti ordine";
"Chart history" = "Durata grafico";
"Default color" = "Predefinito"; // translategemma:4b
"Transparent when no activity" = "Trasparente in assenza di attività";
"Constant color" = "Costante";
// Module Kit
"Open module settings" = "Apri impostazioni modulo";
"Select widget" = "Seleziona il widget %0";
"Open widget settings" = "Apri impostazioni widget";
"Update interval" = "Intervallo di aggiornamento";
"Usage history" = "Cronologia d'uso";
"Details" = "Dettagli";
"Top processes" = "Processi principali";
"Pictogram" = "Pittogramma";
"Module" = "Modulo";
"Widgets" = "Componenti"; // translategemma:4b
"Popup" = "Finestra di dialogo"; // translategemma:4b
"Notifications" = "Notifiche";
"Merge widgets" = "Combina moduli";
"No available widgets to configure" = "Nessun modulo disponibile";
"No options to configure for the popup in this module" = "Nessuna opzione da configurare per le notifiche in questo modulo";
"Process" = "Processo";
"Kill process" = "Chiudi processo";
"Keyboard shortcut" = "Combinazione di tasti"; // translategemma:4b
"Listening..." = "In ascolto..."; // translategemma:4b
// Modules
"Number of top processes" = "Numero di processi principali";
"Update interval for top processes" = "Intervallo d'aggiornamento per processi principali";
"Notification level" = "Livello di notifica";
"Chart color" = "Colore grafico";
"Main chart scaling" = "Scala del grafico principale";
"Scale value" = "Valore scala";
"Text widget value" = "Valore dell'elemento di testo"; // translategemma:4b
// CPU
"CPU usage" = "Utilizzo CPU";
"CPU temperature" = "Temperatura CPU";
"CPU frequency" = "Frequenza CPU";
"System" = "Sistema";
"User" = "Utente";
"Idle" = "Inattiva";
"Show usage per core" = "Mostra uso per core";
"Show hyper-threading cores" = "Mostra hyper-threading core";
"Split the value (System/User)" = "Dividi il valore (Sistema/Utente)";
"Scheduler limit" = "Limite di pianificazione";
"Speed limit" = "Limite velocità";
"Average load" = "Carico medio";
"1 minute" = "1 minuto";
"5 minutes" = "5 minuti";
"15 minutes" = "15 minuti";
"CPU usage threshold" = "Soglia di utilizzo CPU";
"CPU usage is" = "Utilizzo CPU %0";
"Efficiency cores" = "Efficiency core";
"Performance cores" = "Performance core";
"System color" = "Colore sistema";
"User color" = "Colore utente";
"Idle color" = "Colore inattivo";
"Cluster grouping" = "Raggruppamento cluster";
"Efficiency cores color" = "Colore per efficiency core";
"Performance cores color" = "Colore per performance core";
"Total load" = "Carico totale";
"System load" = "Carico sistema";
"User load" = "Carico utente";
"Efficiency cores load" = "Carico efficiency core";
"Performance cores load" = "Carico performance core";
"All cores" = "Tutti i core";
// GPU
"GPU to show" = "GPU da mostrare";
"Show GPU type" = "Mostra il tipo di GPU";
"GPU enabled" = "GPU abilitata";
"GPU disabled" = "GPU disabilitata";
"GPU temperature" = "Temperatura GPU";
"GPU utilization" = "Utilizzo GPU";
"Vendor" = "Produttore";
"Model" = "Modello";
"Status" = "Stato";
"Active" = "Attiva";
"Non active" = "Non attiva";
"Fan speed" = "Velocità ventole";
"Core clock" = "Clock core";
"Memory clock" = "Clock memoria";
"Utilization" = "Utilizzo";
"Render utilization" = "Utilizzo rendering";
"Tiler utilization" = "Utilizzo tassellamento";
"GPU usage threshold" = "Soglia di utilizzo GPU";
"GPU usage is" = "Utilizzo GPU %0";
// RAM
"Memory usage" = "Memoria utilizzata";
"Memory pressure" = "Compressione memoria";
"Total" = "Totale";
"Used" = "Usata";
"App" = "App";
"Wired" = "Cablata";
"Compressed" = "Compressa";
"Free" = "Libera";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Dividi il valore (App/Wired/Compressa)";
"RAM utilization threshold" = "Soglia utilizzo RAM";
"RAM utilization is" = "Utilizzo RAM %0";
"App color" = "Colore App";
"Wired color" = "Colore Cablata";
"Compressed color" = "Colore Compressa";
"Free color" = "Colore Libera";
"Free memory (less than)" = "Memoria libera (meno di)";
"Swap size" = "Dimensione swap";
"Free RAM is" = "RAM libera è %0";
// Disk
"Show removable disks" = "Mostra dischi rimovibili";
"Used disk memory" = "Usato %0 su %1";
"Free disk memory" = "Libero %0 su %1";
"Disk to show" = "Disco da mostrare";
"Open disk" = "Apri disco";
"Switch view" = "Cambia visualizzazione";
"Disk utilization threshold" = "Soglia di utilizzo disco";
"Disk utilization is" = "Utilizzo disco %0";
"Read color" = "Colore lettura";
"Write color" = "Colore scrittura";
"Disk usage" = "Utilizzo Disco";
"Total read" = "Totale letto";
"Total written" = "Totale scritto";
"Write speed" = "Lettura";
"Read speed" = "Scrittura";
"Drives" = "Dischi"; // translategemma:4b
"SMART data" = "Dati SMART"; // translategemma:4b
// Sensors
"Temperature unit" = "Unità di temperatura";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Salva la velocità delle ventole";
"Fan" = "Ventola";
"HID sensors" = "Sensori HID";
"Synchronize fan's control" = "Sincronizza ventole";
"Current" = "Corrente";
"Energy" = "Energia";
"Show unknown sensors" = "Mostra sensori sconosciuti";
"Install fan helper" = "Installa estensione ventole";
"Uninstall fan helper" = "Disinstalla estensione ventole";
"Fan value" = "Valore ventola";
"Turn off fan" = "Disattiva la ventola";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Stai per disattivare la ventola. Questa azione non è consigliata e può danneggiare il tuo mac, sei sicuro di voler proseguire?";
"Sensor threshold" = "Soglia sensore";
"Left fan" = "Ventola sinistra";
"Right fan" = "Ventola destra";
"Fastest fan" = "Ventola più veloce";
"Sensor to show" = "Sensore da visualizzare"; // translategemma:4b
// Network
"Uploading" = "Caricamento"; // translategemma:4b
"Downloading" = "Scarica"; // translategemma:4b
"Public IP" = "IP Pubblico";
"Local IP" = "IP Locale";
"Interface" = "Interfaccia";
"Physical address" = "Indirizzo fisico";
"Refresh" = "Aggiorna";
"Click to copy public IP address" = "Clicca per copiare l'IP pubblico";
"Click to copy local IP address" = "Clicca per copiare l'IP locale";
"Click to copy wifi name" = "Clicca per copiare il nome del wifi";
"Click to copy mac address" = "Clicca per copiare l'indirizzo mac";
"No connection" = "Nessuna connessione";
"Network interface" = "Interfaccia di rete";
"Total download" = "Download totale";
"Total upload" = "Upload totale";
"Reader type" = "Tipo di lettore";
"Interface based" = "Basato sull'interfaccia";
"Processes based" = "Basato sui processi";
"Reset data usage" = "Ripristina l'utilizzo dei dati";
"VPN mode" = "Modalità VPN";
"Standard" = "Standard";
"Security" = "Sicurezza";
"Channel" = "Canale";
"Common scale" = "Scala comune";
"Autodetection" = "Rilevamento automatico";
"Widget activation threshold" = "Soglia di attivamento widget";
"Internet connection" = "Connessione internet";
"Active state color" = "Colore attivo";
"Nonactive state color" = "Colore disattivo";
"Connectivity host (ICMP)" = "Host (ICMP)";
"Leave empty to disable the check" = "Lascia vuoto per disattivare la verifica";
"Connectivity history" = "Cronologia connettività";
"Auto-refresh public IP address" = "Aggiorna automaticamente indirizzo IP pubblico";
"Every hour" = "Ogni ora";
"Every 12 hours" = "Ogni 12 ore";
"Every 24 hours" = "Ogni 24 ore";
"Network activity" = "Attività di rete";
"Last reset" = "Ultimo reset %0 fa";
"Latency" = "Latenza";
"Upload speed" = "Caricamento"; // translategemma:4b
"Download speed" = "Scarica"; // translategemma:4b
"Address" = "Indirizzo"; // translategemma:4b
"WiFi network" = "Rete WiFi"; // translategemma:4b
"Local IP changed" = "L'indirizzo IP locale è stato modificato"; // translategemma:4b
"Public IP changed" = "L'indirizzo IP pubblico è cambiato."; // translategemma:4b
"Previous IP" = "Indirizzo IP precedente: %0"; // translategemma:4b
"New IP" = "Nuovo indirizzo IP: %0"; // translategemma:4b
"Internet connection lost" = "Connessione a Internet persa"; // translategemma:4b
"Internet connection established" = "Connessione a Internet stabilita"; // translategemma:4b
// Battery
"Level" = "Livello";
"Source" = "Sorgente";
"AC Power" = "Alimentazione AC";
"Battery Power" = "Batteria";
"Time" = "Tempo";
"Health" = "Salute";
"Amperage" = "Amperaggio";
"Voltage" = "Tensione";
"Cycles" = "Numero di cicli";
"Temperature" = "Temperatura";
"Power adapter" = "Alimentatore di corrente";
"Power" = "Output";
"Is charging" = "In carica";
"Time to discharge" = "Tempo di scarica";
"Time to charge" = "Tempo di carica";
"Calculating" = "Calcolo in corso";
"Fully charged" = "Carica";
"Not connected" = "Non connesso";
"Low level notification" = "Notifica livello basso";
"High level notification" = "Notifica livello alto";
"Low battery" = "Batteria scarica";
"High battery" = "Batteria carica";
"Battery remaining" = "%0% rimanente";
"Battery remaining to full charge" = "%0% per carica completa";
"Percentage" = "Percentuale";
"Percentage and time" = "Percentuale e tempo";
"Time and percentage" = "Tempo e percentuale";
"Time format" = "Formato del tempo";
"Hide additional information when full" = "Nascondi informazioni addizionali quando piena";
"Last charge" = "Ultima carica";
"Capacity" = "Capacità";
"current / maximum / designed" = "attuale / massima / progettata";
"Low power mode" = "Modalità risparmio energetico";
"Percentage inside the icon" = "Percentuale nell'icona";
"Colorize battery" = "Colora la batteria";
"Charging current" = "Corrente di carica";
"Charging Voltage" = "Tensione di carica";
"Charger state inside the battery" = "Stato del caricatore all'interno della batteria"; // translategemma:4b
// Bluetooth
"Battery to show" = "Batteria da mostrare";
"No Bluetooth devices are available" = "Nessun dispositivo Bluetooth disponibile";
// Clock
"Time zone" = "Fuso Orario";
"Local" = "Locale";
"Calendar" = "Calendario";
"Show week numbers" = "Mostra i numeri della settimana"; // translategemma:4b
"Local time" = "Ora locale";
"Add new clock" = "Aggiungi un nuovo orologio"; // translategemma:4b
"Delete selected clock" = "Elimina l'orologio selezionato"; // translategemma:4b
"Help with datetime format" = "Assistenza per la formattazione di date e orari"; // translategemma:4b
// Colors
"Based on utilization" = "Basato sull'utilizzo";
"Based on pressure" = "Basato sulla pressione";
"Based on cluster" = "Basato sul cluster";
"System accent" = "Accento di sistema";
"Monochrome accent" = "Accento monocromatico";
"Clear" = "Chiaro";
"White" = "Bianco";
"Black" = "Nero";
"Gray" = "Grigio";
"Second gray" = "Secondo grigio";
"Dark gray" = "Grigio scuro";
"Light gray" = "Grigio chiaro";
"Red" = "Rosso";
"Second red" = "Secondo rosso";
"Green" = "Verde";
"Second green" = "Secondo verde";
"Blue" = "Blu";
"Second blue" = "Secondo blu";
"Yellow" = "Giallo";
"Second yellow" = "Secondo giallo";
"Orange" = "Arancione";
"Second orange" = "Secondo arancione";
"Purple" = "Viola";
"Second purple" = "Secondo viola";
"Brown" = "Marrone";
"Second brown" = "Secondo marrone";
"Cyan" = "Ciano";
"Magenta" = "Magenta";
"Pink" = "Rosa";
"Teal" = "Verde acqua";
"Indigo" = "Indaco";
================================================
FILE: Stats/Supporting Files/ja.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by treastrain / Tanaka Ryoga on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 treastrain / Tanaka Ryoga. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "CPU の設定を開く";
"GPU" = "GPU";
"Open GPU settings" = "GPU の設定を開く";
"RAM" = "RAM";
"Open RAM settings" = "RAM の設定を開く";
"Disk" = "ディスク";
"Open Disk settings" = "ディスクの設定を開く";
"Sensors" = "センサー";
"Open Sensors settings" = "センサーの設定を開く";
"Network" = "ネットワーク";
"Open Network settings" = "ネットワークの設定を開く";
"Battery" = "バッテリー";
"Open Battery settings" = "バッテリーの設定を開く";
"Bluetooth" = "ブルートゥース"; // translategemma:4b
"Open Bluetooth settings" = "Bluetooth の設定を開く";
"Clock" = "時計";
"Open Clock settings" = "時計の設定を開く";
// Words
"Unknown" = "不明";
"Version" = "バージョン";
"Processor" = "プロセッサ";
"Memory" = "メモリ";
"Graphics" = "グラフィックス";
"Close" = "閉じる";
"Download" = "ダウンロード";
"Install" = "インストール";
"Cancel" = "キャンセル";
"Unavailable" = "使用できません";
"Yes" = "はい";
"No" = "いいえ";
"Automatic" = "オート";
"Manual" = "マニュアル";
"None" = "なし";
"Dots" = "ドット";
"Arrows" = "矢印";
"Characters" = "文字";
"Short" = "ショート";
"Long" = "ロング";
"Statistics" = "統計";
"Max" = "最大";
"Min" = "最小";
"Reset" = "リセット";
"Alignment" = "配置";
"Left alignment" = "左揃え";
"Center alignment" = "中央揃え";
"Right alignment" = "右揃え";
"Dashboard" = "ダッシュボード";
"Enabled" = "有効";
"Disabled" = "無効";
"Silent" = "自動(サイレント)";
"Units" = "単位";
"Fans" = "ファン";
"Scaling" = "スケーリング"; // translategemma:4b
"Linear" = "線形"; // translategemma:4b
"Square" = "正方形"; // translategemma:4b
"Cube" = "立方体"; // translategemma:4b
"Logarithmic" = "対数"; // translategemma:4b
"Fixed scale" = "修正"; // translategemma:4b
"Cores" = "コア数"; // translategemma:4b
"Settings" = "設定";
"Name" = "名前";
"Format" = "フォーマット";
"Turn off" = "停止";
"Normal" = "通常"; // translategemma:4b
"Warning" = "警告"; // translategemma:4b
"Critical" = "重要な"; // translategemma:4b
"Usage" = "使用方法"; // translategemma:4b
"2 minutes" = "2分"; // translategemma:4b
"3 minutes" = "3分"; // translategemma:4b
"10 minutes" = "10分"; // translategemma:4b
"Import" = "インポート"; // translategemma:4b
"Export" = "エクスポート"; // translategemma:4b
"Separator" = "区切り"; // translategemma:4b
"Read" = "読む"; // translategemma:4b
"Write" = "記述する"; // translategemma:4b
"Frequency" = "頻度"; // translategemma:4b
"Save" = "保存"; // translategemma:4b
"Run" = "実行する"; // translategemma:4b
"Stop" = "停止"; // translategemma:4b
"Uninstall" = "アンインストール"; // translategemma:4b
"1 sec" = "1 秒"; // translategemma:4b
"2 sec" = "2秒"; // translategemma:4b
"3 sec" = "3秒"; // translategemma:4b
"5 sec" = "5 秒"; // translategemma:4b
"10 sec" = "10 秒"; // translategemma:4b
"15 sec" = "15 秒"; // translategemma:4b
"30 sec" = "30秒"; // translategemma:4b
"60 sec" = "60秒"; // translategemma:4b
// Setup
"Stats Setup" = "Stats セットアップ";
"Previous" = "前";
"Previous page" = "前のページ";
"Next" = "次";
"Next page" = "次のページ";
"Finish" = "完了";
"Finish setup" = "セットアップ完了";
"Welcome to Stats" = "Stats へようこそ";
"welcome_message" = "Stats をご利用いただきありがとうございます。Stats は、メニューバーで使用できる、無料のオープンソースの macOS システム監視ツールです。"; // translategemma:4b
"Start the application automatically when starting your Mac" = "Macを起動する際に、アプリケーションを自動的に起動する。"; // translategemma:4b
"Do not start the application automatically when starting your Mac" = "Macを起動する際に、アプリケーションを自動的に起動しないでください。"; // translategemma:4b
"Do everything silently in the background (recommended)" = "バックグラウンドで、すべての作業を静かに実行する(推奨)"; // translategemma:4b
"Check for a new version on startup" = "起動時に新しいバージョンがあるか確認する"; // translategemma:4b
"Check for a new version every day (once a day)" = "毎日新しいバージョンがあるかどうかを確認してください (1日に1回)。"; // translategemma:4b
"Check for a new version every week (once a week)" = "毎週 (1回) 最新バージョンを確認する"; // translategemma:4b
"Check for a new version every month (once a month)" = "毎月、新しいバージョンがあるか確認する(1回)。"; // translategemma:4b
"Never check for updates (not recommended)" = "常にアップデートを確認しない (推奨されません)"; // translategemma:4b
"Anonymous telemetry for better development decisions" = "匿名でのテレメトリ収集により、より良い開発判断が可能になります"; // translategemma:4b
"Share anonymous telemetry data" = "匿名のテレメトリデータを共有する"; // translategemma:4b
"Do not share anonymous telemetry data" = "匿名のテレメトリデータを共有しないでください。"; // translategemma:4b
"The configuration is completed" = "設定が完了しました"; // translategemma:4b
"finish_setup_message" = "すべて準備完了です!"; // translategemma:4b
// Alerts
"New version available" = "新しいバージョンが利用可能です";
"Click to install the new version of Stats" = "クリックして新しいバージョンの Stats をインストール";
"Successfully updated" = "正常にアップデートされました";
"Stats was updated to v" = "Stats v%0 にアップデートしました";
"Reset settings text" = "アプリの設定をすべてリセットし、アプリを再起動します。本当に続けますか?";
"Support text" = "Statsをご利用いただきありがとうございます。Statsが役に立つと感じたら、寄付をご検討ください。少しでも役に立ちます!";
// Settings
"Open Activity Monitor" = "アクティビティモニタを開く";
"Report a bug" = "バグを報告";
"Support the application" = "Stats をサポート";
"Close application" = "Stats を終了";
"Open application settings" = "Stats の設定を開く";
"Open dashboard" = "ダッシュボードを開く";
"No notifications available in this module" = "このモジュールには利用可能な通知がありません";
"Open Calendar" = "カレンダーを開く";
"Toggle the module" = "モジュールの切り替え";
// Application settings
"Update application" = "アプリをアップデートする";
"Check for updates" = "アップデートの確認頻度";
"At start" = "起動時";
"Once per day" = "1日ごと";
"Once per week" = "1週間ごと";
"Once per month" = "1ヶ月ごと";
"Never" = "確認しない";
"Check for update" = "アップデートを確認する";
"Show icon in dock" = "Dock にアイコンを表示";
"Start at login" = "ログイン時に開く";
"Build number" = "ビルド番号";
"Import settings" = "インポート設定"; // translategemma:4b
"Export settings" = "エクスポート設定"; // translategemma:4b
"Reset settings" = "設定をリセット";
"Pause the Stats" = "統計の表示を一時停止"; // translategemma:4b
"Resume the Stats" = "統計データの再開"; // translategemma:4b
"Combined modules" = "統合モジュール"; // translategemma:4b
"Combined details" = "統合された詳細"; // translategemma:4b
"Spacing" = "間隔"; // translategemma:4b
"Share anonymous telemetry" = "匿名のテレメトリを共有する"; // translategemma:4b
"Choose file" = "ファイルを選択する"; // translategemma:4b
"Stress tests" = "ストレステスト"; // translategemma:4b
// Dashboard
"Serial number" = "シリアル番号";
"Model identifier" = "モデル識別子"; // translategemma:4b
"Production year" = "製造年"; // translategemma:4b
"Uptime" = "起動してからの時間";
"Number of cores" = "%0 コア";
"Number of threads" = "%0 スレッド";
"Number of e-cores" = "%0 高効率コア";
"Number of p-cores" = "%0 高性能コア";
"Disks" = "ディスク"; // translategemma:4b
"Display" = "表示"; // translategemma:4b
// Update
"The latest version of Stats installed" = "最新の Stats がインストールされています";
"Downloading..." = "ダウンロード中...";
"Current version: " = "現在のバージョン: ";
"Latest version: " = "最新のバージョン: ";
// Widgets
"Color" = "カラー";
"Label" = "ラベル";
"Box" = "塗りつぶし";
"Frame" = "枠線";
"Value" = "数値";
"Colorize" = "カラーで表示";
"Colorize value" = "数値をカラーで表示";
"Additional information" = "表示する情報";
"Reverse values order" = "使用済みを上段に表示";
"Base" = "単位";
"Display mode" = "表示形式";
"One row" = "1行";
"Two rows" = "2行";
"Mini widget" = "ミニ";
"Line chart widget" = "線グラフ";
"Bar chart widget" = "棒グラフ";
"Pie chart widget" = "円グラフ";
"Network chart widget" = "ネットワーク使用状況チャート";
"Speed widget" = "速度";
"Battery widget" = "バッテリー";
"Stack widget" = "スタック"; // translategemma:4b
"Memory widget" = "未使用/使用済み";
"Static width" = "固定幅";
"Tachometer widget" = "タコメーター";
"State widget" = "ステートウィジェット"; // translategemma:4b
"Text widget" = "テキストウィジェット"; // translategemma:4b
"Battery details widget" = "バッテリー詳細ウィジェット"; // translategemma:4b
"Show symbols" = "シンボルを表示";
"Label widget" = "ラベル";
"Number of reads in the chart" = "読み取り回数のチャート";
"Color of download" = "ダウンロードの色";
"Color of upload" = "アップロードの色";
"Monospaced font" = "等幅フォント";
"Reverse order" = "逆順"; // translategemma:4b
"Chart history" = "グラフの履歴"; // translategemma:4b
"Default color" = "デフォルト"; // translategemma:4b
"Transparent when no activity" = "活動がないときは透明状態"; // translategemma:4b
"Constant color" = "一定"; // translategemma:4b
// Module Kit
"Open module settings" = "このモジュールの設定を開く";
"Select widget" = "%0ウィジェットを選択";
"Open widget settings" = "このウィジェットの設定を開く";
"Update interval" = "更新間隔";
"Usage history" = "履歴";
"Details" = "詳細";
"Top processes" = "上位のプロセス";
"Pictogram" = "ピクトグラム";
"Module" = "モジュール";
"Widgets" = "ウィジェット";
"Popup" = "ポップアップ";
"Notifications" = "通知";
"Merge widgets" = "ウィジェットを結合する";
"No available widgets to configure" = "設定できるウィジェットがありません";
"No options to configure for the popup in this module" = "このモジュールにはポップアップ用に設定するオプションはありません";
"Process" = "手順"; // translategemma:4b
"Kill process" = "プロセスを停止する"; // translategemma:4b
"Keyboard shortcut" = "キーボードショートカット"; // translategemma:4b
"Listening..." = "再生中…"; // translategemma:4b
// Modules
"Number of top processes" = "表示する上位プロセスの数";
"Update interval for top processes" = "上位プロセスの更新間隔";
"Notification level" = "通知レベル";
"Chart color" = "チャートの色";
"Main chart scaling" = "主なグラフのスケール設定"; // translategemma:4b
"Scale value" = "スケール値"; // translategemma:4b
"Text widget value" = "テキストウィジェットの値"; // translategemma:4b
// CPU
"CPU usage" = "CPU 使用率";
"CPU temperature" = "CPU 温度";
"CPU frequency" = "CPU 周波数";
"System" = "システム";
"User" = "ユーザー";
"Idle" = "アイドル";
"Show usage per core" = "コアごとの使用率を表示";
"Show hyper-threading cores" = "ハイパースレッディングコアを表示";
"Split the value (System/User)" = "値を分割する (システム/ユーザー)";
"Scheduler limit" = "スケジューラの上限";
"Speed limit" = "速度の上限";
"Average load" = "平均ロード";
"1 minute" = "1 分";
"5 minutes" = "5 分";
"15 minutes" = "15 分";
"CPU usage threshold" = "CPU使用率の閾値"; // translategemma:4b
"CPU usage is" = "CPUの使用率は%0です。"; // translategemma:4b
"Efficiency cores" = "効率的なコア"; // translategemma:4b
"Performance cores" = "パフォーマンスコア"; // translategemma:4b
"System color" = "システムの色"; // translategemma:4b
"User color" = "ユーザーの色"; // translategemma:4b
"Idle color" = "アイドルの色"; // translategemma:4b
"Cluster grouping" = "クラスタリング"; // translategemma:4b
"Efficiency cores color" = "効率的なコアの色"; // translategemma:4b
"Performance cores color" = "パフォーマンスコアの色"; // translategemma:4b
"Total load" = "合計負荷"; // translategemma:4b
"System load" = "システム負荷"; // translategemma:4b
"User load" = "ユーザー負荷"; // translategemma:4b
"Efficiency cores load" = "効率的なコアのロード"; // translategemma:4b
"Performance cores load" = "パフォーマンスコアの読み込み"; // translategemma:4b
"All cores" = "すべてのコア"; // translategemma:4b
// GPU
"GPU to show" = "表示する GPU";
"Show GPU type" = "GPU の種類を表示";
"GPU enabled" = "GPU 有効";
"GPU disabled" = "GPU 無効";
"GPU temperature" = "GPU 温度";
"GPU utilization" = "GPU 使用率";
"Vendor" = "製造元";
"Model" = "モデル";
"Status" = "状態";
"Active" = "アクティブ";
"Non active" = "非アクティブ";
"Fan speed" = "ファン速度";
"Core clock" = "コアクロック";
"Memory clock" = "メモリクロック";
"Utilization" = "使用率";
"Render utilization" = "レンダリング";
"Tiler utilization" = "タイルレンダリング";
"GPU usage threshold" = "GPU使用量の上限値"; // translategemma:4b
"GPU usage is" = "GPUの使用率は%0です。"; // translategemma:4b
// RAM
"Memory usage" = "メモリ使用率";
"Memory pressure" = "メモリ負荷";
"Total" = "合計";
"Used" = "使用済みメモリ";
"App" = "アプリケーションメモリ";
"Wired" = "確保されているメモリ";
"Compressed" = "圧縮";
"Free" = "空き";
"Swap" = "スワップ使用領域";
"Split the value (App/Wired/Compressed)" = "値を分割する (アプリケーションメモリ/確保されているメモリ/圧縮)";
"RAM utilization threshold" = "RAM使用量の閾値"; // translategemma:4b
"RAM utilization is" = "RAMの使用率は%0です。"; // translategemma:4b
"App color" = "アプリの色"; // translategemma:4b
"Wired color" = "有線カラー"; // translategemma:4b
"Compressed color" = "圧縮された色"; // translategemma:4b
"Free color" = "無料の色"; // translategemma:4b
"Free memory (less than)" = "空いているメモリ (以下参照)"; // translategemma:4b
"Swap size" = "スワップサイズ"; // translategemma:4b
"Free RAM is" = "利用可能なRAMは%0です。"; // translategemma:4b
// Disk
"Show removable disks" = "リムーバブルディスクを表示";
"Used disk memory" = "%0 使用済み / %1";
"Free disk memory" = "%0 空き / %1";
"Disk to show" = "表示するディスク";
"Open disk" = "このディスクを表示";
"Switch view" = "表示を切り替える";
"Disk utilization threshold" = "ディスク使用率の閾値"; // translategemma:4b
"Disk utilization is" = "ディスクの使用率は %0 です。"; // translategemma:4b
"Read color" = "色を認識する"; // translategemma:4b
"Write color" = "色を記述する"; // translategemma:4b
"Disk usage" = "ディスク使用量"; // translategemma:4b
"Total read" = "合計読み込み"; // translategemma:4b
"Total written" = "総文字数"; // translategemma:4b
"Write speed" = "記述する"; // translategemma:4b
"Read speed" = "読む"; // translategemma:4b
"Drives" = "ドライブ"; // translategemma:4b
"SMART data" = "SMARTデータ"; // translategemma:4b
// Sensors
"Temperature unit" = "温度単位";
"Celsius" = "摂氏(℃)";
"Fahrenheit" = "華氏(℉)";
"Save the fan speed" = "ファンの速度を保存";
"Fan" = "ファン";
"HID sensors" = "HID センサー";
"Synchronize fan's control" = "ファンを制御するタイミングを同期する"; // translategemma:4b
"Current" = "現在"; // translategemma:4b
"Energy" = "エネルギー"; // translategemma:4b
"Show unknown sensors" = "未知のセンサーを表示する"; // translategemma:4b
"Install fan helper" = "ファンヘルパーをインストールする"; // translategemma:4b
"Uninstall fan helper" = "ファンヘルパーのアンインストール"; // translategemma:4b
"Fan value" = "ファンの価値"; // translategemma:4b
"Turn off fan" = "ファンの停止";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "ファンの停止はmacに大きなダメージを与える可能性がある為、推奨されません。本当に実行しますか?";
"Sensor threshold" = "センサー閾値"; // translategemma:4b
"Left fan" = "左"; // translategemma:4b
"Right fan" = "正しい"; // translategemma:4b
"Fastest fan" = "最も速い"; // translategemma:4b
"Sensor to show" = "センサー表示"; // translategemma:4b
// Network
"Uploading" = "送信";
"Downloading" = "受信";
"Public IP" = "パブリック IP";
"Local IP" = "ローカル IP";
"Interface" = "インターフェイス";
"Physical address" = "MAC アドレス";
"Refresh" = "更新";
"Click to copy public IP address" = "クリックしてパブリック IP アドレスをコピー";
"Click to copy local IP address" = "クリックしてローカル IP アドレスをコピー";
"Click to copy wifi name" = "クリックして Wi-Fi ネットワーク名をコピー";
"Click to copy mac address" = "クリックして MAC アドレスをコピー";
"No connection" = "未接続";
"Network interface" = "インターフェイス";
"Total download" = "受信の合計";
"Total upload" = "送信の合計";
"Reader type" = "リーダータイプ";
"Interface based" = "インターフェイス";
"Processes based" = "プロセス";
"Reset data usage" = "データ使用履歴をリセット";
"VPN mode" = "VPN モード";
"Standard" = "標準"; // translategemma:4b
"Security" = "セキュリティ"; // translategemma:4b
"Channel" = "チャンネル"; // translategemma:4b
"Common scale" = "一般的なスケール"; // translategemma:4b
"Autodetection" = "自動検出"; // translategemma:4b
"Widget activation threshold" = "ウィジェット有効化の閾値"; // translategemma:4b
"Internet connection" = "インターネット接続"; // translategemma:4b
"Active state color" = "アクティブな状態の色"; // translategemma:4b
"Nonactive state color" = "非アクティブ状態の色"; // translategemma:4b
"Connectivity host (ICMP)" = "接続ホスト (ICMP)"; // translategemma:4b
"Leave empty to disable the check" = "空欄にすることで、機能を無効化します。"; // translategemma:4b
"Connectivity history" = "接続履歴"; // translategemma:4b
"Auto-refresh public IP address" = "自動でパブリックなIPアドレスを更新する"; // translategemma:4b
"Every hour" = "1時間ごとに"; // translategemma:4b
"Every 12 hours" = "12時間ごとに"; // translategemma:4b
"Every 24 hours" = "24時間ごとに"; // translategemma:4b
"Network activity" = "ネットワークアクティビティ"; // translategemma:4b
"Last reset" = "最後にリセットしたのは %0 以前"; // translategemma:4b
"Latency" = "遅延"; // translategemma:4b
"Upload speed" = "アップロード"; // translategemma:4b
"Download speed" = "ダウンロード"; // translategemma:4b
"Address" = "住所"; // translategemma:4b
"WiFi network" = "Wi-Fi ネットワーク"; // translategemma:4b
"Local IP changed" = "ローカルのIPアドレスが変更されました"; // translategemma:4b
"Public IP changed" = "公開IPアドレスが変更されました"; // translategemma:4b
"Previous IP" = "以前のIPアドレス: %0"; // translategemma:4b
"New IP" = "新しいIPアドレス: %0"; // translategemma:4b
"Internet connection lost" = "インターネット接続が失われました"; // translategemma:4b
"Internet connection established" = "インターネット接続が確立されました"; // translategemma:4b
// Battery
"Level" = "充電残量";
"Source" = "電源";
"AC Power" = "電源アダプタ";
"Battery Power" = "バッテリー";
"Time" = "バッテリー残り時間";
"Health" = "状態";
"Amperage" = "電流";
"Voltage" = "電圧";
"Cycles" = "サイクル数";
"Temperature" = "温度";
"Power adapter" = "電源アダプタ";
"Power" = "電力";
"Is charging" = "充電中";
"Time to discharge" = "バッテリー残り時間";
"Time to charge" = "充電完了までの時間";
"Calculating" = "計算中";
"Fully charged" = "フル充電済み";
"Not connected" = "接続されていません";
"Low level notification" = "バッテリー残量低下の通知";
"High level notification" = "バッテリー充電状況の通知";
"Low battery" = "バッテリー残量低下";
"High battery" = "バッテリー充電状況";
"Battery remaining" = "バッテリー残量 残り %0%";
"Battery remaining to full charge" = "フル充電まであと %0%";
"Percentage" = "残量(%)";
"Percentage and time" = "残量(%)と残り時間";
"Time and percentage" = "残り時間と残量(%)";
"Time format" = "時間の表示形式";
"Hide additional information when full" = "フル充電済みのときはアイコンのみ表示";
"Last charge" = "最後の充電から";
"Capacity" = "容量";
"current / maximum / designed" = "current / 最大 / 規格";
"Low power mode" = "低電力モード";
"Percentage inside the icon" = "アイコン内のパーセンテージ"; // translategemma:4b
"Colorize battery" = "バッテリーの色を付ける"; // translategemma:4b
"Charging current" = "充電電流"; // translategemma:4b
"Charging Voltage" = "充電電圧"; // translategemma:4b
"Charger state inside the battery" = "バッテリー内部の充電状態"; // translategemma:4b
// Bluetooth
"Battery to show" = "バッテリー残量を表示するデバイス";
"No Bluetooth devices are available" = "利用可能な Bluetooth デバイスがありません";
// Clock
"Time zone" = "タイムゾーン"; // translategemma:4b
"Local" = "地域"; // translategemma:4b
"Calendar" = "カレンダー"; // translategemma:4b
"Show week numbers" = "週番号を表示する"; // translategemma:4b
"Local time" = "現地の時間"; // translategemma:4b
"Add new clock" = "新しい時計を追加する"; // translategemma:4b
"Delete selected clock" = "選択したクロックを削除する"; // translategemma:4b
"Help with datetime format" = "日付と時刻の形式に関するサポート"; // translategemma:4b
// Colors
"Based on utilization" = "使用率に基づいたカラー";
"Based on pressure" = "負荷に基づいたカラー";
"Based on cluster" = "クラスタに基づいて"; // translategemma:4b
"System accent" = "システムアクセント";
"Monochrome accent" = "モノクロ";
"Clear" = "クリア";
"White" = "ホワイト";
"Black" = "ブラック";
"Gray" = "グレー";
"Second gray" = "システムグレー";
"Dark gray" = "ダークグレー";
"Light gray" = "ライトグレー";
"Red" = "レッド";
"Second red" = "システムレッド";
"Green" = "グリーン";
"Second green" = "システムグリーン";
"Blue" = "ブルー";
"Second blue" = "システムブルー";
"Yellow" = "イエロー";
"Second yellow" = "システムイエロー";
"Orange" = "オレンジ";
"Second orange" = "システムオレンジ";
"Purple" = "パープル";
"Second purple" = "システムパープル";
"Brown" = "ブラウン";
"Second brown" = "システムブラウン";
"Cyan" = "シアン";
"Magenta" = "マゼンタ";
"Pink" = "システムピンク";
"Teal" = "システムティール";
"Indigo" = "システムインディゴ";
================================================
FILE: Stats/Supporting Files/ko.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "CPU 설정 열기";
"GPU" = "GPU";
"Open GPU settings" = "GPU 설정 열기";
"RAM" = "RAM";
"Open RAM settings" = "RAM 설정 열기";
"Disk" = "디스크";
"Open Disk settings" = "디스크 설정 열기";
"Sensors" = "센서";
"Open Sensors settings" = "센서 설정 열기";
"Network" = "네트워크";
"Open Network settings" = "네트워크 설정 열기";
"Battery" = "배터리";
"Open Battery settings" = "배터리 설정 열기";
"Bluetooth" = "블루투스"; // translategemma:4b
"Open Bluetooth settings" = "블루투스 설정 열기";
"Clock" = "시계";
"Open Clock settings" = "시계 설정 ";
// Words
"Unknown" = "알 수 없음";
"Version" = "버전";
"Processor" = "프로세서";
"Memory" = "메모리";
"Graphics" = "그래픽";
"Close" = "닫기";
"Download" = "다운로드";
"Install" = "설치";
"Cancel" = "취소";
"Unavailable" = "사용할 수 없음";
"Yes" = "예";
"No" = "아니요";
"Automatic" = "자동";
"Manual" = "수동";
"None" = "없음";
"Dots" = "점";
"Arrows" = "화살표";
"Characters" = "문자";
"Short" = "짧은";
"Long" = "긴";
"Statistics" = "통계";
"Max" = "최대";
"Min" = "최소";
"Reset" = "재설정";
"Alignment" = "정렬";
"Left alignment" = "왼쪽";
"Center alignment" = "가운데";
"Right alignment" = "오른쪽";
"Dashboard" = "대시보드";
"Enabled" = "사용";
"Disabled" = "사용 안 함";
"Silent" = "조용하게";
"Units" = "단위";
"Fans" = "팬";
"Scaling" = "스케일";
"Linear" = "선형";
"Square" = "제곱";
"Cube" = "세제곱";
"Logarithmic" = "로그";
"Fixed scale" = "고정 스케일";
"Cores" = "코어 수";
"Settings" = "설정";
"Name" = "이름";
"Format" = "형식";
"Turn off" = "끄기";
"Normal" = "정상"; // translategemma:4b
"Warning" = "경고"; // translategemma:4b
"Critical" = "중요"; // translategemma:4b
"Usage" = "사용 방법"; // translategemma:4b
"2 minutes" = "2분"; // translategemma:4b
"3 minutes" = "3분"; // translategemma:4b
"10 minutes" = "10분"; // translategemma:4b
"Import" = "수입"; // translategemma:4b
"Export" = "수출"; // translategemma:4b
"Separator" = "분리"; // translategemma:4b
"Read" = "읽기"; // translategemma:4b
"Write" = "작성"; // translategemma:4b
"Frequency" = "빈도"; // translategemma:4b
"Save" = "저장"; // translategemma:4b
"Run" = "실행"; // translategemma:4b
"Stop" = "중지"; // translategemma:4b
"Uninstall" = "제거"; // translategemma:4b
"1 sec" = "1 초"; // translategemma:4b
"2 sec" = "2초"; // translategemma:4b
"3 sec" = "3초"; // translategemma:4b
"5 sec" = "5초"; // translategemma:4b
"10 sec" = "10초"; // translategemma:4b
"15 sec" = "15초"; // translategemma:4b
"30 sec" = "30초"; // translategemma:4b
"60 sec" = "60초"; // translategemma:4b
// Setup
"Stats Setup" = "Stats 설정";
"Previous" = "이전";
"Previous page" = "이전 페이지";
"Next" = "다음";
"Next page" = "다음 페이지";
"Finish" = "완료";
"Finish setup" = "설정 완료";
"Welcome to Stats" = "Stats에 오신 것을 환영합니다";
"welcome_message" = "당신의 메뉴 바를 위한 무료 오픈 소스 macOS 시스템 모니터, Stats를 사용해 주셔서 감사합니다.";
"Start the application automatically when starting your Mac" = "Mac 시동 시 애플리케이션 자동 시작";
"Do not start the application automatically when starting your Mac" = "Mac 시동 시 애플리케이션 자동 시작 안함";
"Do everything silently in the background (recommended)" = "모든 것을 백그라운드에서 조용하게 처리 (권장)";
"Check for a new version on startup" = "시작할 때 새 버전 확인";
"Check for a new version every day (once a day)" = "매일 새 버전 확인 (하루에 한 번)";
"Check for a new version every week (once a week)" = "주마다 새 버전 확인 (일주일에 한 번)";
"Check for a new version every month (once a month)" = "달마다 새 버전 확인 (한 달에 한 번)";
"Never check for updates (not recommended)" = "업데이트 확인 안 함 (추천하지 않음)";
"Anonymous telemetry for better development decisions" = "익명 사용정보를 통한 더 나은 개발 방향 결정";
"Share anonymous telemetry data" = "익명 사용정보를 공유합니다";
"Do not share anonymous telemetry data" = "익명 사용정보를 공유하지 않습니다";
"The configuration is completed" = "설정이 완료되었습니다";
"finish_setup_message" = "모든 것이 설정되었습니다! \n Stats는 무료 오픈 소스 도구이며 앞으로도 그럴 것입니다. \n 이 애플리케이션이 마음에 드셨다면 프로젝트를 후원 하실 수 있습니다. 늘 감사합니다!";
// Alerts
"New version available" = "새로운 버전을 이용할 수 있습니다";
"Click to install the new version of Stats" = "클릭하여 새 버전의 Stats 설치";
"Successfully updated" = "성공적으로 업데이트되었습니다";
"Stats was updated to v" = "Stats가 v%0 버전으로 업데이트되었습니다";
"Reset settings text" = "모든 애플리케이션 설정이 재설정되고 다시 시작됩니다. 계속 진행하시겠습니까?";
"Support text" = "Stats를 사용해 주셔서 감사합니다!\n\n 이 오픈 소스 프로젝트를 유지 관리하고 개선하는 데는 시간과 리소스가 필요합니다. 여러분의 지원은 모든 사람에게 신뢰할 수 있는 무료 애플리케이션을 계속 제공하는 데 도움이 됩니다.\n\nStats가 도움이 된다면 기여를 고려해 주세요. 여러분의 작은 도움이 큰 힘이 됩니다!";
// Settings
"Open Activity Monitor" = "활동 모니터 열기";
"Report a bug" = "버그 보고";
"Support the application" = "애플리케이션 후원";
"Close application" = "애플리케이션 닫기";
"Open application settings" = "애플리케이션 설정 열기";
"Open dashboard" = "대시보드 열기";
"No notifications available in this module" = "이 모듈에서 사용할 수 있는 알림이 없습니다";
"Open Calendar" = "캘린더 열기";
"Toggle the module" = "모듈 전환";
// Application settings
"Update application" = "애플리케이션 업데이트";
"Check for updates" = "업데이트 확인";
"At start" = "시작할 때";
"Once per day" = "하루에 한 번";
"Once per week" = "일주일에 한 번";
"Once per month" = "한 달에 한 번";
"Never" = "확인하지 않음";
"Check for update" = "업데이트 확인";
"Show icon in dock" = "Dock에 아이콘 표시";
"Start at login" = "로그인 시 시작";
"Build number" = "빌드 번호";
"Import settings" = "설정 가져오기"; // translategemma:4b
"Export settings" = "내보내기 설정"; // translategemma:4b
"Reset settings" = "설정 초기화";
"Pause the Stats" = "Stats 일시정지";
"Resume the Stats" = "Stats 재개";
"Combined modules" = "모듈 병합";
"Combined details" = "결합된 세부 정보"; // translategemma:4b
"Spacing" = "간격";
"Share anonymous telemetry" = "익명 사용정보 공유";
"Choose file" = "파일 선택"; // translategemma:4b
"Stress tests" = "스트레스 테스트"; // translategemma:4b
// Dashboard
"Serial number" = "일련 번호";
"Model identifier" = "모델 식별자"; // translategemma:4b
"Production year" = "생산 연도"; // translategemma:4b
"Uptime" = "가동 시간";
"Number of cores" = "%0 코어";
"Number of threads" = "%0 스레드";
"Number of e-cores" = "%0 효율 코어";
"Number of p-cores" = "%0 성능 코어";
"Disks" = "디스크"; // translategemma:4b
"Display" = "화면 표시"; // translategemma:4b
// Update
"The latest version of Stats installed" = "최신 버전의 Stats가 설치되어 있습니다";
"Downloading..." = "다운로드 중...";
"Current version: " = "현재 버전: ";
"Latest version: " = "최신 버전: ";
// Widgets
"Color" = "색상";
"Label" = "레이블";
"Box" = "상자";
"Frame" = "테두리";
"Value" = "값";
"Colorize" = "색상화";
"Colorize value" = "값 색상화";
"Additional information" = "추가 정보";
"Reverse values order" = "값 순서 반전";
"Base" = "단위";
"Display mode" = "표기 방식";
"One row" = "한 줄";
"Two rows" = "두 줄";
"Mini widget" = "소형 위젯";
"Line chart widget" = "선 도표";
"Bar chart widget" = "막대 도표";
"Pie chart widget" = "원형 도표";
"Network chart widget" = "네트워크 도표";
"Speed widget" = "속도 위젯";
"Battery widget" = "배터리 위젯";
"Stack widget" = "스택 위젯";
"Memory widget" = "메모리 위젯";
"Static width" = "너비 고정";
"Tachometer widget" = "반원 도표";
"State widget" = "상태 위젯";
"Text widget" = "텍스트 위젯";
"Battery details widget" = "배터리 세부 정보 위젯";
"Show symbols" = "기호 표시";
"Label widget" = "레이블 위젯";
"Number of reads in the chart" = "도표의 행 크기";
"Color of download" = "다운로드 색상";
"Color of upload" = "업로드 색상";
"Monospaced font" = "고정폭 서체";
"Reverse order" = "역순"; // translategemma:4b
"Chart history" = "차트 기록"; // translategemma:4b
"Default color" = "기본값"; // translategemma:4b
"Transparent when no activity" = "활동이 없을 때 투명하게 표시"; // translategemma:4b
"Constant color" = "상수"; // translategemma:4b
// Module Kit
"Open module settings" = "모듈 설정 열기";
"Select widget" = "%0 위젯 선택";
"Open widget settings" = "위젯 설정 열기";
"Update interval" = "업데이트 주기";
"Usage history" = "사용 기록";
"Details" = "세부 사항";
"Top processes" = "상위 프로세스";
"Pictogram" = "상태 아이콘";
"Module" = "기준 치수";
"Widgets" = "위젯";
"Popup" = "팝업";
"Notifications" = "알림";
"Merge widgets" = "위젯 병합";
"No available widgets to configure" = "구성할 위젯이 없습니다";
"No options to configure for the popup in this module" = "이 모듈은 구성할 팝업 설정이 없습니다.";
"Process" = "과정"; // translategemma:4b
"Kill process" = "프로세스 종료"; // translategemma:4b
"Keyboard shortcut" = "키보드 단축키"; // translategemma:4b
"Listening..." = "경청 중..."; // translategemma:4b
// Modules
"Number of top processes" = "상위 프로세스 수";
"Update interval for top processes" = "상위 프로세스의 업데이트 주기";
"Notification level" = "알림 수준";
"Chart color" = "도표 색상";
"Main chart scaling" = "주요 차트 스케일 설정"; // translategemma:4b
"Scale value" = "척도 값"; // translategemma:4b
"Text widget value" = "텍스트 위젯 값"; // translategemma:4b
// CPU
"CPU usage" = "CPU 사용량";
"CPU temperature" = "CPU 온도";
"CPU frequency" = "CPU 주파수";
"System" = "시스템";
"User" = "사용자";
"Idle" = "대기";
"Show usage per core" = "코어당 사용량 표시";
"Show hyper-threading cores" = "하이퍼스레딩 코어 표시";
"Split the value (System/User)" = "값 분할 (시스템/사용자)";
"Scheduler limit" = "스케줄러 제한";
"Speed limit" = "속도 제한";
"Average load" = "평균 부하";
"1 minute" = "1분";
"5 minutes" = "5분";
"15 minutes" = "15분";
"CPU usage threshold" = "CPU 사용량 임계값";
"CPU usage is" = "CPU 사용량 %0";
"Efficiency cores" = "효율 코어";
"Performance cores" = "성능 코어";
"System color" = "시스템 색상";
"User color" = "사용자 색상";
"Idle color" = "대기 색상";
"Cluster grouping" = "클러스터 묶기";
"Efficiency cores color" = "효율 코어 색상";
"Performance cores color" = "성능 코어 색상";
"Total load" = "총 용량"; // translategemma:4b
"System load" = "시스템 부하"; // translategemma:4b
"User load" = "사용자 부하"; // translategemma:4b
"Efficiency cores load" = "효율성 코어 로드"; // translategemma:4b
"Performance cores load" = "성능 코어 로드"; // translategemma:4b
"All cores" = "모든 코어"; // translategemma:4b
// GPU
"GPU to show" = "표시할 GPU";
"Show GPU type" = "GPU 유형 표시";
"GPU enabled" = "GPU 사용함";
"GPU disabled" = "GPU 사용 안 함";
"GPU temperature" = "GPU 온도";
"GPU utilization" = "GPU 사용률";
"Vendor" = "공급 업체";
"Model" = "모델";
"Status" = "상태";
"Active" = "활성";
"Non active" = "비활성";
"Fan speed" = "팬 속도";
"Core clock" = "코어 클럭";
"Memory clock" = "메모리 클럭";
"Utilization" = "사용률";
"Render utilization" = "렌더 사용률";
"Tiler utilization" = "타일 사용률";
"GPU usage threshold" = "GPU 사용량 임계값";
"GPU usage is" = "GPU 사용량 %0";
// RAM
"Memory usage" = "메모리 사용량";
"Memory pressure" = "메모리 부하";
"Total" = "전체";
"Used" = "사용";
"App" = "앱";
"Wired" = "연결됨";
"Compressed" = "압축됨";
"Free" = "여유";
"Swap" = "스왑";
"Split the value (App/Wired/Compressed)" = "값 분할 (앱/연결됨/압축됨)";
"RAM utilization threshold" = "RAM 사용률 임계값";
"RAM utilization is" = "RAM 사용률 %0";
"App color" = "앱 색상";
"Wired color" = "연결됨 색상";
"Compressed color" = "압축됨 색상";
"Free color" = "여유 색상";
"Free memory (less than)" = "무료 메모리 (MB/s)"; // translategemma:4b
"Swap size" = "스왑 크기"; // translategemma:4b
"Free RAM is" = "사용 가능한 RAM은 %0입니다."; // translategemma:4b
// Disk
"Show removable disks" = "이동식 디스크 표시";
"Used disk memory" = "%1 중 %0 사용";
"Free disk memory" = "%1 중 %0 여유";
"Disk to show" = "표시할 디스크";
"Open disk" = "디스크 열기";
"Switch view" = "보기 전환";
"Disk utilization threshold" = "디스크 사용률 임계값";
"Disk utilization is" = "디스크 사용률 %0";
"Read color" = "읽기 색상";
"Write color" = "쓰기 색상";
"Disk usage" = "디스크 사용량";
"Total read" = "총 읽기"; // translategemma:4b
"Total written" = "총 작성"; // translategemma:4b
"Write speed" = "작성"; // translategemma:4b
"Read speed" = "읽기"; // translategemma:4b
"Drives" = "드라이브"; // translategemma:4b
"SMART data" = "SMART 데이터"; // translategemma:4b
// Sensors
"Temperature unit" = "온도 단위";
"Celsius" = "섭씨";
"Fahrenheit" = "화씨";
"Save the fan speed" = "팬 속도 저장";
"Fan" = "팬";
"HID sensors" = "HID 센서";
"Synchronize fan's control" = "팬 제어 동기화";
"Current" = "전류";
"Energy" = "에너지";
"Show unknown sensors" = "알 수 없는 센서 표시";
"Install fan helper" = "팬 헬퍼 설치";
"Uninstall fan helper" = "팬 헬퍼 제거";
"Fan value" = "팬 값";
"Turn off fan" = "팬 끄기";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "팬을 끄려고 합니다. 팬을 끄는 행위는 Mac을 손상시킬 수 있으므로 권장하지 않습니다. 정말 팬을 끄겠습니까?";
"Sensor threshold" = "센서 임계값"; // translategemma:4b
"Left fan" = "왼쪽"; // translategemma:4b
"Right fan" = "맞습니다"; // translategemma:4b
"Fastest fan" = "가장 빠른"; // translategemma:4b
"Sensor to show" = "센서 표시"; // translategemma:4b
// Network
"Uploading" = "업로드";
"Downloading" = "다운로드";
"Public IP" = "공용 IP";
"Local IP" = "로컬 IP";
"Interface" = "인터페이스";
"Physical address" = "물리 주소";
"Refresh" = "새로 고침";
"Click to copy public IP address" = "공용 IP 주소를 복사하려면 클릭하세요";
"Click to copy local IP address" = "로컬 IP 주소를 복사하려면 클릭하세요";
"Click to copy wifi name" = "WIFI 이름을 복사하려면 클릭하세요";
"Click to copy mac address" = "MAC 주소를 복사하려면 클릭하세요";
"No connection" = "연결되지 않음";
"Network interface" = "네트워크 인터페이스";
"Total download" = "총 다운로드";
"Total upload" = "총 업로드";
"Reader type" = "읽기 유형";
"Interface based" = "인터페이스 기반";
"Processes based" = "프로세스 기반";
"Reset data usage" = "데이터 사용량 재설정";
"VPN mode" = "VPN 모드";
"Standard" = "표준";
"Security" = "보안";
"Channel" = "채널";
"Common scale" = "통일된 스케일 사용";
"Autodetection" = "자동 감지";
"Widget activation threshold" = "위젯 활성화 임계값";
"Internet connection" = "인터넷 연결";
"Active state color" = "활성 상태 색상";
"Nonactive state color" = "비활성 상태 색상";
"Connectivity host (ICMP)" = "연결성 확인 호스트 (ICMP)";
"Leave empty to disable the check" = "연결성 확인을 비활성화하려면 이 란을 비워 두세요";
"Connectivity history" = "연결성 기록";
"Auto-refresh public IP address" = "공개 IP주소 자동으로 새로고침";
"Every hour" = "매 시간마다";
"Every 12 hours" = "12시간마다";
"Every 24 hours" = "24시간마다";
"Network activity" = "네트워크 활동";
"Last reset" = "마지막 초기화 이후 %0 경과";
"Latency" = "지연 시간"; // translategemma:4b
"Upload speed" = "업로드"; // translategemma:4b
"Download speed" = "다운로드"; // translategemma:4b
"Address" = "주소"; // translategemma:4b
"WiFi network" = "와이파이 네트워크"; // translategemma:4b
"Local IP changed" = "로컬 IP 주소가 변경되었습니다."; // translategemma:4b
"Public IP changed" = "공개 IP 주소가 변경되었습니다."; // translategemma:4b
"Previous IP" = "이전 IP: %0"; // translategemma:4b
"New IP" = "새 IP: %0"; // translategemma:4b
"Internet connection lost" = "인터넷 연결이 끊김"; // translategemma:4b
"Internet connection established" = "인터넷 연결 완료"; // translategemma:4b
// Battery
"Level" = "잔량";
"Source" = "공급원";
"AC Power" = "AC 전원";
"Battery Power" = "배터리 전원";
"Time" = "시간";
"Health" = "상태";
"Amperage" = "전류";
"Voltage" = "전압";
"Cycles" = "사이클";
"Temperature" = "온도";
"Power adapter" = "전원 어댑터";
"Power" = "전원";
"Is charging" = "충전 중";
"Time to discharge" = "방전 시간";
"Time to charge" = "충전 시간";
"Calculating" = "계산 중";
"Fully charged" = "완전히 충전됨";
"Not connected" = "연결되지 않음";
"Low level notification" = "낮은 수준 알림";
"High level notification" = "높은 수준 알림";
"Low battery" = "낮은 배터리";
"High battery" = "높은 배터리";
"Battery remaining" = "%0% 남았습니다";
"Battery remaining to full charge" = "완전 충전까지 %0%";
"Percentage" = "백분율";
"Percentage and time" = "백분율 및 시간";
"Time and percentage" = "시간 및 백분율";
"Time format" = "시간 형식";
"Hide additional information when full" = "완전 충전 시 추가 정보 숨기기";
"Last charge" = "마지막 충전";
"Capacity" = "용량";
"current / maximum / designed" = "현재 / 최대 / 설계";
"Low power mode" = "저전력 모드";
"Percentage inside the icon" = "아이콘 속에 백분율";
"Colorize battery" = "배터리 색상 표시";
"Charging current" = "충전 전류"; // translategemma:4b
"Charging Voltage" = "충전 전압"; // translategemma:4b
"Charger state inside the battery" = "배터리 내부 충전 상태"; // translategemma:4b
// Bluetooth
"Battery to show" = "표시할 배터리";
"No Bluetooth devices are available" = "사용 가능한 Bluetooth 장치가 없습니다";
// Clock
"Time zone" = "시간대";
"Local" = "현지시각";
"Calendar" = "달력"; // translategemma:4b
"Show week numbers" = "주차 번호 표시"; // translategemma:4b
"Local time" = "현지 시간"; // translategemma:4b
"Add new clock" = "새로운 시계 추가"; // translategemma:4b
"Delete selected clock" = "선택된 시계 삭제"; // translategemma:4b
"Help with datetime format" = "datetime 형식에 대한 도움"; // translategemma:4b
// Colors
"Based on utilization" = "사용률 기준";
"Based on pressure" = "부하량 기준";
"Based on cluster" = "클러스터 기준";
"System accent" = "시스템 강조";
"Monochrome accent" = "단색 강조";
"Clear" = "투명";
"White" = "흰색";
"Black" = "검정";
"Gray" = "회색";
"Second gray" = "두 번째 회색";
"Dark gray" = "진한 회색";
"Light gray" = "밝은 회색";
"Red" = "빨강";
"Second red" = "두 번째 빨강";
"Green" = "녹색";
"Second green" = "두 번째 녹색";
"Blue" = "파랑";
"Second blue" = "두 번째 파랑";
"Yellow" = "노랑";
"Second yellow" = "두 번째 노랑";
"Orange" = "주황";
"Second orange" = "두 번째 주황";
"Purple" = "보라";
"Second purple" = "두 번째 보라";
"Brown" = "갈색";
"Second brown" = "두번 째 갈색";
"Cyan" = "시안";
"Magenta" = "마젠타";
"Pink" = "분홍";
"Teal" = "암청색";
"Indigo" = "남색";
================================================
FILE: Stats/Supporting Files/nb.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "Åpne CPU-innstillinger";
"GPU" = "GPU";
"Open GPU settings" = "Åpne GPU-innstillinger";
"RAM" = "Minne";
"Open RAM settings" = "Åpne minneinnstillinger";
"Disk" = "Disk";
"Open Disk settings" = "Åpne diskinnstillinger";
"Sensors" = "Sensorer"; // translategemma:4b
"Open Sensors settings" = "Åpne sensorinnstillinger";
"Network" = "Nettverk"; // translategemma:4b
"Open Network settings" = "Åpne nettverksinnstillinger";
"Battery" = "Batteri";
"Open Battery settings" = "Åpne batteriinnstillinger";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Åpne Bluetooth-innstillinger";
"Clock" = "Klokke";
"Open Clock settings" = "Åpne klokkeinnstillinger";
// Words
"Unknown" = "Ukjent";
"Version" = "Versjon";
"Processor" = "Prosessor";
"Memory" = "Minne";
"Graphics" = "Grafikk";
"Close" = "Lukk";
"Download" = "Nedlasting";
"Install" = "Installer";
"Cancel" = "Avbryt";
"Unavailable" = "Utilgjengelig";
"Yes" = "Ja";
"No" = "Nei";
"Automatic" = "Automatisk";
"Manual" = "Manuell";
"None" = "Ingen";
"Dots" = "Prikker";
"Arrows" = "Piler";
"Characters" = "Tegn";
"Short" = "Kort";
"Long" = "Lang";
"Statistics" = "Statistikk";
"Max" = "Maks";
"Min" = "Mitt"; // translategemma:4b
"Reset" = "Nullstill";
"Alignment" = "Justering";
"Left alignment" = "Venstre";
"Center alignment" = "Sentrum";
"Right alignment" = "Høyre";
"Dashboard" = "Dashboard";
"Enabled" = "Aktivert";
"Disabled" = "Deaktivert";
"Silent" = "Stille";
"Units" = "Enheter";
"Fans" = "Vifter";
"Scaling" = "Skalering";
"Linear" = "Lineær";
"Square" = "Kvadratisk";
"Cube" = "Kubisk";
"Logarithmic" = "Logaritmisk";
"Fixed scale" = "Løst"; // translategemma:4b
"Cores" = "Kjerner";
"Settings" = "Innstillinger";
"Name" = "Navn";
"Format" = "Format";
"Turn off" = "Slå av";
"Normal" = "Normal";
"Warning" = "Advarsel"; // translategemma:4b
"Critical" = "Kritisk"; // translategemma:4b
"Usage" = "Bruk"; // translategemma:4b
"2 minutes" = "2 minutter"; // translategemma:4b
"3 minutes" = "3 minutter"; // translategemma:4b
"10 minutes" = "10 minutter"; // translategemma:4b
"Import" = "Import";
"Export" = "Eksport"; // translategemma:4b
"Separator" = "Skillelinje"; // translategemma:4b
"Read" = "Les"; // translategemma:4b
"Write" = "Skriv"; // translategemma:4b
"Frequency" = "Frekvens"; // translategemma:4b
"Save" = "Lagre"; // translategemma:4b
"Run" = "Kjør"; // translategemma:4b
"Stop" = "Stopp"; // translategemma:4b
"Uninstall" = "Fjern"; // translategemma:4b
"1 sec" = "1 sekund"; // translategemma:4b
"2 sec" = "2 sekunder"; // translategemma:4b
"3 sec" = "3 sekunder"; // translategemma:4b
"5 sec" = "5 sek"; // translategemma:4b
"10 sec" = "10 sek"; // translategemma:4b
"15 sec" = "15 sek"; // translategemma:4b
"30 sec" = "30 sek"; // translategemma:4b
"60 sec" = "60 sek"; // translategemma:4b
// Setup
"Stats Setup" = "Stats-oppsett";
"Previous" = "Forrige";
"Previous page" = "Forrige side";
"Next" = "Neste";
"Next page" = "Neste side";
"Finish" = "Fullfør";
"Finish setup" = "Fullfør oppsett";
"Welcome to Stats" = "Velkommen til Stats";
"welcome_message" = "Takk for at du bruker Stats, en fritt tilgjengelig systemovervåkningsapp for menylinjen i macOS.";
"Start the application automatically when starting your Mac" = "Start applikasjonen automatisk når du starter Mac-en din";
"Do not start the application automatically when starting your Mac" = "Ikke start applikasjonen automatisk når du starter Mac-en din";
"Do everything silently in the background (recommended)" = "Gjør alt stille i bakgrunnen (anbefalt)";
"Check for a new version on startup" = "Se etter ny versjon ved oppstart";
"Check for a new version every day (once a day)" = "Se etter ny versjon én gang hver dag";
"Check for a new version every week (once a week)" = "Se etter ny versjon én gang hver uke";
"Check for a new version every month (once a month)" = "Se etter ny versjon én gang hver måned";
"Never check for updates (not recommended)" = "Aldri se etter ny versjon (ikke anbefalt)";
"Anonymous telemetry for better development decisions" = "Anonym telemetri for bedre utviklingsbeslutninger";
"Share anonymous telemetry data" = "Del anonyme telemetridata";
"Do not share anonymous telemetry data" = "Ikke del anonyme telemetridata";
"The configuration is completed" = "Konfigurasjonen er fullført";
"finish_setup_message" = "Alt er satt opp! \n Stats er et fritt tilgjengelig verktøy, det er gratis og vil alltid være det. \n Hvis du liker Stats, kan du støtte prosjektet, det settes veldig pris på!";
// Alerts
"New version available" = "Ny versjon tilgjengelig";
"Click to install the new version of Stats" = "Klikk for å installere ny versjon av Stats";
"Successfully updated" = "Oppdateringen var vellykket";
"Stats was updated to v" = "Stats ble oppdatert til v%0";
"Reset settings text" = "Alle innstillinger vil bli nullstilt og applikasjonen vil starte på nytt. Er du sikker på at du vil gjøre dette?";
"Support text" = "Takk for at du bruker Stats!\n\nDet krever tid og ressurser å vedlikeholde og forbedre dette åpen kildekode-prosjektet. Din støtte hjelper oss med å fortsette å tilby et gratis og pålitelig program for alle. Hvis du synes Stats er nyttig, kan du vurdere å gi et bidrag. Hver eneste lille bit hjelper!";
// Settings
"Open Activity Monitor" = "Åpne Aktivitetsmonitor";
"Report a bug" = "Meld en feil";
"Support the application" = "Støtt programmet";
"Close application" = "Lukk programmet";
"Open application settings" = "Åpne programinnstillinger";
"Open dashboard" = "Åpne dashboard";
"No notifications available in this module" = "Ingen varsler er tilgjengelige i dette modulen"; // translategemma:4b
"Open Calendar" = "Åpne kalender"; // translategemma:4b
"Toggle the module" = "Aktiver/deaktiver modulen"; // translategemma:4b
// Application settings
"Update application" = "Oppdater programmet";
"Check for updates" = "Se etter oppdateringer";
"At start" = "Ved oppstart";
"Once per day" = "Daglig";
"Once per week" = "Ukentlig";
"Once per month" = "Månedlig";
"Never" = "Aldri";
"Check for update" = "Se etter oppdatering";
"Show icon in dock" = "Vis ikon i dock";
"Start at login" = "Start ved innlogging";
"Build number" = "Byggnummer";
"Import settings" = "Importinnstillinger"; // translategemma:4b
"Export settings" = "Eksportinnstillinger"; // translategemma:4b
"Reset settings" = "Nullstill innstillinger";
"Pause the Stats" = "Sett Stats på pause";
"Resume the Stats" = "Gjenoppta Stats";
"Combined modules" = "Kombinerte moduler";
"Combined details" = "Samlet informasjon"; // translategemma:4b
"Spacing" = "Mellomrom";
"Share anonymous telemetry" = "Del anonyme telemetridata";
"Choose file" = "Velg fil"; // translategemma:4b
"Stress tests" = "Stress-tester"; // translategemma:4b
// Dashboard
"Serial number" = "Serienummer";
"Model identifier" = "Modell-identifikator"; // translategemma:4b
"Production year" = "Produksjonsår"; // translategemma:4b
"Uptime" = "Oppetid";
"Number of cores" = "%0 kjerner";
"Number of threads" = "%0 tråder";
"Number of e-cores" = "%0 effektivitetskjerner";
"Number of p-cores" = "%0 ytelseskjerner";
"Disks" = "Disker"; // translategemma:4b
"Display" = "Visning"; // translategemma:4b
// Update
"The latest version of Stats installed" = "Du har den nyeste versjonen av Stats";
"Downloading..." = "Laster ned...";
"Current version: " = "Nåværende versjon: ";
"Latest version: " = "Nyeste version: ";
// Widgets
"Color" = "Farge";
"Label" = "Merkelapp";
"Box" = "Boks";
"Frame" = "Ramme";
"Value" = "Verdi";
"Colorize" = "Fargelegg";
"Colorize value" = "Fargelegg verdi";
"Additional information" = "Tilleggsinformasjon";
"Reverse values order" = "Reverser rekkefølge på verdier";
"Base" = "Grunn"; // translategemma:4b
"Display mode" = "Visningsmodus";
"One row" = "En rad";
"Two rows" = "To rader";
"Mini widget" = "Mini";
"Line chart widget" = "Linjediagram";
"Bar chart widget" = "Søylediagram";
"Pie chart widget" = "Kakediagram";
"Network chart widget" = "Linjediagram";
"Speed widget" = "Tall";
"Battery widget" = "Batteri";
"Stack widget" = "Bunke";
"Memory widget" = "Tall";
"Static width" = "Statisk bredde";
"Tachometer widget" = "Hastometer"; // translategemma:4b
"State widget" = "Tilstandsdiagram";
"Text widget" = "Tekstfelt"; // translategemma:4b
"Battery details widget" = "Widget for batteriinformasjon"; // translategemma:4b
"Show symbols" = "Vis symboler";
"Label widget" = "Merkelapp";
"Number of reads in the chart" = "Antall avlesninger i diagrammet";
"Color of download" = "Farge for nedlasting";
"Color of upload" = "Farge for opplasting";
"Monospaced font" = "Skrifttype med fast bredde";
"Reverse order" = "Reversert rekkefølge";
"Chart history" = "Grafhistorikk"; // translategemma:4b
"Default color" = "Standard"; // translategemma:4b
"Transparent when no activity" = "Transparent når det ikke er noen aktivitet"; // translategemma:4b
"Constant color" = "Konstant"; // translategemma:4b
// Module Kit
"Open module settings" = "Åpne modulinnstillinger";
"Select widget" = "Velg %0-visning";
"Open widget settings" = "Åpne widgetinnstillinger";
"Update interval" = "Oppdateringsintervall";
"Usage history" = "Brukshistorikk";
"Details" = "Detaljer";
"Top processes" = "Topp-prosesser";
"Pictogram" = "Piktogram";
"Module" = "Modul";
"Widgets" = "Widgeter";
"Popup" = "Pop-up"; // translategemma:4b
"Notifications" = "Varsler";
"Merge widgets" = "Slå sammen diagrammer";
"No available widgets to configure" = "Ingen tilgjengelige diagrammer å konfigurere";
"No options to configure for the popup in this module" = "Ingen innstillinger å konfigurere for popup-en i denne modulen";
"Process" = "Prosess"; // translategemma:4b
"Kill process" = "Avslutt prosessen"; // translategemma:4b
"Keyboard shortcut" = "Tastatursnarvei"; // translategemma:4b
"Listening..." = "Lytt..."; // translategemma:4b
// Modules
"Number of top processes" = "Antall topp-prosesser";
"Update interval for top processes" = "Oppdateringsintervall for topp-prosesser";
"Notification level" = "Varslingsnivå";
"Chart color" = "Diagramfarge";
"Main chart scaling" = "Hovedskala for diagrammet"; // translategemma:4b
"Scale value" = "Skalaverdi"; // translategemma:4b
"Text widget value" = "Verdi for tekstfelt"; // translategemma:4b
// CPU
"CPU usage" = "CPU-bruk";
"CPU temperature" = "CPU-temperatur";
"CPU frequency" = "CPU-frekvens";
"System" = "System";
"User" = "Bruker";
"Idle" = "Ledig";
"Show usage per core" = "Vis bruk per kjerne";
"Show hyper-threading cores" = "Vis hyper-threading-kjerner";
"Split the value (System/User)" = "Del verdien (System/Bruker)";
"Scheduler limit" = "Grense for tidsplanlegger";
"Speed limit" = "Fartsgrense";
"Average load" = "Gjennomsnittsbelastning";
"1 minute" = "1 minutt";
"5 minutes" = "5 minutter";
"15 minutes" = "15 minutter";
"CPU usage threshold" = "CPU-bruksgrense";
"CPU usage is" = "CPU-bruk er %0";
"Efficiency cores" = "Effektivitetskjerner";
"Performance cores" = "Ytelseskjerner";
"System color" = "Systemfarge";
"User color" = "Brukerfarge";
"Idle color" = "Tomgangsfarge";
"Cluster grouping" = "Klyngegruppering";
"Efficiency cores color" = "Farge på effektivitetskjerner";
"Performance cores color" = "Farge på ytelseskjerner";
"Total load" = "Total last"; // translategemma:4b
"System load" = "Systembelastning"; // translategemma:4b
"User load" = "Brukerbelastning"; // translategemma:4b
"Efficiency cores load" = "Effektivitetskerner laster"; // translategemma:4b
"Performance cores load" = "Ytelseskjerner lastes"; // translategemma:4b
"All cores" = "Alle kjerner"; // translategemma:4b
// GPU
"GPU to show" = "GPU å vise";
"Show GPU type" = "Vis GPU-type";
"GPU enabled" = "GPU i bruk";
"GPU disabled" = "GPU ikke i bruk";
"GPU temperature" = "GPU-temperatur";
"GPU utilization" = "GPU-bruk";
"Vendor" = "Produsent";
"Model" = "Modell";
"Status" = "Status";
"Active" = "Aktiv";
"Non active" = "Inaktiv";
"Fan speed" = "Viftehastighet";
"Core clock" = "Kjerneklokke";
"Memory clock" = "Minneklokke";
"Utilization" = "Bruk";
"Render utilization" = "Renderbruk";
"Tiler utilization" = "Rutebruk";
"GPU usage threshold" = "GPU-bruksgrense";
"GPU usage is" = "GPU-bruk er %0";
// RAM
"Memory usage" = "Minnebruk";
"Memory pressure" = "Minnebelastning";
"Total" = "Totalt";
"Used" = "Brukt";
"App" = "Programmer";
"Wired" = "Opptatt";
"Compressed" = "Komprimert";
"Free" = "Ledig";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Del verdien (Programmer/Opptatt/Komprimert)";
"RAM utilization threshold" = "RAM-bruksgrense";
"RAM utilization is" = "RAM-bruk er %0";
"App color" = "App-farge";
"Wired color" = "Bundet farge";
"Compressed color" = "Komprimert farge";
"Free color" = "Ledig farge";
"Free memory (less than)" = "Gratis minne (mindre enn)"; // translategemma:4b
"Swap size" = "Byttes størrelse"; // translategemma:4b
"Free RAM is" = "Gratis RAM er %0"; // translategemma:4b
// Disk
"Show removable disks" = "Vis utløsbare disker";
"Used disk memory" = "Brukte %0 fra %1";
"Free disk memory" = "Ledig %0 fra %1";
"Disk to show" = "Disker å vise";
"Open disk" = "Åpne disken";
"Switch view" = "Bytt visning";
"Disk utilization threshold" = "Disk-bruksgrense";
"Disk utilization is" = "Disk-bruk er %0";
"Read color" = "Lesefarge";
"Write color" = "Skrivefarge";
"Disk usage" = "Diskbruk";
"Total read" = "Total lest"; // translategemma:4b
"Total written" = "Totalt skrevet"; // translategemma:4b
"Write speed" = "Skriv"; // translategemma:4b
"Read speed" = "Les"; // translategemma:4b
"Drives" = "Drives";
"SMART data" = "SMART-data"; // translategemma:4b
// Sensors
"Temperature unit" = "Temperaturenhet";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Lagre viftehastighet";
"Fan" = "Vifte";
"HID sensors" = "HID-sensorer";
"Synchronize fan's control" = "Synkroniser viftekontroll";
"Current" = "Nåværende";
"Energy" = "Energi";
"Show unknown sensors" = "Vis ukjente sensorer";
"Install fan helper" = "Installer viftehjelper";
"Uninstall fan helper" = "Avinstaller viftehjelper";
"Fan value" = "Vifteverdi";
"Turn off fan" = "Slå av viften";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Du slå nå av viften. Dette er en ikke-anbefalt handling som kan skade Mac-en din, er du sikker på at vil gjøre dette?";
"Sensor threshold" = "Sensortilstand"; // translategemma:4b
"Left fan" = "Venstre"; // translategemma:4b
"Right fan" = "Riktig"; // translategemma:4b
"Fastest fan" = "Raskest"; // translategemma:4b
"Sensor to show" = "Sensor for å vise"; // translategemma:4b
// Network
"Uploading" = "Opplasting";
"Downloading" = "Nedlasting";
"Public IP" = "Offentlig IP";
"Local IP" = "Lokal IP";
"Interface" = "Grensesnitt";
"Physical address" = "Fysisk addresse";
"Refresh" = "Oppfrisk";
"Click to copy public IP address" = "Klikk for å kopiere offentlig IP-adresse";
"Click to copy local IP address" = "Klikk for å kopiere lokal IP-adresse";
"Click to copy wifi name" = "Klikk for å kopiere nettverksnavn";
"Click to copy mac address" = "Klikk for å kopiere MAC-adresse";
"No connection" = "Ingen forbindelse";
"Network interface" = "Nettverksgrensesnitt";
"Total download" = "Totalt lastet ned";
"Total upload" = "Totalt lastet opp";
"Reader type" = "Lesertype";
"Interface based" = "Grensesnitt";
"Processes based" = "Prosess";
"Reset data usage" = "Nullstill databruk";
"VPN mode" = "VPN-modues";
"Standard" = "Standard";
"Security" = "Sikkerhet";
"Channel" = "Kanal";
"Common scale" = "Vanlig skala";
"Autodetection" = "Automatisk deteksjon";
"Widget activation threshold" = "Statistikkaktiveringsgrense";
"Internet connection" = "Internett-tilkobling";
"Active state color" = "Aktiv statusfarge";
"Nonactive state color" = "Inaktiv statusfarge";
"Connectivity host (ICMP)" = "Tilkoblingsvert (ICMP)";
"Leave empty to disable the check" = "La stå tom for å deaktivere sjekken";
"Connectivity history" = "Tilkoblingshistorikk";
"Auto-refresh public IP address" = "Auto-oppfrisk offentlig IP-adresse";
"Every hour" = "Hver time";
"Every 12 hours" = "Hver 12. time";
"Every 24 hours" = "Hver 24. time";
"Network activity" = "Nettverksaktivitet";
"Last reset" = "Sist nullstilt %0 siden";
"Latency" = "Forsinkelse"; // translategemma:4b
"Upload speed" = "Last opp"; // translategemma:4b
"Download speed" = "Last ned"; // translategemma:4b
"Address" = "Adresse"; // translategemma:4b
"WiFi network" = "WiFi-nettverk"; // translategemma:4b
"Local IP changed" = "Den lokale IP-adressen har endret seg."; // translategemma:4b
"Public IP changed" = "Den offentlige IP-adressen har endret seg."; // translategemma:4b
"Previous IP" = "Tidligere IP-adresse: %0"; // translategemma:4b
"New IP" = "Ny IP: %0"; // translategemma:4b
"Internet connection lost" = "Internett-tilkoblingen er tapt."; // translategemma:4b
"Internet connection established" = "Internett-tilkobling etablert"; // translategemma:4b
// Battery
"Level" = "Nivå";
"Source" = "Kilde";
"AC Power" = "Strømforsyning";
"Battery Power" = "Batteri";
"Time" = "Tid";
"Health" = "Helse";
"Amperage" = "Strømstyrke";
"Voltage" = "Spenning";
"Cycles" = "Sykluser";
"Temperature" = "Temperatur";
"Power adapter" = "Strømadapter";
"Power" = "Strøm";
"Is charging" = "Lader";
"Time to discharge" = "Tid på å bruke opp";
"Time to charge" = "Tid på å lade";
"Calculating" = "Kalkulerer";
"Fully charged" = "Fullstendig ladet";
"Not connected" = "Ikke koblet til";
"Low level notification" = "Varsling ved lavt nivå";
"High level notification" = "Varsling ved høyt nivå";
"Low battery" = "Lavt batterinivå";
"High battery" = "Høyt batterinivå";
"Battery remaining" = "%0% igjen";
"Battery remaining to full charge" = "%0% til fulladet";
"Percentage" = "Prosent";
"Percentage and time" = "Prosent og tid";
"Time and percentage" = "Tid og prosent";
"Time format" = "Tidsformat";
"Hide additional information when full" = "Skjul ekstra information når fulladet";
"Last charge" = "Siste lading";
"Capacity" = "Kapasitet";
"current / maximum / designed" = "current / maksimum / tiltenkt";
"Low power mode" = "Lav energi-modus";
"Percentage inside the icon" = "Prosent inne i ikonet";
"Colorize battery" = "Fargelegg batteri";
"Charging current" = "Lade-strøm"; // translategemma:4b
"Charging Voltage" = "Ladevoltat"; // translategemma:4b
"Charger state inside the battery" = "Tilstand for laderen inne i batteriet"; // translategemma:4b
// Bluetooth
"Battery to show" = "Batteri å vise";
"No Bluetooth devices are available" = "Ingen Bluetooth-enheter er tilgjengelige";
// Clock
"Time zone" = "Tidssone";
"Local" = "Lokal";
"Calendar" = "Kalender"; // translategemma:4b
"Show week numbers" = "Vis ukenummer"; // translategemma:4b
"Local time" = "Lokal tid"; // translategemma:4b
"Add new clock" = "Legg til ny klokke"; // translategemma:4b
"Delete selected clock" = "Slett valgt klokke"; // translategemma:4b
"Help with datetime format" = "Hjelp med formatering av dato og klokkeslett"; // translategemma:4b
// Colors
"Based on utilization" = "Basert på bruk";
"Based on pressure" = "Basert på trykk";
"Based on cluster" = "Basert på klynge";
"System accent" = "Systemaksent";
"Monochrome accent" = "Ensfarget aksent";
"Clear" = "Blank";
"White" = "Hvit";
"Black" = "Svart";
"Gray" = "Grå";
"Second gray" = "Grå 2";
"Dark gray" = "Mørkegrå";
"Light gray" = "Lysegrå";
"Red" = "Rød";
"Second red" = "Rød 2";
"Green" = "Grønn";
"Second green" = "Grønn 2";
"Blue" = "Blå";
"Second blue" = "Blå 2";
"Yellow" = "Gul";
"Second yellow" = "Gul 2";
"Orange" = "Oransje";
"Second orange" = "Oransje 2";
"Purple" = "Lilla";
"Second purple" = "Lilla 2";
"Brown" = "Brun";
"Second brown" = "Brun 2";
"Cyan" = "Turkis";
"Magenta" = "Magenta";
"Pink" = "Rosa";
"Teal" = "Blågrønn";
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/nl.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Phúc Ngô on 02/04/2021.
//
// Running on macOS Big Sur M1.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "Open CPU instellingen";
"GPU" = "GPU";
"Open GPU settings" = "Open GPU instellingen";
"RAM" = "RAM";
"Open RAM settings" = "Open RAM instellingen";
"Disk" = "Schijf"; // translategemma:4b
"Open Disk settings" = "Open disk instellingen";
"Sensors" = "Sensoren"; // translategemma:4b
"Open Sensors settings" = "Open sensors instellingen";
"Network" = "Netwerk"; // translategemma:4b
"Open Network settings" = "Open network instellingen";
"Battery" = "Batterij"; // translategemma:4b
"Open Battery settings" = "Open battery instellingen";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Open bluetooth instellingen";
"Clock" = "Uur"; // translategemma:4b
"Open Clock settings" = "Open de instellingen voor de klok"; // translategemma:4b
// Words
"Unknown" = "Onbekend";
"Version" = "Versie";
"Processor" = "Processor";
"Memory" = "Geheugen";
"Graphics" = "Grafisch";
"Close" = "Sluiten";
"Download" = "Download";
"Install" = "Installeren";
"Cancel" = "Annuleren";
"Unavailable" = "Onbereikbaar";
"Yes" = "Ja";
"No" = "Nee";
"Automatic" = "Automatisch";
"Manual" = "Handmatig";
"None" = "Geen";
"Dots" = "Punten"; // translategemma:4b
"Arrows" = "Pijlen";
"Characters" = "Tekens";
"Short" = "Kort";
"Long" = "Lang";
"Statistics" = "Statistieken";
"Max" = "Maximaal";
"Min" = "Min";
"Reset" = "Herstellen"; // translategemma:4b
"Alignment" = "Uitlijning";
"Left alignment" = "Links";
"Center alignment" = "Midden";
"Right alignment" = "Rechts";
"Dashboard" = "Dashboard";
"Enabled" = "Aanwezig"; // translategemma:4b
"Disabled" = "Uitgeschakeld";
"Silent" = "Stil"; // translategemma:4b
"Units" = "Eenheden"; // translategemma:4b
"Fans" = "Fans";
"Scaling" = "Schaalbaarheid"; // translategemma:4b
"Linear" = "Lineair"; // translategemma:4b
"Square" = "Vierkant"; // translategemma:4b
"Cube" = "Kubus"; // translategemma:4b
"Logarithmic" = "Logaritmisch"; // translategemma:4b
"Fixed scale" = "Opgelost"; // translategemma:4b
"Cores" = "Kernen"; // translategemma:4b
"Settings" = "Instellingen"; // translategemma:4b
"Name" = "Naam"; // translategemma:4b
"Format" = "Formaat"; // translategemma:4b
"Turn off" = "Uitschakelen"; // translategemma:4b
"Normal" = "Normaal"; // translategemma:4b
"Warning" = "Waarschuwing"; // translategemma:4b
"Critical" = "Cruciaal"; // translategemma:4b
"Usage" = "Gebruik"; // translategemma:4b
"2 minutes" = "2 minuten"; // translategemma:4b
"3 minutes" = "3 minuten"; // translategemma:4b
"10 minutes" = "10 minuten"; // translategemma:4b
"Import" = "Importeren"; // translategemma:4b
"Export" = "Exporter"; // translategemma:4b
"Separator" = "Scheiding"; // translategemma:4b
"Read" = "Lees"; // translategemma:4b
"Write" = "Schrijf"; // translategemma:4b
"Frequency" = "Frequentie"; // translategemma:4b
"Save" = "Opslaan"; // translategemma:4b
"Run" = "Uitvoeren"; // translategemma:4b
"Stop" = "Stoppen"; // translategemma:4b
"Uninstall" = "Verwijderen"; // translategemma:4b
"1 sec" = "1 seconde"; // translategemma:4b
"2 sec" = "2 second"; // translategemma:4b
"3 sec" = "3 seconden"; // translategemma:4b
"5 sec" = "5 seconden"; // translategemma:4b
"10 sec" = "10 sec";
"15 sec" = "15 sec";
"30 sec" = "30 seconden"; // translategemma:4b
"60 sec" = "60 seconden"; // translategemma:4b
// Setup
"Stats Setup" = "Instellingen instellen"; // translategemma:4b
"Previous" = "Vorige"; // translategemma:4b
"Previous page" = "Vorige pagina"; // translategemma:4b
"Next" = "Volgende"; // translategemma:4b
"Next page" = "Volgende pagina"; // translategemma:4b
"Finish" = "Afmaken"; // translategemma:4b
"Finish setup" = "Voltooi de installatie"; // translategemma:4b
"Welcome to Stats" = "Welkom bij Stats"; // translategemma:4b
"welcome_message" = "Bedankt voor het gebruik van Stats, een gratis en open-source macOS-systeemmonitor voor uw menubalk."; // translategemma:4b
"Start the application automatically when starting your Mac" = "Start de applicatie automatisch wanneer u uw Mac opstart."; // translategemma:4b
"Do not start the application automatically when starting your Mac" = "Start de applicatie niet automatisch wanneer u uw Mac opstart."; // translategemma:4b
"Do everything silently in the background (recommended)" = "Voer alles discreet uit op de achtergrond (aanbevolen)"; // translategemma:4b
"Check for a new version on startup" = "Controleer op het opstarten of er een nieuwe versie beschikbaar is."; // translategemma:4b
"Check for a new version every day (once a day)" = "Controleer dagelijks op een nieuwe versie (een keer per dag)"; // translategemma:4b
"Check for a new version every week (once a week)" = "Controleer wekelijks op een nieuwe versie (één keer per week)"; // translategemma:4b
"Check for a new version every month (once a month)" = "Controleer maandelijks (één keer per maand) op een nieuwe versie."; // translategemma:4b
"Never check for updates (not recommended)" = "Controleer nooit op updates (niet aanbevolen)"; // translategemma:4b
"Anonymous telemetry for better development decisions" = "Anonieme telemetrie voor betere beslissingen tijdens de ontwikkeling"; // translategemma:4b
"Share anonymous telemetry data" = "Deel anonieme telemetriegegevens"; // translategemma:4b
"Do not share anonymous telemetry data" = "Deel geen anonieme telemetriegegevens."; // translategemma:4b
"The configuration is completed" = "De configuratie is voltooid."; // translategemma:4b
"finish_setup_message" = "Alles is klaar!"; // translategemma:4b
// Alerts
"New version available" = "Nieuwe versie beschikbaar";
"Click to install the new version of Stats" = "Klik om de nieuwe versie van Stats te installeren";
"Successfully updated" = "Succesvol geüpdatet";
"Stats was updated to v" = "Stats is bijgewerkt naar v%0";
"Reset settings text" = "Alle applicatie instellingen worden gereset en de applicatie wordt opnieuw opgestart. Weet u zeker dat u dit wilt doen?";
"Support text" = "Bedankt voor het gebruik van Stats! Het onderhouden en verbeteren van dit open-source project kost tijd en middelen. Als je Stats nuttig vindt, overweeg dan alsjeblieft om een bijdrage te leveren. Alle beetjes helpen!";
// Settings
"Open Activity Monitor" = "Open Activiteitenweergave";
"Report a bug" = "Meld een bug";
"Support the application" = "Ondersteun de applicatie";
"Close application" = "Applicatie sluiten";
"Open application settings" = "Open de applicatie-instellingen";
"Open dashboard" = "Open dashboard";
"No notifications available in this module" = "Geen meldingen beschikbaar in dit module"; // translategemma:4b
"Open Calendar" = "Open Kalender"; // translategemma:4b
"Toggle the module" = "Activeer/deactiveer het module"; // translategemma:4b
// Application settings
"Update application" = "Applicatie bijwerken";
"Check for updates" = "Controleer op updates";
"At start" = "Bij het opstarten";
"Once per day" = "Eenmaal per dag";
"Once per week" = "Eenmaal per week";
"Once per month" = "Eenmaal per maand";
"Never" = "Nooit";
"Check for update" = "Controleer op updates";
"Show icon in dock" = "Pictogram in dock weergeven";
"Start at login" = "Starten bij inloggen";
"Build number" = "Bouwnummer"; // translategemma:4b
"Import settings" = "Importer instellingen"; // translategemma:4b
"Export settings" = "Exporteerinstellingen"; // translategemma:4b
"Reset settings" = "Instellingen herstellen"; // translategemma:4b
"Pause the Stats" = "Pauzeer de statistieken"; // translategemma:4b
"Resume the Stats" = "Herhaal de statistieken"; // translategemma:4b
"Combined modules" = "Gecombineerde modules"; // translategemma:4b
"Combined details" = "Gezamenlijke details"; // translategemma:4b
"Spacing" = "Ruimte"; // translategemma:4b
"Share anonymous telemetry" = "Deel anonieme telemetriegegevens"; // translategemma:4b
"Choose file" = "Kies bestand"; // translategemma:4b
"Stress tests" = "Stress tests";
// Dashboard
"Serial number" = "Serienummer";
"Model identifier" = "Model-identificatie"; // translategemma:4b
"Production year" = "Jaar van productie"; // translategemma:4b
"Uptime" = "Beschikbaarheid";
"Number of cores" = "%0 kernen"; // translategemma:4b
"Number of threads" = "%0 threads";
"Number of e-cores" = "%0 efficiënte kernen"; // translategemma:4b
"Number of p-cores" = "%0 prestatiecores"; // translategemma:4b
"Disks" = "Schijven"; // translategemma:4b
"Display" = "Weergave"; // translategemma:4b
// Update
"The latest version of Stats installed" = "De laatste versie van Stats is geïnstalleerd.";
"Downloading..." = "Aan het downloaden...";
"Current version: " = "Huidige versie: ";
"Latest version: " = "Nieuwste versie: ";
// Widgets
"Color" = "Kleur";
"Label" = "Label";
"Box" = "Doos"; // translategemma:4b
"Frame" = "Frame";
"Value" = "Waarde";
"Colorize" = "Inkleuren";
"Colorize value" = "Inkleuren waarde";
"Additional information" = "Extra informatie";
"Reverse values order" = "Waarden in omgekeerde volgorde";
"Base" = "Basis";
"Display mode" = "Weergavemodus";
"One row" = "Een rij";
"Two rows" = "Twee rijen";
"Mini widget" = "Miniwidget";
"Line chart widget" = "Lijngrafiek";
"Bar chart widget" = "Staafgrafiek";
"Pie chart widget" = "Taartgrafiek";
"Network chart widget" = "Netwerkgrafiek";
"Speed widget" = "Snelheidswidget";
"Battery widget" = "Batterijwidget";
"Stack widget" = "Stack";
"Memory widget" = "Geheugenwidget";
"Static width" = "Vaste breedte"; // translategemma:4b
"Tachometer widget" = "Snelheidsmeter"; // translategemma:4b
"State widget" = "Statuswidget"; // translategemma:4b
"Text widget" = "Tekstwidget"; // translategemma:4b
"Battery details widget" = "Widget met batterijgegevens"; // translategemma:4b
"Show symbols" = "Toon symbolen";
"Label widget" = "Label";
"Number of reads in the chart" = "Aantal keer gelezen in de grafiek";
"Color of download" = "Kleur van download";
"Color of upload" = "Kleur van upload";
"Monospaced font" = "Monospaced lettertype";
"Reverse order" = "Omgekeerde volgorde"; // translategemma:4b
"Chart history" = "Historie van de grafiek"; // translategemma:4b
"Default color" = "Standaard"; // translategemma:4b
"Transparent when no activity" = "Transparant wanneer er geen activiteit is"; // translategemma:4b
"Constant color" = "Constante"; // translategemma:4b
// Module Kit
"Open module settings" = "Open module-instellingen";
"Select widget" = "Selecteer %0 widget";
"Open widget settings" = "Open widget-instellingen";
"Update interval" = "Bijwerkings-interval";
"Usage history" = "Gebruiksgeschiedenis";
"Details" = "Details";
"Top processes" = "Topprocessen";
"Pictogram" = "Pictogram";
"Module" = "Module";
"Widgets" = "Widgets";
"Popup" = "Pop-up";
"Notifications" = "Meddelelser";
"Merge widgets" = "Widgets samenvoegen";
"No available widgets to configure" = "Geen beschikbare widgets om te configureren";
"No options to configure for the popup in this module" = "Meervoudige configuratie voor pop-op-vinduet en deze module";
"Process" = "Proces"; // translategemma:4b
"Kill process" = "Beëindig proces"; // translategemma:4b
"Keyboard shortcut" = "Sneltoets"; // translategemma:4b
"Listening..." = "Luisteren..."; // translategemma:4b
// Modules
"Number of top processes" = "Aantal topprocessen";
"Update interval for top processes" = "Bewerk interval voor top processen";
"Notification level" = "Meldingsniveau";
"Chart color" = "Kortfarve";
"Main chart scaling" = "Hoofdschaal schaal"; // translategemma:4b
"Scale value" = "Waarde"; // translategemma:4b
"Text widget value" = "Waarde van het tekstwidget-element"; // translategemma:4b
// CPU
"CPU usage" = "CPU gebruik";
"CPU temperature" = "CPU temperatuur";
"CPU frequency" = "CPU frequentie";
"System" = "Systeem";
"User" = "Gebruiker";
"Idle" = "Inactief";
"Show usage per core" = "Gebruik per core weergeven";
"Show hyper-threading cores" = "Hyperthreading cores weergeven";
"Split the value (System/User)" = "Opdel værdien (Systeem/Gebruiker)";
"Scheduler limit" = "Planner limiet";
"Speed limit" = "Snelheidslimiet";
"Average load" = "Gemiddelde belasting ";
"1 minute" = "1 minuut";
"5 minutes" = "5 minuten";
"15 minutes" = "15 minuten";
"CPU usage threshold" = "Drempelwaarde voor CPU-gebruik"; // translategemma:4b
"CPU usage is" = "Het CPU-gebruik is %0"; // translategemma:4b
"Efficiency cores" = "Efficiënte kernen"; // translategemma:4b
"Performance cores" = "Prestatie-cores"; // translategemma:4b
"System color" = "Kleur van het systeem"; // translategemma:4b
"User color" = "Kleur van de gebruiker"; // translategemma:4b
"Idle color" = "Kleur in ruststand"; // translategemma:4b
"Cluster grouping" = "Groeperen van clusters"; // translategemma:4b
"Efficiency cores color" = "Kleuren van de kernen voor efficiëntie"; // translategemma:4b
"Performance cores color" = "Kleur van de prestatie-cores"; // translategemma:4b
"Total load" = "Totale belasting"; // translategemma:4b
"System load" = "Laad op het systeem"; // translategemma:4b
"User load" = "Gebruikersbelasting"; // translategemma:4b
"Efficiency cores load" = "Efficiëntie-cores laden"; // translategemma:4b
"Performance cores load" = "Laad van de prestatie-cores"; // translategemma:4b
"All cores" = "Alle kernen"; // translategemma:4b
// GPU
"GPU to show" = "GPU om te laten zien";
"Show GPU type" = "GPU-type weergeven";
"GPU enabled" = "GPU ingeschakeld";
"GPU disabled" = "GPU uitgeschakeld";
"GPU temperature" = "GPU-temperatuur";
"GPU utilization" = "GPU-gebruik";
"Vendor" = "Fabrikant";
"Model" = "Model";
"Status" = "Status";
"Active" = "Actief";
"Non active" = "Niet actief";
"Fan speed" = "Ventilatorsneelheid";
"Core clock" = "Core klok";
"Memory clock" = "Geheugen klok";
"Utilization" = "Benutting";
"Render utilization" = "Gebruik van render-resources"; // translategemma:4b
"Tiler utilization" = "Optimal gebruik van tegelwerk"; // translategemma:4b
"GPU usage threshold" = "Drempelwaarde voor GPU-gebruik"; // translategemma:4b
"GPU usage is" = "Het gebruik van de GPU bedraagt %0"; // translategemma:4b
// RAM
"Memory usage" = "Geheugengebruik";
"Memory pressure" = "Geheugendruk";
"Total" = "Totaal";
"Used" = "Gebruikt";
"App" = "App";
"Wired" = "Bedraad"; // translategemma:4b
"Compressed" = "Gecomprimeerd";
"Free" = "Vrij";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Opdel værdien (App/Wired/Gecomprimeerd)";
"RAM utilization threshold" = "Drempelwaarde voor het gebruik van RAM"; // translategemma:4b
"RAM utilization is" = "Het gebruik van RAM bedraagt %0"; // translategemma:4b
"App color" = "Kleur van de app"; // translategemma:4b
"Wired color" = "Gestructureerde kleur"; // translategemma:4b
"Compressed color" = "Gecodeerde kleur"; // translategemma:4b
"Free color" = "Gratis kleur"; // translategemma:4b
"Free memory (less than)" = "Vrij geheugen (minder dan)"; // translategemma:4b
"Swap size" = "Grootte van het uitwisselingsgebied"; // translategemma:4b
"Free RAM is" = "Gratis RAM is %0"; // translategemma:4b
// Disk
"Show removable disks" = "Toon verwijderbare schijven";
"Used disk memory" = "%0 van %1 gebruikt";
"Free disk memory" = "%0 van %1 vrij";
"Disk to show" = "Schijf om te laten zien";
"Open disk" = "Open schijf";
"Switch view" = "Wissel weergave";
"Disk utilization threshold" = "Drempelwaarde voor het gebruik van schijfruimte"; // translategemma:4b
"Disk utilization is" = "Het gebruik van de schijf is %0"; // translategemma:4b
"Read color" = "Lees de kleur"; // translategemma:4b
"Write color" = "Schrijf kleur"; // translategemma:4b
"Disk usage" = "Gebruikte schijfruimte"; // translategemma:4b
"Total read" = "Totaal gelezen"; // translategemma:4b
"Total written" = "Totale hoeveelheid geschreven"; // translategemma:4b
"Write speed" = "Schrijf"; // translategemma:4b
"Read speed" = "Lees"; // translategemma:4b
"Drives" = "Schijven"; // translategemma:4b
"SMART data" = "SMART-gegevens"; // translategemma:4b
// Sensors
"Temperature unit" = "Temperatuureenheid";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Ventilatorsnelheid opslaan";
"Fan" = "Ventilator";
"HID sensors" = "Sensoren voor het meten van de hoogte"; // translategemma:4b
"Synchronize fan's control" = "Synchroniseer de bediening van de ventilatoren"; // translategemma:4b
"Current" = "Huidige"; // translategemma:4b
"Energy" = "Energie"; // translategemma:4b
"Show unknown sensors" = "Toon onbekende sensoren"; // translategemma:4b
"Install fan helper" = "Installeer de ventilator-assistent"; // translategemma:4b
"Uninstall fan helper" = "Verwijder de fan helper."; // translategemma:4b
"Fan value" = "Waarde van de ventilator"; // translategemma:4b
"Turn off fan" = "Zet de ventilator uit"; // translategemma:4b
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "U gaat de ventilator uitschakelen. Dit is een onaanbevolen actie die uw Mac kan beschadigen. Bent u er zeker van dat u dit wilt doen?"; // translategemma:4b
"Sensor threshold" = "Grenswaarde sensor"; // translategemma:4b
"Left fan" = "Links"; // translategemma:4b
"Right fan" = "Correct"; // translategemma:4b
"Fastest fan" = "Snelst"; // translategemma:4b
"Sensor to show" = "Sensor om weer te laten zien"; // translategemma:4b
// Network
"Uploading" = "Uploaden";
"Downloading" = "Downloaden";
"Public IP" = "Openbaar IP-adres";
"Local IP" = "Lokaal IP-adres";
"Interface" = "Interface";
"Physical address" = "Fysiek adres";
"Refresh" = "Ververs";
"Click to copy public IP address" = "Klik om openbaar IP-adres te kopiëren";
"Click to copy local IP address" = "Klik om lokaal IP-adres te kopiëren";
"Click to copy wifi name" = "Klik om wifi-naam te kopiëren";
"Click to copy mac address" = "Klik om mac-adres te kopiëren";
"No connection" = "Geen verbinding";
"Network interface" = "Netwerk interface";
"Total download" = "Totaal download";
"Total upload" = "Totaal upload";
"Reader type" = "Lezer type";
"Interface based" = "Interface-gebaseerd";
"Processes based" = "Proces-gebaseerd";
"Reset data usage" = "Reset dataverbruik"; // translategemma:4b
"VPN mode" = "Modus VPN"; // translategemma:4b
"Standard" = "Standaard"; // translategemma:4b
"Security" = "Beveiliging"; // translategemma:4b
"Channel" = "Kanaal"; // translategemma:4b
"Common scale" = "Veelgebruikte schaal"; // translategemma:4b
"Autodetection" = "Automatische detectie"; // translategemma:4b
"Widget activation threshold" = "Drempelwaarde voor het activeren van een widget"; // translategemma:4b
"Internet connection" = "Internetverbinding"; // translategemma:4b
"Active state color" = "Kleur in de actieve toestand"; // translategemma:4b
"Nonactive state color" = "Kleur voor de niet-actieve toestand"; // translategemma:4b
"Connectivity host (ICMP)" = "Host voor verbinding (ICMP)"; // translategemma:4b
"Leave empty to disable the check" = "Laat dit vel leeg, om de controle uit te schakelen."; // translategemma:4b
"Connectivity history" = "Historie van verbindingen"; // translategemma:4b
"Auto-refresh public IP address" = "Automatisch IP-adres van het publieke netwerk herstellen"; // translategemma:4b
"Every hour" = "Elke uur"; // translategemma:4b
"Every 12 hours" = "Elke 12 uur"; // translategemma:4b
"Every 24 hours" = "Elke 24 uur"; // translategemma:4b
"Network activity" = "Netwerkactiviteit"; // translategemma:4b
"Last reset" = "Laatste reset %0 geleden"; // translategemma:4b
"Latency" = "Vertraging"; // translategemma:4b
"Upload speed" = "Uploaden"; // translategemma:4b
"Download speed" = "Download";
"Address" = "Adres"; // translategemma:4b
"WiFi network" = "WiFi-netwerk"; // translategemma:4b
"Local IP changed" = "Het lokale IP-adres is veranderd"; // translategemma:4b
"Public IP changed" = "Het openbare IP-adres is gewijzigd."; // translategemma:4b
"Previous IP" = "Vorige IP-adres: %0"; // translategemma:4b
"New IP" = "Nieuw IP-adres: %0"; // translategemma:4b
"Internet connection lost" = "Verbinding met het internet is verloren gegaan"; // translategemma:4b
"Internet connection established" = "Internetverbinding is opgezet"; // translategemma:4b
// Battery
"Level" = "Niveau";
"Source" = "Bron";
"AC Power" = "Wisselstroom";
"Battery Power" = "Batterijvermogen";
"Time" = "Tijd";
"Health" = "Gezondheid";
"Amperage" = "Stroomsterkte"; // translategemma:4b
"Voltage" = "Spanning"; // translategemma:4b
"Cycles" = "Cycli";
"Temperature" = "Temperatuur";
"Power adapter" = "Oplader";
"Power" = "Vermogen";
"Is charging" = "Aan het opladen";
"Time to discharge" = "Tijd tot ontladen";
"Time to charge" = "Tijd tot opgeladen";
"Calculating" = "Berekenen";
"Fully charged" = "Volledig opgeladen";
"Not connected" = "Niet verbonden";
"Low level notification" = "Melding laag niveau";
"High level notification" = "Melding hoog niveau";
"Low battery" = "Bijna lege batterij";
"High battery" = "Volle batterij";
"Battery remaining" = "%0% over";
"Battery remaining to full charge" = "%0% tot volledig opgeladen";
"Percentage" = "Percentage";
"Percentage and time" = "Percentage en tijd";
"Time and percentage" = "Tijd and Percentage";
"Time format" = "Tijdsnotatie";
"Hide additional information when full" = "Verberg aanvullende informatie wanneer batterij vol is";
"Last charge" = "Laatste oplading";
"Capacity" = "Capaciteit"; // translategemma:4b
"current / maximum / designed" = "huidig / maximum / ontworpen"; // translategemma:4b
"Low power mode" = "Standaardmodus met laag stroomverbruik"; // translategemma:4b
"Percentage inside the icon" = "Percentage binnen het icoon"; // translategemma:4b
"Colorize battery" = "Kleuren de batterij"; // translategemma:4b
"Charging current" = "Laadstroom"; // translategemma:4b
"Charging Voltage" = "Laadspanning"; // translategemma:4b
"Charger state inside the battery" = "Status van de oplaadunit binnenin de batterij"; // translategemma:4b
// Bluetooth
"Battery to show" = "Laadstatus weergeven"; // translategemma:4b
"No Bluetooth devices are available" = "Er zijn geen Bluetooth-apparaten beschikbaar."; // translategemma:4b
// Clock
"Time zone" = "Tijdszone"; // translategemma:4b
"Local" = "Lokaal"; // translategemma:4b
"Calendar" = "Agenda"; // translategemma:4b
"Show week numbers" = "Toon de weeknummers"; // translategemma:4b
"Local time" = "Lokaan tijd"; // translategemma:4b
"Add new clock" = "Voeg een nieuwe klok toe"; // translategemma:4b
"Delete selected clock" = "Verwijder de geselecteerde klok"; // translategemma:4b
"Help with datetime format" = "Hulp bij het formatteren van datums en tijden"; // translategemma:4b
// Colors
"Based on utilization" = "Gebaseerd op gebruik";
"Based on pressure" = "Gebaseerd op druk";
"Based on cluster" = "Op basis van een cluster"; // translategemma:4b
"System accent" = "Systeemaccent"; // translategemma:4b
"Monochrome accent" = "Monochroom accent"; // translategemma:4b
"Clear" = "Helder";
"White" = "Wit";
"Black" = "Zwart";
"Gray" = "Grijs";
"Second gray" = "Tweede grijs";
"Dark gray" = "Donkergrijs";
"Light gray" = "Lichtgrijs";
"Red" = "Rood";
"Second red" = "Tweede rood";
"Green" = "Groen";
"Second green" = "Tweede groen";
"Blue" = "Blauw";
"Second blue" = "Tweede blauw";
"Yellow" = "Geel";
"Second yellow" = "Tweede geel";
"Orange" = "Oranje";
"Second orange" = "Tweede oranje";
"Purple" = "Paars";
"Second purple" = "Tweede paars";
"Brown" = "Bruin";
"Second brown" = "Tweede bruin";
"Cyan" = "Lichtblauw2";
"Magenta" = "Paars";
"Pink" = "Roze";
"Teal" = "Teal";
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/pl.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "Procesor";
"Open CPU settings" = "Otwórz ustawienia procesora";
"GPU" = "Grafika";
"Open GPU settings" = "Otwórz ustawienia grafiki";
"RAM" = "Pamięć";
"Open RAM settings" = "Otwórz ustawienia pamięci";
"Disk" = "Dysk";
"Open Disk settings" = "Otwórz ustawienia dysku";
"Sensors" = "Czujniki";
"Open Sensors settings" = "Otwórz ustawienia czujników";
"Network" = "Sieć";
"Open Network settings" = "Otwórz ustawienia sieci";
"Battery" = "Bateria";
"Open Battery settings" = "Otwórz ustawienia baterii";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Otwórz ustawienia bluetooth";
"Clock" = "Zegar";
"Open Clock settings" = "Otwórz ustawienia zegara";
// Words
"Unknown" = "Nieznany";
"Version" = "Wersja";
"Processor" = "Procesor";
"Memory" = "Pamięć";
"Graphics" = "Grafika";
"Close" = "Zamknij";
"Download" = "Pobierz";
"Install" = "Zainstaluj";
"Cancel" = "Anuluj";
"Unavailable" = "Niedostępny";
"Yes" = "Tak";
"No" = "Nie";
"Automatic" = "Automatyczny";
"Manual" = "Ręczny";
"None" = "Brak";
"Dots" = "Punkty";
"Arrows" = "Strzałki";
"Characters" = "Litery";
"Short" = "Krótki";
"Long" = "Długi";
"Statistics" = "Statystyki";
"Max" = "Max";
"Min" = "Min";
"Reset" = "Zresetować";
"Alignment" = "Wyrównanie";
"Left alignment" = "Do lewej";
"Center alignment" = "Centrum";
"Right alignment" = "Do prawej";
"Dashboard" = "Panel kontrolny"; // translategemma:4b
"Enabled" = "Włączone";
"Disabled" = "Wyłączone";
"Silent" = "Tryb cichy";
"Units" = "Jednostki";
"Fans" = "Wentylatory";
"Scaling" = "Skalowanie";
"Linear" = "Liniowe";
"Square" = "Kwadratowe";
"Cube" = "Kubiczne";
"Logarithmic" = "Logarytmiczne";
"Fixed scale" = "Stałe";
"Cores" = "Rdzeni";
"Settings" = "Ustawienia";
"Name" = "Nazwa";
"Format" = "Format";
"Turn off" = "Wyłączyć";
"Normal" = "Normalny";
"Warning" = "Ostrzeżenie";
"Critical" = "Krytyczny";
"Usage" = "Wykorzystanie";
"2 minutes" = "2 minuty";
"3 minutes" = "3 minuty";
"10 minutes" = "10 minut";
"Import" = "Import";
"Export" = "Eksport";
"Separator" = "Separator";
"Read" = "Odczyt";
"Write" = "Zapis";
"Frequency" = "Częstotliwość";
"Save" = "Zapisać";
"Run" = "Uruchomić";
"Stop" = "Zatrzymać";
"Uninstall" = "Odinstalować";
"1 sec" = "1 sek";
"2 sec" = "2 sek";
"3 sec" = "3 sek";
"5 sec" = "5 sek";
"10 sec" = "10 sek";
"15 sec" = "15 sek";
"30 sec" = "30 sek";
"60 sec" = "60 sek";
// Setup
"Stats Setup" = "Konfiguracja Stats";
"Previous" = "Poprzednia";
"Previous page" = "Poprzednia strona";
"Next" = "Następna";
"Next page" = "Następna strona";
"Finish" = "Zakończ";
"Finish setup" = "Zakończ konfigurację";
"Welcome to Stats" = "Witamy w Stats";
"welcome_message" = "Dziękujemy za korzystanie ze Stats,\n bezpłatnego monitora systemu macOS o otwartym kodzie źródłowym.";
"Start the application automatically when starting your Mac" = "Uruchomiaj aplikację automatycznie podczas uruchamiania komputera";
"Do not start the application automatically when starting your Mac" = "Nie uruchamiaj aplikacji automatycznie podczas uruchamiania komputera";
"Do everything silently in the background (recommended)" = "Rób wszystko w tle (zalecane)";
"Check for a new version on startup" = "Sprawdzaj nową wersję podczas uruchamiania";
"Check for a new version every day (once a day)" = "Sprawdzaj codziennie nową wersję (raz dziennie)";
"Check for a new version every week (once a week)" = "Sprawdzaj nową wersję co tydzień (raz w tygodniu)";
"Check for a new version every month (once a month)" = "Sprawdzaj nową wersję co miesiąc (raz w miesiącu)";
"Never check for updates (not recommended)" = "Nigdy nie sprawdzaj czy są aktualizacje (niezalecane)";
"Anonymous telemetry for better development decisions" = "Anonimowe dane telemetryczne aby podejmować lepsze decyzje rozwojowe";
"Share anonymous telemetry data" = "Udostępniaj anonimowe dane telemetryczne";
"Do not share anonymous telemetry data" = "Nie udostępniaj anonimowych danych telemetrycznych";
"The configuration is completed" = "Konfiguracja zakończona";
"finish_setup_message" = "Wszystko gotowe! \n Stats to narzędzie open source, które jest bezpłatne i zawsze takie będzie. \n Jeśli Ci się spodoba, możesz wesprzeć projekt, to jest zawsze mile widziane!";
// Alerts
"New version available" = "Dostępna nowa wersja";
"Click to install the new version of Stats" = "Kliknij, aby zainstalować nową wersję";
"Successfully updated" = "Zaktualizowano pomyślnie";
"Stats was updated to v" = "Stats został zaktualizowany do v%0";
"Reset settings text" = "Wszystkie ustawienia aplikacji zostaną zresetowane, a aplikacja zostanie uruchomiona ponownie. Czy na pewno chcesz to zrobić?";
"Support text" = "Dziękujemy za korzystanie ze Stats! \n\n Utrzymanie i ulepszanie tego open-source projektu wymaga czasu i zasobów. Twoje wsparcie pomaga nam nadal dostarczać bezpłatną i niezawodną aplikację dla każdego.\n\nJeśli uważasz, że Stats jest pomocny, rozważ dokonanie wpłaty. Każda drobnostka pomaga!";
// Settings
"Open Activity Monitor" = "Otwórz Monitor Aktywności";
"Report a bug" = "Zgłoś problem";
"Support the application" = "Wesprzyj rozwój programu";
"Close application" = "Zamknij program";
"Open application settings" = "Otwórz ustawienia";
"Open dashboard" = "Otwórz dashboard";
"No notifications available in this module" = "W tym module nie są dostępne żadne powiadomienia";
"Open Calendar" = "Otwórz kalendarz";
"Toggle the module" = "Przełącz moduł";
// Application settings
"Update application" = "Aktualizuj aplikacje";
"Check for updates" = "Sprawdzaj aktualizacje";
"At start" = "Przy uruchomieniu";
"Once per day" = "Raz dziennie";
"Once per week" = "Raz w tygodniu";
"Once per month" = "Raz w miesiącu";
"Never" = "Nigdy";
"Check for update" = "Sprawdź aktualizacje";
"Show icon in dock" = "Pokazuj ikonę w docku";
"Start at login" = "Uruchom przy logowaniu";
"Build number" = "Numer kompilacji";
"Import settings" = "Importuj ustawienia";
"Export settings" = "Eksportuj ustawienia";
"Reset settings" = "Resetuj ustawienia";
"Pause the Stats" = "Wstrzymaj Stats";
"Resume the Stats" = "Wznów Stats";
"Combined modules" = "Połączone moduły";
"Combined details" = "Połączone okno";
"Spacing" = "Rozstaw";
"Share anonymous telemetry" = "Udostępnij anonimową telemetrię";
"Choose file" = "Wybrać plik";
"Stress tests" = "Testy obciążeniowe";
// Dashboard
"Serial number" = "Numer seryjny";
"Model identifier" = "Identyfikator modelu";
"Production year" = "Rok produkcji";
"Uptime" = "Czas pracy";
"Number of cores" = "%0 rdzeni";
"Number of threads" = "%0 wątków";
"Number of e-cores" = "%0 rdzeni energooszczędnych";
"Number of p-cores" = "%0 rdzeni wydajnościowych";
"Disks" = "Dyski";
"Display" = "Wyświetlacz";
// Update
"The latest version of Stats installed" = "Najnowsza wersja Stats zainstalowana";
"Downloading..." = "Pobieranie...";
"Current version: " = "Aktualna wersja: ";
"Latest version: " = "Najnowsza wersja: ";
// Widgets
"Color" = "Kolor";
"Label" = "Etykieta";
"Box" = "Pudełko"; // translategemma:4b
"Frame" = "Ramka";
"Value" = "Wartość";
"Colorize" = "Kolorowanie";
"Colorize value" = "Kolorowanie wartości";
"Additional information" = "Informacja dodatkowa";
"Reverse values order" = "Zmień kolejność wyświetlania";
"Base" = "Podstawa";
"Display mode" = "Tryb wyświetlania";
"One row" = "Jeden wiersz";
"Two rows" = "Dwa wiersze";
"Mini widget" = "Mini";
"Line chart widget" = "Wykres liniowy";
"Bar chart widget" = "Wykres słupkowy";
"Pie chart widget" = "Wykres kołowy";
"Network chart widget" = "Wykres sieciowy";
"Speed widget" = "Prędkość";
"Battery widget" = "Bateria";
"Stack widget" = "Stos";
"Memory widget" = "Pamięć";
"Static width" = "Statyczna szerokość";
"Tachometer widget" = "Tachometr";
"State widget" = "Stan";
"Text widget" = "Tekst";
"Battery details widget" = "Widżet szczegółów baterii";
"Show symbols" = "Pokaż symbole";
"Label widget" = "Etykieta";
"Number of reads in the chart" = "Liczba odczytów na wykresie";
"Color of download" = "Kolor pobrania";
"Color of upload" = "Kolor wysyłki";
"Monospaced font" = "Czcionka o stałej szerokości";
"Reverse order" = "Odwrotna kolejność";
"Chart history" = "Historia wykresu";
"Default color" = "Domyślny";
"Transparent when no activity" = "Przejrzysty gdy nie ma żadnej aktywności";
"Constant color" = "Stały";
// Module Kit
"Open module settings" = "Otwórz ustawienia modułu";
"Select widget" = "Wybierz %0 widżet";
"Open widget settings" = "Otwórz ustawienia widgetu";
"Update interval" = "Interwał odświeżania";
"Usage history" = "Historia użycia";
"Details" = "Szczegóły";
"Top processes" = "Top procesy";
"Pictogram" = "Ikonka";
"Module" = "Moduł";
"Widgets" = "Widżety";
"Popup" = "Okienko";
"Notifications" = "Powiadomienia";
"Merge widgets" = "Połącz widżety";
"No available widgets to configure" = "Brak dostępnych widżetów do konfiguracji";
"No options to configure for the popup in this module" = "Brak opcji do skonfigurowania dla okienka w tym module";
"Process" = "Proces";
"Kill process" = "Zamknąć proces";
"Keyboard shortcut" = "Skrót klawiszowy";
"Listening..." = "Oczekuje...";
// Modules
"Number of top processes" = "Liczba procesów";
"Update interval for top processes" = "Interwał odświeżania procesów";
"Notification level" = "Poziom powiadomienia";
"Chart color" = "Kolor wykresu";
"Main chart scaling" = "Skalowanie wykresu głównego";
"Scale value" = "Wartość skali";
"Text widget value" = "Wartość widżetu tekstowego";
// CPU
"CPU usage" = "Obciążenie procesora";
"CPU temperature" = "Temperatura procesora";
"CPU frequency" = "Częstotliwość procesora";
"System" = "System";
"User" = "Użytkownik";
"Idle" = "Nieaktywny";
"Show usage per core" = "Pokaż obciążenie na rdzeń";
"Show hyper-threading cores" = "Pokaż rdzenie Hyper-Threading";
"Split the value (System/User)" = "Podzielić wartość (System/Użytkownik)";
"Scheduler limit" = "Ograniczenie planisty";
"Speed limit" = "Ograniczenie prędkości";
"Average load" = "Średnie obciążenie";
"1 minute" = "1 minuta";
"5 minutes" = "5 minut";
"15 minutes" = "15 minut";
"CPU usage threshold" = "Próg wykorzystania procesora";
"CPU usage is" = "Użycie procesora %0";
"Efficiency cores" = "Rdzenie energooszczędne";
"Performance cores" = "Rdzenie wydajnościowe";
"System color" = "Kolor systemu";
"User color" = "Kolor użytkownika";
"Idle color" = "Nieaktywny kolor";
"Cluster grouping" = "Grupowanie klastrów";
"Efficiency cores color" = "Kolor rdzeni energooszczędnych";
"Performance cores color" = "Kolor rdzeni wydajnościowych";
"Total load" = "Całkowite obciążenie";
"System load" = "Obciążenie systemu";
"User load" = "Obciążenie użytkownika";
"Efficiency cores load" = "Obciążenie rdzeni energooszczędnych";
"Performance cores load" = "Obciążenie rdzeni wydajnościowych";
"All cores" = "Wszystkie rdzenie";
// GPU
"GPU to show" = "GPU do wyświetlenia";
"Show GPU type" = "Pokaż typ GPU";
"GPU enabled" = "GPU włączone";
"GPU disabled" = "GPU wyłączone";
"GPU temperature" = "Temperatura GPU";
"GPU utilization" = "Wykorzystanie GPU";
"Vendor" = "Producent";
"Model" = "Model";
"Status" = "Stan"; // translategemma:4b
"Active" = "Aktywny";
"Non active" = "Nieaktywny";
"Fan speed" = "Prędkość wentylatora";
"Core clock" = "Taktowanie rdzenia";
"Memory clock" = "Taktowanie pamięci";
"Utilization" = "Wykorzystanie";
"Render utilization" = "Wykorzystanie rendera";
"Tiler utilization" = "Wykorzystanie tilera";
"GPU usage threshold" = "Próg wykorzystania GPU";
"GPU usage is" = "Użycie GPU %0";
// RAM
"Memory usage" = "Użycie pamięci";
"Memory pressure" = "Obciążenie pamięci";
"Total" = "Całość";
"Used" = "Użyta";
"App" = "Aplikacje";
"Wired" = "Układowa";
"Compressed" = "Skompresowana";
"Free" = "Wolna";
"Swap" = "Pamięć wymiany";
"Split the value (App/Wired/Compressed)" = "Podzielić wartość (Aplikacje/Układowa/Skompresowana)";
"RAM utilization threshold" = "Próg wykorzystania pamięci RAM";
"RAM utilization is" = "Wykorzystanie pamięci RAM %0";
"App color" = "Kolor aplikacji";
"Wired color" = "Kolor układowej przestrzeni";
"Compressed color" = "Kolor skompresowanej przestrzeni";
"Free color" = "Kolor wolnej przestrzeni";
"Free memory (less than)" = "Wolna pamięć (mniej niż)";
"Swap size" = "Rozmiar swap";
"Free RAM is" = "Wolna pamięć RAM %0";
// Disk
"Show removable disks" = "Pokaż dyski wymienne";
"Used disk memory" = "Wykorzystano %0 z %1";
"Free disk memory" = "Dostępne %0 z %1";
"Disk to show" = "Aktywny dysk";
"Open disk" = "Otwórz dysk";
"Switch view" = "Przełącz widok";
"Disk utilization threshold" = "Próg wykorzystania dysku";
"Disk utilization is" = "Wykorzystanie dysku %0";
"Read color" = "Kolor odczytu";
"Write color" = "Kolor zapisu";
"Disk usage" = "Użycie dysku";
"Total read" = "Całkowity odczyt";
"Total written" = "Całkowity zapis";
"Write speed" = "Zapis";
"Read speed" = "Odczyt";
"Drives" = "Dyski";
"SMART data" = "SMART dane";
// Sensors
"Temperature unit" = "Jednostka temperatury";
"Celsius" = "Celsjusz";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Zapisz prędkość wentylatora";
"Fan" = "Wentylator";
"HID sensors" = "Czujniki HID";
"Synchronize fan's control" = "Synchronizuj sterowanie wentylatorami";
"Current" = "Prąd";
"Energy" = "Energia";
"Show unknown sensors" = "Pokaż nieznane czujniki";
"Install fan helper" = "Zainstaluj narzędzie do wentylatora";
"Uninstall fan helper" = "Odinstaluj narzędzie do wentylatora";
"Fan value" = "Wartość wentylatora";
"Turn off fan" = "Wyłączyć wentylator";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Zamierzasz wyłączyć wentylator. To nie jest zalecana czynność, która może spowodować uszkodzenie komputera Mac. Czy na pewno chcesz to zrobić?";
"Sensor threshold" = "Próg czujnika";
"Left fan" = "Lewy";
"Right fan" = "Prawy";
"Fastest fan" = "Najszybszy";
"Sensor to show" = "Aktywny czujnik";
// Network
"Uploading" = "Wysyłanie";
"Downloading" = "Pobieranie";
"Public IP" = "Publiczny IP";
"Local IP" = "Lokalny IP";
"Interface" = "Interfejs";
"Physical address" = "Adres fizyczny";
"Refresh" = "Odśwież";
"Click to copy public IP address" = "Kliknij, aby skopiować publiczny adres IP";
"Click to copy local IP address" = "Kliknij, aby skopiować lokalny adres IP";
"Click to copy wifi name" = "Kliknij, aby skopiować nazwę Wi-Fi";
"Click to copy mac address" = "Kliknij, aby skopiować adres fizyczny";
"No connection" = "Brak połączenia";
"Network interface" = "Interfejs sieciowy";
"Total download" = "Całkowicie pobrano";
"Total upload" = "Całkowicie wysłano";
"Reader type" = "Sposób odczytu";
"Interface based" = "Interfejs";
"Processes based" = "Procesy";
"Reset data usage" = "Resetuj użycie danych";
"VPN mode" = "Tryb VPN";
"Standard" = "Standard";
"Security" = "Szyfrowanie";
"Channel" = "Kanał";
"Common scale" = "Wspólna skala";
"Autodetection" = "Autodetekcja";
"Widget activation threshold" = "Próg aktywacji widżetu";
"Internet connection" = "Połączenie internetowe";
"Active state color" = "Kolor stanu aktywnego";
"Nonactive state color" = "Kolor stanu nieaktywnego";
"Connectivity host (ICMP)" = "Host łączności (ICMP)";
"Leave empty to disable the check" = "Pozostaw puste, aby wyłączyć sprawdzanie";
"Connectivity history" = "Historia łączności";
"Auto-refresh public IP address" = "Automatyczne odświeżanie adresu IP";
"Every hour" = "Co godzinę";
"Every 12 hours" = "Co 12 godzin";
"Every 24 hours" = "Co 24 godzin";
"Network activity" = "Aktywność sieciowa";
"Last reset" = "Ostatni reset %0 temu";
"Latency" = "Opóźnienie";
"Upload speed" = "Wysyłka";
"Download speed" = "Pobranie";
"Address" = "Adres";
"WiFi network" = "Sieć WiFi";
"Local IP changed" = "Lokalny adres IP został zmieniony";
"Public IP changed" = "Publiczny adres IP został zmieniony";
"Previous IP" = "Poprzedni adres IP: %0";
"New IP" = "Nowy adres IP: %0";
"Internet connection lost" = "Utracono połączenie z internetem";
"Internet connection established" = "Połączenie z internetem zostało przywrócone";
// Battery
"Level" = "Poziom naładowania";
"Source" = "Źródło";
"AC Power" = "Zasilacz";
"Battery Power" = "Bateria";
"Time" = "Czas";
"Health" = "Zdrowie";
"Amperage" = "Natężenie";
"Voltage" = "Napięcie";
"Cycles" = "Liczba cykli";
"Temperature" = "Temperatura";
"Power adapter" = "Zasilacz";
"Power" = "Moc";
"Is charging" = "Ładuje";
"Time to discharge" = "Czas do rozładowania";
"Time to charge" = "Czas do naładowania";
"Calculating" = "Obliczanie";
"Fully charged" = "W pełni naładowana";
"Not connected" = "Nie podłączona";
"Low level notification" = "Powiadomienie o niskim poziomie";
"High level notification" = "Powiadomienie o wysokim poziomie";
"Low battery" = "Niski poziom baterii";
"High battery" = "Wysoki poziom baterii";
"Battery remaining" = "Pozostało %0%";
"Battery remaining to full charge" = "%0% do pełnego naładowania";
"Percentage" = "Procenty";
"Percentage and time" = "Procenty i czas";
"Time and percentage" = "Czas i procenty";
"Time format" = "Format czasu";
"Hide additional information when full" = "Ukryj dodatkowe informacje, gdy bateria jest naładowana";
"Last charge" = "Ostatnie ładowanie";
"Capacity" = "Pojemność";
"current / maximum / designed" = "aktualna / maksymalna / zaprojektowana";
"Low power mode" = "Niskie zużycie energii";
"Percentage inside the icon" = "Procent wewnątrz ikony";
"Colorize battery" = "Kolorowanie baterii";
"Charging current" = "Prąd ładowania";
"Charging Voltage" = "Napięcie ładowania";
"Charger state inside the battery" = "Stan ładowania wewnątrz akumulatora";
// Bluetooth
"Battery to show" = "Bateria do wyświetlenia";
"No Bluetooth devices are available" = "Brak dostępnych urządzeń Bluetooth";
// Clock
"Time zone" = "Strefa czasowa";
"Local" = "Lokalna";
"Calendar" = "Kalendarz";
"Show week numbers" = "Pokaż numery tygodni";
"Local time" = "Czas lokalny";
"Add new clock" = "Dodaj nowy zegar";
"Delete selected clock" = "Usuń wybrany zegar";
"Help with datetime format" = "Pomoc z formatem daty i godziny";
// Colors
"Based on utilization" = "Na podstawie wykorzystania";
"Based on pressure" = "Na podstawie obciążenia";
"Based on cluster" = "Na podstawie klastra";
"System accent" = "Akcent systemowy";
"Monochrome accent" = "Akcent monochromatyczny";
"Clear" = "Przezroczysty";
"White" = "Biały";
"Black" = "Czarny";
"Gray" = "Szary";
"Second gray" = "Drugi szary";
"Dark gray" = "Ciemno szary";
"Light gray" = "Jasno szary";
"Red" = "Czerwony";
"Second red" = "Drugi czerwony";
"Green" = "Zielony";
"Second green" = "Drugi zielony";
"Blue" = "Niebieski";
"Second blue" = "Drugi niebieski";
"Yellow" = "Żółty";
"Second yellow" = "Drugi żółty";
"Orange" = "Pomarańczowy";
"Second orange" = "Drugi pomarańczowy";
"Purple" = "Fioletowy";
"Second purple" = "Drugi fioletowy";
"Brown" = "Brązowy";
"Second brown" = "Drugi brązowy";
"Cyan" = "Cyjan";
"Magenta" = "Magenta";
"Pink" = "Różowy";
"Teal" = "Morski";
"Indigo" = "Indygo";
================================================
FILE: Stats/Supporting Files/popups.psd
================================================
[File too large to display: 15.2 MB]
================================================
FILE: Stats/Supporting Files/pt-BR.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Marcelo Chaves on 11/11/2020, updated by Pedro Serigatto (@pedroserigatto) on 03/03/2023, updated by Paulo Albuquerque (@paulora2405) on 23/10/2023, updated by Rodrigo Schneider (@SCHrodrigo) on 13/06/2024
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Marcelo Chaves. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "Abrir configurações da CPU";
"GPU" = "GPU";
"Open GPU settings" = "Abrir configurações da GPU";
"RAM" = "RAM";
"Open RAM settings" = "Abrir configurações da RAM";
"Disk" = "Disco";
"Open Disk settings" = "Abrir configurações do Disco";
"Sensors" = "Sensores";
"Open Sensors settings" = "Abrir configurações dos Sensores";
"Network" = "Rede";
"Open Network settings" = "Abrir configurações da Rede";
"Battery" = "Bateria";
"Open Battery settings" = "Abrir configurações da Bateria";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Abrir configurações do Bluetooth";
"Clock" = "Relógio";
"Open Clock settings" = "Abrir configurações do Relógio";
// Words
"Unknown" = "Desconhecido";
"Version" = "Versão";
"Processor" = "Processador";
"Memory" = "Memória";
"Graphics" = "Gráficos";
"Close" = "Fechar";
"Download" = "Baixar";
"Install" = "Instalar";
"Cancel" = "Cancelar";
"Unavailable" = "Indisponível";
"Yes" = "Sim";
"No" = "Não";
"Automatic" = "Automático";
"Manual" = "Manual";
"None" = "Nenhum";
"Dots" = "Pontos";
"Arrows" = "Setas";
"Characters" = "Letras";
"Short" = "Curto";
"Long" = "Longo";
"Statistics" = "Estatísticas";
"Max" = "Máx";
"Min" = "Mín";
"Reset" = "Resetar";
"Alignment" = "Alinhamento";
"Left alignment" = "Esquerda";
"Center alignment" = "Centro";
"Right alignment" = "Direita";
"Dashboard" = "Painel Principal";
"Enabled" = "Habilitado";
"Disabled" = "Desabilitado";
"Silent" = "Silencioso";
"Units" = "Unidades";
"Fans" = "Ventoinhas";
"Scaling" = "Escala";
"Linear" = "Linear";
"Square" = "Quadrática";
"Cube" = "Cúbica";
"Logarithmic" = "Logarítmica";
"Fixed scale" = "Escala fixa";
"Cores" = "Núcleos";
"Settings" = "Configurações";
"Name" = "Nome";
"Format" = "Formato";
"Turn off" = "Deligar";
"Normal" = "Normal";
"Warning" = "Aviso";
"Critical" = "Crítico";
"Usage" = "Uso";
"2 minutes" = "2 minutos";
"3 minutes" = "3 minutos";
"10 minutes" = "10 minutos";
"Import" = "Importar";
"Export" = "Exportar";
"Separator" = "Separador"; // translategemma:4b
"Read" = "Leia"; // translategemma:4b
"Write" = "Escreva"; // translategemma:4b
"Frequency" = "Frequência"; // translategemma:4b
"Save" = "Salvar"; // translategemma:4b
"Run" = "Executar"; // translategemma:4b
"Stop" = "Pare"; // translategemma:4b
"Uninstall" = "Desinstalar"; // translategemma:4b
"1 sec" = "1 segundo"; // translategemma:4b
"2 sec" = "2 segundos"; // translategemma:4b
"3 sec" = "3 segundos"; // translategemma:4b
"5 sec" = "5 segundos"; // translategemma:4b
"10 sec" = "10 segundos"; // translategemma:4b
"15 sec" = "15 segundos"; // translategemma:4b
"30 sec" = "30 segundos"; // translategemma:4b
"60 sec" = "60 segundos"; // translategemma:4b
// Setup
"Stats Setup" = "Configurar Stats";
"Previous" = "Anterior";
"Previous page" = "Página anterior";
"Next" = "Próxima";
"Next page" = "Próxima página";
"Finish" = "Finalizar";
"Finish setup" = "Finalizar configuração";
"Welcome to Stats" = "Bem-vindo ao Stats";
"welcome_message" = "Obrigado por usar o Stats, um monitor de sistema para macOS, de código aberto e gratuito, para sua barra de menu.";
"Start the application automatically when starting your Mac" = "Iniciar o aplicativo automaticamente ao iniciar seu Mac";
"Do not start the application automatically when starting your Mac" = "Não iniciar o aplicativo automaticamente ao iniciar seu Mac";
"Do everything silently in the background (recommended)" = "Faça tudo silenciosamente em segundo plano (recomendado)";
"Check for a new version on startup" = "Verifique se há uma nova versão ao iniciar";
"Check for a new version every day (once a day)" = "Verifique se há uma nova versão todos os dias (uma vez por dia)";
"Check for a new version every week (once a week)" = "Verifique se há uma nova versão toda semana (uma vez por semana)";
"Check for a new version every month (once a month)" = "Verifique se há uma nova versão todos os meses (uma vez por mês)";
"Never check for updates (not recommended)" = "Nunca verifique se há atualizações (não recomendado)";
"Anonymous telemetry for better development decisions" = "Telemetria anônima para melhores decisões de desenvolvimento";
"Share anonymous telemetry data" = "Compartilhar dados de telemetria anônimos";
"Do not share anonymous telemetry data" = "Não compartilhar dados de telemetria anônimos";
"The configuration is completed" = "A configuração está concluída";
"finish_setup_message" = "Tudo pronto! \n O Stats é uma ferramenta de código aberto, é gratuita e sempre será. \n Se você gostou, você pode apoiar o projeto, sua ajuda sempre será bem vinda!";
// Alerts
"New version available" = "Nova versão disponível";
"Click to install the new version of Stats" = "Clique para instalar a nova versão do Stats";
"Successfully updated" = "Atualizado com sucesso";
"Stats was updated to v" = "Stats foi atualizado para v%0";
"Reset settings text" = "Todas as configurações do aplicativo serão redefinidas e o aplicativo será reiniciado. Tem certeza de que deseja fazer isso?";
"Support text" = "Obrigado por usar o Stats!\n\nA manutenção e o aprimoramento deste projeto de código aberto exigem tempo e recursos. Seu apoio nos ajuda a continuar fornecendo um aplicativo gratuito e confiável para todos.\n\nSe você acha o Stats útil, considere fazer uma contribuição. Cada pedacinho ajuda!";
// Settings
"Open Activity Monitor" = "Abrir Monitor de Atividades";
"Report a bug" = "Reportar um erro";
"Support the application" = "Apoie o aplicativo";
"Close application" = "Fechar aplicativo";
"Open application settings" = "Abrir configurações do aplicativo";
"Open dashboard" = "Abrir painel principal";
"No notifications available in this module" = "Nenhuma notificação disponível neste módulo";
"Open Calendar" = "Abrir Calendário"; // translategemma:4b
"Toggle the module" = "Ativar/desativar o módulo"; // translategemma:4b
// Application settings
"Update application" = "Atualizar aplicativo";
"Check for updates" = "Verificar se há atualizações";
"At start" = "Ao iniciar";
"Once per day" = "Uma vez por dia";
"Once per week" = "Uma vez por semana";
"Once per month" = "Uma vez por mês";
"Never" = "Nunca";
"Check for update" = "Verificar atualizações";
"Show icon in dock" = "Mostrar ícone no dock";
"Start at login" = "Iniciar no login";
"Build number" = "Número da versão (Build)";
"Import settings" = "Importar configurações";
"Export settings" = "Exportar configurações";
"Reset settings" = "Redefinir configurações";
"Pause the Stats" = "Pausar o Stats";
"Resume the Stats" = "Retomar o Stats";
"Combined modules" = "Combinar módulos";
"Combined details" = "Combinar detalhes";
"Spacing" = "Espaçamento";
"Share anonymous telemetry" = "Compartilhar telemetria anônima";
"Choose file" = "Selecione o arquivo"; // translategemma:4b
"Stress tests" = "Testes de estresse"; // translategemma:4b
// Dashboard
"Serial number" = "Número de série";
"Model identifier" = "Identificador do modelo";
"Production year" = "Ano de produção";
"Uptime" = "Tempo de atividade";
"Number of cores" = "%0 núcleos";
"Number of threads" = "%0 threads";
"Number of e-cores" = "%0 núcleos de eficiência";
"Number of p-cores" = "%0 núcleos de desempenho";
"Disks" = "Discos"; // translategemma:4b
"Display" = "Exibição"; // translategemma:4b
// Update
"The latest version of Stats installed" = "A versão mais recente do Stats está instalada";
"Downloading..." = "Baixando...";
"Current version: " = "Versão atual: ";
"Latest version: " = "Última versão: ";
// Widgets
"Color" = "Cor";
"Label" = "Rótulo";
"Box" = "Caixa";
"Frame" = "Quadro";
"Value" = "Valor";
"Colorize" = "Colorir";
"Colorize value" = "Valor colorido";
"Additional information" = "Informação adicional";
"Reverse values order" = "Ordem inversa dos valores";
"Base" = "Base";
"Display mode" = "Modo de exibição";
"One row" = "Uma linha";
"Two rows" = "Duas linhas";
"Mini widget" = "Mini";
"Line chart widget" = "Gráfico de linhas";
"Bar chart widget" = "Gráfico de barras";
"Pie chart widget" = "Gráfico de pizza";
"Network chart widget" = "Gráfico de rede";
"Speed widget" = "Velocidade";
"Battery widget" = "Bateria";
"Stack widget" = "Pilha";
"Memory widget" = "Memoria";
"Static width" = "Largura fixa";
"Tachometer widget" = "Tacômetro";
"State widget" = "Estado";
"Text widget" = "Elemento de texto"; // translategemma:4b
"Battery details widget" = "Widget de detalhes da bateria"; // translategemma:4b
"Show symbols" = "Mostrar símbolos";
"Label widget" = "Rótulo";
"Number of reads in the chart" = "Número de leituras no gráfico";
"Color of download" = "Cor para download";
"Color of upload" = "Cor para upload";
"Monospaced font" = "Fonte Monoespaçada";
"Reverse order" = "Ordem Reversa";
"Chart history" = "Período do gráfico";
"Default color" = "Padrão"; // translategemma:4b
"Transparent when no activity" = "Transparente quando não há atividade"; // translategemma:4b
"Constant color" = "Constante"; // translategemma:4b
// Module Kit
"Open module settings" = "Abrir configurações do módulo";
"Select widget" = "Selecionar widget %0";
"Open widget settings" = "Abrir configurações do widget";
"Update interval" = "Intervalo de atualização";
"Usage history" = "Histórico de uso";
"Details" = "Detalhes";
"Top processes" = "Principais processos";
"Pictogram" = "Pictograma";
"Module" = "Módulo";
"Widgets" = "Widgets";
"Popup" = "Janela pop-up"; // translategemma:4b
"Notifications" = "Notificações";
"Merge widgets" = "Agrupar widgets";
"No available widgets to configure" = "Nenhum widget disponível para configurar";
"No options to configure for the popup in this module" = "Nenhuma opção para configurar o popup neste módulo";
"Process" = "Processo";
"Kill process" = "Encerrar processo";
"Keyboard shortcut" = "Atalho de teclado"; // translategemma:4b
"Listening..." = "Aguardando..."; // translategemma:4b
// Modules
"Number of top processes" = "Número de processos principais";
"Update interval for top processes" = "Intervalo de atualização para processos principais";
"Notification level" = "Nível de notificação";
"Chart color" = "Cor do gráfico";
"Main chart scaling" = "Escala do gráfico principal";
"Scale value" = "Valor da escala";
"Text widget value" = "Valor do widget de texto"; // translategemma:4b
// CPU
"CPU usage" = "Uso da CPU";
"CPU temperature" = "Temperatura da CPU";
"CPU frequency" = "Frequência da CPU";
"System" = "Sistema";
"User" = "Usuário";
"Idle" = "Ocioso";
"Show usage per core" = "Mostrar uso por núcleo";
"Show hyper-threading cores" = "Mostrar núcleos hyper-threading";
"Split the value (System/User)" = "Dividir o valor (Sistema/Usuário)";
"Scheduler limit" = "Limite do agendador";
"Speed limit" = "Limite de velocidade";
"Average load" = "Carga média";
"1 minute" = "1 minuto";
"5 minutes" = "5 minutos";
"15 minutes" = "15 minutos";
"CPU usage threshold" = "Limite de uso da CPU";
"CPU usage is" = "Uso da CPU é %0";
"Efficiency cores" = "Núcleos de eficiência"; // translategemma:4b
"Performance cores" = "Núcleos de desempenho"; // translategemma:4b
"System color" = "Cor do sistema";
"User color" = "Cor do usuário";
"Idle color" = "Cor pausado";
"Cluster grouping" = "Agrupamento";
"Efficiency cores color" = "Cor para Núcleos de Eficiência";
"Performance cores color" = "Cor para Núcleos de Desempenho";
"Total load" = "Carga total";
"System load" = "Carga do sistema";
"User load" = "Carga do usuário";
"Efficiency cores load" = "Carga dos núcleos de eficiência";
"Performance cores load" = "Carga dos núcleos de desempenho";
"All cores" = "Todas as cores"; // translategemma:4b
// GPU
"GPU to show" = "GPU para mostrar";
"Show GPU type" = "Mostrar tipo de GPU";
"GPU enabled" = "GPU ativada";
"GPU disabled" = "GPU desativada";
"GPU temperature" = "Temperatura da GPU";
"GPU utilization" = "Utilização da GPU";
"Vendor" = "Fabricante";
"Model" = "Modelo";
"Status" = "Status";
"Active" = "Ativo";
"Non active" = "Inativo";
"Fan speed" = "Velocidade da ventoinha";
"Core clock" = "Clock do núcleo";
"Memory clock" = "Clock da memória";
"Utilization" = "Utilização";
"Render utilization" = "Utilização de renderização";
"Tiler utilization" = "Utilização de tiler";
"GPU usage threshold" = "Limite de uso da GPU";
"GPU usage is" = "Uso da GPU é de %0";
// RAM
"Memory usage" = "Memória usada";
"Memory pressure" = "Pressão de memória";
"Total" = "Total";
"Used" = "Usada";
"App" = "Aplicativo"; // translategemma:4b
"Wired" = "Residente";
"Compressed" = "Comprimido";
"Free" = "Livre";
"Swap" = "Troca";
"Split the value (App/Wired/Compressed)" = "Divida o valor (App/Residente/Comprimido)";
"RAM utilization threshold" = "Limite de utilização de RAM";
"RAM utilization is" = "Utilização de RAM de %0";
"App color" = "Cor do app";
"Wired color" = "Cor para residente";
"Compressed color" = "Cor para comprimido";
"Free color" = "Cor livre";
"Free memory (less than)" = "Memória livre (menor que)";
"Swap size" = "Troca usada";
"Free RAM is" = "RAM livre é %0";
// Disk
"Show removable disks" = "Mostrar discos removíveis";
"Used disk memory" = "Usado %0 de %1";
"Free disk memory" = "Livre %0 de %1";
"Disk to show" = "Disco para mostrar";
"Open disk" = "Disco aberto";
"Switch view" = "Mudar de vista";
"Disk utilization threshold" = "Limite de utilização de disco";
"Disk utilization is" = "Utilização de disco é de %0";
"Read color" = "Cor para Leitura";
"Write color" = "Cor para Escrita";
"Disk usage" = "Uso do Disco";
"Total read" = "Total lido";
"Total written" = "Total escrito";
"Write speed" = "Escrever"; // translategemma:4b
"Read speed" = "Leia"; // translategemma:4b
"Drives" = "Unidades"; // translategemma:4b
"SMART data" = "Dados SMART"; // translategemma:4b
// Sensors
"Temperature unit" = "Unidade de temperatura";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Salvar a velocidade do ventoinha";
"Fan" = "Ventoinha";
"HID sensors" = "Sensores HID";
"Synchronize fan's control" = "Sincronizar o controle do ventoinha";
"Current" = "Atual";
"Energy" = "Energia";
"Show unknown sensors" = "Mostrar sensores desconhecidos";
"Install fan helper" = "Instalar software auxiliar para ventoinha";
"Uninstall fan helper" = "Desinstalar software auxiliar para ventoinha";
"Fan value" = "Valor da ventoinha";
"Turn off fan" = "Desligar ventoninha";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Você irá desligar a ventoinha. Esta não é uma ação recomendada, pois pode danificar o seu mac, tem certeza que deseja fazer isto?";
"Sensor threshold" = "Limite do sensor"; // translategemma:4b
"Left fan" = "Esquerda";
"Right fan" = "Direita";
"Fastest fan" = "Ventoinha mais rápida";
"Sensor to show" = "Sensor para exibir"; // translategemma:4b
// Network
"Uploading" = "Enviando";
"Downloading" = "Baixando";
"Public IP" = "IP Público";
"Local IP" = "IP Local";
"Interface" = "Interface";
"Physical address" = "Endereço físico";
"Refresh" = "Atualizar";
"Click to copy public IP address" = "Clique para copiar o endereço de IP público";
"Click to copy local IP address" = "Clique para copiar o endereço de IP local";
"Click to copy wifi name" = "Clique para copiar o nome do wi-fi";
"Click to copy mac address" = "Clique para copiar o endereço mac";
"No connection" = "Sem conexão";
"Network interface" = "Interface de rede";
"Total download" = "Download total";
"Total upload" = "Upload total";
"Reader type" = "Tipo de leitor";
"Interface based" = "Interface";
"Processes based" = "Processo";
"Reset data usage" = "Resetar uso de dados";
"VPN mode" = "Modo VPN";
"Standard" = "Padrão";
"Security" = "Segurança";
"Channel" = "Canal";
"Common scale" = "Escala comum";
"Autodetection" = "Autodetecção";
"Widget activation threshold" = "Limite de ativação do widget";
"Internet connection" = "Conexão com a internet";
"Active state color" = "Cor do estado ativo";
"Nonactive state color" = "Cor do estado inativo";
"Connectivity host (ICMP)" = "Host de conectividade (ICMP)";
"Leave empty to disable the check" = "Deixe em branco para desativar a verificação";
"Connectivity history" = "Histórico de conectividade"; // translategemma:4b
"Auto-refresh public IP address" = "Atualizar endereço IP público automaticamente";
"Every hour" = "Cada 1 hora";
"Every 12 hours" = "Cada 12 horas";
"Every 24 hours" = "Cada 24 horas";
"Network activity" = "Atividade de Rede";
"Last reset" = "Último reset foi %0 atrás";
"Latency" = "Latência"; // translategemma:4b
"Upload speed" = "Enviar"; // translategemma:4b
"Download speed" = "Baixar"; // translategemma:4b
"Address" = "Endereço"; // translategemma:4b
"WiFi network" = "Rede Wi-Fi"; // translategemma:4b
"Local IP changed" = "O endereço IP local foi alterado"; // translategemma:4b
"Public IP changed" = "O endereço IP público foi alterado"; // translategemma:4b
"Previous IP" = "Endereço IP anterior: %0"; // translategemma:4b
"New IP" = "Novo endereço IP: %0"; // translategemma:4b
"Internet connection lost" = "Conexão com a internet perdida"; // translategemma:4b
"Internet connection established" = "Conexão com a internet estabelecida"; // translategemma:4b
// Battery
"Level" = "Nível";
"Source" = "Fonte";
"AC Power" = "Energia AC";
"Battery Power" = "Energia da bateria";
"Time" = "Tempo";
"Health" = "Saúde";
"Amperage" = "Corrente";
"Voltage" = "Tensão";
"Cycles" = "Número de ciclos";
"Temperature" = "Temperatura";
"Power adapter" = "Adaptador de energia";
"Power" = "Energia";
"Is charging" = "Está carregando";
"Time to discharge" = "Tempo para descarregar";
"Time to charge" = "Tempo para carregar";
"Calculating" = "Calculando";
"Fully charged" = "Completamente carregado";
"Not connected" = "Não conectado";
"Low level notification" = "Notificação de baixo nível";
"High level notification" = "Notificação de alto nível";
"Low battery" = "Bateria fraca";
"High battery" = "Bateria cheia";
"Battery remaining" = "%0% restante";
"Battery remaining to full charge" = "%0% até carga completa";
"Percentage" = "Porcentagem";
"Percentage and time" = "Porcentagem e tempo";
"Time and percentage" = "Tempo e porcentagem";
"Time format" = "Formato de hora";
"Hide additional information when full" = "Ocultar informações adicionais quando estiver cheia";
"Last charge" = "Última carga";
"Capacity" = "Capacidade";
"current / maximum / designed" = "atual / máximo / projetado";
"Low power mode" = "Modo de baixa energia";
"Percentage inside the icon" = "Porcentagem dentro do ícone";
"Colorize battery" = "Colorir bateria";
"Charging current" = "Corrente de carregamento";
"Charging Voltage" = "Tensão de carregamento";
"Charger state inside the battery" = "Estado do carregador dentro da bateria"; // translategemma:4b
// Bluetooth
"Battery to show" = "Bateria para mostrar";
"No Bluetooth devices are available" = "Nenhum dispositivo Bluetooth disponível";
// Clock
"Time zone" = "Fuso horário";
"Local" = "Local";
"Calendar" = "Calendário"; // translategemma:4b
"Show week numbers" = "Exibir os números da semana"; // translategemma:4b
"Local time" = "Horário local"; // translategemma:4b
"Add new clock" = "Adicionar um novo relógio"; // translategemma:4b
"Delete selected clock" = "Excluir o relógio selecionado"; // translategemma:4b
"Help with datetime format" = "Ajuda com o formato de data e hora"; // translategemma:4b
// Colors
"Based on utilization" = "Baseado na utilização";
"Based on pressure" = "Baseado na pressão";
"Based on cluster" = "Baseado no agrupamento";
"System accent" = "Cor de ênfase do sistema";
"Monochrome accent" = "Cor de ênfase monocromática";
"Clear" = "Limpo";
"White" = "Branco";
"Black" = "Preto";
"Gray" = "Cinza";
"Second gray" = "Segundo cinza";
"Dark gray" = "Cinza escuro";
"Light gray" = "Cinza claro";
"Red" = "Vermelho";
"Second red" = "Segundo vermelho";
"Green" = "Verde";
"Second green" = "Segundo verde";
"Blue" = "Azul";
"Second blue" = "Segundo azul";
"Yellow" = "Amarelo";
"Second yellow" = "Segundo amarelo";
"Orange" = "Laranja";
"Second orange" = "Segundo laranja";
"Purple" = "Roxo";
"Second purple" = "Segundo roxo";
"Brown" = "Marrom";
"Second brown" = "Segundo marrom";
"Cyan" = "Ciano";
"Magenta" = "Magenta";
"Pink" = "Rosa";
"Teal" = "Azul-Verde";
"Indigo" = "Índigo";
================================================
FILE: Stats/Supporting Files/pt-PT.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Eduardo Santos on 26/02/2023.
// Using Swift 5.0.
// Running on macOS 11.1.
//
// Copyright © 2020 Rafael Fernandes. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "Abrir definições da CPU";
"GPU" = "GPU";
"Open GPU settings" = "Abrir definições da GPU";
"RAM" = "RAM";
"Open RAM settings" = "Abrir definições da RAM";
"Disk" = "Disco"; // translategemma:4b
"Open Disk settings" = "Abrir definições do disco";
"Sensors" = "Sensores"; // translategemma:4b
"Open Sensors settings" = "Abrir definições dos sensores";
"Network" = "Rede"; // translategemma:4b
"Open Network settings" = "Abrir definições da rede";
"Battery" = "Bateria"; // translategemma:4b
"Open Battery settings" = "Abrir definições da bateria";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Abrir definições do bluetooth";
"Clock" = "Relógio"; // translategemma:4b
"Open Clock settings" = "Abrir configurações do relógio"; // translategemma:4b
// Words
"Unknown" = "Desconhecido";
"Version" = "Versão";
"Processor" = "Processador";
"Memory" = "Memória";
"Graphics" = "Gráficos";
"Close" = "Fechar";
"Download" = "Fazer download";
"Install" = "Instalar";
"Cancel" = "Cancelar";
"Unavailable" = "Indisponível";
"Yes" = "Sim";
"No" = "Não";
"Automatic" = "Automático";
"Manual" = "Manual";
"None" = "Nenhum";
"Dots" = "Pontos";
"Arrows" = "Setas";
"Characters" = "Letras";
"Short" = "Curto";
"Long" = "Longo";
"Statistics" = "Estatísticas";
"Max" = "Máximo"; // translategemma:4b
"Min" = "Meu"; // translategemma:4b
"Reset" = "Restabelecer";
"Alignment" = "Alinhamento";
"Left alignment" = "Alinhamento à esq.";
"Center alignment" = "Alinhamento ao centro";
"Right alignment" = "Alinhamento à dir.";
"Dashboard" = "Painel";
"Enabled" = "Ativado";
"Disabled" = "Desativado";
"Silent" = "Silencioso";
"Units" = "Unidades";
"Fans" = "Ventoinhas";
"Scaling" = "Dimensionamento";
"Linear" = "Linear";
"Square" = "Quadrado";
"Cube" = "Cubo";
"Logarithmic" = "Logarítmico";
"Fixed scale" = "Corrigido"; // translategemma:4b
"Cores" = "Cores";
"Settings" = "Definições";
"Name" = "Nome"; // translategemma:4b
"Format" = "Formato"; // translategemma:4b
"Turn off" = "Desligar"; // translategemma:4b
"Normal" = "Normal";
"Warning" = "Aviso"; // translategemma:4b
"Critical" = "Crucial"; // translategemma:4b
"Usage" = "Utilização"; // translategemma:4b
"2 minutes" = "2 minutos"; // translategemma:4b
"3 minutes" = "3 minutos"; // translategemma:4b
"10 minutes" = "10 minutos"; // translategemma:4b
"Import" = "Importação"; // translategemma:4b
"Export" = "Exportar"; // translategemma:4b
"Separator" = "Separador"; // translategemma:4b
"Read" = "Leia"; // translategemma:4b
"Write" = "Escreva"; // translategemma:4b
"Frequency" = "Frequência"; // translategemma:4b
"Save" = "Salvar"; // translategemma:4b
"Run" = "Executar"; // translategemma:4b
"Stop" = "Pare"; // translategemma:4b
"Uninstall" = "Desinstalar"; // translategemma:4b
"1 sec" = "1 segundo"; // translategemma:4b
"2 sec" = "2 segundos"; // translategemma:4b
"3 sec" = "3 segundos"; // translategemma:4b
"5 sec" = "5 segundos"; // translategemma:4b
"10 sec" = "10 segundos"; // translategemma:4b
"15 sec" = "15 segundos"; // translategemma:4b
"30 sec" = "30 segundos"; // translategemma:4b
"60 sec" = "60 segundos"; // translategemma:4b
// Setup
"Stats Setup" = "Configurações do Stats";
"Previous" = "Anterior";
"Previous page" = "Página anterior";
"Next" = "Próximo";
"Next page" = "Próxima página";
"Finish" = "Terminar";
"Finish setup" = "Terminar a configuração";
"Welcome to Stats" = "Bem-vindo ao Stats";
"welcome_message" = "Obrigado por usar o Stats, um monitor de sistema macOS open source grátis para a tua barra de menus.";
"Start the application automatically when starting your Mac" = "Iniciar a aplicação automaticamente ao iniciar o teu Mac";
"Do not start the application automatically when starting your Mac" = "Não iniciar a aplicação automaticamente ao iniciar o teu Mac";
"Do everything silently in the background (recommended)" = "Fazer tudo silenciosamente em segundo plano (recomendado)";
"Check for a new version on startup" = "Verficar se há novas atualizações ao iniciar";
"Check for a new version every day (once a day)" = "Verficar se há novas atualizações uma vez por dia (uma vez por dia)";
"Check for a new version every week (once a week)" = "Verficar se há novas atualizações uma vez por semana (uma vez por semana)";
"Check for a new version every month (once a month)" = "Verficar se há novas atualizações uma vez por mês (uma vez por mês)";
"Never check for updates (not recommended)" = "Nunca verificar se há novas atualizações (não recomendado)";
"Anonymous telemetry for better development decisions" = "Telemetria anónima para melhores decisões de desenvolvimento"; // translategemma:4b
"Share anonymous telemetry data" = "Partilhar dados de telemetria anónimos"; // translategemma:4b
"Do not share anonymous telemetry data" = "Não partilhe dados de telemetria anónimos"; // translategemma:4b
"The configuration is completed" = "A configuração está completa";
"finish_setup_message" = "Está tudo configurado! \n O Stats é uma ferramenta de código aberto, é gratuito e sempre será. \n Se gostas, podes apoiar o projeto, és sempre bem-vindo!";
// Alerts
"New version available" = "Nova versão disponível";
"Click to install the new version of Stats" = "Clique para instalar a nova versão do Stats";
"Successfully updated" = "Atualização feita com sucesso";
"Stats was updated to v" = "Stats foi atualizado para v%0";
"Reset settings text" = "Todas as configurações da aplicação serão restabelecidas e a aplicação será reiniciada. Tens a certeza que queres fazer isso?";
"Support text" = "Obrigado por utilizar o Stats!\n\nA manutenção e melhoria deste projeto de código aberto requer tempo e recursos. O seu apoio ajuda-nos a continuar a fornecer uma aplicação gratuita e fiável para todos.\n\nSe considera o Stats útil, por favor considere fazer uma contribuição. Cada pedacinho ajuda!";
// Settings
"Open Activity Monitor" = "Abrir Monitor de Atividades";
"Report a bug" = "Reportar um erro";
"Support the application" = "Apoia a aplicação";
"Close application" = "Fechar aplicação";
"Open application settings" = "Abrir definições da aplicação";
"Open dashboard" = "Abrir o painel";
"No notifications available in this module" = "Não existem notificações disponíveis neste módulo."; // translategemma:4b
"Open Calendar" = "Abrir Calendário"; // translategemma:4b
"Toggle the module" = "Ativar/Desativar o módulo"; // translategemma:4b
// Application settings
"Update application" = "Atualizar a aplicação";
"Check for updates" = "Procurar por atualizações";
"At start" = "Ao iniciar";
"Once per day" = "Uma vez por dia";
"Once per week" = "Uma vez por semana";
"Once per month" = "Uma vez por mês";
"Never" = "Nunca";
"Check for update" = "Procurar atualização";
"Show icon in dock" = "Mostrar ícone na Dock";
"Start at login" = "Iniciar durante login";
"Build number" = "Número do Build";
"Import settings" = "Configurações de importação"; // translategemma:4b
"Export settings" = "Configurações de exportação"; // translategemma:4b
"Reset settings" = "Restabelecer as definições";
"Pause the Stats" = "Pausar o Stats";
"Resume the Stats" = "Resumir o Stats";
"Combined modules" = "Módulos combinados";
"Combined details" = "Detalhes combinados"; // translategemma:4b
"Spacing" = "Espaçamento";
"Share anonymous telemetry" = "Partilhar dados de telemetria anónimos"; // translategemma:4b
"Choose file" = "Escolher ficheiro"; // translategemma:4b
"Stress tests" = "Testes de stress"; // translategemma:4b
// Dashboard
"Serial number" = "Número de série";
"Model identifier" = "Identificador do modelo"; // translategemma:4b
"Production year" = "Ano de produção"; // translategemma:4b
"Uptime" = "Tempo de atividade";
"Number of cores" = "%0 cores";
"Number of threads" = "%0 threads";
"Number of e-cores" = "%0 eficiência núcleos"; // translategemma:4b
"Number of p-cores" = "%0 núcleos de desempenho"; // translategemma:4b
"Disks" = "Discos"; // translategemma:4b
"Display" = "Exibição"; // translategemma:4b
// Update
"The latest version of Stats installed" = "A versão mais recente do Stats instalada";
"Downloading..." = "Descarregando...";
"Current version: " = "Versão atual: ";
"Latest version: " = "Última versão: ";
// Widgets
"Color" = "Cor";
"Label" = "Rótulo";
"Box" = "Caixa";
"Frame" = "Quadro";
"Value" = "Valor";
"Colorize" = "Colorir";
"Colorize value" = "Valor da colorização";
"Additional information" = "Informação adicional";
"Reverse values order" = "Ordem inversa dos valores";
"Base" = "Base";
"Display mode" = "Modo de exibição";
"One row" = "Uma linha";
"Two rows" = "Duas linhas";
"Mini widget" = "Mini widget";
"Line chart widget" = "Widget de gráfico linear";
"Bar chart widget" = "Widget de gráfico de barras";
"Pie chart widget" = "Widget de gráfico de setores";
"Network chart widget" = "Widget de gráfico de rede";
"Speed widget" = "Widget de velocidade";
"Battery widget" = "Widget de bateria";
"Stack widget" = "Pilha"; // translategemma:4b
"Memory widget" = "Widget de memória";
"Static width" = "Largura estática";
"Tachometer widget" = "Widget de tacómetro";
"State widget" = "Widget de estados";
"Text widget" = "Widget de texto"; // translategemma:4b
"Battery details widget" = "Widget de detalhes da bateria"; // translategemma:4b
"Show symbols" = "Mostrar simbolos";
"Label widget" = "Widget de etiquetas";
"Number of reads in the chart" = "Número de leituras no gráfico";
"Color of download" = "Cor do download";
"Color of upload" = "Cor do upload";
"Monospaced font" = "Fonte monoespaçada"; // translategemma:4b
"Reverse order" = "Ordem inversa"; // translategemma:4b
"Chart history" = "Histórico do gráfico"; // translategemma:4b
"Default color" = "Padrão"; // translategemma:4b
"Transparent when no activity" = "Transparente quando não há atividade"; // translategemma:4b
"Constant color" = "Constante"; // translategemma:4b
// Module Kit
"Open module settings" = "Abrir definições do módulo";
"Select widget" = "Selecionar widget %0";
"Open widget settings" = "Abrir definições do widget";
"Update interval" = "Intervalo de atualização";
"Usage history" = "Historial de utilização";
"Details" = "Detalhes";
"Top processes" = "Processos principais";
"Pictogram" = "Pictograma";
"Module" = "Módulo";
"Widgets" = "Widgets";
"Popup" = "Aparecer";
"Notifications" = "Notificações";
"Merge widgets" = "Agrupar widgets";
"No available widgets to configure" = "Não foram encontrados widgets para configurar";
"No options to configure for the popup in this module" = "Não foram encontradas opções para configurar para o popup deste módulo";
"Process" = "Processo"; // translategemma:4b
"Kill process" = "Terminar processo"; // translategemma:4b
"Keyboard shortcut" = "Atalho de teclado"; // translategemma:4b
"Listening..." = "A ouvir…"; // translategemma:4b
// Modules
"Number of top processes" = "Número de processos principais";
"Update interval for top processes" = "Intervalo de atualização para os processos principais";
"Notification level" = "Nível de notificação";
"Chart color" = "Cor do gráfico";
"Main chart scaling" = "Escala principal do gráfico"; // translategemma:4b
"Scale value" = "Valor da escala"; // translategemma:4b
"Text widget value" = "Valor do widget de texto"; // translategemma:4b
// CPU
"CPU usage" = "Utilização do CPU";
"CPU temperature" = "Temperatura do CPU";
"CPU frequency" = "Frequência do CPU";
"System" = "Sistema";
"User" = "Utilizador";
"Idle" = "Parado";
"Show usage per core" = "Mostrar uso por núcleo";
"Show hyper-threading cores" = "Mostrar núcleos hyper-threading";
"Split the value (System/User)" = "Dividir o valor (Sistema/Utilizador)";
"Scheduler limit" = "Limite do scheduler";
"Speed limit" = "Limite de velocidade";
"Average load" = "Carga média";
"1 minute" = "1 minuto";
"5 minutes" = "5 minutos";
"15 minutes" = "15 minutos";
"CPU usage threshold" = "Limite de uso da CPU";
"CPU usage is" = "Utilização da CPU é %0";
"Efficiency cores" = "Núcleos de eficiência"; // translategemma:4b
"Performance cores" = "Núcleos de desempenho"; // translategemma:4b
"System color" = "Cor do sistema";
"User color" = "Cor do user";
"Idle color" = "Cor em idle";
"Cluster grouping" = "Agrupamento de cluster";
"Efficiency cores color" = "Núcleos de eficiência, cores"; // translategemma:4b
"Performance cores color" = "Cores dos núcleos de desempenho"; // translategemma:4b
"Total load" = "Carga total"; // translategemma:4b
"System load" = "Carga do sistema"; // translategemma:4b
"User load" = "Carga de utilizadores"; // translategemma:4b
"Efficiency cores load" = "Cores de eficiência carregadas"; // translategemma:4b
"Performance cores load" = "Carga dos núcleos de desempenho"; // translategemma:4b
"All cores" = "Todas as cores"; // translategemma:4b
// GPU
"GPU to show" = "GPU para mostrar";
"Show GPU type" = "Mostrar tipo de GPU";
"GPU enabled" = "GPU ativado";
"GPU disabled" = "GPU desativado";
"GPU temperature" = "Temperatura da GPU";
"GPU utilization" = "Utilização da GPU";
"Vendor" = "Vendedor";
"Model" = "Modelo";
"Status" = "Estado";
"Active" = "Ativo";
"Non active" = "Não ativo";
"Fan speed" = "Velocidade das ventoinhas";
"Core clock" = "Frequência base"; // translategemma:4b
"Memory clock" = "Clock de memória"; // translategemma:4b
"Utilization" = "Utilização";
"Render utilization" = "Utilização da renderização"; // translategemma:4b
"Tiler utilization" = "Utilização de azulejistas"; // translategemma:4b
"GPU usage threshold" = "Limite de uso da GPU";
"GPU usage is" = "Utilização da GPU é %0";
// RAM
"Memory usage" = "Memória utilizada";
"Memory pressure" = "Pressão da memória";
"Total" = "Total";
"Used" = "Utilizado";
"App" = "Aplicação"; // translategemma:4b
"Wired" = "Por fio";
"Compressed" = "Comprimido";
"Free" = "Livre";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Divida o valor (App/Por fio/Comprimido)";
"RAM utilization threshold" = "Limite de uso da RAM";
"RAM utilization is" = "Utilização da RAM é %0";
"App color" = "Cor da app";
"Wired color" = "Cor por fio";
"Compressed color" = "Cor comprimida";
"Free color" = "Cor grátis";
"Free memory (less than)" = "Memória livre (menos de)"; // translategemma:4b
"Swap size" = "Tamanho do espaço de troca"; // translategemma:4b
"Free RAM is" = "A RAM disponível é %0"; // translategemma:4b
// Disk
"Show removable disks" = "Mostrar discos removíveis";
"Used disk memory" = "Utilizado %0 de %1";
"Free disk memory" = "Livre %0 de %1";
"Disk to show" = "Disco para mostrar";
"Open disk" = "Disco aberto";
"Switch view" = "Mudar de vista";
"Disk utilization threshold" = "Limite de uso do disco";
"Disk utilization is" = "Utilização do disco é %0";
"Read color" = "Identificar a cor"; // translategemma:4b
"Write color" = "Escreva a cor"; // translategemma:4b
"Disk usage" = "Utilização do disco"; // translategemma:4b
"Total read" = "Total lido"; // translategemma:4b
"Total written" = "Total escrito"; // translategemma:4b
"Write speed" = "Escreva"; // translategemma:4b
"Read speed" = "Leia"; // translategemma:4b
"Drives" = "Unidades de armazenamento"; // translategemma:4b
"SMART data" = "Dados SMART"; // translategemma:4b
// Sensors
"Temperature unit" = "Unidade de temperatura";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Guardar a velocidade da ventoinha";
"Fan" = "Ventoinha";
"HID sensors" = "Sensores HID";
"Synchronize fan's control" = "Sincronizar controlo das ventoinhas";
"Current" = "Atual";
"Energy" = "Energia";
"Show unknown sensors" = "Mostrar sensores desconhecidos";
"Install fan helper" = "Instalar auxiliar das ventoinhas";
"Uninstall fan helper" = "Desinstalar auxiliar das ventoinhas";
"Fan value" = "Valor da ventoinha";
"Turn off fan" = "Desligue o ventilador"; // translategemma:4b
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Você está prestes a desligar o ventilador. Esta é uma ação não recomendada que pode danificar o seu Mac. Tem certeza de que deseja fazê-lo?"; // translategemma:4b
"Sensor threshold" = "Limite do sensor"; // translategemma:4b
"Left fan" = "Esquerdo"; // translategemma:4b
"Right fan" = "Certo"; // translategemma:4b
"Fastest fan" = "Mais rápido"; // translategemma:4b
"Sensor to show" = "Sensor para exibir"; // translategemma:4b
// Network
"Uploading" = "Enviando";
"Downloading" = "Descarregando";
"Public IP" = "IP Público";
"Local IP" = "IP Local";
"Interface" = "Interface";
"Physical address" = "Endereço físico";
"Refresh" = "Atualizar";
"Click to copy public IP address" = "Clica para copiar o endereço de IP público";
"Click to copy local IP address" = "Clica para copiar o endereço de IP local";
"Click to copy wifi name" = "Clica para copiar o nome do wi-fi";
"Click to copy mac address" = "Clica para copiar o endereço mac";
"No connection" = "Sem conexão";
"Network interface" = "Interface de rede";
"Total download" = "Download total";
"Total upload" = "Upload total";
"Reader type" = "Tipo de leitor";
"Interface based" = "Interface";
"Processes based" = "Processo";
"Reset data usage" = "Limpar dados de uso";
"VPN mode" = "Modo da VPN";
"Standard" = "Padrão"; // translategemma:4b
"Security" = "Segurança";
"Channel" = "Canal";
"Common scale" = "Escala comum";
"Autodetection" = "Auto-deteção";
"Widget activation threshold" = "Limite de ativação do widget";
"Internet connection" = "Conexão à internet";
"Active state color" = "Cor do estado ativo";
"Nonactive state color" = "Cor do estado inativo";
"Connectivity host (ICMP)" = "Host da conexão (ICMP)";
"Leave empty to disable the check" = "Deixar vazio para desativar o check";
"Connectivity history" = "Histórico de conectividade"; // translategemma:4b
"Auto-refresh public IP address" = "Atualização automática do endereço IP público"; // translategemma:4b
"Every hour" = "A cada hora"; // translategemma:4b
"Every 12 hours" = "A cada 12 horas"; // translategemma:4b
"Every 24 hours" = "A cada 24 horas"; // translategemma:4b
"Network activity" = "Atividade de rede"; // translategemma:4b
"Last reset" = "A última reinicialização foi realizada %0 dias/semanas/meses/anos atrás"; // translategemma:4b
"Latency" = "Latência"; // translategemma:4b
"Upload speed" = "Enviar"; // translategemma:4b
"Download speed" = "Baixar"; // translategemma:4b
"Address" = "Endereço"; // translategemma:4b
"WiFi network" = "Rede WiFi"; // translategemma:4b
"Local IP changed" = "O endereço IP local foi alterado"; // translategemma:4b
"Public IP changed" = "O endereço IP público foi alterado."; // translategemma:4b
"Previous IP" = "Endereço IP anterior: %0"; // translategemma:4b
"New IP" = "Novo endereço IP: %0"; // translategemma:4b
"Internet connection lost" = "Conexão com a internet perdida"; // translategemma:4b
"Internet connection established" = "Conexão com a Internet estabelecida"; // translategemma:4b
// Battery
"Level" = "Nível";
"Source" = "Fonte";
"AC Power" = "Energia AC"; // translategemma:4b
"Battery Power" = "Carga da bateria";
"Time" = "Tempo";
"Health" = "Saúde";
"Amperage" = "Amperagem";
"Voltage" = "Voltagem";
"Cycles" = "Número de ciclos";
"Temperature" = "Temperatura";
"Power adapter" = "Adaptador de energia";
"Power" = "Energia";
"Is charging" = "Está a carregar";
"Time to discharge" = "Tempo até descarregar";
"Time to charge" = "Tempo até carregar";
"Calculating" = "Calculando";
"Fully charged" = "Completamente carregado";
"Not connected" = "Não conectado";
"Low level notification" = "Notificação de nível baixo";
"High level notification" = "Notificação de nível alto";
"Low battery" = "Bateria fraca";
"High battery" = "Bateria alta";
"Battery remaining" = "%0% bateria restante";
"Battery remaining to full charge" = "%0% para carga completa";
"Percentage" = "Percentagem";
"Percentage and time" = "Percentagem e tempo";
"Time and percentage" = "Tempo e percentagem";
"Time format" = "Formato de hora";
"Hide additional information when full" = "Ocultar informações adicionais quando estiver carregada";
"Last charge" = "Último carregamento";
"Capacity" = "Capacidade";
"current / maximum / designed" = "atual / máxima / desenhada";
"Low power mode" = "Modo poupança de energia";
"Percentage inside the icon" = "Percentagem dentro do ícone";
"Colorize battery" = "Colorir a bateria"; // translategemma:4b
"Charging current" = "Corrente de carregamento"; // translategemma:4b
"Charging Voltage" = "Tensão de carregamento"; // translategemma:4b
"Charger state inside the battery" = "Estado do carregador dentro da bateria"; // translategemma:4b
// Bluetooth
"Battery to show" = "Mostrar bateria";
"No Bluetooth devices are available" = "Não há dispositivos bluetooth disponíveis";
// Clock
"Time zone" = "Fuso horário"; // translategemma:4b
"Local" = "Local";
"Calendar" = "Calendário"; // translategemma:4b
"Show week numbers" = "Mostrar os números da semana"; // translategemma:4b
"Local time" = "Hora local"; // translategemma:4b
"Add new clock" = "Adicionar novo relógio"; // translategemma:4b
"Delete selected clock" = "Excluir o relógio selecionado"; // translategemma:4b
"Help with datetime format" = "Ajuda com o formato de data e hora"; // translategemma:4b
// Colors
"Based on utilization" = "Baseado na utilização";
"Based on pressure" = "Baseado na pressão";
"Based on cluster" = "Baseado no cluster";
"System accent" = "Sinal do sistema"; // translategemma:4b
"Monochrome accent" = "Acabamento monocromático"; // translategemma:4b
"Clear" = "Claro"; // translategemma:4b
"White" = "Branco";
"Black" = "Preto";
"Gray" = "Cinzento";
"Second gray" = "Cinzento secundário";
"Dark gray" = "Cinzento escuro";
"Light gray" = "Cinzento claro";
"Red" = "Vermelho";
"Second red" = "Vermelho secundário";
"Green" = "Verde";
"Second green" = "Verde secundário";
"Blue" = "Azul";
"Second blue" = "Azul secundário";
"Yellow" = "Amarelo";
"Second yellow" = "Amarelo secundário";
"Orange" = "Laranja";
"Second orange" = "Laranja secundário";
"Purple" = "Roxo";
"Second purple" = "Roxo secundário";
"Brown" = "Castanho";
"Second brown" = "Castanho secundário";
"Cyan" = "Ciano";
"Magenta" = "Magenta";
"Pink" = "Rosa";
"Teal" = "Azul petróleo";
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/ro.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Razvan Luta on 31/01/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "Procesor"; // translategemma:4b
"Open CPU settings" = "Deschide setările procesorului"; // translategemma:4b
"GPU" = "GPU";
"Open GPU settings" = "Deschide setările GPU"; // translategemma:4b
"RAM" = "RAM";
"Open RAM settings" = "Deschide setările memoriei RAM"; // translategemma:4b
"Disk" = "Hard disk"; // translategemma:4b
"Open Disk settings" = "Deschide setările discului"; // translategemma:4b
"Sensors" = "Senzori"; // translategemma:4b
"Open Sensors settings" = "Deschide setările senzorilor"; // translategemma:4b
"Network" = "Rețea"; // translategemma:4b
"Open Network settings" = "Deschide setările rețelei"; // translategemma:4b
"Battery" = "Baterie"; // translategemma:4b
"Open Battery settings" = "Deschide setările bateriei"; // translategemma:4b
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Deschide setările Bluetooth"; // translategemma:4b
"Clock" = "Ceas"; // translategemma:4b
"Open Clock settings" = "Deschide setările ceasului"; // translategemma:4b
// Words
"Unknown" = "Necunoscut";
"Version" = "Versiune";
"Processor" = "Procesor";
"Memory" = "Memorie";
"Graphics" = "Grafică";
"Close" = "Închide";
"Download" = "Descarcă";
"Install" = "Instalează";
"Cancel" = "Anulează";
"Unavailable" = "Indisponibil";
"Yes" = "Da";
"No" = "Nu";
"Automatic" = "Automat";
"Manual" = "Instrucțiuni"; // translategemma:4b
"None" = "Niciunul";
"Dots" = "Puncte";
"Arrows" = "Săgeți";
"Characters" = "Litere";
"Short" = "Scurt";
"Long" = "Lung";
"Statistics" = "Statistici";
"Max" = "Max";
"Min" = "Min";
"Reset" = "Resetează"; // translategemma:4b
"Alignment" = "Aliniere"; // translategemma:4b
"Left alignment" = "Stânga"; // translategemma:4b
"Center alignment" = "Centru"; // translategemma:4b
"Right alignment" = "Corect"; // translategemma:4b
"Dashboard" = "Panou de control"; // translategemma:4b
"Enabled" = "Activat"; // translategemma:4b
"Disabled" = "Persoană cu dizabilități"; // translategemma:4b
"Silent" = "Silențios"; // translategemma:4b
"Units" = "Unități"; // translategemma:4b
"Fans" = "Fanii"; // translategemma:4b
"Scaling" = "Scalare"; // translategemma:4b
"Linear" = "Liniar"; // translategemma:4b
"Square" = "Pătrat"; // translategemma:4b
"Cube" = "Cub"; // translategemma:4b
"Logarithmic" = "Logaritmic"; // translategemma:4b
"Fixed scale" = "Reparat"; // translategemma:4b
"Cores" = "Nuclee"; // translategemma:4b
"Settings" = "Setări"; // translategemma:4b
"Name" = "Nume"; // translategemma:4b
"Format" = "Format";
"Turn off" = "Opriți"; // translategemma:4b
"Normal" = "Normal";
"Warning" = "Atenție"; // translategemma:4b
"Critical" = "Esential"; // translategemma:4b
"Usage" = "Utilizare"; // translategemma:4b
"2 minutes" = "2 minute"; // translategemma:4b
"3 minutes" = "3 minute"; // translategemma:4b
"10 minutes" = "10 minute"; // translategemma:4b
"Import" = "Import";
"Export" = "Export";
"Separator" = "Delimitor"; // translategemma:4b
"Read" = "Citește"; // translategemma:4b
"Write" = "Scrie"; // translategemma:4b
"Frequency" = "Frecvență"; // translategemma:4b
"Save" = "Salvați"; // translategemma:4b
"Run" = "Rulează"; // translategemma:4b
"Stop" = "Oprește"; // translategemma:4b
"Uninstall" = "Dezinstale"; // translategemma:4b
"1 sec" = "1 secundă"; // translategemma:4b
"2 sec" = "2 secunde"; // translategemma:4b
"3 sec" = "3 secunde"; // translategemma:4b
"5 sec" = "5 secunde"; // translategemma:4b
"10 sec" = "10 sec";
"15 sec" = "15 sec";
"30 sec" = "30 sec";
"60 sec" = "60 sec";
// Setup
"Stats Setup" = "Configurarea statisticilor"; // translategemma:4b
"Previous" = "Anterior"; // translategemma:4b
"Previous page" = "Pagina anterioară"; // translategemma:4b
"Next" = "Următor"; // translategemma:4b
"Next page" = "Pagina următoare"; // translategemma:4b
"Finish" = "Termină"; // translategemma:4b
"Finish setup" = "Finalizați configurarea"; // translategemma:4b
"Welcome to Stats" = "Bine ați venit la Stats"; // translategemma:4b
"welcome_message" = "Vă mulțumim că utilizați Stats, un monitor gratuit și open-source pentru sistemul macOS, care poate fi integrat în bara de meniu."; // translategemma:4b
"Start the application automatically when starting your Mac" = "Lansați aplicația automat la pornirea Mac-ului."; // translategemma:4b
"Do not start the application automatically when starting your Mac" = "Nu lansați aplicația automat atunci când porniți Mac-ul."; // translategemma:4b
"Do everything silently in the background (recommended)" = "Faceți totul în fundal, în mod discret (recomandat)"; // translategemma:4b
"Check for a new version on startup" = "Verificați dacă există o nouă versiune la pornire."; // translategemma:4b
"Check for a new version every day (once a day)" = "Verificați dacă există o nouă versiune în fiecare zi (o dată pe zi)"; // translategemma:4b
"Check for a new version every week (once a week)" = "Verificați dacă există o nouă versiune o dată pe săptămână."; // translategemma:4b
"Check for a new version every month (once a month)" = "Verificați dacă există o nouă versiune o dată pe lună."; // translategemma:4b
"Never check for updates (not recommended)" = "Nu verificați niciodată actualizările (nu este recomandat)"; // translategemma:4b
"Anonymous telemetry for better development decisions" = "Date de telemetrie anonime pentru o mai bună luare a deciziilor în dezvoltare"; // translategemma:4b
"Share anonymous telemetry data" = "Împărtăși date de telemetrie anonime"; // translategemma:4b
"Do not share anonymous telemetry data" = "Nu distribui datele de telemetrie anonime."; // translategemma:4b
"The configuration is completed" = "Configurația a fost finalizată"; // translategemma:4b
"finish_setup_message" = "Totul este pregătit!"; // translategemma:4b
// Alerts
"New version available" = "Versiune nouă disponibilă";
"Click to install the new version of Stats" = "Click aici pentru a instala noua versiune Stats";
"Successfully updated" = "Actualizat cu succes";
"Stats was updated to v" = "Stats a fost actualizat la v%0";
"Reset settings text" = "Toate setările aplicației vor fi reseteate și aplicația va fi repornită. Sunteți sigur că doriți să faceți acest lucru?"; // translategemma:4b
"Support text" = "Vă mulțumim pentru utilizarea Stats!\n\n Menținerea și îmbunătățirea acestui proiect open-source necesită timp și resurse. Sprijinul dumneavoastră ne ajută să continuăm să oferim o aplicație gratuită și fiabilă pentru toată lumea.\n\nDacă Stats vă este util, vă rugăm să luați în considerare posibilitatea de a face o contribuție. Fiecare bănuț ajută!";
// Settings
"Open Activity Monitor" = "Deschide Monitorul de Activitate";
"Report a bug" = "Raportează o eroare";
"Support the application" = "Susține aplicația";
"Close application" = "Închide aplicația";
"Open application settings" = "Deschide setăriile aplicației";
"Open dashboard" = "Deschide panoul de control"; // translategemma:4b
"No notifications available in this module" = "Nu există notificări disponibile în acest modul"; // translategemma:4b
"Open Calendar" = "Deschide calendarul"; // translategemma:4b
"Toggle the module" = "Activați/dezactivați modulul"; // translategemma:4b
// Application settings
"Update application" = "Actualizează aplicația";
"Check for updates" = "Verifică pentru actualizări";
"At start" = "La pornire";
"Once per day" = "Odată pe zi";
"Once per week" = "Odată pe săptămână";
"Once per month" = "Odată pe lună";
"Never" = "Niciodată";
"Check for update" = "Verifică pentru actualizare";
"Show icon in dock" = "Arată icoana in doc";
"Start at login" = "Pornește la autentificare";
"Build number" = "Număr de versiune"; // translategemma:4b
"Import settings" = "Importați setările"; // translategemma:4b
"Export settings" = "Setări de export"; // translategemma:4b
"Reset settings" = "Resetează setările"; // translategemma:4b
"Pause the Stats" = "Pauză pentru statistici"; // translategemma:4b
"Resume the Stats" = "Reia statisticile"; // translategemma:4b
"Combined modules" = "Module integrate"; // translategemma:4b
"Combined details" = "Detalii combinate"; // translategemma:4b
"Spacing" = "Spațierea"; // translategemma:4b
"Share anonymous telemetry" = "Partajarea datelor anonime de telemetrie"; // translategemma:4b
"Choose file" = "Alege fișier"; // translategemma:4b
"Stress tests" = "Teste de stres"; // translategemma:4b
// Dashboard
"Serial number" = "Număr de serie";
"Model identifier" = "Identificatorul modelului"; // translategemma:4b
"Production year" = "Anul de producție"; // translategemma:4b
"Uptime" = "Timp în opererare";
"Number of cores" = "%0 nuclee"; // translategemma:4b
"Number of threads" = "%0 fire"; // translategemma:4b
"Number of e-cores" = "%0 nuclee cu eficiență ridicată"; // translategemma:4b
"Number of p-cores" = "%0 nuclee de performanță"; // translategemma:4b
"Disks" = "Discuri"; // translategemma:4b
"Display" = "Ecran"; // translategemma:4b
// Update
"The latest version of Stats installed" = "Cea mai recentă versiune de Stats instalată";
"Downloading..." = "Se descarcă...";
"Current version: " = "Versiunea curentă: ";
"Latest version: " = "Versiunea cea mai recentă: ";
// Widgets
"Color" = "Culoare";
"Label" = "Etichetă";
"Box" = "Cutie";
"Frame" = "Cadru";
"Value" = "Valoare";
"Colorize" = "Colorează";
"Colorize value" = "Colorează valoarea";
"Additional information" = "Informații suplimentare";
"Reverse values order" = "Inversează ordinea valoriilor";
"Base" = "Baza";
"Display mode" = "Modul de vizualizare";
"One row" = "Un rând";
"Two rows" = "Două rânduri";
"Mini widget" = "Mini";
"Line chart widget" = "Grafic cu linii"; // translategemma:4b
"Bar chart widget" = "Grafic cu bare"; // translategemma:4b
"Pie chart widget" = "Grafic circular"; // translategemma:4b
"Network chart widget" = "Diagramă de rețea"; // translategemma:4b
"Speed widget" = "Viteză"; // translategemma:4b
"Battery widget" = "Baterie"; // translategemma:4b
"Stack widget" = "Stivă"; // translategemma:4b
"Memory widget" = "Memorie"; // translategemma:4b
"Static width" = "Lățime fixă"; // translategemma:4b
"Tachometer widget" = "Contor de turație"; // translategemma:4b
"State widget" = "Widget de stare"; // translategemma:4b
"Text widget" = "Widget de text"; // translategemma:4b
"Battery details widget" = "Widget cu detalii despre baterie"; // translategemma:4b
"Show symbols" = "Afișează simboluri"; // translategemma:4b
"Label widget" = "Etichetă"; // translategemma:4b
"Number of reads in the chart" = "Numărul de citiri în grafic"; // translategemma:4b
"Color of download" = "Culoarea descărcării"; // translategemma:4b
"Color of upload" = "Culoarea încărcării"; // translategemma:4b
"Monospaced font" = "Font cu caractere de aceeași dimensiune"; // translategemma:4b
"Reverse order" = "În ordine inversă"; // translategemma:4b
"Chart history" = "Istoricul grafic"; // translategemma:4b
"Default color" = "Implicit"; // translategemma:4b
"Transparent when no activity" = "Transparente când nu există activitate"; // translategemma:4b
"Constant color" = "Constant";
// Module Kit
"Open module settings" = "Deschide setăriile modulului";
"Select widget" = "Alege opțiunea %0";
"Open widget settings" = "Deschide setările widget-ului"; // translategemma:4b
"Update interval" = "Intervalul de actualizare";
"Usage history" = "Istoricul de folosință";
"Details" = "Detalii";
"Top processes" = "Procesele de vârf";
"Pictogram" = "Pictogramă";
"Module" = "Modul";
"Widgets" = "Widgeturi";
"Popup" = "Pop-up";
"Notifications" = "Notificări";
"Merge widgets" = "Îmbinați widget-uri";
"No available widgets to configure" = "Nu există widgeturi disponibile de configurat";
"No options to configure for the popup in this module" = "Nu există opțiuni de configurat pentru fereastra pop-up din acest modul";
"Process" = "Proces"; // translategemma:4b
"Kill process" = "Termină procesul"; // translategemma:4b
"Keyboard shortcut" = "Scurtătură de tastatură"; // translategemma:4b
"Listening..." = "Ascult..."; // translategemma:4b
// Modules
"Number of top processes" = "Numărul proceselor de vârf";
"Update interval for top processes" = "Intervalul de actualizare pentru procesele principale"; // translategemma:4b
"Notification level" = "Nivel de notificare"; // translategemma:4b
"Chart color" = "Culoarea graficului"; // translategemma:4b
"Main chart scaling" = "Scala principală a graficului"; // translategemma:4b
"Scale value" = "Valoarea măsurată"; // translategemma:4b
"Text widget value" = "Valoarea widget-ului de text"; // translategemma:4b
// CPU
"CPU usage" = "Utilizarea procesorului";
"CPU temperature" = "Temperatura procesorului";
"CPU frequency" = "Frecvența procesorului";
"System" = "Sistem";
"User" = "Utilizator";
"Idle" = "Inactiv";
"Show usage per core" = "Arată utilizarea pe fiecare nucleu";
"Show hyper-threading cores" = "Arată nucleurile ce lucrează suplimentar";
"Split the value (System/User)" = "Împarte valoarea (Sistem/Utilizator)";
"Scheduler limit" = "Limitul programatorului"; // translategemma:4b
"Speed limit" = "Limitul de viteză"; // translategemma:4b
"Average load" = "Sarcina medie"; // translategemma:4b
"1 minute" = "1 minut"; // translategemma:4b
"5 minutes" = "5 minute"; // translategemma:4b
"15 minutes" = "15 minute"; // translategemma:4b
"CPU usage threshold" = "Limita de utilizare a procesorului"; // translategemma:4b
"CPU usage is" = "Utilizarea procesorului este de `%0"; // translategemma:4b
"Efficiency cores" = "Nuclee de eficiență"; // translategemma:4b
"Performance cores" = "Nuclee de performanță"; // translategemma:4b
"System color" = "Culoarea sistemului"; // translategemma:4b
"User color" = "Culoarea utilizatorului"; // translategemma:4b
"Idle color" = "Culoarea în repaus"; // translategemma:4b
"Cluster grouping" = "Gruparea în clustere"; // translategemma:4b
"Efficiency cores color" = "Cores cu eficiență, culori"; // translategemma:4b
"Performance cores color" = "Culorile nucleelor de performanță"; // translategemma:4b
"Total load" = "Suma totală"; // translategemma:4b
"System load" = "Căderea sistemului"; // translategemma:4b
"User load" = "Căderea sistemului"; // translategemma:4b
"Efficiency cores load" = "Procese eficiente sunt încărcate"; // translategemma:4b
"Performance cores load" = "Procesele de bază încep să funcționeze"; // translategemma:4b
"All cores" = "Toate nucleele"; // translategemma:4b
// GPU
"GPU to show" = "Placa video să fie arătată";
"Show GPU type" = "Arată tipul de placă video";
"GPU enabled" = "Placa video activată";
"GPU disabled" = "Placa video dezactivată";
"GPU temperature" = "Temperatura plăcii video";
"GPU utilization" = "Utilizarea plăcii video";
"Vendor" = "Producătorul";
"Model" = "Modelul";
"Status" = "Statutul";
"Active" = "Activ";
"Non active" = "Inactiv";
"Fan speed" = "Viteza ventilatorului";
"Core clock" = "Frecvența nucleului";
"Memory clock" = "Frecvența memoriei";
"Utilization" = "Gradul de utilizare";
"Render utilization" = "Utilizarea resurselor"; // translategemma:4b
"Tiler utilization" = "Utilizarea tiler-ului"; // translategemma:4b
"GPU usage threshold" = "Limita de utilizare a GPU"; // translategemma:4b
"GPU usage is" = "Utilizarea GPU este `%0"; // translategemma:4b
// RAM
"Memory usage" = "Memoria folosită";
"Memory pressure" = "Presiunea memoriei";
"Total" = "Total";
"Used" = "Folosit";
"App" = "Aplicația";
"Wired" = "Conectat prin cablu";
"Compressed" = "Comprimat";
"Free" = "Gratuit";
"Swap" = "Schimbă";
"Split the value (App/Wired/Compressed)" = "Împarte valoarea (Aplicația/Conectat prin cablu/Comprimat)";
"RAM utilization threshold" = "Limita de utilizare a memoriei RAM"; // translategemma:4b
"RAM utilization is" = "Utilizarea memoriei RAM este de `%0`"; // translategemma:4b
"App color" = "Culoarea aplicației"; // translategemma:4b
"Wired color" = "Culori prin cablu"; // translategemma:4b
"Compressed color" = "Culori comprimate"; // translategemma:4b
"Free color" = "Culoare gratuită"; // translategemma:4b
"Free memory (less than)" = "Memorie disponibilă (mai puțin de)"; // translategemma:4b
"Swap size" = "Dimensiunea spațiului de schimb"; // translategemma:4b
"Free RAM is" = "Memoria RAM disponibilă este %0"; // translategemma:4b
// Disk
"Show removable disks" = "Arată discurile detașabile";
"Used disk memory" = "%0 din %1 folosită";
"Free disk memory" = "%0 din %1 disponibilă";
"Disk to show" = "Discul să fie arătat";
"Open disk" = "Deschide discul";
"Switch view" = "Schimbă vederea";
"Disk utilization threshold" = "Limita de utilizare a discului"; // translategemma:4b
"Disk utilization is" = "Utilizarea discului este de `%0"; // translategemma:4b
"Read color" = "Citeți culorile"; // translategemma:4b
"Write color" = "Scrie culoarea"; // translategemma:4b
"Disk usage" = "Utilizarea spațiului pe disc"; // translategemma:4b
"Total read" = "Total citit"; // translategemma:4b
"Total written" = "Total scris"; // translategemma:4b
"Write speed" = "Scrie"; // translategemma:4b
"Read speed" = "Citiți"; // translategemma:4b
"Drives" = "Unități de stocare"; // translategemma:4b
"SMART data" = "Date SMART"; // translategemma:4b
// Sensors
"Temperature unit" = "Temperatura unității";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Salvați viteza ventilatorului"; // translategemma:4b
"Fan" = "Fan";
"HID sensors" = "Senzori HIDs"; // translategemma:4b
"Synchronize fan's control" = "Sincronizați controlul ventilatorului"; // translategemma:4b
"Current" = "Actual"; // translategemma:4b
"Energy" = "Energie"; // translategemma:4b
"Show unknown sensors" = "Afișează senzori necunoscuți"; // translategemma:4b
"Install fan helper" = "Instalează aplicația de asistență pentru ventilatoare"; // translategemma:4b
"Uninstall fan helper" = "Dezinstalați aplicația \"asistent pentru ventilator"; // translategemma:4b
"Fan value" = "Valoare de fani"; // translategemma:4b
"Turn off fan" = "Oprește ventilatorul"; // translategemma:4b
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Vă veți opri ventilatorul. Aceasta este o acțiune nerecomandată care poate deteriora dispozitivul dumneavoastră Mac. Sunteți sigur că doriți să faceți acest lucru?"; // translategemma:4b
"Sensor threshold" = "Limita senzorului"; // translategemma:4b
"Left fan" = "Stânga"; // translategemma:4b
"Right fan" = "Corect"; // translategemma:4b
"Fastest fan" = "Cel mai rapid"; // translategemma:4b
"Sensor to show" = "Senzor pentru a afișa"; // translategemma:4b
// Network
"Uploading" = "Incarcă";
"Downloading" = "Descarcă";
"Public IP" = "Adresa de IP publică";
"Local IP" = "Adresa de IP locală";
"Interface" = "Interfață";
"Physical address" = "Adresa fizică";
"Refresh" = "Reîmprospătează"; // translategemma:4b
"Click to copy public IP address" = "Click pentru a copia adresa de IP publică";
"Click to copy local IP address" = "Click pentru a copia adresa de IP locală";
"Click to copy wifi name" = "Click pentru a copia numele rețelei WiFi";
"Click to copy mac address" = "Click pentru a copia adresa Mac-ului";
"No connection" = "Fără conexiune";
"Network interface" = "Interfața rețelei";
"Total download" = "Totalul descărcat";
"Total upload" = "Totalul încărcat";
"Reader type" = "Tipul de cititor";
"Interface based" = "Bazat pe interfață";
"Processes based" = "Bazat pe procese";
"Reset data usage" = "Resetează consumul de date"; // translategemma:4b
"VPN mode" = "Mod VPN"; // translategemma:4b
"Standard" = "Standard";
"Security" = "Securitate"; // translategemma:4b
"Channel" = "Canal"; // translategemma:4b
"Common scale" = "Scala comună"; // translategemma:4b
"Autodetection" = "Detectare automată"; // translategemma:4b
"Widget activation threshold" = "Timp de activare al widget-ului"; // translategemma:4b
"Internet connection" = "Conexiune la internet"; // translategemma:4b
"Active state color" = "Culoarea stării active"; // translategemma:4b
"Nonactive state color" = "Culoarea stării inactive"; // translategemma:4b
"Connectivity host (ICMP)" = "Server de conectivitate (ICMP)"; // translategemma:4b
"Leave empty to disable the check" = "Lăsați câmpul gol pentru a dezactiva verificarea."; // translategemma:4b
"Connectivity history" = "Istoricul conexiunilor"; // translategemma:4b
"Auto-refresh public IP address" = "Actualizare automată a adresei IP publice"; // translategemma:4b
"Every hour" = "La fiecare oră"; // translategemma:4b
"Every 12 hours" = "La fiecare 12 ore"; // translategemma:4b
"Every 24 hours" = "La fiecare 24 de ore"; // translategemma:4b
"Network activity" = "Activitatea rețelei"; // translategemma:4b
"Last reset" = "Ultima reseta a fost efectuată acum `%0` de zile."; // translategemma:4b
"Latency" = "Latență"; // translategemma:4b
"Upload speed" = "Încărcare"; // translategemma:4b
"Download speed" = "Descarcă"; // translategemma:4b
"Address" = "Adresă"; // translategemma:4b
"WiFi network" = "Rețea Wi-Fi"; // translategemma:4b
"Local IP changed" = "Adresa IP locală a fost modificată"; // translategemma:4b
"Public IP changed" = "Adresa IP publică a fost modificată."; // translategemma:4b
"Previous IP" = "Adresa IP anterioară: %0"; // translategemma:4b
"New IP" = "Nou adresă IP: %0"; // translategemma:4b
"Internet connection lost" = "Conexiunea la internet a fost pierdută"; // translategemma:4b
"Internet connection established" = "Conexiunea la internet a fost stabilită"; // translategemma:4b
// Battery
"Level" = "Nivel";
"Source" = "Sursă";
"AC Power" = "Alimentare de la rețeaua electrică"; // translategemma:4b
"Battery Power" = "Putere alimentată de baterie"; // translategemma:4b
"Time" = "Timp";
"Health" = "Sănătate";
"Amperage" = "Amperaj";
"Voltage" = "Voltaj";
"Cycles" = "Cicluri";
"Temperature" = "Temperatură";
"Power adapter" = "Adaptor de putere prin cablu";
"Power" = "Putere";
"Is charging" = "Se încarcă";
"Time to discharge" = "Timp până la descărcare";
"Time to charge" = "Timp până la încărcare";
"Calculating" = "Se calculează";
"Fully charged" = "Încărcat complet";
"Not connected" = "Nu este conectat";
"Low level notification" = "Notificație de nivel scăzut";
"High level notification" = "Notificare de nivel înalt"; // translategemma:4b
"Low battery" = "Baterie puțină";
"High battery" = "Baterie de lungă durată"; // translategemma:4b
"Battery remaining" = "%0% rămas";
"Battery remaining to full charge" = "De la 0% la încărcare completă"; // translategemma:4b
"Percentage" = "Procentaj";
"Percentage and time" = "Procentaj și timp";
"Time and percentage" = "Timp si procentaj";
"Time format" = "Formatul timpului";
"Hide additional information when full" = "Ascunde restul informației cand e plină";
"Last charge" = "Ultima încărcare";
"Capacity" = "Capacitate"; // translategemma:4b
"current / maximum / designed" = "actual / maxim / proiectat"; // translategemma:4b
"Low power mode" = "Mod de funcționare cu consum redus"; // translategemma:4b
"Percentage inside the icon" = "Procentul afișat în interiorul iconului"; // translategemma:4b
"Colorize battery" = "Variați culoarea bateriei"; // translategemma:4b
"Charging current" = "Curentul de încărcare"; // translategemma:4b
"Charging Voltage" = "Tensiunea de încărcare"; // translategemma:4b
"Charger state inside the battery" = "Starea încărcătorului în interiorul bateriei"; // translategemma:4b
// Bluetooth
"Battery to show" = "Indicarea stării bateriei"; // translategemma:4b
"No Bluetooth devices are available" = "Nu sunt disponibile dispozitive Bluetooth."; // translategemma:4b
// Clock
"Time zone" = "Fus orar"; // translategemma:4b
"Local" = "Local";
"Calendar" = "Calendar";
"Show week numbers" = "Afișează numerele săptămânii"; // translategemma:4b
"Local time" = "Ora locală"; // translategemma:4b
"Add new clock" = "Adaugă un ceas nou"; // translategemma:4b
"Delete selected clock" = "Șterge ceasul selectat"; // translategemma:4b
"Help with datetime format" = "Ajutor cu formatarea datelor și a orelor"; // translategemma:4b
// Colors
"Based on utilization" = "În funcție de utilizare"; // translategemma:4b
"Based on pressure" = "Pe baza presiunii"; // translategemma:4b
"Based on cluster" = "Bazându-ne pe grupare"; // translategemma:4b
"System accent" = "Accentul sistemului"; // translategemma:4b
"Monochrome accent" = "Accent monocromatic"; // translategemma:4b
"Clear" = "Clar"; // translategemma:4b
"White" = "Alb"; // translategemma:4b
"Black" = "Negru"; // translategemma:4b
"Gray" = "Gri"; // translategemma:4b
"Second gray" = "Al doilea gri"; // translategemma:4b
"Dark gray" = "Gri închis"; // translategemma:4b
"Light gray" = "Gri deschis"; // translategemma:4b
"Red" = "Roșu"; // translategemma:4b
"Second red" = "Al doilea roșu"; // translategemma:4b
"Green" = "Verde"; // translategemma:4b
"Second green" = "Al doilea verde"; // translategemma:4b
"Blue" = "Albastru"; // translategemma:4b
"Second blue" = "Al doilea albastru"; // translategemma:4b
"Yellow" = "Galben"; // translategemma:4b
"Second yellow" = "Al doilea galben"; // translategemma:4b
"Orange" = "Portocaliu"; // translategemma:4b
"Second orange" = "A doua mărime portocalie"; // translategemma:4b
"Purple" = "Violet"; // translategemma:4b
"Second purple" = "Al doilea violet"; // translategemma:4b
"Brown" = "Maro"; // translategemma:4b
"Second brown" = "Al doilea maro"; // translategemma:4b
"Cyan" = "Cian"; // translategemma:4b
"Magenta" = "Magenta";
"Pink" = "Roz"; // translategemma:4b
"Teal" = "Albastru închis"; // translategemma:4b
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/ru.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "Процессор";
"Open CPU settings" = "Открыть настройки ЦП";
"GPU" = "Графический процессор";
"Open GPU settings" = "Открыть настройки графического процессора";
"RAM" = "Оперативная память";
"Open RAM settings" = "Открыть настройки оперативной памяти";
"Disk" = "Диск";
"Open Disk settings" = "Открыть настройки диска";
"Sensors" = "Датчики";
"Open Sensors settings" = "Открыть настройки датчиков";
"Network" = "Сеть";
"Open Network settings" = "Открыть настройки сети";
"Battery" = "Аккумулятор";
"Open Battery settings" = "Открыть настройки аккумулятора";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Открыть настройки bluetooth";
"Clock" = "Часы";
"Open Clock settings" = "Открыть настройки часов";
// Words
"Unknown" = "Неизвестно";
"Version" = "Версия";
"Processor" = "Процессор";
"Memory" = "Память";
"Graphics" = "Графика";
"Close" = "Закрыть";
"Download" = "Скачать";
"Install" = "Установить";
"Cancel" = "Отменить";
"Unavailable" = "Недоступно";
"Yes" = "Да";
"No" = "Нет";
"Automatic" = "Автоматический";
"Manual" = "Ручной";
"None" = "Никакой";
"Dots" = "Точки";
"Arrows" = "Стрелки";
"Characters" = "Символы";
"Short" = "Короткий";
"Long" = "Длинный";
"Statistics" = "Статистика";
"Max" = "Макс.";
"Min" = "Мин.";
"Reset" = "Сбросить";
"Alignment" = "Выравнивание";
"Left alignment" = "по левому";
"Center alignment" = "по центру";
"Right alignment" = "по правому";
"Dashboard" = "Главная страница"; // translategemma:4b
"Enabled" = "Включено";
"Disabled" = "Отключено";
"Silent" = "Бесшумно";
"Units" = "Единицы"; // translategemma:4b
"Fans" = "Вентиляторы";
"Scaling" = "Масштабирование";
"Linear" = "Линейное";
"Square" = "Квадратное";
"Cube" = "Кубическое";
"Logarithmic" = "Логарифмическое";
"Fixed scale" = "Фиксированное";
"Cores" = "Ядра";
"Settings" = "Настройки";
"Name" = "Название";
"Format" = "Формат";
"Turn off" = "Выключить";
"Normal" = "Нормальный";
"Warning" = "Предупреждение";
"Critical" = "Критический";
"Usage" = "Использование";
"2 minutes" = "2 минуты";
"3 minutes" = "3 минуты";
"10 minutes" = "10 минут";
"Import" = "Импорт";
"Export" = "Экспорт";
"Separator" = "Разделитель";
"Read" = "Чтение";
"Write" = "Запись";
"Frequency" = "Частота";
"Save" = "Сохранить";
"Run" = "Запустить";
"Stop" = "Остановить";
"Uninstall" = "Удалить";
"1 sec" = "1 сек";
"2 sec" = "2 сек";
"3 sec" = "3 сек";
"5 sec" = "5 сек";
"10 sec" = "10 сек";
"15 sec" = "15 сек";
"30 sec" = "30 сек";
"60 sec" = "60 сек";
// Setup
"Stats Setup" = "Настройки Stats";
"Previous" = "Назад";
"Previous page" = "Предыдущая страница";
"Next" = "Вперёд";
"Next page" = "Следующая страница";
"Finish" = "Завершить";
"Finish setup" = "Завершить настройку";
"Welcome to Stats" = "Добро пожаловать в Stats";
"welcome_message" = "Спасибо за использование Stats, бесплатного системного монитора macOS с открытым исходным кодом для системного меню.";
"Start the application automatically when starting your Mac" = "Запуск приложения автоматически при запуске Mac";
"Do not start the application automatically when starting your Mac" = "Не запускать приложение автоматически при запуске Mac";
"Do everything silently in the background (recommended)" = "Делать все тихо в фоновом режиме (рекомендуется)";
"Check for a new version on startup" = "Проверять наличие новой версии при запуске";
"Check for a new version every day (once a day)" = "Проверять наличие новой версии каждый день (один раз в день)";
"Check for a new version every week (once a week)" = "Проверять наличие новой версии каждую неделю (раз в неделю)";
"Check for a new version every month (once a month)" = "Проверять наличие новой версии каждый месяц (раз в месяц)";
"Never check for updates (not recommended)" = "Никогда не проверять наличие обновлений (не рекомендуется)";
"Anonymous telemetry for better development decisions" = "Анонимная телеметрия для принятия лучших решений при разработке";
"Share anonymous telemetry data" = "Делится анонимными данными телеметрии";
"Do not share anonymous telemetry data" = "Не передавайте анонимные данные телеметрии";
"The configuration is completed" = "Настройка завершена";
"finish_setup_message" = "Все готово! \n Stats — это инструмент с открытым исходным кодом, он бесплатный и всегда будет бесплатным. \n Если вам нравится, вы можете поддержать проект, это всегда ценно!";
// Alerts
"New version available" = "Доступна новая версия";
"Click to install the new version of Stats" = "Нажмите, чтобы установить новую версию Stats";
"Successfully updated" = "Успешно обновлено";
"Stats was updated to v" = "Stats обновлено до v%0";
"Reset settings text" = "Все настройки приложения будут сброшены, и приложение будет перезапущено. Вы уверены, что хотите это сделать?";
"Support text" = "Спасибо за использование Stats.\n\n Поддержание и улучшение этого проекта с открытым исходным кодом требует времени и ресурсов. Ваша поддержка помогает нам продолжать предоставлять бесплатное и надежное приложение для всех.\n\nЕсли вы находите Stats полезным, пожалуйста, подумайте о том, чтобы сделать взнос. Каждый маленький кусочек помогает!";
// Settings
"Open Activity Monitor" = "Открыть Мониторинг системы";
"Report a bug" = "Сообщить об ошибке";
"Support the application" = "Поддержите приложение";
"Close application" = "Закрыть приложение";
"Open application settings" = "Открыть настройки приложения";
"Open dashboard" = "Открыть dashboard";
"No notifications available in this module" = "В этом модуле нет доступных уведомлений";
"Open Calendar" = "Открыть календарь";
"Toggle the module" = "Переключить модуль";
// Application settings
"Update application" = "Обновить приложение";
"Check for updates" = "Проверять обновления";
"At start" = "При включении";
"Once per day" = "Раз в день";
"Once per week" = "Раз в неделю";
"Once per month" = "Раз в месяц";
"Never" = "Никогда";
"Check for update" = "Проверить обновление";
"Show icon in dock" = "Показать значок в dock";
"Start at login" = "Запуск при входе";
"Build number" = "Номер сборки";
"Import settings" = "Импортировать настройки";
"Export settings" = "Экспортировать настройки";
"Reset settings" = "Сбросить настройки";
"Pause the Stats" = "Приостановить Stats";
"Resume the Stats" = "Возобновить Stats";
"Combined modules" = "Комбинированные модули";
"Combined details" = "Комбинированные детали";
"Spacing" = "Интервал";
"Share anonymous telemetry" = "Делится анонимной телеметрией";
"Choose file" = "Выбрать файл";
"Stress tests" = "Стресс-тесты";
// Dashboard
"Serial number" = "Серийный номер";
"Model identifier" = "Идентификатор модели";
"Production year" = "Год выпуска";
"Uptime" = "Время работы";
"Number of cores" = "%0 ядер";
"Number of threads" = "%0 потоков";
"Number of e-cores" = "%0 энергоэффективных ядер";
"Number of p-cores" = "%0 производительных ядер";
"Disks" = "Диски";
"Display" = "Монитор";
// Update
"The latest version of Stats installed" = "Установлена последняя версия";
"Downloading..." = "Скачивание...";
"Current version: " = "Текущая версия: ";
"Latest version: " = "Последняя версия: ";
// Widgets
"Color" = "Цвет";
"Label" = "Метка";
"Box" = "Коробка"; // translategemma:4b
"Frame" = "Рамка";
"Value" = "Значение";
"Colorize" = "Раскрасить";
"Colorize value" = "Раскрасить значение";
"Additional information" = "Дополнительная информация";
"Reverse values order" = "Изменить порядок сортировки";
"Base" = "Основа";
"Display mode" = "Режим отображения";
"One row" = "Один ряд";
"Two rows" = "Два ряда";
"Mini widget" = "Мини";
"Line chart widget" = "Линейный график";
"Bar chart widget" = "Гистограмма";
"Pie chart widget" = "Круговая диаграмма";
"Network chart widget" = "Сетевая диаграмма";
"Speed widget" = "Скорость";
"Battery widget" = "Батарея";
"Stack widget" = "Стек";
"Memory widget" = "Память";
"Static width" = "Статическая ширина";
"Tachometer widget" = "Тахометр";
"State widget" = "Состояние";
"Text widget" = "Текст";
"Battery details widget" = "Виджет сведений о батарее";
"Show symbols" = "Показать символы";
"Label widget" = "Этикетка";
"Number of reads in the chart" = "Количество чтений на графике";
"Color of download" = "Цвет загрузки";
"Color of upload" = "Цвет выгрузки";
"Monospaced font" = "Моноширинный шрифт";
"Reverse order" = "Обратный порядок";
"Chart history" = "Продолжительность графика";
"Default color" = "По умолчанию";
"Transparent when no activity" = "Прозрачный когда нет активности";
"Constant color" = "Постоянный";
// Module Kit
"Open module settings" = "Открыть настройки модуля";
"Select widget" = "Активировать %0 виджет";
"Open widget settings" = "Открыть настройки виджета";
"Update interval" = "Интервал обновления";
"Usage history" = "История использования";
"Details" = "Подробности";
"Top processes" = "Топ процессы";
"Pictogram" = "Пиктограмма";
"Module" = "Модуль";
"Widgets" = "Виджеты";
"Popup" = "Всплывающее окно";
"Notifications" = "Уведомления";
"Merge widgets" = "Объединить виджеты";
"No available widgets to configure" = "Нет доступных виджетов для настройки";
"No options to configure for the popup in this module" = "Нет параметров для настройки всплывающего окна в этом модуле";
"Process" = "Процесс";
"Kill process" = "Завершить процесс";
"Keyboard shortcut" = "Сочетание клавиш";
"Listening..." = "Слушание...";
// Modules
"Number of top processes" = "Количество процессов";
"Update interval for top processes" = "Интервал обновления процессов";
"Notification level" = "Уровень уведомления";
"Chart color" = "Цвет графика";
"Main chart scaling" = "Масштабирование основного графика";
"Scale value" = "Значение масштаба";
"Text widget value" = "Значение текстового виджета";
// CPU
"CPU usage" = "Использование процессора";
"CPU temperature" = "Температура процессора";
"CPU frequency" = "Частота процессора";
"System" = "Система";
"User" = "Пользователь";
"Idle" = "Свободно";
"Show usage per core" = "Показать использование на ядро";
"Show hyper-threading cores" = "Показать Hyper-Threading ядра";
"Split the value (System/User)" = "Разделить значение (Система/Пользователь)";
"Scheduler limit" = "Ограничение планировщика";
"Speed limit" = "Ограничение скорости";
"Average load" = "Средняя нагрузка";
"1 minute" = "1 минута";
"5 minutes" = "5 минут";
"15 minutes" = "15 минут";
"CPU usage threshold" = "Порог использования процессора";
"CPU usage is" = "Использование процессора %0";
"Efficiency cores" = "Энергоэффективные ядра";
"Performance cores" = "Производительные ядра";
"System color" = "Системный цвет";
"User color" = "Пользовательский цвет";
"Idle color" = "Холостой цвет";
"Cluster grouping" = "Кластерная группировка";
"Efficiency cores color" = "Цвет энергоэффективных ядер";
"Performance cores color" = "Цвет производительных ядер";
"Total load" = "Общая нагрузка";
"System load" = "Системная нагрузка";
"User load" = "Пользовательская нагрузка";
"Efficiency cores load" = "Нагрузка энергоэффективных ядер";
"Performance cores load" = "Нагрузка производительных ядер";
"All cores" = "Все ядра";
// GPU
"GPU to show" = "Активный графический процессор";
"Show GPU type" = "Отображать тип графического процессора";
"GPU enabled" = "Включен";
"GPU disabled" = "Отключен";
"GPU temperature" = "Температура графического процессора";
"GPU utilization" = "Использование графического процессора";
"Vendor" = "Производитель";
"Model" = "Модель";
"Status" = "Статус";
"Active" = "Активная";
"Non active" = "Не активная";
"Fan speed" = "Скорость вентилятора";
"Core clock" = "Частота ядра";
"Memory clock" = "Частота памяти";
"Utilization" = "Использование";
"Render utilization" = "Использование render";
"Tiler utilization" = "Использование tiler";
"GPU usage threshold" = "Порог использования графического процессора";
"GPU usage is" = "Использование графического процессора %0";
// RAM
"Memory usage" = "Нагрузка на память";
"Memory pressure" = "Уровень нагрузки";
"Total" = "Всего";
"Used" = "Используется";
"App" = "Программы";
"Wired" = "Зарезервированная";
"Compressed" = "Сжатая";
"Free" = "Свободная";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Разделить значение (Программы/Зарезервированная/Сжатая)";
"RAM utilization threshold" = "Порог использования оперативной памяти";
"RAM utilization is" = "Использование оперативной памяти %0";
"App color" = "Цвет приложения";
"Wired color" = "Цвет зарезервированной памяти";
"Compressed color" = "Цвет сжатой памяти";
"Free color" = "Цвет свободной памяти";
"Free memory (less than)" = "Свободная память (менее чем)";
"Swap size" = "Размер swap";
"Free RAM is" = "Свободная оперативная память %0";
// Disk
"Show removable disks" = "Показать съемные диски";
"Used disk memory" = "Использовано %0 из %1";
"Free disk memory" = "Свободно %0 из %1";
"Disk to show" = "Активный диск";
"Open disk" = "Открытый диск";
"Switch view" = "Переключить вид";
"Disk utilization threshold" = "Порог использования диска";
"Disk utilization is" = "Использование диска %0";
"Read color" = "Цвет чтения";
"Write color" = "Цвет записи";
"Disk usage" = "Использование диска";
"Total read" = "Всего прочитано";
"Total written" = "Всего записано";
"Write speed" = "Запись";
"Read speed" = "Чтение";
"Drives" = "Диски";
"SMART data" = "SMART данные";
// Sensors
"Temperature unit" = "Единица измерения температуры";
"Celsius" = "Цельсия";
"Fahrenheit" = "Фаренгейта";
"Save the fan speed" = "Сохранить скорость вентилятора";
"Fan" = "Вентилятор";
"HID sensors" = "HID датчики";
"Synchronize fan's control" = "Синхронизировать управление вентиляторами";
"Current" = "Ток";
"Energy" = "Энергия";
"Show unknown sensors" = "Показать неизвестные датчики";
"Install fan helper" = "Установить помощник для вентилятора";
"Uninstall fan helper" = "Удалить помощник для вентилятора";
"Fan value" = "Значение вентилятора";
"Turn off fan" = "Выключить вентилятор";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Вы собираетесь выключить вентилятор. Это не рекомендуемое действие, которое может сломать ваш Mac. Вы уверены, что хотите это сделать?";
"Sensor threshold" = "Порог датчика";
"Left fan" = "Левый";
"Right fan" = "Правый";
"Fastest fan" = "Самый быстрый";
"Sensor to show" = "Активный датчик";
// Network
"Uploading" = "Выгрузка";
"Downloading" = "Загрузка";
"Public IP" = "Публичный IP";
"Local IP" = "Локальный IP";
"Interface" = "Интерфейс";
"Physical address" = "Физический адрес";
"Refresh" = "Обновить";
"Click to copy public IP address" = "Нажмите, чтобы скопировать публичный IP";
"Click to copy local IP address" = "Нажмите, чтобы скопировать локальный IP";
"Click to copy wifi name" = "Нажмите, чтобы скопировать имя Wi-Fi";
"Click to copy mac address" = "Нажмите, чтобы скопировать физический адрес";
"No connection" = "Нет соединения";
"Network interface" = "Сетевой интерфейс";
"Total download" = "Всего скачано";
"Total upload" = "Всего выгружено";
"Reader type" = "Метод чтения";
"Interface based" = "Интерфейс";
"Processes based" = "Процессы";
"Reset data usage" = "Сбросить использование данных";
"VPN mode" = "Режим VPN";
"Standard" = "Стандарт";
"Security" = "Шифрование";
"Channel" = "Канал";
"Common scale" = "Общий масштаб";
"Autodetection" = "Автоопределение";
"Widget activation threshold" = "Порог активации виджета";
"Internet connection" = "Интернет-соединение";
"Active state color" = "Цвет активного состояния";
"Nonactive state color" = "Цвет неактивного состояния";
"Connectivity host (ICMP)" = "Хост подключения (ICMP)";
"Leave empty to disable the check" = "Оставьте пустым, чтобы отключить проверку";
"Connectivity history" = "История подключения";
"Auto-refresh public IP address" = "Автоматическое обновление IP-адреса";
"Every hour" = "Каждый час";
"Every 12 hours" = "Каждые 12 часов";
"Every 24 hours" = "Каждые 24 часа";
"Network activity" = "Сетевая активность";
"Last reset" = "Последний сброс %0 назад";
"Latency" = "Задержка";
"Upload speed" = "Выгрузка";
"Download speed" = "Загрузка";
"Address" = "Адрес";
"WiFi network" = "Сеть Wi-Fi";
"Local IP changed" = "Локальный IP-адрес был изменён";
"Public IP changed" = "Публичный IP-адрес был изменён";
"Previous IP" = "Предыдущий IP-адрес: %0";
"New IP" = "Новый IP-адрес: %0";
"Internet connection lost" = "Соединение с интернетом потеряно";
"Internet connection established" = "Соединение с интернетом восстановлено";
// Battery
"Level" = "Уровень заряда";
"Source" = "Источник";
"AC Power" = "Сеть";
"Battery Power" = "Аккумулятор";
"Time" = "Время";
"Health" = "Состояние";
"Amperage" = "Сила тока";
"Voltage" = "Напряжение";
"Cycles" = "Количество циклов";
"Temperature" = "Температура";
"Power adapter" = "Блок питания";
"Power" = "Мощность";
"Is charging" = "Заряжается";
"Time to discharge" = "Время до разрядки";
"Time to charge" = "Время до зарядки";
"Calculating" = "Вычисления";
"Fully charged" = "Полностью заряжена";
"Not connected" = "Не подключено";
"Low level notification" = "Сообщение о низком уровне заряда";
"High level notification" = "Сообщение о высоком уровне заряда";
"Low battery" = "Низкий заряд аккумулятора";
"High battery" = "Высокий заряд аккумулятора";
"Battery remaining" = "%0% осталось";
"Battery remaining to full charge" = "%0% до полной зарядки";
"Percentage" = "Проценты";
"Percentage and time" = "Проценты и время";
"Time and percentage" = "Время и проценты";
"Time format" = "Формат времени";
"Hide additional information when full" = "Скрыть дополнительную информацию при полном заряде";
"Last charge" = "Последняя зарядка";
"Capacity" = "Емкость";
"current / maximum / designed" = "текущая / максимальная / расчетная";
"Low power mode" = "Режим пониженного энергопотребления";
"Percentage inside the icon" = "Процент внутри виджета";
"Colorize battery" = "Раскрасить батарею";
"Charging current" = "Ток зарядки";
"Charging Voltage" = "Напряжение зарядки";
"Charger state inside the battery" = "Состояние зарядки внутри аккумулятора";
// Bluetooth
"Battery to show" = "Активная батарея";
"No Bluetooth devices are available" = "Нет доступных устройств Bluetooth";
// Clock
"Time zone" = "Часовой пояс";
"Local" = "Локальный";
"Calendar" = "Календарь";
"Show week numbers" = "Показать номер недели";
"Local time" = "Местное время";
"Add new clock" = "Добавить новые часы";
"Delete selected clock" = "Удалить выбранные часы";
"Help with datetime format" = "Помощь с форматом даты и времени";
// Colors
"Based on utilization" = "На основе использования";
"Based on pressure" = "На основании давления";
"Based on cluster" = "На основе кластера";
"System accent" = "Цвет системы";
"Monochrome accent" = "Монохромный акцент";
"Clear" = "Прозрачный";
"White" = "Белый";
"Black" = "Черный";
"Gray" = "серый";
"Second gray" = "Другой серый";
"Dark gray" = "Темно-серый";
"Light gray" = "Светло-серый";
"Red" = "Красный";
"Second red" = "Другой красный";
"Green" = "Зеленый";
"Second green" = "Другой зеленый";
"Blue" = "Голубой";
"Second blue" = "Другой голубой";
"Yellow" = "Желтый";
"Second yellow" = "Другой желтый";
"Orange" = "Оранжевый";
"Second orange" = "Другой оранжевый";
"Purple" = "Фиолетовый";
"Second purple" = "Другой фиолетовый";
"Brown" = "Коричневый";
"Second brown" = "Другой коричневый";
"Cyan" = "Циан";
"Magenta" = "Пурпурный";
"Pink" = "Розовый";
"Teal" = "Бирюзовый";
"Indigo" = "Индиго";
================================================
FILE: Stats/Supporting Files/sk.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Martin Bernat on 02/03/2023.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "Procesor"; // translategemma:4b
"Open CPU settings" = "Otvoriť nastavenie CPU";
"GPU" = "Grafický procesor"; // translategemma:4b
"Open GPU settings" = "Otvoriť nastavenie GPU";
"RAM" = "RAM";
"Open RAM settings" = "Otvoriť nastavenie RAM";
"Disk" = "Disk";
"Open Disk settings" = "Otvoriť nastavenie disku";
"Sensors" = "Senzory";
"Open Sensors settings" = "Otvoriť nastavenie senzorov";
"Network" = "Sieť";
"Open Network settings" = "Otvoriť nastavenie siete";
"Battery" = "Batéria";
"Open Battery settings" = "Otvoriť nastavenie batérie";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Otvoriť nastavenie bluetooth";
"Clock" = "Hodiny"; // translategemma:4b
"Open Clock settings" = "Otvorte nastavenia hodiniek"; // translategemma:4b
// Words
"Unknown" = "Neznámy";
"Version" = "Verzie";
"Processor" = "Procesor";
"Memory" = "Pamäť";
"Graphics" = "Grafika";
"Close" = "Zavrieť";
"Download" = "Stiahnuť";
"Install" = "Inštalovať";
"Cancel" = "Zrušiť";
"Unavailable" = "Nedostupné";
"Yes" = "Áno";
"No" = "Nie";
"Automatic" = "Automaticky";
"Manual" = "Manuálne";
"None" = "Žiadny";
"Dots" = "Bodky";
"Arrows" = "Šípky";
"Characters" = "Znak";
"Short" = "Krátky";
"Long" = "Dlhý";
"Statistics" = "Štatistiky";
"Max" = "Max";
"Min" = "Min";
"Reset" = "Reset";
"Alignment" = "Zarovnanie";
"Left alignment" = "Zarovnať doľava";
"Center alignment" = "Zarovnať na stred";
"Right alignment" = "Zarovnať doprava";
"Dashboard" = "Ovládací panel";
"Enabled" = "Povolené";
"Disabled" = "Zakázané";
"Silent" = "Tichý";
"Units" = "Jednotky";
"Fans" = "Ventilátory";
"Scaling" = "Mierka";
"Linear" = "Lineárna";
"Square" = "Štvorcová";
"Cube" = "Kubická";
"Logarithmic" = "Logaritmická";
"Fixed scale" = "Opravené"; // translategemma:4b
"Cores" = "Jadrá";
"Settings" = "Nastavenia";
"Name" = "Meno"; // translategemma:4b
"Format" = "Formát"; // translategemma:4b
"Turn off" = "Vypnúť"; // translategemma:4b
"Normal" = "Normálny"; // translategemma:4b
"Warning" = "Varovanie"; // translategemma:4b
"Critical" = "Kritický"; // translategemma:4b
"Usage" = "Použitie"; // translategemma:4b
"2 minutes" = "2 minúty"; // translategemma:4b
"3 minutes" = "3 minúty"; // translategemma:4b
"10 minutes" = "10 minút"; // translategemma:4b
"Import" = "Import";
"Export" = "Export";
"Separator" = "Rozdeľovač"; // translategemma:4b
"Read" = "Načítajte"; // translategemma:4b
"Write" = "Napište"; // translategemma:4b
"Frequency" = "Frekvencia"; // translategemma:4b
"Save" = "Uložiť"; // translategemma:4b
"Run" = "Spusti"; // translategemma:4b
"Stop" = "Zastavte"; // translategemma:4b
"Uninstall" = "Odstrániť"; // translategemma:4b
"1 sec" = "1 sekundu"; // translategemma:4b
"2 sec" = "2 sekundy"; // translategemma:4b
"3 sec" = "3 sekundy"; // translategemma:4b
"5 sec" = "5 sekúnd"; // translategemma:4b
"10 sec" = "10 sekúnd"; // translategemma:4b
"15 sec" = "15 sekúnd"; // translategemma:4b
"30 sec" = "30 sekúnd"; // translategemma:4b
"60 sec" = "60 sekúnd"; // translategemma:4b
// Setup
"Stats Setup" = "Nastavenie Stats";
"Previous" = "Späť";
"Previous page" = "Predchádzajúca strana";
"Next" = "Ďalej";
"Next page" = "Ďalšia strana";
"Finish" = "Dokončiť";
"Finish setup" = "Dokončiť nastavenie";
"Welcome to Stats" = "Vitajte v Stats";
"welcome_message" = "Ďakujeme, že používate Stats, bezplatný open source monitor systému macOS.";
"Start the application automatically when starting your Mac" = "Automaticky spustiť aplikáciu pri štarte systému";
"Do not start the application automatically when starting your Mac" = "Nespúšťať aplikáciu automaticky pri štarte systému";
"Do everything silently in the background (recommended)" = "Vykonávať všetko v tichosti na pozadí (odoporúčané)";
"Check for a new version on startup" = "Kontrola novej verzie pri spustení";
"Check for a new version every day (once a day)" = "Kontrola novej verzie každý deň (raz denne)";
"Check for a new version every week (once a week)" = "Kontrola novej verzie každý týždeň (raz týždenne)";
"Check for a new version every month (once a month)" = "Kontrola novej verzie každý mesiac (raz mesačne)";
"Never check for updates (not recommended)" = "Nikdy nekontrolovať aktualizácie (neodporúča sa)";
"Anonymous telemetry for better development decisions" = "Anonymizované telemetrie pre lepšie rozhodovanie pri vývoji"; // translategemma:4b
"Share anonymous telemetry data" = "Zverejni anonymizované údaje o telemetrii"; // translategemma:4b
"Do not share anonymous telemetry data" = "Nezdieľte anonymné údaje z telemetrie."; // translategemma:4b
"The configuration is completed" = "Konfigurácia je dokončená";
"finish_setup_message" = "Všetko je pripravené! \n Stats je open source nástroj, který je zadarmo a vždy bude. \n Ak sa vám páči, môžete projekt podporiť, ceníme si to!";
// Alerts
"New version available" = "Je k dispozícii nová verzia";
"Click to install the new version of Stats" = "Inštalovať novú verziu Stats";
"Successfully updated" = "Úspešne aktualizované";
"Stats was updated to v" = "Stats bol aktualizovaný na v%0";
"Reset settings text" = "Všetky nastavenia aplikácie budú zresetované a aplikácia bude reštartovaná. Ste si istý, že chcete pokračovať?";
"Support text" = "Ďakujeme, že používate Stats!\n\n Udržiavanie a zlepšovanie tohto open-source projektu si vyžaduje čas a zdroje. Vaša podpora nám pomáha pokračovať v poskytovaní bezplatnej a spoľahlivej aplikácie pre každého.\n\nAk považujete Stats za užitočné, zvážte prosím možnosť prispieť. Každý malý kúsok pomôže!";
// Settings
"Open Activity Monitor" = "Otvoriť Monitor aktivity";
"Report a bug" = "Nahlásiť chybu";
"Support the application" = "Podporte aplikáciu";
"Close application" = "Zavrieť aplikáciu";
"Open application settings" = "Otvoriť nastavenie aplikácie";
"Open dashboard" = "Otvoriť ovládací panel";
"No notifications available in this module" = "V tomto module nie sú k dispozícii žiadne upozornenia."; // translategemma:4b
"Open Calendar" = "Otvoriť kalendár"; // translategemma:4b
"Toggle the module" = "Zapnúť/vypnúť modul"; // translategemma:4b
// Application settings
"Update application" = "Aktualizovať aplikáciu";
"Check for updates" = "Skontrolovať aktualizácie";
"At start" = "Pri štarte";
"Once per day" = "Raz denne";
"Once per week" = "Raz týždenne";
"Once per month" = "Raz mesačne";
"Never" = "Nikdy";
"Check for update" = "Skontrolovať aktualizáciu";
"Show icon in dock" = "Zobrazovať ikonu v Docku";
"Start at login" = "Spustiť po prihlásení";
"Build number" = "Číslo buildu";
"Import settings" = "Nastavenia importu"; // translategemma:4b
"Export settings" = "Nastavenia exportu"; // translategemma:4b
"Reset settings" = "Obnoviť nastavenia";
"Pause the Stats" = "Pozastaviť Stats";
"Resume the Stats" = "Obnoviť Stats";
"Combined modules" = "Kombinovať moduly";
"Combined details" = "Spojené informácie"; // translategemma:4b
"Spacing" = "Rozostup"; // translategemma:4b
"Share anonymous telemetry" = "Zdieľte anonymné údaje z telemetrie"; // translategemma:4b
"Choose file" = "Vyberte súbor"; // translategemma:4b
"Stress tests" = "Testy na zaťaženie"; // translategemma:4b
// Dashboard
"Serial number" = "Sériové číslo";
"Model identifier" = "Identifikátor modelu"; // translategemma:4b
"Production year" = "Rok výroby"; // translategemma:4b
"Uptime" = "Dostupnosť"; // translategemma:4b
"Number of cores" = "%0 jadier";
"Number of threads" = "%0 vlákien";
"Number of e-cores" = "%0 efektívnych jadier";
"Number of p-cores" = "%0 výkonných jadier";
"Disks" = "Disky"; // translategemma:4b
"Display" = "Zobrazenie"; // translategemma:4b
// Update
"The latest version of Stats installed" = "Bola nainštalovaná najnovšia verzia Stats";
"Downloading..." = "Sťahovanie...";
"Current version: " = "Aktuálna verzia: ";
"Latest version: " = "Najnovšia verzia: ";
// Widgets
"Color" = "Farba";
"Label" = "Popis";
"Box" = "Políčko";
"Frame" = "Orámovanie";
"Value" = "Hodnota";
"Colorize" = "Vyfarbiť";
"Colorize value" = "Vyfarbiť hodnotu";
"Additional information" = "Ďalšie informácie";
"Reverse values order" = "Obrátené poradie hodnôt";
"Base" = "Základ";
"Display mode" = "Režim zobrazenia";
"One row" = "Jeden riadok";
"Two rows" = "Dva riadky";
"Mini widget" = "Mini";
"Line chart widget" = "Čiarový graf";
"Bar chart widget" = "Stĺpcový graf";
"Pie chart widget" = "Koláčový graf";
"Network chart widget" = "Sieťový graf";
"Speed widget" = "Rýchlosť";
"Battery widget" = "Batéria";
"Stack widget" = "Základňa"; // translategemma:4b
"Memory widget" = "Pamäť";
"Static width" = "Pevná šírka";
"Tachometer widget" = "Rýchlomer"; // translategemma:4b
"State widget" = "Stav";
"Text widget" = "Textové pole"; // translategemma:4b
"Battery details widget" = "Widget s informáciami o batérii"; // translategemma:4b
"Show symbols" = "Zobraziť symboly";
"Label widget" = "Popis";
"Number of reads in the chart" = "Počet záznamov v grafe";
"Color of download" = "Farba sťahovania (download)";
"Color of upload" = "Farba nahrávania (upload)";
"Monospaced font" = "Nerozložené písmo";
"Reverse order" = "V opačnom poradí"; // translategemma:4b
"Chart history" = "História grafu"; // translategemma:4b
"Default color" = "Výchozí"; // translategemma:4b
"Transparent when no activity" = "Pri absencii aktivity je zobrazené ako transparentné."; // translategemma:4b
"Constant color" = "Konštant"; // translategemma:4b
// Module Kit
"Open module settings" = "Otvoriť nastavenia modulu";
"Select widget" = "Vyber widget %0";
"Open widget settings" = "Otvoriť nastavenie widgetu";
"Update interval" = "Interval aktualizácie";
"Usage history" = "História využitia";
"Details" = "Detaily";
"Top processes" = "Top procesy";
"Pictogram" = "Piktogram";
"Module" = "Modul";
"Widgets" = "Miniaplikácie";
"Popup" = "Objaviť sa";
"Notifications" = "Upozornenia";
"Merge widgets" = "Zlúčiť widgety";
"No available widgets to configure" = "Žiadne dostupné widgety na konfiguráciu";
"No options to configure for the popup in this module" = "Žiadne možnosti konfigurácie vyskakovacieho okna v tomto module";
"Process" = "Proces"; // translategemma:4b
"Kill process" = "Zavariť proces"; // translategemma:4b
"Keyboard shortcut" = "Skratka klávesového spáľovania"; // translategemma:4b
"Listening..." = "Poslúchanie..."; // translategemma:4b
// Modules
"Number of top processes" = "Počet top procesov";
"Update interval for top processes" = "Interval aktualizácie pre top procesy";
"Notification level" = "Úroveň upozornenia";
"Chart color" = "Farba grafu";
"Main chart scaling" = "Hlavné škálovanie grafu"; // translategemma:4b
"Scale value" = "Hodnota rozsahu"; // translategemma:4b
"Text widget value" = "Hodnota prvku textového pole"; // translategemma:4b
// CPU
"CPU usage" = "Použitie CPU";
"CPU temperature" = "Teplota CPU";
"CPU frequency" = "Frekvencia CPU";
"System" = "Systém";
"User" = "Používateľ";
"Idle" = "Nečinnosť";
"Show usage per core" = "Zobraziť využitie na jadro";
"Show hyper-threading cores" = "Zobraziť hyper-threading jadrá";
"Split the value (System/User)" = "Rozdeliť hodnotu (Systém/Používateľ)";
"Scheduler limit" = "Limit plánovača";
"Speed limit" = "Limit rýchlosti";
"Average load" = "Priemerné zaťaženie";
"1 minute" = "1 minúta";
"5 minutes" = "5 minút";
"15 minutes" = "15 minút";
"CPU usage threshold" = "Prahová hodnota využitia CPU";
"CPU usage is" = "Využitie procesoru je %0";
"Efficiency cores" = "Efektívne jadrá";
"Performance cores" = "Výkonné jadrá";
"System color" = "Systém";
"User color" = "Používateľ";
"Idle color" = "Nečinnosť";
"Cluster grouping" = "Zoskupenie klastrov";
"Efficiency cores color" = "Efektívne jadrá – farba"; // translategemma:4b
"Performance cores color" = "Farba výkonových jadier"; // translategemma:4b
"Total load" = "Celková záťaž"; // translategemma:4b
"System load" = "Zaťaženie systému"; // translategemma:4b
"User load" = "Zátěž pre používateľa"; // translategemma:4b
"Efficiency cores load" = "Jadra pre efektívnu prevádzku sa načítavajú"; // translategemma:4b
"Performance cores load" = "Načítanie výkonových jadier"; // translategemma:4b
"All cores" = "Všetky jadier"; // translategemma:4b
// GPU
"GPU to show" = "Zobraziť GPU";
"Show GPU type" = "Zobraziť typ GPU";
"GPU enabled" = "GPU aktivované";
"GPU disabled" = "GPU deaktivované";
"GPU temperature" = "Teplota GPU";
"GPU utilization" = "Využitie GPU";
"Vendor" = "Dodávateľ";
"Model" = "Štýl"; // translategemma:4b
"Status" = "Status";
"Active" = "Aktívne";
"Non active" = "Neaktívne";
"Fan speed" = "Rýchlosť ventilátora";
"Core clock" = "Takt jadra";
"Memory clock" = "Takt pamäte";
"Utilization" = "Využitie";
"Render utilization" = "Využitie renderovania";
"Tiler utilization" = "Využitie tilingu";
"GPU usage threshold" = "Prahová hodnota využitia GPU";
"GPU usage is" = "Využitie GPU je %0";
// RAM
"Memory usage" = "Využitie pamäte";
"Memory pressure" = "Zaťaženie pamäte";
"Total" = "Celkom";
"Used" = "Použitá";
"App" = "Aplikačná";
"Wired" = "Pevná";
"Compressed" = "Komprimovaná";
"Free" = "Voľná";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Rozdeliť hodnotu (Aplikačná/Pevná/Komprimovaná)";
"RAM utilization threshold" = "Prahová hodnota využitia pamäte";
"RAM utilization is" = "Využitie pamäte je %0";
"App color" = "Aplikačná";
"Wired color" = "Pevná";
"Compressed color" = "Komprimovaná";
"Free color" = "Voľná";
"Free memory (less than)" = "Voľná pamäť (menej ako)"; // translategemma:4b
"Swap size" = "Veľkosť výmeny"; // translategemma:4b
"Free RAM is" = "Využiteľná RAM je %0"; // translategemma:4b
// Disk
"Show removable disks" = "Zobrazovať vymeniteľné disky";
"Used disk memory" = "Použité %0 z %1";
"Free disk memory" = "Voľné %0 z %1";
"Disk to show" = "Zobraziť disk";
"Open disk" = "Otvoriť disk";
"Switch view" = "Prepnúť pohľad";
"Disk utilization threshold" = "Prahová hodnota využitia disku";
"Disk utilization is" = "Využitie disku je %0";
"Read color" = "Čítajte farby"; // translategemma:4b
"Write color" = "Napište farbu"; // translategemma:4b
"Disk usage" = "Použitie disku"; // translategemma:4b
"Total read" = "Celkový počet prečítaných dát"; // translategemma:4b
"Total written" = "Celkové písané"; // translategemma:4b
"Write speed" = "Napište"; // translategemma:4b
"Read speed" = "Čítajte"; // translategemma:4b
"Drives" = "Rýchlosti"; // translategemma:4b
"SMART data" = "Údaje SMART"; // translategemma:4b
// Sensors
"Temperature unit" = "Jednotka teploty";
"Celsius" = "Celcius"; // translategemma:4b
"Fahrenheit" = "Farenhajm"; // translategemma:4b
"Save the fan speed" = "Uložiť rýchlosť ventilátora";
"Fan" = "Ventilátor";
"HID sensors" = "HID senzory";
"Synchronize fan's control" = "Synchronizácia ovládania ventilátora";
"Current" = "Prúd";
"Energy" = "Energia";
"Show unknown sensors" = "Zobraziť neznáme snímače";
"Install fan helper" = "Inštalácia pomocníka pre ventilátor";
"Uninstall fan helper" = "Odinštalácia pomocníka ventilátora";
"Fan value" = "Otáčky ventilátora";
"Turn off fan" = "Vypnite ventilátor"; // translategemma:4b
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Chystáte sa vypnúť ventilátor. Toto nie je odporúčaná akcia, ktorá môže poškodiť váš Mac. Ste si istí, že to chcete urobiť?"; // translategemma:4b
"Sensor threshold" = "Hranica detekčného senzora"; // translategemma:4b
"Left fan" = "Vľavo"; // translategemma:4b
"Right fan" = "Správne"; // translategemma:4b
"Fastest fan" = "Najrýchlejšie"; // translategemma:4b
"Sensor to show" = "Senzor zobrazí"; // translategemma:4b
// Network
"Uploading" = "Nahrávanie (upload)";
"Downloading" = "Sťahovanie (download)";
"Public IP" = "Verejná IP";
"Local IP" = "Lokálna IP";
"Interface" = "Rozhranie";
"Physical address" = "Fyzická adresa";
"Refresh" = "Obnoviť";
"Click to copy public IP address" = "Kliknutím skopírujete verejnú IP adresu";
"Click to copy local IP address" = "Kliknutím skopírujete lokálnu IP adresu";
"Click to copy wifi name" = "Kliknutím skopírujete názov WIFI";
"Click to copy mac address" = "Kliknutím skopírujete adresu MAC";
"No connection" = "Žiadne pripojenie";
"Network interface" = "Sieťové rozhranie";
"Total download" = "Celkom stiahnuté";
"Total upload" = "Celkom odoslané";
"Reader type" = "Typ peehľadu";
"Interface based" = "Podľa rozhrania";
"Processes based" = "Podľa procesov";
"Reset data usage" = "Resetovať využitie dát";
"VPN mode" = "VPN režim";
"Standard" = "Štandard";
"Security" = "Zabezpečenie";
"Channel" = "Kanál";
"Common scale" = "Spoločná stupnice";
"Autodetection" = "Automatická detekcia";
"Widget activation threshold" = "Prah aktivácie widgetu";
"Internet connection" = "Pripojenie k internetu";
"Active state color" = "Farba aktívneho stavu";
"Nonactive state color" = "Farba neaktívneho stavu";
"Connectivity host (ICMP)" = "Hostiteľ pripojenia (ICMP)";
"Leave empty to disable the check" = "Ponechajte prázdne, ak chcete vypnúť kontrolu";
"Connectivity history" = "História pripojenosti"; // translategemma:4b
"Auto-refresh public IP address" = "Automatické obnovenie verejnej IP adresy"; // translategemma:4b
"Every hour" = "Každú hodinu"; // translategemma:4b
"Every 12 hours" = "Každých 12 hodín"; // translategemma:4b
"Every 24 hours" = "Každých 24 hodín"; // translategemma:4b
"Network activity" = "Aktivity siete"; // translategemma:4b
"Last reset" = "Posledné zresetovanie bolo pred %0 dňami."; // translategemma:4b
"Latency" = "Latencia"; // translategemma:4b
"Upload speed" = "Nahrať"; // translategemma:4b
"Download speed" = "Stiahnuť"; // translategemma:4b
"Address" = "Adresa"; // translategemma:4b
"WiFi network" = "WiFi sieť"; // translategemma:4b
"Local IP changed" = "Lokálna IP adresa bola zmenená"; // translategemma:4b
"Public IP changed" = "Veľmi verejná IP adresa bola zmenená"; // translategemma:4b
"Previous IP" = "Predchádzajúci IP: %0"; // translategemma:4b
"New IP" = "Nová IP: %0"; // translategemma:4b
"Internet connection lost" = "Ztráta internetového pripojenia"; // translategemma:4b
"Internet connection established" = "Pripojenie k internetu bolo vytvorené"; // translategemma:4b
// Battery
"Level" = "Úroveň";
"Source" = "Zdroj";
"AC Power" = "Napájanie zo siete";
"Battery Power" = "Napájanie z batérie";
"Time" = "Čas";
"Health" = "Zdravie";
"Amperage" = "Prúd";
"Voltage" = "Napätie";
"Cycles" = "Počet cyklov";
"Temperature" = "Teplota";
"Power adapter" = "Sieťový adaptér";
"Power" = "Výkon";
"Is charging" = "Nabíja sa";
"Time to discharge" = "Čas do vybitia";
"Time to charge" = "Čas do nabitia";
"Calculating" = "Počítanie";
"Fully charged" = "Plne nabité";
"Not connected" = "Nepripojené";
"Low level notification" = "Upozornenie pri nízkej úrovni";
"High level notification" = "Upozornenie pri vysokej úrovni";
"Low battery" = "Nízka úroveň batérie";
"High battery" = "Vysoká úroveň batérie";
"Battery remaining" = "Ostáva %0 %";
"Battery remaining to full charge" = "%0% do úplného nabitia";
"Percentage" = "Percentá";
"Percentage and time" = "Percentá a čas";
"Time and percentage" = "Čas a percentá";
"Time format" = "Formát času";
"Hide additional information when full" = "Skryť ďalšie informácie, keď je plne nabitá";
"Last charge" = "Posledné nabíjanie";
"Capacity" = "Kapacita";
"current / maximum / designed" = "aktuálne / maximálne / projektované";
"Low power mode" = "Režim nízkej spotreby";
"Percentage inside the icon" = "Percentá vo vnútri ikony";
"Colorize battery" = "Farbi batériu"; // translategemma:4b
"Charging current" = "Prúd nabíjačky"; // translategemma:4b
"Charging Voltage" = "Napäť pri nabíjaní"; // translategemma:4b
"Charger state inside the battery" = "Stav nabíjačky vo vnútri batérie"; // translategemma:4b
// Bluetooth
"Battery to show" = "Zobraziť batériu";
"No Bluetooth devices are available" = "Nie sú k dispozícii žiadne zariadenia Bluetooth";
// Clock
"Time zone" = "Časový pás"; // translategemma:4b
"Local" = "Lokálny"; // translategemma:4b
"Calendar" = "Kalendár"; // translategemma:4b
"Show week numbers" = "Zobrazi čísla týždňov"; // translategemma:4b
"Local time" = "Miestny čas"; // translategemma:4b
"Add new clock" = "Pridajte nové hodiny"; // translategemma:4b
"Delete selected clock" = "Odstrániť vybraný časovač"; // translategemma:4b
"Help with datetime format" = "Pomoc s formátovaním dát a času"; // translategemma:4b
// Colors
"Based on utilization" = "Na základe využívania";
"Based on pressure" = "Na základe vyťaženia";
"Based on cluster" = "Na základe klastra";
"System accent" = "Systémový akcent";
"Monochrome accent" = "Čiernobiely akcent";
"Clear" = "Priehľadná";
"White" = "Biela";
"Black" = "Čierna";
"Gray" = "Sivá";
"Second gray" = "Alternatívna sivá";
"Dark gray" = "Tmavosivá";
"Light gray" = "Svetlosivá";
"Red" = "Červená";
"Second red" = "Alternatívna červená";
"Green" = "Zelená";
"Second green" = "Alternatívna zelená";
"Blue" = "Modrá";
"Second blue" = "Alternatívna modrá";
"Yellow" = "Žltá";
"Second yellow" = "Alternatívna žltá";
"Orange" = "Oranžová";
"Second orange" = "Alternatívna oranžová";
"Purple" = "Fialová";
"Second purple" = "Alternatívna fialová";
"Brown" = "Hnedá";
"Second brown" = "Alternatívna hnedá";
"Cyan" = "Tyrkysová";
"Magenta" = "Purpurová";
"Pink" = "Ružová";
"Teal" = "Sivozelená";
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/sl.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Žiga Povhe (zigapovhe) on 14/05/2022.
// Using Swift 5.0.
// Running on macOS 12.3.1.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPE";
"Open CPU settings" = "Odpri CPE nastavitve";
"GPU" = "GPE";
"Open GPU settings" = "Odpri GPE nastavitve";
"RAM" = "RAM";
"Open RAM settings" = "Odpri nastavitve pomnilnika";
"Disk" = "Disk";
"Open Disk settings" = "Odpri nastavitve diska";
"Sensors" = "Senzorji";
"Open Sensors settings" = "Odpri nastavitve senzorjev";
"Network" = "Omrežje";
"Open Network settings" = "Odpri mrežne nastavitve";
"Battery" = "Baterija";
"Open Battery settings" = "Odpri nastavitve baterije";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Odpri Bluetooth nastavitve";
"Clock" = "Ura";
"Open Clock settings" = "Odpri nastavitve ure";
// Words
"Unknown" = "Neznano";
"Version" = "Različica";
"Processor" = "Procesor";
"Memory" = "Pomnilnik";
"Graphics" = "Grafika";
"Close" = "Zapri";
"Download" = "Prenos";
"Install" = "Namesti";
"Cancel" = "Prekliči";
"Unavailable" = "Ni na voljo";
"Yes" = "Da";
"No" = "Ne";
"Automatic" = "Samodejno";
"Manual" = "Ročno";
"None" = "Nič";
"Dots" = "Pike";
"Arrows" = "Puščice";
"Characters" = "Znaki";
"Short" = "Kratek";
"Long" = "Dolg";
"Statistics" = "Statistika";
"Max" = "Največ";
"Min" = "Najmanj";
"Reset" = "Ponastavi";
"Alignment" = "Poravnava";
"Left alignment" = "Leva poravnava";
"Center alignment" = "Sredinska poravnava";
"Right alignment" = "Desna poravnava";
"Dashboard" = "Nadzorna plošča";
"Enabled" = "Omogočeno";
"Disabled" = "Onemogočeno";
"Silent" = "Tiho";
"Units" = "Enote";
"Fans" = "Ventilatorji";
"Scaling" = "Skaliranje";
"Linear" = "Linearno";
"Square" = "Kvadratno";
"Cube" = "Kubično";
"Logarithmic" = "Logaritmično";
"Fixed scale" = "Fiksna skala";
"Cores" = "Jedra";
"Settings" = "Nastavitve";
"Name" = "Ime";
"Format" = "Format";
"Turn off" = "Izklopi";
"Normal" = "Normal";
"Warning" = "Opozorilo";
"Critical" = "Kritično";
"Usage" = "Uporaba";
"2 minutes" = "2 minuti";
"3 minutes" = "3 minute";
"10 minutes" = "10 minut";
"Import" = "Uvoz";
"Export" = "Izvoz";
"Separator" = "Ločilnik";
"Read" = "Branje";
"Write" = "Pisanje";
"Frequency" = "Frekvenca";
"Save" = "Shrani"; // translategemma:4b
"Run" = "Izvedi"; // translategemma:4b
"Stop" = "Prekli"; // translategemma:4b
"Uninstall" = "Odstranitev"; // translategemma:4b
"1 sec" = "1 sekunda"; // translategemma:4b
"2 sec" = "2 sekunde"; // translategemma:4b
"3 sec" = "3 sekunde"; // translategemma:4b
"5 sec" = "5 sekund"; // translategemma:4b
"10 sec" = "10 sekund"; // translategemma:4b
"15 sec" = "15 sekund"; // translategemma:4b
"30 sec" = "30 sekund"; // translategemma:4b
"60 sec" = "60 sekund"; // translategemma:4b
// Setup
"Stats Setup" = "Stats konfigurator";
"Previous" = "Prejšnji";
"Previous page" = "Prejšnja stran";
"Next" = "Naslednji";
"Next page" = "Naslednja stran";
"Finish" = "Zaključek";
"Finish setup" = "Zaključi konfiguracijo";
"Welcome to Stats" = "Dobrodošli v Stats";
"welcome_message" = "Hvala, ker uporabljate Stats, brezplačni odprtokodni sistemski nadzornik sistema macOS za menijsko vrstico.";
"Start the application automatically when starting your Mac" = "Samodejni zagon programa ob zagonu računalnika Mac";
"Do not start the application automatically when starting your Mac" = "Aplikacije ne zaženi samodejno ob zagonu računalnika Mac";
"Do everything silently in the background (recommended)" = "Vse izvedi tiho v ozadju (priporočljivo)";
"Check for a new version on startup" = "Za novo različico preveri ob zagonu";
"Check for a new version every day (once a day)" = "Za novo različico preveri vsak dan (enkrat na dan)";
"Check for a new version every week (once a week)" = "Za novo različico preveri vsak teden (enkrat na teden)";
"Check for a new version every month (once a month)" = "Za novo različico preveri vsak mesec (enkrat na mesec)";
"Never check for updates (not recommended)" = "Nikoli ne preverjaj ali so na voljo nove posodobitve (ni priporočljivo)";
"Anonymous telemetry for better development decisions" = "Anonimna telemetrija za boljše razvojne odločitve";
"Share anonymous telemetry data" = "Deli anonimne podatke telemetrije";
"Do not share anonymous telemetry data" = "Ne deli anonimne podatke telemetrije";
"The configuration is completed" = "Konfiguracija je zaključena";
"finish_setup_message" = "Vse je pripravljeno! \n Stats je odprtokodno orodje katero je brezplačno in vedno bo. \Če vam je všeč, lahko projekt podprete, kar je vedno dobrodošlo!";
// Alerts
"New version available" = "Na voljo je nova različica";
"Click to install the new version of Stats" = "Klikni za namestitev nove različice programa Stats";
"Successfully updated" = "Uspešno posodobljeno";
"Stats was updated to v" = "Program Stats je bil posodobljen na v%0";
"Reset settings text" = "Vse nastavitve aplikacije se bodo ponastavile in aplikacija se bo znova zagnala. Ali ste prepričani, da to storiti?";
"Support text" = "Zahvaljujemo se vam za uporabo Stats!\n\n Vzdrževanje in izboljševanje tega odprtokodnega projekta zahteva čas in sredstva. Vaša podpora nam pomaga še naprej zagotavljati brezplačno in zanesljivo aplikacijo za vsakogar.\n\nČe vam je program Stats koristen, razmislite o prispevku. Vsaka malenkost šteje!";
// Settings
"Open Activity Monitor" = "Odpri monitor dejavnosti";
"Report a bug" = "Prijavi napako";
"Support the application" = "Donacija za aplikacijo";
"Close application" = "Zapri aplikacijo";
"Open application settings" = "Odpri nastavitve aplikacije";
"Open dashboard" = "Odpri nadzorno ploščo";
"No notifications available in this module" = "V tem modulu niso na voljo nobena obvestila";
"Open Calendar" = "Odpri koledar";
"Toggle the module" = "Vklopi/izklopi modul"; // translategemma:4b
// Application settings
"Update application" = "Posodobi aplikacijo";
"Check for updates" = "Preveri za posodobitve";
"At start" = "Ob zagonu";
"Once per day" = "Enkrat na dan";
"Once per week" = "Enkrat na teden";
"Once per month" = "Enkrat na mesec";
"Never" = "Nikoli";
"Check for update" = "Preveri za posodobitev";
"Show icon in dock" = "Prikaži ikono v opravilni vrstici";
"Start at login" = "Zaženi ob prijavi";
"Build number" = "Številka gradnje";
"Import settings" = "Uvozi nastavitve";
"Export settings" = "Izvozi nastavitve";
"Reset settings" = "Ponastavi nastavitve";
"Pause the Stats" = "Začasno zaustavi Stats";
"Resume the Stats" = "Nadaljuj z izvajanjem Stats";
"Combined modules" = "Združeni moduli";
"Combined details" = "Skupni podatki"; // translategemma:4b
"Spacing" = "Razmik";
"Share anonymous telemetry" = "Deli anonimno telemetrijo";
"Choose file" = "Izberite datoteko"; // translategemma:4b
"Stress tests" = "Testiranje na napetost"; // translategemma:4b
// Dashboard
"Serial number" = "Serijska številka";
"Model identifier" = "Identifikacijska oznaka modela";
"Production year" = "Leto proizvodnje";
"Uptime" = "Čas delovanja";
"Number of cores" = "%0 jeder";
"Number of threads" = "%0 niti";
"Number of e-cores" = "%0 učinkovitih jeder";
"Number of p-cores" = "%0 performančnih jeder";
"Disks" = "Diskji"; // translategemma:4b
"Display" = "Prikaz"; // translategemma:4b
// Update
"The latest version of Stats installed" = "Nameščena je najnovejša različica programa Stats";
"Downloading..." = "Prenašanje...";
"Current version: " = "Trenutna različica: ";
"Latest version: " = "Najnovejša različica: ";
// Widgets
"Color" = "Barva";
"Label" = "Oznaka";
"Box" = "Okno";
"Frame" = "Okvir";
"Value" = "Vrednost";
"Colorize" = "Obarvaj";
"Colorize value" = "Obarvaj vrednost";
"Additional information" = "Dodatne informacije";
"Reverse values order" = "Obrnjen vrstni red vrednosti";
"Base" = "Osnova";
"Display mode" = "Način pogleda";
"One row" = "Ena vrstica";
"Two rows" = "Dve vrstici";
"Mini widget" = "Mini";
"Line chart widget" = "Črtni diagram";
"Bar chart widget" = "Stolpčni diagram";
"Pie chart widget" = "Tortni diagram";
"Network chart widget" = "Mrežni diagram";
"Speed widget" = "Hitrost";
"Battery widget" = "Baterija";
"Stack widget" = "Struktura"; // translategemma:4b
"Memory widget" = "Pomnilnik";
"Static width" = "Statična širina";
"Tachometer widget" = "Tahometer";
"State widget" = "Pripomoček stanja";
"Text widget" = "Vrsta nadzora besedila"; // translategemma:4b
"Battery details widget" = "Widget za podrobnosti o bateriji"; // translategemma:4b
"Show symbols" = "Prikaži simbole";
"Label widget" = "Oznaka";
"Number of reads in the chart" = "Število branj v grafu";
"Color of download" = "Barva prenosa";
"Color of upload" = "Barva oddajanja";
"Monospaced font" = "Pisava s fiksno širino";
"Reverse order" = "Obratni vrstni red";
"Chart history" = "Zgodovina grafa";
"Default color" = "Privzeto";
"Transparent when no activity" = "Prosojno, ko ni dejavnosti";
"Constant color" = "Stalna";
// Module Kit
"Open module settings" = "Odpri nastavitve modula";
"Select widget" = "Izberi %0 pripomoček";
"Open widget settings" = "Odpri nastavitve pripomočka";
"Update interval" = "Interval posodabljanja";
"Usage history" = "Zgodovina uporabe";
"Details" = "Podrobnosti";
"Top processes" = "Glavni procesi";
"Pictogram" = "Piktogram";
"Module" = "Modul";
"Widgets" = "Pripomočki";
"Popup" = "Pojavno okno";
"Notifications" = "Obvestila";
"Merge widgets" = "Združevanje pripomočkov";
"No available widgets to configure" = "Ni razpoložljivih pripomočkov za konfiguracijo";
"No options to configure for the popup in this module" = "V tem modulu ni možnosti za konfiguracijo pojavnega okna";
"Process" = "Proces";
"Kill process" = "Ubij proces";
"Keyboard shortcut" = "Bližnjica na tipkovnici";
"Listening..." = "Poslušam..."; // translategemma:4b
// Modules
"Number of top processes" = "Število glavnih procesov";
"Update interval for top processes" = "Interval posodabljanja za glavne procese";
"Notification level" = "Raven za obvestila";
"Chart color" = "Barva grafa";
"Main chart scaling" = "Skaliranje glavnega grafa";
"Scale value" = "Vrednost skale";
"Text widget value" = "Vrednost komponenta \"Text"; // translategemma:4b
// CPU
"CPU usage" = "CPE poraba";
"CPU temperature" = "CPE temperatura";
"CPU frequency" = "CPE frekvenca";
"System" = "Sistem";
"User" = "Uporabnik";
"Idle" = "Brez dela";
"Show usage per core" = "Prikaži porabo na jedro";
"Show hyper-threading cores" = "Pokaži hyper-threading jedra";
"Split the value (System/User)" = "Razdelitev vrednosti (sistem/uporabnik)";
"Scheduler limit" = "Omejitev načrtovalca";
"Speed limit" = "Omejitev hitrosti";
"Average load" = "Povprečna obremenitev";
"1 minute" = "1 minuta";
"5 minutes" = "5 minut";
"15 minutes" = "15 minut";
"CPU usage threshold" = "Prag uporabe CPE";
"CPU usage is" = "Uporaba CPE je %0";
"Efficiency cores" = "Učinkovita jedra";
"Performance cores" = "Performančna jedra";
"System color" = "Sistemska barva";
"User color" = "Uporabniška barva";
"Idle color" = "Barva mirovanja";
"Cluster grouping" = "Združevanje v gruče";
"Efficiency cores color" = "Barva učinkovitih jeder";
"Performance cores color" = "Barva performančnih jeder";
"Total load" = "Skupna obremenitev";
"System load" = "Sistemska obremenitev";
"User load" = "Uporabniška obremenitev";
"Efficiency cores load" = "Obremenitev učinkovitih jeder";
"Performance cores load" = "Obremenitev performančnih jeder";
"All cores" = "Vsa jedra";
// GPU
"GPU to show" = "GPE za prikaz";
"Show GPU type" = "Prikaži vrsto GPE";
"GPU enabled" = "Omogočen GPE";
"GPU disabled" = "Onemogočen GPE";
"GPU temperature" = "GPE temperatura";
"GPU utilization" = "GPE v uporabi";
"Vendor" = "Proizvajalec";
"Model" = "Model";
"Status" = "Stanje";
"Active" = "Aktivno";
"Non active" = "Neaktivno";
"Fan speed" = "Hitrost ventilatorja";
"Core clock" = "Takt jedra";
"Memory clock" = "Takt pomnilnika";
"Utilization" = "Uporaba";
"Render utilization" = "Izkoriščanje renderja";
"Tiler utilization" = "Uporaba ploščic";
"GPU usage threshold" = "Prag uporabe GPE";
"GPU usage is" = "Uporaba GPE je %0";
// RAM
"Memory usage" = "Poraba pomnilnika";
"Memory pressure" = "Pritisk pomnilnika";
"Total" = "Skupno";
"Used" = "Porabljen";
"App" = "Aplikacije";
"Wired" = "Žični";
"Compressed" = "Stisnjen";
"Free" = "Prost";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Razdelitev vrednosti (Aplikacijski/Žičen/Stisjen)";
"RAM utilization threshold" = "Prag izkoriščenosti RAM-a";
"RAM utilization is" = "Izkoriščenost pomnilnika RAM je %0";
"App color" = "Aplikacijska barva";
"Wired color" = "Žična barva";
"Compressed color" = "Stisnjena barva";
"Free color" = "Prosta barva";
"Free memory (less than)" = "Prosti pomnilnik (manj kot)";
"Swap size" = "Swap velikost";
"Free RAM is" = "RAM je prost %0";
// Disk
"Show removable disks" = "Prikaži odstranljive diske";
"Used disk memory" = "%0 od %1 porabljeno";
"Free disk memory" = "%0 od %1 prosto";
"Disk to show" = "Disk za prikaz";
"Open disk" = "Odpri disk";
"Switch view" = "Zamenjaj pogled";
"Disk utilization threshold" = "Prag izkoriščenosti diska";
"Disk utilization is" = "Izkoriščenost diska je %0";
"Read color" = "Barva branja";
"Write color" = "Barva pisanja";
"Disk usage" = "Uporaba diska";
"Total read" = "Skupaj prebrano";
"Total written" = "Skupaj zapisano";
"Write speed" = "Zapisi";
"Read speed" = "Branja";
"Drives" = "Pogoni";
"SMART data" = "Podatki SMART"; // translategemma:4b
// Sensors
"Temperature unit" = "Enota za temperaturo";
"Celsius" = "Celzija";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Shrani hitrost ventilatorja";
"Fan" = "Ventilator";
"HID sensors" = "HID senzorji";
"Synchronize fan's control" = "Sinhroniziraj krmiljenje ventilatorja";
"Current" = "Tok";
"Energy" = "Energija";
"Show unknown sensors" = "Prikaži neznane senzorje";
"Install fan helper" = "Namesti pomočnika za ventilatorje";
"Uninstall fan helper" = "Odstrani pomočnika za ventilatorje";
"Fan value" = "Vrednost ventilatorja";
"Turn off fan" = "Izklopi ventilator";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Izklopili boste ventilator. To ni priporočljivo dejanje, saj lahko poškoduje vaš računalnik. Ali ste prepričani, da želite to storiti?";
"Sensor threshold" = "Prag zaznavanja";
"Left fan" = "Levi ventilator";
"Right fan" = "Desni ventilator";
"Fastest fan" = "Najhitrejši ventilator";
"Sensor to show" = "Senzor za prikaz";
// Network
"Uploading" = "Oddajanje";
"Downloading" = "Prenašanje";
"Public IP" = "Javni IP";
"Local IP" = "Lokalni IP";
"Interface" = "Vmesnik";
"Physical address" = "Fizični naslov";
"Refresh" = "Osveži";
"Click to copy public IP address" = "Klikni za kopiranje javnega IP naslova";
"Click to copy local IP address" = "Klikni za kopiranje lokalnega IP naslova";
"Click to copy wifi name" = "Klikni za kopiranje imena WiFi";
"Click to copy mac address" = "Klikni za kopiranje MAC naslova";
"No connection" = "Ni povezave";
"Network interface" = "Mrežni vmesnik";
"Total download" = "Skupno prenešeno";
"Total upload" = "Skupno oddano";
"Reader type" = "Tip bralnika";
"Interface based" = "Na osnovi vmesnika";
"Processes based" = "Na podlagi procesa";
"Reset data usage" = "Ponastavi porabo podatkov";
"VPN mode" = "VPN način";
"Standard" = "Standard";
"Security" = "Varnost";
"Channel" = "Kanal";
"Common scale" = "Splošna lestvica";
"Autodetection" = "Samodejna zaznava";
"Widget activation threshold" = "Prag aktiviranja pripomočka";
"Internet connection" = "Internetna povezava";
"Active state color" = "Barva aktivnega stanja";
"Nonactive state color" = "Barva neaktivnega stanja";
"Connectivity host (ICMP)" = "Gostitelj za preverjanje povezljivosti (ICMP)";
"Leave empty to disable the check" = "Pusti prazno, če želiš onemogočiti preverjanje povezljivosti";
"Connectivity history" = "Zgodovina povezljivosti";
"Auto-refresh public IP address" = "Samodejna osvežitev javnega IP naslova";
"Every hour" = "Vsako uro";
"Every 12 hours" = "Vsakih 12 ur";
"Every 24 hours" = "Vsakih 24 ur";
"Network activity" = "Dejavnost v omrežju";
"Last reset" = "Zadnja ponastavitev pred %0";
"Latency" = "Zakasnitev";
"Upload speed" = "Oddajanje";
"Download speed" = "Prenos";
"Address" = "Naslov";
"WiFi network" = "WiFi omrežje"; // translategemma:4b
"Local IP changed" = "Lokalni IP naslov se je spremenil"; // translategemma:4b
"Public IP changed" = "Zunanji IP naslov se je spremenil."; // translategemma:4b
"Previous IP" = "Prejšnji IP: %0"; // translategemma:4b
"New IP" = "Novo IP: %0"; // translategemma:4b
"Internet connection lost" = "Izguba povezave z internetom"; // translategemma:4b
"Internet connection established" = "Ustanovljena je povezava s spletom"; // translategemma:4b
// Battery
"Level" = "Raven";
"Source" = "Vir";
"AC Power" = "Napajanje z izmeničnim tokom";
"Battery Power" = "Napajanje z baterijo";
"Time" = "Čas";
"Health" = "Zdravje";
"Amperage" = "Amperaža";
"Voltage" = "Voltaža";
"Cycles" = "Cikli";
"Temperature" = "Temperatura";
"Power adapter" = "Napajalnik";
"Power" = "Moč";
"Is charging" = "Se polni";
"Time to discharge" = "Čas do prazne baterije";
"Time to charge" = "Čas do polne baterije";
"Calculating" = "Izračunavanje";
"Fully charged" = "Popolnoma napolnjena";
"Not connected" = "Ni priključen";
"Low level notification" = "Obvestilo za nizko raven";
"High level notification" = "Obvestilo za visoko raven";
"Low battery" = "Nizka raven baterije";
"High battery" = "Visoka raven baterije";
"Battery remaining" = "preostane %0%";
"Battery remaining to full charge" = "%0% do polne napolnjenosti";
"Percentage" = "Odstotek";
"Percentage and time" = "Odstotek in čas";
"Time and percentage" = "Čas in odstotek";
"Time format" = "Časovni format";
"Hide additional information when full" = "Skrij dodatne informacije, ko je batarija polna";
"Last charge" = "Zadnje polnjenje";
"Capacity" = "Kapaciteta";
"current / maximum / designed" = "trenutna / največja / predvidena";
"Low power mode" = "Način nizke porabe";
"Percentage inside the icon" = "Odstotek znotraj ikone";
"Colorize battery" = "Obarvanje baterije";
"Charging current" = "Tok polnjenja";
"Charging Voltage" = "Napetost polnjenja";
"Charger state inside the battery" = "Stanje polnila znotraj baterije"; // translategemma:4b
// Bluetooth
"Battery to show" = "Prikaz baterije";
"No Bluetooth devices are available" = "Naprave Bluetooth niso na voljo";
// Clock
"Time zone" = "Časovni pas";
"Local" = "Lokalno";
"Calendar" = "Koledar";
"Show week numbers" = "Prikaži številke tedna"; // translategemma:4b
"Local time" = "Lokalni čas";
"Add new clock" = "Dodajte novo uro"; // translategemma:4b
"Delete selected clock" = "Izbrišite izbran uro"; // translategemma:4b
"Help with datetime format" = "Pomoč pri formiranju datuma in časa"; // translategemma:4b
// Colors
"Based on utilization" = "Na podlagi uporabe";
"Based on pressure" = "Na podlagi pritiska";
"Based on cluster" = "Na podlagi gruč";
"System accent" = "Sistemski poudarek";
"Monochrome accent" = "Enobarvni poudarek";
"Clear" = "Jasna";
"White" = "Bela";
"Black" = "Črna";
"Gray" = "Siva";
"Second gray" = "Druga siva";
"Dark gray" = "Temno siva";
"Light gray" = "Svetlo siva";
"Red" = "Rdeča";
"Second red" = "Druga rdeča";
"Green" = "Zelena";
"Second green" = "Druga zelena";
"Blue" = "Modra";
"Second blue" = "Druga modra";
"Yellow" = "Rumena";
"Second yellow" = "Druga rumena";
"Orange" = "Oranžna";
"Second orange" = "Druga oranžna";
"Purple" = "Vijolična";
"Second purple" = "Druga vijolična";
"Brown" = "Rjava";
"Second brown" = "Druga rjava";
"Cyan" = "Cian"; // translategemma:4b
"Magenta" = "Magenta";
"Pink" = "Roza";
"Teal" = "Teal";
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/sv.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Patrik Persson on 04/01/2025.
// Using Swift 5.0.
// Running on macOS 15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "Öppna CPU-inställningar";
"GPU" = "GPU";
"Open GPU settings" = "Öppna GPU-inställningar";
"RAM" = "RAM";
"Open RAM settings" = "Öppna RAM-inställningar";
"Disk" = "Hårddisk"; // translategemma:4b
"Open Disk settings" = "Öppna diskinställningar";
"Sensors" = "Sensorer";
"Open Sensors settings" = "Öppna sensorinställningar";
"Network" = "Nätverk";
"Open Network settings" = "Öppna nätverksinställningar";
"Battery" = "Batteri";
"Open Battery settings" = "Öppna batteriinställningar";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Öppna Bluetooth-inställningar";
"Clock" = "Klocka";
"Open Clock settings" = "Öppna klockinställningar";
// Words
"Unknown" = "Okänd";
"Version" = "Version";
"Processor" = "Processor";
"Memory" = "Minne";
"Graphics" = "Grafik";
"Close" = "Stäng";
"Download" = "Ladda ner";
"Install" = "Installera";
"Cancel" = "Avbryt";
"Unavailable" = "Ej tillgänglig";
"Yes" = "Ja";
"No" = "Nej";
"Automatic" = "Automatisk";
"Manual" = "Manuell";
"None" = "Ingen";
"Dots" = "Punkter";
"Arrows" = "Pilar";
"Characters" = "Tecken";
"Short" = "Kort";
"Long" = "Lång";
"Statistics" = "Statistik";
"Max" = "Max";
"Min" = "Mitt"; // translategemma:4b
"Reset" = "Återställ";
"Alignment" = "Justering";
"Left alignment" = "Vänster";
"Center alignment" = "Centrerad";
"Right alignment" = "Höger";
"Dashboard" = "Instrumentpanel";
"Enabled" = "Aktiverad";
"Disabled" = "Inaktiverad";
"Silent" = "Tyst";
"Units" = "Enheter";
"Fans" = "Fläktar";
"Scaling" = "Skalning";
"Linear" = "Linjär";
"Square" = "Kvadratisk";
"Cube" = "Kubisk";
"Logarithmic" = "Logaritmisk";
"Fixed scale" = "Fast skala";
"Cores" = "Kärnor";
"Settings" = "Inställningar";
"Name" = "Namn";
"Format" = "Format";
"Turn off" = "Stäng av";
"Normal" = "Normal";
"Warning" = "Varning";
"Critical" = "Kritisk";
"Usage" = "Användning";
"2 minutes" = "2 minuter";
"3 minutes" = "3 minuter";
"10 minutes" = "10 minuter";
"Import" = "Importera";
"Export" = "Exportera";
"Separator" = "Avgränsare";
"Read" = "Läs";
"Write" = "Skriv";
"Frequency" = "Frekvens";
"Save" = "Spara"; // translategemma:4b
"Run" = "Kör"; // translategemma:4b
"Stop" = "Stopp"; // translategemma:4b
"Uninstall" = "Avinstallera"; // translategemma:4b
"1 sec" = "1 sekund"; // translategemma:4b
"2 sec" = "2 sekunder"; // translategemma:4b
"3 sec" = "3 sekunder"; // translategemma:4b
"5 sec" = "5 sekunder"; // translategemma:4b
"10 sec" = "10 sek"; // translategemma:4b
"15 sec" = "15 sek"; // translategemma:4b
"30 sec" = "30 sekunder"; // translategemma:4b
"60 sec" = "60 sekunder"; // translategemma:4b
// Setup
"Stats Setup" = "Stats-installation";
"Previous" = "Föregående";
"Previous page" = "Föregående sida";
"Next" = "Nästa";
"Next page" = "Nästa sida";
"Finish" = "Avsluta";
"Finish setup" = "Avsluta installationen";
"Welcome to Stats" = "Välkommen till Stats";
"welcome_message" = "Tack för att du använder Stats, ett gratis och öppen källkod macOS-systemövervakningsverktyg för din menyrad.";
"Start the application automatically when starting your Mac" = "Starta applikationen automatiskt när du startar din Mac";
"Do not start the application automatically when starting your Mac" = "Starta inte applikationen automatiskt när du startar din Mac";
"Do everything silently in the background (recommended)" = "Gör allt tyst i bakgrunden (rekommenderas)";
"Check for a new version on startup" = "Kontrollera efter en ny version vid uppstart";
"Check for a new version every day (once a day)" = "Kontrollera efter en ny version varje dag (en gång om dagen)";
"Check for a new version every week (once a week)" = "Kontrollera efter en ny version varje vecka (en gång i veckan)";
"Check for a new version every month (once a month)" = "Kontrollera efter en ny version varje månad (en gång i månaden)";
"Never check for updates (not recommended)" = "Kontrollera aldrig efter uppdateringar (rekommenderas inte)";
"Anonymous telemetry for better development decisions" = "Anonym telemetri för bättre utvecklingsbeslut";
"Share anonymous telemetry data" = "Dela anonym telemetridata";
"Do not share anonymous telemetry data" = "Dela inte anonym telemetridata";
"The configuration is completed" = "Konfigurationen är slutförd";
"finish_setup_message" = "Allt är inställt! \n Stats är ett verktyg med öppen källkod, det är gratis och kommer alltid att vara det. \n Om du gillar det kan du stödja projektet, det uppskattas alltid!";
// Alerts
"New version available" = "Ny version tillgänglig";
"Click to install the new version of Stats" = "Klicka för att installera den nya versionen av Stats";
"Successfully updated" = "Uppdateringen lyckades";
"Stats was updated to v" = "Stats uppdaterades till v%0";
"Reset settings text" = "Alla programinställningar kommer att återställas och programmet startas om. Är du säker på att du vill göra detta?";
"Support text" = "Tack för att du använder Stats!\n\n Att underhålla och förbättra detta projekt med öppen källkod tar tid och resurser. Ditt stöd hjälper oss att fortsätta tillhandahålla en gratis och tillförlitlig applikation för alla.\n\nOm du tycker att Stats är användbart kan du överväga att ge ett bidrag. Varje liten bit hjälper!";
// Settings
"Open Activity Monitor" = "Öppna Aktivitetsövervakaren";
"Report a bug" = "Rapportera en bugg";
"Support the application" = "Stöd Stats";
"Close application" = "Stäng applikationen";
"Open application settings" = "Öppna applikationsinställningar";
"Open dashboard" = "Öppna instrumentpanelen";
"No notifications available in this module" = "Inga notifikationer tillgängliga i denna modul";
"Open Calendar" = "Öppna Kalender";
"Toggle the module" = "Aktivera/inaktivera modulen"; // translategemma:4b
// Application settings
"Update application" = "Uppdatera applikationen";
"Check for updates" = "Sök efter uppdateringar";
"At start" = "Vid start";
"Once per day" = "En gång per dag";
"Once per week" = "En gång per vecka";
"Once per month" = "En gång per månad";
"Never" = "Aldrig";
"Check for update" = "Sök efter uppdatering";
"Show icon in dock" = "Visa ikon i Dock";
"Start at login" = "Starta vid inloggning";
"Build number" = "Byggnummer";
"Import settings" = "Importera inställningar";
"Export settings" = "Exportera inställningar";
"Reset settings" = "Återställ inställningar";
"Pause the Stats" = "Pausa Stats";
"Resume the Stats" = "Återuppta Stats";
"Combined modules" = "Kombinerade moduler";
"Combined details" = "Kombinerade detaljer";
"Spacing" = "Mellanrum";
"Share anonymous telemetry" = "Dela anonym telemetri";
"Choose file" = "Välj fil"; // translategemma:4b
"Stress tests" = "Spänningstester"; // translategemma:4b
// Dashboard
"Serial number" = "Serienummer";
"Model identifier" = "Modellidentifierare";
"Production year" = "Tillverkningsår";
"Uptime" = "Upptid";
"Number of cores" = "%0 kärnor";
"Number of threads" = "%0 trådar";
"Number of e-cores" = "%0 effektivitetskärnor";
"Number of p-cores" = "%0 prestandakärnor";
"Disks" = "Skivor"; // translategemma:4b
"Display" = "Visning"; // translategemma:4b
// Update
"The latest version of Stats installed" = "Den senaste versionen av Stats är installerad";
"Downloading..." = "Laddar ner...";
"Current version: " = "Nuvarande version: ";
"Latest version: " = "Senaste version: ";
// Widgets
"Color" = "Färg";
"Label" = "Etikett";
"Box" = "Låda";
"Frame" = "Ram";
"Value" = "Värde";
"Colorize" = "Färglägg";
"Colorize value" = "Färglägg värde";
"Additional information" = "Mer information";
"Reverse values order" = "Växla ordning";
"Base" = "Enhet";
"Display mode" = "Visningsläge";
"One row" = "En rad";
"Two rows" = "Två rader";
"Mini widget" = "Mini";
"Line chart widget" = "Linjediagram";
"Bar chart widget" = "Stapeldiagram";
"Pie chart widget" = "Cirkeldiagram";
"Network chart widget" = "Nätverksdiagram";
"Speed widget" = "Hastighet";
"Battery widget" = "Batteri";
"Stack widget" = "Stack";
"Memory widget" = "Minne";
"Static width" = "Fast bredd";
"Tachometer widget" = "Varvräknare";
"State widget" = "Statwidget";
"Text widget" = "Textwidget"; // translategemma:4b
"Battery details widget" = "Widget för batteriinformation"; // translategemma:4b
"Show symbols" = "Visa symboler";
"Label widget" = "Etikett";
"Number of reads in the chart" = "Antal avläsningar i diagrammet";
"Color of download" = "Färg för nedladdning";
"Color of upload" = "Färg för uppladdning";
"Monospaced font" = "Monospace-typsnitt";
"Reverse order" = "Omvänd ordning";
"Chart history" = "Diagramhistorik";
"Default color" = "Standardfärg";
"Transparent when no activity" = "Genomskinlig vid inaktivitet";
"Constant color" = "Konstant färg";
// Module Kit
"Open module settings" = "Öppna modulinställningar";
"Select widget" = "Välj %0 widget";
"Open widget settings" = "Öppna widgetinställningar";
"Update interval" = "Uppdateringsintervall";
"Usage history" = "Användningshistorik";
"Details" = "Detaljer";
"Top processes" = "Topprocesser";
"Pictogram" = "Piktogram";
"Module" = "Modul";
"Widgets" = "Widgetar"; // translategemma:4b
"Popup" = "Popup";
"Notifications" = "Notifikationer";
"Merge widgets" = "Slå samman widgets";
"No available widgets to configure" = "Inga tillgängliga widgets att konfigurera";
"No options to configure for the popup in this module" = "Inga alternativ att konfigurera för popup-fönstret i denna modul";
"Process" = "Process";
"Kill process" = "Avsluta process";
"Keyboard shortcut" = "Tangentbordsgenväg"; // translategemma:4b
"Listening..." = "Lyssna..."; // translategemma:4b
// Modules
"Number of top processes" = "Antal topprocesser";
"Update interval for top processes" = "Uppdateringsintervall för topprocesser";
"Notification level" = "Notisnivå";
"Chart color" = "Diagramfärg";
"Main chart scaling" = "Huvuddiagramskalning";
"Scale value" = "Skalvärde";
"Text widget value" = "Värdet för textwidget"; // translategemma:4b
// CPU
"CPU usage" = "CPU-användning";
"CPU temperature" = "CPU-temperatur";
"CPU frequency" = "CPU-frekvens";
"System" = "System";
"User" = "Användare";
"Idle" = "Overksam";
"Show usage per core" = "Visa användning per kärna";
"Show hyper-threading cores" = "Visa hypertrådskärnor";
"Split the value (System/User)" = "Dela upp värdet (System/Användare)";
"Scheduler limit" = "Schemaläggargräns";
"Speed limit" = "Hastighetsbegränsning";
"Average load" = "Genomsnittlig belastning";
"1 minute" = "1 minut";
"5 minutes" = "5 minuter";
"15 minutes" = "15 minuter";
"CPU usage threshold" = "Tröskelvärde för CPU-användning";
"CPU usage is" = "CPU-användningen är %0";
"Efficiency cores" = "Effektivitetskärnor";
"Performance cores" = "Prestandakärnor";
"System color" = "Systemfärg";
"User color" = "Användarfärg";
"Idle color" = "Overksamhetsfärg";
"Cluster grouping" = "Klustergruppering";
"Efficiency cores color" = "Effektivitetskärnors färg";
"Performance cores color" = "Prestandakärnors färg";
"Total load" = "Total belastning";
"System load" = "Systembelastning";
"User load" = "Användarbelastning";
"Efficiency cores load" = "Effektivitetskärnors belastning";
"Performance cores load" = "Prestandakärnors belastning";
"All cores" = "Alla kärnor";
// GPU
"GPU to show" = "GPU att visa";
"Show GPU type" = "Visa GPU-typ";
"GPU enabled" = "GPU aktiverad";
"GPU disabled" = "GPU inaktiverad";
"GPU temperature" = "GPU-temperatur";
"GPU utilization" = "GPU-användning";
"Vendor" = "Tillverkare";
"Model" = "Modell";
"Status" = "Status";
"Active" = "Aktiv";
"Non active" = "Inaktiv";
"Fan speed" = "Fläkthastighet";
"Core clock" = "Kärnklocka";
"Memory clock" = "Minnesklocka";
"Utilization" = "Användning";
"Render utilization" = "Render-användning";
"Tiler utilization" = "Tiler-användning";
"GPU usage threshold" = "Tröskelvärde för GPU-användning";
"GPU usage is" = "GPU-användningen är %0";
// RAM
"Memory usage" = "Minnesanvändning";
"Memory pressure" = "Minnestryck";
"Total" = "Totalt";
"Used" = "Använt";
"App" = "Appminne";
"Wired" = "Resident minne";
"Compressed" = "Komprimerat";
"Free" = "Ledigt";
"Swap" = "Växling";
"Split the value (App/Wired/Compressed)" = "Dela upp värdet (App/Resident/Komprimerat)";
"RAM utilization threshold" = "Tröskelvärde för minnesanvändning";
"RAM utilization is" = "Minnesanvändningen är %0";
"App color" = "Appfärg";
"Wired color" = "Resident minnesfärg";
"Compressed color" = "Komprimerat minnesfärg";
"Free color" = "Ledigt minnesfärg";
"Free memory (less than)" = "Ledigt minne (mindre än)";
"Swap size" = "Växlingsstorlek";
"Free RAM is" = "Ledigt RAM är %0";
// Disk
"Show removable disks" = "Visa flyttbara diskar";
"Used disk memory" = "%0 av %1 används";
"Free disk memory" = "%0 av %1 ledigt";
"Disk to show" = "Disk att visa";
"Open disk" = "Öppna disk";
"Switch view" = "Växla vy";
"Disk utilization threshold" = "Tröskelvärde för diskanvändning";
"Disk utilization is" = "Diskanvändningen är %0";
"Read color" = "Läsfärg";
"Write color" = "Skrivfärg";
"Disk usage" = "Diskanvändning";
"Total read" = "Totalt läst";
"Total written" = "Totalt skrivet";
"Write speed" = "Skriv";
"Read speed" = "Läs";
"Drives" = "Drivs"; // translategemma:4b
"SMART data" = "SMART-data"; // translategemma:4b
// Sensors
"Temperature unit" = "Temperaturenhet";
"Celsius" = "Celsius";
"Fahrenheit" = "Fahrenheit";
"Save the fan speed" = "Spara fläkthastighet";
"Fan" = "Fläkt";
"HID sensors" = "HID-sensorer";
"Synchronize fan's control" = "Synkronisera fläktkontroll";
"Current" = "Ström";
"Energy" = "Energi";
"Show unknown sensors" = "Visa okända sensorer";
"Install fan helper" = "Installera fläkthjälpare";
"Uninstall fan helper" = "Avinstallera fläkthjälpare";
"Fan value" = "Fläktvärde";
"Turn off fan" = "Stäng av fläkt";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Du håller på att stänga av fläkten. Detta är en inte rekommenderad åtgärd som kan skada din Mac, är du säker på att du vill göra detta?";
"Sensor threshold" = "Sensortröskel";
"Left fan" = "Vänster";
"Right fan" = "Höger";
"Fastest fan" = "Snabbaste";
"Sensor to show" = "Sensor för att visa"; // translategemma:4b
// Network
"Uploading" = "Uppladdning";
"Downloading" = "Nedladdning";
"Public IP" = "Publik IP";
"Local IP" = "Lokal IP";
"Interface" = "Gränssnitt";
"Physical address" = "Fysisk adress";
"Refresh" = "Uppdatera";
"Click to copy public IP address" = "Klicka för att kopiera publik IP-adress";
"Click to copy local IP address" = "Klicka för att kopiera lokal IP-adress";
"Click to copy wifi name" = "Klicka för att kopiera wifi-namn";
"Click to copy mac address" = "Klicka för att kopiera MAC-adress";
"No connection" = "Ingen anslutning";
"Network interface" = "Nätverksgränssnitt";
"Total download" = "Totalt nedladdat";
"Total upload" = "Totalt uppladdat";
"Reader type" = "Läsartyp";
"Interface based" = "Gränssnittsbaserad";
"Processes based" = "Processbaserad";
"Reset data usage" = "Återställ dataanvändning";
"VPN mode" = "VPN-läge";
"Standard" = "Standard";
"Security" = "Säkerhet";
"Channel" = "Kanal";
"Common scale" = "Gemensam skala";
"Autodetection" = "Autodetektering";
"Widget activation threshold" = "Widgetaktiveringströskel";
"Internet connection" = "Internetanslutning";
"Active state color" = "Aktivt tillståndsfärg";
"Nonactive state color" = "Inaktivt tillståndsfärg";
"Connectivity host (ICMP)" = "Anslutningsvärd (ICMP)";
"Leave empty to disable the check" = "Lämna tomt för att inaktivera kontrollen";
"Connectivity history" = "Anslutningshistorik";
"Auto-refresh public IP address" = "Uppdatera publik IP-adress automatiskt";
"Every hour" = "Varje timme";
"Every 12 hours" = "Var 12:e timme";
"Every 24 hours" = "Var 24:e timme";
"Network activity" = "Nätverksaktivitet";
"Last reset" = "Senaste återställning %0 sedan";
"Latency" = "Fördröjning";
"Upload speed" = "Uppladdning";
"Download speed" = "Nedladdning";
"Address" = "Adress"; // translategemma:4b
"WiFi network" = "WiFi-nätverk"; // translategemma:4b
"Local IP changed" = "Det lokala IP-adressen har ändrats"; // translategemma:4b
"Public IP changed" = "Den publika IP-adressen har ändrats"; // translategemma:4b
"Previous IP" = "Tidigare IP-adress: %0"; // translategemma:4b
"New IP" = "Ny IP-adress: %0"; // translategemma:4b
"Internet connection lost" = "Förlorad internetanslutning"; // translategemma:4b
"Internet connection established" = "Internetanslutning etablerad"; // translategemma:4b
// Battery
"Level" = "Nivå";
"Source" = "Källa";
"AC Power" = "Nätström";
"Battery Power" = "Batteri";
"Time" = "Tid";
"Health" = "Hälsa";
"Amperage" = "Strömstyrka";
"Voltage" = "Spänning";
"Cycles" = "Cyklar";
"Temperature" = "Temperatur";
"Power adapter" = "Strömadapter";
"Power" = "Effekt";
"Is charging" = "Laddar";
"Time to discharge" = "Tid till urladdning";
"Time to charge" = "Tid till fulladdning";
"Calculating" = "Beräknar";
"Fully charged" = "Fulladdad";
"Not connected" = "Ej ansluten";
"Low level notification" = "Låg nivånotis";
"High level notification" = "Hög nivånotis";
"Low battery" = "Lågt batteri";
"High battery" = "Högt batteri";
"Battery remaining" = "%0% kvar";
"Battery remaining to full charge" = "%0% till fulladdning";
"Percentage" = "Procent";
"Percentage and time" = "Procent och tid";
"Time and percentage" = "Tid och procent";
"Time format" = "Tidsformat";
"Hide additional information when full" = "Dölj extra information när fulladdad";
"Last charge" = "Senaste laddning";
"Capacity" = "Kapacitet";
"current / maximum / designed" = "nuvarande / maximal / ursprunglig";
"Low power mode" = "Lågeffektläge";
"Percentage inside the icon" = "Procent inuti ikonen";
"Colorize battery" = "Färglägg batteri";
"Charging current" = "Laddningsström";
"Charging Voltage" = "Laddningsspänning";
"Charger state inside the battery" = "Laddningsstatus inuti batteriet"; // translategemma:4b
// Bluetooth
"Battery to show" = "Batteri att visa";
"No Bluetooth devices are available" = "Inga Bluetooth-enheter tillgängliga";
// Clock
"Time zone" = "Tidszon";
"Local" = "Lokal";
"Calendar" = "Kalender";
"Show week numbers" = "Visa veknummer"; // translategemma:4b
"Local time" = "Lokal tid";
"Add new clock" = "Lägg till en ny klocka"; // translategemma:4b
"Delete selected clock" = "Ta bort vald klocka"; // translategemma:4b
"Help with datetime format" = "Hjälp med att formatera datum och tid"; // translategemma:4b
// Colors
"Based on utilization" = "Baserat på användning";
"Based on pressure" = "Baserat på tryck";
"Based on cluster" = "Baserat på kluster";
"System accent" = "Systemets accent";
"Monochrome accent" = "Svartvit accent";
"Clear" = "Genomskinlig";
"White" = "Vit";
"Black" = "Svart";
"Gray" = "Grå";
"Second gray" = "Alternativ grå";
"Dark gray" = "Mörkgrå";
"Light gray" = "Ljusgrå";
"Red" = "Röd";
"Second red" = "Alternativ röd";
"Green" = "Grön";
"Second green" = "Alternativ grön";
"Blue" = "Blå";
"Second blue" = "Alternativ blå";
"Yellow" = "Gul";
"Second yellow" = "Alternativ gul";
"Orange" = "Orange";
"Second orange" = "Alternativ orange";
"Purple" = "Lila";
"Second purple" = "Alternativ lila";
"Brown" = "Brun";
"Second brown" = "Alternativ brun";
"Cyan" = "Cyan";
"Magenta" = "Magenta";
"Pink" = "Rosa";
"Teal" = "Turkos";
"Indigo" = "Indigo";
================================================
FILE: Stats/Supporting Files/th.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "ซีพียู"; // translategemma:4b
"Open CPU settings" = "เปิดการตั้งค่า CPU";
"GPU" = "หน่วยประมวลผลกราฟิก (GPU)"; // translategemma:4b
"Open GPU settings" = "เปิดการตั้งค่า GPU";
"RAM" = "RAM";
"Open RAM settings" = "เปิดการตั้งค่า RAM";
"Disk" = "ดิสก์";
"Open Disk settings" = "เปิดการตั้งค่าดิสก์";
"Sensors" = "เซ็นเซอร์";
"Open Sensors settings" = "เปิดการตั้งค่าเซ็นเซอร์";
"Network" = "เครือข่าย";
"Open Network settings" = "เปิดการตั้งค่าเครือข่าย";
"Battery" = "แบตเตอรี่";
"Open Battery settings" = "เปิดการตั้งค่าแบตเตอรี่";
"Bluetooth" = "บลูทูธ";
"Open Bluetooth settings" = "เปิดการตั้งค่าบลูทูธ";
"Clock" = "นาฬิกา"; // translategemma:4b
"Open Clock settings" = "เปิดการตั้งค่านาฬิกา"; // translategemma:4b
// Words
"Unknown" = "ไม่ทราบที่มา";
"Version" = "เวอร์ชัน";
"Processor" = "หน่วยประมวลผล";
"Memory" = "หน่วยความจำ";
"Graphics" = "กราฟิก";
"Close" = "ปิด";
"Download" = "ดาวน์โหลด";
"Install" = "ติดตั้ง";
"Cancel" = "ยกเลิก";
"Unavailable" = "ไม่สามารถใช้งานได้";
"Yes" = "ใช่";
"No" = "ไม่";
"Automatic" = "อัตโนมัติ";
"Manual" = "ด้วยตนเอง";
"None" = "ไม่มี";
"Dots" = "จุด";
"Arrows" = "ลูกศร";
"Characters" = "ตัวอักษร";
"Short" = "สั้น";
"Long" = "ยาว";
"Statistics" = "สถิติ";
"Max" = "สูงสุด";
"Min" = "ต่ำสุด";
"Reset" = "รีเซ็ต";
"Alignment" = "การจัดแนว";
"Left alignment" = "จัดแนวซ้าย";
"Center alignment" = "จัดแนวกึ่งกลาง";
"Right alignment" = "จัดแนวขวา";
"Dashboard" = "แดชบอร์ด";
"Enabled" = "เปิดใช้งาน";
"Disabled" = "ปิดใช้งาน";
"Silent" = "ไม่เสียง";
"Units" = "หน่วย";
"Fans" = "พัดลม";
"Scaling" = "ขนาด";
"Linear" = "เชิงเส้น";
"Square" = "สี่เหลี่ยม";
"Cube" = "ลูกบาศก์";
"Logarithmic" = "ลอการิทึม";
"Fixed scale" = "แก้ไขแล้ว"; // translategemma:4b
"Cores" = "หน่วยความจำ";
"Settings" = "การตั้งค่า";
"Name" = "ชื่อ"; // translategemma:4b
"Format" = "รูปแบบ"; // translategemma:4b
"Turn off" = "ปิด"; // translategemma:4b
"Normal" = "ปกติ"; // translategemma:4b
"Warning" = "คำเตือน"; // translategemma:4b
"Critical" = "สำคัญ"; // translategemma:4b
"Usage" = "การใช้งาน"; // translategemma:4b
"2 minutes" = "2 นาที"; // translategemma:4b
"3 minutes" = "3 นาที"; // translategemma:4b
"10 minutes" = "10 นาที"; // translategemma:4b
"Import" = "นำเข้า"; // translategemma:4b
"Export" = "ส่งออก"; // translategemma:4b
"Separator" = "ตัวแบ่ง"; // translategemma:4b
"Read" = "อ่าน"; // translategemma:4b
"Write" = "เขียน"; // translategemma:4b
"Frequency" = "ความถี่"; // translategemma:4b
"Save" = "บันทึก"; // translategemma:4b
"Run" = "รัน"; // translategemma:4b
"Stop" = "หยุด"; // translategemma:4b
"Uninstall" = "ถอนการติดตั้ง"; // translategemma:4b
"1 sec" = "1 วินาที"; // translategemma:4b
"2 sec" = "2 วินาที"; // translategemma:4b
"3 sec" = "3 วินาที"; // translategemma:4b
"5 sec" = "5 วินาที"; // translategemma:4b
"10 sec" = "10 วินาที"; // translategemma:4b
"15 sec" = "15 วินาที"; // translategemma:4b
"30 sec" = "30 วินาที"; // translategemma:4b
"60 sec" = "60 วินาที"; // translategemma:4b
// Setup
"Stats Setup" = "การตั้งค่าสถิติ";
"Previous" = "ก่อนหน้า";
"Previous page" = "หน้าก่อนหน้า";
"Next" = "ถัดไป";
"Next page" = "หน้าถัดไป";
"Finish" = "สิ้นสุด";
"Finish setup" = "สิ้นสุดการตั้งค่า";
"Welcome to Stats" = "ยินดีต้อนรับเข้าสู่ Stats";
"welcome_message" = "ขอบคุณที่ใช้ Stats ซอฟต์แวร์แบบโอเพ่นซอร์สและใช้งานฟรีเพื่อตรวจสอบระบบ Mac ของคุณ";
"Start the application automatically when starting your Mac" = "เริ่มต้นแอปพลิเคชันโดยอัตโนมัติเมื่อเปิดเครื่อง Mac ของคุณ";
"Do not start the application automatically when starting your Mac" = "ไม่ต้องเริ่มแอปพลิเคชันโดยอัตโนมัติเมื่อเปิดเครื่อง Mac ของคุณ";
"Do everything silently in the background (recommended)" = "ทำทุกอย่างเงียบ ๆ ในแบคกราว (แนะนำ)";
"Check for a new version on startup" = "ตรวจสอบเวอร์ชันใหม่เมื่อเริ่มต้น";
"Check for a new version every day (once a day)" = "ตรวจสอบเวอร์ชันใหม่ทุกวัน (วันละครั้ง)";
"Check for a new version every week (once a week)" = "ตรวจสอบเวอร์ชันใหม่ทุกสัปดาห์ (สัปดาห์ละครั้ง)";
"Check for a new version every month (once a month)" = "ตรวจสอบเวอร์ชันใหม่ทุกเดือน (เดือนละครั้ง)";
"Never check for updates (not recommended)" = "ไม่เช็คการอัปเดต (ไม่แนะนำ)";
"Anonymous telemetry for better development decisions" = "ข้อมูล telemetry ที่ไม่ระบุชื่อ เพื่อการตัดสินใจในการพัฒนาที่ดีขึ้น"; // translategemma:4b
"Share anonymous telemetry data" = "แบ่งปันข้อมูล telemetry ที่ไม่ระบุตัวตน"; // translategemma:4b
"Do not share anonymous telemetry data" = "ห้ามเปิดเผยข้อมูล telemetry ที่ไม่ระบุตัวตน"; // translategemma:4b
"The configuration is completed" = "การกำหนดค่าเสร็จสมบูรณ์";
"finish_setup_message" = "ทุกอย่างถูกตั้งค่า! \n Stats เป็นเครื่องมือโอเพ่นซอร์สที่ฟรีและจะเป็นแบบนี้ตลอดไป \n ถ้าคุณชอบ คุณสามารถสนับสนุนโปรเจคนี้ เราพร้อมรับและขอบคุณเสมอ!";
// Alerts
"New version available" = "มีเวอร์ชันใหม่ให้ใช้งาน";
"Click to install the new version of Stats" = "คลิกเพื่อติดตั้งเวอร์ชันใหม่ของ Stats";
"Successfully updated" = "อัพเดทสำเร็จ";
"Stats was updated to v" = "Stats อัพเดทเป็น v%0";
"Reset settings text" = "การตั้งค่าของแอพพลิเคชันทั้งหมดจะถูกรีเซ็ตและแอพพลิเคชันจะเริ่มต้นใหม่ คุณแน่ใจหรือไม่ว่าต้องการดำเนินการนี้?";
"Support text" = "ขอขอบคุณที่ใช้ Stats!\n\n การบำรุงรักษาและปรับปรุงโครงการโอเพนซอร์สนี้ต้องใช้เวลาและทรัพยากร การสนับสนุนของคุณช่วยให้เราสามารถให้บริการแอปพลิเคชันฟรีและเชื่อถือได้สำหรับทุกคนต่อไปได้\n\n หากคุณพบว่า Stats มีประโยชน์ โปรดพิจารณาให้ความช่วยเหลือ ทุก ๆ ความช่วยเหลือเล็ก ๆ น้อย ๆ ล้วนมีประโยชน์!";
// Settings
"Open Activity Monitor" = "เปิด Activity Monitor";
"Report a bug" = "รายงานข้อผิดพลาด";
"Support the application" = "สนับสนุนแอปพลิเคชัน";
"Close application" = "ปิดแอปพลิเคชัน";
"Open application settings" = "เปิดการตั้งค่าแอปพลิเคชัน";
"Open dashboard" = "เปิดแดชบอร์ด";
"No notifications available in this module" = "ไม่มีการแจ้งเตือนใด ๆ ในโมดูลนี้"; // translategemma:4b
"Open Calendar" = "เปิดปฏิทิน"; // translategemma:4b
"Toggle the module" = "เปิด/ปิดโมดูล"; // translategemma:4b
// Application settings
"Update application" = "อัพเดทแอปพลิเคชัน";
"Check for updates" = "ตรวจสอบการอัพเดท";
"At start" = "เริ่มต้น";
"Once per day" = "ครั้งละ 1 วัน";
"Once per week" = "ครั้งละ 1 สัปดาห์";
"Once per month" = "ครั้งละ 1 เดือน";
"Never" = "ไม่เคย";
"Check for update" = "ตรวจสอบการอัพเดท";
"Show icon in dock" = "แสดงไอคอนใน dock";
"Start at login" = "เริ่มต้นเมื่อเข้าสู่ระบบ";
"Build number" = "หมายเลข Build";
"Import settings" = "นำเข้าการตั้งค่า"; // translategemma:4b
"Export settings" = "การตั้งค่าการส่งออก"; // translategemma:4b
"Reset settings" = "รีเซ็ตการตั้งค่า";
"Pause the Stats" = "หยุด Stats";
"Resume the Stats" = "เริ่มต้น Stats";
"Combined modules" = "โมดูลผสม";
"Combined details" = "รายละเอียดที่รวบรวม"; // translategemma:4b
"Spacing" = "ระยะห่าง"; // translategemma:4b
"Share anonymous telemetry" = "แบ่งปันข้อมูล telemetry แบบไม่ระบุตัวตน"; // translategemma:4b
"Choose file" = "เลือกไฟล์"; // translategemma:4b
"Stress tests" = "การทดสอบความเครียด"; // translategemma:4b
// Dashboard
"Serial number" = "หมายเลขซีเรียล";
"Model identifier" = "รหัสรุ่น"; // translategemma:4b
"Production year" = "ปีที่ผลิต"; // translategemma:4b
"Uptime" = "ระยะเวลาที่เครื่องทำงาน";
"Number of cores" = "%0 คอร์"; // translategemma:4b
"Number of threads" = "%0 thread";
"Number of e-cores" = "%0 cores ประสิทธิภาพ";
"Number of p-cores" = "%0 cores ประสิทธิผล";
"Disks" = "ฮาร์ดดิสก์"; // translategemma:4b
"Display" = "แสดงผล"; // translategemma:4b
// Update
"The latest version of Stats installed" = "เวอร์ชันล่าสุดของ Stats ได้รับการติดตั้งแล้ว";
"Downloading..." = "กำลังดาวน์โหลด...";
"Current version: " = "เวอร์ชันปัจจุบัน: ";
"Latest version: " = "เวอร์ชันล่าสุด: ";
// Widgets
"Color" = "สี";
"Label" = "ป้ายชื่อ";
"Box" = "กล่อง";
"Frame" = "เฟรม";
"Value" = "ค่า";
"Colorize" = "สีสัน";
"Colorize value" = "สีสันค่า";
"Additional information" = "ข้อมูลเพิ่มเติม";
"Reverse values order" = "ย้อนกลับลำดับค่า";
"Base" = "ฐาน";
"Display mode" = "แสดงโหมด";
"One row" = "หนึ่งแถว";
"Two rows" = "สองแถว";
"Mini widget" = "วิดเจ็ตขนาดเล็ก";
"Line chart widget" = "แผนภูมิเส้น";
"Bar chart widget" = "แผนภูมิแท่ง";
"Pie chart widget" = "แผนภูมิวงกลม";
"Network chart widget" = "แผนภูมิเครือข่าย";
"Speed widget" = "ความเร็ว";
"Battery widget" = "แบตเตอรี่";
"Stack widget" = "คอลัม"; // translategemma:4b
"Memory widget" = "หน่วยความจำ";
"Static width" = "ความกว้างคงที่";
"Tachometer widget" = "วัดความเร็ว";
"State widget" = "เครื่องวัดสถานะ";
"Text widget" = "ส่วนประกอบสำหรับแสดงข้อความ"; // translategemma:4b
"Battery details widget" = "หน้าปัดข้อมูลแบตเตอรี่"; // translategemma:4b
"Show symbols" = "แสดงสัญลักษณ์";
"Label widget" = "ป้ายชื่อ";
"Number of reads in the chart" = "จำนวนการอ่านในแผนภูมิ";
"Color of download" = "สีของการดาวน์โหลด";
"Color of upload" = "สีของการอัพโหลด";
"Monospaced font" = "แบบอักษรโมโนสเปซ";
"Reverse order" = "ลำดับย้อนกลับ"; // translategemma:4b
"Chart history" = "ประวัติการสร้างกราฟ"; // translategemma:4b
"Default color" = "ค่าเริ่มต้น"; // translategemma:4b
"Transparent when no activity" = "โปร่งใสเมื่อไม่มีการใช้งาน"; // translategemma:4b
"Constant color" = "คงที่"; // translategemma:4b
// Module Kit
"Open module settings" = "เปิดการตั้งค่าโมดูล";
"Select widget" = "เลือก widget %0";
"Open widget settings" = "เปิดการตั้งค่าวิดเจ็ต";
"Update interval" = "ช่วงเวลาปรับปรุง";
"Usage history" = "ประวัติการใช้งาน";
"Details" = "รายละเอียด";
"Top processes" = "กระบวนการบนสุด";
"Pictogram" = "รูปภาพสัญลักษณ์";
"Module" = "โมดูล";
"Widgets" = "วิดเจ็ต";
"Popup" = "ป๊อปอัพ";
"Notifications" = "การแจ้งเตือน";
"Merge widgets" = "รวมวิดเจ็ต";
"No available widgets to configure" = "ไม่มีวิดเจ็ตที่สามารถกำหนดค่าได้";
"No options to configure for the popup in this module" = "ไม่มีตัวเลือกที่สามารถกำหนดค่าสำหรับป๊อปอัปในโมดูลนี้";
"Process" = "กระบวนการ"; // translategemma:4b
"Kill process" = "หยุดกระบวนการ"; // translategemma:4b
"Keyboard shortcut" = "ปุ่มลัดคีย์"; // translategemma:4b
"Listening..." = "กำลังฟัง..."; // translategemma:4b
// Modules
"Number of top processes" = "จำนวนของกระบวนการสูงสุด";
"Update interval for top processes" = "ช่วงเวลาปรับปรุงสำหรับกระบวนการสูงสุด";
"Notification level" = "ระดับแจ้งเตือน";
"Chart color" = "สีแผนภูมิ";
"Main chart scaling" = "การปรับขนาดกราฟหลัก"; // translategemma:4b
"Scale value" = "ค่าสเกล"; // translategemma:4b
"Text widget value" = "ค่าสำหรับส่วนประกอบ Text"; // translategemma:4b
// CPU
"CPU usage" = "การใช้งาน CPU";
"CPU temperature" = "อุณหภูมิ CPU";
"CPU frequency" = "ความถี่ CPU";
"System" = "ระบบ";
"User" = "ผู้ใช้";
"Idle" = "ว่างเปล่า";
"Show usage per core" = "แสดงการใช้งานต่อหนึ่ง core";
"Show hyper-threading cores" = "แสดง hyper-threading cores";
"Split the value (System/User)" = "แบ่งค่า (ระบบ/ผู้ใช้)";
"Scheduler limit" = "ขีดจำกัดของ Scheduler";
"Speed limit" = "ขีดจำกัดความเร็ว";
"Average load" = "โหลดเฉลี่ย";
"1 minute" = "1 นาที";
"5 minutes" = "5 นาที";
"15 minutes" = "15 นาที";
"CPU usage threshold" = "เกณฑ์การใช้งาน CPU";
"CPU usage is" = "การใช้งาน CPU คือ %0";
"Efficiency cores" = "cores ประสิทธิผล";
"Performance cores" = "cores ประสิทธิภาพ";
"System color" = "สีของระบบ";
"User color" = "สีของผู้ใช้";
"Idle color" = "สีของว่างเปล่า";
"Cluster grouping" = "การจัดกลุ่ม Cluster";
"Efficiency cores color" = "จำนวนคอร์ประสิทธิภาพ สี"; // translategemma:4b
"Performance cores color" = "สีของคอร์ประสิทธิภาพ"; // translategemma:4b
"Total load" = "ปริมาณการใช้งานทั้งหมด"; // translategemma:4b
"System load" = "ภาระของระบบ"; // translategemma:4b
"User load" = "ปริมาณการใช้งานของผู้ใช้"; // translategemma:4b
"Efficiency cores load" = "การโหลดคอร์ประสิทธิภาพ"; // translategemma:4b
"Performance cores load" = "การโหลดคอร์ประสิทธิภาพ"; // translategemma:4b
"All cores" = "ทุกคอร์"; // translategemma:4b
// GPU
"GPU to show" = "แสดง GPU";
"Show GPU type" = "แสดงประเภท GPU";
"GPU enabled" = "GPU เปิดการใช้งาน";
"GPU disabled" = "GPU ปิดการใช้งาน";
"GPU temperature" = "อุณหภูมิ GPU";
"GPU utilization" = "ปริมาณการใช้งาน GPU";
"Vendor" = "ผู้ผลิต";
"Model" = "โมเดล";
"Status" = "สถานะ";
"Active" = "ทำงาน";
"Non active" = "ไม่ได้ทำงาน";
"Fan speed" = "ความเร็วพัดลม";
"Core clock" = "ความถี่หลัก"; // translategemma:4b
"Memory clock" = "ความถี่ของหน่วยความจำ"; // translategemma:4b
"Utilization" = "ปริมาณการใช้งาน";
"Render utilization" = "ปริมาณการใช้งาน Render";
"Tiler utilization" = "ปริมาณการใช้งาน Tiler";
"GPU usage threshold" = "เกณฑ์การใช้งาน GPU";
"GPU usage is" = "ปริมาณการใช้งาน GPU คือ %0";
// RAM
"Memory usage" = "การใช้หน่วยความจำ";
"Memory pressure" = "ความกดดันหน่วยของความจำ";
"Total" = "รวม";
"Used" = "ใช้งาน";
"App" = "แอพ";
"Wired" = "แบบมีสาย"; // translategemma:4b
"Compressed" = "บีบอัด";
"Free" = "ว่าง";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "แบ่งค่า (แอพ / Wired / บีบอัด)";
"RAM utilization threshold" = "ขีดจำกัดการใช้หน่วยความจำ";
"RAM utilization is" = "การใช้หน่วยความจำคือ %0";
"App color" = "สีของแอพ";
"Wired color" = "สี Wired";
"Compressed color" = "สีที่บีบอัด";
"Free color" = "สีที่ว่าง";
"Free memory (less than)" = "หน่วยความจำเหลือ (น้อยกว่า)"; // translategemma:4b
"Swap size" = "ขนาดของการแลกเปลี่ยน"; // translategemma:4b
"Free RAM is" = "RAM ที่เหลือคือ %0"; // translategemma:4b
// Disk
"Show removable disks" = "แสดงดิสก์ถอดได้";
"Used disk memory" = "ใช้ %0 จาก %1";
"Free disk memory" = "ว่าง %0 จาก %1";
"Disk to show" = "ดิสก์แสดง";
"Open disk" = "เปิดดิสก์";
"Switch view" = "สลับมุมมอง";
"Disk utilization threshold" = "เกณฑ์การใช้งานดิสก์";
"Disk utilization is" = "ปริมาณการใช้งานดิสก์คือ %0";
"Read color" = "อ่านสี"; // translategemma:4b
"Write color" = "ระบุสี"; // translategemma:4b
"Disk usage" = "การใช้พื้นที่ดิสก์"; // translategemma:4b
"Total read" = "จำนวนที่อ่านทั้งหมด"; // translategemma:4b
"Total written" = "ยอดที่เขียน"; // translategemma:4b
"Write speed" = "เขียน"; // translategemma:4b
"Read speed" = "อ่าน"; // translategemma:4b
"Drives" = "อุปกรณ์"; // translategemma:4b
"SMART data" = "ข้อมูล SMART"; // translategemma:4b
// Sensors
"Temperature unit" = "หน่วยอุณหภูมิ";
"Celsius" = "เซลเซียส";
"Fahrenheit" = "ฟาเรนไฮต์";
"Save the fan speed" = "บันทึกความเร็วพัดลม";
"Fan" = "พัดลม";
"HID sensors" = "เซ็นเซอร์ HID";
"Synchronize fan's control" = "ปรับการควบคุมพัดลม";
"Current" = "ปัจจุบัน";
"Energy" = "พลังงาน";
"Show unknown sensors" = "แสดงเซ็นเซอร์ที่ไม่รู้จัก";
"Install fan helper" = "ติดตั้งตัวช่วยพัดลม";
"Uninstall fan helper" = "ถอนการติดตั้งตัวช่วยพัดลม";
"Fan value" = "ค่าพัดลม";
"Turn off fan" = "ปิดพัดลม"; // translategemma:4b
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "คุณกำลังจะปิดพัดลม นี่เป็นการกระทำที่ไม่แนะนำ ซึ่งอาจทำให้เครื่อง Mac ของคุณเสียหาย คุณแน่ใจหรือไม่ว่าคุณต้องการทำเช่นนั้น"; // translategemma:4b
"Sensor threshold" = "ขีดจำกัดของเซ็นเซอร์"; // translategemma:4b
"Left fan" = "ซ้าย"; // translategemma:4b
"Right fan" = "ถูกต้อง"; // translategemma:4b
"Fastest fan" = "เร็วที่สุด"; // translategemma:4b
"Sensor to show" = "เซ็นเซอร์เพื่อแสดงผล"; // translategemma:4b
// Network
"Uploading" = "กำลังอัปโหลด";
"Downloading" = "กำลังดาวน์โหลด";
"Public IP" = "IP สาธารณะ";
"Local IP" = "IP ในเครือข่าย";
"Interface" = "อินเตอร์เฟส";
"Physical address" = "ที่อยู่ทางกายภาพ";
"Refresh" = "รีเฟรช";
"Click to copy public IP address" = "คลิกเพื่อคัดลอก IP สาธารณะ";
"Click to copy local IP address" = "คลิกเพื่อคัดลอก IP ในเครือข่าย";
"Click to copy wifi name" = "คลิกเพื่อคัดลอกชื่อ WIFI";
"Click to copy mac address" = "คลิกเพื่อคัดลอกที่อยู่ MAC";
"No connection" = "ไม่มีการเชื่อมต่อ";
"Network interface" = "อินเตอร์เฟสเครือข่าย";
"Total download" = "ดาวน์โหลดทั้งหมด";
"Total upload" = "อัปโหลดทั้งหมด";
"Reader type" = "ประเภทของอ่านข้อมูล";
"Interface based" = "ยึดตามอินเตอร์เฟส";
"Processes based" = "ยึดตามกระบวนการ";
"Reset data usage" = "รีเซ็ตการใช้งานข้อมูล";
"VPN mode" = "โหมด VPN";
"Standard" = "มาตรฐาน";
"Security" = "ความปลอดภัย";
"Channel" = "ช่อง";
"Common scale" = "มาตรฐานทั่วไป";
"Autodetection" = "ตรวจหาอัตโนมัติ";
"Widget activation threshold" = "ขีดจำกัดของการเปิดใช้งานวิดเจ็ต";
"Internet connection" = "การเชื่อมต่ออินเทอร์เน็ต";
"Active state color" = "สีของสถานะที่ใช้งาน";
"Nonactive state color" = "สีของสถานะที่ไม่ใช้งาน";
"Connectivity host (ICMP)" = "โฮสต์ที่เชื่อมต่อ (ICMP)";
"Leave empty to disable the check" = "เว้นว่างไว้เพื่อปิดการตรวจสอบ";
"Connectivity history" = "ประวัติการเชื่อมต่อ"; // translategemma:4b
"Auto-refresh public IP address" = "อัปเดตที่อยู่ IP สาธารณะโดยอัตโนมัติ"; // translategemma:4b
"Every hour" = "ทุกชั่วโมง"; // translategemma:4b
"Every 12 hours" = "ทุก 12 ชั่วโมง"; // translategemma:4b
"Every 24 hours" = "ทุก 24 ชั่วโมง"; // translategemma:4b
"Network activity" = "กิจกรรมบนเครือข่าย"; // translategemma:4b
"Last reset" = "การรีเซ็ตครั้งล่าสุดเมื่อ `%0` ที่แล้ว"; // translategemma:4b
"Latency" = "ความหน่วง"; // translategemma:4b
"Upload speed" = "อัปโหลด"; // translategemma:4b
"Download speed" = "ดาวน์โหลด"; // translategemma:4b
"Address" = "ที่อยู่"; // translategemma:4b
"WiFi network" = "เครือข่าย Wi-Fi"; // translategemma:4b
"Local IP changed" = "ที่อยู่ IP ในเครื่องเปลี่ยนไปแล้ว"; // translategemma:4b
"Public IP changed" = "ที่อยู่ IP สาธารณะมีการเปลี่ยนแปลง"; // translategemma:4b
"Previous IP" = "IP ก่อนหน้า: %0"; // translategemma:4b
"New IP" = "IP ใหม่: %0"; // translategemma:4b
"Internet connection lost" = "การเชื่อมต่ออินเทอร์เน็ตขาด"; // translategemma:4b
"Internet connection established" = "การเชื่อมต่ออินเทอร์เน็ตสำเร็จ"; // translategemma:4b
// Battery
"Level" = "ระดับ";
"Source" = "แหล่งที่มา";
"AC Power" = "พลังงาน AC";
"Battery Power" = "พลังงานแบตเตอรี่";
"Time" = "เวลา";
"Health" = "สุขภาพ";
"Amperage" = "กระแสไฟ";
"Voltage" = "แรงดัน";
"Cycles" = "รอบ";
"Temperature" = "อุณหภูมิ";
"Power adapter" = "แปลงพลังงาน";
"Power" = "พลังงาน";
"Is charging" = "กำลังชาร์จ";
"Time to discharge" = "เวลาในการชาร์จ";
"Time to charge" = "เวลาในการชาร์จ";
"Calculating" = "การคำนวณ";
"Fully charged" = "เต็มแล้ว";
"Not connected" = "ไม่ได้เชื่อมต่อ";
"Low level notification" = "การแจ้งเตือนระดับต่ำ";
"High level notification" = "การแจ้งเตือนระดับสูง";
"Low battery" = "แบตเตอรี่ต่ำ";
"High battery" = "แบตเตอรี่สูง";
"Battery remaining" = "%0% เหลือ";
"Battery remaining to full charge" = "%0% ในการเต็มแบต";
"Percentage" = "เปอร์เซ็นต์";
"Percentage and time" = "เปอร์เซ็นต์และเวลา";
"Time and percentage" = "เวลาและเปอร์เซ็นต์";
"Time format" = "รูปแบบเวลา";
"Hide additional information when full" = "ซ่อนข้อมูลเพิ่มเติมเมื่อเต็ม";
"Last charge" = "ชาร์จครั้งสุดท้าย";
"Capacity" = "ความจุ";
"current / maximum / designed" = "ปัจจุบัน / สูงสุด / ออกแบบ";
"Low power mode" = "โหมดพลังงานต่ำ";
"Percentage inside the icon" = "เปอร์เซ็นต์ภายในไอคอน";
"Colorize battery" = "ระบุสีแบตเตอรี่"; // translategemma:4b
"Charging current" = "กระแสไฟฟ้าที่ใช้ในการชาร์จ"; // translategemma:4b
"Charging Voltage" = "แรงดันไฟฟ้าในการชาร์จ"; // translategemma:4b
"Charger state inside the battery" = "สถานะของเครื่องชาร์จภายในแบตเตอรี่"; // translategemma:4b
// Bluetooth
"Battery to show" = "แบตเตอรี่ที่แสดง";
"No Bluetooth devices are available" = "ไม่มีอุปกรณ์ Bluetooth ที่ใช้งานได้";
// Clock
"Time zone" = "เขตเวลา"; // translategemma:4b
"Local" = "ในพื้นที่"; // translategemma:4b
"Calendar" = "ปฏิทิน"; // translategemma:4b
"Show week numbers" = "แสดงหมายเลขสัปดาห์"; // translategemma:4b
"Local time" = "เวลาท้องถิ่น"; // translategemma:4b
"Add new clock" = "เพิ่มนาฬิกาใหม่"; // translategemma:4b
"Delete selected clock" = "ลบนาฬิกาที่เลือก"; // translategemma:4b
"Help with datetime format" = "ขอความช่วยเหลือเกี่ยวกับการจัดรูปแบบวันที่และเวลา"; // translategemma:4b
// Colors
"Based on utilization" = "อ้างอิงจากการใช้งาน";
"Based on pressure" = "อ้างอิงจากความดัน";
"Based on cluster" = "อ้างอิงจากกลุ่ม"; // translategemma:4b
"System accent" = "สีโปรแกรมระบบ";
"Monochrome accent" = "สีขาวและดำ";
"Clear" = "ล้าง";
"White" = "ขาว";
"Black" = "ดำ";
"Gray" = "สีเทา";
"Second gray" = "สีเทาอีก";
"Dark gray" = "สีเทาเข้ม";
"Light gray" = "สีเทาอ่อน";
"Red" = "แดง";
"Second red" = "แดงอีก";
"Green" = "เขียว";
"Second green" = "เขียวอีก";
"Blue" = "น้ำเงิน";
"Second blue" = "น้ำเงินอีก";
"Yellow" = "เหลือง";
"Second yellow" = "เหลืองอีก";
"Orange" = "ส้ม";
"Second orange" = "ส้มอีก";
"Purple" = "ม่วง";
"Second purple" = "ม่วงอีก";
"Brown" = "น้ำตาล";
"Second brown" = "น้ำตาลอีก";
"Cyan" = "เขียวฟ้า";
"Magenta" = "มาเกนตา";
"Pink" = "ชมพู";
"Teal" = "น้ำตาลเขียว";
"Indigo" = "น้ำเงินม่วง";
================================================
FILE: Stats/Supporting Files/tr.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "Çekirdek"; // translategemma:4b
"Open CPU settings" = "CPU ayalarını aç";
"GPU" = "GPU";
"Open GPU settings" = "GPU ayarlarını aç";
"RAM" = "RAM";
"Open RAM settings" = "RAM ayalarını aç";
"Disk" = "Disk";
"Open Disk settings" = "Disk ayarlarını aç";
"Sensors" = "Sensörler";
"Open Sensors settings" = "Sensör ayarlarını aç";
"Network" = "Ağ";
"Open Network settings" = "Ağ ayarlarını aç";
"Battery" = "Batarya";
"Open Battery settings" = "Batarya ayarlarını aç";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Bluetooth ayarlarını aç";
"Clock" = "Saat";
"Open Clock settings" = "Saat ayarlarını aç";
// Words
"Unknown" = "Bilinmeyen";
"Version" = "Sürüm";
"Processor" = "İşlemci";
"Memory" = "Bellek";
"Graphics" = "Grafik";
"Close" = "Kapat";
"Download" = "İndir";
"Install" = "Yükle";
"Cancel" = "İptal";
"Unavailable" = "Kullanım Dışı";
"Yes" = "Evet";
"No" = "Hayır";
"Automatic" = "Otomatik";
"Manual" = "Elle";
"None" = "Yok";
"Dots" = "Noktalar";
"Arrows" = "Oklar";
"Characters" = "Karakterler";
"Short" = "Kısa";
"Long" = "Uzun";
"Statistics" = "İstatistik";
"Max" = "Maks";
"Min" = "Minimum"; // translategemma:4b
"Reset" = "Sıfırla";
"Alignment" = "Hizalama";
"Left alignment" = "Sola hizalı";
"Center alignment" = "Ortaya hizalı";
"Right alignment" = "Sağa hizalı";
"Dashboard" = "Gösterge paneli";
"Enabled" = "Aktif";
"Disabled" = "Devre dışı";
"Silent" = "Sessiz";
"Units" = "Birimler";
"Fans" = "Fanlar";
"Scaling" = "Ölçekli";
"Linear" = "Doğrusal";
"Square" = "Karesel";
"Cube" = "Kübik";
"Logarithmic" = "Logaritmik";
"Fixed scale" = "Belirli ölçek";
"Cores" = "Çekirdekler";
"Settings" = "Ayarlar";
"Name" = "Ad";
"Format" = "Biçim"; // translategemma:4b
"Turn off" = "Kapat";
"Normal" = "Normal";
"Warning" = "Uyarı";
"Critical" = "Kritik";
"Usage" = "Kullanım";
"2 minutes" = "2 dakika";
"3 minutes" = "3 dakika";
"10 minutes" = "10 dakika";
"Import" = "İçe aktar";
"Export" = "Dışa aktar";
"Separator" = "Ayraç";
"Read" = "Oku";
"Write" = "Yaz";
"Frequency" = "Sıklık";
"Save" = "Kaydet";
"Run" = "Çalıştır";
"Stop" = "Durdur";
"Uninstall" = "Kaldır";
"1 sec" = "1 sn";
"2 sec" = "2 sn";
"3 sec" = "3 sn";
"5 sec" = "5 sn";
"10 sec" = "10 sn";
"15 sec" = "15 sn";
"30 sec" = "30 sn";
"60 sec" = "60 sn";
// Setup
"Stats Setup" = "Stats Kurulumu";
"Previous" = "Önceki";
"Previous page" = "Önceki sayfa";
"Next" = "Sonraki";
"Next page" = "Sonraki sayfa";
"Finish" = "Bitir";
"Finish setup" = "Kurulumu bitir";
"Welcome to Stats" = "Stats'a hoş geldiniz";
"welcome_message" = "Stats'ı kullandığınız için teşekkürler, Stats MacOS sistem monitörü açık kaynaklı menü bar uygulamasıdır";
"Start the application automatically when starting your Mac" = "Mac'i açınca otomatik çalıştır";
"Do not start the application automatically when starting your Mac" = "Mac'i açınca otomatik çalıştırma";
"Do everything silently in the background (recommended)" = "Her şeyi arkada sessiz yap (önerilir)";
"Check for a new version on startup" = "Başlangıçta güncellemeleri denetle";
"Check for a new version every day (once a day)" = "Güncellemeleri günde bir defa denetle";
"Check for a new version every week (once a week)" = "Güncellemeleri haftada bir defa denetle";
"Check for a new version every month (once a month)" = "Güncellemeleri ayda bir defa denetle";
"Never check for updates (not recommended)" = "Güncellemeleri denetleme (önerilmez)";
"Anonymous telemetry for better development decisions" = "Daha iyi geliştirme kararları için anonim telemetri";
"Share anonymous telemetry data" = "Anonim telemetri verisi paylaş";
"Do not share anonymous telemetry data" = "Anonim telemetri verisi paylaşma";
"The configuration is completed" = "Konfigürasyon tamamlandı";
"finish_setup_message" = "Her şey hazır! \n Stats açık kaynaklı bir araçtır, Ücretsiz ve hep öyle kalacak. \n Beğendiyseniz projeyi destekleyebilirsiniz, Teşekkürler!";
// Alerts
"New version available" = "Yeni sürüm mevcut";
"Click to install the new version of Stats" = "Stats'ın yeni sürümünü indirmek için tıklayın";
"Successfully updated" = "Başarıyla güncellendi";
"Stats was updated to v" = "Stats v%0 sürümüne güncellendi";
"Reset settings text" = "Bütün uygulama ayarları sıfırlanacak ve uygulama yeniden başlatılacak. Bunu yapmak istediğinize emin misiniz?";
"Support text" = "Stats'i kullandığınız için teşekkür ederiz.\n\n Bu açık kaynak projesini sürdürmek ve geliştirmek zaman ve kaynak gerektirir. Desteğiniz, herkes için ücretsiz ve güvenilir bir uygulama sağlamaya devam etmemize yardımcı olur.\n\nStats'ı yararlı buluyorsanız, lütfen katkıda bulunmayı düşünün. Her küçük parça yardımcı olur!";
// Settings
"Open Activity Monitor" = "Etkinlik Monitörünü Aç";
"Report a bug" = "Hata bildir";
"Support the application" = "Uygulamayı destekleyin";
"Close application" = "Uygulamayı kapat";
"Open application settings" = "Uygulama ayarlarını aç";
"Open dashboard" = "Gösterge panelini aç";
"No notifications available in this module" = "Bu modülde kullanılabilir bildirim mevcut değil";
"Open Calendar" = "Takvimi Aç";
"Toggle the module" = "Modülü aç/kapa";
// Application settings
"Update application" = "Uygulamayı güncelle";
"Check for updates" = "Güncellemeleri denetle";
"At start" = "Başlangıçta";
"Once per day" = "Günlük";
"Once per week" = "Haftalık";
"Once per month" = "Aylık";
"Never" = "Asla";
"Check for update" = "Güncellemeleri denetle";
"Show icon in dock" = "Dock'ta göster";
"Start at login" = "Başlangıçta çalıştır";
"Build number" = "Sürüm numarası";
"Import settings" = "Ayarları içe aktar";
"Export settings" = "Ayarları dışa aktar";
"Reset settings" = "Ayarları sıfırla";
"Pause the Stats" = "Stats'ı Duraklat";
"Resume the Stats" = "Stats'ı Sürdür";
"Combined modules" = "Kombine modüller";
"Combined details" = "Kombine ayrıntılar";
"Spacing" = "Aralıklı dizme";
"Share anonymous telemetry" = "Anonim telemetri paylaş";
"Choose file" = "Dosya seç";
"Stress tests" = "Stres testleri";
// Dashboard
"Serial number" = "Seri numarası";
"Model identifier" = "Model tanımlayıcı";
"Production year" = "Üretim yılı";
"Uptime" = "Çalışma süresi";
"Number of cores" = "%0 çekirdek";
"Number of threads" = "%0 iş parçacığı";
"Number of e-cores" = "%0 verimlilik çekirdeği";
"Number of p-cores" = "%0 performans çekirdeği";
"Disks" = "Diskler";
"Display" = "Ekran";
// Update
"The latest version of Stats installed" = "Stats'ın son sürümü kurulu";
"Downloading..." = "İndiriliyor...";
"Current version: " = "Şu anki sürüm: ";
"Latest version: " = "Son sürüm: ";
// Widgets
"Color" = "Renk";
"Label" = "Etiket";
"Box" = "Kutu Görünüm";
"Frame" = "Çerçeve";
"Value" = "Değer";
"Colorize" = "Renklendir";
"Colorize value" = "Değeri renklendir";
"Additional information" = "Ek bilgi";
"Reverse values order" = "Değerler sırasını tersine çevir";
"Base" = "Temel";
"Display mode" = "Ekran modu";
"One row" = "Tek sıra";
"Two rows" = "İki sıra";
"Mini widget" = "Mini widget'ı";
"Line chart widget" = "Çizgi grafik widget'ı";
"Bar chart widget" = "Çubuk grafik widget'ı";
"Pie chart widget" = "Pasta grafik widget'ı";
"Network chart widget" = "Ağ grafik widget'ı";
"Speed widget" = "Hız widget'ı";
"Battery widget" = "Batarya widget'ı";
"Stack widget" = "Yığın widget'ı";
"Memory widget" = "Bellek widget'ı";
"Static width" = "Sabit genişlik";
"Tachometer widget" = "Takometre widget'ı";
"State widget" = "Durum widget'ı";
"Text widget" = "Metin widget'ı";
"Battery details widget" = "Batarya detayları widget'ı";
"Show symbols" = "Sembolleri göster";
"Label widget" = "Etiket widget'ı";
"Number of reads in the chart" = "Grafikteki okuma sayısı";
"Color of download" = "İndirme rengi";
"Color of upload" = "Yükleme rengi";
"Monospaced font" = "Eşit aralıklı font";
"Reverse order" = "Sıralamayı tersine çevir";
"Chart history" = "Grafik geçmişi";
"Default color" = "Varsayılan renk";
"Transparent when no activity" = "Aktif değilse transparan";
"Constant color" = "Sabit renk";
// Module Kit
"Open module settings" = "Modül ayarlarını aç";
"Select widget" = "%0 widgetını seç";
"Open widget settings" = "Widget ayarlarını aç";
"Update interval" = "Güncelleme aralığı";
"Usage history" = "Kullanım geçmişi";
"Details" = "Detaylar";
"Top processes" = "Ana işlemler";
"Pictogram" = "Piktogram";
"Module" = "Modül";
"Widgets" = "Widget'lar";
"Popup" = "Açılır Pencere";
"Notifications" = "Bildirimler";
"Merge widgets" = "Widget'ları birleştir";
"No available widgets to configure" = "Yapılandırılacak kullanılabilir widget mevcut değil";
"No options to configure for the popup in this module" = "Bu modülde açılır pencere için yapılandırma seçeneği mevcut değil";
"Process" = "İşlem";
"Kill process" = "İşlemi sonlandır";
"Keyboard shortcut" = "Klavye kısayolu";
"Listening..." = "Dinleniyor...";
// Modules
"Number of top processes" = "Zirve işlemlerin sayısı";
"Update interval for top processes" = "Zirve işlemler için güncelleme aralığı";
"Notification level" = "Bildirim düzeyi";
"Chart color" = "Çizelge rengi";
"Main chart scaling" = "Ana çizelge ölçeklendirmesi";
"Scale value" = "Ölçekleme değeri";
"Text widget value" = "Metin widget'ı değeri";
// CPU
"CPU usage" = "CPU kullanımı";
"CPU temperature" = "CPU sıcaklığı";
"CPU frequency" = "CPU frekansı";
"System" = "Sistem";
"User" = "Kullanıcı";
"Idle" = "Boşta";
"Show usage per core" = "Çekirdek başına kullanımı göster";
"Show hyper-threading cores" = "Hyper-threading çekirdeklerini göster";
"Split the value (System/User)" = "Değeri böl (Sistem/Kullanıcı)";
"Scheduler limit" = "Zamanlayıcı sınırı";
"Speed limit" = "Hız limiti";
"Average load" = "Ortalama yük";
"1 minute" = "1 dakika";
"5 minutes" = "5 dakika";
"15 minutes" = "15 dakika";
"CPU usage threshold" = "CPU kullanım eşiği";
"CPU usage is" = "CPU kullanımı %0";
"Efficiency cores" = "Verimlilik Çekirdeği";
"Performance cores" = "Performans Çekirdeği";
"System color" = "Sistem rengi";
"User color" = "Kullanıcı rengi";
"Idle color" = "Boşta rengi";
"Cluster grouping" = "Küme gruplaması";
"Efficiency cores color" = "Verimlilik çekirdeklerinin rengi";
"Performance cores color" = "Performans çekirdeklerinin rengi";
"Total load" = "Toplam yük";
"System load" = "Sistem yükü";
"User load" = "Kullanıcı yükü";
"Efficiency cores load" = "Verimlilik çekirdekleri yükü";
"Performance cores load" = "Performans çekirdekleri yükü";
"All cores" = "Tüm çekirdekler";
// GPU
"GPU to show" = "Gösterilecek GPU";
"Show GPU type" = "GPU türünü göster";
"GPU enabled" = "GPU devrede";
"GPU disabled" = "GPU devre dışı";
"GPU temperature" = "GPU sıcaklığı";
"GPU utilization" = "GPU kullanımı";
"Vendor" = "Üretici";
"Model" = "Model";
"Status" = "Durum";
"Active" = "Aktif";
"Non active" = "Aktif değil";
"Fan speed" = "Fan hızı";
"Core clock" = "Çekirdek frekansı";
"Memory clock" = "Bellek frekansı";
"Utilization" = "Kullanım";
"Render utilization" = "İşleme kullanımı";
"Tiler utilization" = "Tiler kullanımı";
"GPU usage threshold" = "GPU kullanım eşiği";
"GPU usage is" = "GPU kullanımı %0";
// RAM
"Memory usage" = "Ram kullanımı";
"Memory pressure" = "Ram yoğunluğu";
"Total" = "Toplam";
"Used" = "Kullanılan";
"App" = "Uygulama";
"Wired" = "Bağlı";
"Compressed" = "Sıkıştırılmış";
"Free" = "Serbest";
"Swap" = "Takas";
"Split the value (App/Wired/Compressed)" = "Değeri böl (Uygulama/Bağlı/Sıkıştırılmış)";
"RAM utilization threshold" = "RAM kullanım eşiği";
"RAM utilization is" = "RAM Kullanımı %0";
"App color" = "Uygulama rengi";
"Wired color" = "Bağlı rengi";
"Compressed color" = "Sıkıştırılmış rengi";
"Free color" = "Boş rengi";
"Free memory (less than)" = "Boş hafıza (yüzdeden az ise)";
"Swap size" = "Swap boyutu";
"Free RAM is" = "Boştaki RAM %0";
// Disk
"Show removable disks" = "Çıkarılabilir diskleri göster";
"Used disk memory" = "Kullanılan %0, toplam %1";
"Free disk memory" = "Boş %0, toplam %1";
"Disk to show" = "Gösterilecek disk";
"Open disk" = "Açık disk";
"Switch view" = "Görünümü değiştir";
"Disk utilization threshold" = "Disk kullanım eşiği";
"Disk utilization is" = "Disk kullanımı %0";
"Read color" = "Oku rengi";
"Write color" = "Yaz rengi";
"Disk usage" = "Disk kullanımı";
"Total read" = "Toplam okuma";
"Total written" = "Toplam yazılan";
"Write speed" = "Yazma hızı";
"Read speed" = "Okuma hızı";
"Drives" = "Sürücüler";
"SMART data" = "SMART verisi";
// Sensors
"Temperature unit" = "Sıcaklık birimi";
"Celsius" = "Santigrat";
"Fahrenheit" = "Fahrenhayt";
"Save the fan speed" = "Fan hızını kaydet";
"Fan" = "Fan";
"HID sensors" = "HID sensörleri";
"Synchronize fan's control" = "Fanın kontrolünü senkronize et";
"Current" = "Akım";
"Energy" = "Enerji";
"Show unknown sensors" = "Bilinmeyen sensörleri göster";
"Install fan helper" = "Fan yardımcısını kur";
"Uninstall fan helper" = "Fan yardımcısını kaldır";
"Fan value" = "Fan değeri";
"Turn off fan" = "Fanı kapat";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Fanı kapatacaksınız. Bu, Mac'inize zarar verebilecek bir eylemdir ve önerilmez. Bunu yapmak istediğinizden emin misiniz?";
"Sensor threshold" = "Sensör eşiği";
"Left fan" = "Sol fan";
"Right fan" = "Sağ fan";
"Fastest fan" = "En hızlı fan";
"Sensor to show" = "Gösterilecek sensör";
// Network
"Uploading" = "Yükleme";
"Downloading" = "İndirme";
"Public IP" = "Açık IP";
"Local IP" = "Yerel IP";
"Interface" = "Arayüz";
"Physical address" = "Fiziksel adres";
"Refresh" = "Yenile";
"Click to copy public IP address" = "Açık IP adresini kopyalamak için tıklayın";
"Click to copy local IP address" = "Yerel IP adresini kopyalamak için tıklayın";
"Click to copy wifi name" = "Wifi adını kopyalamak için tıklayın";
"Click to copy mac address" = "Mac adresini kopyalamak için tıklayın";
"No connection" = "Bağlantı yok";
"Network interface" = "Ağ arayüzü";
"Total download" = "Toplam indirme";
"Total upload" = "Toplam yükleme";
"Reader type" = "Okuma yöntemi";
"Interface based" = "Arayüz";
"Processes based" = "Süreçler";
"Reset data usage" = "Veri kullanımını sıfırla";
"VPN mode" = "VPN modu";
"Standard" = "Standart";
"Security" = "Güvenlik";
"Channel" = "Kanal";
"Common scale" = "Ortak ölçek";
"Autodetection" = "Otomatik algılama";
"Widget activation threshold" = "Widget aktivasyon eşiği";
"Internet connection" = "Internet bağlantısı";
"Active state color" = "Etkin durum rengi";
"Nonactive state color" = "Etkin olmayan durum rengi";
"Connectivity host (ICMP)" = "Bağlantı sunucusu (ICMP)";
"Leave empty to disable the check" = "Kontrolü devre dışı bırakmak için boş bırakın";
"Connectivity history" = "Bağlantı geçmişi";
"Auto-refresh public IP address" = "Açık IP adresi oto-yenile";
"Every hour" = "Her saat";
"Every 12 hours" = "Her 12 saat";
"Every 24 hours" = "Her 24 saat";
"Network activity" = "Ağ aktivitesi";
"Last reset" = "Son sıfırlama %0 önce";
"Latency" = "Gecikme";
"Upload speed" = "Yükleme hızı";
"Download speed" = "İndirme hızı";
"Address" = "Adres";
"WiFi network" = "WiFi ağı";
"Local IP changed" = "Yerel IP değişti";
"Public IP changed" = "Açık IP değişti";
"Previous IP" = "Önceki IP: %0";
"New IP" = "Yeni IP: %0";
"Internet connection lost" = "İnternet bağlantısı kesildi";
"Internet connection established" = "İnternet bağlantısı kuruldu";
// Battery
"Level" = "Doluluk";
"Source" = "Kaynak";
"AC Power" = "Fişe takılı";
"Battery Power" = "Batarya gücü";
"Time" = "Zaman";
"Health" = "Sağlık";
"Amperage" = "Amper";
"Voltage" = "Voltaj";
"Cycles" = "Döngü sayısı";
"Temperature" = "Sıcaklık";
"Power adapter" = "Güç adaptörü";
"Power" = "Güç";
"Is charging" = "Şarj oluyor";
"Time to discharge" = "Boşalma süresi";
"Time to charge" = "Şarj olma süresi";
"Calculating" = "Hesaplanıyor";
"Fully charged" = "Tam dolu";
"Not connected" = "Bağlı değil";
"Low level notification" = "Düşük seviye bildirimi";
"High level notification" = "Yüksek seviye bildirimi";
"Low battery" = "Düşük batarya";
"High battery" = "Yüksek batarya";
"Battery remaining" = "%0% kaldı";
"Battery remaining to full charge" = "Tam şarja %0% kaldı";
"Percentage" = "Yüzde";
"Percentage and time" = "Yüzde ve zaman";
"Time and percentage" = "Zaman ve yüzde";
"Time format" = "Zaman formatı";
"Hide additional information when full" = "Pil dolduğunda ek bilgileri gizle";
"Last charge" = "Son şarj";
"Capacity" = "Kapasite";
"current / maximum / designed" = "mevcut / maksimum / tasarlanmış";
"Low power mode" = "Düşük güç modu";
"Percentage inside the icon" = "Simgenin içinde yüzde";
"Colorize battery" = "Pili renklendir";
"Charging current" = "Şarj akımı";
"Charging Voltage" = "Şarj Voltajı";
"Charger state inside the battery" = "Şarj durumunun pilin içinde"; // translategemma:4b
// Bluetooth
"Battery to show" = "Gösterilecek pil";
"No Bluetooth devices are available" = "Herhangi bir Bluetooth cihazı mevcut değil";
// Clock
"Time zone" = "Saat dilimi";
"Local" = "Yerel";
"Calendar" = "Takvim";
"Show week numbers" = "Hafta numaralarını göster"; // translategemma:4b
"Local time" = "Yerel zaman";
"Add new clock" = "Yeni saat ekle";
"Delete selected clock" = "Seçili saati sil";
"Help with datetime format" = "Tarih-saat biçimiyle ilgili yardım";
// Colors
"Based on utilization" = "Kullanıma dayalı";
"Based on pressure" = "Yüke dayalı";
"Based on cluster" = "Kümeye dayalı";
"System accent" = "Sistem vurgusu";
"Monochrome accent" = "Siyah-beyaz vurgu";
"Clear" = "Temizle";
"White" = "Beyaz";
"Black" = "Siyah";
"Gray" = "Gri";
"Second gray" = "İkincil gri";
"Dark gray" = "Koyu gri";
"Light gray" = "Açık gri";
"Red" = "Kırmızı";
"Second red" = "İkincil kırmızı";
"Green" = "Yeşil";
"Second green" = "İkincil yeşil";
"Blue" = "Mavi";
"Second blue" = "İkincil mavi";
"Yellow" = "Sarı";
"Second yellow" = "İkincil sarı";
"Orange" = "Turuncu";
"Second orange" = "İkincil turuncu";
"Purple" = "Mor";
"Second purple" = "İkincil mor";
"Brown" = "Kahverengi";
"Second brown" = "İkincil kahverengi";
"Cyan" = "Camgöbeği";
"Magenta" = "Fuşya";
"Pink" = "Pembe";
"Teal" = "Ördekbaşı";
"Indigo" = "Çivit";
================================================
FILE: Stats/Supporting Files/uk.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "Процесор";
"Open CPU settings" = "Відкрити налаштування процесору";
"GPU" = "Графічний процесор";
"Open GPU settings" = "Відкрити налаштування графічного процесора";
"RAM" = "Оперативна пам'ять";
"Open RAM settings" = "Відкрити налаштування оперативної пам'яті";
"Disk" = "Диск";
"Open Disk settings" = "Відкрити налаштування диска";
"Sensors" = "Датчики";
"Open Sensors settings" = "Відкрити налаштування датчиків";
"Network" = "Мережа";
"Open Network settings" = "Відкрити налаштування мережі";
"Battery" = "Акумулятор";
"Open Battery settings" = "Відкрити налаштування акумулятора";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Відкрити налаштування bluetooth";
"Clock" = "Годинник";
"Open Clock settings" = "Відкрити налаштування годинника";
// Words
"Unknown" = "Невідомо";
"Version" = "Версія";
"Processor" = "Процесор";
"Memory" = "Память";
"Graphics" = "Графіка";
"Close" = "Закрити";
"Download" = "Завантажити";
"Install" = "Встановити";
"Cancel" = "Скасувати";
"Unavailable" = "Недоступно";
"Yes" = "Так";
"No" = "Ні";
"Automatic" = "Автоматичний";
"Manual" = "Ручний";
"None" = "Ніяка";
"Dots" = "Пункти";
"Arrows" = "Стрілки";
"Characters" = "Літери";
"Short" = "Короткий";
"Long" = "Довгий";
"Statistics" = "Статистика";
"Max" = "Мін.";
"Min" = "Макс.";
"Reset" = "Скинути";
"Alignment" = "Вирівнювання";
"Left alignment" = "Ліворуч";
"Center alignment" = "По центру";
"Right alignment" = "Праворуч";
"Dashboard" = "Головний екран"; // translategemma:4b
"Enabled" = "Включено";
"Disabled" = "Виключено";
"Silent" = "Безшумно";
"Units" = "Одиниці";
"Fans" = "Вентилятори";
"Scaling" = "Масштабування";
"Linear" = "Лінійне";
"Square" = "Квадратне";
"Cube" = "Кубічне";
"Logarithmic" = "Логарифмічне";
"Fixed scale" = "Фіксована";
"Cores" = "Ядра";
"Settings" = "Налаштування";
"Name" = "Назва";
"Format" = "Формат";
"Turn off" = "Вимкнути";
"Normal" = "Нормальний";
"Warning" = "Попереджнння";
"Critical" = "Критичний";
"Usage" = "Використання";
"2 minutes" = "2 хвилини";
"3 minutes" = "3 хвилини";
"10 minutes" = "10 хвилин";
"Import" = "Імпорт";
"Export" = "Експорт";
"Separator" = "Роздільник";
"Read" = "Зчитування";
"Write" = "Запис";
"Frequency" = "Частота";
"Save" = "Зберегти";
"Run" = "Запустити";
"Stop" = "Зупинити";
"Uninstall" = "Видалити";
"1 sec" = "1 сек";
"2 sec" = "2 сек";
"3 sec" = "3 сек";
"5 sec" = "5 сек";
"10 sec" = "10 сек";
"15 sec" = "15 сек";
"30 sec" = "30 сек";
"60 sec" = "60 сек";
// Setup
"Stats Setup" = "Налаштування";
"Previous" = "Попередня";
"Previous page" = "Попередня сторінка";
"Next" = "Наступна";
"Next page" = "Наступна сторінка";
"Finish" = "Завершити";
"Finish setup" = "Завершити налаштування";
"Welcome to Stats" = "Вітаю в Stats";
"welcome_message" = "Дякуємо, що використовуєте Stats, безкоштовним моніторингом системи macOS з відкритим кодом.";
"Start the application automatically when starting your Mac" = "Запускати програму автоматично під час запуску Mac";
"Do not start the application automatically when starting your Mac" = "Не запускати програму автоматично під час запуску Mac";
"Do everything silently in the background (recommended)" = "Робити усе у фоновому режимі (рекомендовано)";
"Check for a new version on startup" = "Перевірти наявність нової версії під час запуску програми";
"Check for a new version every day (once a day)" = "Щодня перевіряти наявність нової версії (один раз на день)";
"Check for a new version every week (once a week)" = "Щотижня перевіряти наявність нової версії (раз на тиждень)";
"Check for a new version every month (once a month)" = "Перевіряти наявність нової версії щомісяця (раз на місяць)";
"Never check for updates (not recommended)" = "Ніколи не перевіряти наявність оновлень (не рекомендовано)";
"Anonymous telemetry for better development decisions" = "Анонімна телеметрія для кращих рішень щодо програми";
"Share anonymous telemetry data" = "Ділитись анонімними даними телеметрії";
"Do not share anonymous telemetry data" = "Не передавати анонімні дані телеметрії";
"The configuration is completed" = "Налаштування завершено";
"finish_setup_message" = "Все налаштовано! \n Stats — це інструмент з відкритим вихідним кодом, він безкоштовний і таким завжди буде. \n Якщо вам це подобається ви можете підтримати проект, це завжди цінується!";
// Alerts
"New version available" = "Доступна нова версія";
"Click to install the new version of Stats" = "Натисніть щоб встановити нову версію Stats";
"Successfully updated" = "Успішно оновленно";
"Stats was updated to v" = "Stats оновлено до v%0";
"Reset settings text" = "Усі налаштування програми буде скинуто, і програма буде перезапущена. Ви впевнені, що хочете це зробити?";
"Support text" = "Дякуємо за використання Stats!\n\n Підтримка і вдосконалення цього проекту з відкритим вихідним кодом вимагає часу і ресурсів. Ваша підтримка допомагає нам продовжувати надавати безкоштовну і надійну програму для всіх.\n\nЯкщо ви вважаєте Stats корисною, будь ласка, зробіть свій внесок. Кожна копійка допомагає!";
// Settings
"Open Activity Monitor" = "Відкрити Монітор активності";
"Report a bug" = "Повідомити про помилку";
"Support the application" = "Підтримати програму";
"Close application" = "Закрити програму";
"Open application settings" = "Відкрити налаштування";
"Open dashboard" = "Відкрити dashboard";
"No notifications available in this module" = "У цьому модулі немає повідомлень";
"Open Calendar" = "Відкрити календар";
"Toggle the module" = "Перемкніть модуль";
// Application settings
"Update application" = "Оновити програму";
"Check for updates" = "Перевіряти оновленя";
"At start" = "При включенні";
"Once per day" = "Раз на день";
"Once per week" = "Раз на тиждень";
"Once per month" = "Раз на місяць";
"Never" = "Ніколи";
"Check for update" = "Перевірити оновленя";
"Show icon in dock" = "Показувати іконку в dock";
"Start at login" = "Запускати при логуванні";
"Build number" = "Номер збірки";
"Import settings" = "Імпорт налаштувань";
"Export settings" = "Експорт налаштувань";
"Reset settings" = "Скидання налаштувань";
"Pause the Stats" = "Призупинити Stats";
"Resume the Stats" = "Відновити Stats";
"Combined modules" = "Комбіновані модулі";
"Combined details" = "Комбіновані деталі";
"Spacing" = "Інтервал";
"Share anonymous telemetry" = "Ділитись анонімною телеметрією";
"Choose file" = "Виберіть файл";
"Stress tests" = "Стрес-тести";
// Dashboard
"Serial number" = "Серійний номер";
"Model identifier" = "Ідентифікатор моделі";
"Production year" = "Рік випуску";
"Uptime" = "Час роботи";
"Number of cores" = "%0 ядер";
"Number of threads" = "%0 потоків";
"Number of e-cores" = "%0 енергоефективних ядер";
"Number of p-cores" = "%0 високопродуктивних ядер";
"Disks" = "Диски";
"Display" = "Монітор";
// Update
"The latest version of Stats installed" = "Встановлено останню версію";
"Downloading..." = "Завантаження...";
"Current version: " = "Поточна версія: ";
"Latest version: " = "Остання версія: ";
// Widgets
"Color" = "Колір";
"Label" = "Етикетка";
"Box" = "Коробка"; // translategemma:4b
"Frame" = "Рамка";
"Value" = "Значення";
"Colorize" = "Розфарбувати";
"Colorize value" = "Розфарбувати значення";
"Additional information" = "Додаткова інформація";
"Reverse values order" = "Змінити порядок сортування";
"Base" = "Основа";
"Display mode" = "Режим відображення";
"One row" = "Один ряд";
"Two rows" = "Два ряди";
"Mini widget" = "Міні";
"Line chart widget" = "Лінійна діаграма";
"Bar chart widget" = "Гістограма";
"Pie chart widget" = "Кругова діаграма";
"Network chart widget" = "Діаграма мережі";
"Speed widget" = "Швидкість";
"Battery widget" = "Акумулятор";
"Stack widget" = "Стек";
"Memory widget" = "Пам'ять";
"Static width" = "Статична ширина";
"Tachometer widget" = "Тахометр";
"State widget" = "Стан";
"Text widget" = "Текст";
"Battery details widget" = "Віджет даних про акумулятор";
"Show symbols" = "Показати символи";
"Label widget" = "Етикетка";
"Number of reads in the chart" = "Кількість зчитувань на діаграмі";
"Color of download" = "Колір завантаження";
"Color of upload" = "Колір висилання";
"Monospaced font" = "Моноширинний шрифт";
"Reverse order" = "Зворотній порядок";
"Chart history" = "Тривалість діаграми";
"Default color" = "За замовчуванням";
"Transparent when no activity" = "Прозорий коли немає активності";
"Constant color" = "Постійний";
// Module Kit
"Open module settings" = "Відкрити налаштування модуля";
"Select widget" = "Активувати %0 віджет";
"Open widget settings" = "Відкрити налаштування віджета";
"Update interval" = "Інтервал оновлень";
"Usage history" = "Історія використання";
"Details" = "Деталі";
"Top processes" = "Топ процесів";
"Pictogram" = "Піктограма";
"Module" = "Модуль";
"Widgets" = "Віджети";
"Popup" = "Спливаюче віконо";
"Notifications" = "Cповіщення";
"Merge widgets" = "Об’єднайти віджети";
"No available widgets to configure" = "Немає доступних віджетів для налаштування";
"No options to configure for the popup in this module" = "У цьому модулі немає параметрів для налаштування спливаючого вікна";
"Process" = "Процес";
"Kill process" = "Завершити процес";
"Keyboard shortcut" = "Комбінація клавіш";
"Listening..." = "Слухаю...";
// Modules
"Number of top processes" = "Кількість процесів";
"Update interval for top processes" = "Інтервал оновлення для процесів";
"Notification level" = "Рівень сповіщення";
"Chart color" = "Колір діаграми";
"Main chart scaling" = "Масштабування основної діаграми";
"Scale value" = "Значення масштабування";
"Text widget value" = "Значення текстового віджета";
// CPU
"CPU usage" = "Використання процесора";
"CPU temperature" = "Температура процесора";
"CPU frequency" = "Частота процесора";
"System" = "Система";
"User" = "Користувач";
"Idle" = "Вільно";
"Show usage per core" = "Показати використання на одне ядро";
"Show hyper-threading cores" = "Показати Hyper-Threading ядра";
"Split the value (System/User)" = "Розділити значення (Система/Користувач)";
"Scheduler limit" = "Обмеження планувальника";
"Speed limit" = "Обмеження швидкості";
"Average load" = "Середнє навантаження";
"1 minute" = "1 хвилина";
"5 minutes" = "5 хвилин";
"15 minutes" = "15 хвилин";
"CPU usage threshold" = "Поріг використання процесора";
"CPU usage is" = "Використання процесора %0";
"Efficiency cores" = "Енергоефективні ядра";
"Performance cores" = "Високопродуктивні ядра";
"System color" = "Системний колір";
"User color" = "Колір користувача";
"Idle color" = "Колір простою";
"Cluster grouping" = "Кластерне групування";
"Efficiency cores color" = "Колір енергоефективних ядер";
"Performance cores color" = "Колір високопродуктивних ядер";
"Total load" = "Загальне використання";
"System load" = "Використання системи";
"User load" = "Використання користувача";
"Efficiency cores load" = "Використання енергоефективних ядер";
"Performance cores load" = "Використання високопродуктивних ядер";
"All cores" = "Всі ядра";
// GPU
"GPU to show" = "Активний графічний процесор";
"Show GPU type" = "Показувати тип графічного процесора";
"GPU enabled" = "Увімкнено";
"GPU disabled" = "Вимкнено";
"GPU temperature" = "Температура графічного процесора";
"GPU utilization" = "Навантаженість графічного процесора";
"Vendor" = "Постачальник";
"Model" = "Модель";
"Status" = "Статус";
"Active" = "Активна";
"Non active" = "Неактивна";
"Fan speed" = "Швидкість вентилятора";
"Core clock" = "Частота процесора";
"Memory clock" = "Частота пам'яті";
"Utilization" = "Використання";
"Render utilization" = "Використання render";
"Tiler utilization" = "Використання tiler";
"GPU usage threshold" = "Поріг використання графічного процесора";
"GPU usage is" = "Використання графічного процесора %0";
// RAM
"Memory usage" = "Навантаженість пам’яті";
"Memory pressure" = "Рівень навантаження";
"Total" = "Загально";
"Used" = "Використовується";
"App" = "Програми";
"Wired" = "Постійна";
"Compressed" = "Стисненна";
"Free" = "Вільна";
"Swap" = "Swap";
"Split the value (App/Wired/Compressed)" = "Розділити значення (Програми/Постійна/Стисненна)";
"RAM utilization threshold" = "Поріг використання оперативної пам'яті";
"RAM utilization is" = "Використання оперативної пам'яті %0";
"App color" = "Колір програми";
"Wired color" = "Колір постійної пам’яті";
"Compressed color" = "Колір стисненої пам’яті";
"Free color" = "Колір вільної пам’яті";
"Free memory (less than)" = "Вільна пам'ять (менше ніж)";
"Swap size" = "Розмір swap";
"Free RAM is" = "Вільна оперативна пам'ять %0";
// Disk
"Show removable disks" = "Показати зйомні диски";
"Used disk memory" = "Використано %0 з %1";
"Free disk memory" = "Вільно %0 з %1";
"Disk to show" = "Активний диск";
"Open disk" = "Відкрити диск";
"Switch view" = "Переключити вигляд";
"Disk utilization threshold" = "Поріг використання диска";
"Disk utilization is" = "Використання диска %0";
"Read color" = "Колір зчитування";
"Write color" = "Колір запису";
"Disk usage" = "Використання диска";
"Total read" = "Всього прочитано";
"Total written" = "Всього записано";
"Write speed" = "Запис";
"Read speed" = "Зчитування";
"Drives" = "Диски";
"SMART data" = "SMART дані";
// Sensors
"Temperature unit" = "Одиниця виміру температури";
"Celsius" = "Цельсія";
"Fahrenheit" = "Фаренгейта";
"Save the fan speed" = "Зберегти швидкість вентилятора";
"Fan" = "Вентилятор";
"HID sensors" = "HID датчики";
"Synchronize fan's control" = "Синхронізація керування вентиляторами";
"Current" = "Струм";
"Energy" = "Енергія";
"Show unknown sensors" = "Показати невідомі датчики";
"Install fan helper" = "Встановити помічник для вентилятора";
"Uninstall fan helper" = "Видалити помічник для вентилятора";
"Fan value" = "Значення вентилятора";
"Turn off fan" = "Вимкнути вентилятор";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Ви збираєтеся вимкнути вентилятор. Це не рекомендована дія, яка може зламати ваш Mac. Ви впевнені, що хочете це зробити?";
"Sensor threshold" = "Поріг датчика";
"Left fan" = "Лівий";
"Right fan" = "Правий";
"Fastest fan" = "Найшвидший";
"Sensor to show" = "Активний датчик";
// Network
"Uploading" = "Висилання";
"Downloading" = "Завантаження";
"Public IP" = "Публічний IP";
"Local IP" = "Локальний IP";
"Interface" = "Інтерфейс";
"Physical address" = "Фізичний адрес";
"Refresh" = "Оновити";
"Click to copy public IP address" = "Натисніть, щоб скопіювати публічний IP";
"Click to copy local IP address" = "Натисніть, щоб скопіювати локальний IP";
"Click to copy wifi name" = "Натисніть, щоб скопіювати назву wifi";
"Click to copy mac address" = "Натисніть, щоб скопіювати фізичний адрес";
"No connection" = "Немає з'єднання";
"Network interface" = "Мережевий інтерфейс";
"Total download" = "Загально завантажено";
"Total upload" = "Загально вислано";
"Reader type" = "Метод читання";
"Interface based" = "Інтерфейс";
"Processes based" = "Процеси";
"Reset data usage" = "Скинути використання даних";
"VPN mode" = "Режим VPN";
"Standard" = "Стандарт";
"Security" = "Шифрування";
"Channel" = "Канал";
"Common scale" = "Загальна шкала";
"Autodetection" = "Автовизначення";
"Widget activation threshold" = "Поріг активації віджета";
"Internet connection" = "Підключення до інтернету";
"Active state color" = "Колір активного стану";
"Nonactive state color" = "Колір неактивного стану";
"Connectivity host (ICMP)" = "Хост підключення (ICMP)";
"Leave empty to disable the check" = "Залиште пустим, щоб вимкнути перевірку";
"Connectivity history" = "Історія підключення";
"Auto-refresh public IP address" = "Автоматичне оновлення IP-адреси";
"Every hour" = "Щогодини";
"Every 12 hours" = "Кожні 12 годин";
"Every 24 hours" = "Кожні 24 години";
"Network activity" = "Мережева активність";
"Last reset" = "Останнє скидання %0 тому";
"Latency" = "Затримка";
"Upload speed" = "Висилання";
"Download speed" = "Завантаження";
"Address" = "Адреса";
"WiFi network" = "Мережа WiFi";
"Local IP changed" = "Локальна IP-адреса була змінена";
"Public IP changed" = "Публічна IP-адреса була змінена";
"Previous IP" = "Попередня IP-адреса: %0";
"New IP" = "Нова IP-адреса: %0";
"Internet connection lost" = "З’єднання з інтернетом втрачено";
"Internet connection established" = "З’єднання з інтернетом відновлено";
// Battery
"Level" = "Рівень заряду";
"Source" = "Джерело";
"AC Power" = "Мережа";
"Battery Power" = "Акумулятор";
"Time" = "Час";
"Health" = "Стан акумулятора";
"Amperage" = "Сила струму";
"Voltage" = "Напруга";
"Cycles" = "Кількість циклів";
"Temperature" = "Температура";
"Power adapter" = "Блок живлення";
"Power" = "Потужність";
"Is charging" = "Заряджається";
"Time to discharge" = "Час до розрядки";
"Time to charge" = "Час до зарядки";
"Calculating" = "Обчислення";
"Fully charged" = "Повністю заряджена";
"Not connected" = "Не під'єднаний";
"Low level notification" = "Повідомлення про низький рівень заряду";
"High level notification" = "Повідомлення про високий рівень заряду";
"Low battery" = "Низький заряд акумулятора";
"High battery" = "Високий заряд акумулятора";
"Battery remaining" = "%0% залишилось";
"Battery remaining to full charge" = "%0% до повного заряду";
"Percentage" = "Проценти";
"Percentage and time" = "Проценти і час";
"Time and percentage" = "Час і проценти";
"Time format" = "Формат часу";
"Hide additional information when full" = "Приховати додаткову інформацію, якщо акумулятор заряджений";
"Last charge" = "Остання зарядка";
"Capacity" = "Ємність";
"current / maximum / designed" = "поточна / максимальна / запроектована";
"Low power mode" = "Режим низької потужності";
"Percentage inside the icon" = "Відсоток всередині віджету";
"Colorize battery" = "Розфарбувати акумулятор";
"Charging current" = "Струм зарядки";
"Charging Voltage" = "Напруга зарядки";
"Charger state inside the battery" = "Стан зарядки всередині акумулятора";
// Bluetooth
"Battery to show" = "Активна батарея";
"No Bluetooth devices are available" = "Немає пристроїв Bluetooth";
// Clock
"Time zone" = "Часовий пояс";
"Local" = "Місцевий";
"Calendar" = "Календар";
"Show week numbers" = "Показати номери тижнів";
"Local time" = "Місцевий час";
"Add new clock" = "Додати новий годинник";
"Delete selected clock" = "Видалити вибраний годинник";
"Help with datetime format" = "Довідка з форматом дати й часу";
// Colors
"Based on utilization" = "На основі використання";
"Based on pressure" = "На основі тиску";
"Based on cluster" = "На основі кластера";
"System accent" = "Колір системи";
"Monochrome accent" = "Монохромний акцент";
"Clear" = "Прозорий";
"White" = "Білий";
"Black" = "Чорний";
"Gray" = "Сірий";
"Second gray" = "Інший сірий";
"Dark gray" = "Темно-сірий";
"Light gray" = "Світло-сірий";
"Red" = "Червоний";
"Second red" = "Інший червоний";
"Green" = "Зелений";
"Second green" = "Червоний зелений";
"Blue" = "Блакитний";
"Second blue" = "Інший блакитний";
"Yellow" = "Жовтий";
"Second yellow" = "Інший жовтий";
"Orange" = "Помаранчевий";
"Second orange" = "Інший помаранчевий";
"Purple" = "Фіолетовий";
"Second purple" = "Інший фіолетовий";
"Brown" = "Коричневий";
"Second brown" = "Інший коричневий";
"Cyan" = "Циан";
"Magenta" = "Пурпуровий";
"Pink" = "Рожевий";
"Teal" = "Чиряковий";
"Indigo" = "Індіго";
================================================
FILE: Stats/Supporting Files/vi.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "Mở cài đặt CPU";
"GPU" = "GPU";
"Open GPU settings" = "Mở cài đặt GPU";
"RAM" = "RAM";
"Open RAM settings" = "Mở cài đặt RAM";
"Disk" = "Ổ đĩa";
"Open Disk settings" = "Mở cài đặt ổ đĩa";
"Sensors" = "Cảm biến";
"Open Sensors settings" = "Mở cài đặt cảm biến";
"Network" = "Mạng";
"Open Network settings" = "Mở cài đặt mạng";
"Battery" = "Pin";
"Open Battery settings" = "Mở cài đặt Pin";
"Bluetooth" = "Bluetooth";
"Open Bluetooth settings" = "Mở cài đặt Bluetooth";
"Clock" = "Đồng hồ";
"Open Clock settings" = "Mở cài đặt đồng hồ";
// Words
"Unknown" = "Không xác định";
"Version" = "Phiên bản";
"Processor" = "Bộ xử lý";
"Memory" = "Bộ nhớ";
"Graphics" = "Đồ họa";
"Close" = "Đóng";
"Download" = "Tải xuống";
"Install" = "Cài đặt";
"Cancel" = "Hủy";
"Unavailable" = "Không khả dụng";
"Yes" = "Có";
"No" = "Không";
"Automatic" = "Tự động";
"Manual" = "Thủ công";
"None" = "Không có";
"Dots" = "Dấu chấm";
"Arrows" = "Mũi tên";
"Characters" = "Ký tự";
"Short" = "Ngắn";
"Long" = "Dài";
"Statistics" = "Thống kê";
"Max" = "Tối đa";
"Min" = "Tối thiểu";
"Reset" = "Đặt lại";
"Alignment" = "Căn chỉnh";
"Left alignment" = "Căn trái";
"Center alignment" = "Căn giữa";
"Right alignment" = "Căn phải";
"Dashboard" = "Bảng điều khiển";
"Enabled" = "Đã bật";
"Disabled" = "Đã tắt";
"Silent" = "Im lặng";
"Units" = "Đơn vị";
"Fans" = "Quạt";
"Scaling" = "Tỷ lệ";
"Linear" = "Tuyến tính";
"Square" = "Bình phương";
"Cube" = "Lập phương";
"Logarithmic" = "Logarit";
"Fixed scale" = "Tỷ lệ cố định";
"Cores" = "Nhân";
"Settings" = "Cài đặt";
"Name" = "Tên";
"Format" = "Định dạng";
"Turn off" = "Tắt";
"Normal" = "Bình thường";
"Warning" = "Cảnh báo";
"Critical" = "Nguy cấp";
"Usage" = "Sử dụng";
"2 minutes" = "2 phút";
"3 minutes" = "3 phút";
"10 minutes" = "10 phút";
"Import" = "Nhập";
"Export" = "Xuất";
"Separator" = "Phân cách";
"Read" = "Đọc";
"Write" = "Ghi";
"Frequency" = "Tần số";
"Save" = "Lưu";
"Run" = "Chạy";
"Stop" = "Dừng";
"Uninstall" = "Gỡ cài đặt";
"1 sec" = "1 giây";
"2 sec" = "2 giây";
"3 sec" = "3 giây";
"5 sec" = "5 giây";
"10 sec" = "10 giây";
"15 sec" = "15 giây";
"30 sec" = "30 giây";
"60 sec" = "60 giây";
// Setup
"Stats Setup" = "Thiết lập Stats";
"Previous" = "Trước";
"Previous page" = "Trang trước";
"Next" = "Tiếp";
"Next page" = "Trang tiếp theo";
"Finish" = "Hoàn tất";
"Finish setup" = "Hoàn tất thiết lập";
"Welcome to Stats" = "Chào mừng đến với Stats";
"welcome_message" = "Cảm ơn vì đã sử dụng Stats, công cụ giám sát hệ thống macOS miễn phí mã nguồn mở trên thanh menu.";
"Start the application automatically when starting your Mac" = "Tự động khởi động ứng dụng khi bật Mac của bạn";
"Do not start the application automatically when starting your Mac" = "Không tự động khởi động ứng dụng khi bật Mac của bạn";
"Do everything silently in the background (recommended)" = "Thực hiện mọi thứ âm thầm trong nền (khuyến nghị)";
"Check for a new version on startup" = "Kiểm tra phiên bản mới khi khởi động";
"Check for a new version every day (once a day)" = "Kiểm tra phiên bản mới mỗi ngày (một lần một ngày)";
"Check for a new version every week (once a week)" = "Kiểm tra phiên bản mới mỗi tuần (một lần một tuần)";
"Check for a new version every month (once a month)" = "Kiểm tra phiên bản mới mỗi tháng (một lần một tháng)";
"Never check for updates (not recommended)" = "Không bao giờ kiểm tra cập nhật (không khuyến nghị)";
"Anonymous telemetry for better development decisions" = "Dữ liệu ẩn danh để cải thiện quyết định phát triển";
"Share anonymous telemetry data" = "Chia sẻ dữ liệu ẩn danh";
"Do not share anonymous telemetry data" = "Không chia sẻ dữ liệu ẩn danh";
"The configuration is completed" = "Cấu hình đã hoàn tất";
"finish_setup_message" = "Tất cả đã được thiết lập! \n Stats là một công cụ mã nguồn mở miễn phí và sẽ luôn như vậy. \n Nếu bạn yêu thích, hãy ủng hộ dự án, điều đó luôn được đánh giá cao!";
// Alerts
"New version available" = "Có phiên bản mới";
"Click to install the new version of Stats" = "Nhấn để cài đặt phiên bản mới của Stats";
"Successfully updated" = "Cập nhật thành công";
"Stats was updated to v" = "Stats đã được cập nhật lên v%0";
"Reset settings text" = "Tất cả cài đặt ứng dụng sẽ được đặt lại và ứng dụng sẽ khởi động lại. Bạn có chắc chắn muốn thực hiện không?";
"Support text" = "Cảm ơn bạn đã sử dụng Stats!\n\n Việc duy trì và cải thiện dự án nguồn mở này cần có thời gian và nguồn lực. Sự hỗ trợ của bạn giúp chúng tôi tiếp tục cung cấp một ứng dụng miễn phí và đáng tin cậy cho mọi người.\n\nNếu bạn thấy Stats hữu ích, vui lòng cân nhắc đóng góp. Mỗi đóng góp nhỏ đều có ích!";
// Settings
"Open Activity Monitor" = "Mở Activity Monitor";
"Report a bug" = "Báo cáo lỗi";
"Support the application" = "Hỗ trợ ứng dụng";
"Close application" = "Đóng ứng dụng";
"Open application settings" = "Mở cài đặt ứng dụng";
"Open dashboard" = "Mở bảng điều khiển";
"No notifications available in this module" = "Không có thông báo nào trong module này";
"Open Calendar" = "Mở Lịch";
"Toggle the module" = "Bật/tắt module";
// Application settings
"Update application" = "Cập nhật ứng dụng";
"Check for updates" = "Kiểm tra cập nhật";
"At start" = "Khi khởi động";
"Once per day" = "Một lần mỗi ngày";
"Once per week" = "Một lần mỗi tuần";
"Once per month" = "Một lần mỗi tháng";
"Never" = "Không bao giờ";
"Check for update" = "Kiểm tra cập nhật";
"Show icon in dock" = "Hiển thị biểu tượng trong Dock";
"Start at login" = "Khởi động cùng đăng nhập";
"Build number" = "Số bản dựng";
"Import settings" = "Nhập cài đặt";
"Export settings" = "Xuất cài đặt";
"Reset settings" = "Đặt lại cài đặt";
"Pause the Stats" = "Tạm dừng Stats";
"Resume the Stats" = "Tiếp tục Stats";
"Combined modules" = "Các module kết hợp";
"Combined details" = "Chi tiết kết hợp";
"Spacing" = "Khoảng cách";
"Share anonymous telemetry" = "Chia sẻ dữ liệu ẩn danh";
"Choose file" = "Chọn tập tin";
"Stress tests" = "Kiểm tra áp lực";
// Dashboard
"Serial number" = "Số serial";
"Model identifier" = "Mã định danh model";
"Production year" = "Năm sản xuất";
"Uptime" = "Thời gian hoạt động";
"Number of cores" = "%0 nhân";
"Number of threads" = "%0 luồng";
"Number of e-cores" = "%0 nhân tiết kiệm";
"Number of p-cores" = "%0 nhân hiệu năng";
"Disks" = "Ổ cứng"; // translategemma:4b
"Display" = "Hiển thị"; // translategemma:4b
// Update
"The latest version of Stats installed" = "Phiên bản Stats mới nhất đã được cài đặt";
"Downloading..." = "Đang tải xuống...";
"Current version: " = "Phiên bản hiện tại: ";
"Latest version: " = "Phiên bản mới nhất: ";
// Widgets
"Color" = "Màu sắc";
"Label" = "Nhãn";
"Box" = "Hộp";
"Frame" = "Khung";
"Value" = "Giá trị";
"Colorize" = "Tô màu";
"Colorize value" = "Tô màu giá trị";
"Additional information" = "Thông tin bổ sung";
"Reverse values order" = "Đảo thứ tự giá trị";
"Base" = "Cơ sở";
"Display mode" = "Chế độ hiển thị";
"One row" = "Một hàng";
"Two rows" = "Hai hàng";
"Mini widget" = "Widget nhỏ";
"Line chart widget" = "Biểu đồ đường";
"Bar chart widget" = "Biểu đồ cột";
"Pie chart widget" = "Biểu đồ tròn";
"Network chart widget" = "Biểu đồ mạng";
"Speed widget" = "Widget tốc độ";
"Battery widget" = "Widget pin";
"Stack widget" = "Widget xếp chồng";
"Memory widget" = "Widget bộ nhớ";
"Static width" = "Chiều rộng cố định";
"Tachometer widget" = "Widget đo tốc độ";
"State widget" = "Widget trạng thái";
"Text widget" = "Bộ điều khiển văn bản"; // translategemma:4b
"Battery details widget" = "Widget hiển thị thông tin pin"; // translategemma:4b
"Show symbols" = "Hiển thị ký hiệu";
"Label widget" = "Widget nhãn";
"Number of reads in the chart" = "Số lần đọc trong biểu đồ";
"Color of download" = "Màu tải xuống";
"Color of upload" = "Màu tải lên";
"Monospaced font" = "Phông chữ đơn cách";
"Reverse order" = "Đảo ngược thứ tự";
"Chart history" = "Lịch sử biểu đồ";
"Default color" = "Màu mặc định";
"Transparent when no activity" = "Trong suốt khi không có hoạt động";
"Constant color" = "Màu cố định";
// Module Kit
"Open module settings" = "Mở cài đặt module";
"Select widget" = "Chọn widget %0";
"Open widget settings" = "Mở cài đặt widget";
"Update interval" = "Khoảng thời gian cập nhật";
"Usage history" = "Lịch sử sử dụng";
"Details" = "Chi tiết";
"Top processes" = "Các tiến trình hàng đầu";
"Pictogram" = "Biểu tượng";
"Module" = "Mô-đun"; // translategemma:4b
"Widgets" = "Widget"; // translategemma:4b
"Popup" = "Hiển thị cửa sổ"; // translategemma:4b
"Notifications" = "Thông báo";
"Merge widgets" = "Gộp các widget";
"No available widgets to configure" = "Không có widget nào để cấu hình";
"No options to configure for the popup in this module" = "Không có tùy chọn để cấu hình popup trong module này";
"Process" = "Tiến trình";
"Kill process" = "Dừng tiến trình";
"Keyboard shortcut" = "Phím tắt";
"Listening..." = "Đang lắng nghe...";
// Modules
"Number of top processes" = "Số lượng tiến trình hàng đầu";
"Update interval for top processes" = "Khoảng thời gian cập nhật cho các tiến trình hàng đầu";
"Notification level" = "Mức độ thông báo";
"Chart color" = "Màu biểu đồ";
"Main chart scaling" = "Tỷ lệ biểu đồ chính";
"Scale value" = "Giá trị tỷ lệ";
"Text widget value" = "Giá trị của widget văn bản"; // translategemma:4b
// CPU
"CPU usage" = "Sử dụng CPU";
"CPU temperature" = "Nhiệt độ CPU";
"CPU frequency" = "Tần số CPU";
"System" = "Hệ thống";
"User" = "Người dùng";
"Idle" = "Nhàn rỗi";
"Show usage per core" = "Hiển thị sử dụng theo từng nhân";
"Show hyper-threading cores" = "Hiển thị các nhân hyper-threading";
"Split the value (System/User)" = "Tách giá trị (Hệ thống/Người dùng)";
"Scheduler limit" = "Giới hạn bộ lập lịch";
"Speed limit" = "Giới hạn tốc độ";
"Average load" = "Tải trung bình";
"1 minute" = "1 phút";
"5 minutes" = "5 phút";
"15 minutes" = "15 phút";
"CPU usage threshold" = "Ngưỡng sử dụng CPU";
"CPU usage is" = "Sử dụng CPU là %0";
"Efficiency cores" = "Nhân tiết kiệm";
"Performance cores" = "Nhân hiệu năng";
"System color" = "Màu hệ thống";
"User color" = "Màu người dùng";
"Idle color" = "Màu nhàn rỗi";
"Cluster grouping" = "Nhóm cụm";
"Efficiency cores color" = "Màu nhân tiết kiệm";
"Performance cores color" = "Màu nhân hiệu năng";
"Total load" = "Tải tổng";
"System load" = "Tải hệ thống";
"User load" = "Tải người dùng";
"Efficiency cores load" = "Tải nhân tiết kiệm";
"Performance cores load" = "Tải nhân hiệu năng";
"All cores" = "Tất cả các nhân";
// GPU
"GPU to show" = "GPU hiển thị";
"Show GPU type" = "Hiển thị loại GPU";
"GPU enabled" = "GPU đã bật";
"GPU disabled" = "GPU đã tắt";
"GPU temperature" = "Nhiệt độ GPU";
"GPU utilization" = "Mức sử dụng GPU";
"Vendor" = "Nhà cung cấp";
"Model" = "Mô hình";
"Status" = "Trạng thái";
"Active" = "Hoạt động";
"Non active" = "Không hoạt động";
"Fan speed" = "Tốc độ quạt";
"Core clock" = "Xung nhịp lõi";
"Memory clock" = "Xung nhịp bộ nhớ";
"Utilization" = "Sử dụng";
"Render utilization" = "Mức sử dụng kết xuất";
"Tiler utilization" = "Mức sử dụng tiler";
"GPU usage threshold" = "Ngưỡng sử dụng GPU";
"GPU usage is" = "Sử dụng GPU là %0";
// RAM
"Memory usage" = "Sử dụng bộ nhớ";
"Memory pressure" = "Áp lực bộ nhớ";
"Total" = "Tổng";
"Used" = "Đã sử dụng";
"App" = "Ứng dụng";
"Wired" = "Có dây";
"Compressed" = "Đã nén";
"Free" = "Trống";
"Swap" = "Bộ nhớ ảo";
"Split the value (App/Wired/Compressed)" = "Tách giá trị (Ứng dụng/Có dây/Đã nén)";
"RAM utilization threshold" = "Ngưỡng sử dụng RAM";
"RAM utilization is" = "Sử dụng RAM là %0";
"App color" = "Màu ứng dụng";
"Wired color" = "Màu có dây";
"Compressed color" = "Màu đã nén";
"Free color" = "Màu trống";
"Free memory (less than)" = "Bộ nhớ trống (nhỏ hơn)";
"Swap size" = "Dung lượng bộ nhớ ảo";
"Free RAM is" = "RAM trống là %0";
// Disk
"Show removable disks" = "Hiển thị các ổ đĩa rời";
"Used disk memory" = "Đã sử dụng %0 trên tổng %1";
"Free disk memory" = "Còn trống %0 trên tổng %1";
"Disk to show" = "Ổ đĩa để hiển thị";
"Open disk" = "Mở ổ đĩa";
"Switch view" = "Chuyển đổi chế độ xem";
"Disk utilization threshold" = "Ngưỡng sử dụng ổ đĩa";
"Disk utilization is" = "Sử dụng ổ đĩa là %0";
"Read color" = "Màu đọc";
"Write color" = "Màu ghi";
"Disk usage" = "Sử dụng ổ đĩa";
"Total read" = "Tổng dữ liệu đọc";
"Total written" = "Tổng dữ liệu ghi";
"Write speed" = "Tốc độ ghi";
"Read speed" = "Tốc độ đọc";
"Drives" = "Ổ đĩa";
"SMART data" = "Dữ liệu SMART";
// Sensors
"Temperature unit" = "Đơn vị nhiệt độ";
"Celsius" = "Độ C";
"Fahrenheit" = "Độ F";
"Save the fan speed" = "Lưu tốc độ quạt";
"Fan" = "Quạt";
"HID sensors" = "Cảm biến HID";
"Synchronize fan's control" = "Đồng bộ điều khiển quạt";
"Current" = "Dòng điện";
"Energy" = "Năng lượng";
"Show unknown sensors" = "Hiển thị các cảm biến không xác định";
"Install fan helper" = "Cài đặt trợ lý quạt";
"Uninstall fan helper" = "Gỡ cài đặt trợ lý quạt";
"Fan value" = "Giá trị quạt";
"Turn off fan" = "Tắt quạt";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "Bạn sắp tắt quạt. Đây là hành động không được khuyến nghị và có thể gây hư hại cho Mac của bạn. Bạn có chắc chắn muốn thực hiện không?";
"Sensor threshold" = "Ngưỡng cảm biến";
"Left fan" = "Quạt bên trái";
"Right fan" = "Quạt bên phải";
"Fastest fan" = "Quạt nhanh nhất";
"Sensor to show" = "Cảm biến hiển thị";
// Network
"Uploading" = "Đang tải lên";
"Downloading" = "Đang tải xuống";
"Public IP" = "IP công khai";
"Local IP" = "IP nội bộ";
"Interface" = "Giao diện";
"Physical address" = "Địa chỉ vật lý";
"Refresh" = "Làm mới";
"Click to copy public IP address" = "Nhấn để sao chép địa chỉ IP công khai";
"Click to copy local IP address" = "Nhấn để sao chép địa chỉ IP nội bộ";
"Click to copy wifi name" = "Nhấn để sao chép tên wifi";
"Click to copy mac address" = "Nhấn để sao chép địa chỉ mac";
"No connection" = "Không có kết nối";
"Network interface" = "Giao diện mạng";
"Total download" = "Tổng dữ liệu tải xuống";
"Total upload" = "Tổng dữ liệu tải lên";
"Reader type" = "Loại đọc";
"Interface based" = "Dựa trên giao diện";
"Processes based" = "Dựa trên tiến trình";
"Reset data usage" = "Đặt lại dữ liệu sử dụng";
"VPN mode" = "Chế độ VPN";
"Standard" = "Chuẩn";
"Security" = "Bảo mật";
"Channel" = "Kênh";
"Common scale" = "Thang đo chung";
"Autodetection" = "Tự phát hiện";
"Widget activation threshold" = "Ngưỡng kích hoạt widget";
"Internet connection" = "Kết nối internet";
"Active state color" = "Màu trạng thái hoạt động";
"Nonactive state color" = "Màu trạng thái không hoạt động";
"Connectivity host (ICMP)" = "Máy chủ kết nối (ICMP)";
"Leave empty to disable the check" = "Để trống để tắt kiểm tra";
"Connectivity history" = "Lịch sử kết nối";
"Auto-refresh public IP address" = "Tự làm mới địa chỉ IP công khai";
"Every hour" = "Mỗi giờ";
"Every 12 hours" = "Mỗi 12 giờ";
"Every 24 hours" = "Mỗi 24 giờ";
"Network activity" = "Hoạt động mạng";
"Last reset" = "Đặt lại lần cuối cách đây %0";
"Latency" = "Độ trễ";
"Upload speed" = "Tốc độ tải lên";
"Download speed" = "Tốc độ tải xuống";
"Address" = "Địa chỉ";
"WiFi network" = "Mạng WiFi";
"Local IP changed" = "IP nội bộ đã thay đổi";
"Public IP changed" = "IP công khai đã thay đổi";
"Previous IP" = "IP trước đây: %0";
"New IP" = "IP mới: %0";
"Internet connection lost" = "Mất kết nối internet";
"Internet connection established" = "Kết nối internet được thiết lập";
// Battery
"Level" = "Mức";
"Source" = "Nguồn";
"AC Power" = "Nguồn AC";
"Battery Power" = "Nguồn pin";
"Time" = "Thời gian";
"Health" = "Tình trạng";
"Amperage" = "Cường độ dòng điện";
"Voltage" = "Điện áp";
"Cycles" = "Chu kỳ";
"Temperature" = "Nhiệt độ";
"Power adapter" = "Bộ chuyển đổi nguồn";
"Power" = "Nguồn điện";
"Is charging" = "Đang sạc";
"Time to discharge" = "Thời gian sử dụng còn lại";
"Time to charge" = "Thời gian để sạc đầy";
"Calculating" = "Đang tính toán";
"Fully charged" = "Đã sạc đầy";
"Not connected" = "Chưa kết nối";
"Low level notification" = "Thông báo pin yếu";
"High level notification" = "Thông báo pin đầy";
"Low battery" = "Pin yếu";
"High battery" = "Pin đầy";
"Battery remaining" = "Còn lại %0%";
"Battery remaining to full charge" = "Còn lại %0% để sạc đầy";
"Percentage" = "Phần trăm";
"Percentage and time" = "Phần trăm và thời gian";
"Time and percentage" = "Thời gian và phần trăm";
"Time format" = "Định dạng thời gian";
"Hide additional information when full" = "Ẩn thông tin bổ sung khi pin đầy";
"Last charge" = "Lần sạc cuối";
"Capacity" = "Dung lượng";
"current / maximum / designed" = "hiện tại / tối đa / thiết kế";
"Low power mode" = "Chế độ tiết kiệm pin";
"Percentage inside the icon" = "Phần trăm hiển thị trong biểu tượng";
"Colorize battery" = "Tô màu pin";
"Charging current" = "Dòng sạc";
"Charging Voltage" = "Điện áp sạc";
"Charger state inside the battery" = "Trạng thái sạc bên trong pin";
// Bluetooth
"Battery to show" = "Hiển thị pin";
"No Bluetooth devices are available" = "Không có thiết bị Bluetooth nào khả dụng";
// Clock
"Time zone" = "Múi giờ";
"Local" = "Địa phương";
"Calendar" = "Lịch";
"Show week numbers" = "Hiển thị số thứ tự tuần"; // translategemma:4b
"Local time" = "Giờ địa phương";
"Add new clock" = "Thêm đồng hồ mới";
"Delete selected clock" = "Xóa đồng hồ đã chọn";
"Help with datetime format" = "Trợ giúp về định dạng ngày giờ";
// Colors
"Based on utilization" = "Dựa trên mức sử dụng";
"Based on pressure" = "Dựa trên áp lực";
"Based on cluster" = "Dựa trên cụm";
"System accent" = "Điểm nhấn hệ thống";
"Monochrome accent" = "Điểm nhấn đơn sắc";
"Clear" = "Xóa";
"White" = "Trắng";
"Black" = "Đen";
"Gray" = "Xám";
"Second gray" = "Xám nhạt";
"Dark gray" = "Xám đậm";
"Light gray" = "Xám sáng";
"Red" = "Đỏ";
"Second red" = "Đỏ nhạt";
"Green" = "Xanh lá";
"Second green" = "Xanh lá nhạt";
"Blue" = "Xanh dương";
"Second blue" = "Xanh dương nhạt";
"Yellow" = "Vàng";
"Second yellow" = "Vàng nhạt";
"Orange" = "Cam";
"Second orange" = "Cam nhạt";
"Purple" = "Tím";
"Second purple" = "Tím nhạt";
"Brown" = "Nâu";
"Second brown" = "Nâu nhạt";
"Cyan" = "Xanh lam";
"Magenta" = "Hồng cánh sen";
"Pink" = "Hồng";
"Teal" = "Xanh lục lam";
"Indigo" = "Chàm";
================================================
FILE: Stats/Supporting Files/zh-Hans.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "打开CPU设置";
"GPU" = "GPU";
"Open GPU settings" = "打开GPU设置";
"RAM" = "内存";
"Open RAM settings" = "打开内存设置";
"Disk" = "磁盘";
"Open Disk settings" = "打开磁盘设置";
"Sensors" = "传感器";
"Open Sensors settings" = "打开传感器设置";
"Network" = "网络";
"Open Network settings" = "打开网络设置";
"Battery" = "电池";
"Open Battery settings" = "打开电池设置";
"Bluetooth" = "蓝牙";
"Open Bluetooth settings" = "打开蓝牙设置";
"Clock" = "时钟";
"Open Clock settings" = "打开时钟设置";
// Words
"Unknown" = "未知";
"Version" = "版本";
"Processor" = "处理器";
"Memory" = "内存";
"Graphics" = "图形卡"; // Use the same translation as the "about this mac"
"Close" = "关闭";
"Download" = "下载";
"Install" = "安装";
"Cancel" = "取消";
"Unavailable" = "不可用";
"Yes" = "是";
"No" = "否";
"Automatic" = "自动";
"Manual" = "手动";
"None" = "无";
"Dots" = "点";
"Arrows" = "箭头";
"Characters" = "字母";
"Short" = "短";
"Long" = "长";
"Statistics" = "统计";
"Max" = "最大";
"Min" = "最小";
"Reset" = "重置";
"Alignment" = "对齐";
"Left alignment" = "左对齐";
"Center alignment" = "居中";
"Right alignment" = "右对齐";
"Dashboard" = "仪表盘";
"Enabled" = "已启用";
"Disabled" = "已停用";
"Silent" = "运行时静默";
"Units" = "单位";
"Fans" = "风扇";
"Scaling" = "缩放";
"Linear" = "线性";
"Square" = "平方";
"Cube" = "立方";
"Logarithmic" = "对数";
"Fixed scale" = "固定值";
"Cores" = "核心数";
"Settings" = "设置";
"Name" = "名称";
"Format" = "格式";
"Turn off" = "禁用";
"Normal" = "正常";
"Warning" = "警告";
"Critical" = "严重";
"Usage" = "用量";
"2 minutes" = "2 分钟";
"3 minutes" = "3 分钟";
"10 minutes" = "10 分钟";
"Import" = "导入";
"Export" = "导出";
"Separator" = "分隔符";
"Read" = "读取";
"Write" = "写入";
"Frequency" = "频率";
"Save" = "保存";
"Run" = "运行";
"Stop" = "停止";
"Uninstall" = "卸载";
"1 sec" = "1 秒";
"2 sec" = "2 秒";
"3 sec" = "3 秒";
"5 sec" = "5 秒";
"10 sec" = "10 秒";
"15 sec" = "15 秒";
"30 sec" = "30 秒";
"60 sec" = "60 秒";
// Setup
"Stats Setup" = "Stats设置";
"Previous" = "上一步";
"Previous page" = "上一页";
"Next" = "下一步";
"Next page" = "下一页";
"Finish" = "完成";
"Finish setup" = "完成设置";
"Welcome to Stats" = "欢迎使用Stats";
"welcome_message" = "感谢你使用Stats,一个免费的开源macOS菜单栏系统监视器。";
"Start the application automatically when starting your Mac" = "在启动你的Mac时自动开启应用程序";
"Do not start the application automatically when starting your Mac" = "在启动你的Mac时不要自动开启应用程序";
"Do everything silently in the background (recommended)" = "在后台静默运行(推荐)";
"Check for a new version on startup" = "启动时检查新版本";
"Check for a new version every day (once a day)" = "每天检查一次新版本(一天一次)";
"Check for a new version every week (once a week)" = "每周检查一次新版本(一周一次)";
"Check for a new version every month (once a month)" = "每月检查一次新版本(一月一次)";
"Never check for updates (not recommended)" = "不要检查更新(不推荐)";
"Anonymous telemetry for better development decisions" = "匿名诊断数据将用于改善开发决策";
"Share anonymous telemetry data" = "分享匿名诊断数据";
"Do not share anonymous telemetry data" = "不分享匿名诊断数据";
"The configuration is completed" = "配置完成";
"finish_setup_message" = "设置完成!Stats是一个开源且永远免费的工具。如果你喜欢它,你可以支持这个项目,非常感谢!";
// Alerts
"New version available" = "新版本可用";
"Click to install the new version of Stats" = "点击安装Stats的新版本";
"Successfully updated" = "更新成功";
"Stats was updated to v" = "Stats已更新到 v%0";
"Reset settings text" = "应用程序的所有设置将会被重置,应用程序也将重新启动。你确定要这么做吗?";
"Support text" = "感谢您使用Stats!\n\n 维护和改进这个开源项目需要花费时间和资源。您的支持能帮助我们继续给每个人提供这样免费且可靠的应用程序。\n\n如果您觉得Stats对您有帮助,请考虑给我们一份捐助。即使是一点一滴都有很大的帮助!";
// Settings
"Open Activity Monitor" = "打开活动监视器";
"Report a bug" = "报告漏洞";
"Support the application" = "支持本应用";
"Close application" = "关闭应用";
"Open application settings" = "打开应用设置";
"Open dashboard" = "打开仪表盘";
"No notifications available in this module" = "该模块不支持通知";
"Open Calendar" = "打开日历";
"Toggle the module" = "切换模块显示";
// Application settings
"Update application" = "更新应用";
"Check for updates" = "检查更新";
"At start" = "启动时";
"Once per day" = "每日一次";
"Once per week" = "每周一次";
"Once per month" = "每月一次";
"Never" = "从不";
"Check for update" = "检查更新";
"Show icon in dock" = "在程序坞显示图标";
"Start at login" = "登录时打开";
"Build number" = "版本号";
"Import settings" = "导入设置";
"Export settings" = "导出设置";
"Reset settings" = "初始化所有设置";
"Pause the Stats" = "暂停Stats";
"Resume the Stats" = "恢复Stats";
"Combined modules" = "合并模块";
"Combined details" = "合并详情";
"Spacing" = "间距";
"Share anonymous telemetry" = "分享匿名诊断数据";
"Choose file" = "选择文件";
"Stress tests" = "压力测试";
// Dashboard
"Serial number" = "序列号";
"Model identifier" = "机型标识符";
"Production year" = "生产年份";
"Uptime" = "启动时间";
"Number of cores" = "%0 核心";
"Number of threads" = "%0 线程";
"Number of e-cores" = "%0 能效核心";
"Number of p-cores" = "%0 性能核心";
"Disks" = "磁盘"; // translategemma:4b
"Display" = "显示"; // translategemma:4b
// Update
"The latest version of Stats installed" = "已安装最新版Stats";
"Downloading..." = "下载中...";
"Current version: " = "当前版本:";
"Latest version: " = "最新版本:";
// Widgets
"Color" = "颜色";
"Label" = "标签";
"Box" = "底盒";
"Frame" = "外框";
"Value" = "数值";
"Colorize" = "彩色";
"Colorize value" = "数值颜色";
"Additional information" = "附加信息";
"Reverse values order" = "数值反序";
"Base" = "单位";
"Display mode" = "显示模式";
"One row" = "单行";
"Two rows" = "双行";
"Mini widget" = "迷你";
"Line chart widget" = "折线图";
"Bar chart widget" = "柱状图";
"Pie chart widget" = "饼图";
"Network chart widget" = "流量图";
"Speed widget" = "速度";
"Battery widget" = "电池";
"Stack widget" = "叠放";
"Memory widget" = "容量";
"Static width" = "固定宽度";
"Tachometer widget" = "转速计";
"State widget" = "状态";
"Text widget" = "文本";
"Battery details widget" = "电池详情";
"Show symbols" = "显示标识";
"Label widget" = "标签";
"Number of reads in the chart" = "图表显示的读取次数";
"Color of download" = "下载颜色";
"Color of upload" = "上传颜色";
"Monospaced font" = "等宽字体";
"Reverse order" = "调转顺序";
"Chart history" = "图表统计时长";
"Default color" = "默认";
"Transparent when no activity" = "无活动时保持透明";
"Constant color" = "恒定颜色";
// Module Kit
"Open module settings" = "打开模块设置";
"Select widget" = "选择%0小组件";
"Open widget settings" = "打开小组件设置";
"Update interval" = "更新间隔";
"Usage history" = "负载历史";
"Details" = "详细信息";
"Top processes" = "高占用进程";
"Pictogram" = "标识";
"Module" = "模块";
"Widgets" = "小组件";
"Popup" = "弹出窗口";
"Notifications" = "通知";
"Merge widgets" = "合并组件";
"No available widgets to configure" = "没有可配置的小组件";
"No options to configure for the popup in this module" = "该组件没有可配置的选项";
"Process" = "进程";
"Kill process" = "结束进程";
"Keyboard shortcut" = "键盘快捷键";
"Listening..." = "监听中...";
// Modules
"Number of top processes" = "高占用进程数";
"Update interval for top processes" = "高占用进程的更新间隔";
"Notification level" = "通知级别";
"Chart color" = "图表颜色";
"Main chart scaling" = "主图表缩放";
"Scale value" = "缩放值";
"Text widget value" = "文本小组件值";
// CPU
"CPU usage" = "CPU占用";
"CPU temperature" = "CPU温度";
"CPU frequency" = "CPU频率";
"System" = "系统";
"User" = "用户";
"Idle" = "闲置";
"Show usage per core" = "显示每个物理核心占用";
"Show hyper-threading cores" = "显示超线程核心";
"Split the value (System/User)" = "柱状图区分显示(系统/用户)";
"Scheduler limit" = "调度限制";
"Speed limit" = "速度限制";
"Average load" = "平均负载";
"1 minute" = "1 分钟";
"5 minutes" = "5 分钟";
"15 minutes" = "15 分钟";
"CPU usage threshold" = "CPU占用阈值";
"CPU usage is" = "CPU利用率是 %0";
"Efficiency cores" = "能效核心";
"Performance cores" = "性能核心";
"System color" = "系统占用颜色";
"User color" = "用户占用颜色";
"Idle color" = "闲置颜色";
"Cluster grouping" = "按集群分组";
"Efficiency cores color" = "能效核心颜色";
"Performance cores color" = "性能核心颜色";
"Total load" = "总占用";
"System load" = "系统占用";
"User load" = "用户占用";
"Efficiency cores load" = "能效核心占用";
"Performance cores load" = "性能核心占用";
"All cores" = "所有核心";
// GPU
"GPU to show" = "显示的GPU";
"Show GPU type" = "显示GPU类型";
"GPU enabled" = "GPU启用";
"GPU disabled" = "GPU禁用";
"GPU temperature" = "GPU温度";
"GPU utilization" = "GPU利用率";
"Vendor" = "厂商";
"Model" = "型号";
"Status" = "状态";
"Active" = "活跃";
"Non active" = "非活跃";
"Fan speed" = "风扇速度";
"Core clock" = "核心频率";
"Memory clock" = "显存频率";
"Utilization" = "利用率";
"Render utilization" = "渲染利用率";
"Tiler utilization" = "Tiler利用率";
"GPU usage threshold" = "GPU占用阈值";
"GPU usage is" = "GPU利用率是 %0";
// RAM
"Memory usage" = "内存占用";
"Memory pressure" = "内存压力";
"Total" = "共计";
"Used" = "已用";
"App" = "App内存";
"Wired" = "联动内存";
"Compressed" = "被压缩";
"Free" = "可用";
"Swap" = "交换区";
"Split the value (App/Wired/Compressed)" = "柱状图区分显示(App内存/联动内存/被压缩)";
"RAM utilization threshold" = "RAM占用阈值";
"RAM utilization is" = "RAM利用率是 %0";
"App color" = "App内存颜色";
"Wired color" = "联动内存颜色";
"Compressed color" = "被压缩内存颜色";
"Free color" = "可用内存颜色";
"Free memory (less than)" = "可用内存(小于)";
"Swap size" = "已使用交换";
"Free RAM is" = "可有内存还有 %0";
// Disk
"Show removable disks" = "显示可移动磁盘";
"Used disk memory" = "%1 中已用 %0";
"Free disk memory" = "%1 中剩余 %0";
"Disk to show" = "显示的磁盘";
"Open disk" = "打开磁盘";
"Switch view" = "切换视图";
"Disk utilization threshold" = "磁盘占用阈值";
"Disk utilization is" = "磁盘利用率:%0";
"Read color" = "读取颜色";
"Write color" = "写入颜色";
"Disk usage" = "磁盘使用";
"Total read" = "总读取量";
"Total written" = "总写入量";
"Write speed" = "写入速度";
"Read speed" = "读取速度";
"Drives" = "驱动器";
"SMART data" = "SMART数据";
// Sensors
"Temperature unit" = "温度单位";
"Celsius" = "摄氏度";
"Fahrenheit" = "华氏度";
"Save the fan speed" = "保存风扇转速";
"Fan" = "风扇";
"HID sensors" = "HID 传感器";
"Synchronize fan's control" = "同步控制风扇";
"Current" = "电流";
"Energy" = "能耗";
"Show unknown sensors" = "显示未知传感器";
"Install fan helper" = "安装风扇控制程序";
"Uninstall fan helper" = "卸载风扇控制程序";
"Fan value" = "风扇转数";
"Turn off fan" = "禁用风扇";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "你想禁用你的风扇,这可能损害你的Mac。不推荐进行这个操作,你确定要继续吗?";
"Sensor threshold" = "传感器阈值";
"Left fan" = "左侧风扇";
"Right fan" = "右侧风扇";
"Fastest fan" = "最高转速风扇";
"Sensor to show" = "要显示的传感器";
// Network
"Uploading" = "上传";
"Downloading" = "下载";
"Public IP" = "公网IP";
"Local IP" = "本地IP";
"Interface" = "接口";
"Physical address" = "物理地址";
"Refresh" = "刷新";
"Click to copy public IP address" = "点击拷贝公网IP";
"Click to copy local IP address" = "点击拷贝本地IP";
"Click to copy wifi name" = "点击拷贝Wi-Fi名称";
"Click to copy mac address" = "点击拷贝MAC地址";
"No connection" = "无网络连接";
"Network interface" = "网络接口";
"Total download" = "总下载";
"Total upload" = "总上传";
"Reader type" = "读取器类型";
"Interface based" = "基于接口";
"Processes based" = "基于进程";
"Reset data usage" = "重置用量";
"VPN mode" = "VPN 模式";
"Standard" = "标准";
"Security" = "安全";
"Channel" = "频道";
"Common scale" = "统一比例";
"Autodetection" = "自动检测";
"Widget activation threshold" = "小组件激活阈值";
"Internet connection" = "网络连接状态";
"Active state color" = "活跃状态颜色";
"Nonactive state color" = "非活跃状态颜色";
"Connectivity host (ICMP)" = "连接 Host (ICMP)";
"Leave empty to disable the check" = "不在该选项内输入以禁用检查";
"Connectivity history" = "连接历史";
"Auto-refresh public IP address" = "自动刷新公网IP地址";
"Every hour" = "每小时";
"Every 12 hours" = "每12小时";
"Every 24 hours" = "每24小时";
"Network activity" = "网络活动";
"Last reset" = "上次重置于 %0 之前";
"Latency" = "延迟";
"Upload speed" = "上传速度";
"Download speed" = "下载速度";
"Address" = "地址";
"WiFi network" = "WiFi网络";
"Local IP changed" = "本地IP已变更";
"Public IP changed" = "公网IP已变更";
"Previous IP" = "原IP:%0";
"New IP" = "新IP:%0";
"Internet connection lost" = "互联网连接丢失";
"Internet connection established" = "互联网连接已建立";
// Battery
"Level" = "电量";
"Source" = "电源";
"AC Power" = "交流电";
"Battery Power" = "电池";
"Time" = "时间";
"Health" = "健康度";
"Amperage" = "电流";
"Voltage" = "电压";
"Cycles" = "循环数";
"Temperature" = "温度";
"Power adapter" = "电源适配器";
"Power" = "功率";
"Is charging" = "充电中";
"Time to discharge" = "电池剩余时间";
"Time to charge" = "充电所需时间";
"Calculating" = "计算中";
"Fully charged" = "已充满";
"Not connected" = "未连接";
"Low level notification" = "低电量通知";
"High level notification" = "高电量通知";
"Low battery" = "低电量";
"High battery" = "高电量";
"Battery remaining" = "剩余 %0%";
"Battery remaining to full charge" = "%0% 待充";
"Percentage" = "百分比";
"Percentage and time" = "百分比和时间";
"Time and percentage" = "时间和百分比";
"Time format" = "时间格式";
"Hide additional information when full" = "电池充满后隐藏其他信息";
"Last charge" = "最后一次充电";
"Capacity" = "容量";
"current / maximum / designed" = "当前容量 / 最大容量 / 设计容量";
"Low power mode" = "低电量模式";
"Percentage inside the icon" = "图标内百分比";
"Colorize battery" = "启用彩色电池图标";
"Charging current" = "充电电流";
"Charging Voltage" = "充电电压";
"Charger state inside the battery" = "在电池图标内显示充电器状态";
// Bluetooth
"Battery to show" = "显示的设备";
"No Bluetooth devices are available" = "没有可用的蓝牙设备";
// Clock
"Time zone" = "时区";
"Local" = "本地";
"Calendar" = "日历";
"Show week numbers" = "显示周数"; // translategemma:4b
"Local time" = "本地时间";
"Add new clock" = "添加新时钟";
"Delete selected clock" = "删除选定的时钟";
"Help with datetime format" = "日期时间格式帮助";
// Colors
"Based on utilization" = "基于利用率";
"Based on pressure" = "基于压力";
"Based on cluster" = "基于集群";
"System accent" = "系统强调色";
"Monochrome accent" = "黑白";
"Clear" = "透明";
"White" = "白色";
"Black" = "黑色";
"Gray" = "灰色 1";
"Second gray" = "灰色 2";
"Dark gray" = "深灰色";
"Light gray" = "浅灰色";
"Red" = "红色 1";
"Second red" = "红色 2";
"Green" = "绿色 1";
"Second green" = "绿色 2";
"Blue" = "蓝色 1";
"Second blue" = "蓝色 2";
"Yellow" = "黄色 1";
"Second yellow" = "黄色 2";
"Orange" = "橙色 1";
"Second orange" = "橙色 2";
"Purple" = "紫色 1";
"Second purple" = "紫色 2";
"Brown" = "棕色 1";
"Second brown" = "棕色 2";
"Cyan" = "青色";
"Magenta" = "洋红色";
"Pink" = "粉色";
"Teal" = "蓝绿色";
"Indigo" = "靛蓝色";
================================================
FILE: Stats/Supporting Files/zh-Hant.lproj/Localizable.strings
================================================
//
// Localizable.strings
// Stats
//
// Created by Serhiy Mytrovtsiy on 27/08/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
// Modules
"CPU" = "CPU";
"Open CPU settings" = "打開CPU設定";
"GPU" = "GPU";
"Open GPU settings" = "打開GPU設定";
"RAM" = "記憶體";
"Open RAM settings" = "打開記憶體設定";
"Disk" = "磁碟";
"Open Disk settings" = "打開磁碟設定";
"Sensors" = "感知器";
"Open Sensors settings" = "打開感知器設定";
"Network" = "網路";
"Open Network settings" = "打開網路設定";
"Battery" = "電池";
"Open Battery settings" = "打開電池設定";
"Bluetooth" = "藍牙";
"Open Bluetooth settings" = "打開藍牙設定";
"Clock" = "時鐘";
"Open Clock settings" = "打開時鐘設定";
// Words
"Unknown" = "未知";
"Version" = "版本";
"Processor" = "處理器";
"Memory" = "記憶體";
"Graphics" = "顯示卡";
"Close" = "關閉";
"Download" = "下載";
"Install" = "安裝";
"Cancel" = "取消";
"Unavailable" = "不可用";
"Yes" = "是";
"No" = "否";
"Automatic" = "自動";
"Manual" = "手動";
"None" = "無";
"Dots" = "點";
"Arrows" = "箭頭";
"Characters" = "文字";
"Short" = "短";
"Long" = "長";
"Statistics" = "統計資料";
"Max" = "最大值";
"Min" = "最小值";
"Reset" = "重置";
"Alignment" = "對齊";
"Left alignment" = "靠左對齊";
"Center alignment" = "置中對齊";
"Right alignment" = "靠右對齊";
"Dashboard" = "儀表板";
"Enabled" = "啟用";
"Disabled" = "停用";
"Silent" = "寧靜執行";
"Units" = "單位";
"Fans" = "風扇";
"Scaling" = "縮放";
"Linear" = "線性";
"Square" = "平方";
"Cube" = "立方";
"Logarithmic" = "對數";
"Fixed scale" = "固定值";
"Cores" = "核心數";
"Settings" = "設定";
"Name" = "名稱";
"Format" = "格式";
"Turn off" = "關閉";
"Normal" = "正常";
"Warning" = "警告";
"Critical" = "重要通知";
"Usage" = "使用量";
"2 minutes" = "2分鐘";
"3 minutes" = "3分鐘";
"10 minutes" = "10分鐘";
"Import" = "匯入";
"Export" = "匯出";
"Separator" = "分隔符號";
"Read" = "讀取";
"Write" = "寫入";
"Frequency" = "頻率";
"Save" = "儲存";
"Run" = "執行";
"Stop" = "停止";
"Uninstall" = "解除安裝";
"1 sec" = "1秒";
"2 sec" = "2秒";
"3 sec" = "3秒";
"5 sec" = "5秒";
"10 sec" = "10秒";
"15 sec" = "15秒";
"30 sec" = "30秒";
"60 sec" = "60秒";
// Setup
"Stats Setup" = "Stats設定";
"Previous" = "上一步";
"Previous page" = "上一頁";
"Next" = "下一步";
"Next page" = "下一頁";
"Finish" = "完成";
"Finish setup" = "完成設定";
"Welcome to Stats" = "歡迎使用Stats";
"welcome_message" = "感謝您使用Stats!「Stats」是一款讓您在選單列一目瞭然地監看macOS系統資源的開源App。";
"Start the application automatically when starting your Mac" = "啟動 Mac 時自動打開Stats";
"Do not start the application automatically when starting your Mac" = "不要在啟動Mac時自動打開Stats";
"Do everything silently in the background (recommended)" = "在背景寧靜執行(建議)";
"Check for a new version on startup" = "當 Stats 打開時自動檢查新版本";
"Check for a new version every day (once a day)" = "每天檢查是否有新版本(1天1次)";
"Check for a new version every week (once a week)" = "每週檢查是否有新版本(1週1次)";
"Check for a new version every month (once a month)" = "每月檢查是否有新版本(1個月1次)";
"Never check for updates (not recommended)" = "不要檢查更新(不建議)";
"Anonymous telemetry for better development decisions" = "匿名診斷資料將用於改善開發決策";
"Share anonymous telemetry data" = "分享匿名診斷資料";
"Do not share anonymous telemetry data" = "不同意分享診斷資料";
"The configuration is completed" = "組態設定完成!";
"finish_setup_message" = "所有設定均已大功告成!\n Stats是一款開源且永久免費的工具程式。\n 由衷地感謝您喜歡與愛用Stats,也歡迎您支持這個專案!";
// Alerts
"New version available" = "有新版本可用";
"Click to install the new version of Stats" = "安裝Stats的新版本";
"Successfully updated" = "更新成功";
"Stats was updated to v" = "Stats已更新到 v%0";
"Reset settings text" = "應用程式的所有設定將會重置,並會重新打開此應用程式。您確定要這樣做嗎?";
"Support text" = "感謝您使用Stats!維護並改進此開放原始碼專案需要花費時間與資源。如果覺得Stats對您有所幫助,歡迎贊助我們。您的一分一毫都帶給我們莫大的幫助!";
// Settings
"Open Activity Monitor" = "打開活動監視器";
"Report a bug" = "回報錯誤";
"Support the application" = "贊助此應用程式";
"Close application" = "關閉應用程式";
"Open application settings" = "打開應用程式設定";
"Open dashboard" = "打開儀表板";
"No notifications available in this module" = "沒有可用於此模組的通知";
"Open Calendar" = "打開「行事曆」";
"Toggle the module" = "切換為模組顯示";
// Application settings
"Update application" = "更新應用程式";
"Check for updates" = "檢查更新";
"At start" = "打開時檢查";
"Once per day" = "每天1次";
"Once per week" = "每週1次";
"Once per month" = "每月1次";
"Never" = "不檢查更新";
"Check for update" = "檢查是否有更新";
"Show icon in dock" = "在Dock上顯示圖示";
"Start at login" = "登入時打開";
"Build number" = "組建號碼";
"Import settings" = "匯入設定";
"Export settings" = "匯出設定";
"Reset settings" = "重置設定";
"Pause the Stats" = "暫停Stats";
"Resume the Stats" = "恢復Stats";
"Combined modules" = "合併模組";
"Combined details" = "合併詳細資訊";
"Spacing" = "間距";
"Share anonymous telemetry" = "分享匿名診斷資料";
"Choose file" = "選擇檔案";
"Stress tests" = "壓力測試";
// Dashboard
"Serial number" = "序號";
"Model identifier" = "機型識別碼";
"Production year" = "製造年份";
"Uptime" = "開機時間";
"Number of cores" = "%0 核心";
"Number of threads" = "%0 執行緒";
"Number of e-cores" = "%0 個節能核心";
"Number of p-cores" = "%0 個效能核心";
"Disks" = "磁碟機";
"Display" = "顯示器";
// Update
"The latest version of Stats installed" = "已安裝最新版本";
"Downloading..." = "下載中⋯";
"Current version: " = "目前版本:";
"Latest version: " = "最新版本:";
// Widgets
"Color" = "色彩";
"Label" = "標籤";
"Box" = "背景";
"Frame" = "外框";
"Value" = "數值";
"Colorize" = "以色彩顯示";
"Colorize value" = "以色彩顯示數值";
"Additional information" = "附加資訊";
"Reverse values order" = "在上方列顯示已使用數值";
"Base" = "單位";
"Display mode" = "顯示模式";
"One row" = "單列";
"Two rows" = "雙列";
"Mini widget" = "小型";
"Line chart widget" = "線狀圖";
"Bar chart widget" = "柱狀圖";
"Pie chart widget" = "圓形圖";
"Network chart widget" = "網狀圖";
"Speed widget" = "讀寫速度";
"Battery widget" = "電池";
"Stack widget" = "疊放";
"Memory widget" = "容量";
"Static width" = "靜態寬度";
"Tachometer widget" = "轉速計";
"State widget" = "State小工具";
"Text widget" = "文字";
"Battery details widget" = "電池詳細資訊";
"Show symbols" = "顯示符號";
"Label widget" = "標籤";
"Number of reads in the chart" = "圖表顯示的未讀標記";
"Color of download" = "下載的顯示色彩";
"Color of upload" = "上傳的顯示色彩";
"Monospaced font" = "等寬字體";
"Reverse order" = "排序對調";
"Chart history" = "圖表顯示區間";
"Default color" = "預設"; // translategemma:4b
"Transparent when no activity" = "閒置時以透明顯示";
"Constant color" = "恆定色彩";
// Module Kit
"Open module settings" = "打開模組設定";
"Select widget" = "選擇「%0」小工具";
"Open widget settings" = "打開小工具設定";
"Update interval" = "更新頻率";
"Usage history" = "負載歷程記錄";
"Details" = "詳細資訊";
"Top processes" = "高能耗程序";
"Pictogram" = "標示";
"Module" = "模組";
"Widgets" = "小工具";
"Popup" = "彈出式視窗";
"Notifications" = "通知";
"Merge widgets" = "合併小工具";
"No available widgets to configure" = "沒有可用的小工具來進行組態設定";
"No options to configure for the popup in this module" = "沒有可用於此模組的彈出式視窗組態設定選項";
"Process" = "程序";
"Kill process" = "結束程序";
"Keyboard shortcut" = "鍵盤快速鍵";
"Listening..." = "監聽中⋯";
// Modules
"Number of top processes" = "高能耗程序數量";
"Update interval for top processes" = "高能耗程序更新頻率";
"Notification level" = "觸發通知的數值";
"Chart color" = "圖表色彩";
"Main chart scaling" = "主圖表縮放";
"Scale value" = "縮放值";
"Text widget value" = "文字小工具值";
// CPU
"CPU usage" = "CPU使用量";
"CPU temperature" = "CPU溫度";
"CPU frequency" = "CPU頻率";
"System" = "根據系統";
"User" = "使用者";
"Idle" = "閒置";
"Show usage per core" = "顯示每個核心的使用狀態";
"Show hyper-threading cores" = "顯示超執行緒核心";
"Split the value (System/User)" = "分割值 (系統/使用者)";
"Scheduler limit" = "調度限制";
"Speed limit" = "速度限制";
"Average load" = "平均負載";
"1 minute" = "1分鐘";
"5 minutes" = "5分鐘";
"15 minutes" = "15分鐘";
"CPU usage threshold" = "CPU使用量臨界值";
"CPU usage is" = "CPU使用量:%0";
"Efficiency cores" = "節能核心";
"Performance cores" = "效能核心";
"System color" = "系統占用色彩";
"User color" = "使用者占用色彩";
"Idle color" = "閒置色彩";
"Cluster grouping" = "依群集分組";
"Efficiency cores color" = "節能核心色彩";
"Performance cores color" = "效能核心色彩";
"Total load" = "總占用";
"System load" = "系統占用";
"User load" = "使用者占用";
"Efficiency cores load" = "節能核心占用";
"Performance cores load" = "效能核心占用";
"All cores" = "總核心占用";
// GPU
"GPU to show" = "顯示GPU";
"Show GPU type" = "顯示GPU類型";
"GPU enabled" = "啟用GPU";
"GPU disabled" = "停用GPU";
"GPU temperature" = "GPU溫度";
"GPU utilization" = "GPU使用率";
"Vendor" = "製造商";
"Model" = "型號";
"Status" = "狀態";
"Active" = "使用中";
"Non active" = "未在使用";
"Fan speed" = "風扇轉速";
"Core clock" = "核心時脈";
"Memory clock" = "記憶體時脈";
"Utilization" = "利用率";
"Render utilization" = "渲染利用率";
"Tiler utilization" = "圖塊利用率";
"GPU usage threshold" = "GPU使用量臨界值";
"GPU usage is" = "GPU使用量:%0";
// RAM
"Memory usage" = "記憶體使用量";
"Memory pressure" = "記憶體壓力";
"Total" = "總共";
"Used" = "已使用";
"App" = "App記憶體";
"Wired" = "連動";
"Compressed" = "已壓縮";
"Free" = "可使用";
"Swap" = "交換";
"Split the value (App/Wired/Compressed)" = "分割值 (App/固定/壓縮)";
"RAM utilization threshold" = "記憶體利用率臨界值";
"RAM utilization is" = "記憶體利用率:%0";
"App color" = "App色彩";
"Wired color" = "連動記憶體色彩";
"Compressed color" = "已壓縮記憶體色彩";
"Free color" = "閒置記憶體色彩";
"Free memory (less than)" = "可用記憶體 (小於)";
"Swap size" = "已使用交換";
"Free RAM is" = "可用記憶體剩餘 %0";
// Disk
"Show removable disks" = "顯示抽取式磁碟";
"Used disk memory" = "總共:%1,已用:%0";
"Free disk memory" = "總共:%1,可用:%0";
"Disk to show" = "顯示磁碟";
"Open disk" = "開啟磁碟";
"Switch view" = "切換視窗";
"Disk utilization threshold" = "磁碟利用率臨界值";
"Disk utilization is" = "磁碟利用率:%0";
"Read color" = "讀取色彩";
"Write color" = "寫入色彩";
"Disk usage" = "磁碟使用量";
"Total read" = "總讀取";
"Total written" = "總寫入";
"Write speed" = "寫入";
"Read speed" = "讀取";
"Drives" = "磁碟機";
"SMART data" = "SMART資料";
// Sensors
"Temperature unit" = "溫度單位";
"Celsius" = "攝氏 (˚C)";
"Fahrenheit" = "華氏 (˚F)";
"Save the fan speed" = "儲存風扇速度";
"Fan" = "風扇";
"HID sensors" = "HID感知器";
"Synchronize fan's control" = "同步風扇控制";
"Current" = "電流";
"Energy" = "能耗";
"Show unknown sensors" = "顯示未知的感知器";
"Install fan helper" = "安裝風扇輔助程式";
"Uninstall fan helper" = "解除安裝風扇輔助程式";
"Fan value" = "風扇轉速值";
"Turn off fan" = "關閉風扇";
"You are going to turn off the fan. This is not recommended action that can damage your mac, are you sure you want to do that?" = "您似乎要關閉您Mac的風扇。不建議進行此操作,因為這可能會造成Mac的損毀,您確定仍要關閉風扇嗎?";
"Sensor threshold" = "感知器臨界值";
"Left fan" = "左側風扇";
"Right fan" = "右側風扇";
"Fastest fan" = "最快速度";
"Sensor to show" = "要顯示的感知器";
// Network
"Uploading" = "上傳";
"Downloading" = "下載";
"Public IP" = "公用IP";
"Local IP" = "區域IP";
"Interface" = "介面";
"Physical address" = "實體位址";
"Refresh" = "重新整理";
"Click to copy public IP address" = "點按來拷貝公用IP位址";
"Click to copy local IP address" = "點按來拷貝區域IP位址";
"Click to copy wifi name" = "點按來拷貝Wi-Fi名稱";
"Click to copy mac address" = "點按來拷貝MAC位址";
"No connection" = "沒有連線";
"Network interface" = "網路介面卡";
"Total download" = "共下載";
"Total upload" = "共上傳";
"Reader type" = "讀取機類型";
"Interface based" = "依介面卡";
"Processes based" = "依程序";
"Reset data usage" = "重置資料用量";
"VPN mode" = "VPN模式";
"Standard" = "標準";
"Security" = "安全";
"Channel" = "通道";
"Common scale" = "統一比例";
"Autodetection" = "自動檢測";
"Widget activation threshold" = "小工具啟動臨界值";
"Internet connection" = "網際網路連線裝態";
"Active state color" = "已連線狀態色彩";
"Nonactive state color" = "未連線狀態色彩";
"Connectivity host (ICMP)" = "連線能力測試主機(ICMP)";
"Leave empty to disable the check" = "留空來停用檢查";
"Connectivity history" = "連線歷程紀錄";
"Auto-refresh public IP address" = "自動重新整理公用IP位址";
"Every hour" = "每小時";
"Every 12 hours" = "每12小時";
"Every 24 hours" = "每24小時";
"Network activity" = "網路活動";
"Last reset" = "上次重置於 %0 前";
"Latency" = "延遲";
"Upload speed" = "上傳";
"Download speed" = "下載";
"Address" = "位址";
"WiFi network" = "Wi-Fi網路";
"Local IP changed" = "區域IP已變更";
"Public IP changed" = "公用IP已變更";
"Previous IP" = "舊IP:%0";
"New IP" = "新IP:%0";
"Internet connection lost" = "網際網路連線已遺失";
"Internet connection established" = "網際網路連線已建立";
// Battery
"Level" = "電量";
"Source" = "電源";
"AC Power" = "交流電";
"Battery Power" = "電池";
"Time" = "時間";
"Health" = "健康度";
"Amperage" = "電流";
"Voltage" = "電壓";
"Cycles" = "循環使用次數";
"Temperature" = "溫度";
"Power adapter" = "電源供應器";
"Power" = "功率";
"Is charging" = "充電中";
"Time to discharge" = "放電時間";
"Time to charge" = "充電時間";
"Calculating" = "計算中";
"Fully charged" = "已充飽";
"Not connected" = "未連接";
"Low level notification" = "低電量通知";
"High level notification" = "高電量通知";
"Low battery" = "低電量";
"High battery" = "高電量";
"Battery remaining" = "剩餘 %0%";
"Battery remaining to full charge" = "再%0%充飽";
"Percentage" = "百分比";
"Percentage and time" = "百分比和時間";
"Time and percentage" = "時間和百分比";
"Time format" = "時間格式";
"Hide additional information when full" = "電池充飽後隱藏其他資訊";
"Last charge" = "最近一次充電";
"Capacity" = "容量";
"current / maximum / designed" = "目前容量/最大容量/設計容量";
"Low power mode" = "低電量模式";
"Percentage inside the icon" = "在圖示內顯示百分比";
"Colorize battery" = "啟用彩色電池圖示";
"Charging current" = "充電電流";
"Charging Voltage" = "充電電壓";
"Charger state inside the battery" = "在電池圖示中顯示充電狀態";
// Bluetooth
"Battery to show" = "顯示藍牙裝置的電池電量";
"No Bluetooth devices are available" = "沒有可用的藍牙裝置";
// Clock
"Time zone" = "時區";
"Local" = "目前位置";
"Calendar" = "日曆";
"Show week numbers" = "顯示週數";
"Local time" = "當地時間";
"Add new clock" = "加入新時鐘";
"Delete selected clock" = "刪除所選時鐘";
"Help with datetime format" = "時間日期格式說明";
// Colors
"Based on utilization" = "根據使用率";
"Based on pressure" = "根據壓力";
"Based on cluster" = "根據群集";
"System accent" = "系統強調色";
"Monochrome accent" = "黑白";
"Clear" = "透明";
"White" = "白色";
"Black" = "黑色";
"Gray" = "灰色1";
"Second gray" = "灰色2";
"Dark gray" = "深灰色";
"Light gray" = "淺灰色";
"Red" = "紅色1";
"Second red" = "紅色2";
"Green" = "綠色1";
"Second green" = "綠色2";
"Blue" = "藍色1";
"Second blue" = "藍色2";
"Yellow" = "黃色1";
"Second yellow" = "黃色2";
"Orange" = "橙色1";
"Second orange" = "橙色2";
"Purple" = "紫色1";
"Second purple" = "紫色2";
"Brown" = "棕色1";
"Second brown" = "棕色2";
"Cyan" = "青色";
"Magenta" = "洋紅色";
"Pink" = "粉色";
"Teal" = "藍綠色";
"Indigo" = "靛色";
================================================
FILE: Stats/Views/AppSettings.swift
================================================
//
// AppSettings.swift
// Stats
//
// Created by Serhiy Mytrovtsiy on 15/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
class ApplicationSettings: NSStackView {
private var updateIntervalValue: String {
Store.shared.string(key: "update-interval", defaultValue: AppUpdateInterval.silent.rawValue)
}
private var temperatureUnitsValue: String {
get { Store.shared.string(key: "temperature_units", defaultValue: "system") }
set { Store.shared.set(key: "temperature_units", value: newValue) }
}
private var combinedModulesState: Bool {
get { Store.shared.bool(key: "CombinedModules", defaultValue: false) }
set { Store.shared.set(key: "CombinedModules", value: newValue) }
}
private var combinedModulesSpacing: String {
get { Store.shared.string(key: "CombinedModules_spacing", defaultValue: "none") }
set { Store.shared.set(key: "CombinedModules_spacing", value: newValue) }
}
private var combinedModulesSeparator: Bool {
get { Store.shared.bool(key: "CombinedModules_separator", defaultValue: false) }
set { Store.shared.set(key: "CombinedModules_separator", value: newValue) }
}
private var combinedModulesPopup: Bool {
get { Store.shared.bool(key: "CombinedModules_popup", defaultValue: true) }
set { Store.shared.set(key: "CombinedModules_popup", value: newValue) }
}
private var systemWidgetsUpdatesState: Bool {
get {
let userDefaults = UserDefaults(suiteName: "\(Bundle.main.object(forInfoDictionaryKey: "TeamId") as! String).eu.exelban.Stats.widgets")
return userDefaults?.bool(forKey: "systemWidgetsUpdates_state") ?? false
}
set {
let userDefaults = UserDefaults(suiteName: "\(Bundle.main.object(forInfoDictionaryKey: "TeamId") as! String).eu.exelban.Stats.widgets")
userDefaults?.set(newValue, forKey: "systemWidgetsUpdates_state")
}
}
private var updateSelector: NSPopUpButton?
private var startAtLoginBtn: NSSwitch?
private var remoteControlBtn: NSSwitch?
private var combinedModulesView: PreferencesSection?
private var fanHelperView: PreferencesSection?
private var remoteView: PreferencesSection?
private let updateWindow: UpdateWindow = UpdateWindow()
private let moduleSelector: ModuleSelectorView = ModuleSelectorView()
private var CPUeButton: NSButton?
private var CPUpButton: NSButton?
private var GPUButton: NSButton?
private var CPUeTest: CPUeStressTest = CPUeStressTest()
private var CPUpTest: CPUpStressTest = CPUpStressTest()
private var GPUTest: GPUStressTest? = GPUStressTest()
init() {
super.init(frame: NSRect(x: 0, y: 0, width: Constants.Settings.width, height: Constants.Settings.height))
self.translatesAutoresizingMaskIntoConstraints = false
let scrollView = ScrollableStackView(orientation: .vertical)
scrollView.stackView.edgeInsets = NSEdgeInsets(
top: 0,
left: Constants.Settings.margin,
bottom: Constants.Settings.margin,
right: Constants.Settings.margin
)
scrollView.stackView.spacing = Constants.Settings.margin
scrollView.stackView.addArrangedSubview(self.informationView())
self.updateSelector = selectView(
action: #selector(self.toggleUpdateInterval),
items: AppUpdateIntervals,
selected: self.updateIntervalValue
)
self.startAtLoginBtn = switchView(
action: #selector(self.toggleLaunchAtLogin),
state: LaunchAtLogin.isEnabled
)
scrollView.stackView.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Check for updates"), component: self.updateSelector!),
PreferencesRow(localizedString("Temperature"), component: selectView(
action: #selector(self.toggleTemperatureUnits),
items: TemperatureUnits,
selected: self.temperatureUnitsValue
)),
PreferencesRow(localizedString("Show icon in dock"), component: switchView(
action: #selector(self.toggleDock),
state: Store.shared.bool(key: "dockIcon", defaultValue: false)
)),
PreferencesRow(localizedString("Start at login"), component: self.startAtLoginBtn!)
]))
scrollView.stackView.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("macOS widgets"), component: switchView(
action: #selector(self.toggleSystemWidgetsUpdatesState),
state: self.systemWidgetsUpdatesState
))
]))
self.combinedModulesView = PreferencesSection([
PreferencesRow(localizedString("Combined modules"), component: switchView(
action: #selector(self.toggleCombinedModules),
state: self.combinedModulesState
)),
PreferencesRow(component: self.moduleSelector),
PreferencesRow(localizedString("Spacing"), component: selectView(
action: #selector(self.toggleCombinedModulesSpacing),
items: CombinedModulesSpacings,
selected: self.combinedModulesSpacing
)),
PreferencesRow(localizedString("Separator"), component: switchView(
action: #selector(self.toggleCombinedModulesSeparator),
state: self.combinedModulesSeparator
)),
PreferencesRow(localizedString("Combined details"), component: switchView(
action: #selector(self.toggleCombinedModulesPopup),
state: self.combinedModulesPopup
))
])
scrollView.stackView.addArrangedSubview(self.combinedModulesView!)
self.combinedModulesView?.setRowVisibility(1, newState: self.combinedModulesState)
self.combinedModulesView?.setRowVisibility(2, newState: self.combinedModulesState)
self.combinedModulesView?.setRowVisibility(3, newState: self.combinedModulesState)
self.combinedModulesView?.setRowVisibility(4, newState: self.combinedModulesState)
self.remoteControlBtn = switchView(
action: #selector(self.toggleRemoteControlState),
state: Remote.shared.control
)
self.remoteView = PreferencesSection(label: localizedString("Remote (beta)"), [
PreferencesRow(localizedString("Authorization"), component: buttonView(#selector(self.loginToRemote), text: localizedString("Login"))),
PreferencesRow(localizedString("Identificator"), component: textView(Remote.shared.id.uuidString)),
PreferencesRow(localizedString("Monitoring"), component: switchView(
action: #selector(self.toggleRemoteMonitoringState),
state: Remote.shared.monitoring
)),
PreferencesRow(localizedString("Control"), component: self.remoteControlBtn!),
PreferencesRow(component: buttonView(#selector(self.logoutFromRemote), text: localizedString("Logout")))
])
scrollView.stackView.addArrangedSubview(self.remoteView!)
self.remoteView?.setRowVisibility(1, newState: false)
self.remoteView?.setRowVisibility(2, newState: false)
self.remoteView?.setRowVisibility(3, newState: false)
self.remoteView?.setRowVisibility(4, newState: false)
scrollView.stackView.addArrangedSubview(PreferencesSection(label: localizedString("Settings"), [
PreferencesRow(
localizedString("Export settings"),
component: buttonView(#selector(self.exportSettings), text: localizedString("Save"))
),
PreferencesRow(
localizedString("Import settings"),
component: buttonView(#selector(self.importSettings), text: localizedString("Choose file"))
),
PreferencesRow(
localizedString("Reset settings"),
component: buttonView(#selector(self.resetSettings), text: localizedString("Reset"))
)
]))
self.fanHelperView = PreferencesSection([
PreferencesRow(
localizedString("Uninstall fan helper"),
component: buttonView(#selector(self.uninstallHelper), text: localizedString("Uninstall"))
)
])
scrollView.stackView.addArrangedSubview(self.fanHelperView!)
self.addArrangedSubview(scrollView)
let CPUeButton = buttonView(#selector(self.toggleCPUeStressTest), text: localizedString("Run"))
let CPUpButton = buttonView(#selector(self.toggleCPUpStressTest), text: localizedString("Run"))
let GPUButton = buttonView(#selector(self.toggleGPUStressTest), text: localizedString("Run"))
self.CPUeButton = CPUeButton
self.CPUpButton = CPUpButton
self.GPUButton = GPUButton
var tests = [
PreferencesRow(localizedString("Efficiency cores"), component: CPUeButton),
PreferencesRow(localizedString("Performance cores"), component: CPUpButton)
]
if self.GPUTest != nil {
tests.append(PreferencesRow(localizedString("GPU"), component: GPUButton))
}
scrollView.stackView.addArrangedSubview(PreferencesSection(label: localizedString("Stress tests"), tests))
NotificationCenter.default.addObserver(self, selector: #selector(self.toggleUninstallHelperButton), name: .fanHelperState, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.handleRemoteState), name: .remoteState, object: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
NotificationCenter.default.removeObserver(self, name: .fanHelperState, object: nil)
}
internal func viewWillAppear() {
self.startAtLoginBtn?.state = LaunchAtLogin.isEnabled ? .on : .off
self.remoteControlBtn?.state = Remote.shared.control ? .on : .off
var idx = self.updateSelector?.indexOfSelectedItem ?? 0
if let items = self.updateSelector?.menu?.items {
for (i, item) in items.enumerated() {
if let obj = item.representedObject as? String, obj == self.updateIntervalValue {
idx = i
}
}
}
self.updateSelector?.selectItem(at: idx)
}
private func informationView() -> NSView {
let view = NSStackView()
view.heightAnchor.constraint(equalToConstant: 220).isActive = true
view.orientation = .vertical
view.distribution = .fill
view.alignment = .centerY
view.spacing = 0
let container: NSGridView = NSGridView()
container.heightAnchor.constraint(equalToConstant: 180).isActive = true
container.rowSpacing = 0
container.yPlacement = .center
container.xPlacement = .center
let iconView: NSImageView = NSImageView(image: NSImage(named: NSImage.Name("AppIcon"))!)
let statsName: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 22))
statsName.alignment = .center
statsName.font = NSFont.systemFont(ofSize: 20, weight: .regular)
statsName.stringValue = "Stats"
statsName.isSelectable = true
let versionNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String
let statsVersion: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 16))
statsVersion.alignment = .center
statsVersion.font = NSFont.systemFont(ofSize: 12, weight: .regular)
statsVersion.stringValue = "\(localizedString("Version")) \(versionNumber)"
statsVersion.isSelectable = true
statsVersion.toolTip = "\(localizedString("Build number")) \(buildNumber)"
let updateButton: NSButton = NSButton()
updateButton.title = localizedString("Check for update")
updateButton.bezelStyle = .rounded
updateButton.target = self
updateButton.action = #selector(self.updateAction)
container.addRow(with: [iconView])
container.addRow(with: [statsName])
container.addRow(with: [statsVersion])
container.addRow(with: [updateButton])
container.row(at: 1).height = 22
container.row(at: 2).height = 20
container.row(at: 3).height = 30
view.addArrangedSubview(container)
return view
}
// MARK: - actions
@objc private func updateAction() {
updater.check(force: true, completion: { result, error in
if error != nil {
debug("error updater.check(): \(error!.localizedDescription)")
return
}
guard error == nil, let version: version_s = result else {
debug("download error(): \(error!.localizedDescription)")
return
}
DispatchQueue.main.async(execute: {
self.updateWindow.open(version, settingButton: true)
return
})
})
}
@objc private func toggleUpdateInterval(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
Store.shared.set(key: "update-interval", value: key)
}
@objc private func toggleTemperatureUnits(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.temperatureUnitsValue = key
}
@objc private func toggleDock(_ sender: NSButton) {
let state = sender.state
Store.shared.set(key: "dockIcon", value: state == NSControl.StateValue.on)
let dockIconStatus = state == NSControl.StateValue.on ? NSApplication.ActivationPolicy.regular : NSApplication.ActivationPolicy.accessory
NSApp.setActivationPolicy(dockIconStatus)
if state == .off {
NSApplication.shared.activate(ignoringOtherApps: true)
}
}
@objc private func toggleLaunchAtLogin(_ sender: NSButton) {
LaunchAtLogin.isEnabled = sender.state == NSControl.StateValue.on
if !Store.shared.exist(key: "runAtLoginInitialized") {
Store.shared.set(key: "runAtLoginInitialized", value: true)
}
}
@objc private func toggleCombinedModules(_ sender: NSButton) {
self.combinedModulesState = sender.state == NSControl.StateValue.on
self.combinedModulesView?.setRowVisibility(1, newState: self.combinedModulesState)
self.combinedModulesView?.setRowVisibility(2, newState: self.combinedModulesState)
self.combinedModulesView?.setRowVisibility(3, newState: self.combinedModulesState)
self.combinedModulesView?.setRowVisibility(4, newState: self.combinedModulesState)
NotificationCenter.default.post(name: .toggleOneView, object: nil, userInfo: nil)
}
@objc private func toggleCombinedModulesSpacing(_ sender: NSMenuItem) {
guard let key = sender.representedObject as? String else { return }
self.combinedModulesSpacing = key
NotificationCenter.default.post(name: .moduleRearrange, object: nil, userInfo: nil)
}
@objc private func toggleCombinedModulesSeparator(_ sender: NSButton) {
self.combinedModulesSeparator = sender.state == NSControl.StateValue.on
NotificationCenter.default.post(name: .moduleRearrange, object: nil, userInfo: nil)
}
@objc private func toggleCombinedModulesPopup(_ sender: NSButton) {
self.combinedModulesPopup = sender.state == NSControl.StateValue.on
NotificationCenter.default.post(name: .combinedModulesPopup, object: nil, userInfo: nil)
}
@objc private func importSettings() {
let panel = NSOpenPanel()
panel.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.modalPanelWindow)))
panel.begin { (result) in
guard result.rawValue == NSApplication.ModalResponse.OK.rawValue else { return }
if let url = panel.url {
Store.shared.import(from: url)
}
}
}
@objc private func exportSettings() {
let panel = NSSavePanel()
panel.nameFieldStringValue = "Stats.plist"
panel.showsTagField = false
panel.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.modalPanelWindow)))
panel.begin { (result) in
guard result.rawValue == NSApplication.ModalResponse.OK.rawValue else { return }
if let url = panel.url {
Store.shared.export(to: url)
}
}
}
@objc private func resetSettings() {
let alert = NSAlert()
alert.messageText = localizedString("Reset settings")
alert.informativeText = localizedString("Reset settings text")
alert.alertStyle = .warning
alert.addButton(withTitle: localizedString("Yes"))
alert.addButton(withTitle: localizedString("No"))
if alert.runModal() == .alertFirstButtonReturn {
Store.shared.reset()
restartApp(self)
}
}
@objc private func toggleUninstallHelperButton(_ notification: Notification) {
guard let state = notification.userInfo?["state"] as? Bool, let v = self.fanHelperView else {
return
}
v.isHidden = !state
}
@objc private func uninstallHelper() {
SMCHelper.shared.uninstall()
}
@objc private func toggleCPUeStressTest() {
if self.CPUeTest.isRunning {
self.CPUeTest.stop()
self.CPUeButton?.title = localizedString("Run")
} else {
self.CPUeTest.start()
self.CPUeButton?.title = localizedString("Stop")
}
}
@objc private func toggleCPUpStressTest() {
if self.CPUpTest.isRunning {
self.CPUpTest.stop()
self.CPUpButton?.title = localizedString("Run")
} else {
self.CPUpTest.start()
self.CPUpButton?.title = localizedString("Stop")
}
}
@objc private func toggleGPUStressTest() {
guard let test = self.GPUTest else { return }
if test.isRunning {
test.stop()
self.GPUButton?.title = localizedString("Run")
} else {
test.start()
self.GPUButton?.title = localizedString("Stop")
}
}
@objc private func toggleRemoteMonitoringState(_ sender: NSButton) {
Remote.shared.monitoring = sender.state == NSControl.StateValue.on
}
@objc private func toggleRemoteControlState(_ sender: NSButton) {
if sender.state == .on {
let alert = NSAlert()
alert.messageText = localizedString("Warning")
alert.informativeText = localizedString("It is not recommended to enable remote control unless you know what you are doing.")
alert.alertStyle = .warning
alert.addButton(withTitle: localizedString("Enable"))
alert.addButton(withTitle: localizedString("Cancel"))
let response = alert.runModal()
if response == .alertFirstButtonReturn {
Remote.shared.control = true
} else {
sender.state = .off
}
} else {
Remote.shared.control = false
}
}
@objc private func handleRemoteState(_ notification: Notification) {
guard let auth = notification.userInfo?["auth"] as? Bool else { return }
self.setRemoteSettings(auth)
}
@objc private func loginToRemote() {
Remote.shared.login()
}
@objc private func logoutFromRemote() {
Remote.shared.logout()
}
private func setRemoteSettings(_ auth: Bool) {
DispatchQueue.main.async {
if auth {
self.remoteView?.setRowVisibility(1, newState: true)
self.remoteView?.setRowVisibility(2, newState: true)
self.remoteView?.setRowVisibility(3, newState: true)
self.remoteView?.setRowVisibility(4, newState: true)
self.remoteView?.setRowVisibility(0, newState: false)
} else {
self.remoteView?.setRowVisibility(0, newState: true)
self.remoteView?.setRowVisibility(1, newState: false)
self.remoteView?.setRowVisibility(2, newState: false)
self.remoteView?.setRowVisibility(3, newState: false)
self.remoteView?.setRowVisibility(4, newState: false)
}
}
}
@objc private func toggleSystemWidgetsUpdatesState(_ sender: NSButton) {
self.systemWidgetsUpdatesState = sender.state == NSControl.StateValue.on
}
}
private class ModuleSelectorView: NSStackView {
init() {
super.init(frame: NSRect(x: 0, y: 0, width: 0, height: Constants.Widget.height + (Constants.Settings.margin*2)))
self.translatesAutoresizingMaskIntoConstraints = false
self.edgeInsets = NSEdgeInsets(
top: Constants.Settings.margin,
left: Constants.Settings.margin,
bottom: Constants.Settings.margin,
right: Constants.Settings.margin
)
self.spacing = Constants.Settings.margin
let background: NSVisualEffectView = {
let view = NSVisualEffectView(frame: NSRect.zero)
view.blendingMode = .withinWindow
view.material = .contentBackground
view.state = .active
view.wantsLayer = true
view.layer?.cornerRadius = 5
return view
}()
var w = self.spacing
modules.filter({ $0.available }).sorted(by: { $0.combinedPosition < $1.combinedPosition }).forEach { (m: Module) in
let v = ModulePreview(id: m.name, icon: m.config.icon)
self.addArrangedSubview(v)
w += v.frame.width + self.spacing
}
if w < 20 {
w = 20
}
self.addSubview(background, positioned: .below, relativeTo: .none)
self.setFrameSize(NSSize(width: w, height: self.frame.height))
background.setFrameSize(NSSize(width: w, height: self.frame.height))
self.widthAnchor.constraint(equalToConstant: w).isActive = true
self.heightAnchor.constraint(equalToConstant: self.frame.height).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func mouseDown(with event: NSEvent) {
let location = convert(event.locationInWindow, from: nil)
guard let targetIdx = self.views.firstIndex(where: { $0.hitTest(location) != nil }),
let window = self.window, self.views[targetIdx].identifier != nil else {
super.mouseDragged(with: event)
return
}
let view = self.views[targetIdx]
let copy = ViewCopy(view)
copy.zPosition = 2
copy.transform = CATransform3DMakeScale(0.9, 0.9, 1)
// hide the original view, show the copy
view.subviews.forEach({ $0.isHidden = true })
self.layer?.addSublayer(copy)
// hide the copy view, show the original
defer {
copy.removeFromSuperlayer()
view.subviews.forEach({ $0.isHidden = false })
}
var newIdx = -1
let originCenter = view.frame.midX
let originX = view.frame.origin.x
let p0 = convert(event.locationInWindow, from: nil).x
window.trackEvents(matching: [.leftMouseDragged, .leftMouseUp], timeout: 1e6, mode: .eventTracking) { event, stop in
guard let event = event else {
stop.pointee = true
return
}
if event.type == .leftMouseDragged {
let p1 = self.convert(event.locationInWindow, from: nil).x
let diff = p1 - p0
CATransaction.begin()
CATransaction.setDisableActions(true)
copy.frame.origin.x = originX + diff
CATransaction.commit()
let reordered = self.views.map{
(view: $0, x: $0 !== view ? $0.frame.midX : originCenter + diff)
}.sorted{ $0.x < $1.x }.map { $0.view }
guard let nextIndex = reordered.firstIndex(of: view),
let prevIndex = self.views.firstIndex(of: view) else {
stop.pointee = true
return
}
if nextIndex != prevIndex {
newIdx = nextIndex
view.removeFromSuperviewWithoutNeedingDisplay()
self.insertArrangedSubview(view, at: newIdx)
self.layoutSubtreeIfNeeded()
for (i, v) in self.views(in: .leading).compactMap({$0 as? ModulePreview}).enumerated() {
if let m = modules.first(where: { $0.name == v.identifier?.rawValue }) {
m.combinedPosition = i
}
}
}
} else {
if newIdx != -1, let view = self.views[newIdx] as? ModulePreview, let id = view.identifier?.rawValue {
NotificationCenter.default.post(name: .moduleRearrange, object: nil, userInfo: ["id": id])
}
view.mouseUp(with: event)
stop.pointee = true
}
}
}
}
private class ModulePreview: NSStackView {
private let imageView: NSImageView
fileprivate init(id: String, icon: NSImage?) {
self.imageView = NSImageView(frame: NSRect(origin: .zero, size: NSSize(width: Constants.Widget.height, height: Constants.Widget.height)))
let size: CGSize = CGSize(width: Constants.Widget.height + (Constants.Widget.spacing * 2), height: Constants.Widget.height)
super.init(frame: NSRect(x: 0, y: 0, width: size.width, height: size.height))
self.wantsLayer = true
self.layer?.cornerRadius = 2
self.layer?.borderColor = NSColor(red: 221/255, green: 221/255, blue: 221/255, alpha: 1).cgColor
self.layer?.borderWidth = 1
self.layer?.backgroundColor = NSColor.white.cgColor
self.identifier = NSUserInterfaceItemIdentifier(rawValue: id)
self.setAccessibilityElement(true)
self.toolTip = id
self.orientation = .vertical
self.distribution = .fill
self.alignment = .centerY
self.spacing = 0
self.imageView.image = icon
self.imageView.contentTintColor = self.isDarkMode ? .textBackgroundColor : .textColor
self.addArrangedSubview(self.imageView)
self.addTrackingArea(NSTrackingArea(
rect: NSRect(x: 0, y: 0, width: size.width, height: size.height),
options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp],
owner: self,
userInfo: nil
))
NSLayoutConstraint.activate([
self.widthAnchor.constraint(equalToConstant: size.width),
self.heightAnchor.constraint(equalToConstant: size.height)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateLayer() {
self.imageView.contentTintColor = self.isDarkMode ? .textBackgroundColor : .textColor
}
override func mouseEntered(with: NSEvent) {
NSCursor.pointingHand.set()
}
override func mouseExited(with: NSEvent) {
NSCursor.arrow.set()
}
}
================================================
FILE: Stats/Views/CombinedView.swift
================================================
//
// CombinedView.swift
// Stats
//
// Created by Serhiy Mytrovtsiy on 09/01/2023
// Using Swift 5.0
// Running on macOS 13.1
//
// Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class CombinedView: NSObject, NSGestureRecognizerDelegate {
private var menuBarItem: NSStatusItem? = nil
private var view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: 0, height: Constants.Widget.height))
private var popup: PopupWindow? = nil
private var status: Bool {
Store.shared.bool(key: "CombinedModules", defaultValue: false)
}
private var spacing: CGFloat {
CGFloat(Int(Store.shared.string(key: "CombinedModules_spacing", defaultValue: "")) ?? 0)
}
private var separator: Bool {
Store.shared.bool(key: "CombinedModules_separator", defaultValue: false)
}
private var activeModules: [Module] {
modules.filter({ $0.enabled }).sorted(by: { $0.combinedPosition < $1.combinedPosition })
}
private var combinedModulesPopup: Bool {
get { Store.shared.bool(key: "CombinedModules_popup", defaultValue: true) }
set { Store.shared.set(key: "CombinedModules_popup", value: newValue) }
}
override init() {
super.init()
modules.forEach { (m: Module) in
m.menuBar.callback = { [weak self] in
if let s = self?.status, s {
DispatchQueue.main.async(execute: {
self?.recalculate()
})
}
}
}
self.popup = PopupWindow(title: "Combined modules", module: .combined, view: Popup()) { _ in }
if self.status {
self.enable()
}
NotificationCenter.default.addObserver(self, selector: #selector(listenForOneView), name: .toggleOneView, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(listenForModuleRearrrange), name: .moduleRearrange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(listenCombinedModulesPopup), name: .combinedModulesPopup, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(listenForModule), name: .toggleModule, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self, name: .toggleOneView, object: nil)
NotificationCenter.default.removeObserver(self, name: .moduleRearrange, object: nil)
NotificationCenter.default.removeObserver(self, name: .combinedModulesPopup, object: nil)
NotificationCenter.default.removeObserver(self, name: .toggleModule, object: nil)
}
public func enable() {
self.menuBarItem = NSStatusBar.system.statusItem(withLength: 0)
DispatchQueue.main.async(execute: {
self.menuBarItem?.autosaveName = "CombinedModules"
})
self.menuBarItem?.button?.addSubview(self.view)
self.menuBarItem?.button?.image = NSImage()
self.menuBarItem?.button?.toolTip = localizedString("Combined modules")
if !self.combinedModulesPopup {
self.activeModules.forEach { (m: Module) in
m.menuBar.widgets.forEach { w in
w.item.onClick = {
if let window = w.item.window {
NotificationCenter.default.post(name: .togglePopup, object: nil, userInfo: [
"module": m.name,
"widget": w.type,
"origin": window.frame.origin,
"center": window.frame.width/2
])
}
}
}
}
} else {
self.menuBarItem?.button?.target = self
self.menuBarItem?.button?.action = #selector(self.togglePopup)
self.menuBarItem?.button?.sendAction(on: [.leftMouseDown, .rightMouseDown])
}
DispatchQueue.main.async(execute: {
self.recalculate()
})
}
public func disable() {
self.activeModules.forEach { (m: Module) in
m.menuBar.widgets.forEach { w in
w.item.onClick = nil
}
}
if let item = self.menuBarItem {
NSStatusBar.system.removeStatusItem(item)
}
self.menuBarItem = nil
}
private func recalculate() {
self.view.subviews.forEach({ $0.removeFromSuperview() })
var w: CGFloat = 0
var i: Int = 0
self.activeModules.forEach { (m: Module) in
self.view.addSubview(m.menuBar.view)
self.view.subviews[i].setFrameOrigin(NSPoint(x: w, y: 0))
w += m.menuBar.view.frame.width + self.spacing
i += 1
if self.separator && i < 2 * self.activeModules.count - 1 {
let separator = NSView(frame: NSRect(x: w, y: 3, width: 1, height: Constants.Widget.height-6))
separator.wantsLayer = true
separator.layer?.backgroundColor = (separator.isDarkMode ? NSColor.black : NSColor.white).cgColor
self.view.addSubview(separator)
w += 3 + self.spacing
i += 1
}
}
self.view.setFrameSize(NSSize(width: w, height: self.view.frame.height))
self.menuBarItem?.length = w
}
// call when popup appear/disappear
private func visibilityCallback(_ state: Bool) {}
@objc private func togglePopup(_ sender: NSButton) {
guard let popup = self.popup, let item = self.menuBarItem, let window = item.button?.window else { return }
let openedWindows = NSApplication.shared.windows.filter{ $0 is NSPanel }
openedWindows.forEach{ $0.setIsVisible(false) }
if popup.occlusionState.rawValue == 8192 {
NSApplication.shared.activate(ignoringOtherApps: true)
popup.contentView?.invalidateIntrinsicContentSize()
let windowCenter = popup.contentView!.intrinsicContentSize.width / 2
var x = window.frame.origin.x - windowCenter + window.frame.width/2
let y = window.frame.origin.y - popup.contentView!.intrinsicContentSize.height - 3
let maxWidth = NSScreen.screens.map{ $0.frame.width }.reduce(0, +)
if x + popup.contentView!.intrinsicContentSize.width > maxWidth {
x = maxWidth - popup.contentView!.intrinsicContentSize.width - 3
}
popup.setFrameOrigin(NSPoint(x: x, y: y))
popup.setIsVisible(true)
} else {
popup.setIsVisible(false)
}
}
@objc private func listenForOneView(_ notification: Notification) {
guard notification.userInfo?["module"] == nil else { return }
if self.status {
self.enable()
} else {
self.disable()
}
}
@objc private func listenForModuleRearrrange() {
self.recalculate()
}
@objc private func listenCombinedModulesPopup() {
if !self.combinedModulesPopup {
self.activeModules.forEach { (m: Module) in
m.menuBar.widgets.forEach { w in
w.item.onClick = {
if let window = w.item.window {
NotificationCenter.default.post(name: .togglePopup, object: nil, userInfo: [
"module": m.name,
"widget": w.type,
"origin": window.frame.origin,
"center": window.frame.width/2
])
}
}
}
}
self.menuBarItem?.button?.action = nil
} else {
self.activeModules.forEach { (m: Module) in
m.menuBar.widgets.forEach { w in
w.item.onClick = nil
}
}
self.menuBarItem?.button?.target = self
self.menuBarItem?.button?.action = #selector(self.togglePopup)
self.menuBarItem?.button?.sendAction(on: [.leftMouseDown, .rightMouseDown])
}
}
@objc private func listenForModule(_ notification: Notification) {
guard let name = notification.userInfo?["module"] as? String,
let state = notification.userInfo?["state"] as? Bool,
state,
let module = self.activeModules.first(where: { $0.name == name }) else { return }
module.menuBar.widgets.forEach { w in
w.item.onClick = {
if let window = w.item.window {
NotificationCenter.default.post(name: .togglePopup, object: nil, userInfo: [
"module": module.name,
"widget": w.type,
"origin": window.frame.origin,
"center": window.frame.width/2
])
}
}
}
}
}
private class Popup: NSStackView, Popup_p {
fileprivate var keyboardShortcut: [UInt16] = []
fileprivate var sizeCallback: ((NSSize) -> Void)? = nil
init() {
self.keyboardShortcut = Store.shared.array(key: "CombinedModules_popup_keyboardShortcut", defaultValue: []) as? [UInt16] ?? []
super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
self.orientation = .vertical
self.distribution = .fill
self.alignment = .width
self.spacing = Constants.Popup.spacing
self.reinit()
NotificationCenter.default.addObserver(self, selector: #selector(reinit), name: .toggleModule, object: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
NotificationCenter.default.removeObserver(self, name: .toggleOneView, object: nil)
}
fileprivate func settings() -> NSView? { return nil }
fileprivate func appear() {}
fileprivate func disappear() {}
fileprivate func setKeyboardShortcut(_ binding: [UInt16]) {
self.keyboardShortcut = binding
Store.shared.set(key: "CombinedModules_popup_keyboardShortcut", value: binding)
}
@objc private func reinit() {
self.subviews.forEach({ $0.removeFromSuperview() })
let availableModules = modules.filter({ $0.enabled && $0.portal != nil })
availableModules.forEach { (m: Module) in
if let p = m.portal {
self.addArrangedSubview(p)
}
}
let h = CGFloat(availableModules.count) * Constants.Popup.portalHeight + (CGFloat(availableModules.count-1)*Constants.Popup.spacing)
if h > 0 {
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.sizeCallback?(self.frame.size)
}
}
}
================================================
FILE: Stats/Views/Dashboard.swift
================================================
//
// Stats.swift
// Stats
//
// Created by Serhiy Mytrovtsiy on 24/12/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
class Dashboard: NSStackView {
private var processorValue: String {
guard let cpu = SystemKit.shared.device.info.cpu, cpu.name != nil || cpu.physicalCores != nil || cpu.logicalCores != nil else {
return localizedString("Unknown")
}
var value = ""
if let name = cpu.name {
value += name
}
if cpu.physicalCores != nil || cpu.logicalCores != nil {
if !value.isEmpty {
value += "\n"
}
var mini = ""
if let cores = cpu.physicalCores {
mini += localizedString("Number of cores", "\(cores)")
}
if let threads = cpu.logicalCores {
if mini != "" {
mini += ", "
}
mini += localizedString("Number of threads", "\(threads)")
}
value += "\(mini)"
}
if cpu.eCores != nil || cpu.pCores != nil {
if !value.isEmpty {
value += "\n"
}
var mini = ""
if let eCores = cpu.eCores {
mini += localizedString("Number of e-cores", "\(eCores)")
}
if let pCores = cpu.pCores {
if mini != "" {
mini += "\n"
}
mini += localizedString("Number of p-cores", "\(pCores)")
}
value += "\(mini)"
}
return value
}
private var memoryValue: String {
guard let dimms = SystemKit.shared.device.info.ram?.dimms else {
return localizedString("Unknown")
}
let sizeFormatter = ByteCountFormatter()
sizeFormatter.allowedUnits = [.useGB]
sizeFormatter.countStyle = .memory
var value = ""
for i in 0.. NSView {
let container: NSGridView = NSGridView()
container.rowSpacing = 0
container.yPlacement = .center
container.xPlacement = .center
let deviceImageView: NSImageView = NSImageView(image: SystemKit.shared.device.model.icon)
deviceImageView.widthAnchor.constraint(equalToConstant: 140).isActive = true
deviceImageView.heightAnchor.constraint(equalToConstant: 140).isActive = true
let deviceNameField: NSTextField = TextView()
deviceNameField.alignment = .center
deviceNameField.font = NSFont.systemFont(ofSize: 17, weight: .semibold)
deviceNameField.stringValue = SystemKit.shared.device.model.name
deviceNameField.isSelectable = true
deviceNameField.toolTip = SystemKit.shared.device.model.id
let osField: NSTextField = TextView()
osField.alignment = .center
osField.font = NSFont.systemFont(ofSize: 12, weight: .regular)
osField.stringValue = "macOS \(SystemKit.shared.device.os?.name ?? localizedString("Unknown")) (\(SystemKit.shared.device.os?.version.getFullVersion() ?? ""))"
osField.toolTip = SystemKit.shared.device.os?.build ?? localizedString("Unknown")
osField.isSelectable = true
container.addRow(with: [deviceImageView])
container.addRow(with: [deviceNameField])
container.addRow(with: [osField])
container.row(at: 1).height = 22
container.row(at: 2).height = 20
return container
}
@objc private func windowOpens(_ notification: Notification) {
guard let moduleName = notification.userInfo?["module"] as? String, moduleName == "Dashboard" || moduleName == "Combined modules" else { return }
self.uptimeField?.stringValue = self.uptimeValue
}
}
================================================
FILE: Stats/Views/Settings.swift
================================================
//
// Settings.swift
// Stats
//
// Created by Serhiy Mytrovtsiy on 12/04/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
public extension NSToolbarItem.Identifier {
static let toggleButton = NSToolbarItem.Identifier("toggleButton")
static let previewButton = NSToolbarItem.Identifier("previewButton")
}
class SettingsWindow: NSWindow, NSWindowDelegate, NSToolbarDelegate {
private static let size: CGSize = CGSize(width: 720, height: 480)
private static let frameAutosaveName = "eu.exelban.Stats.Settings.WindowFrame"
private let mainView: MainView = MainView(frame: NSRect(x: 0, y: 0, width: 540, height: 480))
private let sidebarView: SidebarView = SidebarView(frame: NSRect(x: 0, y: 0, width: 180, height: 480))
private var dashboard: NSView = Dashboard()
private var settings: ApplicationSettings = ApplicationSettings()
private var toggleButton: NSControl? = nil
private var activeModuleName: String? = nil
private var settingsPreviewButton: NSView? = nil
private var pauseState: Bool { Store.shared.bool(key: "pause", defaultValue: false) }
init() {
super.init(
contentRect: NSRect(
x: NSScreen.main!.frame.width - SettingsWindow.size.width,
y: NSScreen.main!.frame.height - SettingsWindow.size.height,
width: SettingsWindow.size.width,
height: SettingsWindow.size.height
),
styleMask: [.closable, .titled, .miniaturizable, .fullSizeContentView, .resizable],
backing: .buffered,
defer: false
)
let sidebarViewController = NSSplitViewController()
let sidebarVC: NSViewController = NSViewController(nibName: nil, bundle: nil)
sidebarVC.view = self.sidebarView
let mainVC: NSViewController = NSViewController(nibName: nil, bundle: nil)
mainVC.view = self.mainView
let sidebarItem = NSSplitViewItem(sidebarWithViewController: sidebarVC)
let contentItem = NSSplitViewItem(viewController: mainVC)
sidebarItem.canCollapse = false
contentItem.canCollapse = false
sidebarViewController.addSplitViewItem(sidebarItem)
sidebarViewController.addSplitViewItem(contentItem)
contentItem.minimumThickness = 540
let newToolbar = NSToolbar(identifier: "eu.exelban.Stats.Settings.Toolbar")
newToolbar.allowsUserCustomization = false
newToolbar.autosavesConfiguration = true
newToolbar.displayMode = .default
newToolbar.showsBaselineSeparator = true
newToolbar.delegate = self
self.toolbar = newToolbar
self.contentViewController = sidebarViewController
self.titlebarAppearsTransparent = true
if #unavailable(macOS 26.0) {
self.backgroundColor = .clear
}
self.isRestorable = true
self.setFrameAutosaveName(SettingsWindow.frameAutosaveName)
if !self.setFrameUsingName(SettingsWindow.frameAutosaveName) {
self.positionCenter()
}
self.setIsVisible(false)
self.minSize = NSSize(width: SettingsWindow.size.width, height: SettingsWindow.size.height-Constants.Popup.headerHeight)
let windowController = NSWindowController()
windowController.window = self
windowController.loadWindow()
NotificationCenter.default.addObserver(self, selector: #selector(menuCallback), name: .openModuleSettings, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(toggleSettingsHandler), name: .toggleSettings, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(externalModuleToggle), name: .toggleModule, object: nil)
self.sidebarView.openMenu("Dashboard")
}
deinit {
NotificationCenter.default.removeObserver(self, name: .toggleSettings, object: nil)
NotificationCenter.default.removeObserver(self, name: .openModuleSettings, object: nil)
NotificationCenter.default.removeObserver(self, name: .toggleModule, object: nil)
}
override func performKeyEquivalent(with event: NSEvent) -> Bool {
if event.type == NSEvent.EventType.keyDown && event.modifierFlags.contains(.command) {
if event.keyCode == 12 || event.keyCode == 13 {
self.setIsVisible(false)
return true
} else if event.keyCode == 46 {
self.miniaturize(event)
return true
}
}
return super.performKeyEquivalent(with: event)
}
override func mouseUp(with: NSEvent) {
NotificationCenter.default.post(name: .clickInSettings, object: nil, userInfo: nil)
}
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
switch itemIdentifier {
case .previewButton:
let button = SettingsPreviewButton { [weak self] in
guard let moduleName = self?.activeModuleName else { return }
NotificationCenter.default.post(name: .togglePreview, object: nil, userInfo: ["module": moduleName])
}
self.settingsPreviewButton = button
let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
toolbarItem.view = button
toolbarItem.isBordered = false
return toolbarItem
case .toggleButton:
let switchButton = NSSwitch()
switchButton.state = .on
switchButton.action = #selector(self.toggleEnable)
switchButton.target = self
switchButton.controlSize = .small
self.toggleButton = switchButton
let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
toolbarItem.toolTip = localizedString("Toggle the module")
toolbarItem.view = switchButton
toolbarItem.isBordered = false
return toolbarItem
default:
return nil
}
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [.flexibleSpace, .previewButton, .toggleButton]
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [.flexibleSpace, .previewButton, .toggleButton]
}
@objc private func toggleSettingsHandler(_ notification: Notification) {
if !self.isVisible {
self.setIsVisible(true)
self.makeKeyAndOrderFront(nil)
}
if !self.isKeyWindow {
self.orderFrontRegardless()
}
if var name = notification.userInfo?["module"] as? String {
if name == "Combined modules" { name = "Dashboard" }
self.sidebarView.openMenu(name)
}
}
@objc private func menuCallback(_ notification: Notification) {
if let title = notification.userInfo?["module"] as? String {
var view: NSView = NSView()
if let detectedModule = modules.first(where: { $0.config.name == title }) {
if let v = detectedModule.window {
view = v
}
self.activeModuleName = detectedModule.config.name
toggleNSControlState(self.toggleButton, state: detectedModule.enabled ? .on : .off)
self.toggleButton?.isHidden = false
self.settingsPreviewButton?.isHidden = !detectedModule.config.hasPreview
NotificationCenter.default.post(name: .openWindow, object: nil, userInfo: ["module": detectedModule.config.name, "state": true])
} else if title == "Dashboard" {
view = self.dashboard
self.toggleButton?.isHidden = true
self.settingsPreviewButton?.isHidden = true
NotificationCenter.default.post(name: .openWindow, object: nil, userInfo: ["state": false])
} else if title == "Settings" {
self.settings.viewWillAppear()
view = self.settings
self.toggleButton?.isHidden = true
self.settingsPreviewButton?.isHidden = true
NotificationCenter.default.post(name: .openWindow, object: nil, userInfo: ["state": false])
}
self.title = localizedString(title)
self.mainView.setView(view)
self.sidebarView.openMenu(title)
}
}
@objc private func toggleEnable(_ sender: NSControl) {
guard let moduleName = self.activeModuleName else { return }
NotificationCenter.default.post(name: .toggleModule, object: nil, userInfo: ["module": moduleName, "state": controlState(sender)])
}
@objc private func externalModuleToggle(_ notification: Notification) {
if let name = notification.userInfo?["module"] as? String, name == self.activeModuleName {
if let state = notification.userInfo?["state"] as? Bool {
toggleNSControlState(self.toggleButton, state: state ? .on : .off)
}
}
}
internal func setModules() {
self.sidebarView.setModules(modules)
if !self.pauseState && modules.filter({ $0.enabled != false && $0.available != false && !$0.menuBar.widgets.filter({ $0.isActive }).isEmpty }).isEmpty {
self.setIsVisible(true)
}
}
private func positionCenter() {
self.setFrameOrigin(NSPoint(
x: (NSScreen.main!.frame.width - SettingsWindow.size.width)/2,
y: ((NSScreen.main!.frame.height - SettingsWindow.size.height)/1.75)
))
}
}
// MARK: - MainView
private class MainView: NSView {
fileprivate let container: NSStackView = NSStackView()
private let background: NSVisualEffectView = {
let view = NSVisualEffectView(frame: NSRect.zero)
view.blendingMode = .withinWindow
view.material = .contentBackground
view.state = .active
view.translatesAutoresizingMaskIntoConstraints = false
view.setContentHuggingPriority(.defaultLow, for: .horizontal)
view.setContentHuggingPriority(.defaultLow, for: .vertical)
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
return view
}()
override init(frame: NSRect) {
super.init(frame: NSRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.container.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(self.background, positioned: .below, relativeTo: .none)
self.addSubview(self.container)
NSLayoutConstraint.activate([
self.background.leadingAnchor.constraint(equalTo: leadingAnchor),
self.background.trailingAnchor.constraint(equalTo: trailingAnchor),
self.background.topAnchor.constraint(equalTo: topAnchor),
self.background.bottomAnchor.constraint(equalTo: bottomAnchor),
self.container.leadingAnchor.constraint(equalTo: leadingAnchor),
self.container.trailingAnchor.constraint(equalTo: trailingAnchor),
self.container.topAnchor.constraint(equalTo: topAnchor, constant: Constants.Popup.headerHeight*1.4),
self.container.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func setView(_ view: NSView) {
self.container.subviews.forEach{ $0.removeFromSuperview() }
self.container.addArrangedSubview(view)
NSLayoutConstraint.activate([
view.leftAnchor.constraint(equalTo: self.container.leftAnchor),
view.rightAnchor.constraint(equalTo: self.container.rightAnchor),
view.topAnchor.constraint(equalTo: self.container.topAnchor),
view.bottomAnchor.constraint(equalTo: self.container.bottomAnchor)
])
}
}
// MARK: - Sidebar
private class SidebarView: NSStackView {
private let scrollView: ScrollableStackView
private let supportPopover = NSPopover()
private var pauseButton: NSButton? = nil
private var pauseState: Bool {
get { Store.shared.bool(key: "pause", defaultValue: false) }
set { Store.shared.set(key: "pause", value: newValue) }
}
private var dashboardIcon: NSImage { NSImage(systemSymbolName: "circle.grid.3x3.fill", accessibilityDescription: nil)! }
private var settingsIcon: NSImage { iconFromSymbol(name: "gear", scale: .large) }
private var bugIcon: NSImage { iconFromSymbol(name: "ladybug", scale: .large) }
private var supportIcon: NSImage { iconFromSymbol(name: "heart.fill", scale: .large) }
private var pauseIcon: NSImage { iconFromSymbol(name: "pause.fill", scale: .large) }
private var resumeIcon: NSImage { iconFromSymbol(name: "play.fill", scale: .large) }
private var closeIcon: NSImage { iconFromSymbol(name: "power", scale: .large) }
override init(frame: NSRect) {
self.scrollView = ScrollableStackView(frame: NSRect(x: 0, y: 0, width: frame.width, height: frame.height))
self.scrollView.stackView.spacing = 0
self.scrollView.stackView.edgeInsets = NSEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
super.init(frame: frame)
self.orientation = .vertical
self.spacing = 0
self.widthAnchor.constraint(equalToConstant: frame.width).isActive = true
let spacer = NSView()
spacer.heightAnchor.constraint(equalToConstant: 10).isActive = true
self.scrollView.stackView.addArrangedSubview(MenuItem(icon: self.dashboardIcon, title: "Dashboard"))
self.scrollView.stackView.addArrangedSubview(spacer)
self.supportPopover.behavior = .transient
self.supportPopover.contentViewController = self.supportView()
let additionalButtons: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: frame.width, height: 45))
additionalButtons.heightAnchor.constraint(equalToConstant: 45).isActive = true
additionalButtons.orientation = .horizontal
additionalButtons.distribution = .fillEqually
additionalButtons.alignment = .centerY
additionalButtons.spacing = 0
let pauseButton = self.makeButton(title: localizedString("Pause the Stats"), image: self.pauseState ? self.resumeIcon : self.pauseIcon, action: #selector(togglePause))
self.pauseButton = pauseButton
additionalButtons.addArrangedSubview(self.makeButton(title: localizedString("Settings"), image: self.settingsIcon, action: #selector(openSettings)))
additionalButtons.addArrangedSubview(self.makeButton(title: localizedString("Support the application"), image: self.supportIcon, action: #selector(donate)))
additionalButtons.addArrangedSubview(self.makeButton(title: localizedString("Report a bug"), image: self.bugIcon, action: #selector(reportBug)))
additionalButtons.addArrangedSubview(pauseButton)
additionalButtons.addArrangedSubview(self.makeButton(title: localizedString("Close application"), image: self.closeIcon, action: #selector(closeApp)))
let emptySpace = NSView()
emptySpace.heightAnchor.constraint(equalToConstant: 28).isActive = true
self.addArrangedSubview(self.scrollView)
self.addArrangedSubview(additionalButtons)
NotificationCenter.default.addObserver(self, selector: #selector(listenForPause), name: .pause, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self, name: .pause, object: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func openMenu(_ title: String) {
self.scrollView.stackView.subviews.forEach({ (m: NSView) in
if let menu = m as? MenuItem {
if menu.title == title {
menu.activate()
} else {
menu.reset()
}
}
})
}
fileprivate func setModules(_ list: [Module]) {
list.reversed().forEach { (m: Module) in
if !m.available { return }
let menu: NSView = MenuItem(icon: m.config.icon, title: m.config.name)
self.scrollView.stackView.insertArrangedSubview(menu, at: 2)
}
}
private func makeButton(title: String, image: NSImage, action: Selector) -> NSButton {
let button = NSButton()
button.title = title
button.toolTip = title
button.bezelStyle = .regularSquare
button.translatesAutoresizingMaskIntoConstraints = false
button.imageScaling = .scaleNone
button.image = image
button.contentTintColor = .secondaryLabelColor
button.isBordered = false
button.action = action
button.target = self
button.focusRingType = .none
button.widthAnchor.constraint(equalToConstant: 33).isActive = true
let rect = NSRect(x: 0, y: 0, width: 33, height: 45)
let trackingArea = NSTrackingArea(
rect: rect,
options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp],
owner: self,
userInfo: ["button": title]
)
self.addTrackingArea(trackingArea)
return button
}
private func supportView() -> NSViewController {
let vc: NSViewController = NSViewController(nibName: nil, bundle: nil)
let view: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: 180, height: 54))
view.spacing = 10
view.edgeInsets = NSEdgeInsets(top: 0, left: 15, bottom: 0, right: 0)
view.orientation = .horizontal
let github = SupportButtonView(name: "GitHub Sponsors", image: "github", action: {
NSWorkspace.shared.open(URL(string: "https://github.com/sponsors/exelban")!)
})
let paypal = SupportButtonView(name: "PayPal", image: "paypal", action: {
NSWorkspace.shared.open(URL(string: "https://www.paypal.com/donate?hosted_button_id=3DS5JHDBATMTC")!)
})
let koFi = SupportButtonView(name: "Ko-fi", image: "ko-fi", action: {
NSWorkspace.shared.open(URL(string: "https://ko-fi.com/exelban")!)
})
let patreon = SupportButtonView(name: "Patreon", image: "patreon", action: {
NSWorkspace.shared.open(URL(string: "https://patreon.com/exelban")!)
})
view.addArrangedSubview(github)
view.addArrangedSubview(paypal)
view.addArrangedSubview(koFi)
view.addArrangedSubview(patreon)
vc.view = view
return vc
}
@objc private func openSettings() {
NotificationCenter.default.post(name: .openModuleSettings, object: nil, userInfo: ["module": "Settings"])
}
@objc private func reportBug() {
NSWorkspace.shared.open(URL(string: "https://github.com/exelban/stats/issues/new?template=bug_report.md")!)
}
@objc private func donate(_ sender: NSButton) {
self.supportPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.minY)
}
@objc private func closeApp(_ sender: NSButton) {
NSApp.terminate(sender)
}
@objc private func togglePause() {
self.pauseState = !self.pauseState
self.pauseButton?.toolTip = localizedString(self.pauseState ? "Resume the Stats" : "Pause the Stats")
self.pauseButton?.image = self.pauseState ? self.resumeIcon : self.pauseIcon
NotificationCenter.default.post(name: .pause, object: nil, userInfo: ["state": self.pauseState])
}
@objc func listenForPause() {
self.pauseButton?.toolTip = localizedString(self.pauseState ? "Resume the Stats" : "Pause the Stats")
self.pauseButton?.image = self.pauseState ? self.resumeIcon : self.pauseIcon
}
}
private class MenuItem: NSView {
fileprivate let title: String
private var active: Bool = false
private var imageView: NSImageView? = nil
private var titleView: NSTextField? = nil
init(icon: NSImage?, title: String) {
self.title = title
super.init(frame: NSRect.zero)
self.wantsLayer = true
self.layer?.cornerRadius = 5
var toolTip = ""
if title == "Settings" {
toolTip = localizedString("Open application settings")
} else if title == "Dashboard" {
toolTip = localizedString("Open dashboard")
} else {
toolTip = localizedString("Open \(title) settings")
}
self.toolTip = toolTip
let imageView = NSImageView()
if icon != nil {
imageView.image = icon!
}
imageView.frame = NSRect(x: 8, y: (32 - 18)/2, width: 18, height: 18)
imageView.wantsLayer = true
imageView.contentTintColor = .labelColor
self.imageView = imageView
let titleView = TextView(frame: NSRect(x: 34, y: ((32 - 16)/2) + 1, width: 100, height: 16))
titleView.textColor = .labelColor
titleView.font = NSFont.systemFont(ofSize: 13, weight: .regular)
titleView.stringValue = localizedString(title)
self.titleView = titleView
self.addSubview(imageView)
self.addSubview(titleView)
NSLayoutConstraint.activate([
self.heightAnchor.constraint(equalToConstant: 32)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func mouseDown(with: NSEvent) {
self.activate()
}
fileprivate func activate() {
guard !self.active else { return }
self.active = true
NotificationCenter.default.post(name: .openModuleSettings, object: nil, userInfo: ["module": self.title])
self.layer?.backgroundColor = NSColor.selectedContentBackgroundColor.cgColor
self.imageView?.contentTintColor = .white
self.titleView?.textColor = .white
}
fileprivate func reset() {
self.layer?.backgroundColor = .clear
self.imageView?.contentTintColor = .labelColor
self.titleView?.textColor = .labelColor
self.active = false
}
}
private class SettingsPreviewButton: NSStackView {
private var callback: () -> Void
private var settingsIcon: NSImage { iconFromSymbol(name: "gear", scale: .large) }
private var previewIcon: NSImage { iconFromSymbol(name: "command", scale: .large) }
private var button: NSButton? = nil
private var isSettingsEnabled: Bool = false
fileprivate init(callback: @escaping () -> Void) {
self.callback = callback
super.init(frame: .zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.edgeInsets = NSEdgeInsets(
top: Constants.Settings.margin,
left: Constants.Settings.margin,
bottom: Constants.Settings.margin,
right: Constants.Settings.margin
)
self.spacing = Constants.Settings.margin
let button = NSButton()
button.toolTip = localizedString("Open module settings")
button.bezelStyle = .regularSquare
button.translatesAutoresizingMaskIntoConstraints = false
button.imageScaling = .scaleNone
button.image = self.settingsIcon
button.contentTintColor = .secondaryLabelColor
button.isBordered = false
button.action = #selector(self.action)
button.target = self
button.focusRingType = .none
button.widthAnchor.constraint(equalToConstant: Constants.Widget.height).isActive = true
self.button = button
self.addArrangedSubview(button)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func action() {
guard let button = self.button else { return }
self.callback()
self.isSettingsEnabled = !self.isSettingsEnabled
if self.isSettingsEnabled {
button.image = self.previewIcon
button.toolTip = localizedString("Close module settings")
} else {
button.image = self.settingsIcon
button.toolTip = localizedString("Open module settings")
}
}
}
================================================
FILE: Stats/Views/Setup.swift
================================================
//
// Setup.swift
// Stats
//
// Created by Serhiy Mytrovtsiy on 21/07/2022.
// Using Swift 5.0.
// Running on macOS 12.4.
//
// Copyright © 2022 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
private let setupSize: CGSize = CGSize(width: 600, height: 400)
internal class SetupWindow: NSWindow, NSWindowDelegate {
internal var finishHandler: () -> Void = {}
private let view: SetupContainer = SetupContainer()
private let vc: NSViewController = NSViewController(nibName: nil, bundle: nil)
init() {
self.vc.view = self.view
super.init(
contentRect: NSRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height),
styleMask: [.closable, .titled],
backing: .buffered,
defer: true
)
self.contentViewController = self.vc
self.animationBehavior = .default
self.titlebarAppearsTransparent = true
self.delegate = self
self.title = localizedString("Stats Setup")
self.positionCenter()
self.setIsVisible(false)
let windowController = NSWindowController()
windowController.window = self
windowController.loadWindow()
}
internal func show() {
self.setIsVisible(true)
self.orderFrontRegardless()
}
internal func hide() {
self.close()
}
func windowWillClose(_ notification: Notification) {
self.finishHandler()
}
private func positionCenter() {
self.setFrameOrigin(NSPoint(
x: (NSScreen.main!.frame.width - self.view.frame.width)/2,
y: (NSScreen.main!.frame.height - self.view.frame.height)/1.75
))
}
}
private class SetupContainer: NSStackView {
private let pages: [NSView] = [SetupView_1(), SetupView_2(), SetupView_3(), SetupView_end()]
private var main: NSView = NSView()
private var prevBtn: NSButton = NSButton()
private var nextBtn: NSButton = NSButton()
init() {
super.init(frame: NSRect(x: 0, y: 0, width: setupSize.width, height: setupSize.height))
self.orientation = .vertical
self.spacing = 0
self.addArrangedSubview(self.main)
self.addArrangedSubview(self.footerView())
self.setView(i: 0)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
NSColor.tertiaryLabelColor.set()
let line = NSBezierPath()
line.move(to: NSPoint(x: 0, y: 59))
line.line(to: NSPoint(x: self.frame.width, y: 59))
line.lineWidth = 0.25
line.stroke()
}
private func footerView() -> NSView {
let container = NSStackView()
container.orientation = .horizontal
let prev = NSButton()
prev.bezelStyle = .regularSquare
prev.isEnabled = false
prev.title = localizedString("Previous")
prev.toolTip = localizedString("Previous page")
prev.action = #selector(self.prev)
prev.target = self
self.prevBtn = prev
let next = NSButton()
next.bezelStyle = .regularSquare
next.title = localizedString("Next")
next.toolTip = localizedString("Next page")
next.action = #selector(self.next)
next.target = self
self.nextBtn = next
container.addArrangedSubview(prev)
container.addArrangedSubview(next)
NSLayoutConstraint.activate([
container.heightAnchor.constraint(equalToConstant: 60),
prev.heightAnchor.constraint(equalToConstant: 28),
next.heightAnchor.constraint(equalToConstant: 28)
])
return container
}
@objc private func prev() {
if let current = self.main.subviews.first, let idx = self.pages.firstIndex(where: { $0 == current }) {
self.setView(i: idx-1)
}
}
@objc private func next() {
if let current = self.main.subviews.first, let idx = self.pages.firstIndex(where: { $0 == current }) {
if idx+1 >= self.pages.count, let window = self.window as? SetupWindow {
window.hide()
return
}
self.setView(i: idx+1)
}
}
private func setView(i: Int) {
guard self.pages.indices.contains(i) else { return }
if i == 0 {
self.prevBtn.isEnabled = false
self.nextBtn.isEnabled = true
} else if i == self.pages.count-1 {
self.nextBtn.title = localizedString("Finish")
self.nextBtn.toolTip = localizedString("Finish setup")
} else {
self.prevBtn.isEnabled = true
self.nextBtn.isEnabled = true
self.nextBtn.title = localizedString("Next")
self.nextBtn.toolTip = localizedString("Next page")
}
self.main.subviews.forEach({ $0.removeFromSuperview() })
self.main.addSubview(self.pages[i])
}
}
private class SetupView_1: NSStackView {
init() {
super.init(frame: NSRect(x: 0, y: 0, width: setupSize.width, height: setupSize.height - 60))
let container: NSGridView = NSGridView()
container.rowSpacing = 0
container.yPlacement = .center
container.xPlacement = .center
let title: NSTextField = TextView()
title.alignment = .center
title.font = NSFont.systemFont(ofSize: 20, weight: .semibold)
title.stringValue = localizedString("Welcome to Stats")
title.toolTip = localizedString("Welcome to Stats")
title.isSelectable = false
let icon: NSImageView = NSImageView(image: NSImage(named: NSImage.Name("AppIcon"))!)
icon.heightAnchor.constraint(equalToConstant: 120).isActive = true
let message: NSTextField = TextView()
message.alignment = .center
message.font = NSFont.systemFont(ofSize: 12, weight: .regular)
message.stringValue = localizedString("welcome_message")
message.toolTip = localizedString("welcome_message")
message.isSelectable = false
container.addRow(with: [title])
container.addRow(with: [icon])
container.addRow(with: [message])
container.row(at: 0).height = 100
container.row(at: 1).height = 120
self.addArrangedSubview(container)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private class SetupView_2: NSStackView {
init() {
super.init(frame: NSRect(x: 0, y: 0, width: setupSize.width, height: setupSize.height - 60))
let container: NSGridView = NSGridView()
container.rowSpacing = 0
container.yPlacement = .center
container.xPlacement = .center
let title: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: container.frame.width, height: 22))
title.alignment = .center
title.font = NSFont.systemFont(ofSize: 20, weight: .semibold)
title.stringValue = localizedString("Start at login")
title.toolTip = localizedString("Start at login")
title.isSelectable = false
container.addRow(with: [title])
container.addRow(with: [self.content()])
container.row(at: 0).height = 100
self.addArrangedSubview(container)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func content() -> NSView {
let container: NSGridView = NSGridView()
container.addRow(with: [self.option(
tag: 1,
state: LaunchAtLogin.isEnabled,
text: localizedString("Start the application automatically when starting your Mac")
)])
container.addRow(with: [self.option(
tag: 2,
state: !LaunchAtLogin.isEnabled,
text: localizedString("Do not start the application automatically when starting your Mac")
)])
return container
}
private func option(tag: Int, state: Bool, text: String) -> NSView {
let button: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: 30, height: 20))
button.setButtonType(.radio)
button.state = state ? .on : .off
button.title = text
button.action = #selector(self.toggle)
button.isBordered = false
button.isTransparent = false
button.target = self
button.tag = tag
return button
}
@objc private func toggle(_ sender: NSButton) {
LaunchAtLogin.isEnabled = sender.tag == 1
if !Store.shared.exist(key: "runAtLoginInitialized") {
Store.shared.set(key: "runAtLoginInitialized", value: true)
}
}
}
private class SetupView_3: NSStackView {
private var value: AppUpdateInterval {
get {
let value = Store.shared.string(key: "update-interval", defaultValue: AppUpdateInterval.silent.rawValue)
return AppUpdateInterval(rawValue: value) ?? AppUpdateInterval.silent
}
}
init() {
super.init(frame: NSRect(x: 0, y: 0, width: setupSize.width, height: setupSize.height - 60))
let container: NSGridView = NSGridView()
container.rowSpacing = 0
container.yPlacement = .center
container.xPlacement = .center
let title: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: container.frame.width, height: 22))
title.alignment = .center
title.font = NSFont.systemFont(ofSize: 20, weight: .semibold)
title.stringValue = localizedString("Check for updates")
title.toolTip = localizedString("Check for updates")
title.isSelectable = false
container.addRow(with: [title])
container.addRow(with: [self.content()])
container.row(at: 0).height = 100
self.addArrangedSubview(container)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func content() -> NSView {
let container: NSGridView = NSGridView()
container.addRow(with: [self.option(
value: AppUpdateInterval.silent,
text: localizedString("Do everything silently in the background (recommended)")
)])
container.addRow(with: [self.option(
value: AppUpdateInterval.atStart,
text: localizedString("Check for a new version on startup")
)])
container.addRow(with: [NSView()])
container.addRow(with: [self.option(
value: AppUpdateInterval.oncePerDay,
text: localizedString("Check for a new version every day (once a day)")
)])
container.addRow(with: [self.option(
value: AppUpdateInterval.oncePerWeek,
text: localizedString("Check for a new version every week (once a week)")
)])
container.addRow(with: [self.option(
value: AppUpdateInterval.oncePerMonth,
text: localizedString("Check for a new version every month (once a month)")
)])
container.addRow(with: [NSView()])
container.addRow(with: [self.option(
value: AppUpdateInterval.never,
text: localizedString("Never check for updates (not recommended)")
)])
container.row(at: 2).height = 1
container.row(at: container.numberOfRows-2).height = 1
return container
}
private func option(value: AppUpdateInterval, text: String) -> NSView {
let button: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: 30, height: 20))
button.setButtonType(.radio)
button.state = self.value == value ? .on : .off
button.title = text
button.action = #selector(self.toggle)
button.isBordered = false
button.isTransparent = false
button.target = self
button.identifier = NSUserInterfaceItemIdentifier(rawValue: value.rawValue)
return button
}
@objc private func toggle(_ sender: NSButton) {
guard let key = sender.identifier?.rawValue, !key.isEmpty else { return }
Store.shared.set(key: "update-interval", value: key)
}
}
private class SetupView_end: NSStackView {
init() {
super.init(frame: NSRect(x: 0, y: 0, width: setupSize.width, height: setupSize.height - 60))
let container: NSGridView = NSGridView()
container.rowSpacing = 0
container.yPlacement = .center
container.xPlacement = .center
let title: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: container.frame.width, height: 22))
title.alignment = .center
title.font = NSFont.systemFont(ofSize: 20, weight: .semibold)
title.stringValue = localizedString("The configuration is completed")
title.toolTip = localizedString("The configuration is completed")
title.isSelectable = false
let content = NSStackView()
content.orientation = .vertical
let message: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: container.frame.width, height: 16))
message.alignment = .center
message.font = NSFont.systemFont(ofSize: 13, weight: .regular)
message.stringValue = localizedString("finish_setup_message")
message.toolTip = localizedString("finish_setup_message")
message.isSelectable = false
let support: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: 160, height: 50))
support.edgeInsets = NSEdgeInsets(top: 12, left: 0, bottom: 0, right: 0)
support.spacing = 12
support.orientation = .horizontal
let github = SupportButtonView(name: "GitHub Sponsors", image: "github", action: {
NSWorkspace.shared.open(URL(string: "https://github.com/sponsors/exelban")!)
})
let paypal = SupportButtonView(name: "PayPal", image: "paypal", action: {
NSWorkspace.shared.open(URL(string: "https://www.paypal.com/donate?hosted_button_id=3DS5JHDBATMTC")!)
})
let koFi = SupportButtonView(name: "Ko-fi", image: "ko-fi", action: {
NSWorkspace.shared.open(URL(string: "https://ko-fi.com/exelban")!)
})
let patreon = SupportButtonView(name: "Patreon", image: "patreon", action: {
NSWorkspace.shared.open(URL(string: "https://patreon.com/exelban")!)
})
support.addArrangedSubview(github)
support.addArrangedSubview(paypal)
support.addArrangedSubview(koFi)
support.addArrangedSubview(patreon)
content.addArrangedSubview(message)
content.addArrangedSubview(support)
container.addRow(with: [title])
container.addRow(with: [content])
container.row(at: 0).height = 100
self.addArrangedSubview(container)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
internal class SupportButtonView: NSButton {
internal var callback: (() -> Void) = {}
init(name: String, image: String, action: @escaping () -> Void) {
self.callback = action
super.init(frame: NSRect(x: 0, y: 0, width: 30, height: 30))
self.title = name
self.toolTip = name
self.bezelStyle = .regularSquare
self.translatesAutoresizingMaskIntoConstraints = false
self.imageScaling = .scaleProportionallyDown
self.image = Bundle(for: type(of: self)).image(forResource: image)!
self.isBordered = false
self.target = self
self.focusRingType = .none
self.action = #selector(self.click)
self.wantsLayer = true
self.alphaValue = 0.9
self.addTrackingArea(NSTrackingArea(
rect: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height),
options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp],
owner: self,
userInfo: nil
))
NSLayoutConstraint.activate([
self.widthAnchor.constraint(equalToConstant: self.bounds.width),
self.heightAnchor.constraint(equalToConstant: self.bounds.height)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func mouseEntered(with: NSEvent) {
self.alphaValue = 1
NSCursor.pointingHand.set()
}
public override func mouseExited(with: NSEvent) {
self.alphaValue = 0.9
NSCursor.arrow.set()
}
@objc private func click() {
self.callback()
}
}
================================================
FILE: Stats/Views/Support.swift
================================================
//
// Support.swift
// Stats
//
// Created by Serhiy Mytrovtsiy on 14/01/2025
// Using Swift 6.0
// Running on macOS 15.1
//
// Copyright © 2025 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class SupportWindow: NSWindow, NSWindowDelegate {
private let viewController: SupportViewController = SupportViewController()
init() {
super.init(
contentRect: NSRect(
x: NSScreen.main!.frame.width - self.viewController.view.frame.width,
y: NSScreen.main!.frame.height - self.viewController.view.frame.height,
width: self.viewController.view.frame.width,
height: self.viewController.view.frame.height
),
styleMask: [.closable, .titled, .fullSizeContentView],
backing: .buffered,
defer: true
)
self.title = "Support Stats"
self.titleVisibility = .hidden
self.contentViewController = self.viewController
self.titlebarAppearsTransparent = true
self.positionCenter()
self.setIsVisible(false)
let windowController = NSWindowController()
windowController.window = self
windowController.loadWindow()
}
private func positionCenter() {
self.setFrameOrigin(NSPoint(
x: (NSScreen.main!.frame.width - self.viewController.view.frame.width)/2,
y: (NSScreen.main!.frame.height - self.viewController.view.frame.height)/1.75
))
}
internal func show() {
self.setIsVisible(true)
self.orderFrontRegardless()
}
}
private class SupportViewController: NSViewController {
private var support: SupportView
public init() {
self.support = SupportView(frame: NSRect(x: 0, y: 0, width: 460, height: 340))
super.init(nibName: nil, bundle: nil)
self.view = self.support
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private class SupportView: NSStackView {
override init(frame: NSRect) {
super.init(frame: CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.width, height: frame.height))
self.wantsLayer = true
self.orientation = .vertical
self.spacing = 0
let sidebar = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height))
sidebar.material = .sidebar
sidebar.blendingMode = .behindWindow
sidebar.state = .active
self.addSubview(sidebar, positioned: .below, relativeTo: nil)
let container: NSStackView = NSStackView()
container.widthAnchor.constraint(equalToConstant: self.frame.width - 40).isActive = true
container.orientation = .vertical
let textField: NSTextField = TextView()
textField.wantsLayer = false
textField.alignment = .center
textField.font = NSFont.systemFont(ofSize: 14)
textField.stringValue = localizedString("Support text")
textField.isSelectable = false
container.addArrangedSubview(NSView())
container.addArrangedSubview(textField)
container.addArrangedSubview(NSView())
let support: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: 160, height: 60))
support.heightAnchor.constraint(equalToConstant: 80).isActive = true
support.edgeInsets = NSEdgeInsets(top: 12, left: 0, bottom: 0, right: 0)
support.spacing = 20
support.orientation = .horizontal
let github = SupportButtonView(name: "GitHub Sponsors", image: "github", action: {
NSWorkspace.shared.open(URL(string: "https://github.com/sponsors/exelban")!)
})
let paypal = SupportButtonView(name: "PayPal", image: "paypal", action: {
NSWorkspace.shared.open(URL(string: "https://www.paypal.com/donate?hosted_button_id=3DS5JHDBATMTC")!)
})
let koFi = SupportButtonView(name: "Ko-fi", image: "ko-fi", action: {
NSWorkspace.shared.open(URL(string: "https://ko-fi.com/exelban")!)
})
let patreon = SupportButtonView(name: "Patreon", image: "patreon", action: {
NSWorkspace.shared.open(URL(string: "https://patreon.com/exelban")!)
})
support.addArrangedSubview(github)
support.addArrangedSubview(paypal)
support.addArrangedSubview(koFi)
support.addArrangedSubview(patreon)
let footer = NSStackView()
footer.heightAnchor.constraint(equalToConstant: 60).isActive = true
footer.orientation = .horizontal
let close = NSButton()
close.heightAnchor.constraint(equalToConstant: 28).isActive = true
close.bezelStyle = .regularSquare
close.title = localizedString("Close")
close.toolTip = localizedString("Close")
close.action = #selector(self.close)
close.target = self
footer.addArrangedSubview(close)
self.addArrangedSubview(container)
self.addArrangedSubview(support)
self.addArrangedSubview(footer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func close() {
self.window?.close()
}
}
================================================
FILE: Stats/Views/Update.swift
================================================
//
// Update.swift
// Stats
//
// Created by Serhiy Mytrovtsiy on 21/05/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
internal class UpdateWindow: NSWindow, NSWindowDelegate {
private let viewController: UpdateViewController = UpdateViewController()
init() {
super.init(
contentRect: NSRect(
x: NSScreen.main!.frame.width - self.viewController.view.frame.width,
y: NSScreen.main!.frame.height - self.viewController.view.frame.height,
width: self.viewController.view.frame.width,
height: self.viewController.view.frame.height
),
styleMask: [.closable, .titled],
backing: .buffered,
defer: true
)
self.title = "Stats"
self.contentViewController = self.viewController
self.titlebarAppearsTransparent = true
self.positionCenter()
self.setIsVisible(false)
let windowController = NSWindowController()
windowController.window = self
windowController.loadWindow()
}
internal func open(_ v: version_s, settingButton: Bool = false) {
if !self.isVisible || settingButton {
self.setIsVisible(true)
self.makeKeyAndOrderFront(nil)
}
self.viewController.open(v)
}
private func positionCenter() {
self.setFrameOrigin(NSPoint(
x: (NSScreen.main!.frame.width - self.viewController.view.frame.width)/2,
y: (NSScreen.main!.frame.height - self.viewController.view.frame.height)/1.75
))
}
}
private class UpdateViewController: NSViewController {
private var update: UpdateView
public init() {
self.update = UpdateView(frame: NSRect(x: 0, y: 0, width: 280, height: 176))
super.init(nibName: nil, bundle: nil)
self.view = self.update
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func open(_ v: version_s) {
self.update.clear()
if v.newest {
self.update.newVersion(v)
return
}
self.update.noUpdates()
}
}
private class UpdateView: NSView {
private var version: version_s? = nil
private var path: String = ""
override init(frame: NSRect) {
super.init(frame: CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.width, height: frame.height))
self.wantsLayer = true
let sidebar = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height))
sidebar.material = .sidebar
sidebar.blendingMode = .behindWindow
sidebar.state = .active
self.addSubview(sidebar)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func newVersion(_ version: version_s) {
self.version = version
let view: NSStackView = NSStackView(frame: NSRect(
x: Constants.Settings.margin,
y: 0,
width: self.frame.width-(Constants.Settings.margin*2),
height: self.frame.height
))
view.orientation = .vertical
view.alignment = .centerX
view.distribution = .fillEqually
view.spacing = Constants.Settings.margin
view.edgeInsets = NSEdgeInsets(
top: Constants.Settings.margin*2,
left: 0,
bottom: Constants.Settings.margin,
right: 0
)
let header: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: 0, height: 44))
header.heightAnchor.constraint(equalToConstant: header.frame.height).isActive = true
header.orientation = .horizontal
header.spacing = 10
header.distribution = .equalCentering
let icon: NSImageView = NSImageView(image: NSImage(named: NSImage.Name("AppIcon"))!)
icon.setFrameSize(NSSize(width: 44, height: 44))
icon.widthAnchor.constraint(equalToConstant: 44).isActive = true
let title: NSTextField = TextView()
title.font = NSFont.systemFont(ofSize: 14, weight: .medium)
title.stringValue = localizedString("New version available")
header.addArrangedSubview(icon)
header.addArrangedSubview(title)
let versions: NSGridView = NSGridView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 32))
versions.heightAnchor.constraint(equalToConstant: versions.frame.height).isActive = true
versions.rowSpacing = 0
versions.yPlacement = .fill
versions.xPlacement = .fill
let currentVersionTitle: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 16))
currentVersionTitle.stringValue = localizedString("Current version: ")
let currentVersion: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 0))
currentVersion.stringValue = version.current
let latestVersionTitle: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 16))
latestVersionTitle.stringValue = localizedString("Latest version: ")
let latestVersion: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 0))
latestVersion.stringValue = version.latest
versions.addRow(with: [currentVersionTitle, currentVersion])
versions.addRow(with: [latestVersionTitle, latestVersion])
let buttons: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 26))
buttons.heightAnchor.constraint(equalToConstant: buttons.frame.height).isActive = true
buttons.orientation = .horizontal
buttons.spacing = 10
buttons.distribution = .fillEqually
let closeButton: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: view.frame.width/2, height: 26))
closeButton.title = localizedString("Close")
closeButton.bezelStyle = .rounded
closeButton.action = #selector(self.close)
closeButton.target = self
let changelogButton: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: 0, height: 26))
changelogButton.title = localizedString("Changelog")
changelogButton.bezelStyle = .rounded
changelogButton.action = #selector(self.changelog)
changelogButton.target = self
let downloadButton: NSButton = NSButton(frame: NSRect(x: view.frame.width/2, y: 0, width: view.frame.width/2, height: 26))
downloadButton.title = localizedString("Download")
downloadButton.bezelStyle = .rounded
downloadButton.action = #selector(self.download)
downloadButton.target = self
buttons.addArrangedSubview(closeButton)
buttons.addArrangedSubview(changelogButton)
buttons.addArrangedSubview(downloadButton)
view.addArrangedSubview(header)
view.addArrangedSubview(NSView())
view.addArrangedSubview(versions)
view.addArrangedSubview(NSView())
view.addArrangedSubview(buttons)
self.addSubview(view)
}
internal func noUpdates() {
let view: NSView = NSView(frame: NSRect(x: 10, y: 10, width: self.frame.width - 20, height: self.frame.height - 20))
let title: NSTextField = TextView(frame: NSRect(x: 0, y: ((view.frame.height - 18)/2), width: view.frame.width, height: 34))
title.font = NSFont.systemFont(ofSize: 14, weight: .light)
title.alignment = .center
title.stringValue = localizedString("The latest version of Stats installed")
let button: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 26))
button.title = localizedString("Close")
button.bezelStyle = .rounded
button.action = #selector(self.close)
button.target = self
view.addSubview(button)
view.addSubview(title)
self.addSubview(view)
}
internal func clear() {
self.subviews.filter{ !($0 is NSVisualEffectView) }.forEach{ $0.removeFromSuperview() }
}
@objc private func download() {
guard let urlString = self.version?.url, let url = URL(string: urlString) else {
return
}
self.clear()
let view: NSView = NSView(frame: NSRect(x: 10, y: 10, width: self.frame.width - 20, height: self.frame.height - 20 - 26))
let title: NSTextField = TextView(frame: NSRect(x: 0, y: view.frame.height - 28, width: view.frame.width, height: 18))
title.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
title.alignment = .center
title.stringValue = localizedString("Downloading...")
let progressBar: NSProgressIndicator = NSProgressIndicator()
progressBar.frame = NSRect(x: 20, y: 64, width: view.frame.width - 40, height: 22)
progressBar.minValue = 0
progressBar.maxValue = 1
progressBar.isIndeterminate = false
let state: NSTextField = TextView(frame: NSRect(x: 0, y: 48, width: view.frame.width, height: 18))
state.font = NSFont.systemFont(ofSize: 12, weight: .light)
state.alignment = .center
state.textColor = .secondaryLabelColor
state.stringValue = "0%"
let closeButton: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 26))
closeButton.title = localizedString("Cancel")
closeButton.bezelStyle = .rounded
closeButton.action = #selector(self.close)
closeButton.target = self
let installButton: NSButton = NSButton(frame: NSRect(x: view.frame.width/2, y: 0, width: view.frame.width/2, height: 26))
installButton.title = localizedString("Install")
installButton.bezelStyle = .rounded
installButton.action = #selector(self.install)
installButton.target = self
installButton.isHidden = true
updater.download(url, progress: { progress in
DispatchQueue.main.async {
progressBar.doubleValue = progress.fractionCompleted
state.stringValue = "\(Int(progress.fractionCompleted*100))%"
}
}, completion: { path in
self.path = path
DispatchQueue.main.async {
closeButton.setFrameSize(NSSize(width: view.frame.width/2, height: closeButton.frame.height))
installButton.isHidden = false
}
})
view.addSubview(title)
view.addSubview(progressBar)
view.addSubview(state)
view.addSubview(closeButton)
view.addSubview(installButton)
self.addSubview(view)
}
@objc private func close() {
self.window?.close()
}
@objc private func changelog() {
if let version = self.version {
NSWorkspace.shared.open(URL(string: "https://github.com/exelban/stats/releases/tag/\(version.latest)")!)
}
}
@objc private func install() {
updater.install(path: self.path) { error in
if let error {
showAlert("Error update Stats", error, .critical)
}
}
}
}
================================================
FILE: Stats/helpers.swift
================================================
//
// helpers.swift
// Stats
//
// Created by Serhiy Mytrovtsiy on 13/07/2020.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
//
import Cocoa
import Kit
import UserNotifications
extension AppDelegate {
internal func parseArguments() {
let args = CommandLine.arguments
if args.contains("--reset") {
debug("Receive --reset argument. Resetting store (UserDefaults)...")
Store.shared.reset()
}
if let disableIndex = args.firstIndex(of: "--disable") {
if args.indices.contains(disableIndex+1) {
let disableModules = args[disableIndex+1].split(separator: ",")
disableModules.forEach { (moduleName: Substring) in
if let module = modules.first(where: { $0.config.name.lowercased() == moduleName.lowercased()}) {
module.unmount()
}
}
}
}
if let mountIndex = args.firstIndex(of: "--mount-path") {
if args.indices.contains(mountIndex+1) {
let mountPath = args[mountIndex+1]
asyncShell("/usr/bin/hdiutil detach \(mountPath)")
asyncShell("/bin/rm -rf \(mountPath)")
debug("DMG was unmounted and mountPath deleted")
}
}
if let dmgIndex = args.firstIndex(of: "--dmg-path") {
if args.indices.contains(dmgIndex+1) {
asyncShell("/bin/rm -rf \(args[dmgIndex+1])")
debug("DMG was deleted")
}
}
}
internal func parseVersion() {
let key = "version"
let currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
guard let updateInterval = AppUpdateInterval(rawValue: Store.shared.string(key: "update-interval", defaultValue: AppUpdateInterval.silent.rawValue)) else {
return
}
if !Store.shared.exist(key: key) {
Store.shared.reset()
debug("Previous version not detected. Current version (\(currentVersion) set")
} else {
let prevVersion = Store.shared.string(key: key, defaultValue: "")
if prevVersion == currentVersion {
return
}
if updateInterval != .silent && isNewestVersion(currentVersion: prevVersion, latestVersion: currentVersion) {
let title: String = localizedString("Successfully updated")
let subtitle: String = localizedString("Stats was updated to v", currentVersion)
let id = showNotification(title: title, subtitle: subtitle, delegate: self)
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
removeNotification(id)
}
}
debug("Detected previous version \(prevVersion). Current version (\(currentVersion) set")
}
Store.shared.set(key: key, value: currentVersion)
}
internal func defaultValues() {
if Store.shared.exist(key: "runAtLoginInitialized") {
LaunchAtLogin.migrate()
}
if Store.shared.exist(key: "dockIcon") {
let dockIconStatus = Store.shared.bool(key: "dockIcon", defaultValue: false) ? NSApplication.ActivationPolicy.regular : NSApplication.ActivationPolicy.accessory
NSApp.setActivationPolicy(dockIconStatus)
}
self.checkIfShouldShowSupportWindow()
self.supportActivity.interval = 60 * 60 * 24 * 30
self.supportActivity.repeats = true
self.supportActivity.schedule { (completion: @escaping NSBackgroundActivityScheduler.CompletionHandler) in
DispatchQueue.main.async {
self.checkIfShouldShowSupportWindow()
}
completion(NSBackgroundActivityScheduler.Result.finished)
}
if let updateInterval = AppUpdateInterval(rawValue: Store.shared.string(key: "update-interval", defaultValue: AppUpdateInterval.silent.rawValue)) {
self.updateActivity.invalidate()
self.updateActivity.repeats = true
debug("Application update interval is '\(updateInterval.rawValue)'")
switch updateInterval {
case .oncePerDay: self.updateActivity.interval = 60 * 60 * 24
case .oncePerWeek: self.updateActivity.interval = 60 * 60 * 24 * 7
case .oncePerMonth: self.updateActivity.interval = 60 * 60 * 24 * 30
case .atStart:
self.checkForNewVersion()
return
case .silent:
self.checkForNewVersion(silent: true)
return
default: return
}
self.updateActivity.schedule { (completion: @escaping NSBackgroundActivityScheduler.CompletionHandler) in
self.checkForNewVersion()
completion(NSBackgroundActivityScheduler.Result.finished)
}
}
}
internal func setup(completion: @escaping () -> Void) {
if Store.shared.exist(key: "setupProcess") || Store.shared.exist(key: "runAtLoginInitialized") {
completion()
return
}
debug("showing the setup window")
self.setupWindow.show()
self.setupWindow.finishHandler = {
debug("setup is finished, starting the app")
completion()
}
Store.shared.set(key: "setupProcess", value: true)
}
internal func checkForNewVersion(silent: Bool = false) {
updater.check { result, error in
if error != nil {
debug("error updater.check(): \(error!.localizedDescription)")
return
}
guard error == nil, let version: version_s = result else {
debug("download error(): \(error!.localizedDescription)")
return
}
if !version.newest {
return
}
if silent {
if let url = URL(string: version.url) {
updater.download(url, completion: { path in
updater.install(path: path) { error in
if let error {
showAlert("Error update Stats", error, .critical)
}
}
})
}
return
}
debug("show update view because new version of app found: \(version.latest)")
let center = UNUserNotificationCenter.current()
center.getNotificationSettings { settings in
switch settings.authorizationStatus {
case .authorized, .provisional:
self.showUpdateNotification(version: version)
case .denied:
self.showUpdateWindow(version: version)
case .notDetermined:
center.requestAuthorization(options: [.sound, .alert, .badge], completionHandler: { (_, error) in
if error == nil {
NSApplication.shared.registerForRemoteNotifications()
self.showUpdateNotification(version: version)
} else {
self.showUpdateWindow(version: version)
}
})
@unknown default:
self.showUpdateWindow(version: version)
error_msg("unknown notification setting")
}
}
}
}
func checkIfShouldShowSupportWindow() {
if !Store.shared.exist(key: "setupProcess") || !Store.shared.exist(key: "runAtLoginInitialized") {
return
}
let now = Int(Date().timeIntervalSince1970)
if !Store.shared.exist(key: "support_ts") {
Store.shared.set(key: "support_ts", value: now)
self.supportWindow.show()
return
}
let lastShow = Store.shared.int(key: "support_ts", defaultValue: now)
let diff = (now - lastShow) / (60 * 60 * 24)
if diff <= 31 {
debug("The support window was shown \(diff) days ago, stopping...")
return
}
Store.shared.set(key: "support_ts", value: now)
self.supportWindow.show()
}
private func showUpdateNotification(version: version_s) {
debug("show update notification")
_ = showNotification(
title: localizedString("New version available"),
subtitle: localizedString("Click to install the new version of Stats"),
userInfo: ["url": version.url],
delegate: self
)
}
private func showUpdateWindow(version: version_s) {
debug("show update window")
DispatchQueue.main.async(execute: {
self.updateWindow.open(version)
})
}
@objc internal func listenForAppPause() {
for m in modules {
if self.pauseState && m.enabled {
m.disable()
} else if !self.pauseState && !m.enabled && Store.shared.bool(key: "\(m.config.name)_state", defaultValue: m.config.defaultState) {
m.enable()
}
}
self.icon()
}
internal func icon() {
if self.pauseState {
self.menuBarItem = NSStatusBar.system.statusItem(withLength: AppIcon.size.width)
DispatchQueue.main.async(execute: {
self.menuBarItem?.autosaveName = "Stats"
})
self.menuBarItem?.button?.addSubview(AppIcon())
self.menuBarItem?.button?.target = self
self.menuBarItem?.button?.action = #selector(self.openSettings)
self.menuBarItem?.button?.sendAction(on: [.leftMouseDown, .rightMouseDown])
} else {
if let item = self.menuBarItem {
NSStatusBar.system.removeStatusItem(item)
}
self.menuBarItem = nil
}
}
@objc internal func openSettings() {
NotificationCenter.default.post(name: .toggleSettings, object: nil, userInfo: ["module": "Dashboard"])
}
internal func handleKeyEvent(_ event: NSEvent) {
var keyCodes: [UInt16] = []
if event.modifierFlags.contains(.control) { keyCodes.append(59) }
if event.modifierFlags.contains(.shift) { keyCodes.append(60) }
if event.modifierFlags.contains(.command) { keyCodes.append(55) }
if event.modifierFlags.contains(.option) { keyCodes.append(58) }
keyCodes.append(event.keyCode)
guard !keyCodes.isEmpty,
let module = modules.first(where: { $0.enabled && $0.popupKeyboardShortcut == keyCodes }),
let widget = module.menuBar.widgets.filter({ $0.isActive }).first,
let window = widget.item.window else { return }
NotificationCenter.default.post(name: .togglePopup, object: nil, userInfo: [
"module": module.name,
"widget": widget.type,
"origin": window.frame.origin,
"center": window.frame.width/2
])
}
}
================================================
FILE: Stats.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
5C038CF62D86EE8A00516809 /* Remote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C038CF52D86EE8700516809 /* Remote.swift */; };
5C044F7A2B3DE6F3005F6951 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C044F792B3DE6F3005F6951 /* portal.swift */; };
5C0A2A8A292A5B4D009B4C1F /* SMJobBlessUtil.py in Resources */ = {isa = PBXBuildFile; fileRef = 5C0A2A89292A5B4D009B4C1F /* SMJobBlessUtil.py */; };
5C0A9CA22C467AA300EE6A89 /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0A9CA12C467AA300EE6A89 /* widget.swift */; };
5C0A9CA42C467F7A00EE6A89 /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0A9CA32C467F7A00EE6A89 /* widget.swift */; };
5C0A9CA52C46838300EE6A89 /* CPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A97CECA2537331B00742D8F /* CPU.framework */; };
5C0A9CAA2C46838A00EE6A89 /* GPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A90E18924EAD2BB00471E9A /* GPU.framework */; };
5C0A9CAF2C46838F00EE6A89 /* RAM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A81C7562449A41400825D92 /* RAM.framework */; };
5C0A9CB42C46839500EE6A89 /* Disk.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AF9EE0224648751005D2270 /* Disk.framework */; };
5C1E45562D11D66A00525864 /* libIOReport.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1E45552D11D66200525864 /* libIOReport.tbd */; };
5C2016432D36D0EF0049C788 /* Support.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2016422D36D0EF0049C788 /* Support.swift */; };
5C21D80B296C7B81005BA16D /* CombinedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C21D80A296C7B81005BA16D /* CombinedView.swift */; };
5C2229A329CCB3C400F00E69 /* Clock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C22299D29CCB3C400F00E69 /* Clock.framework */; };
5C2229A429CCB3C400F00E69 /* Clock.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5C22299D29CCB3C400F00E69 /* Clock.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5C2229A929CCB41900F00E69 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2229A829CCB41900F00E69 /* main.swift */; };
5C2229AB29CCB53E00F00E69 /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5C2229AA29CCB53E00F00E69 /* config.plist */; };
5C2229AF29CDC08700F00E69 /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2229AE29CDC08700F00E69 /* settings.swift */; };
5C2229B029CDFBF600F00E69 /* Kit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; };
5C2229B829CE3F3300F00E69 /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2229B729CE3F3300F00E69 /* popup.swift */; };
5C2229BD29CF685A00F00E69 /* header.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C2229BB29CF66B100F00E69 /* header.h */; };
5C23BC0229A0102500DBA990 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C23BC0129A0102500DBA990 /* portal.swift */; };
5C23BC0429A014AC00DBA990 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C23BC0329A014AC00DBA990 /* portal.swift */; };
5C23BC0829A03D1200DBA990 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C23BC0729A03D1200DBA990 /* portal.swift */; };
5C23BC0A29A0EDA300DBA990 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C23BC0929A0EDA300DBA990 /* portal.swift */; };
5C23BC0C29A10BE000DBA990 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C23BC0B29A10BE000DBA990 /* portal.swift */; };
5C23BC1029A3B5AE00DBA990 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C23BC0F29A3B5AE00DBA990 /* portal.swift */; };
5C3068732C32E83200B05EFA /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3068722C32E83200B05EFA /* widget.swift */; };
5C4E8BA12B6EEE8E00F148B6 /* lldb.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C4E8BA02B6EEE8E00F148B6 /* lldb.m */; };
5C4E8BB12B6EEEE800F148B6 /* filter_policy.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BA22B6EEEE800F148B6 /* filter_policy.h */; };
5C4E8BB22B6EEEE800F148B6 /* export.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BA32B6EEEE800F148B6 /* export.h */; };
5C4E8BB32B6EEEE800F148B6 /* slice.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BA42B6EEEE800F148B6 /* slice.h */; };
5C4E8BB42B6EEEE800F148B6 /* write_batch.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BA52B6EEEE800F148B6 /* write_batch.h */; };
5C4E8BB52B6EEEE800F148B6 /* status.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BA62B6EEEE800F148B6 /* status.h */; };
5C4E8BB62B6EEEE800F148B6 /* iterator.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BA72B6EEEE800F148B6 /* iterator.h */; };
5C4E8BB72B6EEEE800F148B6 /* table_builder.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BA82B6EEEE800F148B6 /* table_builder.h */; };
5C4E8BB82B6EEEE800F148B6 /* env.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BA92B6EEEE800F148B6 /* env.h */; };
5C4E8BB92B6EEEE800F148B6 /* comparator.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BAA2B6EEEE800F148B6 /* comparator.h */; };
5C4E8BBA2B6EEEE800F148B6 /* c.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BAB2B6EEEE800F148B6 /* c.h */; };
5C4E8BBB2B6EEEE800F148B6 /* options.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BAC2B6EEEE800F148B6 /* options.h */; };
5C4E8BBC2B6EEEE800F148B6 /* table.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BAD2B6EEEE800F148B6 /* table.h */; };
5C4E8BBD2B6EEEE800F148B6 /* cache.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BAE2B6EEEE800F148B6 /* cache.h */; };
5C4E8BBE2B6EEEE800F148B6 /* dumpfile.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BAF2B6EEEE800F148B6 /* dumpfile.h */; };
5C4E8BBF2B6EEEE800F148B6 /* db.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BB02B6EEEE800F148B6 /* db.h */; };
5C4E8BC42B6EF65E00F148B6 /* libleveldb.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E8BC32B6EF65E00F148B6 /* libleveldb.a */; };
5C4E8BC72B6EF98800F148B6 /* DB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4E8BC62B6EF98800F148B6 /* DB.swift */; };
5C4E8BE92B71031A00F148B6 /* Kit.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BE82B7102A700F148B6 /* Kit.h */; settings = {ATTRIBUTES = (Private, ); }; };
5C621D822B4770D6004ED7AF /* process.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C621D812B4770D6004ED7AF /* process.swift */; };
5C645BFF2C591F6600D8342A /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C645BFE2C591F6600D8342A /* widget.swift */; };
5C645C002C591FFA00D8342A /* Net.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A3E17CC247A94AF00449CD1 /* Net.framework */; };
5C645C012C591FFA00D8342A /* Net.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A3E17CC247A94AF00449CD1 /* Net.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5C6F55A72D45694400AB58ED /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6F55A62D45694400AB58ED /* notifications.swift */; };
5C7C1DF42C29A3A00060387D /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C1DF32C29A3A00060387D /* notifications.swift */; };
5C8E001029269C7F0027C75A /* protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE493829265055000F2856 /* protocol.swift */; };
5CA518382B543FE600EBCCC4 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA518372B543FE600EBCCC4 /* portal.swift */; };
5CAA50722C8E417700B13E13 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CAA50712C8E417700B13E13 /* Text.swift */; };
5CB3878A2C35A7110030459D /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB387892C35A7110030459D /* widget.swift */; };
5CC3B4E52F5A033000775E2C /* reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC3B4E42F5A032E00775E2C /* reader.swift */; };
5CCA5CD52D4E8DB3002917F0 /* libIOReport.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1E45552D11D66200525864 /* libIOReport.tbd */; };
5CD342F42B2F2FB700225631 /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD342F32B2F2FB700225631 /* notifications.swift */; };
5CE7E78C2C318512006BC92C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE7E78B2C318512006BC92C /* WidgetKit.framework */; };
5CE7E78E2C318512006BC92C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE7E78D2C318512006BC92C /* SwiftUI.framework */; };
5CE7E7972C318513006BC92C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5CE7E7962C318513006BC92C /* Assets.xcassets */; };
5CE7E79C2C318513006BC92C /* WidgetsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 5CE7E78A2C318512006BC92C /* WidgetsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
5CE7E7A42C318C33006BC92C /* widgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE7E7A32C318C33006BC92C /* widgets.swift */; };
5CF2210D2B1E7EAF006C583F /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2210C2B1E7EAF006C583F /* notifications.swift */; };
5CF221132B1E8078006C583F /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF221122B1E8078006C583F /* notifications.swift */; };
5CF221152B1F4792006C583F /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF221142B1F4792006C583F /* notifications.swift */; };
5CF221172B1F4ACB006C583F /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF221162B1F4ACB006C583F /* notifications.swift */; };
5CF221192B1F8B90006C583F /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF221182B1F8B90006C583F /* notifications.swift */; };
5CF2211B2B1F8CEF006C583F /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2211A2B1F8CEF006C583F /* notifications.swift */; };
5CFE492A29264DF1000F2856 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE492929264DF1000F2856 /* main.swift */; };
5CFE493929265055000F2856 /* protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE493829265055000F2856 /* protocol.swift */; };
5CFE493D2926513E000F2856 /* eu.exelban.Stats.SMC.Helper in Copy Files */ = {isa = PBXBuildFile; fileRef = 5CFE492729264DF1000F2856 /* eu.exelban.Stats.SMC.Helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
5CFE494229265418000F2856 /* uninstall.sh in Resources */ = {isa = PBXBuildFile; fileRef = 5CFE494129265418000F2856 /* uninstall.sh */; };
5CFE494429265421000F2856 /* changelog.py in Resources */ = {isa = PBXBuildFile; fileRef = 5CFE494329265421000F2856 /* changelog.py */; };
5EC1B2E52E2FEAFB007042A6 /* UnitedWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EC1B2E42E2FEAFB007042A6 /* UnitedWidget.swift */; };
5EE8037F29C36BDD0063D37D /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EE8037E29C36BDD0063D37D /* portal.swift */; };
9A045EB72594F8D100ED58F2 /* Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A045EB62594F8D100ED58F2 /* Dashboard.swift */; };
9A11AAD6266FD77F000C1C05 /* Bluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */; };
9A11AAD7266FD77F000C1C05 /* Bluetooth.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9A11AAF4266FD7A7000C1C05 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A11AAF3266FD7A7000C1C05 /* main.swift */; };
9A11AB26266FD828000C1C05 /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9A11AB25266FD828000C1C05 /* config.plist */; };
9A11AB36266FD9F4000C1C05 /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A11AB35266FD9F4000C1C05 /* readers.swift */; };
9A11AB67266FDB69000C1C05 /* Kit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; };
9A2846FE2666A9CC00EC1F6D /* Kit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; };
9A2846FF2666A9CC00EC1F6D /* Kit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9A28475F2666AA2700EC1F6D /* LineChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2847552666AA2700EC1F6D /* LineChart.swift */; };
9A2847602666AA2700EC1F6D /* NetworkChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2847562666AA2700EC1F6D /* NetworkChart.swift */; };
9A2847612666AA2700EC1F6D /* PieChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2847572666AA2700EC1F6D /* PieChart.swift */; };
9A2847622666AA2700EC1F6D /* Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2847582666AA2700EC1F6D /* Label.swift */; };
9A2847632666AA2700EC1F6D /* Mini.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2847592666AA2700EC1F6D /* Mini.swift */; };
9A2847642666AA2700EC1F6D /* Battery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A28475A2666AA2700EC1F6D /* Battery.swift */; };
9A2847652666AA2700EC1F6D /* Memory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A28475B2666AA2700EC1F6D /* Memory.swift */; };
9A2847662666AA2700EC1F6D /* Speed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A28475C2666AA2700EC1F6D /* Speed.swift */; };
9A2847672666AA2700EC1F6D /* BarChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A28475D2666AA2700EC1F6D /* BarChart.swift */; };
9A2847682666AA2700EC1F6D /* Stack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A28475E2666AA2700EC1F6D /* Stack.swift */; };
9A2847792666AA5000EC1F6D /* module.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2847742666AA5000EC1F6D /* module.swift */; };
9A28477A2666AA5000EC1F6D /* window.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2847752666AA5000EC1F6D /* window.swift */; };
9A28477B2666AA5000EC1F6D /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2847762666AA5000EC1F6D /* popup.swift */; };
9A28477C2666AA5000EC1F6D /* reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2847772666AA5000EC1F6D /* reader.swift */; };
9A28477D2666AA5000EC1F6D /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2847782666AA5000EC1F6D /* widget.swift */; };
9A2847C22666AA8700EC1F6D /* Kit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; };
9A2847C72666AA8C00EC1F6D /* Kit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; };
9A2847CC2666AA9100EC1F6D /* Kit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; };
9A2847D12666AA9500EC1F6D /* Kit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; };
9A2847D62666AA9C00EC1F6D /* Kit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; };
9A2847DB2666AAA000EC1F6D /* Kit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; };
9A2847E02666AAA400EC1F6D /* Kit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; };
9A2848082666AB3000EC1F6D /* updater.sh in Resources */ = {isa = PBXBuildFile; fileRef = 9A2848012666AB2F00EC1F6D /* updater.sh */; };
9A2848092666AB3000EC1F6D /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2848022666AB2F00EC1F6D /* Store.swift */; };
9A28480A2666AB3000EC1F6D /* SystemKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2848032666AB2F00EC1F6D /* SystemKit.swift */; };
9A28480B2666AB3000EC1F6D /* Charts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2848042666AB2F00EC1F6D /* Charts.swift */; };
9A28480E2666AB3000EC1F6D /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2848072666AB3000EC1F6D /* Updater.swift */; };
9A28481E2666AB3600EC1F6D /* extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A28481A2666AB3500EC1F6D /* extensions.swift */; };
9A28481F2666AB3600EC1F6D /* constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A28481B2666AB3500EC1F6D /* constants.swift */; };
9A2848202666AB3600EC1F6D /* types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A28481C2666AB3500EC1F6D /* types.swift */; };
9A2848212666AB3600EC1F6D /* helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A28481D2666AB3600EC1F6D /* helpers.swift */; };
9A2848892666AC0100EC1F6D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9A2848882666AC0100EC1F6D /* Assets.xcassets */; };
9A302614286A2A3B00B41D57 /* Repeater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A302613286A2A3B00B41D57 /* Repeater.swift */; };
9A34353B243E278D006B19F9 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A34353A243E278D006B19F9 /* main.swift */; };
9A34353C243E27E8006B19F9 /* LaunchAtLogin.app in Copy Files */ = {isa = PBXBuildFile; fileRef = 9A343527243E26A0006B19F9 /* LaunchAtLogin.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
9A3E17D3247A94AF00449CD1 /* Net.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A3E17CC247A94AF00449CD1 /* Net.framework */; };
9A3E17D4247A94AF00449CD1 /* Net.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A3E17CC247A94AF00449CD1 /* Net.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9A3E17D9247A94B500449CD1 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A3E17D8247A94B500449CD1 /* main.swift */; };
9A3E17DB247A94BC00449CD1 /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A3E17DA247A94BC00449CD1 /* readers.swift */; };
9A3E17EA247B07BF00449CD1 /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A3E17E9247B07BF00449CD1 /* popup.swift */; };
9A46BF36266D6E17001A1117 /* i18n.py in Resources */ = {isa = PBXBuildFile; fileRef = 9A46BF35266D6E17001A1117 /* i18n.py */; };
9A46BF8A266D7D00001A1117 /* smc in CopyFiles */ = {isa = PBXBuildFile; fileRef = 9ADE6FD8265D032100D2FBA8 /* smc */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
9A46C05F266D85F8001A1117 /* smc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADE7038265D059000D2FBA8 /* smc.swift */; };
9A46C06B266D8602001A1117 /* smc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADE7038265D059000D2FBA8 /* smc.swift */; };
9A53EBF924EAFA5200648841 /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A53EBF824EAFA5200648841 /* settings.swift */; };
9A53EBFB24EB041E00648841 /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A53EBFA24EB041E00648841 /* popup.swift */; };
9A58DE9E24B363D800716A9F /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A58DE9D24B363D800716A9F /* popup.swift */; };
9A58DEA024B363F300716A9F /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A58DE9F24B363F300716A9F /* settings.swift */; };
9A58DEA424B3647600716A9F /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A58DEA324B3647600716A9F /* settings.swift */; };
9A5A8447271895B700BC40A4 /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A5A8446271895B700BC40A4 /* Reachability.swift */; };
9A5AF11B2469CE9B00684737 /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A5AF11A2469CE9B00684737 /* popup.swift */; };
9A6CFC0122A1C9F5001E782D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9A6CFC0022A1C9F5001E782D /* Assets.xcassets */; };
9A6EEBBE2685259500897371 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A6EEBBD2685259500897371 /* Logger.swift */; };
9A81C74D24499C7000825D92 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A81C74B24499C7000825D92 /* AppSettings.swift */; };
9A81C74E24499C7000825D92 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A81C74C24499C7000825D92 /* Settings.swift */; };
9A81C75D2449A41400825D92 /* RAM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A81C7562449A41400825D92 /* RAM.framework */; };
9A81C75E2449A41400825D92 /* RAM.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A81C7562449A41400825D92 /* RAM.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9A81C7692449A43600825D92 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A81C7672449A43600825D92 /* main.swift */; };
9A81C76A2449A43600825D92 /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A81C7682449A43600825D92 /* readers.swift */; };
9A83526F2889A03100791BAC /* Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A83526E2889A03100791BAC /* Setup.swift */; };
9A8B923D2696445C00FD6D83 /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8B923C2696445C00FD6D83 /* settings.swift */; };
9A90E19024EAD2BB00471E9A /* GPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A90E18924EAD2BB00471E9A /* GPU.framework */; };
9A90E19124EAD2BB00471E9A /* GPU.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A90E18924EAD2BB00471E9A /* GPU.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9A90E19624EAD35F00471E9A /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A90E19524EAD35F00471E9A /* main.swift */; };
9A90E19824EAD3B000471E9A /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9A90E19724EAD3B000471E9A /* config.plist */; };
9A90E1A324EAD66600471E9A /* reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A90E1A224EAD66600471E9A /* reader.swift */; };
9A94B81F26822DE0001F4F2B /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A94B81E26822DE0001F4F2B /* popup.swift */; };
9A953A1424B9D22D0038EF4B /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A953A1324B9D22D0038EF4B /* settings.swift */; };
9A97CED12537331B00742D8F /* CPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A97CECA2537331B00742D8F /* CPU.framework */; };
9A97CED22537331B00742D8F /* CPU.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A97CECA2537331B00742D8F /* CPU.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9A97CEE92537338600742D8F /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A97CEE82537338600742D8F /* main.swift */; };
9A97CEF1253733D200742D8F /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A97CEF0253733D200742D8F /* readers.swift */; };
9A97CEF6253733E400742D8F /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A97CEF5253733E400742D8F /* popup.swift */; };
9A97CEFB253733F300742D8F /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A97CEFA253733F300742D8F /* settings.swift */; };
9A97CF002537340400742D8F /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9A97CEFF2537340400742D8F /* config.plist */; };
9A9B25BB24F7DE2B00C3CCE6 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9A9B25BD24F7DE2B00C3CCE6 /* Localizable.strings */; };
9A9B8C9D27149A3700218374 /* Tachometer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9B8C9C27149A3700218374 /* Tachometer.swift */; };
9A9EA9452476D34500E3B883 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A9EA9442476D34500E3B883 /* Update.swift */; };
9AA64260244B274200416A33 /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AA6425F244B274200416A33 /* popup.swift */; };
9AABEB7A243FD26200668CB0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AABEB79243FD26200668CB0 /* AppDelegate.swift */; };
9AAC5E41280ACC210043D892 /* RAM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AAC5E40280ACC210043D892 /* RAM.swift */; };
9AB14B77248CEF3500DC6731 /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9AF9EE192464A7B3005D2270 /* config.plist */; };
9AB14B78248CEF3B00DC6731 /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9AF9EE12246492E8005D2270 /* config.plist */; };
9AB14B79248CEF4100DC6731 /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9A3E17DC247A94C300449CD1 /* config.plist */; };
9AB14B7A248CEF4900DC6731 /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9ABFF904248BEC0B00C9041A /* config.plist */; };
9AB3AFA8266D2E2D00DD079C /* updater.sh in Copy Files */ = {isa = PBXBuildFile; fileRef = 9A2848012666AB2F00EC1F6D /* updater.sh */; };
9AB6D03926447CAA003215A5 /* reader.m in Sources */ = {isa = PBXBuildFile; fileRef = 9AB6D03826447CAA003215A5 /* reader.m */; };
9AB7FD7C246B48DB00387FDA /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AB7FD7B246B48DB00387FDA /* settings.swift */; };
9ABFF8FD248BEBCB00C9041A /* Battery.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9ABFF8F6248BEBCB00C9041A /* Battery.framework */; };
9ABFF8FE248BEBCB00C9041A /* Battery.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9ABFF8F6248BEBCB00C9041A /* Battery.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9ABFF903248BEBD700C9041A /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ABFF902248BEBD700C9041A /* main.swift */; };
9ABFF910248BEE7200C9041A /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ABFF90F248BEE7200C9041A /* readers.swift */; };
9ABFF914248C30A800C9041A /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ABFF913248C30A800C9041A /* popup.swift */; };
9AD33AC624BCD3EE007E8820 /* helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD33AC524BCD3EE007E8820 /* helpers.swift */; };
9AD64FA224BF86C100419D59 /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD64FA124BF86C100419D59 /* settings.swift */; };
9AD7F866266F759200E5F863 /* smc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADE7038265D059000D2FBA8 /* smc.swift */; };
9ADE6FDB265D032100D2FBA8 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADE6FDA265D032100D2FBA8 /* main.swift */; };
9ADE702B265D03D100D2FBA8 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9ADE7005265D039300D2FBA8 /* IOKit.framework */; };
9ADE7039265D059000D2FBA8 /* smc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADE7038265D059000D2FBA8 /* smc.swift */; };
9AE29ADC249A50350071B02D /* Sensors.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AE29AD5249A50350071B02D /* Sensors.framework */; };
9AE29ADD249A50350071B02D /* Sensors.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9AE29AD5249A50350071B02D /* Sensors.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9AE29AF3249A51D70071B02D /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE29AF1249A50CD0071B02D /* main.swift */; };
9AE29AF6249A52B00071B02D /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9AE29AF4249A52870071B02D /* config.plist */; };
9AE29AFB249A53DC0071B02D /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE29AF9249A53780071B02D /* readers.swift */; };
9AE29AFC249A53DC0071B02D /* values.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE29AF7249A53420071B02D /* values.swift */; };
9AEBBE4D28D773430082A6A1 /* State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AEBBE4C28D773430082A6A1 /* State.swift */; };
9AF9EE0924648751005D2270 /* Disk.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AF9EE0224648751005D2270 /* Disk.framework */; };
9AF9EE0A24648751005D2270 /* Disk.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9AF9EE0224648751005D2270 /* Disk.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9AF9EE0F2464875F005D2270 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AF9EE0E2464875F005D2270 /* main.swift */; };
9AF9EE1124648ADC005D2270 /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AF9EE1024648ADC005D2270 /* readers.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
5C0A9CA72C46838300EE6A89 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A97CEC92537331B00742D8F;
remoteInfo = CPU;
};
5C0A9CAC2C46838A00EE6A89 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A90E18824EAD2BB00471E9A;
remoteInfo = GPU;
};
5C0A9CB12C46838F00EE6A89 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A81C7552449A41400825D92;
remoteInfo = RAM;
};
5C0A9CB62C46839500EE6A89 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9AF9EE0124648751005D2270;
remoteInfo = Disk;
};
5C2229A129CCB3C400F00E69 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 5C22299C29CCB3C400F00E69;
remoteInfo = Clock;
};
5C2229B229CDFBF600F00E69 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A2846F62666A9CC00EC1F6D;
remoteInfo = Kit;
};
5C645C022C591FFA00D8342A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A3E17CB247A94AF00449CD1;
remoteInfo = Net;
};
5CE7E79A2C318513006BC92C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 5CE7E7892C318512006BC92C;
remoteInfo = WidgetsExtension;
};
9A11AAD4266FD77F000C1C05 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A11AACE266FD77F000C1C05;
remoteInfo = Bluetooth;
};
9A11AB69266FDB69000C1C05 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A2846F62666A9CC00EC1F6D;
remoteInfo = Kit;
};
9A2846FC2666A9CC00EC1F6D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A2846F62666A9CC00EC1F6D;
remoteInfo = Kit;
};
9A2847C42666AA8700EC1F6D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A2846F62666A9CC00EC1F6D;
remoteInfo = Kit;
};
9A2847C92666AA8C00EC1F6D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A2846F62666A9CC00EC1F6D;
remoteInfo = Kit;
};
9A2847CE2666AA9100EC1F6D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A2846F62666A9CC00EC1F6D;
remoteInfo = Kit;
};
9A2847D32666AA9500EC1F6D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A2846F62666A9CC00EC1F6D;
remoteInfo = Kit;
};
9A2847D82666AA9C00EC1F6D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A2846F62666A9CC00EC1F6D;
remoteInfo = Kit;
};
9A2847DD2666AAA000EC1F6D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A2846F62666A9CC00EC1F6D;
remoteInfo = Kit;
};
9A2847E22666AAA400EC1F6D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A2846F62666A9CC00EC1F6D;
remoteInfo = Kit;
};
9A3E17D1247A94AF00449CD1 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A3E17CB247A94AF00449CD1;
remoteInfo = Net;
};
9A81C75B2449A41400825D92 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A81C7552449A41400825D92;
remoteInfo = Memory;
};
9A90E18E24EAD2BB00471E9A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A90E18824EAD2BB00471E9A;
remoteInfo = GPU;
};
9A97CECF2537331B00742D8F /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A97CEC92537331B00742D8F;
remoteInfo = CPU;
};
9AAC5E2F280ACC120043D892 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9A1410F4229E721100D29793;
remoteInfo = Stats;
};
9ABFF8FB248BEBCB00C9041A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9ABFF8F5248BEBCB00C9041A;
remoteInfo = Battery;
};
9AE29ADA249A50350071B02D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9AE29AD4249A50350071B02D;
remoteInfo = Sensors;
};
9AF9EE0724648751005D2270 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9AF9EE0124648751005D2270;
remoteInfo = Disk;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
5C645C042C591FFA00D8342A /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
5C645C012C591FFA00D8342A /* Net.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
5CE7E79D2C318513006BC92C /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
5CE7E79C2C318513006BC92C /* WidgetsExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
5CFE492529264DF1000F2856 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = /usr/share/man/man1/;
dstSubfolderSpec = 0;
files = (
);
runOnlyForDeploymentPostprocessing = 1;
};
5CFE493C29265130000F2856 /* Copy Files */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = Contents/Library/LaunchServices;
dstSubfolderSpec = 1;
files = (
5CFE493D2926513E000F2856 /* eu.exelban.Stats.SMC.Helper in Copy Files */,
);
name = "Copy Files";
runOnlyForDeploymentPostprocessing = 0;
};
9A46BF89266D7CFA001A1117 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 7;
files = (
9A46BF8A266D7D00001A1117 /* smc in CopyFiles */,
);
runOnlyForDeploymentPostprocessing = 0;
};
9A6698E72326AB16001D00E1 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
9AF9EE0A24648751005D2270 /* Disk.framework in Embed Frameworks */,
5C2229A429CCB3C400F00E69 /* Clock.framework in Embed Frameworks */,
9A81C75E2449A41400825D92 /* RAM.framework in Embed Frameworks */,
9AE29ADD249A50350071B02D /* Sensors.framework in Embed Frameworks */,
9A11AAD7266FD77F000C1C05 /* Bluetooth.framework in Embed Frameworks */,
9A2846FF2666A9CC00EC1F6D /* Kit.framework in Embed Frameworks */,
9ABFF8FE248BEBCB00C9041A /* Battery.framework in Embed Frameworks */,
9A3E17D4247A94AF00449CD1 /* Net.framework in Embed Frameworks */,
9A90E19124EAD2BB00471E9A /* GPU.framework in Embed Frameworks */,
9A97CED22537331B00742D8F /* CPU.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
9AB54DAE22A19F96006192E0 /* Copy Files */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = Contents/Library/LoginItems;
dstSubfolderSpec = 1;
files = (
9A34353C243E27E8006B19F9 /* LaunchAtLogin.app in Copy Files */,
);
name = "Copy Files";
runOnlyForDeploymentPostprocessing = 0;
};
9AECEF3D24ACF98800DB95D4 /* Copy Files */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = Scripts;
dstSubfolderSpec = 7;
files = (
9AB3AFA8266D2E2D00DD079C /* updater.sh in Copy Files */,
);
name = "Copy Files";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
0192A7ED2B017E190056F918 /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = ""; };
13BE56042D0E6963001C58EC /* en-AU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-AU"; path = "en-AU.lproj/Localizable.strings"; sourceTree = ""; };
2658929724FC0F3B00FB3B6A /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; };
29AA17CA2926879000709C01 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = ""; };
30370F8F253DCBC0006404D8 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; };
40BE2B202745D63800AE9396 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; };
47665544298DC92F00F7B709 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; };
4921436D25319699000A1C47 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; };
4F92E6432D0F293100EA593F /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; };
5C038CF52D86EE8700516809 /* Remote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Remote.swift; sourceTree = ""; };
5C044F792B3DE6F3005F6951 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; };
5C0A2A89292A5B4D009B4C1F /* SMJobBlessUtil.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = SMJobBlessUtil.py; sourceTree = ""; };
5C0A9CA12C467AA300EE6A89 /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = ""; };
5C0A9CA32C467F7A00EE6A89 /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = ""; };
5C0E550A2B5D545A00FFF1FB /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; };
5C1E45552D11D66200525864 /* libIOReport.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libIOReport.tbd; path = usr/lib/libIOReport.tbd; sourceTree = SDKROOT; };
5C1E45572D11D67600525864 /* bridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bridge.h; sourceTree = ""; };
5C2016422D36D0EF0049C788 /* Support.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Support.swift; sourceTree = ""; };
5C21D80A296C7B81005BA16D /* CombinedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombinedView.swift; sourceTree = ""; };
5C22299D29CCB3C400F00E69 /* Clock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Clock.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5C2229A829CCB41900F00E69 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
5C2229AA29CCB53E00F00E69 /* config.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = config.plist; sourceTree = ""; };
5C2229AE29CDC08700F00E69 /* settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = settings.swift; sourceTree = ""; };
5C2229B729CE3F3300F00E69 /* popup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = popup.swift; sourceTree = ""; };
5C2229BB29CF66B100F00E69 /* header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = header.h; sourceTree = ""; };
5C23BC0129A0102500DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; };
5C23BC0329A014AC00DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; };
5C23BC0729A03D1200DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; };
5C23BC0929A0EDA300DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; };
5C23BC0B29A10BE000DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; };
5C23BC0F29A3B5AE00DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; };
5C3068722C32E83200B05EFA /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = ""; };
5C32910A2C315A250010012D /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/Localizable.strings"; sourceTree = ""; };
5C4E8B9F2B6EEE6D00F148B6 /* lldb.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = lldb.h; sourceTree = ""; };
5C4E8BA02B6EEE8E00F148B6 /* lldb.m */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; path = lldb.m; sourceTree = ""; };
5C4E8BA22B6EEEE800F148B6 /* filter_policy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = filter_policy.h; sourceTree = ""; };
5C4E8BA32B6EEEE800F148B6 /* export.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = export.h; sourceTree = ""; };
5C4E8BA42B6EEEE800F148B6 /* slice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = slice.h; sourceTree = ""; };
5C4E8BA52B6EEEE800F148B6 /* write_batch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = write_batch.h; sourceTree = ""; };
5C4E8BA62B6EEEE800F148B6 /* status.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = status.h; sourceTree = ""; };
5C4E8BA72B6EEEE800F148B6 /* iterator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = iterator.h; sourceTree = ""; };
5C4E8BA82B6EEEE800F148B6 /* table_builder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = table_builder.h; sourceTree = ""; };
5C4E8BA92B6EEEE800F148B6 /* env.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = env.h; sourceTree = ""; };
5C4E8BAA2B6EEEE800F148B6 /* comparator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = comparator.h; sourceTree = ""; };
5C4E8BAB2B6EEEE800F148B6 /* c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = c.h; sourceTree = ""; };
5C4E8BAC2B6EEEE800F148B6 /* options.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = options.h; sourceTree = ""; };
5C4E8BAD2B6EEEE800F148B6 /* table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = table.h; sourceTree = ""; };
5C4E8BAE2B6EEEE800F148B6 /* cache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = cache.h; sourceTree = ""; };
5C4E8BAF2B6EEEE800F148B6 /* dumpfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = dumpfile.h; sourceTree = ""; };
5C4E8BB02B6EEEE800F148B6 /* db.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = db.h; sourceTree = ""; };
5C4E8BC32B6EF65E00F148B6 /* libleveldb.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libleveldb.a; sourceTree = ""; };
5C4E8BC62B6EF98800F148B6 /* DB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DB.swift; sourceTree = ""; };
5C4E8BE82B7102A700F148B6 /* Kit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Kit.h; sourceTree = ""; };
5C621D812B4770D6004ED7AF /* process.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = process.swift; sourceTree = ""; };
5C645BFE2C591F6600D8342A /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = ""; };
5C6F55A62D45694400AB58ED /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; };
5C7C1DF32C29A3A00060387D /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; };
5C9F90A02A76B30500D41748 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = ""; };
5CA518372B543FE600EBCCC4 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; };
5CAA50712C8E417700B13E13 /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = ""; };
5CB387892C35A7110030459D /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = ""; };
5CC3B4E42F5A032E00775E2C /* reader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = reader.swift; sourceTree = ""; };
5CD342F32B2F2FB700225631 /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; };
5CE7E78A2C318512006BC92C /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
5CE7E78B2C318512006BC92C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
5CE7E78D2C318512006BC92C /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
5CE7E7962C318513006BC92C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
5CE7E7982C318513006BC92C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
5CE7E7992C318513006BC92C /* Widgets.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Widgets.entitlements; sourceTree = ""; };
5CE7E7A32C318C33006BC92C /* widgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widgets.swift; sourceTree = ""; };
5CF2210C2B1E7EAF006C583F /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; };
5CF221122B1E8078006C583F /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; };
5CF221142B1F4792006C583F /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; };
5CF221162B1F4ACB006C583F /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; };
5CF221182B1F8B90006C583F /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = "