Initial
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
/*.app
|
||||
/*.tar.gz
|
||||
*.xcodeproj
|
||||
7
LICENSE
Normal file
7
LICENSE
Normal file
@@ -0,0 +1,7 @@
|
||||
Copyright 2026 relikd
|
||||
|
||||
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.
|
||||
55
Makefile
Normal file
55
Makefile
Normal file
@@ -0,0 +1,55 @@
|
||||
# usage: make [CONFIG=debug|release]
|
||||
|
||||
ifeq ($(CONFIG), debug)
|
||||
CFLAGS=-Onone -g
|
||||
else
|
||||
CFLAGS=-O
|
||||
endif
|
||||
|
||||
PLIST=$(shell grep -A1 $(1) src/Info.plist | tail -1 | cut -d'>' -f2 | cut -d'<' -f1)
|
||||
HAS_SIGN_IDENTITY=$(shell security find-identity -v -p codesigning | grep -q "Apple Development" && echo 1 || echo 0)
|
||||
|
||||
|
||||
Menuscript.app: SDK_PATH=$(shell xcrun --show-sdk-path --sdk macosx)
|
||||
Menuscript.app: src/* examples/*
|
||||
@mkdir -p Menuscript.app/Contents/MacOS/
|
||||
swiftc ${CFLAGS} src/main.swift -target x86_64-apple-macos10.13 \
|
||||
-emit-executable -sdk ${SDK_PATH} -o bin_x64
|
||||
swiftc ${CFLAGS} src/main.swift -target arm64-apple-macos10.13 \
|
||||
-emit-executable -sdk ${SDK_PATH} -o bin_arm64
|
||||
lipo -create bin_x64 bin_arm64 -o Menuscript.app/Contents/MacOS/Menuscript
|
||||
@rm bin_x64 bin_arm64
|
||||
@echo 'APPL????' > Menuscript.app/Contents/PkgInfo
|
||||
@mkdir -p Menuscript.app/Contents/Resources/
|
||||
@cp src/AppIcon.icns Menuscript.app/Contents/Resources/AppIcon.icns
|
||||
@rm -rf Menuscript.app/Contents/Resources/examples/
|
||||
@cp -R examples/ Menuscript.app/Contents/Resources/examples/
|
||||
@cp src/Info.plist Menuscript.app/Contents/Info.plist
|
||||
@touch Menuscript.app
|
||||
@echo
|
||||
ifeq ($(HAS_SIGN_IDENTITY),1)
|
||||
codesign -v -s 'Apple Development' --options=runtime --timestamp Menuscript.app
|
||||
else
|
||||
codesign -v -s - Menuscript.app
|
||||
endif
|
||||
@echo
|
||||
@echo 'Verify Signature...'
|
||||
@echo
|
||||
codesign -dvv Menuscript.app
|
||||
@echo
|
||||
codesign -vvv --deep --strict Menuscript.app
|
||||
ifeq ($(HAS_SIGN_IDENTITY),1)
|
||||
@echo
|
||||
-spctl -vvv --assess --type exec Menuscript.app
|
||||
endif
|
||||
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf Menuscript.app bin_x64 bin_arm64
|
||||
|
||||
|
||||
.PHONY: release
|
||||
release: VERSION=$(call PLIST,CFBundleShortVersionString)
|
||||
release: Menuscript.app
|
||||
tar -czf "Menuscript_v${VERSION}.tar.gz" Menuscript.app
|
||||
67
README.md
Normal file
67
README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
[](#install)
|
||||
[](https://github.com/relikd/Menuscript/releases)
|
||||
[](https://github.com/relikd/Menuscript/releases)
|
||||
|
||||
<img src="img/icon.svg" width="180" height="180">
|
||||
|
||||
# Menuscript
|
||||
|
||||
A menu bar script executor.
|
||||
|
||||
<img src="img/screenshot.png" width="390" height="205">
|
||||
|
||||
Menuscript adds a status bar menu to call custom (user defined) scripts.
|
||||
The app reads the content of a directory and adds all executable files to the status menu.
|
||||
The screenshot above represents the content of the [examples](examples/) directory.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
1) Define your own script directory in Preferences.
|
||||
2) Add subdirectories and scripts to your scripts dir.
|
||||
3) Run a script from your status menu.
|
||||
4) Depending on your script action, you may want to allow `Full Disk Access` for Menuscript.
|
||||
|
||||
*Note:* Menuscript reloads the directory structure each time you open the status menu, no need to restart the app.
|
||||
|
||||
|
||||
### Requirements
|
||||
|
||||
Script files are called as is.
|
||||
Therefore each script __must__ have the executable flag (`chmod 755` or `chmod +x`) and __must__ execute itself on double-click.
|
||||
The latter can be achieved by adding a shebang (e.g., `#!/bin/sh`) to the script file.
|
||||
You may want to omit the file extension (in case of Python, that would launch the editor instead of executing the script).
|
||||
|
||||
Apart from that, there is no limitation on the script language.
|
||||
You can use Bash, Python, Swift, Ruby, whatever.
|
||||
And of course, you can always write a script wrapper to call something else.
|
||||
|
||||
=> If you can call the script with `open` (e.g., `open myscript`), it will work in the status menu too.
|
||||
|
||||
|
||||
### Configuration
|
||||
|
||||
There are a few ways to modify the menu structure:
|
||||
|
||||
#### Sort Order
|
||||
|
||||
By default, menu items are sorted in alphabetic order (case-insensitive).
|
||||
You can change the item order by prepending numbers to the filename.
|
||||
It does not matter whether you prepend your files with `42` or `42.` or `42 - `, as long as the first character is a number.
|
||||
|
||||
You dont need to rename all items either.
|
||||
Each item has a default numerical order of `100`.
|
||||
By prepending a number lower or higher than 100, you can place items at the top or bottom of the menu respectively.
|
||||
|
||||
#### Modifier Flags
|
||||
|
||||
Modifier flags change what happens if you click on the menu item.
|
||||
Flags are defined by adding a text snippet to the filename.
|
||||
These constant strings are defined:
|
||||
|
||||
- __[txt]__: Execute the script and dump all output in a new `TextEdit` window (useful for reports or log files, etc.)
|
||||
- __[verbose]__: Usually, script files are executed in the background. With this flag, a new `Terminal` window will open and show the activley running script (useful for continuous output like `top` or `netstat -w`, etc.)
|
||||
|
||||
#### Menu Icon
|
||||
|
||||
A subdirectory can have a custom icon if the folder contains an image file named `icon.X` (where `X` is one of: `svg`, `png`, `jpg`, `jpeg`, `gif`, `ico`).
|
||||
2
examples/Custom Sort Order/1. One
Executable file
2
examples/Custom Sort Order/1. One
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
say 1
|
||||
2
examples/Custom Sort Order/2. Two
Executable file
2
examples/Custom Sort Order/2. Two
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
say 2
|
||||
2
examples/Custom Sort Order/3. Three
Executable file
2
examples/Custom Sort Order/3. Three
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
say 3
|
||||
2
examples/Custom Sort Order/4. Four
Executable file
2
examples/Custom Sort Order/4. Four
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
say 4
|
||||
2
examples/Custom Sort Order/9 Nine
Executable file
2
examples/Custom Sort Order/9 Nine
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
say 9
|
||||
2
examples/Custom Sort Order/999 Zero
Executable file
2
examples/Custom Sort Order/999 Zero
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
say 0
|
||||
2
examples/Custom Sort Order/Five
Executable file
2
examples/Custom Sort Order/Five
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
say 5
|
||||
4
examples/Flags/999 Python example [verbose]
Executable file
4
examples/Flags/999 Python example [verbose]
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
print()
|
||||
print("=> 2^8 is", 2**8)
|
||||
print()
|
||||
5
examples/Flags/icon.svg
Normal file
5
examples/Flags/icon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 8 8">
|
||||
<circle cx="4" cy="4" r="3" fill="turquoise"/>
|
||||
<rect x="3.5" width="1" height="8" fill="cyan"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 214 B |
2
examples/Flags/ifconfig (text editor) [txt]
Executable file
2
examples/Flags/ifconfig (text editor) [txt]
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
ifconfig
|
||||
2
examples/Flags/top (updated) [verbose]
Executable file
2
examples/Flags/top (updated) [verbose]
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
top
|
||||
2
examples/Open Desktop Folder
Executable file
2
examples/Open Desktop Folder
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
open ~/Desktop
|
||||
8
examples/Toggle Desktop Icons
Executable file
8
examples/Toggle Desktop Icons
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
prev=$(defaults read com.apple.finder CreateDesktop)
|
||||
if [ "$prev" = 0 ]; then
|
||||
flag=1
|
||||
else
|
||||
flag=0
|
||||
fi
|
||||
defaults write com.apple.finder CreateDesktop $flag && killall Finder
|
||||
28
img/icon-sm.svg
Normal file
28
img/icon-sm.svg
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1000 1000">
|
||||
<defs>
|
||||
<linearGradient id="lg1" x2="0%" y2="100%">
|
||||
<stop offset="0" stop-color="#f9f9f9"/>
|
||||
<stop offset="1" stop-color="#c3c3c3"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="lg2" x2="0%" y2="100%">
|
||||
<stop offset="0" stop-color="#7C93E8"/>
|
||||
<stop offset="1" stop-color="#395DE0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="mask">
|
||||
<path d="M0,18q0,-18,18,-18h64q18,0,18,18v64q0,18,-18,18h-64q-18,0,-18,-18Z" transform="scale(10)"/>
|
||||
</clipPath>
|
||||
<style>text{font:500 280px "SF Pro Text", sans-serif}</style>
|
||||
</defs>
|
||||
<g clip-path="url(#mask)">
|
||||
<rect id="bg" width="1000" height="1000" fill="#fdfdfd"/>
|
||||
<rect id="menu" width="1000" height="470" fill="url(#lg1)"/>
|
||||
<rect id="menu_line" y="470" width="1000" height="30" fill="#0e0e0e"/>
|
||||
<rect id="blue" x="500" width="600" height="470" fill="url(#lg2)"/>
|
||||
<rect id="blue_line" x="500" y="470" width="600" height="30" fill="#2847C5"/>
|
||||
<g transform="translate(562.5,62)scale(0.375)">
|
||||
<path id="_x2318_" d="M335,335m-145,0a145,145,0,1,1,145,-145v620a145,145,0,1,1,-145,-145h620a145,145,0,1,1,-145,145v-620a145,145,0,1,1,145,145Z" stroke="#fff" stroke-width="90" fill="transparent"/>
|
||||
</g>
|
||||
<text x="75" y="840">run.sh</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
30
img/icon.svg
Normal file
30
img/icon.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1024 1024">
|
||||
<defs>
|
||||
<linearGradient id="lg1" x2="0%" y2="100%">
|
||||
<stop offset="0" stop-color="#f9f9f9"/>
|
||||
<stop offset="1" stop-color="#c3c3c3"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="lg2" x2="0%" y2="100%">
|
||||
<stop offset="0" stop-color="#7C93E8"/>
|
||||
<stop offset="1" stop-color="#395DE0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="mask">
|
||||
<path d="M0,18q0,-18,18,-18h64q18,0,18,18v64q0,18,-18,18h-64q-18,0,-18,-18Z" transform="scale(10.24)"/>
|
||||
</clipPath>
|
||||
<style>text{font:500 260px "SF Pro Text", sans-serif}</style>
|
||||
</defs>
|
||||
<g clip-path="url(#mask)" transform="translate(104,104) scale(0.797)">
|
||||
<rect id="bg" width="1024" height="1024" fill="#fdfdfd"/>
|
||||
<rect id="menu" width="1024" height="331" fill="url(#lg1)"/>
|
||||
<rect id="menu_line" y="331" width="1024" height="20" fill="#0e0e0e"/>
|
||||
<rect id="blue" x="512" width="402" height="331" fill="url(#lg2)"/>
|
||||
<rect id="blue_line" x="512" y="331" width="402" height="20" fill="#2847C5"/>
|
||||
<g transform="translate(587,40) scale(0.251)">
|
||||
<path id="_x2318_" d="M335,335m-145,0a145,145,0,1,1,145,-145v620a145,145,0,1,1,-145,-145h620a145,145,0,1,1,-145,145v-620a145,145,0,1,1,145,145Z" stroke="#fff" stroke-width="90" fill="transparent"/>
|
||||
</g>
|
||||
<text x="72" y="590">run.sh</text>
|
||||
<rect id="sep" y="672" width="1024" height="20" fill="#ccc"/>
|
||||
<text x="72" y="931">Quit</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
img/screenshot.png
Normal file
BIN
img/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
src/AppIcon.icns
Normal file
BIN
src/AppIcon.icns
Normal file
Binary file not shown.
36
src/Info.plist
Normal file
36
src/Info.plist
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>de.relikd.Menuscript</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Menuscript</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Menuscript</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13</string>
|
||||
<key>LSBackgroundOnly</key>
|
||||
<true/>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSSupportsSuddenTermination</key>
|
||||
<true/>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2026 relikd.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
276
src/main.swift
Executable file
276
src/main.swift
Executable file
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env swift
|
||||
import AppKit
|
||||
import Cocoa
|
||||
import SwiftUI
|
||||
|
||||
class FlippedView: NSView {
|
||||
override var isFlipped: Bool { true }
|
||||
}
|
||||
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
||||
private var statusItem: NSStatusItem!
|
||||
private weak var settingsWindow: NSWindow? = nil
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
// UserDefaults.standard.register(defaults: ["storage": "~/"])
|
||||
initStatusIcon()
|
||||
if userStorageURL() == nil {
|
||||
showSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private func initStatusIcon() {
|
||||
self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
self.statusItem.button?.title = "⌘"
|
||||
// self.statusItem.button?.image = NSImage.statusIcon
|
||||
self.statusItem.menu = NSMenu(title: resolvedStorageURL().path)
|
||||
self.statusItem.menu?.delegate = self
|
||||
}
|
||||
|
||||
func menuDidClose(_ menu: NSMenu) {
|
||||
if menu == self.statusItem.menu {
|
||||
self.statusItem.menu = NSMenu(title: menu.title)
|
||||
self.statusItem.menu?.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Delegate method not used. Here to prevent weird `NSMenu` behavior.
|
||||
Otherwise, Cmd-Q (Quit) will traverse all submenus (incl. creating unopened).
|
||||
*/
|
||||
func menuHasKeyEquivalent(_ menu: NSMenu, for event: NSEvent, target: AutoreleasingUnsafeMutablePointer<AnyObject?>, action: UnsafeMutablePointer<Selector?>) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func menuNeedsUpdate(_ menu: NSMenu) {
|
||||
for entry in Entry.listDir(menu.title) {
|
||||
let itm = menu.addItem(withTitle: entry.title, action: nil, keyEquivalent: "")
|
||||
itm.representedObject = entry
|
||||
itm.image = entry.icon()
|
||||
|
||||
if entry.isDir {
|
||||
itm.submenu = NSMenu(title: entry.url.path)
|
||||
itm.submenu?.delegate = self
|
||||
} else {
|
||||
itm.action = #selector(menuItemCallback)
|
||||
}
|
||||
}
|
||||
|
||||
if menu == self.statusItem.menu {
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
menu.addItem(withTitle: "Preferences", action: #selector(showSettings), keyEquivalent: ",")
|
||||
menu.addItem(withTitle: "Quit", action: #selector(NSApp.terminate), keyEquivalent: "q")
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func menuItemCallback(sender: NSMenuItem) {
|
||||
(sender.representedObject as! Entry).run()
|
||||
}
|
||||
|
||||
// MARK: - Manage storage path
|
||||
|
||||
private func resolvedStorageURL() -> URL {
|
||||
userStorageURL()
|
||||
// if run via .app bundle
|
||||
?? Bundle.main.url(forResource: "examples", withExtension: nil)
|
||||
// if calling swift directly
|
||||
?? URL(string: "file://" + FileManager.default.currentDirectoryPath + "/" + #file)!
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("examples")
|
||||
}
|
||||
|
||||
private func userStorageURL() -> URL? {
|
||||
UserDefaults.standard.url(forKey: "storage")
|
||||
}
|
||||
|
||||
@objc func selectStoragePath() {
|
||||
let panel = NSOpenPanel()
|
||||
panel.canChooseDirectories = true
|
||||
panel.canCreateDirectories = true
|
||||
panel.canChooseFiles = false
|
||||
panel.allowsMultipleSelection = false
|
||||
panel.begin {
|
||||
if $0 == .OK {
|
||||
UserDefaults.standard.set(panel.url, forKey: "storage")
|
||||
self.statusItem.menu?.title = panel.url!.path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func openStoragePath() {
|
||||
NSWorkspace.shared.open(resolvedStorageURL())
|
||||
}
|
||||
|
||||
// MARK: - Settings View
|
||||
|
||||
@objc func showSettings() {
|
||||
if self.settingsWindow == nil {
|
||||
let win = NSWindow(
|
||||
contentRect: NSMakeRect(0, 0, 400, 115),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered,
|
||||
defer: true)
|
||||
self.settingsWindow = win
|
||||
win.title = "Preferences"
|
||||
win.isReleasedWhenClosed = false // because .buffered
|
||||
win.contentView = initSettingsView()
|
||||
win.contentMinSize = NSMakeSize(300, win.contentView!.frame.size.height)
|
||||
win.center()
|
||||
win.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
self.settingsWindow!.orderFrontRegardless()
|
||||
}
|
||||
|
||||
private func initSettingsView() -> NSView {
|
||||
let pad = 20.0
|
||||
let view = FlippedView(frame: NSMakeRect(0, 0, 400, 120))
|
||||
|
||||
let lbl = NSTextField(frame: NSMakeRect(pad, pad, 400 - 2 * pad, 22))
|
||||
lbl.isEditable = false
|
||||
lbl.isBezeled = false
|
||||
lbl.drawsBackground = false
|
||||
lbl.stringValue = "Scripts storage path:"
|
||||
view.addSubview(lbl)
|
||||
|
||||
let pth = NSPathControl(frame: NSMakeRect(pad, NSMaxY(lbl.frame), 400 - 2 * pad, 22))
|
||||
pth.autoresizingMask = [.maxYMargin, .width]
|
||||
pth.isEditable = true
|
||||
pth.allowedTypes = ["public.folder"]
|
||||
pth.pathStyle = .standard
|
||||
pth.url = resolvedStorageURL()
|
||||
view.addSubview(pth)
|
||||
|
||||
let chg = NSButton(title: "Change", target: self, action: #selector(selectStoragePath))
|
||||
chg.frame = NSMakeRect(pad, NSMaxY(pth.frame), 90, 40)
|
||||
chg.autoresizingMask = [.maxXMargin, .maxYMargin]
|
||||
view.addSubview(chg)
|
||||
|
||||
let opn = NSButton(title: "Open", target: self, action: #selector(openStoragePath))
|
||||
opn.frame = NSMakeRect(NSMaxX(chg.frame), NSMaxY(pth.frame), 90, 40)
|
||||
// opn.autoresizingMask = [.maxXMargin, .maxYMargin]
|
||||
view.addSubview(opn)
|
||||
view.frame.size.height = NSMaxY(pth.frame) + 40 + pad
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - A menu item
|
||||
|
||||
struct Entry {
|
||||
enum Flags {
|
||||
case Verbose
|
||||
case Text
|
||||
}
|
||||
|
||||
let order: Int
|
||||
let title: String
|
||||
let flags: [Flags]
|
||||
let url: URL
|
||||
|
||||
var isDir: Bool { url.hasDirectoryPath }
|
||||
|
||||
init(_ url: URL) {
|
||||
self.url = url
|
||||
// TODO: remove file extension?
|
||||
var fname = url.lastPathComponent
|
||||
var idx = fname.firstIndex { !$0.isWholeNumber } ?? fname.startIndex
|
||||
// sort order
|
||||
if let order = Int(fname[..<idx]) {
|
||||
self.order = order
|
||||
idx = fname[idx..<fname.endIndex].firstIndex { !$0.isWhitespace } ?? idx
|
||||
idx = fname[idx..<fname.endIndex].firstIndex { !$0.isPunctuation } ?? idx
|
||||
fname = String(fname[idx..<fname.endIndex])
|
||||
} else {
|
||||
self.order = 100
|
||||
}
|
||||
// flags
|
||||
var flags: [Flags] = []
|
||||
for (key, flag) in ["verbose": Flags.Verbose, "txt": .Text] {
|
||||
if fname.contains("[" + key + "]") {
|
||||
fname = fname.replacingOccurrences(of: "[" + key + "]", with: "")
|
||||
.replacingOccurrences(of: " ", with: " ")
|
||||
flags.append(flag)
|
||||
}
|
||||
}
|
||||
self.flags = flags
|
||||
self.title = fname.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
|
||||
static func listDir(_ path: String) -> [Entry] {
|
||||
var rv: [Entry] = []
|
||||
for url
|
||||
in (try? FileManager.default.contentsOfDirectory(
|
||||
at: URL(string: path)!,
|
||||
includingPropertiesForKeys: [.isDirectoryKey, .isExecutableKey])) ?? []
|
||||
{
|
||||
if url.hasDirectoryPath || FileManager.default.isExecutableFile(atPath: url.path) {
|
||||
rv.append(Entry(url))
|
||||
}
|
||||
}
|
||||
return rv.sorted()
|
||||
}
|
||||
|
||||
func icon() -> NSImage? {
|
||||
guard isDir else {
|
||||
return nil
|
||||
}
|
||||
var img: NSImage? = nil
|
||||
for ext in ["svg", "png", "jpg", "jpeg", "gif", "ico"] {
|
||||
let iconPath = self.url.appendingPathComponent("icon." + ext)
|
||||
if FileManager.default.fileExists(atPath: iconPath.path) {
|
||||
img = NSImage(contentsOf: iconPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
if img == nil {
|
||||
img = NSImage(named: NSImage.folderName)
|
||||
}
|
||||
img?.size = NSMakeSize(16, 16)
|
||||
return img
|
||||
}
|
||||
|
||||
func run() {
|
||||
let proc = Process()
|
||||
proc.currentDirectoryURL = self.url.deletingLastPathComponent()
|
||||
// proc.environment = ["HOME": "$HOME"]
|
||||
if self.flags.contains(.Verbose) {
|
||||
proc.launchPath = "/usr/bin/open"
|
||||
proc.arguments = [self.url.path]
|
||||
} else {
|
||||
proc.executableURL = self.url
|
||||
}
|
||||
|
||||
if self.flags.contains(.Text) {
|
||||
// open result in default text editor
|
||||
let io = Pipe()
|
||||
proc.standardOutput = io
|
||||
proc.standardError = io
|
||||
|
||||
proc.launch()
|
||||
|
||||
let p2 = Process()
|
||||
p2.launchPath = "/usr/bin/open"
|
||||
p2.arguments = ["-f"]
|
||||
p2.standardInput = io
|
||||
p2.launch()
|
||||
|
||||
// let data = io.fileHandleForReading.readDataToEndOfFile()
|
||||
// let output = String(data: data, encoding: .utf8) ?? ""
|
||||
} else {
|
||||
proc.launch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Entry : Comparable {
|
||||
static func < (lhs: Entry, rhs: Entry) -> Bool {
|
||||
return (lhs.order, lhs.title) < (rhs.order, rhs.title)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Main Entry
|
||||
|
||||
let delegate = AppDelegate()
|
||||
NSApplication.shared.delegate = delegate
|
||||
NSApplication.shared.run()
|
||||
// _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
|
||||
Reference in New Issue
Block a user