diff --git a/docs/html/File.html b/docs/html/File.html
index 86102b6..f9f6460 100644
--- a/docs/html/File.html
+++ b/docs/html/File.html
@@ -198,6 +198,11 @@
path
- The initial directory path
shortcuts
- The list of shortcuts containing keybindings and actions for the file entity
+options
- A table containing various options that configures the file instance
+showHiddenFiles
- A flag to display hidden files in the file selection modal. Defaults to false
+sortAttribute
- The file attribute to sort the file selection list by. File attributes come from hs.fs.dir. Defaults to modification
(last modified timestamp)
+
+
Each shortcut
item should be a list with items at the following indices:
diff --git a/docs/markdown/File.md b/docs/markdown/File.md
index a4b8fd9..f09bec3 100644
--- a/docs/markdown/File.md
+++ b/docs/markdown/File.md
@@ -71,7 +71,7 @@ File class that subclasses [Entity](Entity.html) to represent some directory or
| **Signature** | `File:initialize(path, shortcuts)` |
| **Type** | Method |
| **Description** | Initializes a new file instance with its path and custom shortcuts. By default, a cheatsheet and common shortcuts are initialized. |
-| **Parameters** | - `path` - The initial directory path
- `shortcuts` - The list of shortcuts containing keybindings and actions for the file entity
|
+| **Parameters** | - `path` - The initial directory path
- `shortcuts` - The list of shortcuts containing keybindings and actions for the file entity
- `options` - A table containing various options that configures the file instance
- `showHiddenFiles` - A flag to display hidden files in the file selection modal. Defaults to `false`
- `sortAttribute` - The file attribute to sort the file selection list by. File attributes come from [hs.fs.dir](http://www.hammerspoon.org/docs/hs.fs.html#dir). Defaults to `modification` (last modified timestamp)
|
| **Returns** | |
| [move](#move) | |
diff --git a/spec/file.spec.lua b/spec/file.spec.lua
index 7a7585c..6a31415 100644
--- a/spec/file.spec.lua
+++ b/spec/file.spec.lua
@@ -73,11 +73,26 @@ local function initializeTests()
{
name = "creates a file entity instance",
args = { "~/.vimrc" },
+ absolutePath = "/Users/test/.vimrc",
},
{
- name = "creates a file entity instance",
+ name = "creates a directory file entity instance",
args = { "~/Downloads" },
- isDirectory = true,
+ absolutePath = "/Users/test/Downloads",
+ getAttributes = function() return { mode = "directory" } end,
+ },
+ {
+ name = "notifies on invalid file path error",
+ args = { "~/.vimrc" },
+ absolutePath = nil,
+ notifiesError = true,
+ },
+ {
+ name = "notifies on get file attributes error",
+ args = { "~/.vimrc" },
+ getAttributes = function() return error("Unable to get file attributes") end,
+ absolutePath = "/Users/test/.vimrc",
+ notifiesError = true,
},
}
@@ -87,16 +102,21 @@ local function initializeTests()
local mockHs = hammerspoonMocker()
local file = require("file")
local mockFs = {
- attributes = function() return { mode = test.isDirectory and "directory" } end,
- pathToAbsolute = function() end,
+ attributes = test.getAttributes or function() return { mode = "file" } end,
+ pathToAbsolute = function() return test.absolutePath end,
}
_G.hs = mock(mockHs({ fs = mockFs }))
+ file.notifyError = spy.new(function() end)
file.mergeShortcuts = function() end
file:initialize(table.unpack(test.args))
- assert.are.same(file.path, test.args[1])
+ if test.notifiesError then
+ assert.spy(file.notifyError).was.called(1)
+ else
+ assert.are.same(file.path, test.args[1])
+ end
end)
end
end
diff --git a/src/file.lua b/src/file.lua
index fbec15f..95ae5d6 100644
--- a/src/file.lua
+++ b/src/file.lua
@@ -56,6 +56,9 @@ File.behaviors = Entity.behaviors + {
--- Parameters:
--- * `path` - The initial directory path
--- * `shortcuts` - The list of shortcuts containing keybindings and actions for the file entity
+--- * `options` - A table containing various options that configures the file instance
+--- * `showHiddenFiles` - A flag to display hidden files in the file selection modal. Defaults to `false`
+--- * `sortAttribute` - The file attribute to sort the file selection list by. File attributes come from [hs.fs.dir](http://www.hammerspoon.org/docs/hs.fs.html#dir). Defaults to `modification` (last modified timestamp)
---
--- Each `shortcut` item should be a list with items at the following indices:
--- * `1` - An optional table containing zero or more of the following keyboard modifiers: `"cmd"`, `"alt"`, `"shift"`, `"ctrl"`, `"fn"`
@@ -67,9 +70,24 @@ File.behaviors = Entity.behaviors + {
---
--- Returns:
--- * None
-function File:initialize(path, shortcuts)
+function File:initialize(path, shortcuts, options)
+ options = options or {}
+
local absolutePath = hs.fs.pathToAbsolute(path)
- local attributes = hs.fs.attributes(absolutePath) or {}
+
+ if not absolutePath then
+ self.notifyError("Error initializing File entity", "Path "..path.." may not exist.")
+ return
+ end
+
+ local success, value = pcall(function() return hs.fs.attributes(absolutePath) or {} end)
+
+ if not success then
+ self.notifyError("Error initializing File entity", value or "")
+ return
+ end
+
+ local attributes = value
local isDirectory = attributes.mode == "directory"
local openFileInFolder = self:createEvent(path, function(target) self.open(target) end)
@@ -117,6 +135,8 @@ function File:initialize(path, shortcuts)
self.path = path
self.shortcuts = mergedShortcuts
self.cheatsheet = cheatsheet
+ self.showHiddenFiles = options.showHiddenFiles or false
+ self.sortAttribute = options.sortAttribute or "modification"
local cheatsheetDescription = "Ki shortcut keybindings registered for file "..self.path
self.cheatsheet:init(self.path, cheatsheetDescription, mergedShortcuts)
@@ -226,7 +246,6 @@ function File:showFileSelectionModal(path, handler)
local parentPathRegex = "^(.+)/.+$"
local absolutePath = hs.fs.pathToAbsolute(path)
local parentDirectory = absolutePath:match(parentPathRegex) or "/"
- local iterator, directory = hs.fs.dir(absolutePath)
-- Add selection modal shortcut to open files with cmd + return
local function openFile(modal)
@@ -235,11 +254,23 @@ function File:showFileSelectionModal(path, handler)
handler(choice.filePath, true)
modal:cancel()
end
+ -- Add selection modal shortcut to toggle hidden files cmd + shift + "."
+ local function toggleHiddenFiles(modal)
+ modal:cancel()
+ self.showHiddenFiles = not self.showHiddenFiles
+
+ -- Defer execution to avoid conflicts with the prior selection modal that just closed
+ hs.timer.doAfter(0, function()
+ self:showFileSelectionModal(path, handler)
+ end)
+ end
local navigationShortcuts = {
{ { "cmd" }, "return", openFile },
+ { { "cmd", "shift" }, ".", toggleHiddenFiles },
}
self.selectionModalShortcuts = self.mergeShortcuts(navigationShortcuts, self.selectionModalShortcuts)
+ local iterator, directory = hs.fs.dir(absolutePath)
if iterator == nil then
self.notifyError("Error walking the path at "..path)
return
@@ -247,28 +278,45 @@ function File:showFileSelectionModal(path, handler)
for file in iterator, directory do
local filePath = absolutePath.."/"..file
+ local attributes = hs.fs.attributes(filePath) or {}
local displayName = hs.fs.displayName(filePath) or file
+ local isHiddenFile = string.sub(file, 1, 1) == "."
+ local shouldShowFile = isHiddenFile and self.showHiddenFiles or not isHiddenFile
local subText = filePath
- if file == "." then
- displayName = file
- subText = absolutePath.." (Current directory)"
- filePath = absolutePath
- elseif file == ".." then
- displayName = file
- subText = parentDirectory.." (Parent directory)"
- filePath = parentDirectory
+ if file ~= "." and file ~= ".." and shouldShowFile then
+ table.insert(choices, {
+ text = displayName,
+ subText = subText,
+ file = file,
+ filePath = filePath,
+ image = filePath and self.getFileIcon(filePath),
+ fileAttributes = attributes,
+ })
end
-
- table.insert(choices, {
- text = displayName,
- subText = subText,
- file = file,
- filePath = filePath,
- image = filePath and self.getFileIcon(filePath),
- })
end
+ -- Sort choices by last modified timestamp and add current/parent directories to choices
+ table.sort(choices, function(a, b)
+ local value1 = a.fileAttributes[self.sortAttribute]
+ local value2 = b.fileAttributes[self.sortAttribute]
+ return value1 > value2
+ end)
+ table.insert(choices, {
+ text = "..",
+ subText = parentDirectory.." (Parent directory)",
+ file = "..",
+ filePath = parentDirectory,
+ image = self.getFileIcon(absolutePath),
+ })
+ table.insert(choices, {
+ text = ".",
+ subText = absolutePath.." (Current directory)",
+ file = ".",
+ filePath = absolutePath,
+ image = self.getFileIcon(absolutePath),
+ })
+
self.showSelectionModal(choices, function(choice)
if choice then
handler(choice.filePath)