So I wrote one. This is a virtual filesystem driver for Midnight Commander built using @zranger1’s library for communicating with Pixelblazes. It makes Pixelblazes appear as read-only devices which can be browsed just like a regular disk. The installation is a little more involved than just copying a single file, but I’ll describe what worked for me.
~=~=~=~=~
Midnight Commander comes preinstalled on some *n*x distributions, and is available from the package manager on all the common ones. On a fresh copy of Ubuntu under WSL, I had to install it with:
$ sudo apt-get install mc
The virtual filesystem driver needs Python3 (which is installed by default on recent distributions), but my WSL didn’t have the websocket-client
package nor the Python package manager installed, so I first had to do:
$ sudo apt-get install pip3
…to get the package manager, and then:
$ pip3 install websocket-client
…to get the package itself.
Midnight Commander looks for plugins in a particular location; the easiest way to find out where is to ask it:
$ mc -F
…and look in the [User data]
section of the output for the location of the ‘extfs.d’ folder. For example, if your username is {yourUsernameHere}:
Home directory: /home/{yourUsernameHere}
Profile root directory: /home/{yourUsernameHere}
[System data]
Config directory: /etc/mc/
Data directory: /usr/share/mc/
File extension handlers: /usr/lib/mc/ext.d/
VFS plugins and scripts: /usr/lib/mc/
extfs.d: /usr/lib/mc/extfs.d/
fish: /usr/lib/mc/fish/
[User data]
Config directory: /home/{yourUsernameHere}/.config/mc/
Data directory: /home/{yourUsernameHere}/.local/share/mc/
skins: /home/{yourUsernameHere}/.local/share/mc/skins/
extfs.d: /home/{yourUsernameHere}/.local/share/mc/extfs.d/ <== THIS IS THE ONE
fish: /home/{yourUsernameHere}/.local/share/mc/fish/
mcedit macros: /home/{yourUsernameHere}/.local/share/mc/mc.macros
mcedit external macros: /home/{yourUsernameHere}/.local/share/mc/mcedit/macros.d/macro.*
Cache directory: /home/{yourUsernameHere}/.cache/mc/
In my case, the folder didn’t exist, so I needed to create it:
$ mkdir /home/{yourUsernameHere}/.local/share/mc/extfs.d/
Then, save the following file into the ‘extfs.d’ folder as ‘pb+’ (no suffix):
Save this file as 'pb+'
#!/usr/bin/env python3
import os, argparse, pathlib, urllib, requests, json, struct, time, math
from pixelblaze import Pixelblaze, PixelblazeEnumerator
from datetime import datetime
# ------------------------------------------------
# USER-MODIFIABLE SETTINGS
scanTime = 3 # how long to listen when scanning for Pixelblazes
includePatternSettings = False # exponentially increases loading time; don't do it!
debug = False
# ------------------------------------------------
# CONSTANTS
tempFolder = pathlib.Path.home() / '.cache/mc'
# Canonical folder and file names
folderFolders = 'folders'
folderFiles = 'files'
folderConfiguration = 'Configuration'
folderPatterns = 'Patterns'
folderPlaylists = 'Playlists'
filenameDeviceSettings = 'deviceSettings.json'
filenameExpanderSettings = 'expanderSettings.json'
filenameMapperSettings = 'pixelmap.js'
filenamePlaylists = 'defaultPlaylist.json'
# ------------------------------------------------
# LZstring code borrowed from https://github.com/NickWaterton/Pixelblaze-Async/blob/main/pixelblaze_async/lzstring.py
def decompressFromUint8Array(compressed):
if compressed is None:
return ""
if compressed == "":
return None
resetValue = 128
dictionary = {}
enlargeIn = 4
dictSize = 4
numBits = 3
entry = ""
result = []
val=compressed[0]
position=resetValue
index=1
for i in range(3):
dictionary[i] = i
bits = 0
maxpower = math.pow(2, 2)
power = 1
while power != maxpower:
resb = val & position
position >>= 1
if position == 0:
position = resetValue
val = compressed[index]
index += 1
bits |= power if resb > 0 else 0
power <<= 1
next = bits
if next == 0:
bits = 0
maxpower = math.pow(2, 8)
power = 1
while power != maxpower:
resb = val & position
position >>= 1
if position == 0:
position = resetValue
val = compressed[index]
index += 1
bits |= power if resb > 0 else 0
power <<= 1
c = chr(bits)
elif next == 1:
bits = 0
maxpower = math.pow(2, 16)
power = 1
while power != maxpower:
resb = val & position
position >>= 1
if position == 0:
position = resetValue
val = compressed[index]
index += 1
bits |= power if resb > 0 else 0
power <<= 1
c = chr(bits)
elif next == 2:
return ""
#print(bits)
dictionary[3] = c
w = c
result.append(c)
counter = 0
while True:
counter += 1
if index > len(compressed):
return ""
bits = 0
maxpower = math.pow(2, numBits)
power = 1
while power != maxpower:
resb = val & position
position >>= 1
if position == 0:
position = resetValue
val = compressed[index]
index += 1
bits |= power if resb > 0 else 0
power <<= 1
c = bits
if c == 0:
bits = 0
maxpower = math.pow(2, 8)
power = 1
while power != maxpower:
resb = val & position
position >>= 1
if position == 0:
position = resetValue
val = compressed[index]
index += 1
bits |= power if resb > 0 else 0
power <<= 1
dictionary[dictSize] = chr(bits)
dictSize += 1
c = dictSize - 1
enlargeIn -= 1
elif c == 1:
bits = 0
maxpower = math.pow(2, 16)
power = 1
while power != maxpower:
resb = val & position
position >>= 1
if position == 0:
position = resetValue
val = compressed[index]
index += 1
bits |= power if resb > 0 else 0
power <<= 1
dictionary[dictSize] = chr(bits)
dictSize += 1
c = dictSize - 1
enlargeIn -= 1
elif c == 2:
return "".join(result)
if enlargeIn == 0:
enlargeIn = math.pow(2, numBits)
numBits += 1
if c in dictionary:
entry = dictionary[c]
else:
if c == dictSize:
entry = w + w[0]
else:
return None
result.append(entry)
# Add w+entry[0] to the dictionary.
dictionary[dictSize] = w + entry[0]
dictSize += 1
enlargeIn -= 1
w = entry
if enlargeIn == 0:
enlargeIn = math.pow(2, numBits)
numBits += 1
# ------------------------------------------------
# Pixelblaze client library borrowed (and heavily modified) from https://github.com/zranger1/pixelblaze-client/blob/main/pixelblaze/pixelblaze.py
"""
Copyright 2020 JEM (ZRanger1)
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import websocket, socket, json, time, struct, threading
class Pixelblaze:
ws = None
connected = False
flash_save_enabled = False
default_recv_timeout = 1
ipAddr = None
def __init__(self, addr):
self.open(addr)
def open(self, addr):
if self.connected is False:
uri = "ws://" + addr + ":81"
self.ws = websocket.create_connection(uri, sockopt=((socket.SOL_SOCKET, socket.SO_REUSEADDR, 1),
(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),))
self.ws.settimeout(self.default_recv_timeout)
self.ipAddr = addr
self.connected = True
def close(self):
"""Close websocket connection"""
if self.connected is True:
self.ws.close()
self.connected = False
def ws_flush(self):
self.ws.settimeout(0.1)
try:
while True:
self.ws.recv()
except websocket._exceptions.WebSocketTimeoutException:
self.ws.settimeout(self.default_recv_timeout)
return
def ws_recv(self, wantBinary=False):
result = None
try:
while True:
result = self.ws.recv()
if (wantBinary is False) and (type(result) is str):
break
elif (wantBinary is True) and (result[0] == 0x07):
break
else:
result = None
except websocket._exceptions.WebSocketTimeoutException:
return None
except websocket._exceptions.WebSocketConnectionClosedException:
self.connected = False
raise
return result
def send_string(self, cmd):
self.ws.send(cmd.encode("utf-8"))
def getHardwareConfig(self):
self.ws_flush()
self.send_string('{"getConfig": true}')
hw = dict()
p1 = self.ws_recv()
while p1 is not None:
p2 = json.loads(p1)
hw = {**hw, **p2}
p1 = self.ws_recv()
return hw
def getPatternList(self):
patterns = dict()
self.ws_flush()
self.send_string("{ \"listPrograms\" : true }")
frame = self.ws_recv(True)
while frame is not None:
listFrame = frame[2:].decode("utf-8")
listFrame = listFrame.split("\n")
listFrame = [m.split("\t") for m in listFrame]
for pat in listFrame:
if len(pat) == 2:
patterns[pat[0]] = pat[1]
if frame[1] & 0x04:
break
frame = self.ws_recv(True)
return patterns
class PixelblazeEnumerator:
PORT = 1889
SYNC_ID = 890
BEACON_PACKET = 42
DEVICE_TIMEOUT = 30000
LIST_CHECK_INTERVAL = 5000
listTimeoutCheck = 0
isRunning = False
threadObj = None
listener = None
devices = dict()
autoSync = False
def __init__(self, hostIP="0.0.0.0"):
self.start(hostIP)
def __del__(self):
self.stop()
def _time_in_millis(self):
return int(round(time.time() * 1000)) % 0xFFFFFFFF
def _unpack_beacon(self, data):
return struct.unpack("<LLL", data)
def start(self, hostIP):
try:
self.listener = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.listener.bind((hostIP, self.PORT))
self.threadObj = threading.Thread(target=self._listen)
self.isRunning = True
self.listTimeoutCheck = 0
self.threadObj.start()
return True
except socket.error as e:
print(e)
self.stop()
return False
def stop(self):
"""
Stop listening for datagrams, terminate listener thread and close socket.
"""
if self.listener is None:
return
else:
self.isRunning = False
self.threadObj.join()
time.sleep(0.5)
self.listener.close()
self.threadObj = None
self.listener = None
def _listen(self):
while self.isRunning:
data, addr = self.listener.recvfrom(1024)
now = self._time_in_millis()
pkt = self._unpack_beacon(data)
if pkt[0] == self.BEACON_PACKET:
self.devices[pkt[1]] = {"address": addr, "timestamp": now, "sender_id": pkt[1], "sender_time": pkt[2]}
if (now - self.listTimeoutCheck) >= self.LIST_CHECK_INTERVAL:
newlist = dict()
for dev, record in self.devices.items():
if (now - record["timestamp"]) <= self.DEVICE_TIMEOUT:
newlist[dev] = record
self.devices = newlist
self.listTimeoutCheck = now
def getPixelblazeList(self):
dev = []
for record in self.devices.values():
dev.append(record["address"][0])
return dev
# ------------------------------------------------
def urlFor(host, pathFragment):
return urllib.parse.urlunsplit(('http', host, pathFragment, None, None))
# ------------------------------------------------
def loadSchema(useCache):
cacheFilename = str(tempFolder / 'pbCache.json')
cache = {
}
if useCache:
with open(cacheFilename, "r") as fp:
cache = json.loads(fp.read())
else:
pbList = PixelblazeEnumerator()
time.sleep(scanTime)
pbList.stop()
for pixelblazeIP in pbList.getPixelblazeList():
try:
pb = Pixelblaze(pixelblazeIP)
hardwareConfig = pb.getHardwareConfig()
pixelblazeName = safeFilename(hardwareConfig['name'])
cache[pixelblazeName] = { 'ipAddress': pixelblazeIP, folderFolders: { } }
cache[pixelblazeName][folderFolders][folderConfiguration] = { folderFiles: { } }
cache[pixelblazeName][folderFolders][folderPatterns] = { folderFiles: { } }
cache[pixelblazeName][folderFolders][folderPlaylists] = { folderFiles: { } }
cache[pixelblazeName][folderFolders][folderConfiguration][folderFiles][filenameDeviceSettings] = urlFor(pixelblazeIP, '/config.json')
pixelmap = urlFor(pixelblazeIP, '/pixelmap.txt')
if getSizeFromPixelblaze(pixelmap):
cache[pixelblazeName][folderFolders][folderConfiguration][folderFiles][filenameMapperSettings] = pixelmap
expander = urlFor(pixelblazeIP, '/obconf.dat')
if hardwareConfig['ledType'] == 5:
cache[pixelblazeName][folderFolders][folderConfiguration][folderFiles][filenameExpanderSettings] = expander
playlist = urlFor(pixelblazeIP, '/l/_defaultplaylist_')
if getSizeFromPixelblaze(playlist):
cache[pixelblazeName][folderFolders][folderPlaylists][folderFiles][filenamePlaylists] = playlist
for patternId, patternName in pb.getPatternList().items():
pattern = urlFor(pixelblazeIP, '/p/' + patternId)
cache[pixelblazeName][folderFolders][folderPatterns][folderFiles][safeFilename(patternName) + '.js'] = pattern
if includePatternSettings:
patternSettings = pattern + '.c'
if getSizeFromPixelblaze(patternSettings):
cache[pixelblazeName][folderFolders][folderPatterns][folderFiles][safeFilename(patternName) + '.json'] = patternSettings
pb.close()
except Exception as e:
logToFile('loadSchema: Error fetching Pixelblaze details from ' + pixelblazeIP + ": " + repr(e))
with open(cacheFilename, "w") as fp:
json.dump(cache, fp)
return cache
# ------------------------------------------------
def logToFile(message):
if debug:
logFilename = str(tempFolder / 'extfs.log')
with open(logFilename, mode='a') as logFile:
logFile.write("[" + datetime.now().strftime("%d-%m-%Y %H:%M") + "] " + message + '\n')
# ------------------------------------------------
def safeFilename(unsafeName):
return unsafeName.replace("/", "∕")
# ------------------------------------------------
def getSizeFromPixelblaze(url):
try:
with requests.get(url, stream=True) as r:
if r.status_code == 200:
return r.headers['Content-length']
except Exception as e:
logToFile('Error fetching size of ' + url + ": " + repr(e))
return None
# ------------------------------------------------
def downloadFromPixelblaze(url, filename):
try:
#time.sleep(0.2)
with requests.get(url) as r:
if r.status_code == 200:
open(filename, 'wb').write(r.content)
return True
else:
logToFile('downloadFromPixelblaze: GET from url ' + url + ' returned ' + str(r.status_code))
except Exception as e:
logToFile('downloadFromPixelblaze: error fetching ' + url + ": " + repr(e))
return False
# ------------------------------------------------
def convertExpanderFile(inputFilePath, outputFilePath):
with open(inputFilePath, "rb") as fp:
binaryData = fp.read(1)
magicNumber = struct.unpack('<B', binaryData)[0]
if magicNumber != 5:
logToFile('decodeExpander: obconf.dat has incorrect magic number: ' + str(magicNumber))
return None
binaryData = fp.read()
binarySize = len(binaryData)
if binarySize % 96 != 0:
logToFile('decodeExpander: obconf.dat has incorrect length: ' + str(binarySize))
rowSize = 12
boards = { 'boards': [ ] }
ledTypes = [ 'notUsed', 'WS2812B', 'drawAll', 'APA102 Data', 'APA102 Clock' ]
colorOrders = { 0x24: 'RGB', 0x18: 'RBG', 0x09: 'BRG', 0x06: 'BGR', 0x21: 'GRB', 0x12: 'GBR', 0xE4: 'RGBW', 0xE1: 'GRBW' }
for row in range(binarySize // rowSize):
offsets = struct.unpack('<4B2H4x', binaryData[(row * rowSize):(row + 1) * rowSize])
boardAddress = offsets[0] >> 3
channel = offsets[0] % 8
ledType = offsets[1]
numElements = offsets[2]
colorOrder = offsets[3]
pixelCount = offsets[4]
startIndex = offsets[5]
notUsed = offsets[6:10]
board = row // 8
rowNumber = row % 8
if rowNumber == 0:
boards['boards'].append( { 'address': boardAddress, 'rows': { } } )
boards['boards'][board]['rows'][rowNumber] = [ ]
if ledType == 1 or ledType == 3:
boards['boards'][board]['rows'][rowNumber].append( { 'channel': channel, 'type': ledTypes[ledType], 'startIndex': startIndex, 'count': pixelCount, 'options': colorOrders[colorOrder] } )
else:
boards['boards'][board]['rows'][rowNumber].append( { 'channel': channel, 'type': ledTypes[ledType] } )
with open(outputFilePath, "w") as fp:
fp.write(json.dumps(boards, indent=2))
# ------------------------------------------------
def convertPatternFile(inputFilePath, outputFilePath):
with open(inputFilePath, "rb") as fp:
binaryData = fp.read()
header_size = 44
offsets = struct.unpack('<11I', binaryData[:header_size])
name_offset = offsets[1]
name_length = offsets[2]
jpeg_offset = offsets[3]
jpeg_length = offsets[4]
source_offset = offsets[7]
source_length = offsets[8]
bytecode_offset = offsets[9]
bytecode_length = offsets[10]
with open(outputFilePath, "w") as fp:
epeSource = {}
epeSource['source'] = json.loads(decompressFromUint8Array(binaryData[source_offset:source_offset+source_length]))
fp.write(epeSource['source']['main'])
# ------------------------------------------------
def convertPlaylistFile(inputFilePath, outputFilePath):
with open(inputFilePath, "r") as fp:
playlist = json.loads(fp.read())
for item in range(len(playlist['items'])):
playlist['items'][item]['id'] = 'Test'
with open(outputFilePath, "w") as fp:
fp.write(json.dumps(playlist, indent=2))
# ------------------------------------------------
def addDirectoryEntry(itemName, itemSize, itemMeta):
itemPermissions = ''
if itemMeta == 'd':
itemPermissions = itemMeta + 'rwx' + 'r-x' + 'r-x'
else:
itemPermissions = itemMeta + "rw-" + "r--" + "r--"
itemLinks = "1"
itemOwner = "wizard"
itemGroup = "wizard"
itemTime = datetime.fromtimestamp(0)
line = itemPermissions + " " + itemLinks + " " + itemOwner + " " + itemGroup + " " + str(itemSize) + " " + itemTime.strftime("%m-%d-%Y %H:%M") + " " + itemName
print(line)
# EXTFS Command: list archivename
def pbList(args): # archivename
logToFile("pbList called with archivename=" + args.archivename)
devices = loadSchema(False)
for device in devices.keys():
folders = devices[device][folderFolders]
addDirectoryEntry(device, len(folders), "d")
for folder in folders.keys():
files = folders[folder][folderFiles]
addDirectoryEntry(str(pathlib.Path(device, folder)), len(files), "d")
for file in files.keys():
addDirectoryEntry(str(pathlib.Path(device, folder, file)), 999999, "-") # ugly kludge; we may need to get the actual filesize to make MC behave itself.
exit(0)
# ------------------------------------------------
# EXTFS command: "copyout archivename storedfilename extractto"
def pbCopyOut(args): # archivename, storedfilename, extractto
logToFile("pbCopyOut called with archivename=" + args.archivename + ", storedfilename=" + args.storedfilename + ", extractto=" + args.extractto)
devices = loadSchema(True)
segments = args.storedfilename.split('/')
if len(segments) != 3:
logToFile('pbCopyOut: couldn''t make sense of storedfilename segments:' + repr(segments))
exit(1)
device = segments[0]
if device not in devices.keys():
logToFile('pbCopyOut: pixelblazeName ' + device + ' not found in cache ' + repr(devices.items()))
exit(1)
folder = segments[1]
if folder not in devices[device][folderFolders].keys():
logToFile('pbCopyOut: folder ' + folder + ' not found in cache for pixelblazeName ' + device)
exit(1)
file = segments[2]
if file not in devices[device][folderFolders][folder][folderFiles].keys():
logToFile('pbCopyOut: file ' + file + ' not found in cache for pixelblazeName ' + device + 'and folder ' + folder)
exit(1)
url = devices[device][folderFolders][folder][folderFiles][file]
if folder == folderConfiguration and file == filenameExpanderSettings:
tempFilename = str(pathlib.Path(args.extractto).with_suffix('.dat'))
logToFile('pbCopyOut: downloading from URL ' + url + ' to ' + tempFilename)
if downloadFromPixelblaze(url, tempFilename):
logToFile('pbCopyOut: converting ' + tempFilename + ' to ' + args.extractto)
convertExpanderFile(tempFilename, args.extractto)
os.remove(tempFilename)
exit(0)
else:
logToFile('pbCopyOut: what happened?')
elif folder == folderConfiguration and file == filenameDeviceSettings:
tempFilename = str(pathlib.Path(args.extractto).with_suffix('.tmp'))
logToFile('pbCopyOut: downloading from URL ' + url + ' to ' + tempFilename)
if downloadFromPixelblaze(url, tempFilename):
logToFile('pbCopyOut: converting ' + tempFilename + ' to ' + args.extractto)
with open(tempFilename, "r") as fp:
settings = json.loads(fp.read())
with open(args.extractto, "w") as fp:
fp.write(json.dumps(settings, indent=2))
os.remove(tempFilename)
exit(0)
else:
logToFile('pbCopyOut: what happened?')
elif folder == folderPlaylists:
tempFilename = str(pathlib.Path(args.extractto).with_suffix('.tmp'))
logToFile('pbCopyOut: downloading from URL: ' + url + ' to ' + tempFilename)
if downloadFromPixelblaze(url, tempFilename):
logToFile('pbCopyOut: converting ' + tempFilename + ' to: ' + args.extractto)
with open(tempFilename, "r") as fp:
playlist = json.loads(fp.read())
for item in range(len(playlist['items'])):
logToFile("folder: " + repr(devices[device][folderFolders][folderPatterns][folderFiles]))
for patternName, patternId in devices[device][folderFolders][folderPatterns][folderFiles].items():
if patternId.endswith(playlist['items'][item]['id']):
playlist['items'][item]['id'] = patternName[:-3]
with open(args.extractto, "w") as fp:
fp.write(json.dumps(playlist, indent=2))
os.remove(tempFilename)
exit(0)
elif folder == folderPatterns and file.endswith('.js'):
tempFilename = str(pathlib.Path(args.extractto).with_suffix('.bin'))
logToFile('pbCopyOut: downloading from URL: ' + url + ' to ' + tempFilename)
if downloadFromPixelblaze(url, tempFilename):
logToFile('pbCopyOut: converting ' + tempFilename + ' to: ' + args.extractto)
convertPatternFile(tempFilename, args.extractto)
os.remove(tempFilename)
exit(0)
else:
logToFile('pbCopyOut: downloading from URL: ' + url + ' to ' + args.extractto)
if downloadFromPixelblaze(url, args.extractto):
exit(0)
exit(1)
# ------------------------------------------------
# EXTFS command: "copyin archivename storedfilename sourcefile"
def pbCopyIn(args):
exit(1)
# ------------------------------------------------
# EXTFS command: "rm archivename storedfilename"
def pbRm(args):
exit(1)
# ------------------------------------------------
# EXTFS command: "mkdir archivename dirname"
def pbMkdir(args):
exit(1)
# ------------------------------------------------
# EXTFS command: "rmdir archivename dirname"
def pbRmdir(args):
exit(1)
# ------------------------------------------------
# EXTFS command: "run"
def pbRun(args):
exit(1)
# ------------------------------------------------
# Program start.
if __name__ == "__main__":
# Parse command line.
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
# EXTFS command: "list archivename"
parser_list = subparsers.add_parser('list')
parser_list.add_argument('archivename', nargs='?', default='*')
parser_list.set_defaults(func=pbList)
# EXTFS command: "copyout archivename storedfilename extractto"
parser_copyout = subparsers.add_parser('copyout')
parser_copyout.add_argument('archivename', nargs='?', default='*')
parser_copyout.add_argument('storedfilename', nargs='?', default='*')
parser_copyout.add_argument('extractto', nargs='?', default='*')
parser_copyout.set_defaults(func=pbCopyOut)
# EXTFS command: "copyin archivename storedfilename sourcefile"
parser_copyin = subparsers.add_parser('copyin')
parser_copyin.add_argument('archivename', nargs='?', default='*')
parser_copyin.add_argument('storedfilename', nargs='?', default='*')
parser_copyin.add_argument('sourcefile', nargs='?', default='*')
parser_copyin.set_defaults(func=pbCopyIn)
# EXTFS command: "rm archivename storedfilename"
parser_rm = subparsers.add_parser('rm')
parser_rm.add_argument('archivename', nargs='?', default='*')
parser_rm.add_argument('storedfilename', nargs='?', default='*')
parser_rm.set_defaults(func=pbRm)
# EXTFS command: "mkdir archivename dirname"
parser_mkdir = subparsers.add_parser('mkdir')
parser_mkdir.add_argument('archivename', nargs='?', default='*')
parser_mkdir.add_argument('dirname', nargs='?', default='*')
parser_mkdir.set_defaults(func=pbMkdir)
# EXTFS command: "rmdir archivename dirname"
parser_rmdir = subparsers.add_parser('rmdir')
parser_rmdir.add_argument('archivename', nargs='?', default='*')
parser_rmdir.add_argument('dirname', nargs='?', default='*')
parser_rmdir.set_defaults(func=pbRmdir)
# EXTFS command: "run"
parser_run = subparsers.add_parser('run')
parser_run.add_argument('archivename', nargs='?', default='*')
parser_run.add_argument('dirname', nargs='?', default='*')
parser_run.set_defaults(func=pbRun)
# parse the args and call whatever function was selected
args = parser.parse_args()
args.func(args)
# We shouldn't get here, so if we did it's an error.
exit(1)
And finally, the file needs to be marked as ‘executable’:
$ chmod +x /home/{yourUsernameHere}/.local/share/mc/extfs.d/pb+
~=~=~=~=~
The Pixelblaze filesystem is named “pb://”, so you can specify it as one of the (up to two, both optional) starting folders for Midnight Commander like this:
$ mc pb://
Within Midnight Commander, you can also open the Pixelblaze filesystem in the current pane by entering (in the command prompt at the bottom of the window) the command:
$ cd pb://
If you haven’t used it before, there are some good tutorials here and here that show the many useful things you can do with Midnight Commander.
Enjoy!