ref: Exec, ActionFlag, Entry
This commit is contained in:
169
src/main.swift
169
src/main.swift
@@ -52,7 +52,7 @@ 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) {
|
||||||
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()
|
||||||
@@ -73,7 +73,13 @@ 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
|
// MARK: - Helper
|
||||||
@@ -170,18 +176,100 @@ 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
|
||||||
|
|
||||||
|
static func from(_ filename: inout String) -> Self {
|
||||||
|
for (key, flag) in [
|
||||||
|
"[txt]": ActionFlag.Text,
|
||||||
|
"[verbose]": .Verbose,
|
||||||
|
] {
|
||||||
|
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 }
|
||||||
|
|
||||||
@@ -189,43 +277,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(fileURLWithPath: 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
|
||||||
@@ -244,38 +309,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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user