14 Commits
v1.0.0 ... main

Author SHA1 Message Date
relikd
c41c92e637 chore: bump version 2026-02-07 12:41:14 +01:00
relikd
a7f9b3d48c doc: add example menu item icon 2026-02-07 12:35:29 +01:00
relikd
6867488a4a feat: ignore items which start with underscore 2026-02-07 12:25:08 +01:00
relikd
284eb1aa5c feat: icons for menu items 2026-02-07 12:17:06 +01:00
relikd
d514bfb610 feat: dynamic menus 2026-02-07 12:13:53 +01:00
relikd
8053110349 feat: support for .icns icons 2026-02-07 11:54:26 +01:00
relikd
e2743903e3 feat: new flags inactive and ignore 2026-02-03 19:25:06 +01:00
relikd
dcfe16cb9b ref: Exec, ActionFlag, Entry 2026-02-03 19:19:54 +01:00
relikd
8242a64a5d feat: changelog 2026-01-28 12:58:59 +01:00
relikd
355cf0ded1 feat: svg status icon 2026-01-28 12:48:07 +01:00
relikd
fa5551f272 fix: svg transparent fill color 2026-01-28 12:45:33 +01:00
relikd
b6eaa8d3c4 ref: move files to dedicated res folder 2026-01-28 12:44:43 +01:00
relikd
1af81e5b4a fix: file path on macOS 10.13 2026-01-28 12:43:09 +01:00
relikd
05714ef69e fix: remove unused SwiftUI import 2026-01-27 23:53:34 +01:00
25 changed files with 283 additions and 113 deletions

29
CHANGELOG.md Normal file
View File

@@ -0,0 +1,29 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.0] 2026-02-07
Added:
- Dynamic menus (`[dyn]` flag)
- Modifier flags `[ignore]` and `[inactive]`
- Icons for menu items (additional to folder icons)
- `.icns` icons
- Hide menu items which start with underscore
## [1.0.1] 2026-01-28
Fixed:
- Crash on macOS 10.13 if path contains space character
- Larger status icon glyph
## [1.0.0] 2026-01-27
Initial release
[1.1.0]: https://github.com/relikd/Menuscript/compare/v1.0.1...v1.1.0
[1.0.1]: https://github.com/relikd/Menuscript/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/relikd/Menuscript/compare/1eca425c5f453bc0ed47f780d491656549d3ab53...v1.0.0

View File

@@ -11,7 +11,7 @@ HAS_SIGN_IDENTITY=$(shell security find-identity -v -p codesigning | grep -q "Ap
Menuscript.app: SDK_PATH=$(shell xcrun --show-sdk-path --sdk macosx) Menuscript.app: SDK_PATH=$(shell xcrun --show-sdk-path --sdk macosx)
Menuscript.app: src/* examples/* Menuscript.app: src/* res/**
@mkdir -p Menuscript.app/Contents/MacOS/ @mkdir -p Menuscript.app/Contents/MacOS/
swiftc ${CFLAGS} src/main.swift -target x86_64-apple-macos10.13 \ swiftc ${CFLAGS} src/main.swift -target x86_64-apple-macos10.13 \
-emit-executable -sdk ${SDK_PATH} -o bin_x64 -emit-executable -sdk ${SDK_PATH} -o bin_x64
@@ -20,11 +20,10 @@ Menuscript.app: src/* examples/*
lipo -create bin_x64 bin_arm64 -o Menuscript.app/Contents/MacOS/Menuscript lipo -create bin_x64 bin_arm64 -o Menuscript.app/Contents/MacOS/Menuscript
@rm bin_x64 bin_arm64 @rm bin_x64 bin_arm64
@echo 'APPL????' > Menuscript.app/Contents/PkgInfo @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 @cp src/Info.plist Menuscript.app/Contents/Info.plist
@find res -name .DS_Store -delete
@rm -rf Menuscript.app/Contents/Resources/
@cp -R res/ Menuscript.app/Contents/Resources/
@touch Menuscript.app @touch Menuscript.app
@echo @echo
ifeq ($(HAS_SIGN_IDENTITY),1) ifeq ($(HAS_SIGN_IDENTITY),1)

View File

@@ -12,7 +12,7 @@ A menu bar script executor.
Menuscript adds a status bar menu to call custom (user defined) scripts. 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 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. See [res/examples](res/examples/) directory.
## Usage ## Usage
@@ -36,13 +36,18 @@ Apart from that, there is no limitation on the script language.
You can use Bash, Python, Swift, Ruby, whatever. You can use Bash, Python, Swift, Ruby, whatever.
And of course, you can always write a script wrapper to call something else. 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. => If you can run the script with `open` (e.g., `open myscript`), it will work in the status menu too.
### Configuration ### Configuration
There are a few ways to modify the menu structure: There are a few ways to modify the menu structure:
#### 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`, `icns`).
For menu items, the icon file should be named exactly like the script file plus one of the icon extensions (e.g., `cmd.sh` -> `cmd.sh.png`).
#### Sort Order #### Sort Order
By default, menu items are sorted in alphabetic order (case-insensitive). By default, menu items are sorted in alphabetic order (case-insensitive).
@@ -60,8 +65,33 @@ Flags are defined by adding a text snippet to the filename.
These constant strings are defined: 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.) - __[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.) - __[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.)
- __[inactive]__: Make menu item non-clickable (will appear greyed out)
- __[ignore]__: Do not show menu item (no menu entry)
- __[dyn]__: If set, the script file is responsible to generate its menu items.
See [Dynamic Menus](#dynamic-menus).
#### Menu Icon #### Dynamic Menus
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`). A single script file can generate a whole menu with multiple menu items ("[dyn]" flag).
As with other scripts, the filename will be used as menu title, in this case a menu folder.
The dynamic script is called with environment variables `ACTION` and `ITEM`.
Action can be one of: `list` (generate menu), `click` (item clicked), or `icon` (should return image data).
- `ACTION=list` is called without `ITEM` env and should return a list of menu items to display.
Just print each menu item on a separate line.
Item titles can use the same [modifier flags](#modifier-flags) as normal script files.
But contrary to normal script files, sort order is defined by the dynamic script (you should not use numbers).
- `ACTION=click` always provides an `ITEM` with the currently clicked menu item title (after removing modifier flags).
Your script should perform the intended action of the menu item.
- `ACTION=icon` may return an icon for any menu item (or return nothing to omit the icon).
`ITEM` is empty for the folder menu icon and set to the menu title if called for an individual item (same as `click` action).
You should return image data, e.g., by printing the content of an image file or returning SVG code.
You could use the icon to display an online status for example.
Keep in mind, that the menu is generated only once after the status menu opens.
Limitations: dynamic menus cannot have dynamic submenus.
Also, dynamic items cannot use the verbose flag (for now).

View File

@@ -21,7 +21,7 @@
<rect id="blue" x="500" width="600" height="470" fill="url(#lg2)"/> <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"/> <rect id="blue_line" x="500" y="470" width="600" height="30" fill="#2847C5"/>
<g transform="translate(562.5,62)scale(0.35)"> <g transform="translate(562.5,62)scale(0.35)">
<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"/> <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="none"/>
</g> </g>
<text x="75" y="840">run.sh</text> <text x="75" y="840">run.sh</text>
</g> </g>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -21,7 +21,7 @@
<rect id="blue" x="512" width="402" height="331" fill="url(#lg2)"/> <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"/> <rect id="blue_line" x="512" y="331" width="402" height="20" fill="#2847C5"/>
<g transform="translate(587,40) scale(0.251)"> <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"/> <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="none"/>
</g> </g>
<text x="72" y="590">run.sh</text> <text x="72" y="590">run.sh</text>
<rect id="sep" y="672" width="1024" height="20" fill="#ccc"/> <rect id="sep" y="672" width="1024" height="20" fill="#ccc"/>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

31
res/examples/Dyn [dyn] Executable file
View File

@@ -0,0 +1,31 @@
#!/bin/sh
status() {
echo '<svg viewBox="0 0 8 8"><circle cx="4" cy="4" r="4" fill="' "$1" '"/></svg>'
}
case $ACTION in
list)
echo "Item 1 [verbose]"
echo "Item 2 (Text Editor) [txt]"
echo "Item 3 [ignore]"
echo "Item 4 [txt]"
echo "🔴 Status"
echo "Time: $(date) [inactive]"
;;
click)
case $ITEM in
"Item 1") echo "First item clicked";;
"Item 2 (Text Editor)") echo "Second item clicked";;
*) echo "else: click >$ITEM<";;
esac;;
icon)
case $ITEM in
"") cat _icons/draw.png;; # Main menu icon
"Item 1") cat _icons/draw.png;; # relative to root scripts dir
*Status) status green;;
esac;;
*)
# [verbose] is missing the env variables
echo "unsupported action $ACTION, item: $ITEM"
esac

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 8 8">
<rect y="3.5" width="8" height="1" fill="violet"/>
</svg>

After

Width:  |  Height:  |  Size: 169 B

View File

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

3
res/status.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
<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="black" stroke-width="90" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 263 B

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.0</string> <string>1.1.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>3</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>10.13</string> <string>10.13</string>
<key>LSBackgroundOnly</key> <key>LSBackgroundOnly</key>

View File

@@ -1,7 +1,6 @@
#!/usr/bin/env swift #!/usr/bin/env swift
import AppKit import AppKit
import Cocoa import Cocoa
import SwiftUI
class FlippedView: NSView { class FlippedView: NSView {
override var isFlipped: Bool { true } override var isFlipped: Bool { true }
@@ -21,12 +20,22 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
private func initStatusIcon() { private func initStatusIcon() {
self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
self.statusItem.button?.title = "" if let img = statusIcon() {
// self.statusItem.button?.image = NSImage.statusIcon self.statusItem.button?.image = img
} else {
self.statusItem.button?.title = "" // cant load svg (<10.15)
}
self.statusItem.menu = NSMenu(title: resolvedStorageURL().path) self.statusItem.menu = NSMenu(title: resolvedStorageURL().path)
self.statusItem.menu?.delegate = self self.statusItem.menu?.delegate = self
} }
private func statusIcon() -> NSImage? {
let img = NSImage(contentsOf: resFile("status", "svg"))
img?.isTemplate = true
img?.size = NSMakeSize(14, 14)
return img
}
func menuDidClose(_ menu: NSMenu) { func menuDidClose(_ menu: NSMenu) {
if menu == self.statusItem.menu { if menu == self.statusItem.menu {
self.statusItem.menu = NSMenu(title: menu.title) self.statusItem.menu = NSMenu(title: menu.title)
@@ -43,15 +52,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
} }
func menuNeedsUpdate(_ menu: NSMenu) { func menuNeedsUpdate(_ menu: NSMenu) {
for entry in Entry.listDir(menu.title) { for entry in listDir(menu.title) {
if entry.action == .Ignore {
continue
}
let itm = menu.addItem(withTitle: entry.title, action: nil, keyEquivalent: "") let itm = menu.addItem(withTitle: entry.title, action: nil, keyEquivalent: "")
itm.representedObject = entry itm.representedObject = entry
itm.image = entry.icon() itm.image = entry.icon()
if entry.isDir { if entry.isDir || entry.action == .Dynamic {
itm.submenu = NSMenu(title: entry.url.path) itm.submenu = NSMenu(title: entry.url.path)
itm.submenu?.delegate = self itm.submenu?.delegate = self
} else { } else if entry.action != .Inactive {
itm.action = #selector(menuItemCallback) itm.action = #selector(menuItemCallback)
} }
} }
@@ -64,20 +76,28 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
} }
@objc private func menuItemCallback(sender: NSMenuItem) { @objc private func menuItemCallback(sender: NSMenuItem) {
(sender.representedObject as! Entry).run() let entry = sender.representedObject as! Entry
let cmd = Exec(file: entry.url, env: ["ACTION": "click", "ITEM": entry.title])
if entry.action == .Text {
cmd.editor()
} else {
cmd.run(verbose: entry.action == .Verbose)
}
}
// MARK: - Helper
private func resFile(_ name: String, _ ext: String?) -> URL {
// if run via .app bundle
return Bundle.main.url(forResource: name, withExtension: ext)
// if calling swift directly
?? URL(fileURLWithPath: #file + "/../../res/" + name + (ext == nil ? "" : ("." + ext!)))
} }
// MARK: - Manage storage path // MARK: - Manage storage path
private func resolvedStorageURL() -> URL { private func resolvedStorageURL() -> URL {
userStorageURL() userStorageURL() ?? resFile("examples", nil)
// 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? { private func userStorageURL() -> URL? {
@@ -159,115 +179,169 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
} }
} }
// MARK: - A menu item
struct Entry { // MARK: - Command executor
enum Flags {
case Verbose struct Exec {
case Text let file: URL
var env: [String: String]? = nil
private func prepare() -> Process {
let proc = Process()
proc.environment = self.env
proc.currentDirectoryURL = self.file.deletingLastPathComponent()
proc.executableURL = self.file
return proc
} }
private func toPipe() -> Pipe {
let proc = prepare()
let io = Pipe()
proc.standardOutput = io
proc.standardError = io
proc.launch()
return io
}
/// run command (ignoring output)
func run(verbose: Bool = false) {
let proc = prepare()
if verbose {
proc.executableURL = URL(fileURLWithPath: "/usr/bin/open")
proc.arguments = [self.file.path]
}
proc.launch()
}
/// open result in default text editor
func editor() {
let p2 = Process()
p2.launchPath = "/usr/bin/open"
p2.arguments = ["-f"]
p2.standardInput = toPipe()
p2.launch()
}
/// run command and return output
func readData() -> Data {
return toPipe().fileHandleForReading.readDataToEndOfFile()
}
/// run command and return output
func readString() -> String {
return String(data: readData(), encoding: .utf8) ?? ""
}
}
// MARK: - static methods
func listDir(_ path: String) -> [Entry] {
let target = URL(fileURLWithPath: path)
var rv: [Entry] = []
// dynamic menu
if !target.hasDirectoryPath {
Exec(file: target, env: ["ACTION": "list"]).readString().enumerateLines { line, stop in
rv.append(Entry(target, title: line))
}
return rv
}
// else: static menu
for url in (try? FileManager.default.contentsOfDirectory(at: target, includingPropertiesForKeys: [.isDirectoryKey, .isExecutableKey])) ?? [] {
if url.hasDirectoryPath || FileManager.default.isExecutableFile(atPath: url.path),
!url.lastPathComponent.hasPrefix("_")
{
rv.append(Entry(url))
}
}
return rv.sorted()
}
// MARK: - A menu item
enum ActionFlag {
case Default
case Text
case Verbose
case Inactive
case Ignore
case Dynamic
static func from(_ filename: inout String) -> Self {
for (key, flag) in [
"[txt]": ActionFlag.Text,
"[verbose]": .Verbose,
"[inactive]": .Inactive,
"[ignore]": .Ignore,
"[dyn]": .Dynamic,
] {
if filename.contains(key) {
filename = filename
.replacingOccurrences(of: key, with: "")
.replacingOccurrences(of: " ", with: " ")
return flag
}
}
return Default
}
}
struct Entry {
let order: Int let order: Int
let title: String
let flags: [Flags]
let url: URL let url: URL
let title: String
let action: ActionFlag
let hasDynParent: Bool
var isDir: Bool { url.hasDirectoryPath } var isDir: Bool { url.hasDirectoryPath }
init(_ url: URL) { init(_ url: URL, title: String? = nil) {
self.hasDynParent = title != nil
self.url = url self.url = url
// TODO: remove file extension? // TODO: remove file extension?
var fname = url.lastPathComponent var fname = title ?? url.lastPathComponent
var idx = fname.firstIndex { !$0.isWholeNumber } ?? fname.startIndex var customOrder = 100
// sort order if !hasDynParent {
if let order = Int(fname[..<idx]) { var idx = fname.firstIndex { !$0.isWholeNumber } ?? fname.startIndex
self.order = order // sort order
idx = fname[idx..<fname.endIndex].firstIndex { !$0.isWhitespace } ?? idx if let order = Int(fname[..<idx]) {
idx = fname[idx..<fname.endIndex].firstIndex { !$0.isPunctuation } ?? idx customOrder = order
fname = String(fname[idx..<fname.endIndex]) idx = fname[idx..<fname.endIndex].firstIndex { !$0.isWhitespace } ?? idx
} else { idx = fname[idx..<fname.endIndex].firstIndex { !$0.isPunctuation } ?? idx
self.order = 100 fname = String(fname[idx..<fname.endIndex])
}
// 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.order = customOrder
self.action = ActionFlag.from(&fname)
self.title = fname.trimmingCharacters(in: .whitespaces) 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? { func icon() -> NSImage? {
guard isDir else {
return nil
}
var img: NSImage? = nil var img: NSImage? = nil
for ext in ["svg", "png", "jpg", "jpeg", "gif", "ico"] { if !hasDynParent {
let iconPath = self.url.appendingPathComponent("icon." + ext) let basePath = self.isDir ? self.url.appendingPathComponent("icon") : self.url
if FileManager.default.fileExists(atPath: iconPath.path) { for ext in ["svg", "png", "jpg", "jpeg", "gif", "ico", "icns"] {
img = NSImage(contentsOf: iconPath) let iconPath = basePath.appendingPathExtension(ext)
break if FileManager.default.fileExists(atPath: iconPath.path) {
img = NSImage(contentsOf: iconPath)
break
}
}
if img == nil, self.isDir {
img = NSImage(named: NSImage.folderName)
} }
} }
if img == nil { if img == nil, action == .Dynamic || hasDynParent {
img = NSImage(named: NSImage.folderName) let cmd = Exec(file: self.url, env: ["ACTION": "icon", "ITEM": hasDynParent ? self.title : ""])
img = NSImage(data: cmd.readData())
} }
img?.size = NSMakeSize(16, 16) img?.size = NSMakeSize(16, 16)
return img 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 { extension Entry: Comparable {
static func < (lhs: Entry, rhs: Entry) -> Bool { static func < (lhs: Entry, rhs: Entry) -> Bool {
return (lhs.order, lhs.title) < (rhs.order, rhs.title) return (lhs.order, lhs.title) < (rhs.order, rhs.title)
} }