From 50598220a3d8ec297f7c1165155449b729ef9e45 Mon Sep 17 00:00:00 2001 From: sbosse Date: Mon, 21 Jul 2025 23:11:47 +0200 Subject: [PATCH] Mon 21 Jul 22:43:21 CEST 2025 --- js/term/widgets/filemanager.js | 420 +++++++++++++++++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 js/term/widgets/filemanager.js diff --git a/js/term/widgets/filemanager.js b/js/term/widgets/filemanager.js new file mode 100644 index 0000000..00f4117 --- /dev/null +++ b/js/term/widgets/filemanager.js @@ -0,0 +1,420 @@ +/** + ** ============================== + ** O O O OOOO + ** O O O O O O + ** O O O O O O + ** OOOO OOOO O OOO OOOO + ** O O O O O O O + ** O O O O O O O + ** OOOO OOOO O O OOOO + ** ============================== + ** Dr. Stefan Bosse http://www.bsslab.de + ** + ** COPYRIGHT: THIS SOFTWARE, EXECUTABLE AND SOURCE CODE IS OWNED + ** BY THE AUTHOR(S). + ** THIS SOURCE CODE MAY NOT BE COPIED, EXTRACTED, + ** MODIFIED, OR OTHERWISE USED IN A CONTEXT + ** OUTSIDE OF THE SOFTWARE SYSTEM. + ** + ** $AUTHORS: Christopher Jeffrey and contributors, Stefan Bosse + ** $INITIAL: (C) 2013-2018, Christopher Jeffrey and contributors + ** $VERSION: 1.4.2 + ** + ** $INFO: + * + * filemanager.js - file manager element for blessed + * + * Events: 'ioerror','cd','file' + * + * New options: okayButton, cancelButton, autohide, select (select emits file event), + * noshow, arrows: {up:'[-]',down:'[+]',width:3,height:1,fg:'red',bg:'default'}}, + * box:{bg,border}, input:{fg,bg,border} + * + ** $ENDOFINFO + */ + +var options = { + version:'1.4.2' +} +/** + * Modules + */ +var Comp = Require('com/compat'); + +var path = Require('path') + , fs = Require('fs'); + + +var Node = Require('term/widgets/node'); +var List = Require('term/widgets/list'); +var Button = Require('term/widgets/button'); +var Helpers = Require('term/helpers'); +var Box = Require('term/widgets/box'); +var TextBox = Require('term/widgets/textbox'); +var Arrows = Require('term/widgets/arrows'); +var Screen = Require('term/widgets/screen'); + +/** + * FileManager + */ + +function FileManager(options) { + var self = this, + bbox, + off1=0, + off2=0, + arrows=options.arrows; + + if (!instanceOf(this,Node)) { + return new FileManager(options); + } + + options = options || {}; + options.parseTags = true; + options.mouse = true; + options.arrows=_; // optional arrows are handled here, not in list + + if (options.parent == Screen.global) { + // Screen overlay; adjust top/height settings + bbox=Helpers.bbox(Screen.global,options); + if (options.box) bbox.top += 1,bbox.height -= 2,bbox.width -= 4,bbox.left += 2; + if (options.input) bbox.height -= 3; + if (options.cancelButton||options.okayButton) bbox.height -= 1; + if (arrows) bbox.width -= 4,bbox.left += 2; + options.top=bbox.top; + options.left=bbox.left; + options.height=bbox.height; + options.width=bbox.width; + } + + // options.label = ' {blue-fg}%path{/blue-fg} '; + List.call(this, options); + + options.arrows=arrows; + + this.cwd = options.cwd || process.cwd(); + this.file = this.cwd; + this.value = this.cwd; + this.noshow = options.noshow; + + if (options.parent == this.screen) { + // Collect clickable elements of this widget + this._clickable=this.screen.clickable; + this.screen.clickable=[]; + + // compute for button positions + bbox=Helpers.bbox(this.screen,options); + if (options.cancelButton||options.okayButton) off1=2; + if (options.input) off2=3; + if (options.box) + this._.box = new Box({ + top:bbox.top-2, + width:bbox.width+8, + left:bbox.left-4, + height:bbox.height+3+off1+off2, + hidden:options.hidden, + border:options.box.border, + style:{ + label:options.label, + fg:options.box.fg, + bg:options.box.bg||'white' + } + }); + if (this._.box) this.screen.append(this._.box); + + if (options.input) { + this._.input = new TextBox({ + screen: this.screen, + top: bbox.top+bbox.height+(options.box?1:0)-1, + height: options.input.border&&options.input.border.type=='line'?3:1, + width: bbox.width, + left: bbox.left, + keys: options.input.mutable?true:undefined, + vi: options.input.mutable?true:undefined, + mouse: options.input.mutable?true:undefined, + inputOnFocus: options.input.mutable?true:undefined, + value: options.input.value||'', + hidden:options.hidden, + border: options.input.border, + style: { + fg:options.input.fg||'black', + bg:options.input.bg||'white', + bold:true + } + }); + this.screen.append(this._.input); + } + + if (options.okayButton) { + this._.okay = new Button({ + screen: this.screen, + top: bbox.top+bbox.height+(options.box?1:0)+off2, + height: 1, + left: bbox.left+1, + width: 10, + content: options.okayButton, + align: 'center', + style: { + fg:'white', + bg: 'blue', + bold:true, + }, + autoFocus: false, + hidden:options.hidden, + mouse: true + }); + this._.okay.on('press',function () { + var item=self.items[self.selected], + value = self._.input? + self._.input.getValue(): + item.content.replace(/\{[^{}]+\}/g, '').replace(/@$/, ''), + file=path.resolve(self.cwd, value); + self.emit('file', file); + self.hide(); + }); + this.screen.append(this._.okay); + } + if (options.cancelButton) { + this._.cancel = new Button({ + screen: this.screen, + top: bbox.top+bbox.height+(options.box?1:0)+off2, + height: 1, + left: bbox.left+bbox.width-10-1, + width: 10, + content: options.cancelButton, + align: 'center', + style: { + fg:'white', + bg: 'red', + bold:true, + }, + autoFocus: false, + hidden:options.hidden, + mouse: true + }); + this._.cancel.on('press',function () { self.hide(); }); + this.screen.append(this._.cancel); + } + if (options.arrows) + Arrows( + self, + options, + function () {self.emit('element wheelup')}, + function () {self.emit('element wheeldown')}, + true + ); + this._hide=this.hide; + this.hide = function() { + self._hide(); + if (self._.box) self._.box.hide(); + if (self._.input) self._.input.hide(); + if (self._.okay) self._.okay.hide(); + if (self._.cancel) self._.cancel.hide(); + if (self._.up) self._.up.hide(); + if (self._.down) self._.down.hide(); + self.screen.render(); + // restore all clickable elements + self.screen.clickable=self._clickable; + } + this._show = this.show; + this.show = function() { + // save all screen clickable elements; enable only this clickables + self._clickable=self.screen.clickable; + self.screen.clickable=self.clickable; + self._show(); + if (self._.box) self._.box.show(); + if (self._.input) self._.input.show(); + if (self._.okay) self._.okay.show(); + if (self._.cancel) self._.cancel.show(); + if (self._.up) self._.up.show(); + if (self._.down) self._.down.show(); + self.screen.render(); + } + + // Save clickable elements of this widget; restore screen + this.clickable=this.screen.clickable; + this.screen.clickable=this._clickable; + } + if (options.label && ~options.label.indexOf('%path')) { + this._label.setContent(options.label.replace('%path', this.cwd)); + } + if (this._.input) + this.on('selected', function(item) { + var value = item.content.replace(/\{[^{}]+\}/g, '').replace(/@$/, ''); + if (value.indexOf('/') != -1) value=''; + self._.input.setValue(value); + self._.input.update(); + }); + + + this.on('select', function(item) { + var value = item.content.replace(/\{[^{}]+\}/g, '').replace(/@$/, '') + , file = path.resolve(self.cwd, value); + return fs.stat(file, function(err, stat) { + var _cwd=self.cwd; + if (err) { + return self.emit('ioerror', err, file); + } + self.file = file; + self.value = file; + if (stat.isDirectory()) { + self.cwd = file; + self.refresh(undefined,function (err) { + if (err) self.cwd=_cwd; + else if (options.label && ~options.label.indexOf('%path')) { + self._label.setContent(options.label.replace('%path', self.cwd)); + self.emit('cd', file, self.cwd); + self.screen.render(); + } + }); + } else { + if (self.options.select) self.emit('file', file); + if (self.options.select && self.options.autohide) self.hide(); + } + }); + }); + + +} + +//FileManager.prototype.__proto__ = List.prototype; +inheritPrototype(FileManager,List); + +FileManager.prototype.type = 'file-manager'; + +FileManager.prototype.refresh = function(cwd, callback) { + var self = this; + + if (cwd) this.cwd = cwd; + else cwd = this.cwd; + + return fs.readdir(cwd, function(err, list) { + if (err && err.code === 'ENOENT') { + self.cwd = cwd !== process.env.HOME + ? process.env.HOME + : '/'; + return self.refresh(undefined,callback); + } + + if (err) { + if (callback) return callback(err); + return self.emit('ioerror', err, cwd); + } + + var dirs = [] + , files = []; + + list.unshift('..'); + + list.forEach(function(name) { + var f = path.resolve(cwd, name) + , stat; + + try { + stat = fs.lstatSync(f); + } catch (e) { + ; + } + + if ((stat && stat.isDirectory()) || name === '..') { + dirs.push({ + name: name, + text: '{light-blue-fg}' + name + '{/light-blue-fg}/', + dir: true + }); + } else if (stat && stat.isSymbolicLink()) { + files.push({ + name: name, + text: '{light-cyan-fg}' + name + '{/light-cyan-fg}@', + dir: false + }); + } else { + files.push({ + name: name, + text: name, + dir: false + }); + } + }); + + dirs = Helpers.asort(dirs); + files = Helpers.asort(files); + + list = dirs.concat(files).map(function(data) { + return data.text; + }); + + self.setItems(list); + self.select(0); + self.screen.render(); + + self.emit('refresh'); + + if (callback) callback(); + }); + +}; + +FileManager.prototype.pick = function(cwd, callback) { + if (!callback) { + callback = cwd; + cwd = null; + } + var self = this + , focused = this.screen.focused === this + , hidden = this.hidden + , onfile + , oncancel; + + function resume() { + self.removeListener('file', onfile); + self.removeListener('cancel', oncancel); + if (hidden) { + self.hide(); + } + if (!focused) { + self.screen.restoreFocus(); + } + self.screen.render(); + } + + this.on('file', onfile = function(file) { + resume(); + return callback(null, file); + }); + + this.on('cancel', oncancel = function() { + resume(); + return callback(); + }); + + this.refresh(cwd, function(err) { + if (err) return callback(err); + + if (hidden) { + self.show(); + } + + if (!focused) { + self.screen.saveFocus(); + self.focus(); + } + + self.screen.render(); + }); +}; + +FileManager.prototype.reset = function(cwd, callback) { + if (!callback) { + callback = cwd; + cwd = null; + } + this.cwd = cwd || this.options.cwd; + this.refresh(callback); +}; + +/** + * Expose + */ + +module.exports = FileManager;