Table of Contents
- Introduction
- Screenshot
- Current Features
- Code
Introduction
This is motivated by @alex0’s description #2 about reusing Logseq’s outliner for file management. This implementation is:
- Different on purpose:
- Forget most conveniences and prepare to learn a different workflow.
- It tries to blend with Logseq’s experience.
- Logseq is different and this is too.
- Experimental:
- No specific direction, other than proving the concept
- Highly UNTESTED
- If you want quality, use instead the file manager of your operating system.
- Missing important features. Here are only a few:
- No consideration for access rights
- No check for duplicate block entries
- NO UNDO
- Almost no safety:
- Not easy to make a mess, but certainly possible
- Use at own risk
- It is technically a SECURITY RISK
- Don’t use it in production
- Not easy to make a mess, but certainly possible
Screenshot
Current Features
- Use a simple macro to turn any block into a node about your file system, e.g.:
{{foldername C:/pathto/parentfolder}}
- Such blocks can be safely:
- deleted:
- no changes to the file system
- moved:
- position is irrelevant
- copied:
- no changes to the file system
- each copy is independent
- deleted:
- Hover to show the available buttons.
- Safely
click
on any button to see if that path currently exists in the file system.- Simple clicks make no changes.
Alt + click
proceeds to changes without confirmation.- Basic info is provided as messages and console logs.
Alt + click
on buttonRead
to auto-generate blocks in a hierarchy that matches the one currently on your drive.- This may take some time for big trees.
- Try first with a small one.
- Repeat to refresh:
- The blocks are not synced with the file system.
- Any outdated blocks get deleted.
- Copies don’t get affected.
- Can use this feature for comparisons.
- Copies don’t get affected.
- This may take some time for big trees.
- Expand a folder-block to show its contents.
- Nested blocks don’t get affected by expanding/collapsing.
- They maintain their values since the last
Read
of any of their ancestor blocks.
- They maintain their values since the last
- Nested blocks don’t get affected by expanding/collapsing.
- Zoom on a block to focus as usually.
Alt + click
on buttonDelete
to delete both the underlying file/folder and block.- As mentioned earlier, deleting manually a block or file/folder doesn’t update its pair.
- Subfolders and sub-blocks get also deleted.
Alt + click
on buttonCreate
to create a missing folder and all of its missing ancestors (if any) recursively.- This doesn’t create files.
Code
- At the end of file
preload.js
, normally inside...\Logseq\app-0.9.13\resources\app\js
:contextBridge.exposeInMainWorld('getfs', ()=>fs )
- This is the security risk.
- It also gets affected by updates.
- Inside file
config.edn
, insidemacros{}
:
:filename "<div class='kit filesystem' data-kit='filesystem' data-type='file' data-name='$1'></div>"
:foldername "<div class='kit filesystem' data-kit='filesystem' data-type='folder' data-name='$1'></div>"
- Inside file
custom.css
:
button.filesystem {
font-size: 12px;
line-height: 14px;
margin: 0px 8px 0px 8px;
padding: 2px 4px 2px 4px;
}
button.filesystem.create {
background: #ffee00;
}
button.filesystem.delete {
background: #ff4400;
}
button.filesystem.read {
background: #ff9900;
}
div.block-content button.filesystem {
visibility: hidden;
}
div.block-content:hover button.filesystem {
visibility: visible;
}
- This code requires having kits inside file
custom.js
. - Inside page
FileSystem
in Logseq, put the following code in one or more javascript code-block(s):
const LS = logseq.api
const Module = logseq.Module
const Kits = Module.Kits
function statusMsg(status, msg){
Module.Msg.ofStatus(msg, status)
}
const error = statusMsg.bind(null, "error")
const info = statusMsg.bind(null, "info")
const success = statusMsg.bind(null, "success")
const fs = window.getfs && getfs()
if (!fs) return error("missing fs")
const Block = (Module.Block || Module.setChild("Block"))
.setStatic(function forEachChild(block, cb){
block.children.find( (arr)=>{
const child = LS.get_block(arr[1])
if (child) return cb(child)
})
})
.setStatic(function parentOf(block){
return LS.get_block(block.parent.id)
})
const FS = Module.setChild("FileSystem")
.setStatic(function button(name, handler, arg){
const btn = Kits.createElementOfClass("button", "filesystem", name)
btn.classList.add(name.toLowerCase())
Kits.addClickEventToButton(handler.bind(null, arg), btn)
return btn
})
.setStatic(function create(block, cb){
const parent = Block.parentOf(block)
if (!parent) return cb("exists")
create(parent, (res)=>{
if (res !== "done" && res !== "exists") return cb(res)
const path = FS.fullPathOfBlock(block)
fs.access(path, (err)=>{
if (!err) return cb("exists")
fs.mkdir(path, (err)=>{
if (!err) console.log("CREATED FOLDER: " + path)
cb(err ? err.message : "done")
})
})
})
})
.setStatic(function deleteMissingBlockChildren(block, cb){
const rest = []
var rem = 0
Block.forEachChild(block, (child)=>{
rem += 1
fs.access(FS.fullPathOfBlock(child), (err)=>{
if (err) {
LS.remove_block(child.uuid)
console.log("DELETED BLOCK: " + FS.nameOfBlock(child))
} else {
rest.push(child)
}
if (!--rem) cb(rest)
})
})
if (!rem) cb(rest)
})
.setStatic(function fullPathOfBlock(block){
const name = FS.nameOfBlock(block)
return (!name) ? "" : FS.pathOfBlock(Block.parentOf(block)) + name
})
.setStatic(function insertChildBlock(parentBlock, path, name){
const property = FS.isFolder(path + "/" + name) ? "foldername" : "filename"
const nameblock = {properties: {[property]: name}}
var found
Block.forEachChild(parentBlock, (sibling)=>{
if (FS.nameCompare(sibling, nameblock) > -1) return found = sibling
})
if (found && !FS.nameCompare(found, nameblock)) return found.uuid
const customUUID = LS.new_block_uuid()
const properties = {[property]: name}
const options = {customUUID, properties}
if (found) {
options.before = true
options.sibling = true
}
const content = "{{" + property + " " + name + "}}"
const uuid = (found || parentBlock).uuid
LS.insert_block(uuid, content, options)
})
.setStatic(function isFolder(path){
try { fs.readdirSync(path) }
catch { return false }
return true
})
.setStatic(function nameCompare(a, b){
const aprops = a.properties
const bprops = b.properties
const aname = aprops.filename
const bname = bprops.filename
if (aname && !bname) return 1
if (!aname && bname) return -1
const prop = (aname) ? "filename" : "foldername"
return aprops[prop].toLowerCase().localeCompare(bprops[prop].toLowerCase())
})
.setStatic(function nameOfBlock(block){
const properties = block.properties
return properties.foldername || properties.filename
})
.setStatic(function nameOfErr(err){
return err.message.slice(0, err.message.indexOf(":"))
})
.setStatic(function onCreateClicked(blockId, e){
const block = LS.get_block(blockId)
fs.access(FS.fullPathOfBlock(block), (err)=>{
if (!err) return info("exists")
if (e.altKey) FS.create(block, FS.onCreated)
else info("Alt to create")
})
})
.setStatic(function onCreated(res){
if (res === "done") success("created")
else if (res === "exists") error("root should exist") // special
else error(res)
})
.setStatic(function onDeleteClicked(blockId, e){
const block = LS.get_block(blockId)
const fullPath = FS.fullPathOfBlock(block)
fs.access(fullPath, (err)=>{
if (err) return info("doesn't exist")
if (e.altKey) FS.unlink(fullPath, block)
else info("Alt to delete")
})
})
.setStatic(function onReadClicked(blockId, e){
const block = LS.get_block(blockId)
fs.access(FS.fullPathOfBlock(block), (err)=>{
if (err) return info("doesn't exist")
if (e.altKey) FS.updateTreeBlock(block)
else info("Alt to read")
})
})
.setStatic(function onTreeBlockUpdated(spanIcon, iconHtml, res){
spanIcon.innerHTML = iconHtml
if (res === "done") success("done")
else error(res)
})
.setStatic(function onUnlinkFinished(type, path, block, err){
if (err) return error(FS.nameOfErr(err) + ": " + path)
console.log("DELETED " + type + ": " + path)
success("deleted")
LS.remove_block(block.uuid)
console.log("DELETED BLOCK: " + FS.nameOfBlock(block))
})
.setStatic(function pathOfBlock(block){
const foldername = block && block.properties.foldername
return (!foldername) ? "" : pathOfBlock(Block.parentOf(block)) + foldername + "/"
})
.setStatic(function sortBlocks(blocks){
blocks.sort(FS.nameCompare)
var previous
const options = {sibling: true}
blocks.forEach( (block)=>{
if (previous) {
LS.move_block(block.uuid, previous.uuid, options)
}
previous = block
})
})
.setStatic(function unlink(path, block){
if (FS.isFolder(path)) {
const cb = FS.onUnlinkFinished.bind(null, "FOLDER", path, block)
fs.rmdir(path, {recursive: true, force: true}, cb)
} else {
const cb = FS.onUnlinkFinished.bind(null, "FILE", path, block)
fs.unlink(path, cb)
}
})
.setStatic(function updateChildren(parentBlock, cb){
var rem = 0
Block.forEachChild(parentBlock, (child)=>{
if (!child.properties.foldername) return;
rem += 1
FS.updateFolderBlock(child, ()=>{ if (!--rem) cb("done") } )
})
if (!rem) cb("done")
})
.setStatic(function updateDiv(div, blockId){
const divParent = div.closest(".block-content")
divParent.querySelector(".block-content-inner").remove()
const divProps = divParent.querySelector(".block-properties div")
const block = LS.get_block(blockId)
const foldername = block.properties.foldername
const spanIcon = Kits.createElementOfClass("span", "ti")
spanIcon.innerHTML = (foldername) ? "" : ""
divProps.firstChild.remove()
divProps.firstChild.remove()
divProps.prepend(spanIcon, " ")
if (foldername) divProps.append(
FS.button("Create", FS.onCreateClicked, blockId),
FS.button("Read", FS.onReadClicked, blockId)
)
divProps.append( FS.button("Delete", FS.onDeleteClicked, blockId) )
})
.setStatic(function updateFolderBlock(block, cb){
const path = FS.fullPathOfBlock(block)
const uuidParent = block.uuid
fs.readdir(path, (err, names)=>{
if (err) return cb(FS.nameOfErr(err) + ": " + path)
FS.deleteMissingBlockChildren(block, (rest)=>{
if (!names.length) return cb("done");
if (rest.length) FS.sortBlocks(rest)
names.forEach( (name)=>{
FS.insertChildBlock(block, path, name)
block = LS.get_block(uuidParent)
})
if (!rest.length) LS.set_block_collapsed(uuidParent, true)
FS.updateChildren(block, cb)
})
})
})
.setStatic(function updateTreeBlock(block){
const selector = "div.ls-block [blockid='" + block.uuid + "'] .ti"
const spanIcon = document.querySelector(selector)
const cb = FS.onTreeBlockUpdated.bind(null, spanIcon, spanIcon.innerHTML)
spanIcon.innerHTML = ""
FS.updateFolderBlock(block, cb)
})
logseq.kits.setStatic(function filesystem(div){
const type = div.dataset.type
const name = div.dataset.name
const blockId = div.closest(".ls-block").getAttribute("blockid")
LS.upsert_block_property(blockId, type + "name", name)
setTimeout(FS.updateDiv, 0, div, blockId) // wait UI
})