neo-neo-bblessed
Version:
A fork of neo-blessed (which is a fork of blessed) with bug fixes and maintenance.
283 lines (234 loc) • 6.49 kB
text/typescript
/**
* filemanager.js - file manager element for blessed
* Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
* https://github.com/chjj/blessed
*/
/**
* Modules
*/
import * as path from 'path';
import * as fs from 'fs';
import * as helpers from '../helpers.js';
import Node from './node.js';
import listFactory from './list.js';
const List = listFactory.List;
/**
* Interfaces
*/
interface FileManagerOptions {
cwd?: string;
label?: string;
parseTags?: boolean;
[key: string]: any;
}
interface FileItem {
name: string;
text: string;
dir: boolean;
}
interface FileManagerLabel {
setContent(content: string): void;
}
interface FileManagerScreen {
focused: any;
render(): void;
saveFocus(): void;
restoreFocus(): void;
}
interface FileManagerInterface extends List {
type: string;
cwd: string;
file: string;
value: string;
_label: FileManagerLabel;
screen: FileManagerScreen;
options: FileManagerOptions;
hidden: boolean;
// Methods
refresh(cwd?: string | Function, callback?: Function): any;
pick(cwd?: string | Function, callback?: Function): void;
reset(cwd?: string | Function, callback?: Function): void;
emit(event: string, ...args: any[]): any;
on(event: string, listener: Function): void;
removeListener(event: string, listener: Function): void;
setItems(items: string[]): void;
select(index: number): void;
hide(): void;
show(): void;
focus(): void;
}
/**
* FileManager - Modern ES6 Class
*/
class FileManager extends List {
type = 'file-manager';
cwd: string;
file: string;
value: string;
constructor(options?: FileManagerOptions) {
// Handle malformed options gracefully
if (!options || typeof options !== 'object' || Array.isArray(options)) {
options = {};
}
// Force filemanager-specific options
options.parseTags = true;
// options.label = ' {blue-fg}%path{/blue-fg} ';
super(options);
this.cwd = options.cwd || process.cwd();
this.file = this.cwd;
this.value = this.cwd;
if (options.label && ~options.label.indexOf('%path')) {
this._label.setContent(options.label.replace('%path', this.cwd));
}
this.on('select', (item: any) => {
const value = item.content.replace(/\{[^{}]+\}/g, '').replace(/@$/, '');
const file = path.resolve(this.cwd, value);
return fs.stat(
file,
(err: NodeJS.ErrnoException | null, stat?: fs.Stats) => {
if (err) {
return this.emit('error', err, file);
}
this.file = file;
this.value = file;
if (stat!.isDirectory()) {
this.emit('cd', file, this.cwd);
this.cwd = file;
if (options!.label && ~options!.label.indexOf('%path')) {
this._label.setContent(options!.label.replace('%path', file));
}
this.refresh();
} else {
this.emit('file', file);
}
}
);
});
}
refresh(cwd?: string | Function, callback?: Function): any {
if (!callback) {
callback = cwd;
cwd = null;
}
if (cwd) this.cwd = cwd as string;
else cwd = this.cwd;
return fs.readdir(
cwd as string,
(err: NodeJS.ErrnoException | null, list?: string[]) => {
if (err && err.code === 'ENOENT') {
this.cwd = cwd !== process.env.HOME ? process.env.HOME! : '/';
return this.refresh(callback);
}
if (err) {
if (callback) return callback(err);
return this.emit('error', err, cwd);
}
let dirs: FileItem[] = [];
let files: FileItem[] = [];
list!.unshift('..');
list!.forEach((name: string) => {
const f = path.resolve(cwd as string, name);
let stat: fs.Stats | undefined;
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((data: FileItem) => {
return data.text;
});
this.setItems(list);
this.select(0);
this.screen.render();
this.emit('refresh');
if (callback) callback();
}
);
}
pick(cwd?: string | Function, callback?: Function): void {
if (!callback) {
callback = cwd;
cwd = null;
}
const focused = this.screen.focused === this;
const hidden = this.hidden;
let onfile: Function;
let oncancel: Function;
const resume = () => {
this.removeListener('file', onfile);
this.removeListener('cancel', oncancel);
if (hidden) {
this.hide();
}
if (!focused) {
this.screen.restoreFocus();
}
this.screen.render();
};
this.on(
'file',
(onfile = (file: string) => {
resume();
return callback!(null, file);
})
);
this.on(
'cancel',
(oncancel = () => {
resume();
return callback!();
})
);
this.refresh(cwd as string, (err?: NodeJS.ErrnoException) => {
if (err) return callback!(err);
if (hidden) {
this.show();
}
if (!focused) {
this.screen.saveFocus();
this.focus();
}
this.screen.render();
});
}
reset(cwd?: string | Function, callback?: Function): void {
if (!callback) {
callback = cwd;
cwd = null;
}
this.cwd = (cwd as string) || this.options.cwd;
this.refresh(callback);
}
}
/**
* Factory function for backward compatibility
*/
function fileManager(options?: FileManagerOptions): FileManagerInterface {
return new FileManager(options) as FileManagerInterface;
}
// Attach the class as a property for direct access
fileManager.FileManager = FileManager;
/**
* Expose
*/
export default fileManager;