Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c41c92e637 | ||
|
|
a7f9b3d48c | ||
|
|
6867488a4a | ||
|
|
284eb1aa5c | ||
|
|
d514bfb610 | ||
|
|
8053110349 | ||
|
|
e2743903e3 | ||
|
|
dcfe16cb9b | ||
|
|
8242a64a5d | ||
|
|
355cf0ded1 | ||
|
|
fa5551f272 | ||
|
|
b6eaa8d3c4 | ||
|
|
1af81e5b4a | ||
|
|
05714ef69e |
29
CHANGELOG.md
Normal 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
|
||||
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: src/* examples/*
|
||||
Menuscript.app: src/* res/**
|
||||
@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
|
||||
@@ -20,11 +20,10 @@ Menuscript.app: src/* examples/*
|
||||
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
|
||||
@find res -name .DS_Store -delete
|
||||
@rm -rf Menuscript.app/Contents/Resources/
|
||||
@cp -R res/ Menuscript.app/Contents/Resources/
|
||||
@touch Menuscript.app
|
||||
@echo
|
||||
ifeq ($(HAS_SIGN_IDENTITY),1)
|
||||
|
||||
40
README.md
@@ -12,7 +12,7 @@ A menu bar script executor.
|
||||
|
||||
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.
|
||||
See [res/examples](res/examples/) directory.
|
||||
|
||||
|
||||
## Usage
|
||||
@@ -36,13 +36,18 @@ 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.
|
||||
=> If you can run 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:
|
||||
|
||||
#### 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
|
||||
|
||||
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:
|
||||
|
||||
- __[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).
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<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.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>
|
||||
<text x="75" y="840">run.sh</text>
|
||||
</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_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"/>
|
||||
<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>
|
||||
<text x="72" y="590">run.sh</text>
|
||||
<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
@@ -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
|
||||
4
res/examples/Dyn [dyn].svg
Normal 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 |
|
Before Width: | Height: | Size: 214 B After Width: | Height: | Size: 214 B |
BIN
res/examples/_icons/draw.png
Normal file
|
After Width: | Height: | Size: 616 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>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<string>1.1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>3</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13</string>
|
||||
<key>LSBackgroundOnly</key>
|
||||
|
||||
272
src/main.swift
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env swift
|
||||
import AppKit
|
||||
import Cocoa
|
||||
import SwiftUI
|
||||
|
||||
class FlippedView: NSView {
|
||||
override var isFlipped: Bool { true }
|
||||
@@ -21,12 +20,22 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
||||
|
||||
private func initStatusIcon() {
|
||||
self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
self.statusItem.button?.title = "⌘"
|
||||
// self.statusItem.button?.image = NSImage.statusIcon
|
||||
if let img = 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?.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) {
|
||||
if menu == self.statusItem.menu {
|
||||
self.statusItem.menu = NSMenu(title: menu.title)
|
||||
@@ -43,15 +52,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
||||
}
|
||||
|
||||
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: "")
|
||||
itm.representedObject = entry
|
||||
itm.image = entry.icon()
|
||||
|
||||
if entry.isDir {
|
||||
if entry.isDir || entry.action == .Dynamic {
|
||||
itm.submenu = NSMenu(title: entry.url.path)
|
||||
itm.submenu?.delegate = self
|
||||
} else {
|
||||
} else if entry.action != .Inactive {
|
||||
itm.action = #selector(menuItemCallback)
|
||||
}
|
||||
}
|
||||
@@ -64,20 +76,28 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
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")
|
||||
userStorageURL() ?? resFile("examples", nil)
|
||||
}
|
||||
|
||||
private func userStorageURL() -> URL? {
|
||||
@@ -159,115 +179,169 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - A menu item
|
||||
|
||||
struct Entry {
|
||||
enum Flags {
|
||||
case Verbose
|
||||
case Text
|
||||
// MARK: - Command executor
|
||||
|
||||
struct Exec {
|
||||
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 title: String
|
||||
let flags: [Flags]
|
||||
let url: URL
|
||||
let title: String
|
||||
let action: ActionFlag
|
||||
let hasDynParent: Bool
|
||||
|
||||
var isDir: Bool { url.hasDirectoryPath }
|
||||
|
||||
init(_ url: URL) {
|
||||
init(_ url: URL, title: String? = nil) {
|
||||
self.hasDynParent = title != nil
|
||||
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)
|
||||
var fname = title ?? url.lastPathComponent
|
||||
var customOrder = 100
|
||||
if !hasDynParent {
|
||||
var idx = fname.firstIndex { !$0.isWholeNumber } ?? fname.startIndex
|
||||
// sort order
|
||||
if let order = Int(fname[..<idx]) {
|
||||
customOrder = 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])
|
||||
}
|
||||
}
|
||||
self.flags = flags
|
||||
self.order = customOrder
|
||||
self.action = ActionFlag.from(&fname)
|
||||
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 !hasDynParent {
|
||||
let basePath = self.isDir ? self.url.appendingPathComponent("icon") : self.url
|
||||
for ext in ["svg", "png", "jpg", "jpeg", "gif", "ico", "icns"] {
|
||||
let iconPath = basePath.appendingPathExtension(ext)
|
||||
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 {
|
||||
img = NSImage(named: NSImage.folderName)
|
||||
if img == nil, action == .Dynamic || hasDynParent {
|
||||
let cmd = Exec(file: self.url, env: ["ACTION": "icon", "ITEM": hasDynParent ? self.title : ""])
|
||||
img = NSImage(data: cmd.readData())
|
||||
}
|
||||
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 {
|
||||
extension Entry: Comparable {
|
||||
static func < (lhs: Entry, rhs: Entry) -> Bool {
|
||||
return (lhs.order, lhs.title) < (rhs.order, rhs.title)
|
||||
}
|
||||
|
||||