Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2743903e3 | ||
|
|
dcfe16cb9b | ||
|
|
8242a64a5d | ||
|
|
355cf0ded1 | ||
|
|
fa5551f272 | ||
|
|
b6eaa8d3c4 | ||
|
|
1af81e5b4a | ||
|
|
05714ef69e |
19
CHANGELOG.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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.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.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
|
||||||
9
Makefile
@@ -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)
|
||||||
|
|||||||
13
README.md
@@ -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.
|
The screenshot above represents the content of the [res/examples](res/examples/) directory.
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -43,6 +43,10 @@ And of course, you can always write a script wrapper to call something else.
|
|||||||
|
|
||||||
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`).
|
||||||
|
|
||||||
#### 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 +64,9 @@ 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)
|
||||||
|
|
||||||
#### 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`).
|
|
||||||
|
|||||||
@@ -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 |
@@ -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 |
|
Before Width: | Height: | Size: 214 B After Width: | Height: | Size: 214 B |
3
res/status.svg
Normal 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 |
@@ -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.0.1</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>2</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>10.13</string>
|
<string>10.13</string>
|
||||||
<key>LSBackgroundOnly</key>
|
<key>LSBackgroundOnly</key>
|
||||||
|
|||||||
211
src/main.swift
@@ -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,7 +52,10 @@ 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()
|
||||||
@@ -51,7 +63,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|||||||
if entry.isDir {
|
if entry.isDir {
|
||||||
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, args: nil)
|
||||||
|
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,18 +179,104 @@ 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 args: [String]? = nil
|
||||||
|
|
||||||
|
private func prepare() -> Process {
|
||||||
|
let proc = Process()
|
||||||
|
// proc.environment = ["HOME": "$HOME"]
|
||||||
|
proc.currentDirectoryURL = self.file.deletingLastPathComponent()
|
||||||
|
proc.executableURL = self.file
|
||||||
|
proc.arguments = args
|
||||||
|
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 read() -> String {
|
||||||
|
let data = toPipe().fileHandleForReading.readDataToEndOfFile()
|
||||||
|
return String(data: data, encoding: .utf8) ?? ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - static methods
|
||||||
|
|
||||||
|
func listDir(_ path: String) -> [Entry] {
|
||||||
|
let target = URL(fileURLWithPath: path)
|
||||||
|
var rv: [Entry] = []
|
||||||
|
for url in (try? FileManager.default.contentsOfDirectory(at: target, includingPropertiesForKeys: [.isDirectoryKey, .isExecutableKey])) ?? [] {
|
||||||
|
if url.hasDirectoryPath || FileManager.default.isExecutableFile(atPath: url.path) {
|
||||||
|
rv.append(Entry(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rv.sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - A menu item
|
||||||
|
|
||||||
|
enum ActionFlag {
|
||||||
|
case Default
|
||||||
|
case Text
|
||||||
|
case Verbose
|
||||||
|
case Inactive
|
||||||
|
case Ignore
|
||||||
|
|
||||||
|
static func from(_ filename: inout String) -> Self {
|
||||||
|
for (key, flag) in [
|
||||||
|
"[txt]": ActionFlag.Text,
|
||||||
|
"[verbose]": .Verbose,
|
||||||
|
"[inactive]": .Inactive,
|
||||||
|
"[ignore]": .Ignore,
|
||||||
|
] {
|
||||||
|
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
|
||||||
|
|
||||||
var isDir: Bool { url.hasDirectoryPath }
|
var isDir: Bool { url.hasDirectoryPath }
|
||||||
|
|
||||||
@@ -178,43 +284,20 @@ struct Entry {
|
|||||||
self.url = url
|
self.url = url
|
||||||
// TODO: remove file extension?
|
// TODO: remove file extension?
|
||||||
var fname = url.lastPathComponent
|
var fname = url.lastPathComponent
|
||||||
|
var customOrder = 100
|
||||||
var idx = fname.firstIndex { !$0.isWholeNumber } ?? fname.startIndex
|
var idx = fname.firstIndex { !$0.isWholeNumber } ?? fname.startIndex
|
||||||
// sort order
|
// sort order
|
||||||
if let order = Int(fname[..<idx]) {
|
if let order = Int(fname[..<idx]) {
|
||||||
self.order = order
|
customOrder = order
|
||||||
idx = fname[idx..<fname.endIndex].firstIndex { !$0.isWhitespace } ?? idx
|
idx = fname[idx..<fname.endIndex].firstIndex { !$0.isWhitespace } ?? idx
|
||||||
idx = fname[idx..<fname.endIndex].firstIndex { !$0.isPunctuation } ?? idx
|
idx = fname[idx..<fname.endIndex].firstIndex { !$0.isPunctuation } ?? idx
|
||||||
fname = String(fname[idx..<fname.endIndex])
|
fname = String(fname[idx..<fname.endIndex])
|
||||||
} else {
|
|
||||||
self.order = 100
|
|
||||||
}
|
}
|
||||||
// flags
|
self.order = customOrder
|
||||||
var flags: [Flags] = []
|
self.action = ActionFlag.from(&fname)
|
||||||
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)
|
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 {
|
guard isDir else {
|
||||||
return nil
|
return nil
|
||||||
@@ -233,38 +316,6 @@ struct Entry {
|
|||||||
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 {
|
||||||
|
|||||||