diff --git a/src/app/Action/ActionBar.js b/src/app/Action/ActionBar.js index 9bbed35..1e237bc 100644 --- a/src/app/Action/ActionBar.js +++ b/src/app/Action/ActionBar.js @@ -1,10 +1,9 @@ -const { ReliefStyle } = imports.gi.Gtk; +const { Box, Button, ReliefStyle, VSeparator } = imports.gi.Gtk; const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; const { connect } = require("inferno-mobx"); const { autoBind } = require("../Gjs/autoBind"); -const ActionBarRm = require("./ActionBarRm").default; -const { ActionService } = require("./ActionService"); +const { h } = require("../Gjs/GtkInferno"); +const ActionBarItem = require("./ActionBarItem").default; const actions = [ { id: "cursorService.view", label: "View", shortcut: "F3" }, @@ -16,42 +15,24 @@ const actions = [ { id: "windowService.exit", label: "Exit", shortcut: "Alt+F4" }, ]; -/** - * @typedef IProps - * @property {ActionService} actionService - * - * @param {IProps} props - */ -function ActionBar(props) { - Component.call(this, props); - autoBind(this, ActionBar.prototype, __filename); -} - -ActionBar.prototype = Object.create(Component.prototype); - -/** - * @type {IProps} - */ -ActionBar.prototype.props = undefined; +class ActionBar extends Component { + constructor() { + super(); + autoBind(this, ActionBar.prototype, __filename); + } -ActionBar.prototype.render = function() { - return ( - h("box", { expand: false }, actions.reduce((prev, action) => prev.concat([ - action.id === "rm" ? h(ActionBarRm, { + render() { + return ( + h(Box, { expand: false }, actions.reduce((prev, action) => prev.concat([ + h(ActionBarItem, { + action, key: action.id, - label: action.shortcut + " " + action.label, - }) : h("button", { - can_focus: false, - expand: true, - key: action.id, - label: action.shortcut + " " + action.label, - on_pressed: this.props.actionService.get(action.id).handler, - relief: ReliefStyle.NONE, }), - h("v-separator", { key: action.id + "+" }), - ]), [])) - ); -}; + + h(VSeparator, { key: action.id + "+" }), + ]), /** @type {any[]} */ ([]))) + ); + } +} exports.ActionBar = ActionBar; -exports.default = connect(["actionService"])(ActionBar); diff --git a/src/app/Action/ActionBar.test.js b/src/app/Action/ActionBar.test.js index e759398..985f5eb 100644 --- a/src/app/Action/ActionBar.test.js +++ b/src/app/Action/ActionBar.test.js @@ -1,21 +1,7 @@ -const expect = require("expect"); -const h = require("inferno-hyperscript").default; -const { find, shallow } = require("../Test/Test"); const { ActionBar } = require("./ActionBar"); describe("ActionBar", () => { - it("dispatches action without payload", () => { - const handler = expect.createSpy(); - - /** @type {any} */ - const actionService = { - get: () => ({ handler }), - }; - - const tree = shallow(h(ActionBar, { actionService })); - const button = find(tree, x => x.type === "button"); - - button.props.on_pressed(); - expect(handler).toHaveBeenCalledWith(); + it("renders", () => { + new ActionBar().render(); }); }); diff --git a/src/app/Action/ActionBarItem.js b/src/app/Action/ActionBarItem.js new file mode 100644 index 0000000..d877f44 --- /dev/null +++ b/src/app/Action/ActionBarItem.js @@ -0,0 +1,53 @@ +const { Button, ReliefStyle } = imports.gi.Gtk; +const Component = require("inferno-component").default; +const { connect } = require("inferno-mobx"); +const { autoBind } = require("../Gjs/autoBind"); +const { h } = require("../Gjs/GtkInferno"); +const ActionBarRm = require("./ActionBarRm").default; +const { ActionService } = require("./ActionService"); + +/** + * @typedef IProps + * @property {{ id: string, label: string, shortcut: string }} action + * @property {ActionService?} [actionService] + * + * @extends Component + */ +class ActionBarItem extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + autoBind(this, ActionBarItem.prototype, __filename); + } + + /** + * @param {Button} button + */ + ref(button) { + const { get } = /** @type {ActionService} */ (this.props.actionService); + button.connect("pressed", get(this.props.action.id).handler); + } + + render() { + const { id, label, shortcut } = this.props.action; + + if (id === "rm") { + return h(ActionBarRm, { label: shortcut + " " + label }); + } + + return ( + h(Button, { + can_focus: false, + expand: true, + label: shortcut + " " + label, + ref: this.ref, + relief: ReliefStyle.NONE, + }) + ); + } +} + +exports.ActionBarItem = ActionBarItem; +exports.default = connect(["actionService"])(ActionBarItem); diff --git a/src/app/Action/ActionBarItem.test.js b/src/app/Action/ActionBarItem.test.js new file mode 100644 index 0000000..93d9986 --- /dev/null +++ b/src/app/Action/ActionBarItem.test.js @@ -0,0 +1,37 @@ +const expect = require("expect"); +const { ActionBarItem } = require("./ActionBarItem"); + +describe("ActionBarItem", () => { + it("renders", () => { + const action = { + id: "windowService.exit", + label: "Exit", + shortcut: "Alt+F4", + }; + + new ActionBarItem({ action }).render(); + }); + + it("connects handler", () => { + const handler = expect.createSpy(); + + /** @type {any} */ + const actionService = { + get: () => ({ handler }), + }; + + /** @type {any} */ + const button = { + connect: expect.createSpy(), + }; + + const action = { + id: "windowService.exit", + label: "Exit", + shortcut: "Alt+F4", + }; + + new ActionBarItem({ action, actionService }).ref(button); + expect(button.connect).toHaveBeenCalledWith("pressed", handler); + }); +}); diff --git a/src/app/Action/ActionBarRm.js b/src/app/Action/ActionBarRm.js index 46bd153..3fffae3 100644 --- a/src/app/Action/ActionBarRm.js +++ b/src/app/Action/ActionBarRm.js @@ -1,7 +1,7 @@ const { DragAction } = imports.gi.Gdk; -const { DestDefaults, ReliefStyle } = imports.gi.Gtk; +const { Button, DestDefaults, ReliefStyle } = imports.gi.Gtk; const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; +const { h } = require("../Gjs/GtkInferno"); const { connect } = require("inferno-mobx"); const { autoBind } = require("../Gjs/autoBind"); const { JobService } = require("../Job/JobService"); @@ -10,77 +10,70 @@ const { SelectionService } = require("../Selection/SelectionService"); /** * @typedef IProps - * @property {JobService} jobService - * @property {PanelService} panelService - * @property {SelectionService} selectionService + * @property {JobService?} [jobService] + * @property {PanelService?} [panelService] + * @property {SelectionService?} [selectionService] * @property {string} label * - * @param {IProps} props + * @extends Component */ -function ActionBarRm(props) { - Component.call(this, props); - autoBind(this, ActionBarRm.prototype, __filename); -} - -ActionBarRm.prototype = Object.create(Component.prototype); - -/** - * @type {IProps} - */ -ActionBarRm.prototype.props = undefined; +class ActionBarRm extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + autoBind(this, ActionBarRm.prototype, __filename); + } -/** - * @param {any} _node - * @param {{ get_selected_action(): number }} _dragContext - * @param {number} _x - * @param {number} _y - * @param {{ get_uris(): string[] }} selectionData - */ -ActionBarRm.prototype.handleDrop = function( - _node, - _dragContext, - _x, - _y, - selectionData, -) { - const { jobService, panelService } = this.props; - const uris = selectionData.get_uris(); + // tslint:disable:variable-name + /** + * @param {any} _node + * @param {any} _dragContext + * @param {number} _x + * @param {number} _y + * @param {{ get_uris(): string[] }} selectionData + */ + handleDrop(_node, _dragContext, _x, _y, selectionData) { + const { run } = /** @type {JobService} */ (this.props.jobService); + const { refresh } = /** @type {PanelService} */ (this.props.panelService); + const uris = selectionData.get_uris(); - jobService.run( - { + run({ destUri: "", type: "rm", uris, - }, - panelService.refresh, - ); -}; + }, refresh); + } + // tslint-enable: variable-name -ActionBarRm.prototype.handlePressed = function() { - this.props.selectionService.rm(); -}; + handlePressed() { + const { rm } = /** @type {SelectionService} */ (this.props.selectionService); + rm(); + } -/** - * @param {any} node - */ -ActionBarRm.prototype.ref = function(node) { - node.drag_dest_set(DestDefaults.ALL, [], DragAction.MOVE); - node.drag_dest_add_uri_targets(); -}; + /** + * @param {Button} node + */ + ref(node) { + node.connect("drag-data-received", this.handleDrop); + node.connect("pressed", this.handlePressed); + node.drag_dest_set(DestDefaults.ALL, [], DragAction.MOVE); + node.drag_dest_add_uri_targets(); + } -ActionBarRm.prototype.render = function() { - return ( - h("button", { - can_focus: false, - expand: true, - label: this.props.label, - on_drag_data_received: this.handleDrop, - on_pressed: this.handlePressed, - ref: this.ref, - relief: ReliefStyle.NONE, - }) - ); -}; + render() { + return ( + h(Button, { + can_focus: false, + expand: true, + label: this.props.label, + ref: this.ref, + relief: ReliefStyle.NONE, + }) + ); + } +} exports.ActionBarRm = ActionBarRm; exports.default = connect(["jobService", "panelService", "selectionService"])( diff --git a/src/app/Action/ActionBarRm.test.js b/src/app/Action/ActionBarRm.test.js index a1d002b..d9163f4 100644 --- a/src/app/Action/ActionBarRm.test.js +++ b/src/app/Action/ActionBarRm.test.js @@ -1,31 +1,24 @@ const { DragAction } = imports.gi.Gdk; const expect = require("expect"); +const noop = require("lodash/noop"); const { h } = require("../Gjs/GtkInferno"); const { find, shallow } = require("../Test/Test"); const { ActionBarRm } = require("./ActionBarRm"); describe("ActionBarRm", () => { it("renders", () => { - new ActionBarRm({ - jobService: undefined, - label: "", - panelService: undefined, - selectionService: undefined, - }).render(); + new ActionBarRm({ label: "" }).render(); }); it("enables drop", () => { + /** @type {any} */ const node = { + connect: noop, drag_dest_add_uri_targets: expect.createSpy(), drag_dest_set: expect.createSpy(), }; - new ActionBarRm({ - jobService: undefined, - label: "", - panelService: undefined, - selectionService: undefined, - }).ref(node); + new ActionBarRm({ label: "" }).ref(node); expect(node.drag_dest_set).toHaveBeenCalled(); expect(node.drag_dest_add_uri_targets).toHaveBeenCalled(); @@ -51,7 +44,6 @@ describe("ActionBarRm", () => { jobService, label: "", panelService, - selectionService: undefined, }).handleDrop(undefined, undefined, 0, 0, selectionData); expect(jobService.run).toHaveBeenCalledWith({ @@ -67,8 +59,10 @@ describe("ActionBarRm", () => { rm: expect.createSpy(), }; - const button = shallow(h(ActionBarRm, { selectionService })); - button.props.on_pressed(); + new ActionBarRm({ + label: "", + selectionService, + }).handlePressed(); expect(selectionService.rm).toHaveBeenCalled(); }); diff --git a/src/app/Action/ActionService.js b/src/app/Action/ActionService.js index 36930ee..d653724 100644 --- a/src/app/Action/ActionService.js +++ b/src/app/Action/ActionService.js @@ -48,7 +48,7 @@ class ActionService { }); } - return this.actions.get(id); + return /** @type {Action} */ (this.actions.get(id)); } } diff --git a/src/app/App.js b/src/app/App.js index d28cfd5..ca5527d 100644 --- a/src/app/App.js +++ b/src/app/App.js @@ -1,9 +1,9 @@ -const Gtk = imports.gi.Gtk; -const h = require("inferno-hyperscript").default; -const ActionBar = require("./Action/ActionBar").default; +const { Box, HBox, HSeparator, Orientation } = imports.gi.Gtk; +const { ActionBar } = require("./Action/ActionBar"); const CtxMenu = require("./CtxMenu/CtxMenu").default; +const { h } = require("./Gjs/GtkInferno"); const Jobs = require("./Job/Jobs").default; -const MenuBar = require("./Menu/MenuBar").default; +const { MenuBar } = require("./Menu/MenuBar"); const Panel = require("./Panel/Panel").default; const Places = require("./Place/Places").default; const Prompt = require("./Prompt/Prompt").default; @@ -11,16 +11,16 @@ const Toolbar = require("./Toolbar/Toolbar").default; exports.render = () => { return ( - h("box", { orientation: Gtk.Orientation.VERTICAL }, [ + h(Box, { orientation: Orientation.VERTICAL }, [ h(MenuBar), h(Toolbar), - h("h-separator"), - h("h-box", [ + h(HSeparator), + h(HBox, [ h(Places, { panelId: 0 }), h(Places, { panelId: 1 }), ]), - h("h-separator"), - h("h-box", { homogeneous: true, spacing: 1 }, [0, 1].map(panelId => h(Panel, { + h(HSeparator), + h(HBox, { homogeneous: true, spacing: 1 }, [0, 1].map(panelId => h(Panel, { id: panelId, key: panelId, }))), diff --git a/src/app/CtxMenu/CtxMenu.js b/src/app/CtxMenu/CtxMenu.js index c3bbaae..6efc32e 100644 --- a/src/app/CtxMenu/CtxMenu.js +++ b/src/app/CtxMenu/CtxMenu.js @@ -1,5 +1,6 @@ +const { Menu, SeparatorMenuItem } = imports.gi.Gtk; const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; +const { h } = require("../Gjs/GtkInferno"); const { connect } = require("inferno-mobx"); const { autoBind } = require("../Gjs/autoBind"); const { RefService } = require("../Ref/RefService"); @@ -9,59 +10,62 @@ const CtxMenuHandler = require("./CtxMenuHandler").default; /** * @typedef IProps - * @property {RefService} refService - * @property {SelectionService} selectionService + * @property {RefService?} [refService] + * @property {SelectionService?} [selectionService] * - * @param {IProps} props + * @extends Component */ -function CtxMenu(props) { - Component.call(this, props); - autoBind(this, CtxMenu.prototype, __filename); -} - -CtxMenu.prototype = Object.create(Component.prototype); +class CtxMenu extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + autoBind(this, CtxMenu.prototype, __filename); + } -/** @type {IProps} */ -CtxMenu.prototype.props = undefined; + render() { + const { set } = /** @type {RefService} */ (this.props.refService); + const { handlers, getUris } = + /** @type {SelectionService} */ (this.props.selectionService); -CtxMenu.prototype.render = function() { - const { handlers, getUris } = this.props.selectionService; - const iconSize = 16; - const uris = getUris(); + const iconSize = 16; + const uris = getUris(); - return ( - h("stub-box", [ - h("menu", { ref: this.props.refService.set("ctxMenu") }, [ - ...handlers.map(handler => { - return h(CtxMenuHandler, { handler, iconSize, uris }); - }), + return ( + h("stub-box", [ + h(Menu, { ref: set("ctxMenu") }, [ + ...handlers.map(handler => { + return h(CtxMenuHandler, { handler, iconSize, uris }); + }), - h("separator-menu-item"), + h(SeparatorMenuItem), - h(CtxMenuAction, { - icon: "edit-cut", - iconSize, - id: "selectionService.cut", - label: "Cut", - }), + h(CtxMenuAction, { + icon: "edit-cut", + iconSize, + id: "selectionService.cut", + label: "Cut", + }), - h(CtxMenuAction, { - icon: "edit-copy", - iconSize, - id: "selectionService.copy", - label: "Copy", - }), + h(CtxMenuAction, { + icon: "edit-copy", + iconSize, + id: "selectionService.copy", + label: "Copy", + }), - h(CtxMenuAction, { - icon: "edit-paste", - iconSize, - id: "directoryService.paste", - label: "Paste", - }), - ]), - ]) - ); -}; + h(CtxMenuAction, { + icon: "edit-paste", + iconSize, + id: "directoryService.paste", + label: "Paste", + }), + ]), + ]) + ); + } +} exports.CtxMenu = CtxMenu; exports.default = connect(["refService", "selectionService"])(CtxMenu); diff --git a/src/app/CtxMenu/CtxMenuAction.js b/src/app/CtxMenu/CtxMenuAction.js index 11c7d3b..d6dcf29 100644 --- a/src/app/CtxMenu/CtxMenuAction.js +++ b/src/app/CtxMenu/CtxMenuAction.js @@ -7,48 +7,45 @@ const { h } = require("../Gjs/GtkInferno"); /** * @typedef IProps - * @property {ActionService} actionService + * @property {ActionService?} [actionService] * @property {string} icon * @property {number} iconSize * @property {string} id * @property {string} label * - * @param {IProps} props + * @extends Component */ -function CtxMenuAction(props) { - Component.call(this, props); - autoBind(this, CtxMenuAction.prototype, __filename); -} - -CtxMenuAction.prototype = Object.create(Component.prototype); - -/** - * @type {IProps} - */ -CtxMenuAction.prototype.props = undefined; - -/** - * @param {MenuItem} menuItem - */ -CtxMenuAction.prototype.ref = function(menuItem) { - if (menuItem) { - const action = this.props.actionService.get(this.props.id); - menuItem.connect("activate", action.handler); +class CtxMenuAction extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + autoBind(this, CtxMenuAction.prototype, __filename); } -}; -CtxMenuAction.prototype.render = function() { - return h(MenuItem, { ref: this.ref }, [ - h(Box, [ - h(Image, { - icon_name: this.props.icon + "-symbolic", - pixel_size: this.props.iconSize, - }), + /** + * @param {MenuItem} menuItem + */ + ref(menuItem) { + if (menuItem) { + const { get } = /** @type {ActionService} */ (this.props.actionService); + menuItem.connect("activate", get(this.props.id).handler); + } + } - h(Label, { label: this.props.label }), - ]), - ]); -}; + render() { + return h(MenuItem, { ref: this.ref }, [ + h(Box, [ + h(Image, { + icon_name: this.props.icon + "-symbolic", + pixel_size: this.props.iconSize, + }), + h(Label, { label: this.props.label }), + ]), + ]); + } +} exports.CtxMenuAction = CtxMenuAction; exports.default = connect(["actionService"])(CtxMenuAction); diff --git a/src/app/CtxMenu/CtxMenuHandler.js b/src/app/CtxMenu/CtxMenuHandler.js index 56c74aa..3258816 100644 --- a/src/app/CtxMenu/CtxMenuHandler.js +++ b/src/app/CtxMenu/CtxMenuHandler.js @@ -1,6 +1,6 @@ -const { IconSize } = imports.gi.Gtk; +const { Box, IconSize, Image, Label, MenuItem } = imports.gi.Gtk; const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; +const { h } = require("../Gjs/GtkInferno"); const { connect } = require("inferno-mobx"); const { autoBind } = require("../Gjs/autoBind"); const { GioService } = require("../Gio/GioService"); @@ -8,41 +8,50 @@ const { FileHandler } = require("../../domain/File/FileHandler"); /** * @typedef IProps - * @property {GioService} gioService + * @property {GioService?} [gioService] * @property {FileHandler} handler * @property {number} iconSize * @property {string[]} uris * - * @param {IProps} props + * @extends Component */ -function CtxMenuHandler(props) { - Component.call(this, props); - autoBind(this, CtxMenuHandler.prototype, __filename); +class CtxMenuHandler extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + autoBind(this, CtxMenuHandler.prototype, __filename); + } + + handleActivate() { + const { launch } = /** @type {GioService} */ (this.props.gioService); + launch(this.props.handler, this.props.uris); + } + + /** + * @param {MenuItem} menuItem + */ + ref(menuItem) { + menuItem.connect("activate", this.handleActivate); + } + + render() { + const { displayName, icon } = this.props.handler; + + return ( + h(MenuItem, { ref: this.ref }, [ + h(Box, [ + h(Image, { + icon_name: icon || "utilities-terminal", + pixel_size: this.props.iconSize, + }), + h(Label, { label: displayName }), + ]), + ]) + ); + } } -CtxMenuHandler.prototype = Object.create(Component.prototype); - -/** @type {IProps} */ -CtxMenuHandler.prototype.props = undefined; - -CtxMenuHandler.prototype.handleActivate = function() { - this.props.gioService.launch(this.props.handler, this.props.uris); -}; - -CtxMenuHandler.prototype.render = function() { - const { displayName, icon } = this.props.handler; - - return h("menu-item", { on_activate: this.handleActivate }, [ - h("box", [ - h("image", { - icon_name: icon || "utilities-terminal", - pixel_size: this.props.iconSize, - }), - - h("label", { label: displayName }), - ]), - ]); -}; - exports.CtxMenuHandler = CtxMenuHandler; exports.default = connect(["gioService"])(CtxMenuHandler); diff --git a/src/app/Cursor/CursorService.js b/src/app/Cursor/CursorService.js index 13af613..2308185 100644 --- a/src/app/Cursor/CursorService.js +++ b/src/app/Cursor/CursorService.js @@ -14,11 +14,11 @@ const { TabService } = require("../Tab/TabService"); class CursorService { /** * @typedef IProps - * @property {DialogService} dialogService - * @property {DirectoryService} directoryService - * @property {GioService} gioService - * @property {PanelService} panelService - * @property {TabService} tabService + * @property {DialogService?} [dialogService] + * @property {DirectoryService?} [directoryService] + * @property {GioService?} [gioService] + * @property {PanelService?} [panelService] + * @property {TabService?} [tabService] * * @param {IProps} props */ @@ -33,11 +33,16 @@ class CursorService { * Opens file in terminal, with EDITOR environment variable. */ edit() { - const { dialogService, directoryService } = this.props; + const { alert } = + /** @type {DialogService} */ (this.props.dialogService); + + const { terminal } = + /** @type {DirectoryService} */ (this.props.directoryService); + const editor = this.env.EDITOR; if (!editor) { - dialogService.alert(`You have to define EDITOR environment variable.`); + alert(`You have to define EDITOR environment variable.`); return; } @@ -45,41 +50,49 @@ class CursorService { const match = /^file:\/\/(.+)/.exec(file.uri); if (!match) { - dialogService.alert(`${file.uri} is not local.`); + alert(`${file.uri} is not local.`); return; } - directoryService.terminal(["-e", editor, decodeURIComponent(match[1])]); + terminal(["-e", editor, decodeURIComponent(match[1])]); } open() { - const { dialogService, gioService, panelService } = this.props; + const { alert } = + /** @type {DialogService} */ (this.props.dialogService); + + const { getHandlers, launch } = + /** @type {GioService} */ (this.props.gioService); + + const { levelUp, ls } = + /** @type {PanelService} */ (this.props.panelService); + const { fileType, name, uri } = this.getCursor(); if (name === "..") { - panelService.levelUp(); + levelUp(); return; } if (fileType === FileType.DIRECTORY) { - panelService.ls(uri); + ls(uri); return; } - gioService.getHandlers(uri, (error, result) => { + getHandlers(uri, (error, result) => { if (error) { - dialogService.alert(error.message); + alert(error.message); return; } const { contentType, handlers } = result; if (!handlers.length) { - dialogService.alert("No handlers registered for " + contentType + "."); + alert("No handlers registered for " + contentType + "."); return; } - gioService.launch(handlers[0], [uri]); + launch(handlers[0], [uri]); }); } @@ -87,11 +100,16 @@ class CursorService { * Opens file in terminal, with PAGER environment variable. */ view() { - const { dialogService, directoryService } = this.props; + const { alert } = + /** @type {DialogService} */ (this.props.dialogService); + + const { terminal } = + /** @type {DirectoryService} */ (this.props.directoryService); + const pager = this.env.PAGER; if (!pager) { - dialogService.alert(`You have to define PAGER environment variable.`); + alert(`You have to define PAGER environment variable.`); return; } @@ -99,21 +117,24 @@ class CursorService { const match = /^file:\/\/(.+)/.exec(file.uri); if (!match) { - dialogService.alert(`${file.uri} is not local.`); + alert(`${file.uri} is not local.`); return; } - directoryService.terminal(["-e", pager, decodeURIComponent(match[1])]); + terminal(["-e", pager, decodeURIComponent(match[1])]); } /** * @private */ getCursor() { - const { panelService, tabService } = this.props; - const activeTabId = panelService.getActiveTabId(); + const { getActiveTabId } = + /** @type {PanelService} */ (this.props.panelService); + + const { getCursor } = + /** @type {TabService} */ (this.props.tabService); - return tabService.getCursor(activeTabId); + return getCursor(getActiveTabId()); } } diff --git a/src/app/Cursor/CursorService.test.js b/src/app/Cursor/CursorService.test.js index 7163be5..af992c3 100644 --- a/src/app/Cursor/CursorService.test.js +++ b/src/app/Cursor/CursorService.test.js @@ -17,9 +17,6 @@ describe("CursorService", () => { const cursorService = new CursorService({ dialogService, directoryService, - gioService: undefined, - panelService: undefined, - tabService: undefined, }); cursorService.env = { EDITOR: undefined }; @@ -53,7 +50,6 @@ describe("CursorService", () => { const cursorService = new CursorService({ dialogService, directoryService, - gioService: undefined, panelService, tabService, }); @@ -89,7 +85,6 @@ describe("CursorService", () => { const cursorService = new CursorService({ dialogService, directoryService, - gioService: undefined, panelService, tabService, }); @@ -118,9 +113,6 @@ describe("CursorService", () => { const cursorService = new CursorService({ dialogService, directoryService, - gioService: undefined, - panelService: undefined, - tabService: undefined, }); cursorService.env = { PAGER: undefined }; @@ -154,7 +146,6 @@ describe("CursorService", () => { const cursorService = new CursorService({ dialogService, directoryService, - gioService: undefined, panelService, tabService, }); @@ -190,7 +181,6 @@ describe("CursorService", () => { const cursorService = new CursorService({ dialogService, directoryService, - gioService: undefined, panelService, tabService, }); diff --git a/src/app/Dialog/DialogService.js b/src/app/Dialog/DialogService.js index 871e62d..dc087d4 100644 --- a/src/app/Dialog/DialogService.js +++ b/src/app/Dialog/DialogService.js @@ -9,110 +9,111 @@ const { const noop = require("lodash/noop"); const { autoBind } = require("../Gjs/autoBind"); -/** - * @param {Window} win - */ -function DialogService(win, _Entry = Entry, _MessageDialog = MessageDialog) { - autoBind(this, DialogService.prototype, __filename); - - this.win = win; - this.Entry = _Entry; - this.MessageDialog = _MessageDialog; -} +class DialogService { + /** + * @param {Window} win + */ + constructor(win) { + autoBind(this, DialogService.prototype, __filename); + this.win = win; + this.Entry = Entry; + this.MessageDialog = MessageDialog; + } + + /** + * @param {string} text + * @param {(() => void)=} callback + */ + alert(text, callback = noop) { + const dialog = new this.MessageDialog({ + buttons: ButtonsType.CLOSE, + modal: true, + text: text, + transient_for: this.win, + window_position: WindowPosition.CENTER, + }); + + dialog.connect("response", () => { + dialog.destroy(); + callback(); + }); + + dialog.show(); + } + + /** + * @param {string} text + * @param {(result: boolean | null) => void} callback + */ + confirm(text, callback) { + const dialog = new this.MessageDialog({ + buttons: ButtonsType.YES_NO, + modal: true, + text: text, + transient_for: this.win, + window_position: WindowPosition.CENTER, + }); + + dialog.connect("response", (_, response) => { + dialog.destroy(); + + if (response === ResponseType.YES) { + callback(true); + return; + } + + if (response === ResponseType.NO) { + callback(false); + return; + } -/** - * @param {string} text - * @param {(() => void)=} callback - */ -DialogService.prototype.alert = function(text, callback = noop) { - const dialog = new this.MessageDialog({ - buttons: ButtonsType.CLOSE, - modal: true, - text: text, - transient_for: this.win, - window_position: WindowPosition.CENTER, - }); - - dialog.connect("response", () => { - dialog.destroy(); - callback(); - }); - - dialog.show(); -}; - -/** - * @param {string} text - * @param {(result: boolean | null) => void} callback - */ -DialogService.prototype.confirm = function(text, callback) { - const dialog = new this.MessageDialog({ - buttons: ButtonsType.YES_NO, - modal: true, - text: text, - transient_for: this.win, - window_position: WindowPosition.CENTER, - }); - - dialog.connect("response", (_, response) => { - dialog.destroy(); - - if (response === ResponseType.YES) { - callback(true); - return; - } - - if (response === ResponseType.NO) { - callback(false); - return; - } - - callback(null); - }); - - dialog.show(); -}; - -/** - * @param {string} text - * @param {string} initialValue - * @param {(text: string | null) => void} callback - */ -DialogService.prototype.prompt = function(text, initialValue, callback) { - const dialog = new this.MessageDialog({ - buttons: ButtonsType.OK_CANCEL, - modal: true, - text: text, - transient_for: this.win, - window_position: WindowPosition.CENTER, - }); - - const entry = new this.Entry({ text: initialValue }); - dialog.get_content_area().add(entry); - entry.connect("activate", () => { - const entryText = entry.text; - dialog.destroy(); - callback(entryText); - }); - - dialog.connect("response", (_, response) => { - const entryText = entry.text; - dialog.destroy(); - - if (response === ResponseType.OK) { + callback(null); + }); + + dialog.show(); + } + + /** + * @param {string} text + * @param {string} initialValue + * @param {(text: string | null) => void} callback + */ + prompt(text, initialValue, callback) { + const dialog = new this.MessageDialog({ + buttons: ButtonsType.OK_CANCEL, + modal: true, + text: text, + transient_for: this.win, + window_position: WindowPosition.CENTER, + }); + + const entry = new this.Entry({ text: initialValue }); + dialog.get_content_area().add(entry); + entry.connect("activate", () => { + const entryText = entry.text; + dialog.destroy(); callback(entryText); - return; - } + }); - if (response === ResponseType.CANCEL) { - callback(null); - return; - } + dialog.connect("response", (_, response) => { + const entryText = entry.text; + dialog.destroy(); + + if (response === ResponseType.OK) { + callback(entryText); + return; + } - callback(null); - }); + if (response === ResponseType.CANCEL) { + callback(null); + return; + } - dialog.show_all(); -}; + callback(null); + }); + + dialog.show_all(); + } +} exports.DialogService = DialogService; diff --git a/src/app/Directory/Directory.js b/src/app/Directory/Directory.js index 2bcae28..39ada91 100644 --- a/src/app/Directory/Directory.js +++ b/src/app/Directory/Directory.js @@ -2,7 +2,6 @@ const Gdk = imports.gi.Gdk; const { DragAction } = Gdk; const { TreeView } = imports.gi.Gtk; const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; const { connect } = require("inferno-mobx"); const assign = require("lodash/assign"); const isEqual = require("lodash/isEqual"); @@ -16,12 +15,13 @@ const { observable, } = require("mobx"); const { File } = require("../../domain/File/File"); +const { Tab } = require("../../domain/Tab/Tab"); +const { ActionService } = require("../Action/ActionService"); const { CursorService } = require("../Cursor/CursorService"); -const { DirectoryService } = require("../Directory/DirectoryService"); const { autoBind } = require("../Gjs/autoBind"); +const { h } = require("../Gjs/GtkInferno"); const { JobService } = require("../Job/JobService"); const { CHECKBOX, GICON, TEXT } = require("../ListStore/ListStore"); -const { OppositeService } = require("../Opposite/OppositeService"); const { PanelService } = require("../Panel/PanelService"); const { PlaceService } = require("../Place/PlaceService"); const { RefService } = require("../Ref/RefService"); @@ -44,423 +44,394 @@ const select = require("./select").default; * @property {any} which * * @typedef IProps - * @property {CursorService} cursorService - * @property {DirectoryService} directoryService - * @property {JobService} jobService + * @property {CursorService?} [cursorService] + * @property {JobService?} [jobService] * @property {number} panelId - * @property {OppositeService} oppositeService - * @property {PanelService} panelService - * @property {PlaceService} placeService - * @property {RefService} refService - * @property {SelectionService} selectionService - * @property {TabService} tabService - * @property {WindowService} windowService + * @property {PanelService?} [panelService] + * @property {PlaceService?} [placeService] + * @property {RefService?} [refService] + * @property {SelectionService?} [selectionService] + * @property {TabService?} [tabService] + * @property {WindowService?} [windowService] * - * @param {IProps} props + * @extends Component */ -function Directory(props) { - Component.call(this, props); - autoBind(this, Directory.prototype, __filename); - - extendObservable(this, { - cols: computed(this.getCols), - files: computed(this.getFiles), - node: observable.ref(undefined), - ref: action(this.ref), - tab: computed(this.getTab), - tabId: computed(this.getTabId), - }); - - this.unsubscribeUpdate = autorun(this.focusIfActive); -} - -Directory.prototype = Object.create(Component.prototype); - -Directory.prototype.cols = [ - { - name: "isSelected", - title: null, - type: CHECKBOX, - }, - { title: null, name: "icon", type: GICON }, - { title: "Name", name: "filename", type: TEXT, expand: true }, - { title: "Ext", name: "ext", type: TEXT, min_width: 50 }, - { title: "Size", name: "size", type: TEXT, min_width: 55 }, - { title: "Date", name: "mtime", type: TEXT, min_width: 125 }, - { title: "Attr", name: "mode", type: TEXT, min_width: 45 }, -]; - -/** @type {{ grab_focus(): void }}} */ -Directory.prototype.node = undefined; - -/** @type {IProps} */ -Directory.prototype.props = undefined; - -/** @type {{ cursor: number, location: string, selected: number[], sortedBy: string }} */ -Directory.prototype.tab = undefined; - -/** @type {number} */ -Directory.prototype.tabId = undefined; +class Directory extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + + /** + * @type {{ [key: number]: { ctrl?: () => void, default?: () => void } }} + */ + this.handlers = {}; + + /** @type {TreeView}} */ + this.node = (/** @type {any} */ (undefined)); + + /** @type {Tab} */ + this.tab = (/** @type {any} */ (undefined)); + + this.tabId = 0; + + autoBind(this, Directory.prototype, __filename); + + extendObservable(this, { + cols: computed(this.getCols), + files: computed(this.getFiles), + node: observable.ref(undefined), + ref: action(this.ref), + tab: computed(this.getTab), + tabId: computed(this.getTabId), + }); + + this.unsubscribeUpdate = autorun(this.focusIfActive); + } -Directory.prototype.componentWillUnmount = function() { - this.unsubscribeUpdate(); -}; + componentDidMount() { + const { get } = + /** @type {ActionService} */ (this.props.actionService); + + this.handlers = { + [Gdk.KEY_BackSpace]: { none: get("panelService.levelUp") }, + [Gdk.KEY_F2]: { none: get("windowService.refresh") }, + [Gdk.KEY_F3]: { none: get("cursorService.view") }, + [Gdk.KEY_F4]: { none: get("cursorService.edit") }, + [Gdk.KEY_F5]: { none: get("oppositeService.cp") }, + [Gdk.KEY_F6]: { none: get("oppositeService.mv") }, + [Gdk.KEY_F7]: { none: get("directoryService.mkdir") }, + [Gdk.KEY_F8]: { none: get("selectionService.rm") }, + [Gdk.KEY_a]: { ctrl: get("selectionService.selectAll") }, + [Gdk.KEY_b]: { ctrl: get("windowService.showHidSys") }, + [Gdk.KEY_c]: { ctrl: get("selectionService.copy") }, + [Gdk.KEY_d]: { ctrl: get("selectionService.deselectAll") }, + [Gdk.KEY_I]: { ctrl: get("selectionService.invert") }, + [Gdk.KEY_j]: { ctrl: get("jobService.list") }, + [Gdk.KEY_l]: { ctrl: get("panelService.ls") }, + [Gdk.KEY_t]: { ctrl: get("panelService.createTab") }, + [Gdk.KEY_v]: { ctrl: get("directoryService.paste") }, + [Gdk.KEY_w]: { ctrl: get("panelService.removeTab") }, + [Gdk.KEY_x]: { ctrl: get("selectionService.cut") }, + }; + } -Directory.prototype.getTabId = function() { - return this.props.panelService.entities[this.props.panelId].activeTabId; -}; + componentWillUnmount() { + this.unsubscribeUpdate(); + } -Directory.prototype.getTab = function() { - return this.props.tabService.entities[this.tabId]; -}; + getTabId() { + const { getActiveTabId } = + /** @type {PanelService} */ (this.props.panelService); -Directory.prototype.getFiles = function() { - return this.props.tabService.visibleFiles[this.tabId]; -}; + return getActiveTabId(this.props.panelId); + } -Directory.prototype.focusIfActive = function() { - const isActive = this.props.panelService.activeId === this.props.panelId; + getTab() { + const { entities } = + /** @type {TabService} */ (this.props.tabService); - if (isActive && this.node) { - this.node.grab_focus(); + return entities[this.tabId]; } -}; -/** - * @param {number} index - */ -Directory.prototype.handleActivated = function(index) { - const { cursorService, panelService } = this.props; - panelService.cursor(this.props.panelId, index); - cursorService.open(); -}; + getFiles() { + const { visibleFiles } = + /** @type {TabService} */ (this.props.tabService); -/** - * @param {string} colName - */ -Directory.prototype.handleClicked = function(colName) { - this.props.tabService.sorted({ - by: colName, - tabId: this.tabId, - }); -}; - -/** - * @param {{ index: number, mouseEvent?: any }} ev - */ -Directory.prototype.handleCursor = function(ev) { - const { index, mouseEvent } = ev; - const { panelService, selectionService } = this.props; + return visibleFiles[this.tabId]; + } - panelService.cursor(this.props.panelId, index); + focusIfActive() { + const { activeId } = + /** @type {PanelService} */ (this.props.panelService); - if (mouseEvent) { - const button = mouseEvent.get_button()[1]; + const isActive = activeId === this.props.panelId; - if (button === Gdk.BUTTON_SECONDARY) { - selectionService.menu({ mouseEvent }); + if (isActive && this.node) { + this.node.grab_focus(); } } -}; - -/** - * @param {any} _node - * @param {any} _dragContext - * @param {{ set_uris(uris: string[]): void }} selectionData - */ -Directory.prototype.handleDrag = function(_node, _dragContext, selectionData) { - const uris = this.props.selectionService.getUris(); - selectionData.set_uris(uris); -}; - -/** - * @param {any} _node - * @param {{ get_selected_action(): number }} dragContext - * @param {number} _x - * @param {number} _y - * @param {{ get_uris(): string[] }} selectionData - */ -Directory.prototype.handleDrop = function( - _node, - dragContext, - _x, - _y, - selectionData, -) { - const { run } = this.props.jobService; - const { refresh } = this.props.windowService; - - const selectedAction = dragContext.get_selected_action(); - const uris = selectionData.get_uris(); - const { location } = this.tab; - - run( - { - destUri: location, - type: selectedAction === DragAction.MOVE ? "mv" : "cp", - uris, - }, - refresh, - ); -}; - -/** - * @param {KeyPressEvent} ev - */ -Directory.prototype.handleKeyPressEvent = function(ev) { - const { cursor, selected } = this.tab; - const state = { - cursor: cursor, - indices: range(0, this.getFiles().length), - limit: ev.limit, - selected: selected, - top: ev.top, - }; + /** + * @param {number} index + */ + handleActivated(index) { + const { open } = + /** @type {CursorService} */ (this.props.cursorService); - const nextState = select(state, ev); + const { cursor } = + /** @type {PanelService} */ (this.props.panelService); - if (state !== nextState) { - this.handleCursor({ index: nextState.cursor }); - - if (!isEqual(state.selected.slice(), nextState.selected.slice())) { - this.props.tabService.selected(this.tabId, nextState.selected); - } + cursor(this.props.panelId, index); + open(); + } - return true; + /** + * @param {string} colName + */ + handleClicked(colName) { + const { sorted } = + /** @type {TabService} */ (this.props.tabService); + + sorted({ + by: colName, + tabId: this.tabId, + }); } - const { - cursorService, - directoryService, - jobService, - oppositeService, - panelId, - panelService, - placeService, - selectionService, - windowService, - } = this.props; - - switch (ev.which) { - case Gdk.KEY_BackSpace: - panelService.levelUp(panelId); - break; - - case Gdk.KEY_Menu: - selectionService.menu({ - keyEvent: ev.nativeEvent, - rect: ev.rect, - win: ev.win, - }); - break; - - case Gdk.KEY_ISO_Left_Tab: - case Gdk.KEY_Tab: - if (ev.ctrlKey && ev.shiftKey) { - panelService.prevTab(panelId); - } else if (ev.ctrlKey) { - panelService.nextTab(panelId); - } else { - panelService.toggleActive(); - } - return true; + /** + * @param {{ index: number, mouseEvent?: any }} ev + */ + handleCursor(ev) { + const { index, mouseEvent } = ev; - case Gdk.KEY_1: - if (ev.altKey) { - placeService.list(0); - return true; - } - break; + const { cursor } = + /** @type {PanelService} */ (this.props.panelService); - case Gdk.KEY_2: - if (ev.altKey) { - placeService.list(1); - return true; - } - break; + cursor(this.props.panelId, index); - case Gdk.KEY_F2: - windowService.refresh(); - break; + if (mouseEvent) { + const button = mouseEvent.get_button()[1]; - case Gdk.KEY_F3: - cursorService.view(); - break; + if (button === Gdk.BUTTON_SECONDARY) { + const { menu } = + /** @type {SelectionService} */ (this.props.selectionService); - case Gdk.KEY_F4: - cursorService.edit(); - break; - - case Gdk.KEY_F5: - oppositeService.cp(); - break; - - case Gdk.KEY_F6: - oppositeService.mv(); - break; + menu({ mouseEvent }); + } + } + } - case Gdk.KEY_F7: - directoryService.mkdir(); - break; + // tslint:disable:variable-name + /** + * @param {any} _node + * @param {any} _dragContext + * @param {{ set_uris(uris: string[]): void }} selectionData + */ + handleDrag(_node, _dragContext, selectionData) { + const { getUris } = + /** @type {SelectionService} */ (this.props.selectionService); + + const uris = getUris(); + selectionData.set_uris(uris); + } - case Gdk.KEY_F8: - selectionService.rm(); - break; + /** + * @param {any} _node + * @param {{ get_selected_action(): number }} dragContext + * @param {number} _x + * @param {number} _y + * @param {{ get_uris(): string[] }} selectionData + */ + handleDrop(_node, dragContext, _x, _y, selectionData) { + const { run } = + /** @type {JobService} */ (this.props.jobService); + + const { refresh } = + /** @type {WindowService} */ (this.props.windowService); + + const selectedAction = dragContext.get_selected_action(); + const uris = selectionData.get_uris(); + const { location } = this.tab; + + run({ + destUri: location, + type: selectedAction === DragAction.MOVE ? "mv" : "cp", + uris, + }, refresh); + } + // tslint-enable: variable-name - case Gdk.KEY_a: - if (ev.ctrlKey) { - selectionService.selectAll(); - } - break; + /** + * @param {KeyPressEvent} ev + */ + handleKeyPressEvent(ev) { + const tabService = + /** @type {TabService} */ (this.props.tabService); - case Gdk.KEY_b: - if (ev.ctrlKey) { - windowService.showHidSys(); - } - break; + const { cursor, selected } = this.tab; - case Gdk.KEY_c: - if (ev.ctrlKey) { - selectionService.copy(); - } - break; + const state = { + cursor: cursor, + indices: range(0, this.getFiles().length), + limit: ev.limit, + selected: selected, + top: ev.top, + }; - case Gdk.KEY_d: - if (ev.ctrlKey) { - selectionService.deselectAll(); - } - break; + const nextState = select(state, ev); - case Gdk.KEY_I: - if (ev.ctrlKey) { - selectionService.invert(); - } - break; + if (state !== nextState) { + this.handleCursor({ index: nextState.cursor }); - case Gdk.KEY_j: - if (ev.ctrlKey) { - jobService.list(); + if (!isEqual(state.selected.slice(), nextState.selected.slice())) { + tabService.selected(this.tabId, nextState.selected); } - break; - case Gdk.KEY_l: - if (ev.ctrlKey) { - panelService.ls(); - } - break; + return true; + } - case Gdk.KEY_t: - if (ev.ctrlKey) { - panelService.createTab(); - } - break; + switch (ev.which) { + case Gdk.KEY_Menu: + const { menu } = + /** @type {SelectionService} */ (this.props.selectionService); + + menu({ + keyEvent: ev.nativeEvent, + rect: ev.rect, + win: ev.win, + }); + break; + + case Gdk.KEY_ISO_Left_Tab: + case Gdk.KEY_Tab: + const { nextTab, prevTab, toggleActive } = + /** @type {PanelService} */ (this.props.panelService); + + if (ev.ctrlKey && ev.shiftKey) { + prevTab(); + } else if (ev.ctrlKey) { + nextTab(); + } else { + toggleActive(); + } + return true; - case Gdk.KEY_v: - if (ev.ctrlKey) { - directoryService.paste(); - } - break; + case Gdk.KEY_1: + if (ev.altKey) { + const { list } = + /** @type {PlaceService} */ (this.props.placeService); + + list(0); + return true; + } + break; + + case Gdk.KEY_2: + if (ev.altKey) { + const { list } = + /** @type {PlaceService} */ (this.props.placeService); + + list(1); + return true; + } + break; + + default: + const handler = this.handlers[ev.which]; + + if (handler && !ev.ctrlKey && handler.default) { + handler.default(); + return true; + } + + if (handler && ev.ctrlKey && handler.ctrl) { + handler.ctrl(); + return true; + } + } - case Gdk.KEY_w: - if (ev.ctrlKey) { - panelService.removeTab(); - } - break; + return false; + } - case Gdk.KEY_x: - if (ev.ctrlKey) { - selectionService.cut(); - } - break; + /** + * @param {TreeView} node + */ + handleLayout(node) { + const { set } = /** @type {RefService} */ (this.props.refService); + set("panel" + this.props.panelId)(node); + this.focusIfActive(); } - return false; -}; + /** + * @param {number} index + */ + handleSelected(index) { + let { selected } = this.tab; + selected = + selected.indexOf(index) === -1 + ? selected.concat(index) + : selected.filter(x => x !== index); -/** - * @param {TreeView} node - */ -Directory.prototype.handleLayout = function(node) { - this.props.refService.set("panel" + this.props.panelId)(node); - this.focusIfActive(); -}; + const panelService = + /** @type {PanelService} */ (this.props.panelService); -/** - * @param {number} index - */ -Directory.prototype.handleSelected = function(index) { - let { selected } = this.tab; + panelService.selected(this.props.panelId, selected); + } - selected = - selected.indexOf(index) === -1 - ? selected.concat(index) - : selected.filter(x => x !== index); + /** + * @param {{ name: string, title: string | null }} col + */ + prefixSort(col) { + const { sortedBy } = this.tab; - this.props.panelService.selected(this.props.panelId, selected); -}; + if (col.name === sortedBy) { + return assign({}, col, { title: "↑" + col.title }); + } -/** - * @param {{ name: string, title: string }} col - */ -Directory.prototype.prefixSort = function(col) { - const { sortedBy } = this.tab; + if ("-" + col.name === sortedBy) { + return assign({}, col, { title: "↓" + col.title }); + } - if (col.name === sortedBy) { - return assign({}, col, { title: "↑" + col.title }); + return col; } - if ("-" + col.name === sortedBy) { - return assign({}, col, { title: "↓" + col.title }); + getCols() { + return Directory.prototype.cols + .map(this.prefixSort) + .map(col => assign({}, col, { + on_clicked: () => this.handleClicked(col.name), + on_toggled: col.name === "isSelected" ? this.handleSelected : undefined, + })); } - return col; -}; + /** + * @param {TreeView} node + */ + ref(node) { + this.node = node; + } -Directory.prototype.getCols = function() { - return Directory.prototype.cols.map(this.prefixSort).map(( - /** @type {any} */ col, - ) => - assign({}, col, { - on_clicked: () => this.handleClicked(col.name), - on_toggled: col.name === "isSelected" ? this.handleSelected : undefined, - }), - ); -}; + render() { + const { cursor } = this.tab; + return ( + h("tree-view", { + activatedCallback: this.handleActivated, + cols: this.cols, + cursor, + cursorCallback: this.handleCursor, + dragAction: DragAction.COPY + DragAction.MOVE, + keyPressEventCallback: this.handleKeyPressEvent, + layoutCallback: this.handleLayout, + on_drag_data_get: this.handleDrag, + on_drag_data_received: this.handleDrop, + ref: this.ref, + }, h(DirectoryFiles, { tabId: this.tabId })) + ); + } +} -/** - * @param {TreeView} node - */ -Directory.prototype.ref = function(node) { - this.node = node; -}; - -Directory.prototype.render = function() { - const { cursor } = this.tab; - - return ( - h("tree-view", { - activatedCallback: this.handleActivated, - cols: this.cols, - cursor, - cursorCallback: this.handleCursor, - dragAction: DragAction.COPY + DragAction.MOVE, - keyPressEventCallback: this.handleKeyPressEvent, - layoutCallback: this.handleLayout, - on_drag_data_get: this.handleDrag, - on_drag_data_received: this.handleDrop, - ref: this.ref, - }, h(DirectoryFiles, { tabId: this.tabId })) - ); -}; +Directory.prototype.cols = [ + { + name: "isSelected", + title: null, + type: CHECKBOX, + }, + { title: null, name: "icon", type: GICON }, + { title: "Name", name: "filename", type: TEXT, expand: true }, + { title: "Ext", name: "ext", type: TEXT, min_width: 50 }, + { title: "Size", name: "size", type: TEXT, min_width: 55 }, + { title: "Date", name: "mtime", type: TEXT, min_width: 125 }, + { title: "Attr", name: "mode", type: TEXT, min_width: 45 }, +]; exports.Directory = Directory; exports.default = connect([ + "actionService", "cursorService", - "directoryService", "jobService", - "oppositeService", "panelService", "placeService", "refService", diff --git a/src/app/Directory/Directory.test.js b/src/app/Directory/Directory.test.js index 79a1324..ea3a0dc 100644 --- a/src/app/Directory/Directory.test.js +++ b/src/app/Directory/Directory.test.js @@ -13,9 +13,7 @@ describe("Directory", () => { const panelService = { activeId: 0, - entities: { - "0": { activeTabId: 0 }, - }, + getActiveTabId: () => 0, }; /** @type {any} */ @@ -55,9 +53,7 @@ describe("Directory", () => { /** @type {any} */ const panelService = { activeId: 0, - entities: { - "0": { activeTabId: 0 }, - }, + getActiveTabId: () => 0, }; /** @type {any} */ @@ -68,17 +64,9 @@ describe("Directory", () => { }; const instance = new Directory({ - cursorService: undefined, - directoryService: undefined, - jobService: undefined, - oppositeService: undefined, panelId: 0, panelService, - placeService: undefined, - refService: undefined, - selectionService: undefined, tabService, - windowService: undefined, }); tabService.entities[0].sortedBy = "ext"; @@ -105,17 +93,8 @@ describe("Directory", () => { }); const instance = new Directory({ - cursorService: undefined, - directoryService: undefined, - jobService: undefined, - oppositeService: undefined, panelId: 0, panelService, - placeService: undefined, - refService: undefined, - selectionService: undefined, - tabService: undefined, - windowService: undefined, }); panelService.activeId = 0; @@ -144,16 +123,8 @@ describe("Directory", () => { const instance = new Directory({ cursorService, - directoryService: undefined, - jobService: undefined, - oppositeService: undefined, panelId: 0, panelService, - placeService: undefined, - refService: undefined, - selectionService: undefined, - tabService: undefined, - windowService: undefined, }); instance.handleActivated(2); @@ -167,9 +138,7 @@ describe("Directory", () => { const panelService = { activeId: 0, cursor: createSpy().andReturn(undefined), - entities: { - "0": { activeTabId: 0 }, - }, + getActiveTabId: () => 0, selected: createSpy().andReturn(undefined), }; @@ -184,17 +153,9 @@ describe("Directory", () => { }; const instance = new Directory({ - cursorService: undefined, - directoryService: undefined, - jobService: undefined, - oppositeService: undefined, panelId: 0, panelService, - placeService: undefined, - refService: undefined, - selectionService: undefined, tabService, - windowService: undefined, }); instance.handleCursor({ index: 1 }); @@ -210,9 +171,7 @@ describe("Directory", () => { /** @type {any} */ const panelService = { activeId: 0, - entities: { - "0": { activeTabId: 0 }, - }, + getActiveTabId: () => 0, }; /** @type {any} */ @@ -221,17 +180,9 @@ describe("Directory", () => { }; const instance = new Directory({ - cursorService: undefined, - directoryService: undefined, - jobService: undefined, - oppositeService: undefined, panelId: 0, panelService, - placeService: undefined, - refService: undefined, - selectionService: undefined, tabService, - windowService: undefined, }); instance.handleClicked("ext"); diff --git a/src/app/Directory/DirectoryFile.js b/src/app/Directory/DirectoryFile.js index 4cb414b..2211702 100644 --- a/src/app/Directory/DirectoryFile.js +++ b/src/app/Directory/DirectoryFile.js @@ -1,7 +1,7 @@ const { FileType } = imports.gi.Gio; const GLib = imports.gi.GLib; const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; +const { h } = require("../Gjs/GtkInferno"); const { connect } = require("inferno-mobx"); const { File } = require("../../domain/File/File"); const { autoBind } = require("../Gjs/autoBind"); @@ -12,80 +12,76 @@ const formatSize = require("../Size/formatSize").default; * @property {File} file * @property {boolean} isSelected * - * @param {IProps} props + * @extends Component */ -function DirectoryFile(props) { - Component.call(this, props); - autoBind(this, DirectoryFile.prototype, __filename); -} - -DirectoryFile.prototype = Object.create(Component.prototype); +class DirectoryFile extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + autoBind(this, DirectoryFile.prototype, __filename); + } -/** @type {IProps} */ -DirectoryFile.prototype.props = undefined; + name() { + const file = this.props.file; + let filename = file.name; + let ext = ""; -DirectoryFile.prototype.name = function() { - const file = this.props.file; - let filename = file.name; - let ext = ""; + const matches = /^(.+)\.(.*?)$/.exec(file.name); - const matches = /^(.+)\.(.*?)$/.exec(file.name); + if (file.fileType !== FileType.DIRECTORY && file.name !== ".." && matches) { + filename = matches[1]; + ext = matches[2]; + } - if (file.fileType !== FileType.DIRECTORY && file.name !== ".." && matches) { - filename = matches[1]; - ext = matches[2]; + if (file.fileType === FileType.DIRECTORY) { + filename = "[" + file.name + "]"; + } + return [filename, ext]; } - if (file.fileType === FileType.DIRECTORY) { - filename = "[" + file.name + "]"; + size() { + const file = this.props.file; + return file.fileType === FileType.DIRECTORY ? "" : formatSize(file.size); } - return [filename, ext]; -}; - -DirectoryFile.prototype.size = function() { - const file = this.props.file; - return file.fileType === FileType.DIRECTORY ? "" : formatSize(file.size); -}; - -DirectoryFile.prototype.mtime = function() { - const time = this.props.file.modificationTime; - const date = new Date(time * 1000); - - const month = ("00" + (date.getMonth() + 1)).slice(-2); - const day = ("00" + date.getDate()).slice(-2); - const year = ("0000" + date.getFullYear()).slice(-4); - const hours = ("00" + date.getHours()).slice(-2); - const minutes = ("00" + date.getMinutes()).slice(-2); + mtime() { + const time = this.props.file.modificationTime; + const date = new Date(time * 1000); - return [month, day, year].join("/") + " " + [hours, minutes].join(":"); -}; + const month = ("00" + (date.getMonth() + 1)).slice(-2); + const day = ("00" + date.getDate()).slice(-2); + const year = ("0000" + date.getFullYear()).slice(-4); + const hours = ("00" + date.getHours()).slice(-2); + const minutes = ("00" + date.getMinutes()).slice(-2); -/** - * @param {string} input - */ -DirectoryFile.prototype.shouldSearchSkip = function(input) { - return !GLib.pattern_match_simple( - input.toLowerCase() + "*", - this.props.file.name.toLowerCase(), - ); -}; + return [month, day, year].join("/") + " " + [hours, minutes].join(":"); + } -DirectoryFile.prototype.render = function() { - const { file, isSelected } = this.props; - const [filename, ext] = this.name(); + /** + * @param {string} input + */ + shouldSearchSkip(input) { + return !GLib.pattern_match_simple(input.toLowerCase() + "*", this.props.file.name.toLowerCase()); + } - return h("stub", { - ext, - filename, - icon: file, - isSelected, - mode: file.mode, - mtime: this.mtime(), - shouldSearchSkip: this.shouldSearchSkip, - size: this.size(), - }); -}; + render() { + const { file, isSelected } = this.props; + const [filename, ext] = this.name(); + + return h("stub", { + ext, + filename, + icon: file, + isSelected, + mode: file.mode, + mtime: this.mtime(), + shouldSearchSkip: this.shouldSearchSkip, + size: this.size(), + }); + } +} exports.DirectoryFile = DirectoryFile; exports.default = connect([])(DirectoryFile); diff --git a/src/app/Directory/DirectoryFile.test.js b/src/app/Directory/DirectoryFile.test.js index 7725dff..1a2522a 100644 --- a/src/app/Directory/DirectoryFile.test.js +++ b/src/app/Directory/DirectoryFile.test.js @@ -1,25 +1,16 @@ const { FileType } = imports.gi.Gio; const expect = require("expect"); -const h = require("inferno-hyperscript").default; const { File } = require("../../domain/File/File"); -const { DirectoryFile } = require("./DirectoryFile"); +const { h } = require("../Gjs/GtkInferno"); const { shallow } = require("../Test/Test"); const { TreeView } = require("../TreeView/TreeView"); +const { DirectoryFile } = require("./DirectoryFile"); describe("DirectoryFile", () => { it("renders without crashing", () => { - const file = { - fileType: FileType.REGULAR, - icon: "some gio icon", - iconType: "GICON", - modificationTime: 1490397889, - name: "foo.bar", - size: 1000, - }; - shallow( h(DirectoryFile, { - file, + file: new File(), isSelected: false, }), ); diff --git a/src/app/Directory/DirectoryFiles.js b/src/app/Directory/DirectoryFiles.js index fea3b13..190ff12 100644 --- a/src/app/Directory/DirectoryFiles.js +++ b/src/app/Directory/DirectoryFiles.js @@ -1,8 +1,8 @@ const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; const { connect } = require("inferno-mobx"); const { File } = require("../../domain/File/File"); const { autoBind } = require("../Gjs/autoBind"); +const { h } = require("../Gjs/GtkInferno"); const { TabService } = require("../Tab/TabService"); const { TreeViewBody } = require("../TreeView/TreeViewBody"); const { DirectoryFile } = require("./DirectoryFile"); @@ -10,40 +10,38 @@ const { DirectoryFile } = require("./DirectoryFile"); /** * @typedef IProps * @property {number} tabId - * @property {TabService} tabService + * @property {TabService?} [tabService] * - * @param {IProps} props + * @extends Component */ -function DirectoryFiles(props) { - Component.call(this, props); - autoBind(this, DirectoryFiles.prototype, __filename); -} - -DirectoryFiles.prototype = Object.create(Component.prototype); - -/** @type {IProps} */ -DirectoryFiles.prototype.props = undefined; - -DirectoryFiles.prototype.render = function() { - const tabId = this.props.tabId; - const { entities, visibleFiles } = this.props.tabService; - - const files = visibleFiles[tabId]; - const selected = entities[tabId].selected; - - return ( - h(TreeViewBody, - files.map((file, index) => { +class DirectoryFiles extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + autoBind(this, DirectoryFiles.prototype, __filename); + } + + render() { + const tabId = this.props.tabId; + const { entities, visibleFiles } = + /** @type {TabService} */ (this.props.tabService); + + const files = visibleFiles[tabId]; + const selected = entities[tabId].selected; + + return ( + h(TreeViewBody, files.map((file, index) => { return h(DirectoryFile, { file, isSelected: selected.indexOf(index) !== -1, key: file.name, }); - }), - ) - ); -}; + })) + ); + } +} exports.DirectoryFiles = DirectoryFiles; - exports.default = connect(["tabService"])(DirectoryFiles); diff --git a/src/app/Directory/DirectoryFiles.test.js b/src/app/Directory/DirectoryFiles.test.js index f68afc7..c4103bb 100644 --- a/src/app/Directory/DirectoryFiles.test.js +++ b/src/app/Directory/DirectoryFiles.test.js @@ -1,10 +1,4 @@ -const expect = require("expect"); -const h = require("inferno-hyperscript").default; -const assign = require("lodash/assign"); -const { observable } = require("mobx"); -const { createSpy } = expect; const { DirectoryFiles } = require("./DirectoryFiles"); -const { shallow } = require("../Test/Test"); describe("DirectoryFiles", () => { it("renders without crashing", () => { diff --git a/src/app/Directory/DirectoryService.js b/src/app/Directory/DirectoryService.js index cecade9..50b8add 100644 --- a/src/app/Directory/DirectoryService.js +++ b/src/app/Directory/DirectoryService.js @@ -11,11 +11,11 @@ const { PanelService } = require("../Panel/PanelService"); class DirectoryService { /** * @typedef IProps - * @property {ClipboardService} clipboardService - * @property {DialogService} dialogService - * @property {GioService} gioService - * @property {JobService} jobService - * @property {PanelService} panelService + * @property {ClipboardService?} [clipboardService] + * @property {DialogService?} [dialogService] + * @property {GioService?} [gioService] + * @property {JobService?} [jobService] + * @property {PanelService?} [panelService] * * @param {IProps} props */ @@ -36,16 +36,28 @@ class DirectoryService { return; } - const { dialogService, gioService } = this.props; + const { alert } = + /** @type {DialogService} */ (this.props.dialogService); + + const { spawn } = + /** @type {GioService} */ (this.props.gioService); + const location = this.getLocation(); if (location.indexOf("file:///") !== 0) { - dialogService.alert("Operation not supported."); + alert("Operation not supported."); return; } - gioService.spawn({ - argv: GLib.shell_parse_argv(cmd)[1], + const argv = GLib.shell_parse_argv(cmd)[1]; + + if (!argv) { + this.terminal(); + return; + } + + spawn({ + argv, cwd: location.replace(/^file:\/\//, ""), }); } @@ -54,23 +66,31 @@ class DirectoryService { * Creates a child directory, prompting for name. */ mkdir() { - const { dialogService, gioService, panelService } = this.props; + const { prompt } = + /** @type {DialogService} */ (this.props.dialogService); + + const { mkdir } = + /** @type {GioService} */ (this.props.gioService); + + const { refresh } = + /** @type {PanelService} */ (this.props.panelService); + const location = this.getLocation(); - dialogService.prompt("Name of the new dir:", "", name => { + prompt("Name of the new dir:", "", name => { if (!name) { return; } const uri = this.getChild(location, name); - gioService.mkdir(uri, error => { + mkdir(uri, error => { if (error) { - dialogService.alert(error.message); + alert(error.message); return; } - panelService.refresh(); + refresh(); }); }); } @@ -80,10 +100,17 @@ class DirectoryService { * app marked the list written to clipboard. */ paste() { - const { paste } = this.props.clipboardService; - const { alert } = this.props.dialogService; - const { run } = this.props.jobService; - const { refresh } = this.props.panelService; + const { paste } = + /** @type {ClipboardService} */ (this.props.clipboardService); + + const { alert } = + /** @type {DialogService} */ (this.props.dialogService); + + const { run } = + /** @type {JobService} */ (this.props.jobService); + + const { refresh } = + /** @type {PanelService} */ (this.props.panelService); paste((_, text) => { if (!text) { @@ -118,16 +145,20 @@ class DirectoryService { * @param {(string[])=} argv */ terminal(argv) { - const { dialogService, gioService } = this.props; + const { spawn } = + /** @type {GioService} */ (this.props.gioService); const location = this.getLocation(); if (location.indexOf("file:///") !== 0) { - dialogService.alert("Operation not supported."); + const { alert } = + /** @type {DialogService} */ (this.props.dialogService); + + alert("Operation not supported."); return; } - gioService.spawn({ + spawn({ argv: ["x-terminal-emulator"].concat(argv || []), cwd: location.replace(/^file:\/\//, ""), }); @@ -138,23 +169,31 @@ class DirectoryService { * modification time. */ touch() { - const { dialogService, gioService, panelService } = this.props; + const { alert, prompt } = + /** @type {DialogService} */ (this.props.dialogService); + + const { touch } = + /** @type {GioService} */ (this.props.gioService); + + const { refresh } = + /** @type {PanelService} */ (this.props.panelService); + const location = this.getLocation(); - dialogService.prompt("Name of the new file:", "", name => { + prompt("Name of the new file:", "", name => { if (!name) { return; } const uri = this.getChild(location, name); - gioService.touch(uri, error => { + touch(uri, error => { if (error) { - dialogService.alert(error.message); + alert(error.message); return; } - panelService.refresh(); + refresh(); }); }); } @@ -181,8 +220,10 @@ class DirectoryService { * @private */ getLocation() { - const { panelService } = this.props; - const { location } = panelService.getActiveTab(); + const { getActiveTab } = + /** @type {PanelService} */ (this.props.panelService); + + const { location } = getActiveTab(); return location; } diff --git a/src/app/Directory/DirectoryService.test.js b/src/app/Directory/DirectoryService.test.js index a54cdb1..9224db3 100644 --- a/src/app/Directory/DirectoryService.test.js +++ b/src/app/Directory/DirectoryService.test.js @@ -32,10 +32,8 @@ describe("DirectoryService", () => { }; const directoryService = new DirectoryService({ - clipboardService: undefined, dialogService, gioService: new GioService(Gio), - jobService: undefined, panelService, }); @@ -59,10 +57,7 @@ describe("DirectoryService", () => { }; const directoryService = new DirectoryService({ - clipboardService: undefined, - dialogService: undefined, gioService: new GioService(Gio), - jobService: undefined, panelService, }); diff --git a/src/app/Directory/select.test.js b/src/app/Directory/select.test.js index fb01cf3..3c2082f 100644 --- a/src/app/Directory/select.test.js +++ b/src/app/Directory/select.test.js @@ -264,18 +264,18 @@ function Selected(selected) { } const xs = selected.split(" ").map(x => Number(x)); - const _selected = []; + const selectedArray = []; for (let i = 0; i < xs.length; i += 2) { const iFirst = xs[i]; const iLast = xs[i + 1]; for (let j = iFirst; j <= iLast; j++) { - _selected.push(j); + selectedArray.push(j); } } - return _selected; + return selectedArray; } /** diff --git a/src/app/Gio/GioService.js b/src/app/Gio/GioService.js index 11564fe..46ca012 100644 --- a/src/app/Gio/GioService.js +++ b/src/app/Gio/GioService.js @@ -1,3 +1,5 @@ +// tslint:disable:variable-name + const Gio = imports.gi.Gio; const { AppInfo, @@ -23,311 +25,313 @@ const { gioAsync } = require("./gioAsync"); /** * Let the front-end use drives. */ -function GioService(_Gio = Gio) { - autoBind(this, GioService.prototype, __filename); - - this.fileAttributes = "standard::*,time::*,unix::*"; - this.Gio = _Gio; -} - -/** - * Opens URIs in an application. - * - * @param {FileHandler} handler - * @param {string[]} uris - */ -GioService.prototype.launch = function(handler, uris) { - const gAppInfo = this.Gio.AppInfo.create_from_commandline( - handler.commandline, - null, - AppInfoCreateFlags.NONE, - ); +class GioService { + constructor(_Gio = Gio) { + autoBind(this, GioService.prototype, __filename); - const gFiles = uris.map(x => this.Gio.File.new_for_uri(x)); - gAppInfo.launch(gFiles, null); -}; + this.fileAttributes = "standard::*,time::*,unix::*"; + this.Gio = _Gio; + } -/** - * Lists every file in a given directory. - * - * @param {string} uri - * @param {(error: Error, files: File[]) => void} callback - */ -GioService.prototype.ls = function(uri, callback) { /** - * @type {File[]} + * Opens URIs in an application. + * + * @param {FileHandler} handler + * @param {string[]} uris */ - let files = []; + launch(handler, uris) { + const gAppInfo = this.Gio.AppInfo.create_from_commandline( + handler.commandline, + null, + AppInfoCreateFlags.NONE, + ); - const dir = this.Gio.File.new_for_uri(uri); - const parent = dir.get_parent(); + const gFiles = uris.map(x => this.Gio.File.new_for_uri(x)); + gAppInfo.launch(gFiles, null); + } /** - * @param {any} _callback + * Lists every file in a given directory. + * + * @param {string} uri + * @param {(error: Error, files: File[]) => void} callback */ - const handleRequest = _callback => { - gioAsync( - dir, - "query_info", - this.fileAttributes, - FileQueryInfoFlags.NONE, - PRIORITY_DEFAULT, - null, - _callback, + ls(uri, callback) { + /** + * @type {File[]} + */ + let files = []; + + const dir = this.Gio.File.new_for_uri(uri); + const parent = dir.get_parent(); + + /** + * @param {any} _callback + */ + const handleRequest = _callback => { + gioAsync( + dir, + "query_info", + this.fileAttributes, + FileQueryInfoFlags.NONE, + PRIORITY_DEFAULT, + null, + _callback, + ); + }; + + /** + * @param {FileInfo} selfInfo + * @param {any} _callback + */ + const handleSelf = (selfInfo, _callback) => { + const selfFile = mapGFileInfoToFile(selfInfo); + selfFile.displayName = "."; + selfFile.mountUri = this.getMountUri(dir); + selfFile.name = "."; + selfFile.uri = dir.get_uri(); + files = [selfFile]; + + if (!parent) { + _callback(null, null); + return; + } + + gioAsync( + parent, + "query_info", + this.fileAttributes, + FileQueryInfoFlags.NONE, + PRIORITY_DEFAULT, + null, + _callback, + ); + }; + + /** + * @param {FileInfo} parentInfo + * @param {any} _callback + */ + const handleParent = (parentInfo, _callback) => { + if (parentInfo) { + const parentFile = mapGFileInfoToFile(parentInfo); + parentFile.displayName = ".."; + parentFile.name = ".."; + parentFile.icon = "go-up"; + parentFile.iconType = "ICON_NAME"; + parentFile.uri = parent.get_uri(); + files = files.concat(parentFile); + } + + gioAsync( + dir, + "enumerate_children", + this.fileAttributes, + FileQueryInfoFlags.NONE, + PRIORITY_DEFAULT, + null, + _callback, + ); + }; + + /** + * @param {any} enumerator + * @param {any} _callback + */ + const handleChildren = (enumerator, _callback) => { + gioAsync( + enumerator, + "next_files", + MAXINT32, + PRIORITY_DEFAULT, + null, + _callback, + ); + }; + + /** + * @param {FileInfo[]} list + * @param {any} _callback + */ + const handleInfos = (list, _callback) => { + files = files.concat(list.map(mapGFileInfoToFile)); + _callback(null, files); + }; + + /** + * @param {FileInfo} gFileInfo + */ + const mapGFileInfoToFile = gFileInfo => { + const mode = gFileInfo.get_attribute_as_string("unix::mode"); + const name = gFileInfo.get_name(); + + /** @type {File} */ + const file = { + displayName: gFileInfo.get_display_name(), + fileType: gFileInfo.get_file_type(), + icon: gFileInfo.get_icon().to_string(), + iconType: "GICON", + mode: Number(mode) + .toString(8) + .slice(-4), + modificationTime: gFileInfo.get_modification_time().tv_sec, + mountUri: "", + name: name, + size: gFileInfo.get_size(), + uri: dir.get_child(name).get_uri(), + }; + + return file; + }; + + waterfall( + [handleRequest, handleSelf, handleParent, handleChildren, handleInfos], + callback, ); - }; + } /** - * @param {FileInfo} selfInfo - * @param {any} _callback + * Gets content type of a given file, and apps that can open it. + * + * @param {string} uri + * @param {(error: Error, result: { contentType: string, handlers: FileHandler[] }) => void} callback */ - const handleSelf = (selfInfo, _callback) => { - const selfFile = mapGFileInfoToFile(selfInfo); - selfFile.displayName = "."; - selfFile.mountUri = this.getMountUri(dir); - selfFile.name = "."; - selfFile.uri = dir.get_uri(); - files = [selfFile]; - - if (!parent) { - _callback(null, null); - return; - } + getHandlers(uri, callback) { + const file = this.Gio.File.new_for_uri(uri); gioAsync( - parent, + file, "query_info", this.fileAttributes, FileQueryInfoFlags.NONE, PRIORITY_DEFAULT, null, - _callback, + (/** @type {Error} */ error, /** @type {FileInfo} */ gFileInfo) => { + if (error) { + callback(error); + return; + } + + const contentType = gFileInfo.get_content_type(); + + /** @type {AppInfo[]} */ + const gAppInfos = this.Gio.AppInfo.get_all_for_type(contentType); + + const def = this.Gio.AppInfo.get_default_for_type(contentType, false); + if (def) { + gAppInfos.unshift(def); + } + + let handlers = gAppInfos.map(gAppInfo => { + const icon = gAppInfo.get_icon(); + return { + commandline: gAppInfo.get_commandline(), + displayName: gAppInfo.get_display_name(), + icon: icon ? icon.to_string() : null, + }; + }); + + handlers = uniqBy(handlers, x => x.commandline); + + callback(null, { + contentType, + handlers, + }); + }, ); - }; + } /** - * @param {FileInfo} parentInfo - * @param {any} _callback + * Returns root uri of the mount enclosing a given file. + * + * @param {GioFile} gFile */ - const handleParent = (parentInfo, _callback) => { - if (parentInfo) { - const parentFile = mapGFileInfoToFile(parentInfo); - parentFile.displayName = ".."; - parentFile.name = ".."; - parentFile.icon = "go-up"; - parentFile.iconType = "ICON_NAME"; - parentFile.uri = parent.get_uri(); - files = files.concat(parentFile); + getMountUri(gFile) { + let mount = null; + + try { + mount = gFile.find_enclosing_mount(null); + } catch (err) { + return "file:///"; } + return mount.get_root().get_uri(); + } + + /** + * Creates a directory. + * + * @param {string} uri + * @param {(error: Error) => void} callback + */ + mkdir(uri, callback) { gioAsync( - dir, - "enumerate_children", - this.fileAttributes, - FileQueryInfoFlags.NONE, + this.Gio.File.new_for_uri(uri), + "make_directory", PRIORITY_DEFAULT, null, - _callback, + callback, ); - }; + } /** - * @param {any} enumerator - * @param {any} _callback + * Creates a file. + * + * @param {string} uri + * @param {(error: Error) => void} callback */ - const handleChildren = (enumerator, _callback) => { + touch(uri, callback) { gioAsync( - enumerator, - "next_files", - MAXINT32, + this.Gio.File.new_for_uri(uri), + "create", + FileCreateFlags.NONE, PRIORITY_DEFAULT, null, - _callback, + callback, ); - }; - - /** - * @param {FileInfo[]} list - * @param {any} _callback - */ - const handleInfos = (list, _callback) => { - files = files.concat(list.map(mapGFileInfoToFile)); - _callback(null, files); - }; + } /** - * @param {FileInfo} gFileInfo + * Spawns a subprocess in a given working directory. + * + * @param {{ argv: string[], cwd?: string }} props */ - const mapGFileInfoToFile = gFileInfo => { - const mode = gFileInfo.get_attribute_as_string("unix::mode"); - const name = gFileInfo.get_name(); - - /** @type {File} */ - const file = { - displayName: gFileInfo.get_display_name(), - fileType: gFileInfo.get_file_type(), - icon: gFileInfo.get_icon().to_string(), - iconType: "GICON", - mode: Number(mode) - .toString(8) - .slice(-4), - modificationTime: gFileInfo.get_modification_time().tv_sec, - mountUri: "", - name: name, - size: gFileInfo.get_size(), - uri: dir.get_child(name).get_uri(), - }; - - return file; - }; - - waterfall( - [handleRequest, handleSelf, handleParent, handleChildren, handleInfos], - callback, - ); -}; - -/** - * Gets content type of a given file, and apps that can open it. - * - * @param {string} uri - * @param {(error: Error, result: { contentType: string, handlers: FileHandler[] }) => void} callback - */ -GioService.prototype.getHandlers = function(uri, callback) { - const file = this.Gio.File.new_for_uri(uri); - - gioAsync( - file, - "query_info", - this.fileAttributes, - FileQueryInfoFlags.NONE, - PRIORITY_DEFAULT, - null, - (/** @type {Error} */ error, /** @type {FileInfo} */ gFileInfo) => { - if (error) { - callback(error); - return; - } - - const contentType = gFileInfo.get_content_type(); - - /** @type {AppInfo[]} */ - const gAppInfos = this.Gio.AppInfo.get_all_for_type(contentType); - - const def = this.Gio.AppInfo.get_default_for_type(contentType, false); - if (def) { - gAppInfos.unshift(def); - } - - let handlers = gAppInfos.map(gAppInfo => { - const icon = gAppInfo.get_icon(); - return { - commandline: gAppInfo.get_commandline(), - displayName: gAppInfo.get_display_name(), - icon: icon ? icon.to_string() : null, - }; - }); - - handlers = uniqBy(handlers, x => x.commandline); - - callback(null, { - contentType, - handlers, - }); - }, - ); -}; + spawn(props) { + const { argv, cwd } = props; + const launcher = new this.Gio.SubprocessLauncher(); -/** - * Returns root uri of the mount enclosing a given file. - * - * @param {GioFile} gFile - */ -GioService.prototype.getMountUri = function(gFile) { - let mount = null; + if (cwd) { + launcher.set_cwd(cwd); + } - try { - mount = gFile.find_enclosing_mount(null); - } catch (err) { - return "file:///"; + launcher.set_flags(SubprocessFlags.NONE); + return launcher.spawnv(argv); } - return mount.get_root().get_uri(); -}; - -/** - * Creates a directory. - * - * @param {string} uri - * @param {(error: Error) => void} callback - */ -GioService.prototype.mkdir = function(uri, callback) { - gioAsync( - this.Gio.File.new_for_uri(uri), - "make_directory", - PRIORITY_DEFAULT, - null, - callback, - ); -}; - -/** - * Creates a file. - * - * @param {string} uri - * @param {(error: Error) => void} callback - */ -GioService.prototype.touch = function(uri, callback) { - gioAsync( - this.Gio.File.new_for_uri(uri), - "create", - FileCreateFlags.NONE, - PRIORITY_DEFAULT, - null, - callback, - ); -}; - -/** - * Spawns a subprocess in a given working directory. - * - * @param {{ argv: string[], cwd?: string }} props - */ -GioService.prototype.spawn = function(props) { - const { argv, cwd } = props; - const launcher = new this.Gio.SubprocessLauncher(); - - if (cwd) { - launcher.set_cwd(cwd); + /** + * Runs a subprocess and returns its output. + * + * @param {string[]} argv + * @param {(error: Error, stdout: string) => void} callback + */ + communicate(argv, callback = noop) { + const subprocess = new this.Gio.Subprocess({ + argv, + flags: SubprocessFlags.STDOUT_PIPE, + }); + + subprocess.init(null); + + gioAsync(subprocess, "communicate_utf8", null, null, ( + /** @type {any} */ + _, + /** @type {string[]} */ + result, + ) => { + const stdout = result[1]; + callback(null, stdout); + }); } - - launcher.set_flags(SubprocessFlags.NONE); - return launcher.spawnv(argv); -}; - -/** - * Runs a subprocess and returns its output. - * - * @param {string[]} argv - * @param {(error: Error, stdout: string) => void} callback - */ -GioService.prototype.communicate = function(argv, callback = noop) { - const subprocess = new this.Gio.Subprocess({ - argv, - flags: SubprocessFlags.STDOUT_PIPE, - }); - - subprocess.init(null); - - gioAsync(subprocess, "communicate_utf8", null, null, ( - /** @type {any} */ - _, - /** @type {string[]} */ - result, - ) => { - const stdout = result[1]; - callback(null, stdout); - }); -}; +} exports.GioService = GioService; diff --git a/src/app/Gio/Worker.js b/src/app/Gio/Worker.js index 6f57418..f196ba7 100644 --- a/src/app/Gio/Worker.js +++ b/src/app/Gio/Worker.js @@ -8,281 +8,280 @@ const { autoBind } = require("../Gjs/autoBind"); /** * Tasks intended to run in a separate process because they are heavy on IO or * GObject Introspection doesn't provide respective asynchronous methods. - * - * @param {WorkerProps} props - * @param {(event: WorkerError | WorkerProgress | WorkerSuccess) => void} emit */ -function Worker(props, emit, Gio = imports.gi.Gio) { - this.emit = emit; - this.Gio = Gio; - this.props = props; - autoBind(this, Worker.prototype, __filename); -} - -/** - * Performs the requested action. - */ -Worker.prototype.run = function() { - try { - this[this.props.type](); - } catch (error) { - this.emit({ - message: error.message, - stack: error.stack, - type: "error", - }); - return; +class Worker { + /** + * @param {WorkerProps} props + * @param {(event: WorkerError | WorkerProgress | WorkerSuccess) => void} emit + */ + constructor(props, emit, Gio = imports.gi.Gio) { + this.emit = emit; + this.Gio = Gio; + this.props = props; + autoBind(this, Worker.prototype, __filename); } - this.emit({ type: "success" }); -}; - -/** - * Copies sources to a destination directory. Recurses if a source is a - * directory. Splices the first URI component relative to the source if the - * source URI ends with a slash, or if there is only one source URI and the - * destination isn't an existing directory. - */ -Worker.prototype.cp = function() { - const data = this.prepare(); - - data.files.forEach((file, totalDoneCount) => { - if (file.gFileInfo.get_file_type() === FileType.DIRECTORY) { - this.cpDirNode(file); + /** + * Performs the requested action. + */ + run() { + try { + this[this.props.type](); + } catch (error) { + this.emit({ + message: error.message, + stack: error.stack, + type: "error", + }); return; } - const uri = file.gFile.get_uri(); - const dest = file.dest.get_uri(); - - /** @type {File} */ - const gFile = file.gFile; + this.emit({ type: "success" }); + } - gFile.copy( - file.dest, - FileCopyFlags.OVERWRITE + + /** + * Copies sources to a destination directory. Recurses if a source is a + * directory. Splices the first URI component relative to the source if the + * source URI ends with a slash, or if there is only one source URI and the + * destination isn't an existing directory. + */ + cp() { + const data = this.prepare(); + data.files.forEach((file, totalDoneCount) => { + if (file.gFileInfo.get_file_type() === FileType.DIRECTORY) { + this.cpDirNode(file); + return; + } + const uri = file.gFile.get_uri(); + const dest = file.dest.get_uri(); + /** @type {File} */ + const gFile = file.gFile; + gFile.copy( + file.dest, + FileCopyFlags.OVERWRITE + FileCopyFlags.NOFOLLOW_SYMLINKS + FileCopyFlags.ALL_METADATA, - null, - (doneSize, size) => { - this.emit({ - dest, - doneSize, - size, - totalCount: data.files.length, - totalDoneCount, - totalDoneSize: data.totalDoneSize + doneSize, - totalSize: data.totalSize, - type: "progress", - uri, - }); - }, - ); - - data.totalDoneSize += file.gFileInfo.get_size(); - }); -}; - -/** - * Moves sources to a destination directory. Uses cp followed by rm. - */ -Worker.prototype.mv = function() { - this.cp(); - this.rm(); -}; + null, + (doneSize, size) => { + this.emit({ + dest, + doneSize, + size, + totalCount: data.files.length, + totalDoneCount, + totalDoneSize: data.totalDoneSize + doneSize, + totalSize: data.totalSize, + type: "progress", + uri, + }); + }, + ); + + data.totalDoneSize += file.gFileInfo.get_size(); + }); + } -/** - * Deletes files. Recurses into directories. - */ -Worker.prototype.rm = function() { - /** @type {any[]} */ - const gFiles = this.props.uris.map(x => this.Gio.file_new_for_uri(x)); + /** + * Moves sources to a destination directory. Uses cp followed by rm. + */ + mv() { + this.cp(); + this.rm(); + } /** - * @type {any[]} + * Deletes files. Recurses into directories. */ - const files = gFiles.reduce((prev, gFile) => { - return prev.concat(this.flatten(gFile).files); - }, []); - - files.reverse(); - - files.forEach((file, totalDoneCount) => { - const uri = file.gFile.get_uri(); - file.gFile.delete(null); - - this.emit({ - dest: "", - doneSize: 0, - size: 0, - totalCount: files.length, - totalDoneCount, - totalDoneSize: 0, - totalSize: 0, - type: "progress", - uri, + rm() { + /** @type {any[]} */ + const gFiles = this.props.uris.map(x => this.Gio.file_new_for_uri(x)); + + /** + * @type {any[]} + */ + const files = gFiles.reduce((prev, gFile) => { + return prev.concat(this.flatten(gFile).files); + }, []); + + files.reverse(); + + files.forEach((file, totalDoneCount) => { + const uri = file.gFile.get_uri(); + file.gFile.delete(null); + + this.emit({ + dest: "", + doneSize: 0, + size: 0, + totalCount: files.length, + totalDoneCount, + totalDoneSize: 0, + totalSize: 0, + type: "progress", + uri, + }); }); - }); -}; + } -/** - * Traverses source URIs. Maps every source file to a destination file. - * Initializes fields to keep track of processed size. - */ -Worker.prototype.prepare = function() { - const { destUri, uris } = this.props; - - const dest = this.Gio.file_new_for_uri(destUri); - - const isDestExistingDir = - dest.query_exists(null) && - dest - .query_info("standard::*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null) - .get_file_type() === FileType.DIRECTORY; - - const willCreateDest = uris.length === 1 && !isDestExistingDir; - - /** @type {any[]} */ - const files = []; - - const data = { - files, - totalDoneSize: 0, - totalSize: 0, - }; - - for (const srcUri of uris) { - const src = this.Gio.file_new_for_uri(srcUri); - const srcName = src.get_basename(); - const splice = srcUri[srcUri.length - 1] === "/" || willCreateDest; - const uriData = this.flatten(src); - - uriData.files.forEach(file => { - if (!splice && !file.relativePath) { - file.destUri = dest.get_child(srcName).get_uri(); - file.dest = this.Gio.file_new_for_uri(file.destUri); - return; - } + /** + * Traverses source URIs. Maps every source file to a destination file. + * Initializes fields to keep track of processed size. + */ + prepare() { + const { destUri, uris } = this.props; - if (!splice && file.relativePath) { - file.destUri = dest - .get_child(srcName) - .get_child(file.relativePath) - .get_uri(); - file.dest = this.Gio.file_new_for_uri(file.destUri); - return; - } + const dest = this.Gio.file_new_for_uri(destUri); - if (splice && !file.relativePath) { - file.destUri = dest.get_uri(); - file.dest = dest; - return; - } + const isDestExistingDir = + dest.query_exists(null) && + dest + .query_info("standard::*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null) + .get_file_type() === FileType.DIRECTORY; - if (splice && file.relativePath) { - file.destUri = dest.get_child(file.relativePath).get_uri(); - file.dest = this.Gio.file_new_for_uri(file.destUri); - } - }); + const willCreateDest = uris.length === 1 && !isDestExistingDir; - data.files = data.files.concat(uriData.files); - data.totalSize += uriData.totalSize; - } + /** @type {any[]} */ + const files = []; - return data; -}; + const data = { + files, + totalDoneSize: 0, + totalSize: 0, + }; + + for (const srcUri of uris) { + const src = this.Gio.file_new_for_uri(srcUri); + const srcName = src.get_basename(); + const splice = srcUri[srcUri.length - 1] === "/" || willCreateDest; + const uriData = this.flatten(src); + + uriData.files.forEach(file => { + if (!splice && !file.relativePath) { + file.destUri = dest.get_child(srcName).get_uri(); + file.dest = this.Gio.file_new_for_uri(file.destUri); + return; + } + + if (!splice && file.relativePath) { + file.destUri = dest + .get_child(srcName) + .get_child(file.relativePath) + .get_uri(); + file.dest = this.Gio.file_new_for_uri(file.destUri); + return; + } + + if (splice && !file.relativePath) { + file.destUri = dest.get_uri(); + file.dest = dest; + return; + } + + if (splice && file.relativePath) { + file.destUri = dest.get_child(file.relativePath).get_uri(); + file.dest = this.Gio.file_new_for_uri(file.destUri); + } + }); + + data.files = data.files.concat(uriData.files); + data.totalSize += uriData.totalSize; + } -/** - * Creates a given directory if it doesn't exist. Copies source attributes - * to the destination. - * - * @param {any} file - */ -Worker.prototype.cpDirNode = function(file) { - if (!file.dest.query_exists(null)) { - file.dest.make_directory(null); + return data; } - file.gFile.copy_attributes(file.dest, FileCopyFlags.ALL_METADATA, null); -}; - -/** - * Given a point in a file hierarchy, finds all files and their total size. - * - * @param {File} gFile - */ -Worker.prototype.flatten = function(gFile) { - /** @type {any[]} */ - const files = []; + /** + * Creates a given directory if it doesn't exist. Copies source attributes + * to the destination. + * + * @param {any} file + */ + cpDirNode(file) { + if (!file.dest.query_exists(null)) { + file.dest.make_directory(null); + } - const data = { - files, - totalSize: 0, - }; + file.gFile.copy_attributes(file.dest, FileCopyFlags.ALL_METADATA, null); + } /** - * @param {any} x + * Given a point in a file hierarchy, finds all files and their total size. + * + * @param {File} gFile */ - const handleFile = x => { - data.files.push(x); - data.totalSize += x.gFileInfo.get_size(); + flatten(gFile) { + /** @type {any[]} */ + const files = []; - if (x.gFileInfo.get_file_type() === FileType.DIRECTORY) { - this.children(gFile, x.gFile).forEach(handleFile); - } - }; + const data = { + files, + totalSize: 0, + }; + + /** + * @param {any} x + */ + const handleFile = x => { + data.files.push(x); + data.totalSize += x.gFileInfo.get_size(); + + if (x.gFileInfo.get_file_type() === FileType.DIRECTORY) { + this.children(gFile, x.gFile).forEach(handleFile); + } + }; - /** @type {string} */ - const relativePath = null; + /** @type {string} */ + const relativePath = null; - const file = { - gFile: gFile, - gFileInfo: gFile.query_info( + const file = { + gFile: gFile, + gFileInfo: gFile.query_info( + "standard::*", + FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + null, + ), + relativePath, + }; + + handleFile(file); + + return data; + } + + /** + * For every child of a parent, gets Gio.File and Gio.FileInfo references + * and a path relative to the given ancestor. + * + * @param {File} ancestor + * @param {File} parent + */ + children(ancestor, parent) { + const enumerator = parent.enumerate_children( "standard::*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null, - ), - relativePath, - }; - - handleFile(file); + ); - return data; -}; + const files = []; -/** - * For every child of a parent, gets Gio.File and Gio.FileInfo references - * and a path relative to the given ancestor. - * - * @param {File} ancestor - * @param {File} parent - */ -Worker.prototype.children = function(ancestor, parent) { - const enumerator = parent.enumerate_children( - "standard::*", - FileQueryInfoFlags.NOFOLLOW_SYMLINKS, - null, - ); + while (true) { + const gFileInfo = enumerator.next_file(null); - const files = []; + if (!gFileInfo) { + break; + } - while (true) { - const gFileInfo = enumerator.next_file(null); + const gFile = parent.get_child(gFileInfo.get_name()); - if (!gFileInfo) { - break; + files.push({ + gFile, + gFileInfo, + relativePath: ancestor.get_relative_path(gFile), + }); } - const gFile = parent.get_child(gFileInfo.get_name()); - - files.push({ - gFile, - gFileInfo, - relativePath: ancestor.get_relative_path(gFile), - }); + return files; } - - return files; -}; +} exports.Worker = Worker; diff --git a/src/app/Gio/gioAsync.test.js b/src/app/Gio/gioAsync.test.js index 974ff05..a2cb206 100644 --- a/src/app/Gio/gioAsync.test.js +++ b/src/app/Gio/gioAsync.test.js @@ -27,35 +27,37 @@ describe("gioAsync", () => { }); function setup() { - function AsyncResult() { - this.type = "ASYNC_RESULT"; + class AsyncResult { + constructor() { + this.type = "ASYNC_RESULT"; + } } - /** - * @param {boolean} exists - */ - function GioFile(exists) { - this.make_directory_async = this.make_directory_async.bind(this); - this.make_directory_finish = this.make_directory_finish.bind(this); - this.exists = exists; - } + class GioFile { + /** + * @param {boolean} exists + */ + constructor(exists) { + this.exists = exists; - GioFile.prototype.make_directory_async = function() { - arguments[arguments.length - 1](null, new AsyncResult()); - }; + /** + * @param {AsyncResult} asyncResult + */ + this.make_directory_finish = (asyncResult) => { + expect(asyncResult.type).toBe("ASYNC_RESULT"); - /** - * @param {AsyncResult} asyncResult - */ - GioFile.prototype.make_directory_finish = function(asyncResult) { - expect(asyncResult.type).toBe("ASYNC_RESULT"); + if (this.exists) { + throw new Error("Directory exists."); + } else { + return true; + } + }; + } - if (this.exists) { - throw new Error("Directory exists."); - } else { - return true; + make_directory_async() { + arguments[arguments.length - 1](null, new AsyncResult()); } - }; + } return { GioFile: GioFile }; } diff --git a/src/app/Gjs/GtkDom.js b/src/app/Gjs/GtkDom.js index 3e5c18d..5320051 100644 --- a/src/app/Gjs/GtkDom.js +++ b/src/app/Gjs/GtkDom.js @@ -25,7 +25,7 @@ function removeAllChildren() { }); } -function GtkDom(GLib = imports.gi.GLib, Gtk = imports.gi.Gtk, _window = window) { +function GtkDom(GLib = imports.gi.GLib, Gtk = imports.gi.Gtk, win = window) { this.createElement = this.createElement.bind(this); this.domify = this.domify.bind(this); this.require = this.require.bind(this); @@ -35,7 +35,7 @@ function GtkDom(GLib = imports.gi.GLib, Gtk = imports.gi.Gtk, _window = window) /** @type {{ [key: string]: any }} */ this.Gtk = Gtk; - this.window = _window; + this.window = win; } /** @@ -160,12 +160,9 @@ GtkDom.prototype.domify = function(node) { const ownValue = getValue(this); try { - const _children = this.get_children() + children = this.get_children() .map(getValue) .filter((/** @type {any} */ x) => x !== ownValue); - if (_children.length) { - children = _children; - } } catch (err) { // Is not a container. } diff --git a/src/app/Gjs/GtkInferno.d.ts b/src/app/Gjs/GtkInferno.d.ts index 95590b7..13ecaac 100644 --- a/src/app/Gjs/GtkInferno.d.ts +++ b/src/app/Gjs/GtkInferno.d.ts @@ -1,21 +1,47 @@ import { VNode } from "inferno"; -type Component = (new (props: T) => any) | ((props: T) => any); +type Component = (new (props: T) => { render(): VNode }) | ((props: T) => VNode); -type Widget = new (...args: any[]) => T; +type Widget = new (...args: any[]) => ({ parent_instance: any } & T); + +export function connect(services: string): (component: T) => T; export function h( - component: Widget | Component, + component: Component | Widget, children?: Array ): VNode; export function h( - component: Widget | Component, - props: Partial, + component: Component | Widget, + props: Partial, children?: Array ): VNode; export function h( tag: "stub-box", children: Array +): VNode; + +export function h( + tag: "stub", + props: any, + children?: Array, +): VNode; + +// FIXME: Delete. +export function h( + tag: "menu-item-with-submenu", + props: any, + children: Array +): VNode; + +// FIXME: Delete. +export function h( + tag: "tree-view", + props: any, + children: VNode ): VNode; \ No newline at end of file diff --git a/src/app/Gjs/GtkInferno.js b/src/app/Gjs/GtkInferno.js index c7300be..3328cbf 100644 --- a/src/app/Gjs/GtkInferno.js +++ b/src/app/Gjs/GtkInferno.js @@ -1,4 +1,7 @@ const hyperscript = require("inferno-hyperscript").default; +const { connect } = require("inferno-mobx"); + +exports.connect = connect; /** * @see ./GtkInferno.d.ts diff --git a/src/app/Gjs/KeyListener.js b/src/app/Gjs/KeyListener.js index 91c2752..121b01c 100644 --- a/src/app/Gjs/KeyListener.js +++ b/src/app/Gjs/KeyListener.js @@ -2,82 +2,85 @@ const Gdk = imports.gi.Gdk; const { Event } = Gdk; const { autoBind } = require("./autoBind"); -/** - * @param {any} node - */ -function KeyListener(node) { - autoBind(this, KeyListener.prototype, __filename); - - this.node = node; - this.onKeyPress = undefined; - this.onKeyRelease = undefined; - this.pressed = {}; +class KeyListener { + /** + * @param {any} node + */ + constructor(node) { + autoBind(this, KeyListener.prototype, __filename); + this.node = node; + this.onKeyPress = undefined; + this.onKeyRelease = undefined; + this.pressed = {}; + this.node.connect("key-press-event", this.handleKeyPress); + this.node.connect("key-release-event", this.handleKeyRelease); + } - this.node.connect("key-press-event", this.handleKeyPress); - this.node.connect("key-release-event", this.handleKeyRelease); -} + /** + * @param {any} _ + * @param {Event} nativeEv + */ + handleKeyPress(_, nativeEv) { + const keyval = nativeEv.get_keyval()[1]; -/** - * @param {any} _ - * @param {Event} nativeEv - */ -KeyListener.prototype.handleKeyPress = function(_, nativeEv) { - const keyval = nativeEv.get_keyval()[1]; + if (!this.onKeyPress) { + this.pressed[keyval] = true; + return false; + } - if (!this.onKeyPress) { + const ev = new Ev(this.pressed, keyval, nativeEv); + const result = this.onKeyPress(ev); this.pressed[keyval] = true; - return false; + + return result; } - const ev = new Ev(this.pressed, keyval, nativeEv); - const result = this.onKeyPress(ev); - this.pressed[keyval] = true; + /** + * @param {any} _ + * @param {Event} nativeEv + */ + handleKeyRelease(_, nativeEv) { + const keyval = nativeEv.get_keyval()[1]; - return result; -}; -/** - * @param {any} _ - * @param {Event} nativeEv - */ -KeyListener.prototype.handleKeyRelease = function(_, nativeEv) { - const keyval = nativeEv.get_keyval()[1]; + if (!this.onKeyRelease) { + this.pressed[keyval] = false; + return false; + } - if (!this.onKeyRelease) { + const ev = new Ev(this.pressed, keyval, nativeEv); + const result = this.onKeyRelease(ev); this.pressed[keyval] = false; - return false; - } - const ev = new Ev(this.pressed, keyval, nativeEv); - const result = this.onKeyRelease(ev); - this.pressed[keyval] = false; + return result; + } - return result; -}; -/** - * @param {string} evName - * @param {(ev: Ev) => void} callback - */ -KeyListener.prototype.on = function(evName, callback) { - if (evName === "key-press-event") { - this.onKeyPress = callback; - } else { - this.onKeyRelease = callback; + /** + * @param {string} evName + * @param {(ev: Ev) => void} callback + */ + on(evName, callback) { + if (evName === "key-press-event") { + this.onKeyPress = callback; + } else { + this.onKeyRelease = callback; + } } -}; +} -exports.Ev = Ev; -/** - * @param {{ [key: number]: boolean }} pressed - * @param {number} which - * @param {Event} nativeEvent - */ -function Ev(pressed, which, nativeEvent) { - this.nativeEvent = nativeEvent; - this.which = which; - this.ctrlKey = pressed[Gdk.KEY_Control_L] || pressed[Gdk.KEY_Control_R]; - this.shiftKey = pressed[Gdk.KEY_Shift_L] || pressed[Gdk.KEY_Shift_R]; - this.altKey = pressed[Gdk.KEY_Alt_L] || pressed[Gdk.KEY_Alt_R]; - this.metaKey = pressed[Gdk.KEY_Meta_L] || pressed[Gdk.KEY_Meta_R]; +class Ev { + /** + * @param {{ [key: number]: boolean }} pressed + * @param {number} which + * @param {Event} nativeEvent + */ + constructor(pressed, which, nativeEvent) { + this.nativeEvent = nativeEvent; + this.which = which; + this.ctrlKey = pressed[Gdk.KEY_Control_L] || pressed[Gdk.KEY_Control_R]; + this.shiftKey = pressed[Gdk.KEY_Shift_L] || pressed[Gdk.KEY_Shift_R]; + this.altKey = pressed[Gdk.KEY_Alt_L] || pressed[Gdk.KEY_Alt_R]; + this.metaKey = pressed[Gdk.KEY_Meta_L] || pressed[Gdk.KEY_Meta_R]; + } } exports.default = KeyListener; diff --git a/src/app/Gjs/Require.js b/src/app/Gjs/Require.js index 261d165..586019a 100644 --- a/src/app/Gjs/Require.js +++ b/src/app/Gjs/Require.js @@ -1,548 +1,541 @@ const { FileQueryInfoFlags, FileType } = imports.gi.Gio; -function Require( - Gio = imports.gi.Gio, - GLib = imports.gi.GLib, - _imports = imports, - _window = window, -) { - this.Gio = Gio; - this.GLib = GLib; - this.imports = _imports; - this.window = _window; - - const keys = Object.getOwnPropertyNames(Require.prototype); - - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const val = this[key]; - - if (key !== "constructor" && typeof val === "function") { - this[key] = val.bind(this); +// tslint:disable:no-var-keyword +var Require = class { + // tslint:enable:no-var-keyword + constructor(Gio = imports.gi.Gio, GLib = imports.gi.GLib, imp = imports, win = window) { + this.Gio = Gio; + this.GLib = GLib; + this.imports = imp; + this.window = win; + + const keys = Object.getOwnPropertyNames(Require.prototype); + const self = /** @type {{ [key: string]: any }} */ (this); + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const val = self[key]; + + if (key !== "constructor" && typeof val === "function") { + self[key] = val.bind(self); + } } - } - /** - * Object where filenames of the modules that have ever been required are keys. - */ - this.filenames = {}; + /** + * Object where filenames of the modules that have ever been required are keys. + */ + this.filenames = {}; - /** - * Modules, indexed by filename. - */ - this.cache = {}; + /** + * Modules, indexed by filename. + */ + this.cache = {}; - /** - * Arrays of required modules, indexed by parent filename. - */ - this.dependencies = {}; + /** + * Arrays of required modules, indexed by parent filename. + */ + this.dependencies = {}; - /** - * File system polling interval. - */ - this.HMR_INTERVAL = 1000; + /** + * File system polling interval. + */ + this.HMR_INTERVAL = 1000; - /** - * Delay to make sure file has finished being written. - */ - this.HMR_TIMEOUT = 1000; + /** + * Delay to make sure file has finished being written. + */ + this.HMR_TIMEOUT = 1000; - /** - * Regular expression to get current module path from error stack. - */ - this.RE = /\n.*?@(.*?):/; + /** + * Regular expression to get current module path from error stack. + */ + this.RE = /\n.*?@(.*?):/; + + /** + * Creates function from string. Breaks coverage for the module defining it, + * so defined in `./Fun` that is required when `require` is available. + * + * @type {any} + */ + this.Fun = undefined; + } /** - * Creates function from string. Breaks coverage for the module defining it, - * so defined in `./Fun` that is required when `require` is available. + * Invalidates cache and calls a function when a module file is changed. * - * @type {any} + * @param {string} dirname + * @param {string} path + * @param {() => void} callback */ - this.Fun = undefined; -} + accept(dirname, path, callback) { + const filename = this.resolve(dirname, path); + let lastContents = {}; + let isVerifyingChange = false; -/** - * Invalidates cache and calls a function when a module file is changed. - * - * @param {string} dirname - * @param {string} path - * @param {() => void} callback - */ -Require.prototype.accept = function(dirname, path, callback) { - const filename = this.resolve(dirname, path); - let lastContents = {}; - let isVerifyingChange = false; - - const handleChanged = () => { - if (isVerifyingChange) { - return; - } + const handleChanged = () => { + if (isVerifyingChange) { + return; + } - isVerifyingChange = true; + isVerifyingChange = true; - this.GLib.timeout_add(this.GLib.PRIORITY_DEFAULT, this.HMR_TIMEOUT, () => { - this.flatten(filename).forEach(_filename => { - if (!isVerifyingChange) { - return; - } + this.GLib.timeout_add(this.GLib.PRIORITY_DEFAULT, this.HMR_TIMEOUT, () => { + this.flatten(filename).forEach(filenameToRead => { + if (!isVerifyingChange) { + return; + } - const contents = String(this.GLib.file_get_contents(_filename)[1]); + const contents = String( + this.GLib.file_get_contents(filenameToRead)[1], + ); - if (lastContents[_filename] !== contents) { - lastContents[_filename] = contents; + if (lastContents[filenameToRead] !== contents) { + lastContents[filenameToRead] = contents; - this.flatten(filename).forEach(__filename => { - delete this.cache[__filename]; - }); + this.flatten(filename).forEach(filenameToInvalidate => { + delete this.cache[filenameToInvalidate]; + }); - isVerifyingChange = false; - callback(); - } + isVerifyingChange = false; + callback(); + } + }); + + isVerifyingChange = false; + return false; }); + }; - isVerifyingChange = false; - return false; + this.flatten(filename).forEach(filenameToRead => { + lastContents[filenameToRead] = String(this.GLib.file_get_contents(filenameToRead)[1]); }); - }; - this.flatten(filename).forEach(_filename => { - lastContents[_filename] = String(this.GLib.file_get_contents(_filename)[1]); - }); - - this.GLib.timeout_add(this.GLib.PRIORITY_DEFAULT, this.HMR_INTERVAL, () => { - if (isVerifyingChange) { - return true; - } - - this.flatten(filename).forEach(_filename => { + this.GLib.timeout_add(this.GLib.PRIORITY_DEFAULT, this.HMR_INTERVAL, () => { if (isVerifyingChange) { - return; + return true; } - const contents = String(this.GLib.file_get_contents(_filename)[1]); + this.flatten(filename).forEach(filenameToRead => { + if (isVerifyingChange) { + return; + } - if (lastContents[_filename] !== contents) { - handleChanged(); - } + const contents = String(this.GLib.file_get_contents(filenameToRead)[1]); + + if (lastContents[filenameToRead] !== contents) { + handleChanged(); + } + }); + + return true; }); + } - return true; - }); -}; + /** + * Gets pathnames of a cached module and all its dependencies. + * + * @param {string} filename + */ + flatten(filename) { + let result = [filename]; + let nextResult = null; + let same = false; + + while (!same) { + nextResult = result + .reduce((prev, x) => prev.concat(this.dependencies[x]), result) + .filter((x, i, a) => a.indexOf(x) === i); + + same = result.length === nextResult.length; + result = nextResult; + } -/** - * Gets pathnames of a cached module and all its dependencies. - * - * @param {string} filename - */ -Require.prototype.flatten = function(filename) { - let result = [filename]; - let nextResult = null; - let same = false; - - while (!same) { - nextResult = result - .reduce((prev, x) => prev.concat(this.dependencies[x]), result) - .filter((x, i, a) => a.indexOf(x) === i); - - same = result.length === nextResult.length; - result = nextResult; + return result; } - return result; -}; + /** + * Returns a cached module or creates an empty one. Normalizes the path. + * + * @param {string} path + */ + getOrCreate(path) { + const gFile = this.Gio.File.new_for_path(path); + const dirname = gFile.get_parent().get_path(); + const filename = gFile.get_path(); + + const module = this.cache[filename] || + (this.cache[filename] = { + exports: {}, + filename: filename, + hot: { accept: this.accept.bind(null, dirname) }, + }); -/** - * Returns a cached module or creates an empty one. Normalizes the path. - * - * @param {string} path - */ -Require.prototype.getOrCreate = function(path) { - const gFile = this.Gio.File.new_for_path(path); - const dirname = gFile.get_parent().get_path(); - const filename = gFile.get_path(); - - const module = - this.cache[filename] || - (this.cache[filename] = { - exports: {}, - filename: filename, - hot: { accept: this.accept.bind(null, dirname) }, - }); + this.filenames[filename] = true; - this.filenames[filename] = true; + if (!this.dependencies[filename]) { + this.dependencies[filename] = []; + } - if (!this.dependencies[filename]) { - this.dependencies[filename] = []; + return module; } - return module; -}; + /** + * Defines __filename, __dirname, exports, module and require. Does just + * enough to use some CommonJS from npm that isn't dependent on node. + */ + require() { + const window = this.window; + + Object.defineProperty(window, "__filename", { + /** + * Returns the full path to the module that requested it. + */ + get: () => { + const path = this.RE.exec(new Error().stack)[1]; + return this.Gio.File.new_for_path(path).get_path(); + }, + }); -/** - * Defines __filename, __dirname, exports, module and require. Does just - * enough to use some CommonJS from npm that isn't dependent on node. - */ -Require.prototype.require = function() { - const window = this.window; + Object.defineProperty(window, "__dirname", { + /** + * Returns the full path to the parent dir of the module that requested it. + */ + get: () => { + const path = this.RE.exec(new Error().stack)[1]; + return this.Gio.File.new_for_path(path) + .get_path() + .replace(/.[^/]+$/, ""); + }, + }); - Object.defineProperty(window, "__filename", { - /** - * Returns the full path to the module that requested it. - */ - get: () => { - const path = this.RE.exec(new Error().stack)[1]; - return this.Gio.File.new_for_path(path).get_path(); - }, - }); + Object.defineProperty(window, "exports", { + /** + * Returns the exports property of the module that requested it. Note: if + * you refer to exports after reassigning module.exports, this won't behave + * like CommonJS would. + */ + get: () => { + const path = this.RE.exec(new Error().stack)[1]; + const module = this.getOrCreate(path); + return module.exports; + }, + }); - Object.defineProperty(window, "__dirname", { - /** - * Returns the full path to the parent dir of the module that requested it. - */ - get: () => { - const path = this.RE.exec(new Error().stack)[1]; - return this.Gio.File.new_for_path(path) - .get_path() - .replace(/.[^/]+$/, ""); - }, - }); - - Object.defineProperty(window, "exports", { - /** - * Returns the exports property of the module that requested it. Note: if - * you refer to exports after reassigning module.exports, this won't behave - * like CommonJS would. - */ - get: () => { - const path = this.RE.exec(new Error().stack)[1]; - const module = this.getOrCreate(path); - return module.exports; - }, - }); - - Object.defineProperty(window, "module", { - /** - * Returns the meta object of the module that requested it, so you can - * replace the default exported object if you really need to. - */ - get: () => { - const path = this.RE.exec(new Error().stack)[1]; - const module = this.getOrCreate(path); - return module; - }, - }); - - Object.defineProperty(window, "require", { - /** - * Returns the require function bound to filename of the module that - * requested it. - */ - get: () => { - const parentPath = this.RE.exec(new Error().stack)[1]; - const gFile = this.Gio.File.new_for_path(parentPath); - const parentFilename = gFile.get_path(); - - const require = this.requireModule.bind(null, parentFilename); - require.cache = this.cache; - require.resolve = this.resolve.bind(null, gFile.get_parent().get_path()); - return require; - }, - }); - - this.REQUIRE = memoize(this.REQUIRE); - this.LOAD_AS_FILE = memoize(this.LOAD_AS_FILE); - this.LOAD_INDEX = memoize(this.LOAD_INDEX); - this.LOAD_AS_DIRECTORY = memoize(this.LOAD_AS_DIRECTORY); - this.LOAD_NODE_MODULES = memoize(this.LOAD_NODE_MODULES); - this.NODE_MODULES_PATHS = memoize(this.NODE_MODULES_PATHS); - this.IS_FILE = memoize(this.IS_FILE); - this.DIRNAME = memoize(this.DIRNAME); - this.JOIN = memoize(this.JOIN); - - this.Fun = require("./Fun").Fun; - - // exports.Require = Require; // FIXME -}; + Object.defineProperty(window, "module", { + /** + * Returns the meta object of the module that requested it, so you can + * replace the default exported object if you really need to. + */ + get: () => { + const path = this.RE.exec(new Error().stack)[1]; + const module = this.getOrCreate(path); + return module; + }, + }); -/** - * Require(X) from module at path Y. - * - * @see https://nodejs.org/api/modules.html#modules_all_together - * - * @param {string} X - * @param {string} Y - */ -Require.prototype.REQUIRE = function(X, Y) { - if (X[0] === "/") { - Y = "/"; // filesystem root + Object.defineProperty(window, "require", { + /** + * Returns the require function bound to filename of the module that + * requested it. + */ + get: () => { + const parentPath = this.RE.exec(new Error().stack)[1]; + const gFile = this.Gio.File.new_for_path(parentPath); + const parentFilename = gFile.get_path(); + const require = this.requireModule.bind(null, parentFilename); + require.cache = this.cache; + require.resolve = this.resolve.bind(null, gFile.get_parent().get_path()); + return require; + }, + }); + + this.REQUIRE = memoize(this.REQUIRE); + this.LOAD_AS_FILE = memoize(this.LOAD_AS_FILE); + this.LOAD_INDEX = memoize(this.LOAD_INDEX); + this.LOAD_AS_DIRECTORY = memoize(this.LOAD_AS_DIRECTORY); + this.LOAD_NODE_MODULES = memoize(this.LOAD_NODE_MODULES); + this.NODE_MODULES_PATHS = memoize(this.NODE_MODULES_PATHS); + this.IS_FILE = memoize(this.IS_FILE); + this.DIRNAME = memoize(this.DIRNAME); + this.JOIN = memoize(this.JOIN); + + this.Fun = require("./Fun").Fun; + + // exports.Require = Require; // FIXME } - let result = ""; + /** + * Require(X) from module at path Y. + * + * @see https://nodejs.org/api/modules.html#modules_all_together + * + * @param {string} X + * @param {string} Y + */ + REQUIRE(X, Y) { + if (X[0] === "/") { + Y = "/"; // filesystem root + } + + let result = ""; - if (X.slice(0, 2) === "./" || X[0] === "/" || X.slice(0, 3) === "../") { - result = - this.LOAD_AS_FILE(this.JOIN(Y, X)) || - this.LOAD_AS_DIRECTORY(this.JOIN(Y, X)); + if (X.slice(0, 2) === "./" || X[0] === "/" || X.slice(0, 3) === "../") { + result = + this.LOAD_AS_FILE(this.JOIN(Y, X)) || + this.LOAD_AS_DIRECTORY(this.JOIN(Y, X)); + + if (result) { + return result; + } + } + + result = this.LOAD_NODE_MODULES(X, Y); if (result) { return result; } - } - result = this.LOAD_NODE_MODULES(X, Y); - - if (result) { - return result; + throw new Error("Module not found: " + X); } - throw new Error("Module not found: " + X); -}; - -/** - * @param {string} X - */ -Require.prototype.LOAD_AS_FILE = function(X) { - if (this.IS_FILE(X)) { - return X; - } + /** + * @param {string} X + */ + LOAD_AS_FILE(X) { + if (this.IS_FILE(X)) { + return X; + } - if (this.IS_FILE(`${X}.js`)) { - return `${X}.js`; - } + if (this.IS_FILE(`${X}.js`)) { + return `${X}.js`; + } - if (this.IS_FILE(`${X}.json`)) { - return `${X}.json`; + if (this.IS_FILE(`${X}.json`)) { + return `${X}.json`; + } } -}; -/** - * @param {string} X - */ -Require.prototype.LOAD_INDEX = function(X) { - if (this.IS_FILE(this.JOIN(X, "index.js"))) { - return this.JOIN(X, "index.js"); - } + /** + * @param {string} X + */ + LOAD_INDEX(X) { + if (this.IS_FILE(this.JOIN(X, "index.js"))) { + return this.JOIN(X, "index.js"); + } - if (this.IS_FILE(this.JOIN(X, "index.json"))) { - return this.JOIN(X, "index.json"); + if (this.IS_FILE(this.JOIN(X, "index.json"))) { + return this.JOIN(X, "index.json"); + } } -}; -/** - * @param {string} X - */ -Require.prototype.LOAD_AS_DIRECTORY = function(X) { - if (this.IS_FILE(this.JOIN(X, "package.json"))) { - const contents = String( - this.GLib.file_get_contents(this.JOIN(X, "package.json"))[1], - ); - const M = this.JOIN(X, JSON.parse(contents).main); - const result = this.LOAD_AS_FILE(M) || this.LOAD_INDEX(M); + /** + * @param {string} X + */ + LOAD_AS_DIRECTORY(X) { + if (this.IS_FILE(this.JOIN(X, "package.json"))) { + const contents = String( + this.GLib.file_get_contents(this.JOIN(X, "package.json"))[1], + ); + const M = this.JOIN(X, JSON.parse(contents).main); + const result = this.LOAD_AS_FILE(M) || this.LOAD_INDEX(M); - if (result) { - return result; + if (result) { + return result; + } } - } - return this.LOAD_INDEX(X); -}; + return this.LOAD_INDEX(X); + } -/** - * @param {string} X - * @param {string} START - */ -Require.prototype.LOAD_NODE_MODULES = function(X, START) { - const DIRS = this.NODE_MODULES_PATHS(START); + /** + * @param {string} X + * @param {string} START + */ + LOAD_NODE_MODULES(X, START) { + const DIRS = this.NODE_MODULES_PATHS(START); - for (const DIR of DIRS) { - const result = - this.LOAD_AS_FILE(this.JOIN(DIR, X)) || - this.LOAD_AS_DIRECTORY(this.JOIN(DIR, X)); + for (const DIR of DIRS) { + const result = + this.LOAD_AS_FILE(this.JOIN(DIR, X)) || + this.LOAD_AS_DIRECTORY(this.JOIN(DIR, X)); - if (result) { - return result; + if (result) { + return result; + } } } -}; -/** - * @param {string} START - */ -Require.prototype.NODE_MODULES_PATHS = function(START) { - const PARTS = START.split("/"); - let I = PARTS.length - 1; - PARTS[0] = "/"; - - /** @type {string[]} */ - const DIRS = []; - - while (I >= 0) { - if (PARTS[I] !== "node_modules") { - DIRS.push( - PARTS.slice(0, I + 1) - .concat("node_modules") - .reduce(this.JOIN), - ); + /** + * @param {string} START + */ + NODE_MODULES_PATHS(START) { + const PARTS = START.split("/"); + let I = PARTS.length - 1; + PARTS[0] = "/"; + + /** @type {string[]} */ + const DIRS = []; + + while (I >= 0) { + if (PARTS[I] !== "node_modules") { + DIRS.push( + PARTS.slice(0, I + 1) + .concat("node_modules") + .reduce(this.JOIN), + ); + } + + I = I - 1; } - I = I - 1; + return DIRS; } - return DIRS; -}; + /** + * @param {string} X + */ + IS_FILE(X) { + const gFile = this.Gio.file_new_for_path(X); -/** - * @param {string} X - */ -Require.prototype.IS_FILE = function(X) { - const gFile = this.Gio.file_new_for_path(X); + if (!gFile.query_exists(null)) { + return false; + } - if (!gFile.query_exists(null)) { - return false; + const gFileInfo = gFile.query_info( + "standard::type", + FileQueryInfoFlags.NONE, + null, + ); + + return gFileInfo.get_file_type() === FileType.REGULAR; } - const gFileInfo = gFile.query_info( - "standard::type", - FileQueryInfoFlags.NONE, - null, - ); + /** + * @param {string} X + */ + DIRNAME(X) { + return this.Gio.file_new_for_path(X) + .get_parent() + .get_path(); + } - return gFileInfo.get_file_type() === FileType.REGULAR; -}; + /** + * @param {string} X + * @param {string} Y + */ + JOIN(X, Y) { + return this.Gio.file_new_for_path(X) + .resolve_relative_path(Y) + .get_path(); + } -/** - * @param {string} X - */ -Require.prototype.DIRNAME = function(X) { - return this.Gio.file_new_for_path(X) - .get_parent() - .get_path(); -}; + /** + * Gets a normalized local pathname. Understands Dot and Dot Dot, or looks into + * node_modules up to the root. Adds '.js' suffix if omitted. + * + * @param {string} dirname + * @param {string} path + */ + resolve(dirname, path) { + return this.REQUIRE(path, dirname); + } -/** - * @param {string} X - * @param {string} Y - */ -Require.prototype.JOIN = function(X, Y) { - return this.Gio.file_new_for_path(X) - .resolve_relative_path(Y) - .get_path(); -}; + /** + * Loads a module by evaluating file contents in a closure. For example, this + * can be used to require lodash/toString, which Gjs can't import natively. Or + * to reload a module that has been deleted from cache. + * + * @param {string} parentFilename + * @param {string} path + */ + requireClosure(parentFilename, path) { + const dirname = this.Gio.file_new_for_path(parentFilename) + .get_parent() + .get_path(); + const filename = this.resolve(dirname, path); + + if (this.cache[filename]) { + return this.cache[filename].exports; + } -/** - * Gets a normalized local pathname. Understands Dot and Dot Dot, or looks into - * node_modules up to the root. Adds '.js' suffix if omitted. - * - * @param {string} dirname - * @param {string} path - */ -Require.prototype.resolve = function(dirname, path) { - return this.REQUIRE(path, dirname); -}; + let contents = String(this.GLib.file_get_contents(filename)[1]); + const module = this.getOrCreate(filename); -/** - * Loads a module by evaluating file contents in a closure. For example, this - * can be used to require lodash/toString, which Gjs can't import natively. Or - * to reload a module that has been deleted from cache. - * - * @param {string} parentFilename - * @param {string} path - */ -Require.prototype.requireClosure = function(parentFilename, path) { - const dirname = this.Gio.file_new_for_path(parentFilename) - .get_parent() - .get_path(); - const filename = this.resolve(dirname, path); - - if (this.cache[filename]) { - return this.cache[filename].exports; - } + const require = this.requireModule.bind(null, filename); + require.cache = this.cache; + require.resolve = this.resolve.bind(null, dirname); - let contents = String(this.GLib.file_get_contents(filename)[1]); - const module = this.getOrCreate(filename); + if (contents.indexOf("//# sourceURL=") === -1) { + contents += `\n//# sourceURL=${filename}`; + } - const require = this.requireModule.bind(null, filename); - require.cache = this.cache; - require.resolve = this.resolve.bind(null, dirname); + this.Fun("exports", "require", "module", "__filename", "__dirname", + contents, + )(module.exports, require, module, filename, dirname); - if (contents.indexOf("//# sourceURL=") === -1) { - contents += `\n//# sourceURL=${filename}`; - } + if (!this.dependencies[parentFilename]) { + this.dependencies[parentFilename] = [filename]; + } else if (this.dependencies[parentFilename].indexOf(filename) === -1) { + this.dependencies[parentFilename].push(filename); + } - this.Fun("exports", "require", "module", "__filename", "__dirname", contents)( - module.exports, - require, - module, - filename, - dirname, - ); - - if (!this.dependencies[parentFilename]) { - this.dependencies[parentFilename] = [filename]; - } else if (this.dependencies[parentFilename].indexOf(filename) === -1) { - this.dependencies[parentFilename].push(filename); + return module.exports; } - return module.exports; -}; + /** + * Loads a module and returns its exports. Caches the module. + * + * @param {string} parentFilename + * @param {string} path + */ + requireModule(parentFilename, path) { + const dirname = this.Gio.File.new_for_path(parentFilename) + .get_parent() + .get_path(); + const filename = this.resolve(dirname, path); + + if (this.cache[filename]) { + return this.cache[filename].exports; + } -/** - * Loads a module and returns its exports. Caches the module. - * - * @param {string} parentFilename - * @param {string} path - */ -Require.prototype.requireModule = function(parentFilename, path) { - const dirname = this.Gio.File.new_for_path(parentFilename) - .get_parent() - .get_path(); - const filename = this.resolve(dirname, path); - - if (this.cache[filename]) { - return this.cache[filename].exports; - } + if (this.filenames[filename]) { + // The module has been deleted from cache. - if (this.filenames[filename]) { - // The module has been deleted from cache. + if (this.dependencies[filename]) { + this.dependencies[filename].splice(0); + } - if (this.dependencies[filename]) { - this.dependencies[filename].splice(0); + return this.requireClosure(parentFilename, path); } - return this.requireClosure(parentFilename, path); - } + if (this.GLib.getenv("NODE_ENV") === "production" && this.Fun) { + // Faster than importing dirs. + return this.requireClosure(parentFilename, path); + } - if (this.GLib.getenv("NODE_ENV") === "production" && this.Fun) { - // Faster than importing dirs. - return this.requireClosure(parentFilename, path); - } + const parts = filename + .replace(this.imports.searchPath[this.imports.searchPath.length - 1] + "/", "") + .replace(/\.js$/, "") + .split("/"); - const parts = filename - .replace( - this.imports.searchPath[this.imports.searchPath.length - 1] + "/", - "", - ) - .replace(/\.js$/, "") - .split("/"); + if (parts[parts.length - 1] === "toString") { + return this.requireClosure(parentFilename, path); + } - if (parts[parts.length - 1] === "toString") { - return this.requireClosure(parentFilename, path); - } + const module = this.getOrCreate(filename); + let current = this.imports; + parts.forEach(x => { + current = current[x]; + }); - const module = this.getOrCreate(filename); - let current = this.imports; - parts.forEach(x => { - current = current[x]; - }); + if (!this.dependencies[parentFilename]) { + this.dependencies[parentFilename] = [filename]; + } else if (this.dependencies[parentFilename].indexOf(filename) === -1) { + this.dependencies[parentFilename].push(filename); + } - if (!this.dependencies[parentFilename]) { - this.dependencies[parentFilename] = [filename]; - } else if (this.dependencies[parentFilename].indexOf(filename) === -1) { - this.dependencies[parentFilename].push(filename); + return module.exports; } - - return module.exports; }; /** diff --git a/src/app/Job/Job.js b/src/app/Job/Job.js index 46edcf2..b697082 100644 --- a/src/app/Job/Job.js +++ b/src/app/Job/Job.js @@ -8,66 +8,67 @@ const { JobService } = require("./JobService"); /** * @typedef IProps - * @property {JobService} jobService + * @property {JobService?} [jobService] * @property {number} pid * - * @param {IProps} props + * @extends Component */ -function Job(props) { - Component.call(this, props); - autoBind(this, Job.prototype, __filename); -} - -Job.spacing = 10; - -Job.prototype = Object.create(Component.prototype); - -/** @type {IProps} */ -Job.prototype.props = undefined; +class Job extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + autoBind(this, Job.prototype, __filename); + } -Job.prototype.cancel = function() { - this.props.jobService.cancel(this.props.pid); -}; + cancel() { + const jobService = /** @type {JobService} */ (this.props.jobService); + jobService.cancel(this.props.pid); + } -/** - * @param {Button} cancelButton - */ -Job.prototype.useCancelButton = function(cancelButton) { - if (cancelButton) { - cancelButton.connect("clicked", this.cancel); + /** + * @param {Button} cancelButton + */ + useCancelButton(cancelButton) { + if (cancelButton) { + cancelButton.connect("clicked", this.cancel); + } } -}; -Job.prototype.render = function() { - const job = this.props.jobService.jobs[this.props.pid]; - const type = this.props.jobService.types[this.props.pid]; + render() { + const { jobs, types } = /** @type {JobService} */ (this.props.jobService); + const job = jobs[this.props.pid]; + const type = types[this.props.pid]; - return h(Box, { orientation: Orientation.VERTICAL }, [ - h(Label, { label: `Type: ${type}` }), + return h(Box, { orientation: Orientation.VERTICAL }, [ + h(Label, { label: `Type: ${type}` }), + h(Label, { label: `Src: ${job.uri}` }), + h(Label, { label: `Dest: ${job.dest}` }), - h(Label, { label: `Src: ${job.uri}` }), - h(Label, { label: `Dest: ${job.dest}` }), + h(ProgressBar, { + fraction: job.totalDoneCount / job.totalCount || 0, + margin_top: Job.spacing, + show_text: true, + text: `${job.totalDoneCount} / ${job.totalCount}`, + }), - h(ProgressBar, { - fraction: job.totalDoneCount / job.totalCount || 0, - margin_top: Job.spacing, - show_text: true, - text: `${job.totalDoneCount} / ${job.totalCount}`, - }), + h(ProgressBar, { + fraction: job.totalDoneSize / job.totalSize || 0, + margin_bottom: Job.spacing, + show_text: true, + text: `${formatSize(job.totalDoneSize)} / ${formatSize(job.totalSize)}`, + }), - h(ProgressBar, { - fraction: job.totalDoneSize / job.totalSize || 0, - margin_bottom: Job.spacing, - show_text: true, - text: `${formatSize(job.totalDoneSize)} / ${formatSize(job.totalSize)}`, - }), + h(Button, { + label: "Cancel", + ref: this.useCancelButton, + }), + ]); + } +} - h(Button, { - label: "Cancel", - ref: this.useCancelButton, - }), - ]); -}; +Job.spacing = 10; exports.Job = Job; exports.default = connect(["jobService"])(Job); diff --git a/src/app/Job/Jobs.js b/src/app/Job/Jobs.js index 73451f3..b84461a 100644 --- a/src/app/Job/Jobs.js +++ b/src/app/Job/Jobs.js @@ -10,67 +10,71 @@ const { JobService } = require("./JobService"); /** * @typedef IProps - * @property {JobService} jobService - * @property {RefService} refService + * @property {JobService?} [jobService] + * @property {RefService?} [refService] * - * @param {IProps} props + * @extends Component */ -function Jobs(props) { - Component.call(this, props); - autoBind(this, Jobs.prototype, __filename); -} - -Jobs.prototype = Object.create(Component.prototype); - -/** @type {IProps} */ -Jobs.prototype.props = undefined; +class Jobs extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + autoBind(this, Jobs.prototype, __filename); + } -/** - * @param {Popover} popover - */ -Jobs.prototype.usePopover = function(popover) { - this.props.refService.set("jobs")(popover); -}; + /** + * @param {Popover} popover + */ + usePopover(popover) { + const { set } = + /** @type {RefService} */ (this.props.refService); -Jobs.prototype.render = function() { - const inner = [ - h(Label, { - key: "tip", - label: "Cp/mv/rm progress displays here", - }), - ]; + set("jobs")(popover); + } - const { statefulPids } = this.props.jobService; + render() { + const inner = [ + h(Label, { + key: "tip", + label: "Cp/mv/rm progress displays here", + }), + ]; - if (statefulPids.length) { - inner.splice(0); + const { statefulPids } = + /** @type {JobService} */ (this.props.jobService); - for (const pid of statefulPids) { - inner.push( - h(Job, { - key: pid, - pid, - }), - ); + if (statefulPids.length) { + inner.splice(0); - if (pid !== statefulPids[statefulPids.length - 1]) { + for (const pid of statefulPids) { inner.push( - h(HSeparator, { key: `${pid}+` }), + h(Job, { + key: pid, + pid, + }), ); + + if (pid !== statefulPids[statefulPids.length - 1]) { + inner.push( + h(HSeparator, { key: `${pid}+` }), + ); + } } } - } - return h("stub-box", [ - h(Popover, { ref: this.usePopover }, [ - h(Box, { - margin: Job.spacing, - orientation: Orientation.VERTICAL, - spacing: Job.spacing, - }, inner), - ]), - ]); -}; + return h("stub-box", [ + h(Popover, { ref: this.usePopover }, [ + h(Box, { + margin: Job.spacing, + orientation: Orientation.VERTICAL, + spacing: Job.spacing, + }, inner), + ]), + ]); + } +} exports.Jobs = Jobs; exports.default = connect(["jobService", "refService"])(Jobs); diff --git a/src/app/Location/Location.js b/src/app/Location/Location.js index fa7e94d..396dc54 100644 --- a/src/app/Location/Location.js +++ b/src/app/Location/Location.js @@ -11,99 +11,95 @@ const { TabService } = require("../Tab/TabService"); /** * @typedef IProps * @property {number} panelId - * @property {PanelService} panelService - * @property {TabService} tabService + * @property {PanelService?} [panelService] + * @property {TabService?} [tabService] * - * @param {IProps} props + * @extends Component */ -function Location(props) { - Component.call(this, props); - - autoBind(this, Location.prototype, __filename); - - extendObservable(this, { - list: observable.ref(undefined), - refList: action(this.refList), - refRow: action(this.refRow), - row: observable.ref(undefined), - }); - - this.unsubscribeSelection = autorun(this.updateSelection); -} - -Location.prototype = Object.create(Component.prototype); - -/** - * @type {{ select_row(node: any): void, unselect_row(node: any): void }} - */ -Location.prototype.list = undefined; - -/** - * @type {IProps} - */ -Location.prototype.props = undefined; - -/** - * @type {any} - */ -Location.prototype.row = undefined; - -Location.prototype.componentWillUnmount = function() { - this.unsubscribeSelection(); -}; +class Location extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + + /** + * @type {{ select_row(node: any): void, unselect_row(node: any): void }} + */ + this.list = undefined; + + /** + * @type {any} + */ + this.row = undefined; + + autoBind(this, Location.prototype, __filename); + extendObservable(this, { + list: observable.ref(undefined), + refList: action(this.refList), + refRow: action(this.refRow), + row: observable.ref(undefined), + }); + this.unsubscribeSelection = autorun(this.updateSelection); + } -Location.prototype.isActive = function() { - return this.props.panelService.activeId === this.props.panelId; -}; + componentWillUnmount() { + this.unsubscribeSelection(); + } -Location.prototype.tab = function() { - const { activeTabId } = this.props.panelService.entities[this.props.panelId]; - return this.props.tabService.entities[activeTabId]; -}; + isActive() { + return this.props.panelService.activeId === this.props.panelId; + } -/** - * @param {ListBox} node - */ -Location.prototype.refList = function(node) { - this.list = node; -}; + tab() { + const { activeTabId } = this.props.panelService.entities[this.props.panelId]; + return this.props.tabService.entities[activeTabId]; + } -/** - * @param {ListBoxRow} node - */ -Location.prototype.refRow = function(node) { - this.row = node; -}; + /** + * @param {ListBox} node + */ + refList(node) { + this.list = node; + } -Location.prototype.updateSelection = function() { - if (!this.list || !this.row) { - return; + /** + * @param {ListBoxRow} node + */ + refRow(node) { + this.row = node; } - if (this.isActive()) { - this.list.select_row(this.row); - } else { - this.list.unselect_row(this.row); + updateSelection() { + if (!this.list || !this.row) { + return; + } + + if (this.isActive()) { + this.list.select_row(this.row); + } else { + this.list.unselect_row(this.row); + } } -}; -Location.prototype.render = function() { - const { location } = this.tab(); - const label = location.replace(/\/?$/, "/*").replace(/^file:\/\//, ""); - return ( - h(ListBox, { ref: this.refList }, [ - h(ListBoxRow, { ref: this.refRow }, [ - h(Box, { border_width: 2 }, [ - h(Box, { border_width: 2 }), - h(Label, { - ellipsize: EllipsizeMode.MIDDLE, - label: label, - }), + render() { + const { location } = this.tab(); + const label = location.replace(/\/?$/, "/*").replace(/^file:\/\//, ""); + return ( + h(ListBox, { ref: this.refList }, [ + h(ListBoxRow, { ref: this.refRow }, [ + h(Box, { border_width: 2 }, [ + h(Box, { border_width: 2 }), + h(Label, { + ellipsize: EllipsizeMode.MIDDLE, + label: label, + }), + ]), ]), - ]), - ]) - ); -}; + ]) + ); + } +} exports.Location = Location; exports.default = connect(["panelService", "tabService"])(Location); diff --git a/src/app/Location/Location.test.js b/src/app/Location/Location.test.js index 2385fda..37e99ad 100644 --- a/src/app/Location/Location.test.js +++ b/src/app/Location/Location.test.js @@ -1,11 +1,12 @@ const expect = require("expect"); -const h = require("inferno-hyperscript").default; const { observable } = require("mobx"); -const { Location } = require("./Location"); +const { h } = require("../Gjs/GtkInferno"); const { shallow } = require("../Test/Test"); +const { Location } = require("./Location"); describe("Location", () => { it("renders without crashing", () => { + /** @type {any} */ const panelService = { entities: { "0": { @@ -14,6 +15,7 @@ describe("Location", () => { }, }; + /** @type {any} */ const tabService = { entities: { "0": { diff --git a/src/app/Menu/MenuBar.js b/src/app/Menu/MenuBar.js index 88fa506..1dbcef6 100644 --- a/src/app/Menu/MenuBar.js +++ b/src/app/Menu/MenuBar.js @@ -1,7 +1,8 @@ +const { MenuBar, MenuItem } = imports.gi.Gtk; const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; const { connect } = require("inferno-mobx"); -const { ActionService } = require("../Action/ActionService"); +const { h } = require("../Gjs/GtkInferno"); +const MenuBarAction = require("./MenuBarAction").default; const menus = [ { @@ -58,37 +59,19 @@ const menus = [ }, ]; -/** - * @typedef IProps - * @property {ActionService} actionService - * - * @param {IProps} props - */ -function MenuBar(props) { - Component.call(this, props); +class MenuBarComponent extends Component { + render() { + return ( + h(MenuBar, menus.map(menu => ( + h("menu-item-with-submenu", { + key: menu.label, + label: menu.label, + }, + menu.children.map(action => h(MenuBarAction, { action })), + ) + ))) + ); + } } -MenuBar.prototype = Object.create(Component.prototype); - -/** @type {IProps} */ -MenuBar.prototype.props = undefined; - -MenuBar.prototype.render = function() { - return ( - h("menu-bar", - menus.map(menu => ( - h("menu-item-with-submenu", { key: menu.label, label: menu.label }, - menu.children.map(child => ( - h("menu-item", { - label: child.label, - on_activate: this.props.actionService.get(child.id).handler, - }) - )), - )), - ), - ) - ); -}; - -exports.MenuBar = MenuBar; -exports.default = connect(["actionService"])(MenuBar); +exports.MenuBar = MenuBarComponent; diff --git a/src/app/Menu/MenuBar.test.js b/src/app/Menu/MenuBar.test.js index db2e9f1..f33aed4 100644 --- a/src/app/Menu/MenuBar.test.js +++ b/src/app/Menu/MenuBar.test.js @@ -1,31 +1,7 @@ -const expect = require("expect"); const { MenuBar } = require("./MenuBar"); describe("MenuBar", () => { - it("renders without crashing", () => { - const handler = expect.createSpy(); - - /** @type {any} */ - const actionService = { - get: () => ({ handler }), - }; - - new MenuBar({ actionService }).render(); - }); - - it("calls action when user activates item", () => { - const handler = expect.createSpy(); - - /** @type {any} */ - const actionService = { - get: () => ({ handler }), - }; - - /** @type {any} */ - const tree = new MenuBar({ actionService }).render(); - - tree.children[0].children[0].props.on_activate(); - - expect(handler).toHaveBeenCalled(); + it("renders", () => { + new MenuBar().render(); }); }); diff --git a/src/app/Menu/MenuBarAction.js b/src/app/Menu/MenuBarAction.js new file mode 100644 index 0000000..39ccae5 --- /dev/null +++ b/src/app/Menu/MenuBarAction.js @@ -0,0 +1,48 @@ +const { MenuItem } = imports.gi.Gtk; +const Component = require("inferno-component").default; +const { connect } = require("inferno-mobx"); +const { ActionService } = require("../Action/ActionService"); +const { autoBind } = require("../Gjs/autoBind"); +const { h } = require("../Gjs/GtkInferno"); + +/** + * @typedef IProps + * @property {{ id: string, label: string }} action + * @property {ActionService?} [actionService] + * + * @extends Component + */ +class MenuBarAction extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + autoBind(this, MenuBarAction.prototype, __filename); + } + + /** + * @param {MenuItem | null} menuItem + */ + ref(menuItem) { + if (menuItem) { + const { get } = + /** @type {ActionService} */ (this.props.actionService); + + const { handler } = get(this.props.action.id); + menuItem.connect("activate", handler); + } + } + + render() { + return ( + h(MenuItem, { + label: this.props.action.label, + ref: this.ref, + }) + ); + } +} + +exports.MenuBarAction = MenuBarAction; +exports.default = connect(["actionService"])(MenuBarAction); diff --git a/src/app/Menu/MenuBarAction.test.js b/src/app/Menu/MenuBarAction.test.js new file mode 100644 index 0000000..5ffb2ad --- /dev/null +++ b/src/app/Menu/MenuBarAction.test.js @@ -0,0 +1,35 @@ +const expect = require("expect"); +const { MenuBarAction } = require("./MenuBarAction"); + +describe("MenuBarAction", () => { + it("renders", () => { + const action = { + id: "selectionService.copy", + label: "Copy", + }; + + new MenuBarAction({ action }).render(); + }); + + it("connects handler", () => { + const handler = expect.createSpy(); + + /** @type {any} */ + const actionService = { + get: () => ({ handler }), + }; + + /** @type {any} */ + const menuItem = { + connect: expect.createSpy(), + }; + + const action = { + id: "selectionService.copy", + label: "Copy", + }; + + new MenuBarAction({ action, actionService }).ref(menuItem); + expect(menuItem.connect).toHaveBeenCalledWith("activate", handler); + }); +}); diff --git a/src/app/Menu/MenuItemWithSubmenu.js b/src/app/Menu/MenuItemWithSubmenu.js index dda4b93..9c567e5 100644 --- a/src/app/Menu/MenuItemWithSubmenu.js +++ b/src/app/Menu/MenuItemWithSubmenu.js @@ -2,70 +2,71 @@ const assign = require("lodash/assign"); const noop = require("lodash/noop"); const { autoBind } = require("../Gjs/autoBind"); -/** - * @param {MenuItemWithSubmenu} node - */ -function MenuItemWithSubmenu(node, _document = document) { - node.document = _document; - this.useNodeAsThis.call(node); - return node; -} - -/** @type {typeof document} */ -MenuItemWithSubmenu.prototype.document = undefined; +class MenuItemWithSubmenu { + /** + * @param {MenuItemWithSubmenu} node + */ + constructor(node, document = window.document) { + /** @type {typeof document} */ + this.document = document; -/** - * Gtk menu widget, patched with `../Gjs/GtkDom`. - * @type {any} - */ -MenuItemWithSubmenu.prototype.submenu = undefined; + /** + * Gtk menu widget, patched with `../Gjs/GtkDom`. + * @type {any} + */ + this.submenu = undefined; -MenuItemWithSubmenu.prototype.useNodeAsThis = function() { - autoBind(this, MenuItemWithSubmenu.prototype, __filename); + /** + * Native method. Sets submenu. + * @type {(gtkMenu: any) => void} + */ + this.set_submenu = undefined; - this.submenu = this.document.createElement("menu"); - this.set_submenu(this.submenu); + node.document = document; + this.useNodeAsThis.call(node); + return node; + } - Object.defineProperties(this, { - firstChild: { get: () => this.submenu.firstChild }, - textContent: { set: (value) => this.submenu.textContent = value }, - }); -}; + useNodeAsThis() { + autoBind(this, MenuItemWithSubmenu.prototype, __filename); + this.submenu = this.document.createElement("menu"); + this.set_submenu(this.submenu); -/** - * @param {any} newChild - * @param {any=} existingChild - */ -MenuItemWithSubmenu.prototype.insertBefore = function(newChild, existingChild) { - return this.submenu.insertBefore(newChild, existingChild); -}; + Object.defineProperties(this, { + firstChild: { get: () => this.submenu.firstChild }, + textContent: { set: (value) => this.submenu.textContent = value }, + }); + } -/** - * @param {any} newChild - */ -MenuItemWithSubmenu.prototype.appendChild = function(newChild) { - this.submenu.appendChild(newChild); -}; + /** + * @param {any} newChild + * @param {any=} existingChild + */ + insertBefore(newChild, existingChild) { + return this.submenu.insertBefore(newChild, existingChild); + } -/** - * @param {any} row - */ -MenuItemWithSubmenu.prototype.removeChild = function(row) { - return this.submenu.removeChild(row); -}; + /** + * @param {any} newChild + */ + appendChild(newChild) { + this.submenu.appendChild(newChild); + } -/** - * @param {any} newChild - * @param {any} oldChild - */ -MenuItemWithSubmenu.prototype.replaceChild = function(newChild, oldChild) { - return this.submenu.replaceChild(newChild, oldChild); -}; + /** + * @param {any} row + */ + removeChild(row) { + return this.submenu.removeChild(row); + } -/** - * Native method. Sets submenu. - * @type {(gtkMenu: any) => void} - */ -MenuItemWithSubmenu.prototype.set_submenu = undefined; + /** + * @param {any} newChild + * @param {any} oldChild + */ + replaceChild(newChild, oldChild) { + return this.submenu.replaceChild(newChild, oldChild); + } +} exports.MenuItemWithSubmenu = MenuItemWithSubmenu; diff --git a/src/app/Mount/Mount.js b/src/app/Mount/Mount.js index 3774b3b..39614c9 100644 --- a/src/app/Mount/Mount.js +++ b/src/app/Mount/Mount.js @@ -1,9 +1,9 @@ -const { ReliefStyle } = imports.gi.Gtk; +const { Box, Button, Label, ReliefStyle, VSeparator } = imports.gi.Gtk; const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; const { connect } = require("inferno-mobx"); const noop = require("lodash/noop"); const { autoBind } = require("../Gjs/autoBind"); +const { h } = require("../Gjs/GtkInferno"); const { setTimeout } = require("../Gjs/setTimeout"); const { GICON, TEXT } = require("../ListStore/ListStore"); const minLength = require("../MinLength/minLength").default; @@ -16,103 +16,114 @@ const formatSize = require("../Size/formatSize").default; /** * @typedef IProps * @property {number} panelId - * @property {PanelService} panelService - * @property {PlaceService} placeService - * @property {RefService} refService + * @property {PanelService?} [panelService] + * @property {PlaceService?} [placeService] + * @property {RefService?} [refService] * - * @param {IProps} props + * @extends Component */ -function Mount(props) { - Component.call(this, props); - autoBind(this, Mount.prototype, __filename); -} +class Mount extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + this.onChanged = noop; + autoBind(this, Mount.prototype, __filename); + } -Mount.prototype = Object.create(Component.prototype); + handleFocus() { + setTimeout(() => { + const node = this.props.refService.get("panel" + this.props.panelId); -/** - * @type {IProps} - */ -Mount.prototype.props = undefined; + if (node) { + node.grab_focus(); + } + }, 0); + } -Mount.prototype.onChanged = noop; + handleLevelUp() { + this.props.panelService.levelUp(this.props.panelId); + } -Mount.prototype.handleFocus = function() { - setTimeout(() => { - const node = this.props.refService.get("panel" + this.props.panelId); + handleRoot() { + this.props.panelService.root(this.props.panelId); + } - if (node) { - node.grab_focus(); - } - }, 0); -}; + /** + * @param {Button} button + */ + refLevelUp(button) { + button.connect("clicked", this.handleLevelUp); + } -Mount.prototype.handleLevelUp = function() { - this.props.panelService.levelUp(this.props.panelId); -}; + /** + * @param {Button} button + */ + refRoot(button) { + button.connect("clicked", this.handleRoot); + } -Mount.prototype.handleRoot = function() { - this.props.panelService.root(this.props.panelId); -}; + render() { + const { panelId, panelService, placeService } = this.props; + const { entities, names } = placeService; -Mount.prototype.render = function() { - const { panelId, panelService, placeService } = this.props; - const { entities, names } = placeService; + const activeUri = panelService.getActiveMountUri(panelId); - const activeUri = panelService.getActiveMountUri(panelId); + const activeMount = names + .map(x => entities[x]) + .filter(mount => mount.rootUri === activeUri)[0]; - const activeMount = names - .map(x => entities[x]) - .filter(mount => mount.rootUri === activeUri)[0]; + const name = activeMount.name; - const name = activeMount.name; + const status = "[" + name + "] " + + formatSize(activeMount.filesystemFree) + " of " + + formatSize(activeMount.filesystemSize) + " free"; - const status = "[" + name + "] " + - formatSize(activeMount.filesystemFree) + " of " + - formatSize(activeMount.filesystemSize) + " free"; - - return ( - h("box", { expand: false }, [ - h("box", [ - h(Select, { - cols: [ - { name: "text", type: TEXT, pack: "pack_end" }, - { name: "icon", type: GICON }, - ], - on_changed: this.onChanged, - on_focus: this.handleFocus, - on_layout: this.props.refService.set("mounts" + this.props.panelId), - rows: names.map(x => entities[x]).map(mount => ({ - icon: { - icon: mount.icon, - iconType: mount.iconType, - }, - text: minLength(names, mount.name), - value: mount.name, - })), - value: name, - }), - ]), - h("box", { border_width: 4, expand: true }, [ - h("label", { label: status }), - ]), - h("v-separator"), - h("box", [ - h("button", { - on_clicked: this.handleRoot, - relief: ReliefStyle.NONE, - }, [ - h("label", { label: "\\" }), - ]), - h("button", { - on_clicked: this.handleLevelUp, - relief: ReliefStyle.NONE, - }, [ - h("label", { label: ".." }), - ]), - ]), - ]) - ); -}; + return ( + h(Box, { expand: false }, [ + h(Box, [ + h(Select, { + cols: [ + { name: "text", type: TEXT, pack: "pack_end" }, + { name: "icon", type: GICON }, + ], + on_changed: this.onChanged, + on_focus: this.handleFocus, + on_layout: this.props.refService.set("mounts" + this.props.panelId), + rows: names.map(x => entities[x]).map(mount => ({ + icon: { + icon: mount.icon, + iconType: mount.iconType, + }, + text: minLength(names, mount.name), + value: mount.name, + })), + value: name, + }), + ]), + h(Box, { border_width: 4, expand: true }, [ + h(Label, { label: status }), + ]), + h(VSeparator), + h(Box, [ + h(Button, { + ref: this.refRoot, + relief: ReliefStyle.NONE, + }, [ + h(Label, { label: "\\" }), + ]), + h(Button, { + ref: this.refLevelUp, + relief: ReliefStyle.NONE, + }, [ + h(Label, { label: ".." }), + ]), + ]), + ]) + ); + } +} exports.Mount = Mount; exports.default = connect(["panelService", "placeService", "refService"])(Mount); diff --git a/src/app/Mount/Mount.test.js b/src/app/Mount/Mount.test.js index 1a2aea8..91c3cb6 100644 --- a/src/app/Mount/Mount.test.js +++ b/src/app/Mount/Mount.test.js @@ -43,7 +43,6 @@ describe("Mount", () => { new Mount({ panelId: 0, panelService, - placeService: undefined, refService: new RefService(), }).handleLevelUp(); diff --git a/src/app/Opposite/OppositeService.js b/src/app/Opposite/OppositeService.js index f78daa7..994160f 100644 --- a/src/app/Opposite/OppositeService.js +++ b/src/app/Opposite/OppositeService.js @@ -12,11 +12,11 @@ const { TabService } = require("../Tab/TabService"); class OppositeService { /** * @typedef IProps - * @property {DialogService} dialogService - * @property {JobService} jobService - * @property {PanelService} panelService - * @property {SelectionService} selectionService - * @property {TabService} tabService + * @property {DialogService?} [dialogService] + * @property {JobService?} [jobService] + * @property {PanelService?} [panelService] + * @property {SelectionService?} [selectionService] + * @property {TabService?} [tabService] * * @param {IProps} props */ diff --git a/src/app/Panel/Panel.js b/src/app/Panel/Panel.js index 2832f96..f6bf2e6 100644 --- a/src/app/Panel/Panel.js +++ b/src/app/Panel/Panel.js @@ -1,6 +1,6 @@ -const { Orientation, PolicyType } = imports.gi.Gtk; -const h = require("inferno-hyperscript").default; +const { Box, HSeparator, Orientation, PolicyType, ScrolledWindow } = imports.gi.Gtk; const Directory = require("../Directory/Directory").default; +const { h } = require("../Gjs/GtkInferno"); const Location = require("../Location/Location").default; const Mount = require("../Mount/Mount").default; const Stats = require("../Stats/Stats").default; @@ -8,13 +8,13 @@ const TabList = require("../Tab/TabList").default; exports.default = Panel; /** - * @param {{ id: string }} props + * @param {{ id: number }} props */ function Panel(props) { const panelId = props.id; return ( - h("box", { orientation: Orientation.VERTICAL }, [ + h(Box, { orientation: Orientation.VERTICAL }, [ h(Mount, { key: "MOUNT", panelId, @@ -23,13 +23,13 @@ function Panel(props) { key: "TAB_LIST", panelId, }), - h("h-separator"), + h(HSeparator), h(Location, { key: "LOCATION", panelId, }), - h("h-separator"), - h("scrolled-window", { + h(HSeparator), + h(ScrolledWindow, { expand: true, hscrollbar_policy: PolicyType.NEVER, key: "DIRECTORY", diff --git a/src/app/Panel/Panel.test.js b/src/app/Panel/Panel.test.js index 41a34f9..6da08dd 100644 --- a/src/app/Panel/Panel.test.js +++ b/src/app/Panel/Panel.test.js @@ -1,6 +1,6 @@ -const h = require("inferno-hyperscript").default; -const Panel = require("./Panel").default; +const { h } = require("../Gjs/GtkInferno"); const { shallow } = require("../Test/Test"); +const Panel = require("./Panel").default; describe("Panel", () => { it("renders without crashing", () => { diff --git a/src/app/Panel/PanelService.js b/src/app/Panel/PanelService.js index fc01180..5e82ee0 100644 --- a/src/app/Panel/PanelService.js +++ b/src/app/Panel/PanelService.js @@ -19,9 +19,9 @@ const { TabService } = require("../Tab/TabService"); class PanelService { /** * @typedef IProps - * @property {DialogService} dialogService - * @property {PlaceService} placeService - * @property {TabService} tabService + * @property {DialogService?} [dialogService] + * @property {PlaceService?} [placeService] + * @property {TabService?} [tabService] * * @param {IProps} props */ @@ -269,11 +269,9 @@ class PanelService { /** * Sets the next tab in panel as active, or the first if the last * is active. - * - * @param {number} panelId */ - nextTab(panelId) { - const { activeTabId, tabIds } = this.entities[panelId]; + nextTab() { + const { activeTabId, tabIds } = this.entities[this.activeId]; let index = tabIds.indexOf(activeTabId) + 1; @@ -287,11 +285,9 @@ class PanelService { /** * Sets the previous tab in panel as active, or the last if the first * is active. - * - * @param {number} panelId */ - prevTab(panelId) { - const { activeTabId, tabIds } = this.entities[panelId]; + prevTab() { + const { activeTabId, tabIds } = this.entities[this.activeId]; let index = tabIds.indexOf(activeTabId) - 1; diff --git a/src/app/Panel/PanelService.test.js b/src/app/Panel/PanelService.test.js index cf03736..a9021fc 100644 --- a/src/app/Panel/PanelService.test.js +++ b/src/app/Panel/PanelService.test.js @@ -19,11 +19,7 @@ describe("PanelService", () => { sortedBy: "-date", }; - const panelService = new PanelService({ - dialogService: undefined, - placeService: undefined, - tabService, - }); + const panelService = new PanelService({ tabService }); panelService.activeId = 0; panelService.entities[0] = { activeTabId: 0, @@ -55,11 +51,7 @@ describe("PanelService", () => { }, }; - const panelService = new PanelService({ - dialogService: undefined, - placeService: undefined, - tabService, - }); + const panelService = new PanelService({ tabService }); panelService.entities[0].activeTabId = 0; panelService.entities[0].history = ["file:///"]; @@ -71,10 +63,7 @@ describe("PanelService", () => { }); it("switches panel to next tab", () => { - /** @type {any} */ - const tabService = undefined; - - const panelService = new PanelService(tabService); + const panelService = new PanelService({}); const entities = { "0": { @@ -84,46 +73,35 @@ describe("PanelService", () => { }; panelService.entities = entities; - panelService.nextTab(0); + panelService.nextTab(); expect(panelService.entities[0].activeTabId).toBe(2); - panelService.nextTab(0); + panelService.nextTab(); expect(panelService.entities[0].activeTabId).toBe(8); - panelService.nextTab(0); + panelService.nextTab(); expect(panelService.entities[0].activeTabId).toBe(0); }); it("switches panel to prev tab", () => { - /** @type {any} */ - const tabService = undefined; + const panelService = new PanelService({}); - const panelService = new PanelService(tabService); - - const entities = { - "0": { - activeTabId: 0, - tabIds: [0, 2, 8], - }, - }; - panelService.entities = entities; + panelService.activeId = 0; + panelService.entities[0].activeTabId = 0; + panelService.entities[0].tabIds = [0, 2, 8]; - panelService.prevTab(0); + panelService.prevTab(); expect(panelService.entities[0].activeTabId).toBe(8); - panelService.prevTab(0); + panelService.prevTab(); expect(panelService.entities[0].activeTabId).toBe(2); - panelService.prevTab(0); + panelService.prevTab(); expect(panelService.entities[0].activeTabId).toBe(0); }); it("removes tab, active if no id", () => { - const panelService = new PanelService({ - dialogService: undefined, - placeService: undefined, - tabService: undefined, - }); + const panelService = new PanelService({}); panelService.activeId = 1; panelService.entities[1].activeTabId = 1; @@ -136,11 +114,7 @@ describe("PanelService", () => { }); it("removes tab", () => { - const panelService = new PanelService({ - dialogService: undefined, - placeService: undefined, - tabService: undefined, - }); + const panelService = new PanelService({}); panelService.activeId = 1; panelService.entities[1].activeTabId = 1; @@ -153,11 +127,7 @@ describe("PanelService", () => { }); it("sets active tab", () => { - const panelService = new PanelService({ - dialogService: undefined, - placeService: undefined, - tabService: undefined, - }); + const panelService = new PanelService({}); panelService.entities[0].activeTabId = -1; @@ -167,10 +137,7 @@ describe("PanelService", () => { }); it("toggles active panel", () => { - /** @type {any} */ - const tabService = undefined; - - const panelService = new PanelService(tabService); + const panelService = new PanelService({}); panelService.activeId = 0; panelService.toggleActive(); diff --git a/src/app/Place/PlaceService.js b/src/app/Place/PlaceService.js index 076563b..6a945a7 100644 --- a/src/app/Place/PlaceService.js +++ b/src/app/Place/PlaceService.js @@ -74,11 +74,11 @@ class PlaceService { /** * Mounts a remote place, such as SFTP. * - * @param {string} _uri + * @param {string} uriStr * @param {(error: Error, uri: string) => void} callback */ - mount(_uri, callback) { - const uri = Uri(_uri); + mount(uriStr, callback) { + const uri = Uri(uriStr); const { auth, username, password, host } = uri; if (!uri.pathname) { @@ -216,15 +216,15 @@ class PlaceService { map( gVolumes, - (gVolume, _callback) => { + (gVolume, volumeCallback) => { const label = gVolume.get_identifier("label"); const uuid = gVolume.get_identifier("uuid"); const gMount = gVolume.get_mount(); if (gMount) { - this.serializeMount(gMount, _callback); + this.serializeMount(gMount, volumeCallback); } else { - _callback(null, { + volumeCallback(null, { filesystemFree: 0, filesystemSize: 0, icon: "drive-harddisk", diff --git a/src/app/Place/Places.js b/src/app/Place/Places.js index 0e2a587..dfd52e2 100644 --- a/src/app/Place/Places.js +++ b/src/app/Place/Places.js @@ -1,5 +1,6 @@ -const h = require("inferno-hyperscript").default; +const { Box } = imports.gi.Gtk; const { connect } = require("inferno-mobx"); +const { h } = require("../Gjs/GtkInferno"); const minLength = require("../MinLength/minLength").default; const PlacesEntry = require("./PlacesEntry").default; const { PlaceService } = require("./PlaceService"); @@ -7,22 +8,22 @@ const { PlaceService } = require("./PlaceService"); /** * @typedef IProps * @property {number} panelId - * @property {PlaceService} placeService + * @property {PlaceService?} [placeService] * * @param {IProps} props */ function Places(props) { const { entities, names } = props.placeService; - return h("box", [ - names.map(x => entities[x]).map(place => { - return h(PlacesEntry, { + return ( + h(Box, names.map(x => entities[x]).map(place => ( + h(PlacesEntry, { panelId: this.props.panelId, place, short: minLength(names, place.name), - }); - }), - ]); + }) + ))) + ); } exports.Places = Places; diff --git a/src/app/Place/Places.test.js b/src/app/Place/Places.test.js index c653b39..4b3dfc0 100644 --- a/src/app/Place/Places.test.js +++ b/src/app/Place/Places.test.js @@ -1,9 +1,10 @@ -const h = require("inferno-hyperscript").default; +const { h } = require("../Gjs/GtkInferno"); const { shallow } = require("../Test/Test"); const { Places } = require("./Places"); describe("Places", () => { it("renders without crashing", () => { + /** @type {any} */ const placeService = { entities: { Music: { @@ -22,32 +23,10 @@ describe("Places", () => { names: ["System", "Music"], }; - const panelService = { - entities: { - "0": { activeTabId: 0 }, - }, - }; - - const tabService = { - entities: { - "0": { - files: [ - { - mountUri: "file:///media/System", - name: ".", - }, - ], - location: "file:///media/System/tmp", - }, - }, - }; - shallow( h(Places, { panelId: 0, - panelService: panelService, placeService: placeService, - tabService: tabService, }), ); }); diff --git a/src/app/Place/PlacesEntry.js b/src/app/Place/PlacesEntry.js index 4681478..983c68e 100644 --- a/src/app/Place/PlacesEntry.js +++ b/src/app/Place/PlacesEntry.js @@ -1,9 +1,10 @@ const Gtk = imports.gi.Gtk; +const { Box, Image, Label } = Gtk; const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; const { connect } = require("inferno-mobx"); const { Place } = require("../../domain/Place/Place"); const { autoBind } = require("../Gjs/autoBind"); +const { h } = require("../Gjs/GtkInferno"); const Icon = require("../Icon/Icon").default; const minLength = require("../MinLength/minLength").default; const { PanelService } = require("../Panel/PanelService"); @@ -14,97 +15,102 @@ const { PlaceService } = require("./PlaceService"); * @typedef IProps * @property {number} panelId * @property {Place} place - * @property {PanelService} panelService + * @property {PanelService?} [panelService] * @property {string} short - * @property {PlaceService} placeService + * @property {PlaceService?} [placeService] * - * @param {IProps} props + * @extends Component */ -function PlacesEntry(props) { - Component.call(this, props); - autoBind(this, PlacesEntry.prototype, __filename); -} +class PlacesEntry extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + autoBind(this, PlacesEntry.prototype, __filename); + } -PlacesEntry.prototype = Object.create(Component.prototype); + activeUri() { + const { getActiveMountUri } = + /** @type {PanelService} */ (this.props.panelService); -/** @type {IProps} */ -PlacesEntry.prototype.props = undefined; + return getActiveMountUri(this.props.panelId); + } -PlacesEntry.prototype.activeUri = function() { - return this.props.panelService.getActiveMountUri(this.props.panelId); -}; + isActive() { + return this.props.place.rootUri === this.activeUri(); + } -PlacesEntry.prototype.isActive = function() { - return this.props.place.rootUri === this.activeUri(); -}; + handleClicked() { + const { getActiveTab, ls, refresh } = + /** @type {PanelService} */ (this.props.panelService); -PlacesEntry.prototype.handleClicked = function() { - const { panelService, place, placeService } = this.props; - const { location } = this.props.panelService.getActiveTab(); - const isActive = place.rootUri === this.activeUri(); + const { mountUuid, unmount } = + /** @type {PlaceService} */ (this.props.placeService); - const menu = new Gtk.Menu(); - let item; + const { panelId, place } = this.props; + const { location } = getActiveTab(); + const isActive = place.rootUri === this.activeUri(); - if (place.rootUri && place.rootUri !== location) { - item = new Gtk.MenuItem(); - item.label = "Open"; - item.connect("activate", () => { - this.props.panelService.ls(place.rootUri, this.props.panelId); - }); - menu.add(item); - } + const menu = new Gtk.Menu(); + let item; - if (place.rootUri && !isActive) { - item = new Gtk.MenuItem(); - item.label = "Unmount"; - item.connect("activate", () => { - placeService.unmount(place.rootUri, panelService.refresh); - }); - menu.add(item); - } + if (place.rootUri && place.rootUri !== location) { + item = new Gtk.MenuItem(); + item.label = "Open"; + item.connect("activate", () => { + ls(place.rootUri, panelId); + }); + menu.add(item); + } - if (!place.rootUri) { - item = new Gtk.MenuItem(); - item.label = "Mount"; - item.connect("activate", () => { - placeService.mountUuid(place.uuid, panelService.refresh); - }); - menu.add(item); - } + if (place.rootUri && !isActive) { + item = new Gtk.MenuItem(); + item.label = "Unmount"; + item.connect("activate", () => { + unmount(place.rootUri, refresh); + }); + menu.add(item); + } - if (!item) { - return; - } + if (!place.rootUri) { + item = new Gtk.MenuItem(); + item.label = "Mount"; + item.connect("activate", () => { + mountUuid(place.uuid, refresh); + }); + menu.add(item); + } + + if (!item) { + return; + } - menu.show_all(); - menu.popup(null, null, null, null, null); -}; + menu.show_all(); + menu.popup(null, null, null, 0, 0); + } -PlacesEntry.prototype.render = function() { - const { place, short } = this.props; - const { icon, iconType, name } = place; + render() { + const { place, short } = this.props; + const { icon, iconType, name } = place; - return h( - ToggleButton, - { + return h(ToggleButton, { active: this.isActive(), can_focus: false, - on_clicked: this.handleClicked, + pressedCallback: this.handleClicked, relief: Gtk.ReliefStyle.NONE, tooltip_text: name, - }, - [ - h("box", { spacing: 4 }, [ - h("image", { - gicon: Icon({ icon: icon, iconType: iconType }), - icon_size: Gtk.IconSize.SMALL_TOOLBAR, - }), - h("label", { label: short }), - ]), - ], - ); -}; + }, [ + h(Box, { spacing: 4 }, [ + h(Image, { + gicon: Icon({ icon: icon, iconType: iconType }), + icon_size: Gtk.IconSize.SMALL_TOOLBAR, + }), + h(Label, { label: short }), + ]), + ]); + } +} exports.PlacesEntry = PlacesEntry; exports.default = connect(["panelService", "placeService"])(PlacesEntry); diff --git a/src/app/Place/PlacesEntry.test.js b/src/app/Place/PlacesEntry.test.js index 9ac469d..2a9a8e3 100644 --- a/src/app/Place/PlacesEntry.test.js +++ b/src/app/Place/PlacesEntry.test.js @@ -18,9 +18,8 @@ describe("PlacesEntry", () => { iconType: "ICON_NAME", name: "/", rootUri: "file:///", - uuid: null, + uuid: "", }, - placeService: undefined, short: "/", }).render(); @@ -34,9 +33,8 @@ describe("PlacesEntry", () => { iconType: "ICON_NAME", name: "Music", rootUri: "file:///media/Music", - uuid: null, + uuid: "", }, - placeService: undefined, short: "M", }).render(); }); diff --git a/src/app/Prompt/Prompt.js b/src/app/Prompt/Prompt.js index e2b8b3d..6b902bc 100644 --- a/src/app/Prompt/Prompt.js +++ b/src/app/Prompt/Prompt.js @@ -1,52 +1,66 @@ +const { Box, Entry, Label } = imports.gi.Gtk; const { EllipsizeMode } = imports.gi.Pango; const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; const { connect } = require("inferno-mobx"); const { DirectoryService } = require("../Directory/DirectoryService"); const { autoBind } = require("../Gjs/autoBind"); +const { h } = require("../Gjs/GtkInferno"); const { PanelService } = require("../Panel/PanelService"); /** * @typedef IProps - * @property {DirectoryService} directoryService - * @property {PanelService} panelService + * @property {DirectoryService?} [directoryService] + * @property {PanelService?} [panelService] * - * @param {IProps} props + * @extends Component */ -function Prompt(props) { - Component.call(this, props); - autoBind(this, Prompt.prototype, __filename); -} +class Prompt extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + autoBind(this, Prompt.prototype, __filename); + } -Prompt.prototype = Object.create(Component.prototype); + /** + * @param {{ text: string | null }} entry + */ + handleActivate(entry) { + const { exec } = + /** @type {DirectoryService} */ (this.props.directoryService); -/** @type {IProps} */ -Prompt.prototype.props = undefined; + if (entry.text) { + exec(entry.text); + } + } -/** - * @param {{ text?: string }} node - */ -Prompt.prototype.activate = function(node) { - if (node.text) { - this.props.directoryService.exec(node.text); + /** + * @param {Entry} entry + */ + ref(entry) { + entry.connect("activate", this.handleActivate); } -}; - -Prompt.prototype.render = function() { - const { location } = this.props.panelService.getActiveTab(); - - return ( - h("box", { expand: false }, [ - h("box", { border_width: 4 }), - h("label", { - ellipsize: EllipsizeMode.MIDDLE, - label: location.replace(/^file:\/\//, "") + "$", - }), - h("box", { border_width: 4 }), - h("entry", { expand: true, on_activate: this.activate }), - ]) - ); -}; + + render() { + const { getActiveTab } = + /** @type {PanelService} */ (this.props.panelService); + + const { location } = getActiveTab(); + + return ( + h(Box, { expand: false }, [ + h(Box, { border_width: 4 }), + h(Label, { + ellipsize: EllipsizeMode.MIDDLE, + label: location.replace(/^file:\/\//, "") + "$", + }), + h(Box, { border_width: 4 }), + h(Entry, { expand: true, ref: this.ref }), + ]) + ); + } +} exports.Prompt = Prompt; exports.default = connect(["directoryService", "panelService"])(Prompt); diff --git a/src/app/Prompt/Prompt.test.js b/src/app/Prompt/Prompt.test.js index 6b703d2..2b9965a 100644 --- a/src/app/Prompt/Prompt.test.js +++ b/src/app/Prompt/Prompt.test.js @@ -34,7 +34,7 @@ describe("Prompt", () => { directoryService, panelService, }) - .activate({ text: "x-terminal-emulator -e ranger" }); + .handleActivate({ text: "x-terminal-emulator -e ranger" }); expect(directoryService.exec).toHaveBeenCalledWith("x-terminal-emulator -e ranger"); }); diff --git a/src/app/Ref/RefService.test.js b/src/app/Ref/RefService.test.js index 7afa256..e70e2fa 100644 --- a/src/app/Ref/RefService.test.js +++ b/src/app/Ref/RefService.test.js @@ -1,8 +1,5 @@ const expect = require("expect"); -const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; const { RefService } = require("./RefService"); -const { render } = require("inferno"); describe("RefService", () => { it("stores reference", () => { diff --git a/src/app/Select/Select.js b/src/app/Select/Select.js index bca018f..ae4bf36 100644 --- a/src/app/Select/Select.js +++ b/src/app/Select/Select.js @@ -1,8 +1,8 @@ const { ComboBox } = imports.gi.Gtk; const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; const isEqual = require("lodash/isEqual"); const { autoBind } = require("../Gjs/autoBind"); +const { h } = require("../Gjs/GtkInferno"); const ListStore = require("../ListStore/ListStore"); /** @@ -14,74 +14,74 @@ const ListStore = require("../ListStore/ListStore"); * @property {any[]} rows * @property {string} value * - * @param {IProps} props + * @extends Component */ -function Select(props) { - Component.call(this, props); - autoBind(this, Select.prototype, __filename); -} +class Select extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); -Select.prototype = Object.create(Component.prototype); + /** + * @type {ComboBox} + */ + this.node = (/** @type {any} */ (undefined)); -/** - * @type {ComboBox} - */ -Select.prototype.node = undefined; - -/** - * @type {IProps} - */ -Select.prototype.props = undefined; + autoBind(this, Select.prototype, __filename); + } -/** - * @param {ComboBox} node - */ -Select.prototype.init = function(node) { - if (!node || this.node) { - return; + /** + * @param {IProps} nextProps + */ + shouldComponentUpdate(nextProps) { + return !isEqual(this.props, nextProps); } - /** @type {any} FIXME */ - const store = ListStore.fromProps(this.props); + componentDidUpdate() { + const store = ListStore.fromProps(this.props); - this.node = node; - node.set_model(store); - this.props.cols.forEach((col, i) => ListStore.configureColumn(node, col, i)); - this.updateActive(); - this.props.on_layout(node); -}; + this.node.set_model(store); + this.updateActive(); + } -/** - * @param {IProps} nextProps - */ -Select.prototype.shouldComponentUpdate = function(nextProps) { - return !isEqual(this.props, nextProps); -}; + updateActive() { + for (let i = 0; i < this.props.rows.length; i++) { + if (this.props.rows[i].value === this.props.value) { + this.node.set_active(i); + break; + } + } + } -Select.prototype.componentDidUpdate = function() { - /** @type {any} FIXME */ - const store = ListStore.fromProps(this.props); + /** + * @param {ComboBox} node + */ + ref(node) { + if (!node || this.node) { + return; + } - this.node.set_model(store); - this.updateActive(); -}; + this.node = node; + node.connect("changed", this.props.on_changed); + node.connect("focus", this.props.on_focus); -Select.prototype.updateActive = function() { - for (let i = 0; i < this.props.rows.length; i++) { - if (this.props.rows[i].value === this.props.value) { - this.node.set_active(i); - break; - } + const store = ListStore.fromProps(this.props); + node.set_model(store); + this.props.cols.forEach((col, i) => ListStore.configureColumn(node, col, i)); + + this.updateActive(); + this.props.on_layout(node); } -}; -Select.prototype.render = function() { - return h("combo-box", { - focus_on_click: false, - on_changed: this.props.on_changed, - on_focus: this.props.on_focus, - ref: this.init, - }); -}; + render() { + return ( + h(ComboBox, { + focus_on_click: false, + ref: this.ref, + }) + ); + } +} exports.default = Select; diff --git a/src/app/Selection/SelectionService.js b/src/app/Selection/SelectionService.js index 23df702..264e112 100644 --- a/src/app/Selection/SelectionService.js +++ b/src/app/Selection/SelectionService.js @@ -16,13 +16,13 @@ const { TabService } = require("../Tab/TabService"); class SelectionService { /** * @typedef IProps - * @property {ClipboardService} clipboardService - * @property {DialogService} dialogService - * @property {GioService} gioService - * @property {JobService} jobService - * @property {PanelService} panelService - * @property {RefService} refService - * @property {TabService} tabService + * @property {ClipboardService?} [clipboardService] + * @property {DialogService?} [dialogService] + * @property {GioService?} [gioService] + * @property {JobService?} [jobService] + * @property {PanelService?} [panelService] + * @property {RefService?} [refService] + * @property {TabService?} [tabService] * * @param {IProps} props */ @@ -46,7 +46,9 @@ class SelectionService { * Stores URIs in clipboard, for another app to copy. */ copy() { - const { copy } = this.props.clipboardService; + const { copy } = + /** @type {ClipboardService} */ (this.props.clipboardService); + copy(this.getUris()); } @@ -54,7 +56,9 @@ class SelectionService { * Stores URIs in clipboard, for another app to move. */ cut() { - const { cut } = this.props.clipboardService; + const { cut } = + /** @type {ClipboardService} */ (this.props.clipboardService); + cut(this.getUris()); } @@ -62,23 +66,35 @@ class SelectionService { * Deselects all files. */ deselectAll() { - const { panelService, tabService } = this.props; - tabService.deselectAll(panelService.getActiveTabId()); + const { getActiveTabId } = + /** @type {PanelService} */ (this.props.panelService); + + const { deselectAll } = + /** @type {TabService} */ (this.props.tabService); + + deselectAll(getActiveTabId()); } /** * Deselects files, prompting for name pattern. */ deselectGlob() { - const { dialogService, panelService, tabService } = this.props; + const { prompt } = + /** @type {DialogService} */ (this.props.dialogService); + + const { getActiveTabId } = + /** @type {PanelService} */ (this.props.panelService); + + const { deselectGlob } = + /** @type {TabService} */ (this.props.tabService); - dialogService.prompt("Pattern:", "", pattern => { + prompt("Pattern:", "", pattern => { if (!pattern) { return; } - tabService.deselectGlob({ - id: panelService.getActiveTabId(), + deselectGlob({ + id: getActiveTabId(), pattern, }); }); @@ -97,13 +113,19 @@ class SelectionService { * Returns file objects. */ getFiles() { - const { panelService, tabService } = this.props; + const { getActiveTabId } = + /** @type {PanelService} */ (this.props.panelService); - const activeTabId = panelService.getActiveTabId(); - const { cursor, selected } = tabService.entities[activeTabId]; - const files = tabService.visibleFiles[activeTabId]; + const { entities, visibleFiles } = + /** @type {TabService} */ (this.props.tabService); - return selected.length ? selected.map(index => files[index]) : [files[cursor]]; + const activeTabId = getActiveTabId(); + const { cursor, selected } = entities[activeTabId]; + const files = visibleFiles[activeTabId]; + + return selected.length + ? selected.map(index => files[index]) + : [files[cursor]]; } /** @@ -117,33 +139,46 @@ class SelectionService { * Deselects selected files, and selects non-selected files. */ invert() { - const { panelService, tabService } = this.props; - tabService.invert(panelService.getActiveTabId()); + const { getActiveTabId } = + /** @type {PanelService} */ (this.props.panelService); + + const { invert } = + /** @type {TabService} */ (this.props.tabService); + + invert(getActiveTabId()); } /** * @param {{ keyEvent?: Event, mouseEvent?: Event, rect?: Rectangle, win?: Window }} props */ menu(props) { - const { dialogService, gioService, refService } = this.props; + const { alert } = + /** @type {DialogService} */ (this.props.dialogService); + + const { getHandlers } = + /** @type {GioService} */ (this.props.gioService); + + const { get } = + /** @type {RefService} */ (this.props.refService); + const { keyEvent, mouseEvent, rect, win } = props; const uri = this.getUris()[0]; - gioService.getHandlers(uri, (error, result) => { + getHandlers(uri, (error, result) => { if (error) { - dialogService.alert(error.message); + alert(error.message); return; } const { contentType, handlers } = result; if (!handlers.length) { - dialogService.alert("No handlers registered for " + contentType + "."); + alert("No handlers registered for " + contentType + "."); return; } this.setHandlers(handlers); - const menu = refService.get("ctxMenu"); + const menu = get("ctxMenu"); if (mouseEvent) { menu.popup_at_pointer(mouseEvent); @@ -159,9 +194,14 @@ class SelectionService { * Removes files, prompting for confirmation. */ rm() { - const { confirm } = this.props.dialogService; - const { run } = this.props.jobService; - const { refresh } = this.props.panelService; + const { confirm } = + /** @type {DialogService} */ (this.props.dialogService); + + const { run } = + /** @type {JobService} */ (this.props.jobService); + + const { refresh } = + /** @type {PanelService} */ (this.props.panelService); const uris = this.getUris(); const urisStr = this.formatUris(); @@ -186,8 +226,13 @@ class SelectionService { * Selects all files. */ selectAll() { - const { panelService, tabService } = this.props; - tabService.selectAll(panelService.getActiveTabId()); + const { getActiveTabId } = + /** @type {PanelService} */ (this.props.panelService); + + const { selectAll } = + /** @type {TabService} */ (this.props.tabService); + + selectAll(getActiveTabId()); } /** @@ -195,24 +240,32 @@ class SelectionService { * opposite panel. */ selectDiff() { - const { panelService, tabService } = this.props; + const { entities } = + /** @type {PanelService} */ (this.props.panelService); + + const { selectDiff } = + /** @type {TabService} */ (this.props.tabService); - tabService.selectDiff( - panelService.entities[0].activeTabId, - panelService.entities[1].activeTabId, - ); + selectDiff(entities[0].activeTabId, entities[1].activeTabId); } /** * Selects files, prompting for name pattern. */ selectGlob() { - const { dialogService, panelService, tabService } = this.props; + const { prompt } = + /** @type {DialogService} */ (this.props.dialogService); + + const { getActiveTabId } = + /** @type {PanelService} */ (this.props.panelService); - dialogService.prompt("Pattern:", "", pattern => { + const { selectGlob } = + /** @type {TabService} */ (this.props.tabService); + + prompt("Pattern:", "", pattern => { if (pattern) { - tabService.selectGlob({ - id: panelService.getActiveTabId(), + selectGlob({ + id: getActiveTabId(), pattern, }); } @@ -227,16 +280,6 @@ class SelectionService { setHandlers(handlers) { this.handlers = handlers; } - - /** - * @private - */ - getActiveTab() { - const { panelService, tabService } = this.props; - const tabId = panelService.getActiveTabId(); - - return tabService.entities[tabId]; - } } exports.SelectionService = SelectionService; diff --git a/src/app/Services.js b/src/app/Services.js index 7f1552e..46040c5 100644 --- a/src/app/Services.js +++ b/src/app/Services.js @@ -15,108 +15,110 @@ const { SelectionService } = require("./Selection/SelectionService"); const { TabService } = require("./Tab/TabService"); const { WindowService } = require("./Window/WindowService"); -/** - * @param {Window} window - */ -function Services(window) { - const dialogService = new DialogService(window); - - const gioService = new GioService(); - - const refService = new RefService(); - - // --- - - const clipboardService = new ClipboardService({ - gioService, - }); - - const jobService = new JobService({ - refService, - }); - - const placeService = new PlaceService({ - refService, - }); - - const tabService = new TabService({ - gioService, - }); - - // --- - - const panelService = new PanelService({ - dialogService, - placeService, - tabService, - }); - - // --- - - const directoryService = new DirectoryService({ - clipboardService, - dialogService, - gioService, - jobService, - panelService, - }); - - const windowService = new WindowService({ - panelService, - placeService, - tabService, - window, - }); - - // --- - - const cursorService = new CursorService({ - dialogService, - directoryService, - gioService, - panelService, - tabService, - }); - - const selectionService = new SelectionService({ - clipboardService, - dialogService, - gioService, - jobService, - panelService, - refService, - tabService, - }); - - // --- - - const oppositeService = new OppositeService({ - dialogService, - jobService, - panelService, - selectionService, - tabService, - }); - - // --- - - this.clipboardService = clipboardService; - this.cursorService = cursorService; - this.dialogService = dialogService; - this.directoryService = directoryService; - this.gioService = gioService; - this.jobService = jobService; - this.oppositeService = oppositeService; - this.panelService = panelService; - this.placeService = placeService; - this.refService = refService; - this.selectionService = selectionService; - this.tabService = tabService; - this.windowService = windowService; - - // --- - - this.actionService = new ActionService(this); +class Services { + /** + * @param {Window} window + */ + constructor(window) { + const dialogService = new DialogService(window); + + const gioService = new GioService(); + + const refService = new RefService(); + + // --- + + const clipboardService = new ClipboardService({ + gioService, + }); + + const jobService = new JobService({ + refService, + }); + + const placeService = new PlaceService({ + refService, + }); + + const tabService = new TabService({ + gioService, + }); + + // --- + + const panelService = new PanelService({ + dialogService, + placeService, + tabService, + }); + + // --- + + const directoryService = new DirectoryService({ + clipboardService, + dialogService, + gioService, + jobService, + panelService, + }); + + const windowService = new WindowService({ + panelService, + placeService, + tabService, + window, + }); + + // --- + + const cursorService = new CursorService({ + dialogService, + directoryService, + gioService, + panelService, + tabService, + }); + + const selectionService = new SelectionService({ + clipboardService, + dialogService, + gioService, + jobService, + panelService, + refService, + tabService, + }); + + // --- + + const oppositeService = new OppositeService({ + dialogService, + jobService, + panelService, + selectionService, + tabService, + }); + + // --- + + this.clipboardService = clipboardService; + this.cursorService = cursorService; + this.dialogService = dialogService; + this.directoryService = directoryService; + this.gioService = gioService; + this.jobService = jobService; + this.oppositeService = oppositeService; + this.panelService = panelService; + this.placeService = placeService; + this.refService = refService; + this.selectionService = selectionService; + this.tabService = tabService; + this.windowService = windowService; + + // --- + + this.actionService = new ActionService(this); + } } exports.Services = Services; diff --git a/src/app/Stats/Stats.js b/src/app/Stats/Stats.js index c2bd95d..34e3125 100644 --- a/src/app/Stats/Stats.js +++ b/src/app/Stats/Stats.js @@ -1,9 +1,10 @@ +const { Box, Label } = imports.gi.Gtk; const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; const { connect } = require("inferno-mobx"); const { computed, extendObservable } = require("mobx"); const { File } = require("../../domain/File/File"); const { autoBind } = require("../Gjs/autoBind"); +const { h } = require("../Gjs/GtkInferno"); const { PanelService } = require("../Panel/PanelService"); const formatSize = require("../Size/formatSize").default; const { TabService } = require("../Tab/TabService"); @@ -11,60 +12,66 @@ const { TabService } = require("../Tab/TabService"); /** * @typedef IProps * @property {number} panelId - * @property {PanelService} panelService - * @property {TabService} tabService + * @property {PanelService?} [panelService] + * @property {TabService?} [tabService] * - * @param {IProps} props + * @extends Component */ -function Stats(props) { - Component.call(this, props); - autoBind(this, Stats.prototype, __filename); +class Stats extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); - extendObservable(this, { - data: computed(this.getData), - }); -} + /** + * @type {{ selectedCount: number, selectedSize: number, totalCount: number, totalSize: number }} + */ + this.data = (/** @type {any} */ (undefined)); -Stats.prototype = Object.create(Component.prototype); + autoBind(this, Stats.prototype, __filename); -/** - * @type {{ selectedCount: number, selectedSize: number, totalCount: number, totalSize: number }} - */ -Stats.prototype.data = undefined; + extendObservable(this, { + data: computed(this.getData), + }); + } -/** - * @type {IProps} - */ -Stats.prototype.props = undefined; + getData() { + const { panelId } = this.props; + + const { getActiveTabId } = + /** @type {PanelService} */ (this.props.panelService); -Stats.prototype.getData = function() { - const { panelId, panelService, tabService } = this.props; - const tabId = panelService.getActiveTabId(panelId); - const tab = tabService.entities[tabId]; + const { entities, visibleFiles } = + /** @type {TabService} */ (this.props.tabService); - const files = tabService.visibleFiles[tabId]; - const selected = tab.selected; + const tabId = getActiveTabId(panelId); + const tab = entities[tabId]; - return { - selectedCount: selected.length, - selectedSize: TotalSize(selected.map(index => files[index])), - totalCount: files.length, - totalSize: TotalSize(files), - }; -}; + const files = visibleFiles[tabId]; + const selected = tab.selected; -Stats.prototype.render = function() { - const { selectedCount, selectedSize, totalCount, totalSize } = this.data; + return { + selectedCount: selected.length, + selectedSize: TotalSize(selected.map(index => files[index])), + totalCount: files.length, + totalSize: TotalSize(files), + }; + } - return ( - h("box", { border_width: 4 }, [ - h("label", { - label: formatSize(selectedSize) + " / " + formatSize(totalSize) + - " in " + selectedCount + " / " + totalCount + " file(s)", - }), - ]) - ); -}; + render() { + const { selectedCount, selectedSize, totalCount, totalSize } = this.data; + + return ( + h(Box, { border_width: 4 }, [ + h(Label, { + label: formatSize(selectedSize) + " / " + formatSize(totalSize) + + " in " + selectedCount + " / " + totalCount + " file(s)", + }), + ]) + ); + } +} exports.Stats = Stats; exports.default = connect(["panelService", "tabService"])(Stats); diff --git a/src/app/Stats/Stats.test.js b/src/app/Stats/Stats.test.js index ea8a5f6..49a39c1 100644 --- a/src/app/Stats/Stats.test.js +++ b/src/app/Stats/Stats.test.js @@ -1,6 +1,6 @@ const expect = require("expect"); -const h = require("inferno-hyperscript").default; const { Stats } = require("./Stats"); +const { h } = require("../Gjs/GtkInferno"); const { shallow } = require("../Test/Test"); describe("Stats", () => { diff --git a/src/app/Tab/TabList.js b/src/app/Tab/TabList.js index 5972a56..3cf09a9 100644 --- a/src/app/Tab/TabList.js +++ b/src/app/Tab/TabList.js @@ -1,46 +1,46 @@ +const { Box } = imports.gi.Gtk; const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; const { connect } = require("inferno-mobx"); const { autoBind } = require("../Gjs/autoBind"); +const { h } = require("../Gjs/GtkInferno"); const { PanelService } = require("../Panel/PanelService"); const TabListItem = require("./TabListItem").default; /** * @typedef IProps * @property {number} panelId - * @property {PanelService} panelService + * @property {PanelService?} [panelService] * - * @param {IProps} props + * @extends Component */ -function TabList(props) { - Component.call(this, props); - autoBind(this, TabList.prototype, __filename); -} +class TabList extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + autoBind(this, TabList.prototype, __filename); + } -TabList.prototype = Object.create(Component.prototype); + render() { + const { entities } = + /** @type {PanelService} */ (this.props.panelService); -/** - * @type {IProps} - */ -TabList.prototype.props = undefined; + const panel = entities[this.props.panelId]; + const { activeTabId, tabIds } = panel; -TabList.prototype.render = function() { - const panel = this.props.panelService.entities[this.props.panelId]; - const { activeTabId, tabIds } = panel; - - return tabIds.length === 1 ? h("box") : ( - h("box", [ - tabIds.map(id => ( - h(TabListItem, { - active: activeTabId === id, - id: id, - key: id, - panelId: this.props.panelId, - }) - )), - ]) + return tabIds.length === 1 ? h(Box) : ( + h(Box, tabIds.map(id => ( + h(TabListItem, { + active: activeTabId === id, + id: id, + key: id, + panelId: this.props.panelId, + })), + )) ); -}; + } +} exports.TabList = TabList; exports.default = connect(["panelService"])(TabList); diff --git a/src/app/Tab/TabList.test.js b/src/app/Tab/TabList.test.js index d0f9c46..d8888d9 100644 --- a/src/app/Tab/TabList.test.js +++ b/src/app/Tab/TabList.test.js @@ -1,10 +1,11 @@ const assign = require("lodash/assign"); -const h = require("inferno-hyperscript").default; +const { h } = require("../Gjs/GtkInferno"); const { shallow } = require("../Test/Test"); const { TabList } = require("./TabList"); describe("TabList", () => { it("renders without crashing", () => { + /** @type {any} */ const panelService = { entities: { "0": { @@ -17,7 +18,7 @@ describe("TabList", () => { shallow( h(TabList, { panelId: 0, - panelService: panelService, + panelService, }), ); }); diff --git a/src/app/Tab/TabListItem.js b/src/app/Tab/TabListItem.js index 2de134e..dd11e0f 100644 --- a/src/app/Tab/TabListItem.js +++ b/src/app/Tab/TabListItem.js @@ -1,8 +1,8 @@ -const { IconSize, ReliefStyle } = imports.gi.Gtk; +const { Box, IconSize, Image, Label, ReliefStyle } = imports.gi.Gtk; const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; const { connect } = require("inferno-mobx"); const { autoBind } = require("../Gjs/autoBind"); +const { h } = require("../Gjs/GtkInferno"); const { PanelService } = require("../Panel/PanelService"); const { TabService } = require("../Tab/TabService"); const ToggleButton = require("../ToggleButton/ToggleButton").default; @@ -13,51 +13,52 @@ const ToggleButton = require("../ToggleButton/ToggleButton").default; * @property {string} icon * @property {number} id * @property {number} panelId - * @property {PanelService} panelService - * @property {TabService} tabService + * @property {PanelService?} [panelService] + * @property {TabService?} [tabService] * - * @param {IProps} props + * @extends Component */ -function TabListItem(props) { - Component.call(this, props); - autoBind(this, TabListItem.prototype, __filename); -} +class TabListItem extends Component { + /** + * @param {IProps} props + */ + constructor(props) { + super(props); + autoBind(this, TabListItem.prototype, __filename); + } -TabListItem.prototype = Object.create(Component.prototype); + handleClicked() { + const panelService = /** @type {PanelService} */ (this.props.panelService); + panelService.setActiveTab(this.props.id); + } -/** - * @type {IProps} - */ -TabListItem.prototype.props = undefined; + render() { + const { active, icon } = this.props; + const { entities } = /** @type {TabService} */ (this.props.tabService); -TabListItem.prototype.handleClicked = function() { - this.props.panelService.setActiveTab(this.props.id); -}; + const { location } = entities[this.props.id]; + let text = location.replace(/^.*\//, "") || "/"; -TabListItem.prototype.render = function() { - const { active, icon } = this.props; - const { location } = this.props.tabService.entities[this.props.id]; - let text = location.replace(/^.*\//, "") || "/"; - - return ( - h(ToggleButton, { - active: active, - can_focus: false, - on_clicked: this.handleClicked, - relief: ReliefStyle.NONE, - }, [ - h("box", { spacing: 4 }, [ - icon ? ( - h("image", { - icon_name: icon + "-symbolic", - icon_size: IconSize.SMALL_TOOLBAR, - }) - ) : null, - h("label", { label: text }), - ]), - ]) - ); -}; + return ( + h(ToggleButton, { + active: active, + can_focus: false, + pressedCallback: this.handleClicked, + relief: ReliefStyle.NONE, + }, [ + h(Box, { spacing: 4 }, [ + icon ? ( + h(Image, { + icon_name: icon + "-symbolic", + icon_size: IconSize.SMALL_TOOLBAR, + }) + ) : null, + h(Label, { label: text }), + ]), + ]) + ); + } +} exports.TabListItem = TabListItem; exports.default = connect(["panelService", "tabService"])(TabListItem); diff --git a/src/app/Tab/TabListItem.test.js b/src/app/Tab/TabListItem.test.js index d320a56..a056002 100644 --- a/src/app/Tab/TabListItem.test.js +++ b/src/app/Tab/TabListItem.test.js @@ -1,12 +1,13 @@ const expect = require("expect"); -const h = require("inferno-hyperscript").default; const assign = require("lodash/assign"); const noop = require("lodash/noop"); +const { h } = require("../Gjs/GtkInferno"); const { shallow } = require("../Test/Test"); const { TabListItem } = require("./TabListItem"); describe("TabListItem", () => { it("renders without crashing", () => { + /** @type {any} */ const tabService = { entities: { "0": { location: "file:///" }, @@ -18,7 +19,7 @@ describe("TabListItem", () => { h(TabListItem, { id: 0, panelId: 1, - tabService: tabService, + tabService, }), ); @@ -26,7 +27,7 @@ describe("TabListItem", () => { h(TabListItem, { id: 1, panelId: 1, - tabService: tabService, + tabService, }), ); }); diff --git a/src/app/Tab/TabService.js b/src/app/Tab/TabService.js index 12acd36..631e501 100644 --- a/src/app/Tab/TabService.js +++ b/src/app/Tab/TabService.js @@ -127,7 +127,7 @@ class TabService { this.showHidSys = false; /** @type {{ [id: string]: File[] }} */ - this.visibleFiles = undefined; + this.visibleFiles = {}; autoBind(this, TabService.prototype, __filename); diff --git a/src/app/Tab/TabService.test.js b/src/app/Tab/TabService.test.js index fa16a4b..c5d8a0f 100644 --- a/src/app/Tab/TabService.test.js +++ b/src/app/Tab/TabService.test.js @@ -99,7 +99,7 @@ describe("TabService", () => { return file; }); tabService.entities[0].selected = []; - tabService.entities[0].sortedBy = undefined; + tabService.entities[0].sortedBy = ""; tabService.sorted({ tabId: 0, by: "filename" }); expect(tabService.entities[0].files.map(x => x.name)).toEqual([ @@ -178,9 +178,9 @@ describe("TabService", () => { file.name = name; return file; - }), - tabService.entities[0].selected = []; - tabService.entities[0].sortedBy = undefined; + }); + tabService.entities[0].selected = []; + tabService.entities[0].sortedBy = ""; tabService.sorted({ tabId: 0, by: "ext" }); expect(tabService.entities[0].files.map(x => x.name)).toEqual([ diff --git a/src/app/Test/Test.js b/src/app/Test/Test.js index 1389f72..30b14f4 100644 --- a/src/app/Test/Test.js +++ b/src/app/Test/Test.js @@ -9,16 +9,16 @@ exports.EmptyArray = []; exports.EmptyProps = {}; /** @type {string} */ -exports.NoString = null; +exports.NoString = (/** @type {any} */ (null)); let description = ""; /** - * @param {string} _description + * @param {string} nextDescription * @param {() => void} callback */ -function describe(_description, callback) { - description = _description; +function describe(nextDescription, callback) { + description = nextDescription; callback(); description = ""; } diff --git a/src/app/ToggleButton/ToggleButton.js b/src/app/ToggleButton/ToggleButton.js index f6e8771..33ba4dd 100644 --- a/src/app/ToggleButton/ToggleButton.js +++ b/src/app/ToggleButton/ToggleButton.js @@ -1,62 +1,62 @@ -const { StateFlags } = imports.gi.Gtk; +const { Button, StateFlags } = imports.gi.Gtk; const assign = require("lodash/assign"); const Component = require("inferno-component").default; -const h = require("inferno-hyperscript").default; const { autoBind } = require("../Gjs/autoBind"); +const { h } = require("../Gjs/GtkInferno"); /** - * @typedef INode - * @property {(flags: number, clear: boolean) => void} set_state_flags - * @property {(flags: number) => void} unset_state_flags - * * @typedef IProps * @property {boolean} active + * @property {any?} [pressedCallback] * - * @param {IProps} props + * @extends Component */ -function ToggleButton(props) { - Component.call(this, props); - autoBind(this, ToggleButton.prototype, __filename); -} +class ToggleButton extends Component { + /** + * @param {Partial