Compare commits

..

No commits in common. "6bd4d97db2fc7971519a3c42a23e556d27a68b35" and "7737e3ad6dec499ca5ce9c99466667c6e85f0cca" have entirely different histories.

168 changed files with 4239 additions and 6499 deletions

3
.gitignore vendored
View File

@ -1,6 +1,3 @@
.idea/
*.zip
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/

View File

@ -1,3 +1,5 @@
# SolarFM
# SolarFM
SolarFM is a Gtk+ Python file manager.
@ -12,11 +14,6 @@ sudo apt-get install python3.8 wget python3-setproctitle python3-gi ffmpegthumbn
# TODO
<ul>
<li>Add simpleish plugin system to run bash/python scripts.</li>
<li>Add simpleish preview plugin for various file types.</li>
<li>Add simpleish file chmod, chown, stats, etc plugin for file management.</li>
<li>Add simpleish search plugin to do recursive search and show.</li>
<li>Add simpleish bulk-renamer.</li>
<li>Add a basic favorites manager plugin.</li>
</ul>
# Images

Binary file not shown.

View File

@ -1,61 +0,0 @@
### Note
Copy the example and rename it to your desired name. Plugins define a ui target slot with the 'ui_target' requests data but don't have to if not directly interacted with.
Plugins must have a run method defined; though, you do not need to necessarily do anything within it. The run method implies that the passed in event system or other data is ready for the plugin to use.
### Manifest Example (All are required even if empty.)
```
class Manifest:
path: str = os.path.dirname(os.path.realpath(__file__))
name: str = "Example Plugin"
author: str = "John Doe"
version: str = "0.0.1"
support: str = ""
requests: {} = {
'ui_target': "plugin_control_list",
'pass_fm_events': "true"
}
```
### Requests
```
requests: {} = {
'ui_target': "plugin_control_list",
'ui_target_id': "<some other Gtk Glade ID>" # Only needed if using "other" in "ui_target". See below for predefined "ui_target" options...
'pass_fm_events': "true" # If empty or not present will be ignored.
'bind_keys': [f"{name}||send_message:<Control>f"],
f"{name}||do_save:<Control>s"] # Bind keys with method and key pare using list. Must pass "name" like shown with delimiter to its right.
}
```
UI Targets:
<ul>
<li>main_Window</li>
<li>main_menu_bar</li>
<li>path_menu_bar</li>
<li>plugin_control_list</li>
<li>window_(1-4)</li>
<li>context_menu</li>
<li>other</li>
</ul>
### Methods
```
# Must define and return a widget if "ui_target" is defined.
def get_ui_element(self):
button = Gtk.Button(label=self.name)
button.connect("button-release-event", self._do_download)
return button
# Must define in plugin if "pass_fm_events" is set to "true" string.
def set_fm_event_system(self, fm_event_system):
self._fm_event_system = fm_event_system
# Must define regardless if needed. Can just pass if plugin does stuff in its __init__
def run(self):
self._module_event_observer()
```

2
plugins/README.txt Normal file
View File

@ -0,0 +1,2 @@
### Note
Copy the example and rename it to your desired name. The Main class and passed in arguments are required. You don't necessarily need to use the passed in socket_id or event_system.

View File

@ -0,0 +1,37 @@
# Python imports
import sys, traceback, threading, inspect, os, time
# Gtk imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class Main:
def __init__(self, socket_id, event_system):
self._socket_id = socket_id
self._event_system = event_system
self._gtk_plug = Gtk.Plug.new(self._socket_id)
self.start_loop()
@threaded
def start_loop(self):
i = 0
cycles = 5
alive = True
while alive:
if i == cycles:
alive = False
self._event_system.push_gui_event(["some_type", "display_message", ("warning", str(i), None)])
i += 1
time.sleep(1)

View File

@ -1,685 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.16"/>
<object class="GtkDialog" id="file_properties_dialog">
<property name="can-focus">False</property>
<property name="border-width">6</property>
<property name="title" translatable="yes">File Properties</property>
<property name="modal">True</property>
<property name="window-position">center-on-parent</property>
<property name="default-width">420</property>
<property name="destroy-with-parent">True</property>
<property name="type-hint">dialog</property>
<property name="skip-taskbar-hint">True</property>
<property name="skip-pager-hint">True</property>
<property name="gravity">center</property>
<signal name="response" handler="on_filePropertiesDlg_response" swapped="no"/>
<child internal-child="vbox">
<object class="GtkBox" id="dialog_vbox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">12</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="dialog_action_area">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="can-default">True</property>
<property name="receives-default">False</property>
<property name="use-stock">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="ok_button">
<property name="label">gtk-ok</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="can-default">True</property>
<property name="receives-default">False</property>
<property name="use-stock">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack-type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkNotebook" id="notebook">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="border-width">6</property>
<child>
<object class="GtkAlignment" id="alignment2">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="top-padding">6</property>
<property name="bottom-padding">6</property>
<property name="left-padding">12</property>
<child>
<object class="GtkTable" id="general_table">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">4</property>
<property name="n-rows">7</property>
<property name="n-columns">2</property>
<property name="column-spacing">12</property>
<property name="row-spacing">6</property>
<child>
<object class="GtkLabel" id="label_filename">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;File _Name:&lt;/b&gt;</property>
<property name="use-markup">True</property>
<property name="use-underline">True</property>
<property name="mnemonic-widget">file_name</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkEntry" id="file_name">
<property name="visible">True</property>
<property name="can-focus">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="right-attach">2</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkLabel" id="label20">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;_Location:&lt;/b&gt;</property>
<property name="use-markup">True</property>
<property name="use-underline">True</property>
<property name="mnemonic-widget">file_location</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="top-attach">1</property>
<property name="bottom-attach">2</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkEntry" id="file_location">
<property name="visible">True</property>
<property name="can-focus">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="right-attach">2</property>
<property name="top-attach">1</property>
<property name="bottom-attach">2</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_target">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Link _Target:&lt;/b&gt;</property>
<property name="use-markup">True</property>
<property name="use-underline">True</property>
<property name="mnemonic-widget">file_target</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="top-attach">2</property>
<property name="bottom-attach">3</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkEntry" id="file_target">
<property name="visible">True</property>
<property name="can-focus">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="right-attach">2</property>
<property name="top-attach">2</property>
<property name="bottom-attach">3</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Type:&lt;/b&gt;</property>
<property name="use-markup">True</property>
<property name="use-underline">True</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="top-attach">3</property>
<property name="bottom-attach">4</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options">GTK_FILL</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="mime_type">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="selectable">True</property>
<property name="ellipsize">end</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="right-attach">2</property>
<property name="top-attach">3</property>
<property name="bottom-attach">4</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkLabel" id="label5">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Size:&lt;/b&gt;</property>
<property name="use-markup">True</property>
<property name="use-underline">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="top-attach">4</property>
<property name="bottom-attach">5</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkLabel" id="file_size">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="selectable">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="right-attach">2</property>
<property name="top-attach">4</property>
<property name="bottom-attach">5</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkLabel" id="label7">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;_Modified:&lt;/b&gt;</property>
<property name="use-markup">True</property>
<property name="use-underline">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="top-attach">5</property>
<property name="bottom-attach">6</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkLabel" id="label13">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;_Accessed:&lt;/b&gt;</property>
<property name="use-markup">True</property>
<property name="use-underline">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="top-attach">6</property>
<property name="bottom-attach">7</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkEntry" id="mtime">
<property name="visible">True</property>
<property name="can-focus">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="right-attach">2</property>
<property name="top-attach">5</property>
<property name="bottom-attach">6</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkEntry" id="atime">
<property name="visible">True</property>
<property name="can-focus">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="right-attach">2</property>
<property name="top-attach">6</property>
<property name="bottom-attach">7</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="tab">
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">_Info</property>
<property name="use-underline">True</property>
</object>
<packing>
<property name="tab-fill">False</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="alignment1">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="top-padding">6</property>
<property name="bottom-padding">6</property>
<property name="left-padding">12</property>
<child>
<object class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkTable" id="table3">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">2</property>
<property name="n-rows">2</property>
<property name="n-columns">2</property>
<property name="column-spacing">12</property>
<property name="row-spacing">6</property>
<child>
<object class="GtkLabel" id="owner_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;_Owner:&lt;/b&gt;</property>
<property name="use-markup">True</property>
<property name="use-underline">True</property>
<property name="mnemonic-widget">file_owner</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkLabel" id="group_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;_Group:&lt;/b&gt;</property>
<property name="use-markup">True</property>
<property name="use-underline">True</property>
<property name="mnemonic-widget">file_group</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="top-attach">1</property>
<property name="bottom-attach">2</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkEntry" id="file_owner">
<property name="visible">True</property>
<property name="can-focus">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="right-attach">2</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkEntry" id="file_group">
<property name="visible">True</property>
<property name="can-focus">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="right-attach">2</property>
<property name="top-attach">1</property>
<property name="bottom-attach">2</property>
<property name="y-options"/>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkHSeparator" id="hseparator1">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkTable" id="table2">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">4</property>
<property name="n-rows">3</property>
<property name="n-columns">5</property>
<property name="column-spacing">12</property>
<property name="row-spacing">6</property>
<child>
<object class="GtkLabel" id="label17">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Owner:&lt;/b&gt;</property>
<property name="use-markup">True</property>
<property name="use-underline">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkLabel" id="label18">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Group:&lt;/b&gt;</property>
<property name="use-markup">True</property>
<property name="use-underline">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="top-attach">1</property>
<property name="bottom-attach">2</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkLabel" id="label19">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Other:&lt;/b&gt;</property>
<property name="use-markup">True</property>
<property name="use-underline">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="top-attach">2</property>
<property name="bottom-attach">3</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="owner_r">
<property name="label" translatable="yes">Read</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="border-width">2</property>
<property name="use-underline">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="right-attach">2</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="group_r">
<property name="label" translatable="yes">Read</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="border-width">2</property>
<property name="use-underline">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="right-attach">2</property>
<property name="top-attach">1</property>
<property name="bottom-attach">2</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="others_r">
<property name="label" translatable="yes">Read</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="border-width">2</property>
<property name="use-underline">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="right-attach">2</property>
<property name="top-attach">2</property>
<property name="bottom-attach">3</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="owner_w">
<property name="label" translatable="yes">Write</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="border-width">2</property>
<property name="use-underline">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="left-attach">2</property>
<property name="right-attach">3</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="group_w">
<property name="label" translatable="yes">Write</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="border-width">2</property>
<property name="use-underline">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="left-attach">2</property>
<property name="right-attach">3</property>
<property name="top-attach">1</property>
<property name="bottom-attach">2</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="others_w">
<property name="label" translatable="yes">Write</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="border-width">2</property>
<property name="use-underline">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="left-attach">2</property>
<property name="right-attach">3</property>
<property name="top-attach">2</property>
<property name="bottom-attach">3</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="owner_x">
<property name="label" translatable="yes">Execute</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="border-width">2</property>
<property name="use-underline">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="left-attach">3</property>
<property name="right-attach">4</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="group_x">
<property name="label" translatable="yes">Execute</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="border-width">2</property>
<property name="use-underline">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="left-attach">3</property>
<property name="right-attach">4</property>
<property name="top-attach">1</property>
<property name="bottom-attach">2</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="others_x">
<property name="label" translatable="yes">Execute</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="border-width">2</property>
<property name="use-underline">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="left-attach">3</property>
<property name="right-attach">4</property>
<property name="top-attach">2</property>
<property name="bottom-attach">3</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options"/>
</packing>
</child>
<child>
<object class="GtkVSeparator" id="vseparator1">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="left-attach">4</property>
<property name="right-attach">5</property>
<property name="bottom-attach">3</property>
<property name="x-options">GTK_FILL</property>
<property name="y-options">GTK_FILL</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">_Permissions</property>
<property name="use-underline">True</property>
</object>
<packing>
<property name="position">1</property>
<property name="tab-fill">False</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<action-widgets>
<action-widget response="-6">cancel_button</action-widget>
<action-widget response="-5">ok_button</action-widget>
</action-widgets>
</object>
</interface>

View File

@ -1,283 +0,0 @@
# Python imports
import os, threading, subprocess, time, pwd, grp
from datetime import datetime
# Gtk imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib, Gio
# Application imports
# NOTE: Threads WILL NOT die with parent's destruction.
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
return wrapper
# NOTE: Threads WILL die with parent's destruction.
def daemon_threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class Manifest:
path: str = os.path.dirname(os.path.realpath(__file__))
name: str = "Properties"
author: str = "ITDominator"
version: str = "0.0.1"
support: str = ""
requests: {} = {
'ui_target': "context_menu",
'pass_fm_events': "true"
}
class Properties:
file_uri: str = None
file_name: str = None
file_location: str = None
file_target: str = None
mime_type: str = None
file_size: str = None
mtime: int = None
atime: int = None
file_owner: str = None
file_group: str = None
chmod_stat: str = None
class Plugin(Manifest):
def __init__(self):
self._GLADE_FILE = f"{self.path}/file_properties.glade"
self._builder = None
self._properties_dialog = None
self._event_system = None
self._event_sleep_time = .5
self._event_message = None
self._file_name = None
self._file_location = None
self._file_target = None
self._mime_type = None
self._file_size = None
self._mtime = None
self._atime = None
self._file_owner = None
self._file_group = None
self._chmod_map: {} = {
"7": "rwx",
"6": "rw",
"5": "rx",
"4": "r",
"3": "wx",
"2": "w",
"1": "x",
"0": ""
}
self._chmod_map_counter: {} = {
"rwx": "7",
"rw": "6",
"rx": "5",
"r": "4",
"wx": "3",
"w": "2",
"x": "1",
"": "0"
}
def get_ui_element(self):
self._builder = Gtk.Builder()
self._builder.add_from_file(self._GLADE_FILE)
self._properties_dialog = self._builder.get_object("file_properties_dialog")
self._file_name = self._builder.get_object("file_name")
self._file_location = self._builder.get_object("file_location")
self._file_target = self._builder.get_object("file_target")
self._mime_type = self._builder.get_object("mime_type")
self._file_size = self._builder.get_object("file_size")
self._mtime = self._builder.get_object("mtime")
self._atime = self._builder.get_object("atime")
self._file_owner = self._builder.get_object("file_owner")
self._file_group = self._builder.get_object("file_group")
button = Gtk.Button(label=self.name)
button.connect("button-release-event", self._show_properties_page)
return button
def set_fm_event_system(self, fm_event_system):
self._event_system = fm_event_system
def run(self):
self._module_event_observer()
@threaded
def _show_properties_page(self, widget=None, eve=None):
self._event_system.push_gui_event([self.name, "get_current_state", ()])
self.wait_for_fm_message()
state = self._event_message
self._event_message = None
GLib.idle_add(self._process_changes, (state))
def _process_changes(self, state):
if len(state.selected_files) == 1:
uri = state.selected_files[0]
path = state.tab.get_current_directory()
properties = self._set_ui_data(uri, path)
response = self._properties_dialog.run()
if response in [Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT]:
self._properties_dialog.hide()
self._update_file(properties)
self._properties_dialog.hide()
def _update_file(self, properties):
chmod_stat = self._get_check_boxes()
if chmod_stat is not properties.chmod_stat:
try:
print("\nNew chmod flags...")
print(f"Old: {''.join(properties.chmod_stat)}")
print(f"New: {chmod_stat}")
command = ["chmod", f"{chmod_stat}", properties.file_uri]
with subprocess.Popen(command, stdout=subprocess.PIPE) as proc:
result = proc.stdout.read().decode("UTF-8").strip()
print(result)
except Exception as e:
print(f"Couldn't chmod\nFile: {properties.file_uri}")
print( repr(e) )
owner = self._file_owner.get_text()
group = self._file_group.get_text()
if owner is not properties.file_owner or group is not properties.file_group:
try:
print("\nNew owner/group flags...")
print(f"Old:\n\tOwner: {properties.file_owner}\n\tGroup: {properties.file_group}")
print(f"New:\n\tOwner: {owner}\n\tGroup: {group}")
uid = pwd.getpwnam(owner).pw_uid
gid = grp.getgrnam(group).gr_gid
os.chown(properties.file_uri, uid, gid)
except Exception as e:
print(f"Couldn't chmod\nFile: {properties.file_uri}")
print( repr(e) )
def _set_ui_data(self, uri, path):
properties = Properties()
file_info = Gio.File.new_for_path(uri).query_info(attributes="standard::*,owner::*,time::access,time::changed",
flags=Gio.FileQueryInfoFlags.NONE,
cancellable=None)
is_symlink = file_info.get_attribute_as_string("standard::is-symlink")
properties.file_uri = uri
properties.file_target = file_info.get_attribute_as_string("standard::symlink-target") if is_symlink else ""
properties.file_name = file_info.get_display_name()
properties.file_location = path
properties.mime_type = file_info.get_content_type()
properties.file_size = self._sizeof_fmt(file_info.get_size())
properties.mtime = datetime.fromtimestamp( int(file_info.get_attribute_as_string("time::changed")) ).strftime("%A, %B %d, %Y %I:%M:%S")
properties.atime = datetime.fromtimestamp( int(file_info.get_attribute_as_string("time::access")) ).strftime("%A, %B %d, %Y %I:%M:%S")
properties.file_owner = file_info.get_attribute_as_string("owner::user")
properties.file_group = file_info.get_attribute_as_string("owner::group")
# NOTE: Read = 4, Write = 2, Exec = 1
command = ["stat", "-c", "%a", uri]
with subprocess.Popen(command, stdout=subprocess.PIPE) as proc:
properties.chmod_stat = list(proc.stdout.read().decode("UTF-8").strip())
owner = self._chmod_map[f"{properties.chmod_stat[0]}"]
group = self._chmod_map[f"{properties.chmod_stat[1]}"]
others = self._chmod_map[f"{properties.chmod_stat[2]}"]
self._reset_check_boxes()
self._set_check_boxes([["owner", owner], ["group", group], ["others", others]])
self._file_name.set_text(properties.file_name)
self._file_location.set_text(properties.file_location)
self._file_target.set_text(properties.file_target)
self._mime_type.set_label(properties.mime_type)
self._file_size.set_label(properties.file_size)
self._mtime.set_text(properties.mtime)
self._atime.set_text(properties.atime)
self._file_owner.set_text(properties.file_owner)
self._file_group.set_text(properties.file_group)
return properties
def _get_check_boxes(self):
perms = [[], [], []]
for i, target in enumerate(["owner", "group", "others"]):
for type in ["r", "w", "x"]:
is_active = self._builder.get_object(f"{target}_{type}").get_active()
if is_active:
perms[i].append(type)
digits = []
for perm in perms:
digits.append(self._chmod_map_counter[ ''.join(perm) ])
return ''.join(digits)
def _set_check_boxes(self, targets):
for name, target in targets:
for type in list(target):
obj = f"{name}_{type}"
self._builder.get_object(obj).set_active(True)
def _reset_check_boxes(self):
for target in ["owner", "group", "others"]:
for type in ["r", "w", "x"]:
self._builder.get_object(f"{target}_{type}").set_active(False)
def _sizeof_fmt(self, num, suffix="B"):
for unit in ["", "K", "M", "G", "T", "Pi", "Ei", "Zi"]:
if abs(num) < 1024.0:
return f"{num:3.1f} {unit}{suffix}"
num /= 1024.0
return f"{num:.1f} Yi{suffix}"
def wait_for_fm_message(self):
while not self._event_message:
pass
@daemon_threaded
def _module_event_observer(self):
while True:
time.sleep(self._event_sleep_time)
event = self._event_system.read_module_event()
if event:
try:
if event[0] == self.name:
target_id, method_target, data = self._event_system.consume_module_event()
if not method_target:
self._event_message = data
else:
method = getattr(self.__class__, f"{method_target}")
if data:
data = method(*(self, *data))
else:
method(*(self,))
except Exception as e:
print(repr(e))

View File

@ -1,88 +0,0 @@
# Python imports
import os, threading, subprocess, time
# Gtk imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
# NOTE: Threads WILL NOT die with parent's destruction.
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
return wrapper
# NOTE: Threads WILL die with parent's destruction.
def daemon_threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class Manifest:
path: str = os.path.dirname(os.path.realpath(__file__))
name: str = "Example Plugin"
author: str = "John Doe"
version: str = "0.0.1"
support: str = ""
requests: {} = {
'ui_target': "plugin_control_list",
'pass_fm_events': "true",
'bind_keys': [f"{name}||send_message:<Control>f"]
}
class Plugin(Manifest):
def __init__(self):
self._event_system = None
self._event_sleep_time = .5
self._event_message = None
def get_ui_element(self):
button = Gtk.Button(label=self.name)
button.connect("button-release-event", self.send_message)
return button
def set_fm_event_system(self, fm_event_system):
self._event_system = fm_event_system
def run(self):
self._module_event_observer()
def send_message(self, widget=None, eve=None):
message = "Hello, World!"
print("here")
self._event_system.push_gui_event([self.name, "display_message", ("warning", message, None)])
def wait_for_fm_message(self):
while not self._event_message:
pass
@daemon_threaded
def _module_event_observer(self):
while True:
time.sleep(self._event_sleep_time)
event = self._event_system.read_module_event()
if event:
try:
if event[0] == self.name:
target_id, method_target, data = self._event_system.consume_module_event()
if not method_target:
self._event_message = data
else:
method = getattr(self.__class__, f"{method_target}")
if data:
data = method(*(self, *data))
else:
method(*(self,))
except Exception as e:
print(repr(e))

View File

@ -1,18 +0,0 @@
#!/bin/bash
# . CONFIG.sh
# set -o xtrace ## To debug scripts
# set -o errexit ## To exit on error
# set -o errunset ## To exit if a variable is referenced but not set
function main() {
cd "$(dirname "")"
echo "Working Dir: " $(pwd)
source "/home/abaddon/Portable_Apps/py-venvs/yt-dlp-venv/venv/bin/activate"
LINK=`xclip -selection clipboard -o`
yt-dlp --write-sub --embed-sub --sub-langs en -o "${1}/%(title)s.%(ext)s" "${LINK}"
}
main "$@";

View File

@ -1,92 +0,0 @@
# Python imports
import os, threading, subprocess, time
# Gtk imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
# NOTE: Threads WILL NOT die with parent's destruction.
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
return wrapper
# NOTE: Threads WILL die with parent's destruction.
def daemon_threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class Manifest:
path: str = os.path.dirname(os.path.realpath(__file__))
name: str = "Youtube Download"
author: str = "ITDominator"
version: str = "0.0.1"
support: str = ""
requests: {} = {
'ui_target': "plugin_control_list",
'pass_fm_events': "true"
}
class Plugin(Manifest):
def __init__(self):
self._event_system = None
self._event_sleep_time = .5
self._event_message = None
def get_ui_element(self):
button = Gtk.Button(label=self.name)
button.connect("button-release-event", self._do_download)
return button
def set_fm_event_system(self, fm_event_system):
self._event_system = fm_event_system
def run(self):
self._module_event_observer()
@threaded
def _do_download(self, widget=None, eve=None):
self._event_system.push_gui_event([self.name, "get_current_state", ()])
self.wait_for_fm_message()
state = self._event_message
subprocess.Popen([f'{self.path}/download.sh' , state.tab.get_current_directory()])
self._event_message = None
def wait_for_fm_message(self):
while not self._event_message:
pass
@daemon_threaded
def _module_event_observer(self):
while True:
time.sleep(self._event_sleep_time)
event = self._event_system.read_module_event()
if event:
try:
if event[0] == self.name:
target_id, method_target, data = self._event_system.consume_module_event()
if not method_target:
self._event_message = data
else:
method = getattr(self.__class__, f"{method_target}")
if data:
data = method(*(self, *data))
else:
method(*(self,))
except Exception as e:
print(repr(e))

View File

@ -0,0 +1,12 @@
#!/bin/bash
# set -o xtrace ## To debug scripts
# set -o errexit ## To exit on error
# set -o errunset ## To exit if a variable is referenced but not set
function main() {
find . -name "__pycache__" -exec rm -rf $1 {} \;
find . -name "*.pyc" -exec rm -rf $1 {} \;
}
main

View File

@ -4,25 +4,24 @@ import builtins
# Lib imports
# Application imports
from ipc_server import IPCServer
from signal_classes import IPCServerMixin
class EventSystem(IPCServer):
""" Inheret IPCServerMixin. Create an pub/sub systems. """
class Builtins(IPCServerMixin):
"""Docstring for __builtins__ extender"""
def __init__(self):
super(EventSystem, self).__init__()
# NOTE: The format used is list of [type, target, (data,)] Where:
# type is useful context for control flow,
# target is the method to call,
# data is the method parameters to give
# NOTE: The format used is list of [type, target, data]
# Where data may be any kind of data
self._gui_events = []
self._module_events = []
self._fm_events = []
self.is_ipc_alive = False
self.ipc_authkey = b'solarfm-ipc'
self.ipc_address = '127.0.0.1'
self.ipc_port = 4848
self.ipc_timeout = 15.0
# Makeshift fake "events" type system FIFO
@ -31,9 +30,9 @@ class EventSystem(IPCServer):
return self._gui_events.pop(0)
return None
def _pop_module_event(self):
if len(self._module_events) > 0:
return self._module_events.pop(0)
def _pop_fm_event(self):
if len(self._fm_events) > 0:
return self._fm_events.pop(0)
return None
@ -42,33 +41,33 @@ class EventSystem(IPCServer):
self._gui_events.append(event)
return None
raise Exception("Invald event format! Please do: [type, target, (data,)]")
raise Exception("Invald event format! Please do: [type, target, data]")
def push_module_event(self, event):
def push_fm_event(self, event):
if len(event) == 3:
self._module_events.append(event)
self._fm_events.append(event)
return None
raise Exception("Invald event format! Please do: [type, target, (data,)]")
raise Exception("Invald event format! Please do: [type, target, data]")
def read_gui_event(self):
return self._gui_events[0]
def read_module_event(self):
return self._module_events[0]
def read_fm_event(self):
return self._fm_events[0]
def consume_gui_event(self):
return self._pop_gui_event()
def consume_module_event(self):
return self._pop_module_event()
def consume_fm_event(self):
return self._pop_fm_event()
# NOTE: Just reminding myself we can add to builtins two different ways...
# __builtins__.update({"event_system": Builtins()})
builtins.app_name = "SolarFM"
builtins.event_system = EventSystem()
builtins.event_system = Builtins()
builtins.event_sleep_time = 0.2
builtins.debug = False
builtins.trace_debug = False

View File

@ -1,3 +1,54 @@
"""
Base module
"""
# Python imports
import os, inspect, time
# Lib imports
# Application imports
from utils import Settings
from signal_classes import Controller
from __builtins__ import Builtins
class Main(Builtins):
def __init__(self, args, unknownargs):
if not debug:
event_system.create_ipc_server()
time.sleep(0.2)
if not trace_debug:
if not event_system.is_ipc_alive:
if unknownargs:
for arg in unknownargs:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.send_ipc_message(message)
if args.new_tab and os.path.isdir(args.new_tab):
message = f"FILE|{args.new_tab}"
event_system.send_ipc_message(message)
raise Exception("IPC Server Exists: Will send path(s) to it and close...")
settings = Settings()
settings.create_window()
controller = Controller(args, unknownargs, settings)
if not controller:
raise Exception("Controller exited and doesn't exist...")
# Gets the methods from the classes and sets to handler.
# Then, builder connects to any signals it needs.
classes = [controller]
handlers = {}
for c in classes:
methods = None
try:
methods = inspect.getmembers(c, predicate=inspect.ismethod)
handlers.update(methods)
except Exception as e:
print(repr(e))
settings.builder.connect_signals(handlers)

View File

@ -15,12 +15,10 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from app import Application
from __init__ import Main
if __name__ == "__main__":
""" Set process title, get arguments, and create GTK main thread. """
try:
# import web_pdb
# web_pdb.set_trace()
@ -35,7 +33,7 @@ if __name__ == "__main__":
# Read arguments (If any...)
args, unknownargs = parser.parse_known_args()
Application(args, unknownargs)
Main(args, unknownargs)
Gtk.main()
except Exception as e:
traceback.print_exc()

View File

@ -1,55 +0,0 @@
# Python imports
import os, inspect, time
# Lib imports
# Application imports
from utils.settings import Settings
from context.controller import Controller
from __builtins__ import EventSystem
class Application(EventSystem):
""" Create Settings and Controller classes. Bind signal to Builder. Inherit from Builtins to bind global methods and classes. """
def __init__(self, args, unknownargs):
if not trace_debug:
event_system.create_ipc_server()
time.sleep(0.1)
if not event_system.is_ipc_alive:
if unknownargs:
for arg in unknownargs:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.send_ipc_message(message)
if args.new_tab and os.path.isdir(args.new_tab):
message = f"FILE|{args.new_tab}"
event_system.send_ipc_message(message)
raise Exception("IPC Server Exists: Will send path(s) to it and close...")
settings = Settings()
settings.create_window()
controller = Controller(args, unknownargs, settings)
if not controller:
raise Exception("Controller exited and doesn't exist...")
# Gets the methods from the classes and sets to handler.
# Then, builder connects to any signals it needs.
classes = [controller]
handlers = {}
for c in classes:
methods = None
try:
methods = inspect.getmembers(c, predicate=inspect.ismethod)
handlers.update(methods)
except Exception as e:
print(repr(e))
settings.builder.connect_signals(handlers)

View File

@ -1,3 +0,0 @@
"""
Gtk Bound Signal Module
"""

View File

@ -1,171 +0,0 @@
# Python imports
import os, gc, threading, time
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib
# Application imports
from .mixins.exception_hook_mixin import ExceptionHookMixin
from .mixins.ui_mixin import UIMixin
from .signals.ipc_signals_mixin import IPCSignalsMixin
from .signals.keyboard_signals_mixin import KeyboardSignalsMixin
from .controller_data import Controller_Data
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class Controller(UIMixin, KeyboardSignalsMixin, IPCSignalsMixin, ExceptionHookMixin, Controller_Data):
""" Controller coordinates the mixins and is somewhat the root hub of it all. """
def __init__(self, args, unknownargs, _settings):
self.setup_controller_data(_settings)
self.window.show()
self.generate_windows(self.state)
self.plugins.launch_plugins()
if debug:
self.window.set_interactive_debugging(True)
if not trace_debug:
self.gui_event_observer()
if unknownargs:
for arg in unknownargs:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.send_ipc_message(message)
if args.new_tab and os.path.isdir(args.new_tab):
message = f"FILE|{args.new_tab}"
event_system.send_ipc_message(message)
def tear_down(self, widget=None, eve=None):
event_system.send_ipc_message("close server")
self.fm_controller.save_state()
time.sleep(event_sleep_time)
Gtk.main_quit()
@threaded
def gui_event_observer(self):
while True:
time.sleep(event_sleep_time)
event = event_system.consume_gui_event()
if event:
try:
type, target, data = event
if type:
method = getattr(self.__class__, "handle_gui_event_and_set_message")
GLib.idle_add(method, *(self, type, target, data))
else:
method = getattr(self.__class__, target)
GLib.idle_add(method, *(self, *data,))
except Exception as e:
print(repr(e))
def handle_gui_event_and_set_message(self, type, target, parameters):
method = getattr(self.__class__, f"{target}")
data = method(*(self, *parameters))
self.plugins.send_message_to_plugin(type, data)
def open_terminal(self, widget=None, eve=None):
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
dir = tab.get_current_directory()
tab.execute(f"{tab.terminal_app}", dir)
def save_load_session(self, action="save_session"):
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
save_load_dialog = self.builder.get_object("save_load_dialog")
if action == "save_session":
self.fm_controller.save_state()
return
elif action == "save_session_as":
save_load_dialog.set_action(Gtk.FileChooserAction.SAVE)
elif action == "load_session":
save_load_dialog.set_action(Gtk.FileChooserAction.OPEN)
else:
raise Exception(f"Unknown action given: {action}")
save_load_dialog.set_current_folder(tab.get_current_directory())
save_load_dialog.set_current_name("session.json")
response = save_load_dialog.run()
if response == Gtk.ResponseType.OK:
if action == "save_session_as":
path = f"{save_load_dialog.get_current_folder()}/{save_load_dialog.get_current_name()}"
self.fm_controller.save_state(path)
elif action == "load_session":
path = f"{save_load_dialog.get_file().get_path()}"
session_json = self.fm_controller.load_state(path)
self.load_session(session_json)
if (response == Gtk.ResponseType.CANCEL) or (response == Gtk.ResponseType.DELETE_EVENT):
pass
save_load_dialog.hide()
def load_session(self, session_json):
if debug:
print(f"Session Data: {session_json}")
self.ctrl_down = False
self.shift_down = False
self.alt_down = False
for notebook in self.notebooks:
self.clear_children(notebook)
self.fm_controller.unload_tabs_and_windows()
self.generate_windows(session_json)
gc.collect()
def do_action_from_menu_controls(self, widget, event_button):
action = widget.get_name()
self.hide_context_menu()
self.hide_new_file_menu()
self.hide_edit_file_menu()
if action == "open":
self.open_files()
if action == "open_with":
self.show_appchooser_menu()
if action == "execute":
self.execute_files()
if action == "execute_in_terminal":
self.execute_files(in_terminal=True)
if action == "rename":
self.rename_files()
if action == "cut":
self.to_copy_files.clear()
self.cut_files()
if action == "copy":
self.to_cut_files.clear()
self.copy_files()
if action == "paste":
self.paste_files()
if action == "archive":
self.show_archiver_dialogue()
if action == "delete":
self.delete_files()
if action == "trash":
self.trash_files()
if action == "go_to_trash":
self.path_entry.set_text(self.trash_files_path)
if action == "restore_from_trash":
self.restore_trash_files()
if action == "empty_trash":
self.empty_trash()
if action == "create":
self.show_new_file_menu()
if action in ["save_session", "save_session_as", "load_session"]:
self.save_load_session(action)

View File

@ -1,3 +0,0 @@
"""
Mixins module
"""

View File

@ -1,62 +0,0 @@
# Python imports
import traceback, threading, time
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib
# Application imports
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class ExceptionHookMixin:
""" ExceptionHookMixin custom exception hook to reroute to a Gtk text area. """
def custom_except_hook(self, exec_type, value, _traceback):
trace = ''.join(traceback.format_tb(_traceback))
data = f"Exec Type: {exec_type} <--> Value: {value}\n\n{trace}\n\n\n\n"
start_itr = self.message_buffer.get_start_iter()
self.message_buffer.place_cursor(start_itr)
self.display_message(self.error, data)
def display_message(self, type, text, seconds=None):
self.message_buffer.insert_at_cursor(text)
self.message_popup_widget.popup()
if seconds:
self.hide_message_timeout(seconds)
@threaded
def hide_message_timeout(self, seconds=3):
time.sleep(seconds)
GLib.idle_add(self.message_popup_widget.popdown)
def save_debug_alerts(self, widget=None, eve=None):
start_itr, end_itr = self.message_buffer.get_bounds()
save_location_prompt = Gtk.FileChooserDialog("Choose Save Folder", self.window, \
action = Gtk.FileChooserAction.SAVE, \
buttons = (Gtk.STOCK_CANCEL, \
Gtk.ResponseType.CANCEL, \
Gtk.STOCK_SAVE, \
Gtk.ResponseType.OK))
text = self.message_buffer.get_text(start_itr, end_itr, False)
resp = save_location_prompt.run()
if (resp == Gtk.ResponseType.CANCEL) or (resp == Gtk.ResponseType.DELETE_EVENT):
pass
elif resp == Gtk.ResponseType.OK:
target = save_location_prompt.get_filename();
with open(target, "w") as f:
f.write(text)
save_location_prompt.destroy()
def set_arc_buffer_text(self, widget=None, eve=None):
sid = widget.get_active_id()
self.arc_command_buffer.set_text(self.arc_commands[int(sid)])

View File

@ -1,3 +0,0 @@
"""
UI module
"""

View File

@ -1,202 +0,0 @@
# Python imports
import os
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from .widget_mixin import WidgetMixin
class TabMixin(WidgetMixin):
"""docstring for TabMixin"""
def create_tab(self, wid, path=None):
notebook = self.builder.get_object(f"window_{wid}")
path_entry = self.builder.get_object(f"path_entry")
tab = self.fm_controller.add_tab_for_window_by_nickname(f"window_{wid}")
tab.logger = self.logger
tab.set_wid(wid)
if path: tab.set_path(path)
tab_widget = self.create_tab_widget(tab)
scroll, store = self.create_icon_grid_widget(tab, wid)
# TODO: Fix global logic to make the below work too
# scroll, store = self.create_icon_tree_widget(tab, wid)
index = notebook.append_page(scroll, tab_widget)
self.fm_controller.set__wid_and_tid(wid, tab.get_id())
path_entry.set_text(tab.get_current_directory())
notebook.show_all()
notebook.set_current_page(index)
ctx = notebook.get_style_context()
ctx.add_class("notebook-unselected-focus")
notebook.set_tab_reorderable(scroll, True)
self.load_store(tab, store)
self.set_window_title()
self.set_file_watcher(tab)
def close_tab(self, button, eve=None):
notebook = button.get_parent().get_parent()
wid = int(notebook.get_name()[-1])
tid = self.get_id_from_tab_box(button.get_parent())
scroll = self.builder.get_object(f"{wid}|{tid}")
page = notebook.page_num(scroll)
tab = self.get_fm_window(wid).get_tab_by_id(tid)
watcher = tab.get_dir_watcher()
watcher.cancel()
self.get_fm_window(wid).delete_tab_by_id(tid)
notebook.remove_page(page)
self.fm_controller.save_state()
self.set_window_title()
def on_tab_reorder(self, child, page_num, new_index):
wid, tid = page_num.get_name().split("|")
window = self.get_fm_window(wid)
tab = None
for i, tab in enumerate(window.get_all_tabs()):
if tab.get_id() == tid:
_tab = window.get_tab_by_id(tid)
watcher = _tab.get_dir_watcher()
watcher.cancel()
window.get_all_tabs().insert(new_index, window.get_all_tabs().pop(i))
tab = window.get_tab_by_id(tid)
self.set_file_watcher(tab)
self.fm_controller.save_state()
def on_tab_switch_update(self, notebook, content=None, index=None):
self.selected_files.clear()
wid, tid = content.get_children()[0].get_name().split("|")
self.fm_controller.set__wid_and_tid(wid, tid)
self.set_path_text(wid, tid)
self.set_window_title()
def get_id_from_tab_box(self, tab_box):
return tab_box.get_children()[2].get_text()
def get_tab_label(self, notebook, icon_grid):
return notebook.get_tab_label(icon_grid.get_parent()).get_children()[0]
def get_tab_close(self, notebook, icon_grid):
return notebook.get_tab_label(icon_grid.get_parent()).get_children()[1]
def get_tab_icon_grid_from_notebook(self, notebook):
return notebook.get_children()[1].get_children()[0]
def refresh_tab(data=None):
wid, tid, tab, icon_grid, store = self.get_current_state()
tab.load_directory()
self.load_store(tab, store)
def update_tab(self, tab_label, tab, store, wid, tid):
self.load_store(tab, store)
self.set_path_text(wid, tid)
char_width = len(tab.get_end_of_path())
tab_label.set_width_chars(char_width)
tab_label.set_label(tab.get_end_of_path())
self.set_window_title()
self.set_file_watcher(tab)
self.fm_controller.save_state()
def do_action_from_bar_controls(self, widget, eve=None):
action = widget.get_name()
wid, tid = self.fm_controller.get_active_wid_and_tid()
notebook = self.builder.get_object(f"window_{wid}")
store, tab_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
if action == "create_tab":
dir = tab.get_current_directory()
self.create_tab(wid, dir)
self.fm_controller.save_state()
return
if action == "go_up":
tab.pop_from_path()
if action == "go_home":
tab.set_to_home()
if action == "refresh_tab":
tab.load_directory()
if action == "path_entry":
focused_obj = self.window.get_focus()
dir = f"{tab.get_current_directory()}/"
path = widget.get_text()
if isinstance(focused_obj, Gtk.Entry):
path_menu_buttons = self.builder.get_object("path_menu_buttons")
query = widget.get_text().replace(dir, "")
files = tab.get_files() + tab.get_hidden()
self.clear_children(path_menu_buttons)
show_path_menu = False
for file, hash in files:
if os.path.isdir(f"{dir}{file}"):
if query.lower() in file.lower():
button = Gtk.Button(label=file)
button.show()
button.connect("clicked", self.set_path_entry)
path_menu_buttons.add(button)
show_path_menu = True
if not show_path_menu:
self.path_menu.popdown()
else:
self.path_menu.popup()
widget.grab_focus_without_selecting()
widget.set_position(-1)
if path.endswith(".") or path == dir:
return
if not tab.set_path(path):
return
self.update_tab(tab_label, tab, store, wid, tid)
try:
widget.grab_focus_without_selecting()
widget.set_position(-1)
except Exception as e:
pass
def set_path_entry(self, button=None, eve=None):
wid, tid, tab, icon_grid, store = self.get_current_state()
path = f"{tab.get_current_directory()}/{button.get_label()}"
path_entry = self.builder.get_object("path_entry")
path_entry.set_text(path)
path_entry.grab_focus_without_selecting()
path_entry.set_position(-1)
self.path_menu.popdown()
def keyboard_close_tab(self):
wid, tid = self.fm_controller.get_active_wid_and_tid()
notebook = self.builder.get_object(f"window_{wid}")
scroll = self.builder.get_object(f"{wid}|{tid}")
page = notebook.page_num(scroll)
tab = self.get_fm_window(wid).get_tab_by_id(tid)
watcher = tab.get_dir_watcher()
watcher.cancel()
self.get_fm_window(wid).delete_tab_by_id(tid)
notebook.remove_page(page)
self.fm_controller.save_state()
self.set_window_title()
def show_hide_hidden_files(self):
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
tab.set_hiding_hidden(not tab.is_hiding_hidden())
tab.load_directory()
self.builder.get_object("refresh_tab").released()

View File

@ -1,14 +0,0 @@
# Python imports
# Gtk imports
# Application imports
from .show_hide_mixin import ShowHideMixin
from .ui.widget_file_action_mixin import WidgetFileActionMixin
from .ui.pane_mixin import PaneMixin
from .ui.window_mixin import WindowMixin
from .show_hide_mixin import ShowHideMixin
class UIMixin(WidgetFileActionMixin, PaneMixin, WindowMixin, ShowHideMixin):
pass

View File

@ -1,3 +0,0 @@
"""
Signals module
"""

View File

@ -1,114 +0,0 @@
# Python imports
import re
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk, Gdk
# Application imports
valid_keyvalue_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]")
class KeyboardSignalsMixin:
""" KeyboardSignalsMixin keyboard hooks controller. """
def unset_keys_and_data(self, widget=None, eve=None):
self.ctrl_down = False
self.shift_down = False
self.alt_down = False
self.is_searching = False
def global_key_press_controller(self, eve, user_data):
keyname = Gdk.keyval_name(user_data.keyval).lower()
if keyname.replace("_l", "").replace("_r", "") in ["control", "alt", "shift"]:
if "control" in keyname:
self.ctrl_down = True
if "shift" in keyname:
self.shift_down = True
if "alt" in keyname:
self.alt_down = True
# NOTE: Yes, this should actually be mapped to some key controller setting
# file or something. Sue me.
def global_key_release_controller(self, eve, user_data):
keyname = Gdk.keyval_name(user_data.keyval).lower()
if debug:
print(f"global_key_release_controller > key > {keyname}")
if keyname.replace("_l", "").replace("_r", "") in ["control", "alt", "shift"]:
if "control" in keyname:
self.ctrl_down = False
if "shift" in keyname:
self.shift_down = False
if "alt" in keyname:
self.alt_down = False
if self.ctrl_down and self.shift_down and keyname == "t":
self.unset_keys_and_data()
self.trash_files()
if self.ctrl_down:
if keyname in ["1", "kp_1", "2", "kp_2", "3", "kp_3", "4", "kp_4"]:
self.builder.get_object(f"tggl_notebook_{keyname.strip('kp_')}").released()
if keyname == "q":
self.tear_down()
if keyname == "slash" or keyname == "home":
self.builder.get_object("go_home").released()
if keyname == "r" or keyname == "f5":
self.builder.get_object("refresh_tab").released()
if keyname == "up" or keyname == "u":
self.builder.get_object("go_up").released()
if keyname == "l":
self.unset_keys_and_data()
self.builder.get_object("path_entry").grab_focus()
if keyname == "t":
self.builder.get_object("create_tab").released()
if keyname == "o":
self.unset_keys_and_data()
self.open_files()
if keyname == "w":
self.keyboard_close_tab()
if keyname == "h":
self.show_hide_hidden_files()
if keyname == "e":
self.unset_keys_and_data()
self.rename_files()
if keyname == "c":
self.copy_files()
self.to_cut_files.clear()
if keyname == "x":
self.to_copy_files.clear()
self.cut_files()
if keyname == "v":
self.paste_files()
if keyname == "n":
self.unset_keys_and_data()
self.show_new_file_menu()
if keyname == "delete":
self.unset_keys_and_data()
self.delete_files()
if keyname == "f2":
self.unset_keys_and_data()
self.rename_files()
if keyname == "f4":
self.unset_keys_and_data()
self.open_terminal()
if keyname in ["alt_l", "alt_r"]:
top_main_menubar = self.builder.get_object("top_main_menubar")
top_main_menubar.hide() if top_main_menubar.is_visible() else top_main_menubar.show()
if re.fullmatch(valid_keyvalue_pat, keyname):
if not self.is_searching and not self.ctrl_down \
and not self.shift_down and not self.alt_down:
focused_obj = self.window.get_focus()
if isinstance(focused_obj, Gtk.IconView):
self.is_searching = True
wid, tid, self.search_tab, self.search_icon_grid, store = self.get_current_state()
self.unset_keys_and_data()
self.popup_search_files(wid, keyname)
return

View File

@ -1,3 +0,0 @@
"""
Gtk Bound Plugins Module
"""

View File

@ -1,83 +0,0 @@
# Python imports
import os, sys, importlib, traceback
from os.path import join, isdir
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gio
# Application imports
class Plugin:
name = None
module = None
reference = None
class Plugins:
"""Plugins controller"""
def __init__(self, settings):
self._settings = settings
self._builder = self._settings.get_builder()
self._plugins_path = self._settings.get_plugins_path()
self._plugins_dir_watcher = None
self._plugin_collection = []
def launch_plugins(self):
self._set_plugins_watcher()
self.load_plugins()
def _set_plugins_watcher(self):
self._plugins_dir_watcher = Gio.File.new_for_path(self._plugins_path) \
.monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, Gio.Cancellable())
self._plugins_dir_watcher.connect("changed", self._on_plugins_changed, ())
def _on_plugins_changed(self, file_monitor, file, other_file=None, eve_type=None, data=None):
if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED,
Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN,
Gio.FileMonitorEvent.MOVED_OUT]:
self.reload_plugins(file)
# @threaded
def load_plugins(self, file=None):
print(f"Loading plugins...")
parent_path = os.getcwd()
for file in os.listdir(self._plugins_path):
try:
path = join(self._plugins_path, file)
if isdir(path):
os.chdir(path)
sys.path.insert(0, path)
spec = importlib.util.spec_from_file_location(file, join(path, "__main__.py"))
app = importlib.util.module_from_spec(spec)
spec.loader.exec_module(app)
plugin_reference = app.Plugin(self._builder, event_system)
plugin = Plugin()
plugin.name = plugin_reference.get_plugin_name()
plugin.module = path
plugin.reference = plugin_reference
self._plugin_collection.append(plugin)
except Exception as e:
print("Malformed plugin! Not loading!")
traceback.print_exc()
os.chdir(parent_path)
def reload_plugins(self, file=None):
print(f"Reloading plugins... stub.")
def send_message_to_plugin(self, type, data):
print("Trying to send message to plugin...")
for plugin in self._plugin_collection:
if type in plugin.name:
print('Found plugin; posting message...')
plugin.reference.set_message(data)

View File

@ -1,3 +1 @@
"""
Root of ShellFM
"""
from .windows import WindowController

View File

@ -0,0 +1,66 @@
# Python imports
from random import randint
# Lib imports
# Application imports
from .view import View
class Window:
def __init__(self):
self.id_length = 10
self.id = ""
self.name = ""
self.nickname = ""
self.isHidden = False
self.views = []
self.generate_id()
def random_with_N_digits(self, n):
range_start = 10**(n-1)
range_end = (10**n)-1
return randint(range_start, range_end)
def generate_id(self):
self.id = str(self.random_with_N_digits(self.id_length))
def get_window_id(self):
return self.id
def create_view(self):
view = View()
self.views.append(view)
return view
def pop_view(self):
self.views.pop()
def delete_view_by_id(self, vid):
for view in self.views:
if view.id == vid:
self.views.remove(view)
break
def get_view_by_id(self, vid):
for view in self.views:
if view.id == vid:
return view
def get_view_by_index(self, index):
return self.views[index]
def get_views_count(self):
return len(self.views)
def get_all_views(self):
return self.views
def list_files_from_views(self):
for view in self.views:
print(view.files)

View File

@ -0,0 +1,181 @@
# Python imports
import threading, subprocess, time, json
from os import path
# Lib imports
# Application imports
from . import Window
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class WindowController:
def __init__(self):
USER_HOME = path.expanduser('~')
CONFIG_PATH = USER_HOME + "/.config/solarfm"
self.session_file = CONFIG_PATH + "/session.json"
self._event_sleep_time = 1
self.active_window_id = ""
self.active_tab_id = ""
self.windows = []
if not trace_debug:
self.fm_event_observer()
@threaded
def fm_event_observer(self):
while True:
time.sleep(event_sleep_time)
event = event_system.consume_fm_event()
if event:
print(event)
def set_active_data(self, wid, tid):
self.active_window_id = str(wid)
self.active_tab_id = str(tid)
def get_active_data(self):
return self.active_window_id, self.active_tab_id
def create_window(self):
window = Window()
window.name = "window_" + window.id
window.nickname = "window_" + str(len(self.windows) + 1)
self.windows.append(window)
return window
def add_view_for_window(self, win_id):
for window in self.windows:
if window.id == win_id:
return window.create_view()
def add_view_for_window_by_name(self, name):
for window in self.windows:
if window.name == name:
return window.create_view()
def add_view_for_window_by_nickname(self, nickname):
for window in self.windows:
if window.nickname == nickname:
return window.create_view()
def pop_window(self):
self.windows.pop()
def delete_window_by_id(self, win_id):
for window in self.windows:
if window.id == win_id:
self.windows.remove(window)
break
def delete_window_by_name(self, name):
for window in self.windows:
if window.name == name:
self.windows.remove(window)
break
def delete_window_by_nickname(self, nickname):
for window in self.windows:
if window.nickname == nickname:
self.windows.remove(window)
break
def get_window_by_id(self, win_id):
for window in self.windows:
if window.id == win_id:
return window
raise(f"No Window by ID {win_id} found!")
def get_window_by_name(self, name):
for window in self.windows:
if window.name == name:
return window
raise(f"No Window by Name {name} found!")
def get_window_by_nickname(self, nickname):
for window in self.windows:
if window.nickname == nickname:
return window
raise(f"No Window by Nickname {nickname} found!")
def get_window_by_index(self, index):
return self.windows[index]
def get_all_windows(self):
return self.windows
def set_window_nickname(self, win_id = None, nickname = ""):
for window in self.windows:
if window.id == win_id:
window.nickname = nickname
def list_windows(self):
print("\n[ ---- Windows ---- ]\n")
for window in self.windows:
print(f"\nID: {window.id}")
print(f"Name: {window.name}")
print(f"Nickname: {window.nickname}")
print(f"Is Hidden: {window.isHidden}")
print(f"View Count: {window.get_views_count()}")
print("\n-------------------------\n")
def list_files_from_views_of_window(self, win_id):
for window in self.windows:
if window.id == win_id:
window.list_files_from_views()
break
def get_views_count(self, win_id):
for window in self.windows:
if window.id == win_id:
return window.get_views_count()
def get_views_from_window(self, win_id):
for window in self.windows:
if window.id == win_id:
return window.get_all_views()
def save_state(self):
windows = []
for window in self.windows:
views = []
for view in window.views:
views.append(view.get_current_directory())
windows.append(
[
{
'window':{
"ID": window.id,
"Name": window.name,
"Nickname": window.nickname,
"isHidden": f"{window.isHidden}",
'views': views
}
}
]
)
with open(self.session_file, 'w') as outfile:
json.dump(windows, outfile, separators=(',', ':'), indent=4)
def load_state(self):
if path.isfile(self.session_file):
with open(self.session_file) as infile:
return json.load(infile)

View File

@ -1,3 +1,2 @@
"""
Window module
"""
from .Window import Window
from .WindowController import WindowController

View File

@ -1,185 +0,0 @@
# Python imports
import threading, json
from os import path
# Lib imports
# Application imports
from .window import Window
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class WindowController:
def __init__(self):
USER_HOME = path.expanduser('~')
CONFIG_PATH = USER_HOME + "/.config/solarfm"
self._session_file = CONFIG_PATH + "/session.json"
self._event_sleep_time = 1
self._active_window_id = ""
self._active_tab_id = ""
self._windows = []
def set__wid_and_tid(self, wid, tid):
self._active_window_id = str(wid)
self._active_tab_id = str(tid)
def get_active_wid_and_tid(self):
return self._active_window_id, self._active_tab_id
def create_window(self):
window = Window()
window.set_nickname(f"window_{str(len(self._windows) + 1)}")
self._windows.append(window)
return window
def add_tab_for_window(self, win_id):
for window in self._windows:
if window.get_id() == win_id:
return window.create_tab()
def add_tab_for_window_by_name(self, name):
for window in self._windows:
if window.get_name() == name:
return window.create_tab()
def add_tab_for_window_by_nickname(self, nickname):
for window in self._windows:
if window.get_nickname() == nickname:
return window.create_tab()
def pop_window(self):
self._windows.pop()
def delete_window_by_id(self, win_id):
for window in self._windows:
if window.get_id() == win_id:
self._windows.remove(window)
break
def delete_window_by_name(self, name):
for window in self._windows:
if window.get_name() == name:
self._windows.remove(window)
break
def delete_window_by_nickname(self, nickname):
for window in self._windows:
if window.get_nickname() == nickname:
self._windows.remove(window)
break
def get_window_by_id(self, win_id):
for window in self._windows:
if window.get_id() == win_id:
return window
raise(f"No Window by ID {win_id} found!")
def get_window_by_name(self, name):
for window in self._windows:
if window.get_name() == name:
return window
raise(f"No Window by Name {name} found!")
def get_window_by_nickname(self, nickname):
for window in self._windows:
if window.get_nickname() == nickname:
return window
raise(f"No Window by Nickname {nickname} found!")
def get_window_by_index(self, index):
return self._windows[index]
def get_all_windows(self):
return self._windows
def set_window_nickname(self, win_id = None, nickname = ""):
for window in self._windows:
if window.get_id() == win_id:
window.set_nickname(nickname)
def list_windows(self):
print("\n[ ---- Windows ---- ]\n")
for window in self._windows:
print(f"\nID: {window.get_id()}")
print(f"Name: {window.get_name()}")
print(f"Nickname: {window.get_nickname()}")
print(f"Is Hidden: {window.is_hidden()}")
print(f"Tab Count: {window.get_tabs_count()}")
print("\n-------------------------\n")
def list_files_from_tabs_of_window(self, win_id):
for window in self._windows:
if window.get_id() == win_id:
window.list_files_from_tabs()
break
def get_tabs_count(self, win_id):
for window in self._windows:
if window.get_id() == win_id:
return window.get_tabs_count()
def get_tabs_from_window(self, win_id):
for window in self._windows:
if window.get_id() == win_id:
return window.get_all_tabs()
def unload_tabs_and_windows(self):
for window in self._windows:
window.get_all_tabs().clear()
self._windows.clear()
def save_state(self, session_file = None):
if not session_file:
session_file = self._session_file
if len(self._windows) > 0:
windows = []
for window in self._windows:
tabs = []
for tab in window.get_all_tabs():
tabs.append(tab.get_current_directory())
windows.append(
[
{
'window':{
"ID": window.get_id(),
"Name": window.get_name(),
"Nickname": window.get_nickname(),
"isHidden": f"{window.is_hidden()}",
'tabs': tabs
}
}
]
)
with open(session_file, 'w') as outfile:
json.dump(windows, outfile, separators=(',', ':'), indent=4)
else:
raise Exception("Window data corrupted! Can not save session!")
def load_state(self, session_file = None):
if not session_file:
session_file = self._session_file
if path.isfile(session_file):
with open(session_file) as infile:
return json.load(infile)

View File

@ -1,3 +0,0 @@
"""
Tabs module
"""

View File

@ -1,3 +0,0 @@
"""
Icons mixins module
"""

View File

@ -1,5 +1,6 @@
# Python imports
import hashlib, re
import hashlib
import os
from os import listdir
from os.path import isdir, isfile, join
@ -10,43 +11,64 @@ from random import randint
# Application imports
from .utils.settings import Settings
from .utils.launcher import Launcher
from .utils.filehandler import FileHandler
from .icons.icon import Icon
from .path import Path
from .utils import Settings, Launcher, FileHandler
from .icons import Icon
from . import Path
class Tab(Settings, FileHandler, Launcher, Icon, Path):
class View(Settings, FileHandler, Launcher, Icon, Path):
def __init__(self):
self.logger = None
self._id_length = 10
self. logger = None
self.id_length = 10
self._id = ""
self._wid = None
self._dir_watcher = None
self._hide_hidden = self.HIDE_HIDDEN_FILES
self._files = []
self._dirs = []
self._vids = []
self._images = []
self._desktop = []
self._ungrouped = []
self._hidden = []
self.id = ""
self.wid = None
self.dir_watcher = None
self.hide_hidden = self.HIDE_HIDDEN_FILES
self.files = []
self.dirs = []
self.vids = []
self.images = []
self.desktop = []
self.ungrouped = []
self.hidden = []
self._generate_id()
self.generate_id()
self.set_to_home()
def random_with_N_digits(self, n):
range_start = 10**(n-1)
range_end = (10**n)-1
return randint(range_start, range_end)
def generate_id(self):
self.id = str(self.random_with_N_digits(self.id_length))
def get_tab_id(self):
return self.id
def set_wid(self, _wid):
self.wid = _wid
def get_wid(self):
return self.wid
def set_dir_watcher(self, watcher):
self.dir_watcher = watcher
def get_dir_watcher(self):
return self.dir_watcher
def load_directory(self):
path = self.get_path()
self._dirs = []
self._vids = []
self._images = []
self._desktop = []
self._ungrouped = []
self._hidden = []
self._files = []
path = self.get_path()
self.dirs = []
self.vids = []
self.images = []
self.desktop = []
self.ungrouped = []
self.hidden = []
self.files = []
if not isdir(path):
self.set_to_home()
@ -54,31 +76,40 @@ class Tab(Settings, FileHandler, Launcher, Icon, Path):
for f in listdir(path):
file = join(path, f)
if self._hide_hidden:
if self.hide_hidden:
if f.startswith('.'):
self._hidden.append(f)
self.hidden.append(f)
continue
if isfile(file):
lowerName = file.lower()
if lowerName.endswith(self.fvideos):
self._vids.append(f)
self.vids.append(f)
elif lowerName.endswith(self.fimages):
self._images.append(f)
self.images.append(f)
elif lowerName.endswith((".desktop",)):
self._desktop.append(f)
self.desktop.append(f)
else:
self._ungrouped.append(f)
self.ungrouped.append(f)
else:
self._dirs.append(f)
self.dirs.append(f)
self._dirs.sort(key=self._natural_keys)
self._vids.sort(key=self._natural_keys)
self._images.sort(key=self._natural_keys)
self._desktop.sort(key=self._natural_keys)
self._ungrouped.sort(key=self._natural_keys)
self.dirs.sort()
self.vids.sort()
self.images.sort()
self.desktop.sort()
self.ungrouped.sort()
self._files = self._dirs + self._vids + self._images + self._desktop + self._ungrouped
self.files = self.dirs + self.vids + self.images + self.desktop + self.ungrouped
def hash_text(self, text):
return hashlib.sha256(str.encode(text)).hexdigest()[:18]
def hash_set(self, arry):
data = []
for arr in arry:
data.append([arr, self.hash_text(arr)])
return data
def is_folder_locked(self, hash):
if self.lock_folder:
@ -98,18 +129,18 @@ class Tab(Settings, FileHandler, Launcher, Icon, Path):
def get_not_hidden_count(self):
return len(self._files) + \
len(self._dirs) + \
len(self._vids) + \
len(self._images) + \
len(self._desktop) + \
len(self._ungrouped)
return len(self.files) + \
len(self.dirs) + \
len(self.vids) + \
len(self.images) + \
len(self.desktop) + \
len(self.ungrouped)
def get_hidden_count(self):
return len(self._hidden)
return len(self.hidden)
def get_files_count(self):
return len(self._files)
return len(self.files)
def get_path_part_from_hash(self, hash):
files = self.get_files()
@ -123,13 +154,13 @@ class Tab(Settings, FileHandler, Launcher, Icon, Path):
return file
def get_files_formatted(self):
files = self._hash_set(self._files),
dirs = self._hash_set(self._dirs),
files = self.hash_set(self.files),
dirs = self.hash_set(self.dirs),
videos = self.get_videos(),
images = self._hash_set(self._images),
desktops = self._hash_set(self._desktop),
ungrouped = self._hash_set(self._ungrouped)
hidden = self._hash_set(self._hidden)
images = self.hash_set(self.images),
desktops = self.hash_set(self.desktop),
ungrouped = self.hash_set(self.ungrouped)
hidden = self.hash_set(self.hidden)
return {
'path_head': self.get_path(),
@ -147,7 +178,7 @@ class Tab(Settings, FileHandler, Launcher, Icon, Path):
def get_pixbuf_icon_str_combo(self):
data = []
dir = self.get_current_directory()
for file in self._files:
for file in self.files:
icon = self.create_icon(dir, file).get_pixbuf()
data.append([icon, file])
@ -157,7 +188,7 @@ class Tab(Settings, FileHandler, Launcher, Icon, Path):
def get_gtk_icon_str_combo(self):
data = []
dir = self.get_current_directory()
for file in self._files:
for file in self.files:
icon = self.create_icon(dir, file)
data.append([icon, file[0]])
@ -176,71 +207,23 @@ class Tab(Settings, FileHandler, Launcher, Icon, Path):
size = len(parts)
return parts[size - 1]
def set_hiding_hidden(self, state):
self._hide_hidden = state
def is_hiding_hidden(self):
return self._hide_hidden
def get_dot_dots(self):
return self._hash_set(['.', '..'])
return self.hash_set(['.', '..'])
def get_files(self):
return self._hash_set(self._files)
return self.hash_set(self.files)
def get_dirs(self):
return self._hash_set(self._dirs)
return self.hash_set(self.dirs)
def get_videos(self):
return self._hash_set(self._vids)
return self.hash_set(self.vids)
def get_images(self):
return self._hash_set(self._images)
return self.hash_set(self.images)
def get_desktops(self):
return self._hash_set(self._desktop)
return self.hash_set(self.desktop)
def get_ungrouped(self):
return self._hash_set(self._ungrouped)
def get_hidden(self):
return self._hash_set(self._hidden)
def get_id(self):
return self._id
def set_wid(self, _wid):
self._wid = _wid
def get_wid(self):
return self._wid
def set_dir_watcher(self, watcher):
self._dir_watcher = watcher
def get_dir_watcher(self):
return self._dir_watcher
def _atoi(self, text):
return int(text) if text.isdigit() else text
def _natural_keys(self, text):
return [ self._atoi(c) for c in re.split('(\d+)',text) ]
def _hash_text(self, text):
return hashlib.sha256(str.encode(text)).hexdigest()[:18]
def _hash_set(self, arry):
data = []
for arr in arry:
data.append([arr, self._hash_text(arr)])
return data
def _random_with_N_digits(self, n):
range_start = 10**(n-1)
range_end = (10**n)-1
return randint(range_start, range_end)
def _generate_id(self):
self._id = str(self._random_with_N_digits(self._id_length))
return self.hash_set(self.ungrouped)

View File

@ -0,0 +1,5 @@
from .utils import *
from .icons import *
from .Path import Path
from .View import View

View File

@ -3,13 +3,10 @@ import os, subprocess, threading, hashlib
from os.path import isfile
# Gtk imports
import gi
gi.require_version('GdkPixbuf', '2.0')
from gi.repository import GdkPixbuf
# Application imports
from .mixins.desktopiconmixin import DesktopIconMixin
from .mixins.videoiconmixin import VideoIconMixin
from .mixins import *
def threaded(fn):

View File

@ -0,0 +1,4 @@
from .mixins import DesktopIconMixin
from .mixins import VideoIconMixin
from .Icon import Icon

View File

@ -3,6 +3,9 @@ import os, subprocess, hashlib
from os.path import isfile
# Gtk imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from .xdg.DesktopEntry import DesktopEntry

View File

@ -0,0 +1,4 @@
from . import xdg
from .VideoIconMixin import VideoIconMixin
from .DesktopIconMixin import DesktopIconMixin

View File

@ -1,5 +1,5 @@
# Python imports
import os, shutil
import os, shutil, subprocess, threading
# Lib imports

View File

@ -0,0 +1,3 @@
from .Settings import Settings
from .Launcher import Launcher
from .FileHandler import FileHandler

View File

@ -1,87 +0,0 @@
# Python imports
from random import randint
# Lib imports
# Application imports
from .tabs.tab import Tab
class Window:
def __init__(self):
self._id_length = 10
self._id = ""
self._name = ""
self._nickname = ""
self._isHidden = False
self._tabs = []
self._generate_id()
self._set_name()
def create_tab(self):
tab = Tab()
self._tabs.append(tab)
return tab
def pop_tab(self):
self._tabs.pop()
def delete_tab_by_id(self, vid):
for tab in self._tabs:
if tab.get_id() == vid:
self._tabs.remove(tab)
break
def get_tab_by_id(self, vid):
for tab in self._tabs:
if tab.get_id() == vid:
return tab
def get_tab_by_index(self, index):
return self._tabs[index]
def get_tabs_count(self):
return len(self._tabs)
def get_all_tabs(self):
return self._tabs
def get_id(self):
return self._id
def get_name(self):
return self._name
def get_nickname(self):
return self._nickname
def is_hidden(self):
return self._isHidden
def list_files_from_tabs(self):
for tab in self._tabs:
print(tab.get_files())
def set_nickname(self, nickname):
self._nickname = f"{nickname}"
def set_is_hidden(self, state):
self._isHidden = f"{state}"
def _set_name(self):
self._name = "window_" + self.get_id()
def _random_with_N_digits(self, n):
range_start = 10**(n-1)
range_end = (10**n)-1
return randint(range_start, range_end)
def _generate_id(self):
self._id = str(self._random_with_N_digits(self._id_length))

View File

@ -0,0 +1,165 @@
# Python imports
import sys, traceback, threading, inspect, os, time
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib
# Application imports
from .mixins.ui import *
from .mixins import ShowHideMixin, KeyboardSignalsMixin
from . import Controller_Data
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class Controller(WidgetFileActionMixin, PaneMixin, WindowMixin, ShowHideMixin, \
KeyboardSignalsMixin, Controller_Data):
def __init__(self, args, unknownargs, _settings):
# sys.excepthook = self.custom_except_hook
self.setup_controller_data(_settings)
self.window.show()
self.generate_windows(self.state)
self.plugins.launch_plugins()
if not trace_debug:
self.gui_event_observer()
if unknownargs:
for arg in unknownargs:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.send_ipc_message(message)
if args.new_tab and os.path.isdir(args.new_tab):
message = f"FILE|{args.new_tab}"
event_system.send_ipc_message(message)
def tear_down(self, widget=None, eve=None):
event_system.send_ipc_message("close server")
self.window_controller.save_state()
time.sleep(event_sleep_time)
Gtk.main_quit()
@threaded
def gui_event_observer(self):
while True:
time.sleep(event_sleep_time)
event = event_system.consume_gui_event()
if event:
try:
type, target, data = event
method = getattr(self.__class__, type)
GLib.idle_add(method, (self, data,))
except Exception as e:
print(repr(e))
def custom_except_hook(self, exctype, value, _traceback):
trace = ''.join(traceback.format_tb(_traceback))
data = f"Exectype: {exctype} <--> Value: {value}\n\n{trace}\n\n\n\n"
start_itr = self.message_buffer.get_start_iter()
self.message_buffer.place_cursor(start_itr)
self.display_message(self.error, data)
def display_message(self, type, text, seconds=None):
self.message_buffer.insert_at_cursor(text)
self.message_widget.popup()
if seconds:
self.hide_message_timeout(seconds)
@threaded
def hide_message_timeout(self, seconds=3):
time.sleep(seconds)
GLib.idle_add(self.message_widget.popdown)
def save_debug_alerts(self, widget=None, eve=None):
start_itr, end_itr = self.message_buffer.get_bounds()
save_location_prompt = Gtk.FileChooserDialog("Choose Save Folder", self.window, \
action = Gtk.FileChooserAction.SAVE, \
buttons = (Gtk.STOCK_CANCEL, \
Gtk.ResponseType.CANCEL, \
Gtk.STOCK_SAVE, \
Gtk.ResponseType.OK))
text = self.message_buffer.get_text(start_itr, end_itr, False)
resp = save_location_prompt.run()
if (resp == Gtk.ResponseType.CANCEL) or (resp == Gtk.ResponseType.DELETE_EVENT):
pass
elif resp == Gtk.ResponseType.OK:
target = save_location_prompt.get_filename();
with open(target, "w") as f:
f.write(text)
save_location_prompt.destroy()
def set_arc_buffer_text(self, widget=None, eve=None):
id = widget.get_active_id()
self.arc_command_buffer.set_text(self.arc_commands[int(id)])
def clear_children(self, widget):
for child in widget.get_children():
widget.remove(child)
def get_current_state(self):
wid, tid = self.window_controller.get_active_data()
view = self.get_fm_window(wid).get_view_by_id(tid)
iconview = self.builder.get_object(f"{wid}|{tid}|iconview")
store = iconview.get_model()
return wid, tid, view, iconview, store
def do_action_from_menu_controls(self, widget, eventbutton):
action = widget.get_name()
self.ctrlDown = True
self.hide_context_menu()
self.hide_new_file_menu()
self.hide_edit_file_menu()
if action == "open":
self.open_files()
if action == "open_with":
self.show_appchooser_menu()
if action == "execute":
self.execute_files()
if action == "execute_in_terminal":
self.execute_files(in_terminal=True)
if action == "rename":
self.rename_files()
if action == "cut":
self.to_copy_files.clear()
self.cut_files()
if action == "copy":
self.to_cut_files.clear()
self.copy_files()
if action == "paste":
self.paste_files()
if action == "archive":
self.show_archiver_dialogue()
if action == "delete":
self.delete_files()
if action == "trash":
self.trash_files()
if action == "go_to_trash":
self.builder.get_object("path_entry").set_text(self.trash_files_path)
if action == "restore_from_trash":
self.restore_trash_files()
if action == "empty_trash":
self.empty_trash()
if action == "create":
self.create_files()
self.hide_new_file_menu()
self.ctrlDown = False

View File

@ -1,25 +1,26 @@
# Python imports
import sys, os, signal
import signal
# Lib imports
from gi.repository import GLib
# Application imports
from shellfm import WindowController
from trasher.xdgtrash import XDGTrash
from shellfm.windows.controller import WindowController
from plugins.plugins import Plugins
from . import Plugins
class Controller_Data:
""" Controller_Data contains most of the state of the app at ay given time. It also has some support methods. """
def has_method(self, o, name):
return callable(getattr(o, name, None))
def setup_controller_data(self, _settings):
self.trashman = XDGTrash()
self.fm_controller = WindowController()
self.window_controller = WindowController()
self.plugins = Plugins(_settings)
self.state = self.fm_controller.load_state()
self.state = self.window_controller.load_state()
self.trashman.regenerate()
self.settings = _settings
@ -31,19 +32,18 @@ class Controller_Data:
self.window2 = self.builder.get_object("window_2")
self.window3 = self.builder.get_object("window_3")
self.window4 = self.builder.get_object("window_4")
self.message_popup_widget = self.builder.get_object("message_popup_widget")
self.message_text_view = self.builder.get_object("message_text_view")
self.message_widget = self.builder.get_object("message_widget")
self.message_view = self.builder.get_object("message_view")
self.message_buffer = self.builder.get_object("message_buffer")
self.arc_command_buffer = self.builder.get_object("arc_command_buffer")
self.exists_file_rename_bttn = self.builder.get_object("exists_file_rename_bttn")
self.warning_alert = self.builder.get_object("warning_alert")
self.edit_file_menu = self.builder.get_object("edit_file_menu")
self.file_exists_dialog = self.builder.get_object("file_exists_dialog")
self.exists_file_label = self.builder.get_object("exists_file_label")
self.exists_file_field = self.builder.get_object("exists_file_field")
self.path_menu = self.builder.get_object("path_menu")
self.path_entry = self.builder.get_object("path_entry")
self.exists_file_rename_bttn = self.builder.get_object("exists_file_rename_bttn")
self.bottom_size_label = self.builder.get_object("bottom_size_label")
self.bottom_file_count_label = self.builder.get_object("bottom_file_count_label")
@ -78,80 +78,32 @@ class Controller_Data:
'xz -cz %N > %O'
]
self.notebooks = [self.window1, self.window2, self.window3, self.window4]
self.selected_files = []
self.to_copy_files = []
self.to_cut_files = []
self.soft_update_lock = {}
self.notebooks = [self.window1, self.window2, self.window3, self.window4]
self.selected_files = []
self.to_copy_files = []
self.to_cut_files = []
self.single_click_open = False
self.is_pane1_hidden = False
self.is_pane2_hidden = False
self.is_pane3_hidden = False
self.is_pane4_hidden = False
self.single_click_open = False
self.is_pane1_hidden = False
self.is_pane2_hidden = False
self.is_pane3_hidden = False
self.is_pane4_hidden = False
self.override_drop_dest = None
self.is_searching = False
self.search_icon_grid = None
self.search_tab = None
self.search_iconview = None
self.search_view = None
self.skip_edit = False
self.cancel_edit = False
self.ctrl_down = False
self.shift_down = False
self.alt_down = False
self.skip_edit = False
self.cancel_edit = False
self.ctrlDown = False
self.shiftDown = False
self.altDown = False
self.success = "#88cc27"
self.warning = "#ffa800"
self.error = "#ff0000"
self.success_color = self.settings.get_success_color()
self.warning_color = self.settings.get_warning_color()
self.error_color = self.settings.get_error_color()
sys.excepthook = self.custom_except_hook
self.window.connect("delete-event", self.tear_down)
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.tear_down)
def get_current_state(self):
'''
Returns the state info most useful for any given context and action intent.
Parameters:
a (obj): self
Returns:
wid, tid, tab, icon_grid, store
'''
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
icon_grid = self.builder.get_object(f"{wid}|{tid}|icon_grid")
store = icon_grid.get_model()
return wid, tid, tab, icon_grid, store
def clear_console(self):
''' Clears the terminal screen. '''
os.system('cls' if os.name == 'nt' else 'clear')
def call_method(self, _method_name, data = None):
'''
Calls a method from scope of class.
Parameters:
a (obj): self
b (str): method name to be called
c (*): Data (if any) to be passed to the method.
Note: It must be structured according to the given methods requirements.
Returns:
Return data is that which the calling method gives.
'''
method_name = str(_method_name)
method = getattr(self, method_name, lambda data: f"No valid key passed...\nkey={method_name}\nargs={data}")
return method(data) if data else method()
def has_method(self, obj, name):
''' Checks if a given method exists. '''
return callable(getattr(obj, name, None))
def clear_children(self, widget):
''' Clear children of a gtk widget. '''
for child in widget.get_children():
widget.remove(child)

View File

@ -0,0 +1,64 @@
# Python imports
import threading, socket, time
from multiprocessing.connection import Listener, Client
# Lib imports
# Application imports
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class IPCServerMixin:
@threaded
def create_ipc_server(self):
listener = Listener((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey)
self.is_ipc_alive = True
while True:
conn = listener.accept()
start_time = time.time()
print(f"New Connection: {listener.last_accepted}")
while True:
msg = conn.recv()
if debug:
print(msg)
if "FILE|" in msg:
file = msg.split("FILE|")[1].strip()
if file:
event_system.push_gui_event(["create_tab_from_ipc", None, file])
conn.close()
break
if msg == 'close connection':
conn.close()
break
if msg == 'close server':
conn.close()
break
# NOTE: Not perfect but insures we don't lockup the connection for too long.
end_time = time.time()
if (end - start) > self.ipc_timeout:
conn.close()
listener.close()
def send_ipc_message(self, message="Empty Data..."):
try:
conn = Client((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey)
conn.send(message)
conn.send('close connection')
except Exception as e:
print(repr(e))

View File

@ -0,0 +1,41 @@
# Python imports
import importlib
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gio
# Application imports
class Plugins:
"""docstring for Plugins"""
def __init__(self, settings):
self._settings = settings
self._plugins_path = self._settings.get_plugins_path()
self._plugins_dir_watcher = None
self._socket = Gtk.Socket().new()
def launch_plugins(self):
self._set_plugins_watcher()
self.load_plugins()
def _set_plugins_watcher(self):
self._plugins_dir_watcher = Gio.File.new_for_path(self._plugins_path) \
.monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, Gio.Cancellable())
self._plugins_dir_watcher.connect("changed", self._on_plugins_changed, ())
def _on_plugins_changed(self, file_monitor, file, other_file=None, eve_type=None, data=None):
if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED,
Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN,
Gio.FileMonitorEvent.MOVED_OUT]:
self.load_plugins(file)
def load_plugins(self, file=None):
print(f"(Re)loading plugins...")
print(locals())
# importlib.reload(stl_utils)

View File

@ -0,0 +1,8 @@
"""
Gtk Bound Signal Module
"""
from .mixins import *
from .IPCServerMixin import IPCServerMixin
from .Plugins import Plugins
from .Controller_Data import Controller_Data
from .Controller import Controller

View File

@ -0,0 +1,120 @@
# Python imports
import re
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk, Gdk
# Application imports
valid_keyvalue_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]")
class KeyboardSignalsMixin:
def unset_keys_and_data(self, widget=None, eve=None):
self.ctrlDown = False
self.shiftDown = False
self.altDown = False
self.is_searching = False
def global_key_press_controller(self, eve, user_data):
keyname = Gdk.keyval_name(user_data.keyval).lower()
if "control" in keyname or "alt" in keyname or "shift" in keyname:
if "control" in keyname:
self.ctrlDown = True
if "shift" in keyname:
self.shiftDown = True
if "alt" in keyname:
self.altDown = True
# NOTE: Yes, this should actually be mapped to some key controller setting
# file or something. Sue me.
def global_key_release_controller(self, eve, user_data):
keyname = Gdk.keyval_name(user_data.keyval).lower()
if debug:
print(f"global_key_release_controller > key > {keyname}")
if "control" in keyname or "alt" in keyname or "shift" in keyname:
if "control" in keyname:
self.ctrlDown = False
if "shift" in keyname:
self.shiftDown = False
if "alt" in keyname:
self.altDown = False
if self.ctrlDown and self.shiftDown and keyname == "t":
self.trash_files()
if re.fullmatch(valid_keyvalue_pat, keyname):
if not self.is_searching and not self.ctrlDown \
and not self.shiftDown and not self.altDown:
focused_obj = self.window.get_focus()
if isinstance(focused_obj, Gtk.IconView):
self.is_searching = True
wid, tid, self.search_view, self.search_iconview, store = self.get_current_state()
self.popup_search_files(wid, keyname)
return
if (self.ctrlDown and keyname in ["1", "kp_1"]):
self.builder.get_object("tggl_notebook_1").released()
if (self.ctrlDown and keyname in ["2", "kp_2"]):
self.builder.get_object("tggl_notebook_2").released()
if (self.ctrlDown and keyname in ["3", "kp_3"]):
self.builder.get_object("tggl_notebook_3").released()
if (self.ctrlDown and keyname in ["4", "kp_4"]):
self.builder.get_object("tggl_notebook_4").released()
if self.ctrlDown and keyname == "q":
self.tear_down()
if (self.ctrlDown and keyname == "slash") or keyname == "home":
self.builder.get_object("go_home").released()
if (self.ctrlDown and keyname == "r") or keyname == "f5":
self.builder.get_object("refresh_view").released()
if (self.ctrlDown and keyname == "up") or (self.ctrlDown and keyname == "u"):
self.builder.get_object("go_up").released()
if self.ctrlDown and keyname == "l":
self.builder.get_object("path_entry").grab_focus()
if self.ctrlDown and keyname == "t":
self.builder.get_object("create_tab").released()
if self.ctrlDown and keyname == "o":
self.open_files()
if self.ctrlDown and keyname == "w":
self.keyboard_close_tab()
if self.ctrlDown and keyname == "h":
self.show_hide_hidden_files()
if (self.ctrlDown and keyname == "e"):
self.edit_files()
if self.ctrlDown and keyname == "c":
self.to_cut_files.clear()
self.copy_files()
if self.ctrlDown and keyname == "x":
self.to_copy_files.clear()
self.cut_files()
if self.ctrlDown and keyname == "v":
self.paste_files()
if self.ctrlDown and keyname == "n":
self.show_new_file_menu()
if keyname in ["alt_l", "alt_r"]:
top_main_menubar = self.builder.get_object("top_main_menubar")
if top_main_menubar.is_visible():
top_main_menubar.hide()
else:
top_main_menubar.show()
if keyname == "delete":
self.delete_files()
if keyname == "f2":
self.rename_files()
if keyname == "f4":
wid, tid = self.window_controller.get_active_data()
view = self.get_fm_window(wid).get_view_by_id(tid)
dir = view.get_current_directory()
view.execute(f"{view.terminal_app}", dir)

View File

@ -4,18 +4,19 @@
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk, Gdk
from gi.repository import Gtk, Gdk, Gio
# Application imports
class ShowHideMixin:
def show_messages_popup(self, type, text, seconds=None):
self.message_popup_widget.popup()
self.message_widget.popup()
def stop_file_searching(self, widget=None, eve=None):
self.is_searching = False
def show_exists_page(self, widget=None, eve=None):
response = self.file_exists_dialog.run()
self.file_exists_dialog.hide()
@ -48,7 +49,7 @@ class ShowHideMixin:
def show_about_page(self, widget=None, eve=None):
about_page = self.builder.get_object("about_page")
response = about_page.run()
if response in [Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT]:
if (response == Gtk.ResponseType.CANCEL) or (response == Gtk.ResponseType.DELETE_EVENT):
self.hide_about_page()
def hide_about_page(self, widget=None, eve=None):
@ -56,11 +57,11 @@ class ShowHideMixin:
def show_archiver_dialogue(self, widget=None, eve=None):
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
wid, tid = self.window_controller.get_active_data()
view = self.get_fm_window(wid).get_view_by_id(tid)
archiver_dialogue = self.builder.get_object("archiver_dialogue")
archiver_dialogue.set_action(Gtk.FileChooserAction.SAVE)
archiver_dialogue.set_current_folder(tab.get_current_directory())
archiver_dialogue.set_current_folder(view.get_current_directory())
archiver_dialogue.set_current_name("arc.7z")
response = archiver_dialogue.run()
@ -80,14 +81,12 @@ class ShowHideMixin:
appchooser_widget = self.builder.get_object("appchooser_widget")
response = appchooser_menu.run()
if response == Gtk.ResponseType.CANCEL:
self.hide_appchooser_menu()
if response == Gtk.ResponseType.OK:
self.open_with_files(appchooser_widget)
self.hide_appchooser_menu()
if response == Gtk.ResponseType.CANCEL:
self.hide_appchooser_menu()
def hide_appchooser_menu(self, widget=None, eve=None):
self.builder.get_object("appchooser_menu").hide()
@ -96,34 +95,20 @@ class ShowHideMixin:
dialog.response(Gtk.ResponseType.OK)
def show_plugins_popup(self, widget=None, eve=None):
self.builder.get_object("plugin_controls").popup()
def hide_plugins_popup(self, widget=None, eve=None):
self.builder.get_object("plugin_controls").hide()
def show_context_menu(self, widget=None, eve=None):
self.builder.get_object("context_menu_popup").run()
self.builder.get_object("context_menu").run()
def hide_context_menu(self, widget=None, eve=None):
self.builder.get_object("context_menu_popup").hide()
self.builder.get_object("context_menu").hide()
def show_new_file_menu(self, widget=None, eve=None):
context_menu_fname = self.builder.get_object("context_menu_fname")
context_menu_fname.set_text("")
context_menu_fname.grab_focus()
new_file_menu = self.builder.get_object("new_file_menu")
response = new_file_menu.run()
if response == Gtk.ResponseType.APPLY:
self.create_files()
if response == Gtk.ResponseType.CANCEL:
self.hide_new_file_menu()
self.builder.get_object("new_file_menu").run()
def hide_new_file_menu(self, widget=None, eve=None):
self.builder.get_object("new_file_menu").hide()
def show_edit_file_menu(self, widget=None, eve=None):
if widget:
widget.grab_focus()
@ -139,7 +124,7 @@ class ShowHideMixin:
def hide_edit_file_menu_enter_key(self, widget=None, eve=None):
keyname = Gdk.keyval_name(eve.keyval).lower()
if keyname in ["return", "enter"]:
if "return" in keyname or "enter" in keyname:
self.builder.get_object("edit_file_menu").hide()
def hide_edit_file_menu_skip(self, widget=None, eve=None):

View File

@ -0,0 +1,2 @@
from .KeyboardSignalsMixin import KeyboardSignalsMixin
from .ShowHideMixin import ShowHideMixin

View File

@ -39,6 +39,8 @@ class PaneMixin:
def toggle_notebook_pane(self, widget, eve=None):
name = widget.get_name()
pane_index = int(name[-1])
pane = None
master_pane = self.builder.get_object("pane_master")
pane = self.builder.get_object("pane_top") if pane_index in [1, 2] else self.builder.get_object("pane_bottom")
@ -48,12 +50,16 @@ class PaneMixin:
self._save_state(state, pane_index)
return
child = pane.get_child1() if pane_index in [1, 3] else pane.get_child2()
child = None
if pane_index in [1, 3]:
child = pane.get_child1()
elif pane_index in [2, 4]:
child = pane.get_child2()
self.toggle_pane(child)
self._save_state(state, pane_index)
def _save_state(self, state, pane_index):
window = self.fm_controller.get_window_by_index(pane_index - 1)
window.set_is_hidden(state)
self.fm_controller.save_state()
window = self.window_controller.get_window_by_index(pane_index - 1)
window.isHidden = state
self.window_controller.save_state()

View File

@ -0,0 +1,223 @@
# Python imports
import os
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk, Gdk
# Application imports
from . import WidgetMixin
class TabMixin(WidgetMixin):
"""docstring for TabMixin"""
def create_tab_from_ipc(data):
self, path = data
wid, tid = self.window_controller.get_active_data()
notebook = self.builder.get_object(f"window_{wid}")
if notebook.is_visible():
self.create_tab(wid, path)
return
if not self.is_pane4_hidden:
self.create_tab(4, path)
elif not self.is_pane3_hidden:
self.create_tab(3, path)
elif not self.is_pane2_hidden:
self.create_tab(2, path)
elif not self.is_pane1_hidden:
self.create_tab(1, path)
def create_tab(self, wid, path=None):
notebook = self.builder.get_object(f"window_{wid}")
path_entry = self.builder.get_object(f"path_entry")
view = self.window_controller.add_view_for_window_by_nickname(f"window_{wid}")
view.logger = self.logger
view.set_wid(wid)
if path: view.set_path(path)
tab = self.create_tab_widget(view)
scroll, store = self.create_grid_iconview_widget(view, wid)
# scroll, store = self.create_grid_treeview_widget(view, wid)
index = notebook.append_page(scroll, tab)
self.window_controller.set_active_data(wid, view.get_tab_id())
path_entry.set_text(view.get_current_directory())
notebook.show_all()
notebook.set_current_page(index)
ctx = notebook.get_style_context()
ctx.add_class("notebook-unselected-focus")
notebook.set_tab_reorderable(scroll, True)
self.load_store(view, store)
self.set_window_title()
self.set_file_watcher(view)
def close_tab(self, button, eve=None):
notebook = button.get_parent().get_parent()
tid = self.get_tab_id_from_tab_box(button.get_parent())
wid = int(notebook.get_name()[-1])
scroll = self.builder.get_object(f"{wid}|{tid}")
page = notebook.page_num(scroll)
view = self.get_fm_window(wid).get_view_by_id(tid)
watcher = view.get_dir_watcher()
watcher.cancel()
self.get_fm_window(wid).delete_view_by_id(tid)
notebook.remove_page(page)
self.window_controller.save_state()
self.set_window_title()
def on_tab_reorder(self, child, page_num, new_index):
wid, tid = page_num.get_name().split("|")
window = self.get_fm_window(wid)
view = None
for i, view in enumerate(window.views):
if view.id == tid:
_view = window.get_view_by_id(tid)
watcher = _view.get_dir_watcher()
watcher.cancel()
window.views.insert(new_index, window.views.pop(i))
view = window.get_view_by_id(tid)
self.set_file_watcher(view)
self.window_controller.save_state()
def on_tab_switch_update(self, notebook, content=None, index=None):
self.selected_files.clear()
wid, tid = content.get_children()[0].get_name().split("|")
self.window_controller.set_active_data(wid, tid)
self.set_path_text(wid, tid)
self.set_window_title()
def get_tab_id_from_tab_box(self, tab_box):
tid = tab_box.get_children()[2]
return tid.get_text()
def get_tab_label(self, notebook, iconview):
return notebook.get_tab_label(iconview.get_parent()).get_children()[0]
def get_tab_close(self, notebook, iconview):
return notebook.get_tab_label(iconview.get_parent()).get_children()[1]
def get_tab_iconview_from_notebook(self, notebook):
return notebook.get_children()[1].get_children()[0]
def refresh_tab(data=None):
wid, tid, view, iconview, store = self.get_current_state()
view.load_directory()
self.load_store(view, store)
def update_view(self, tab_label, view, store, wid, tid):
self.load_store(view, store)
self.set_path_text(wid, tid)
char_width = len(view.get_end_of_path())
tab_label.set_width_chars(char_width)
tab_label.set_label(view.get_end_of_path())
self.set_window_title()
self.set_file_watcher(view)
self.window_controller.save_state()
def do_action_from_bar_controls(self, widget, eve=None):
action = widget.get_name()
wid, tid = self.window_controller.get_active_data()
notebook = self.builder.get_object(f"window_{wid}")
store, tab_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
view = self.get_fm_window(wid).get_view_by_id(tid)
if action == "go_up":
view.pop_from_path()
if action == "go_home":
view.set_to_home()
if action == "refresh_view":
view.load_directory()
if action == "create_tab":
dir = view.get_current_directory()
self.create_tab(wid, dir)
self.window_controller.save_state()
return
if action == "path_entry":
focused_obj = self.window.get_focus()
dir = f"{view.get_current_directory()}/"
path = widget.get_text()
if isinstance(focused_obj, Gtk.Entry):
button_box = self.path_menu.get_children()[0].get_children()[0].get_children()[0]
query = widget.get_text().replace(dir, "")
files = view.files + view.hidden
self.clear_children(button_box)
show_path_menu = False
for file in files:
if os.path.isdir(f"{dir}{file}"):
if query.lower() in file.lower():
button = Gtk.Button(label=file)
button.show()
button.connect("clicked", self.set_path_entry)
button_box.add(button)
show_path_menu = True
if not show_path_menu:
self.path_menu.popdown()
else:
self.path_menu.popup()
widget.grab_focus_without_selecting()
widget.set_position(-1)
if path.endswith(".") or path == dir:
return
traversed = view.set_path(path)
if not traversed:
return
self.update_view(tab_label, view, store, wid, tid)
try:
widget.grab_focus_without_selecting()
widget.set_position(-1)
except Exception as e:
pass
def set_path_entry(self, button=None, eve=None):
wid, tid, view, iconview, store = self.get_current_state()
path = f"{view.get_current_directory()}/{button.get_label()}"
path_entry = self.builder.get_object("path_entry")
path_entry.set_text(path)
path_entry.grab_focus_without_selecting()
path_entry.set_position(-1)
self.path_menu.popdown()
def keyboard_close_tab(self):
wid, tid = self.window_controller.get_active_data()
notebook = self.builder.get_object(f"window_{wid}")
scroll = self.builder.get_object(f"{wid}|{tid}")
page = notebook.page_num(scroll)
view = self.get_fm_window(wid).get_view_by_id(tid)
watcher = view.get_dir_watcher()
watcher.cancel()
self.get_fm_window(wid).delete_view_by_id(tid)
notebook.remove_page(page)
self.window_controller.save_state()
self.set_window_title()
# File control events
def show_hide_hidden_files(self):
wid, tid = self.window_controller.get_active_data()
view = self.get_fm_window(wid).get_view_by_id(tid)
view.hide_hidden = not view.hide_hidden
view.load_directory()
self.builder.get_object("refresh_view").released()

View File

@ -1,23 +1,17 @@
# Python imports
import os, time, threading, shlex
import os
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject, GLib, Gio
from gi.repository import Gtk, GObject, Gio
# Application imports
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class WidgetFileActionMixin:
"""docstring for WidgetFileActionMixin"""
def sizeof_fmt(self, num, suffix="B"):
for unit in ["", "K", "M", "G", "T", "Pi", "Ei", "Zi"]:
if abs(num) < 1024.0:
@ -40,70 +34,47 @@ class WidgetFileActionMixin:
return size
def set_file_watcher(self, tab):
if tab.get_dir_watcher():
watcher = tab.get_dir_watcher()
def set_file_watcher(self, view):
if view.get_dir_watcher():
watcher = view.get_dir_watcher()
watcher.cancel()
if debug:
self.logger.debug(f"Watcher Is Cancelled: {watcher.is_cancelled()}")
print(f"Watcher Is Cancelled: {watcher.is_cancelled()}")
cur_dir = tab.get_current_directory()
cur_dir = view.get_current_directory()
# Temp updating too much with current events we are checking for.
# Seems to cause invalid iter errors in WidbetMixin > update_store
if cur_dir == "/tmp":
watcher = None
return
dir_watcher = Gio.File.new_for_path(cur_dir) \
.monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, Gio.Cancellable())
wid = tab.get_wid()
tid = tab.get_id()
wid = view.get_wid()
tid = view.get_tab_id()
dir_watcher.connect("changed", self.dir_watch_updates, (f"{wid}|{tid}",))
tab.set_dir_watcher(dir_watcher)
view.set_dir_watcher(dir_watcher)
# NOTE: Too lazy to impliment a proper update handler and so just regen store and update tab.
# Use a lock system to prevent too many update calls for certain instances but user can manually refresh if they have urgency
def dir_watch_updates(self, file_monitor, file, other_file=None, eve_type=None, data=None):
if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED,
Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN,
Gio.FileMonitorEvent.MOVED_OUT]:
if debug:
self.logger.debug(eve_type)
Gio.FileMonitorEvent.MOVED_OUT]:
if debug:
print(eve_type)
if eve_type in [Gio.FileMonitorEvent.MOVED_IN, Gio.FileMonitorEvent.MOVED_OUT]:
self.update_on_soft_lock_end(data[0])
elif data[0] in self.soft_update_lock.keys():
self.soft_update_lock[data[0]]["last_update_time"] = time.time()
else:
self.soft_lock_countdown(data[0])
wid, tid = data[0].split("|")
notebook = self.builder.get_object(f"window_{wid}")
view = self.get_fm_window(wid).get_view_by_id(tid)
iconview = self.builder.get_object(f"{wid}|{tid}|iconview")
store = iconview.get_model()
_store, tab_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
@threaded
def soft_lock_countdown(self, tab_widget):
self.soft_update_lock[tab_widget] = { "last_update_time": time.time()}
view.load_directory()
self.load_store(view, store)
tab_label.set_label(view.get_end_of_path())
self.set_bottom_labels(view)
lock = True
while lock:
time.sleep(0.6)
last_update_time = self.soft_update_lock[tab_widget]["last_update_time"]
current_time = time.time()
if (current_time - last_update_time) > 0.6:
lock = False
self.soft_update_lock.pop(tab_widget, None)
GLib.idle_add(self.update_on_soft_lock_end, *(tab_widget,))
def update_on_soft_lock_end(self, tab_widget):
wid, tid = tab_widget.split("|")
notebook = self.builder.get_object(f"window_{wid}")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
icon_grid = self.builder.get_object(f"{wid}|{tid}|icon_grid")
store = icon_grid.get_model()
_store, tab_widget_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
tab.load_directory()
self.load_store(tab, store)
tab_widget_label.set_label(tab.get_end_of_path())
state = self.get_current_state()
if [wid, tid] in [state.wid, state.tid]:
self.set_bottom_labels(tab)
def popup_search_files(self, wid, keyname):
@ -114,57 +85,60 @@ class WidgetFileActionMixin:
entry.set_position(-1)
def do_file_search(self, widget, eve=None):
query = widget.get_text().lower()
self.search_icon_grid.unselect_all()
for i, file in enumerate(self.search_tab.get_files()):
if query and query in file[0].lower():
query = widget.get_text()
self.search_iconview.unselect_all()
for i, file in enumerate(self.search_view.files):
if query and query in file.lower():
path = Gtk.TreePath().new_from_indices([i])
self.search_icon_grid.select_path(path)
self.search_iconview.select_path(path)
items = self.search_icon_grid.get_selected_items()
if len(items) > 0:
self.search_icon_grid.scroll_to_path(items[-1], True, 0.5, 0.5)
items = self.search_iconview.get_selected_items()
if len(items) == 1:
self.search_iconview.scroll_to_path(items[0], True, 0.5, 0.5)
def open_files(self):
state = self.get_current_state()
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
wid, tid, view, iconview, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
for file in uris:
state.tab.open_file_locally(file)
view.open_file_locally(file)
def open_with_files(self, appchooser_widget):
state = self.get_current_state()
wid, tid, view, iconview, store = self.get_current_state()
app_info = appchooser_widget.get_app_info()
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files)
state.tab.app_chooser_exec(app_info, uris)
uris = self.format_to_uris(store, wid, tid, self.selected_files)
view.app_chooser_exec(app_info, uris)
def execute_files(self, in_terminal=False):
state = self.get_current_state()
paths = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
current_dir = state.tab.get_current_directory()
wid, tid, view, iconview, store = self.get_current_state()
paths = self.format_to_uris(store, wid, tid, self.selected_files, True)
current_dir = view.get_current_directory()
command = None
for path in paths:
command = f"{shlex.quote(path)}" if not in_terminal else f"{state.tab.terminal_app} -e {shlex.quote(path)}"
state.tab.execute(shlex.split(command), start_dir=state.tab.get_current_directory())
command = f"exec '{path}'" if not in_terminal else f"{view.terminal_app} -e '{path}'"
view.execute(command, start_dir=view.get_current_directory(), use_os_system=False)
def archive_files(self, archiver_dialogue):
state = self.get_current_state()
paths = [shlex.quote(p) for p in self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)]
wid, tid, view, iconview, store = self.get_current_state()
paths = self.format_to_uris(store, wid, tid, self.selected_files, True)
save_target = archiver_dialogue.get_filename();
sItr, eItr = self.arc_command_buffer.get_bounds()
pre_command = self.arc_command_buffer.get_text(sItr, eItr, False)
pre_command = pre_command.replace("%o", shlex.quote(save_target))
pre_command = pre_command.replace("%o", save_target)
pre_command = pre_command.replace("%N", ' '.join(paths))
command = f"{state.tab.terminal_app} -e {shlex.quote(pre_command)}"
command = f"{view.terminal_app} -e '{pre_command}'"
state.tab.execute(shlex.split(command), start_dir=shlex.quote(state.tab.get_current_directory()))
view.execute(command, start_dir=None, use_os_system=True)
def rename_files(self):
rename_label = self.builder.get_object("file_to_rename_label")
rename_input = self.builder.get_object("new_rename_fname")
state = self.get_current_state()
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
wid, tid, view, iconview, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
for uri in uris:
entry = uri.split("/")[-1]
@ -172,7 +146,6 @@ class WidgetFileActionMixin:
rename_input.set_text(entry)
self.show_edit_file_menu(rename_input)
if self.skip_edit:
self.skip_edit = False
continue
@ -181,7 +154,7 @@ class WidgetFileActionMixin:
break
rname_to = rename_input.get_text().strip()
target = f"{state.tab.get_current_directory()}/{rname_to}"
target = f"{view.get_current_directory()}/{rname_to}"
self.handle_files([uri], "rename", target)
@ -191,30 +164,28 @@ class WidgetFileActionMixin:
self.selected_files.clear()
def cut_files(self):
self.to_copy_files.clear()
state = self.get_current_state()
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
wid, tid, view, iconview, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
self.to_cut_files = uris
def copy_files(self):
self.to_cut_files.clear()
state = self.get_current_state()
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
wid, tid, view, iconview, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
self.to_copy_files = uris
def paste_files(self):
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
target = f"{tab.get_current_directory()}"
wid, tid = self.window_controller.get_active_data()
view = self.get_fm_window(wid).get_view_by_id(tid)
target = f"{view.get_current_directory()}"
if self.to_copy_files:
if len(self.to_copy_files) > 0:
self.handle_files(self.to_copy_files, "copy", target)
elif self.to_cut_files:
elif len(self.to_cut_files) > 0:
self.handle_files(self.to_cut_files, "move", target)
def delete_files(self):
state = self.get_current_state()
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
wid, tid, view, iconview, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
response = None
self.warning_alert.format_secondary_text(f"Do you really want to delete the {len(uris)} file(s)?")
@ -228,7 +199,7 @@ class WidgetFileActionMixin:
type = file.query_file_type(flags=Gio.FileQueryInfoFlags.NONE)
if type == Gio.FileType.DIRECTORY:
state.tab.delete_file( file.get_path() )
view.delete_file( file.get_path() )
else:
file.delete(cancellable=None)
else:
@ -236,14 +207,14 @@ class WidgetFileActionMixin:
def trash_files(self):
state = self.get_current_state()
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
wid, tid, view, iconview, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
for uri in uris:
self.trashman.trash(uri, False)
def restore_trash_files(self):
state = self.get_current_state()
uris = self.format_to_uris(state.store, state.wid, state.tid, self.selected_files, True)
wid, tid, view, iconview, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
for uri in uris:
self.trashman.restore(filename=uri.split("/")[-1], verbose=False)
@ -256,9 +227,9 @@ class WidgetFileActionMixin:
file_name = fname_field.get_text().strip()
type = self.builder.get_object("context_menu_type_toggle").get_state()
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
target = f"{tab.get_current_directory()}"
wid, tid = self.window_controller.get_active_data()
view = self.get_fm_window(wid).get_view_by_id(tid)
target = f"{view.get_current_directory()}"
if file_name:
path = f"{target}/{file_name}"
@ -268,12 +239,12 @@ class WidgetFileActionMixin:
else: # Create Folder
self.handle_files([path], "create_dir")
self.hide_new_file_menu()
fname_field.set_text("")
def move_files(self, files, target):
self.handle_files(files, "move", target)
# NOTE: Gtk recommends using fail flow than pre check which is more
# NOTE: Gtk recommends using fail flow than pre check existence which is more
# race condition proof. They're right; but, they can't even delete
# directories properly. So... f**k them. I'll do it my way.
def handle_files(self, paths, action, _target_path=None):
@ -290,9 +261,6 @@ class WidgetFileActionMixin:
file = Gio.File.new_for_path(path)
if _target_path:
if file.get_parent().get_path() == _target_path:
raise Exception("Parent dir of target and file locations are the same! Won't copy or move!")
if os.path.isdir(_target_path):
info = file.query_info("standard::display-name", 0, cancellable=None)
_target = f"{_target_path}/{info.get_display_name()}"
@ -305,7 +273,8 @@ class WidgetFileActionMixin:
if _file.query_exists():
if not overwrite_all and not rename_auto_all:
self.setup_exists_data(file, _file)
self.exists_file_label.set_label(_file.get_basename())
self.exists_file_field.set_text(_file.get_basename())
response = self.show_exists_page()
if response == "overwrite_all":
@ -326,9 +295,9 @@ class WidgetFileActionMixin:
type = _file.query_file_type(flags=Gio.FileQueryInfoFlags.NONE)
if type == Gio.FileType.DIRECTORY:
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
tab.delete_file( _file.get_path() )
wid, tid = self.window_controller.get_active_data()
view = self.get_fm_window(wid).get_view_by_id(tid)
view.delete_file( _file.get_path() )
else:
_file.delete(cancellable=None)
@ -353,16 +322,16 @@ class WidgetFileActionMixin:
type = file.query_file_type(flags=Gio.FileQueryInfoFlags.NONE)
if type == Gio.FileType.DIRECTORY:
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
wid, tid = self.window_controller.get_active_data()
view = self.get_fm_window(wid).get_view_by_id(tid)
fPath = file.get_path()
tPath = target.get_path()
state = True
if action == "copy":
tab.copy_file(fPath, tPath)
view.copy_file(fPath, tPath)
if action == "move" or action == "rename":
tab.move_file(fPath, tPath)
view.move_file(fPath, tPath)
else:
if action == "copy":
file.copy(target, flags=Gio.FileCopyFlags.BACKUP, cancellable=None)
@ -375,45 +344,6 @@ class WidgetFileActionMixin:
self.exists_file_rename_bttn.set_sensitive(False)
def setup_exists_data(self, from_file, to_file):
from_info = from_file.query_info("standard::*,time::modified", 0, cancellable=None)
to_info = to_file.query_info("standard::*,time::modified", 0, cancellable=None)
exists_file_diff_from = self.builder.get_object("exists_file_diff_from")
exists_file_diff_to = self.builder.get_object("exists_file_diff_to")
exists_file_from = self.builder.get_object("exists_file_from")
exists_file_to = self.builder.get_object("exists_file_to")
from_date = from_info.get_modification_date_time()
to_date = to_info.get_modification_date_time()
from_size = from_info.get_size()
to_size = to_info.get_size()
exists_file_from.set_label(from_file.get_parent().get_path())
exists_file_to.set_label(to_file.get_parent().get_path())
self.exists_file_label.set_label(to_file.get_basename())
self.exists_file_field.set_text(to_file.get_basename())
# Returns: -1, 0 or 1 if dt1 is less than, equal to or greater than dt2.
age = GLib.DateTime.compare(from_date, to_date)
age_text = "( same time )"
if age == -1:
age_text = "older"
if age == 1:
age_text = "newer"
size_text = "( same size )"
if from_size < to_size:
size_text = "smaller"
if from_size > to_size:
size_text = "larger"
from_label_text = f"{age_text} & {size_text}"
if age_text != "( same time )" or size_text != "( same size )":
from_label_text = f"{from_date.format('%F %R')} {self.sizeof_fmt(from_size)} ( {from_size} bytes ) ( {age_text} & {size_text} )"
to_label_text = f"{to_date.format('%F %R')} {self.sizeof_fmt(to_size)} ( {to_size} bytes )"
exists_file_diff_from.set_text(from_label_text)
exists_file_diff_to.set_text(to_label_text)
def rename_proc(self, gio_file):
full_path = gio_file.get_path()
@ -424,10 +354,10 @@ class WidgetFileActionMixin:
start = "-copy"
if debug:
self.logger.debug(f"Path: {full_path}")
self.logger.debug(f"Base Path: {base_path}")
self.logger.debug(f'Name: {file_name}')
self.logger.debug(f"Extension: {extension}")
print(f"Path: {full_path}")
print(f"Base Path: {base_path}")
print(f'Name: {file_name}')
print(f"Extension: {extension}")
i = 2
while target.query_exists():

View File

@ -20,31 +20,29 @@ def threaded(fn):
class WidgetMixin:
"""docstring for WidgetMixin"""
def load_store(self, tab, store, save_state=False):
def load_store(self, view, store, save_state=False):
store.clear()
dir = tab.get_current_directory()
files = tab.get_files()
dir = view.get_current_directory()
files = view.get_files()
for i, file in enumerate(files):
store.append([None, file[0]])
self.create_icon(i, tab, store, dir, file[0])
self.create_icon(i, view, store, dir, file[0])
# NOTE: Not likely called often from here but it could be useful
if save_state:
self.fm_controller.save_state()
self.window_controller.save_state()
@threaded
def create_icon(self, i, tab, store, dir, file):
icon = tab.create_icon(dir, file)
def create_icon(self, i, view, store, dir, file):
icon = view.create_icon(dir, file)
fpath = f"{dir}/{file}"
GLib.idle_add(self.update_store, (i, store, icon, tab, fpath,))
GLib.idle_add(self.update_store, (i, store, icon, view, fpath,))
# NOTE: Might need to keep an eye on this throwing invalid iters when too
# many updates are happening to a folder. Example: /tmp
def update_store(self, item):
i, store, icon, tab, fpath = item
i, store, icon, view, fpath = item
itr = None
try:
@ -60,12 +58,12 @@ class WidgetMixin:
return
if not icon:
icon = self.get_system_thumbnail(fpath, tab.SYS_ICON_WH[0])
icon = self.get_system_thumbnail(fpath, view.SYS_ICON_WH[0])
if not icon:
if fpath.endswith(".gif"):
icon = GdkPixbuf.PixbufAnimation.get_static_image(fpath)
else:
icon = GdkPixbuf.Pixbuf.new_from_file(tab.DEFAULT_ICON)
icon = GdkPixbuf.Pixbuf.new_from_file(view.DEFAULT_ICON)
store.set_value(itr, 0, icon)
@ -90,29 +88,31 @@ class WidgetMixin:
return None
def create_tab_widget(self, tab):
tab_widget = Gtk.ButtonBox()
def create_tab_widget(self, view):
tab = Gtk.ButtonBox()
label = Gtk.Label()
tid = Gtk.Label()
close = Gtk.Button()
icon = Gtk.Image(stock=Gtk.STOCK_CLOSE)
label.set_label(f"{tab.get_end_of_path()}")
label.set_width_chars(len(tab.get_end_of_path()))
label.set_label(f"{view.get_end_of_path()}")
label.set_width_chars(len(view.get_end_of_path()))
label.set_xalign(0.0)
tid.set_label(f"{tab.get_id()}")
tid.set_label(f"{view.id}")
close.add(icon)
tab_widget.add(label)
tab_widget.add(close)
tab_widget.add(tid)
tab.add(label)
tab.add(close)
tab.add(tid)
close.connect("released", self.close_tab)
tab_widget.show_all()
tab.show_all()
tid.hide()
return tab_widget
return tab
def create_icon_grid_widget(self, tab, wid):
def create_grid_iconview_widget(self, view, wid):
scroll = Gtk.ScrolledWindow()
grid = Gtk.IconView()
store = Gtk.ListStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str)
@ -132,11 +132,13 @@ class WidgetMixin:
grid.set_column_spacing(18)
grid.connect("button_release_event", self.grid_icon_single_click)
grid.connect("item-activated", self.grid_icon_double_click)
grid.connect("selection-changed", self.grid_set_selected_items)
grid.connect("drag-data-get", self.grid_on_drag_set)
grid.connect("drag-data-received", self.grid_on_drag_data_received)
grid.connect("drag-motion", self.grid_on_drag_motion)
grid.connect("item-activated", self.grid_icon_double_click)
# grid.connect("toggle-cursor-item", self.grid_cursor_toggled)
# grid.connect("notify", self.grid_cursor_toggled)
grid.connect("selection-changed", self.grid_set_selected_items)
grid.connect("drag-data-get", self.grid_on_drag_set)
grid.connect("drag-data-received", self.grid_on_drag_data_received)
grid.connect("drag-motion", self.grid_on_drag_motion)
URI_TARGET_TYPE = 80
@ -148,16 +150,17 @@ class WidgetMixin:
grid.show_all()
scroll.add(grid)
grid.set_name(f"{wid}|{tab.get_id()}")
scroll.set_name(f"{wid}|{tab.get_id()}")
self.builder.expose_object(f"{wid}|{tab.get_id()}|icon_grid", grid)
self.builder.expose_object(f"{wid}|{tab.get_id()}", scroll)
grid.set_name(f"{wid}|{view.id}")
scroll.set_name(f"{wid}|{view.id}")
self.builder.expose_object(f"{wid}|{view.id}|iconview", grid)
self.builder.expose_object(f"{wid}|{view.id}", scroll)
return scroll, store
def create_icon_tree_widget(self, tab, wid):
def create_grid_treeview_widget(self, view, wid):
scroll = Gtk.ScrolledWindow()
grid = Gtk.TreeView()
store = Gtk.TreeStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str)
store = Gtk.ListStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str)
# store = Gtk.TreeStore(GdkPixbuf.Pixbuf or None, str)
column = Gtk.TreeViewColumn("Icons")
icon = Gtk.CellRendererPixbuf()
name = Gtk.CellRendererText()
@ -181,10 +184,10 @@ class WidgetMixin:
grid.set_enable_tree_lines(False)
grid.connect("button_release_event", self.grid_icon_single_click)
grid.connect("row-activated", self.grid_icon_double_click)
grid.connect("drag-data-get", self.grid_on_drag_set)
grid.connect("drag-data-received", self.grid_on_drag_data_received)
grid.connect("drag-motion", self.grid_on_drag_motion)
grid.connect("row-activated", self.grid_icon_double_click)
grid.connect("drag-data-get", self.grid_on_drag_set)
grid.connect("drag-data-received", self.grid_on_drag_data_received)
grid.connect("drag-motion", self.grid_on_drag_motion)
URI_TARGET_TYPE = 80
uri_target = Gtk.TargetEntry.new('text/uri-list', Gtk.TargetFlags(0), URI_TARGET_TYPE)
@ -196,23 +199,23 @@ class WidgetMixin:
grid.show_all()
scroll.add(grid)
grid.set_name(f"{wid}|{tab.get_id()}")
scroll.set_name(f"{wid}|{tab.get_id()}")
grid.set_name(f"{wid}|{view.id}")
scroll.set_name(f"{wid}|{view.id}")
grid.columns_autosize()
self.builder.expose_object(f"{wid}|{tab.get_id()}", scroll)
self.builder.expose_object(f"{wid}|{view.id}", scroll)
return scroll, store
def get_store_and_label_from_notebook(self, notebook, _name):
icon_grid = None
icon_view = None
tab_label = None
store = None
for obj in notebook.get_children():
icon_grid = obj.get_children()[0]
name = icon_grid.get_name()
icon_view = obj.get_children()[0]
name = icon_view.get_name()
if name == _name:
store = icon_grid.get_model()
store = icon_view.get_model()
tab_label = notebook.get_tab_label(obj).get_children()[0]
return store, tab_label

View File

@ -9,56 +9,58 @@ gi.require_version('Gdk', '3.0')
from gi.repository import Gdk, Gio
# Application imports
from .tab_mixin import TabMixin
from . import TabMixin, WidgetMixin
class WindowMixin(TabMixin):
"""docstring for WindowMixin"""
def generate_windows(self, session_json = None):
if session_json:
for j, value in enumerate(session_json):
def generate_windows(self, data = None):
if data:
for j, value in enumerate(data):
i = j + 1
notebook_tggl_button = self.builder.get_object(f"tggl_notebook_{i}")
is_hidden = True if value[0]["window"]["isHidden"] == "True" else False
tabs = value[0]["window"]["tabs"]
self.fm_controller.create_window()
notebook_tggl_button.set_active(True)
isHidden = True if value[0]["window"]["isHidden"] == "True" else False
object = self.builder.get_object(f"tggl_notebook_{i}")
views = value[0]["window"]["views"]
self.window_controller.create_window()
object.set_active(True)
for tab in tabs:
self.create_new_tab_notebook(None, i, tab)
for view in views:
self.create_new_view_notebook(None, i, view)
if is_hidden:
self.toggle_notebook_pane(notebook_tggl_button)
if isHidden:
self.toggle_notebook_pane(object)
try:
if not self.is_pane4_hidden:
icon_grid = self.window4.get_children()[1].get_children()[0]
icon_view = self.window4.get_children()[1].get_children()[0]
icon_view.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE))
elif not self.is_pane3_hidden:
icon_grid = self.window3.get_children()[1].get_children()[0]
icon_view = self.window3.get_children()[1].get_children()[0]
icon_view.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE))
elif not self.is_pane2_hidden:
icon_grid = self.window2.get_children()[1].get_children()[0]
icon_view = self.window2.get_children()[1].get_children()[0]
icon_view.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE))
elif not self.is_pane1_hidden:
icon_grid = self.window1.get_children()[1].get_children()[0]
icon_grid.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE))
icon_grid.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE))
icon_view = self.window1.get_children()[1].get_children()[0]
icon_view.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE))
except Exception as e:
print("\n: The saved session might be missing window data! :\nLocation: ~/.config/solarfm/session.json\nFix: Back it up and delete it to reset.\n")
print(repr(e))
else:
for j in range(0, 4):
i = j + 1
self.fm_controller.create_window()
self.create_new_tab_notebook(None, i, None)
self.window_controller.create_window()
self.create_new_view_notebook(None, i, None)
def get_fm_window(self, wid):
return self.fm_controller.get_window_by_nickname(f"window_{wid}")
return self.window_controller.get_window_by_nickname(f"window_{wid}")
def format_to_uris(self, store, wid, tid, treePaths, use_just_path=False):
tab = self.get_fm_window(wid).get_tab_by_id(tid)
dir = tab.get_current_directory()
view = self.get_fm_window(wid).get_view_by_id(tid)
dir = view.get_current_directory()
uris = []
for path in treePaths:
@ -76,10 +78,10 @@ class WindowMixin(TabMixin):
return uris
def set_bottom_labels(self, tab):
_wid, _tid, _tab, icon_grid, store = self.get_current_state()
selected_files = icon_grid.get_selected_items()
current_directory = tab.get_current_directory()
def set_bottom_labels(self, view):
_wid, _tid, _view, iconview, store = self.get_current_state()
selected_files = iconview.get_selected_items()
current_directory = view.get_current_directory()
path_file = Gio.File.new_for_path(current_directory)
mount_file = path_file.query_filesystem_info(attributes="filesystem::*", cancellable=None)
formatted_mount_free = self.sizeof_fmt( int(mount_file.get_attribute_as_string("filesystem::free")) )
@ -94,8 +96,8 @@ class WindowMixin(TabMixin):
# If something selected
self.bottom_size_label.set_label(f"{formatted_mount_free} free / {formatted_mount_size}")
self.bottom_path_label.set_label(tab.get_current_directory())
if selected_files:
self.bottom_path_label.set_label(view.get_current_directory())
if len(selected_files) > 0:
uris = self.format_to_uris(store, _wid, _tid, selected_files, True)
combined_size = 0
for uri in uris:
@ -111,29 +113,29 @@ class WindowMixin(TabMixin):
formatted_size = self.sizeof_fmt(combined_size)
if tab.is_hiding_hidden():
self.bottom_path_label.set_label(f" {len(uris)} / {tab.get_files_count()} ({formatted_size})")
if view.hide_hidden:
self.bottom_path_label.set_label(f" {len(uris)} / {view.get_files_count()} ({formatted_size})")
else:
self.bottom_path_label.set_label(f" {len(uris)} / {tab.get_not_hidden_count()} ({formatted_size})")
self.bottom_path_label.set_label(f" {len(uris)} / {view.get_not_hidden_count()} ({formatted_size})")
return
# If nothing selected
if tab.get_hidden():
if tab.get_hidden_count() > 0:
self.bottom_file_count_label.set_label(f"{tab.get_not_hidden_count()} visible ({tab.get_hidden_count()} hidden)")
if view.hide_hidden:
if view.get_hidden_count() > 0:
self.bottom_file_count_label.set_label(f"{view.get_not_hidden_count()} visible ({view.get_hidden_count()} hidden)")
else:
self.bottom_file_count_label.set_label(f"{tab.get_files_count()} items")
self.bottom_file_count_label.set_label(f"{view.get_files_count()} items")
else:
self.bottom_file_count_label.set_label(f"{tab.get_files_count()} items")
self.bottom_file_count_label.set_label(f"{view.get_files_count()} items")
def set_window_title(self):
wid, tid = self.fm_controller.get_active_wid_and_tid()
wid, tid = self.window_controller.get_active_data()
notebook = self.builder.get_object(f"window_{wid}")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
dir = tab.get_current_directory()
view = self.get_fm_window(wid).get_view_by_id(tid)
dir = view.get_current_directory()
for _notebook in self.notebooks:
ctx = _notebook.get_style_context()
@ -144,74 +146,72 @@ class WindowMixin(TabMixin):
ctx.remove_class("notebook-unselected-focus")
ctx.add_class("notebook-selected-focus")
self.window.set_title(f"SolarFM ~ {dir}")
self.set_bottom_labels(tab)
self.window.set_title("SolarFM ~ " + dir)
self.set_bottom_labels(view)
def set_path_text(self, wid, tid):
path_entry = self.builder.get_object("path_entry")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
path_entry.set_text(tab.get_current_directory())
view = self.get_fm_window(wid).get_view_by_id(tid)
path_entry.set_text(view.get_current_directory())
def grid_set_selected_items(self, icons_grid):
self.selected_files = icons_grid.get_selected_items()
def grid_set_selected_items(self, iconview):
self.selected_files = iconview.get_selected_items()
def grid_cursor_toggled(self, icons_grid):
def grid_cursor_toggled(self, iconview):
print("wat...")
def grid_icon_single_click(self, icons_grid, eve):
def grid_icon_single_click(self, iconview, eve):
try:
self.path_menu.popdown()
wid, tid = icons_grid.get_name().split("|")
self.fm_controller.set__wid_and_tid(wid, tid)
wid, tid = iconview.get_name().split("|")
self.window_controller.set_active_data(wid, tid)
self.set_path_text(wid, tid)
self.set_window_title()
if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 1: # l-click
if self.single_click_open: # FIXME: need to find a way to pass the model index
self.grid_icon_double_click(icons_grid)
self.grid_icon_double_click(iconview)
elif eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 3: # r-click
self.show_context_menu()
except Exception as e:
print(repr(e))
self.display_message(self.error_color, f"{repr(e)}")
self.display_message(self.error, f"{repr(e)}")
def grid_icon_double_click(self, icons_grid, item, data=None):
def grid_icon_double_click(self, iconview, item, data=None):
try:
if self.ctrl_down and self.shift_down:
self.unset_keys_and_data()
if self.ctrlDown and self.shiftDown:
self.execute_files(in_terminal=True)
return
elif self.ctrl_down:
self.unset_keys_and_data()
elif self.ctrlDown:
self.execute_files()
return
wid, tid, tab, _icons_grid, store = self.get_current_state()
wid, tid, view, _iconview, store = self.get_current_state()
notebook = self.builder.get_object(f"window_{wid}")
tab_label = self.get_tab_label(notebook, icons_grid)
tab_label = self.get_tab_label(notebook, iconview)
fileName = store[item][1]
dir = tab.get_current_directory()
dir = view.get_current_directory()
file = f"{dir}/{fileName}"
if isdir(file):
tab.set_path(file)
self.update_tab(tab_label, tab, store, wid, tid)
view.set_path(file)
self.update_view(tab_label, view, store, wid, tid)
else:
self.open_files()
except Exception as e:
self.display_message(self.error_color, f"{repr(e)}")
self.display_message(self.error, f"{repr(e)}")
def grid_on_drag_set(self, icons_grid, drag_context, data, info, time):
action = icons_grid.get_name()
def grid_on_drag_set(self, iconview, drag_context, data, info, time):
action = iconview.get_name()
wid, tid = action.split("|")
store = icons_grid.get_model()
treePaths = icons_grid.get_selected_items()
store = iconview.get_model()
treePaths = iconview.get_selected_items()
# NOTE: Need URIs as URI format for DnD to work. Will strip 'file://'
# further down call chain when doing internal fm stuff.
uris = self.format_to_uris(store, wid, tid, treePaths)
@ -220,30 +220,30 @@ class WindowMixin(TabMixin):
data.set_uris(uris)
data.set_text(uris_text, -1)
def grid_on_drag_motion(self, icons_grid, drag_context, x, y, data):
current = '|'.join(self.fm_controller.get_active_wid_and_tid())
target = icons_grid.get_name()
def grid_on_drag_motion(self, iconview, drag_context, x, y, data):
current = '|'.join(self.window_controller.get_active_data())
target = iconview.get_name()
wid, tid = target.split("|")
store = icons_grid.get_model()
treePath = icons_grid.get_drag_dest_item().path
store = iconview.get_model()
treePath = iconview.get_drag_dest_item().path
if treePath:
uri = self.format_to_uris(store, wid, tid, treePath)[0].replace("file://", "")
self.override_drop_dest = uri if isdir(uri) else None
if target not in current:
self.fm_controller.set__wid_and_tid(wid, tid)
self.window_controller.set_active_data(wid, tid)
def grid_on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
if info == 80:
wid, tid = self.fm_controller.get_active_wid_and_tid()
wid, tid = self.window_controller.get_active_data()
notebook = self.builder.get_object(f"window_{wid}")
store, tab_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
view = self.get_fm_window(wid).get_view_by_id(tid)
uris = data.get_uris()
dest = f"{tab.get_current_directory()}" if not self.override_drop_dest else self.override_drop_dest
dest = f"{view.get_current_directory()}" if not self.override_drop_dest else self.override_drop_dest
if len(uris) == 0:
uris = data.get_text().split("\n")
@ -252,5 +252,5 @@ class WindowMixin(TabMixin):
self.move_files(uris, dest)
def create_new_tab_notebook(self, widget=None, wid=None, path=None):
def create_new_view_notebook(self, widget=None, wid=None, path=None):
self.create_tab(wid, path)

View File

@ -0,0 +1,5 @@
from .PaneMixin import PaneMixin
from .WidgetMixin import WidgetMixin
from .TabMixin import TabMixin
from .WindowMixin import WindowMixin
from .WidgetFileActionMixin import WidgetFileActionMixin

View File

@ -1,3 +0,0 @@
"""
Trasher module
"""

View File

@ -21,7 +21,7 @@ class Trash(object):
if os.path.isfile(item):
size = size + os.path.getsize(item)
elif os.path.isdir(item):
size = size + self.size_dir(item)
size = size + size_dir(item)
return size

View File

View File

@ -0,0 +1,95 @@
# Python imports
import os
from os import path
# Gtk imports
import gi, cairo
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk as gtk
from gi.repository import Gdk as gdk
# Application imports
from . import Logger
class Settings:
def __init__(self):
self.builder = gtk.Builder()
self.SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__))
self.USER_HOME = path.expanduser('~')
self.CONFIG_PATH = f"{self.USER_HOME}/.config/{app_name.lower()}"
self.PLUGINS_PATH = f"{self.CONFIG_PATH}/plugins"
self.USR_SOLARFM = f"/usr/share/{app_name.lower()}"
self.cssFile = f"{self.CONFIG_PATH}/stylesheet.css"
self.windows_glade = f"{self.CONFIG_PATH}/Main_Window.glade"
self.DEFAULT_ICONS = f"{self.CONFIG_PATH}/icons"
self.window_icon = f"{self.DEFAULT_ICONS}/{app_name.lower()}.png"
self.main_window = None
if not os.path.exists(self.CONFIG_PATH):
os.mkdir(self.CONFIG_PATH)
if not os.path.exists(self.PLUGINS_PATH):
os.mkdir(self.PLUGINS_PATH)
if not os.path.exists(self.windows_glade):
self.windows_glade = f"{self.USR_SOLARFM}/Main_Window.glade"
if not os.path.exists(self.cssFile):
self.cssFile = f"{self.USR_SOLARFM}/stylesheet.css"
if not os.path.exists(self.window_icon):
self.window_icon = f"{self.USR_SOLARFM}/icons/{app_name.lower()}.png"
if not os.path.exists(self.DEFAULT_ICONS):
self.DEFAULT_ICONS = f"{self.USR_SOLARFM}/icons"
self.logger = Logger(self.CONFIG_PATH).get_logger()
self.builder.add_from_file(self.windows_glade)
def create_window(self):
# Get window and connect signals
self.main_window = self.builder.get_object("Main_Window")
self._set_window_data()
def _set_window_data(self):
self.main_window.set_icon_from_file(self.window_icon)
screen = self.main_window.get_screen()
visual = screen.get_rgba_visual()
if visual != None and screen.is_composited():
self.main_window.set_visual(visual)
self.main_window.set_app_paintable(True)
self.main_window.connect("draw", self._area_draw)
# bind css file
cssProvider = gtk.CssProvider()
cssProvider.load_from_path(self.cssFile)
screen = gdk.Screen.get_default()
styleContext = gtk.StyleContext()
styleContext.add_provider_for_screen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_USER)
def _area_draw(self, widget, cr):
cr.set_source_rgba(0, 0, 0, 0.54)
cr.set_operator(cairo.OPERATOR_SOURCE)
cr.paint()
cr.set_operator(cairo.OPERATOR_OVER)
def get_monitor_data(self):
screen = self.builder.get_object("Main_Window").get_screen()
monitors = []
for m in range(screen.get_n_monitors()):
monitors.append(screen.get_monitor_geometry(m))
for monitor in monitors:
print("{}x{}+{}+{}".format(monitor.width, monitor.height, monitor.x, monitor.y))
return monitors
def get_builder(self): return self.builder
def get_logger(self): return self.logger
def get_main_window(self): return self.main_window
def get_plugins_path(self): return self.PLUGINS_PATH

View File

@ -1,3 +1,6 @@
"""
Utils module
"""
from .Logger import Logger
from .Settings import Settings

View File

@ -1,102 +0,0 @@
# Python imports
import os
from os import path
# Gtk imports
import gi, cairo
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
# Application imports
from .logger import Logger
class Settings:
def __init__(self):
self._SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__))
self._USER_HOME = path.expanduser('~')
self._CONFIG_PATH = f"{self._USER_HOME}/.config/{app_name.lower()}"
self._PLUGINS_PATH = f"{self._CONFIG_PATH}/plugins"
self._USR_SOLARFM = f"/usr/share/{app_name.lower()}"
self._CSS_FILE = f"{self._CONFIG_PATH}/stylesheet.css"
self._WINDOWS_GLADE = f"{self._CONFIG_PATH}/Main_Window.glade"
self._DEFAULT_ICONS = f"{self._CONFIG_PATH}/icons"
self._WINDOW_ICON = f"{self._DEFAULT_ICONS}/{app_name.lower()}.png"
if not os.path.exists(self._CONFIG_PATH):
os.mkdir(self._CONFIG_PATH)
if not os.path.exists(self._PLUGINS_PATH):
os.mkdir(self._PLUGINS_PATH)
if not os.path.exists(self._WINDOWS_GLADE):
self._WINDOWS_GLADE = f"{self._USR_SOLARFM}/Main_Window.glade"
if not os.path.exists(self._CSS_FILE):
self._CSS_FILE = f"{self._USR_SOLARFM}/stylesheet.css"
if not os.path.exists(self._WINDOW_ICON):
self._WINDOW_ICON = f"{self._USR_SOLARFM}/icons/{app_name.lower()}.png"
if not os.path.exists(self._DEFAULT_ICONS):
self._DEFAULT_ICONS = f"{self._USR_SOLARFM}/icons"
self._success_color = "#88cc27"
self._warning_color = "#ffa800"
self._error_color = "#ff0000"
self.main_window = None
self.logger = Logger(self._CONFIG_PATH).get_logger()
self.builder = Gtk.Builder()
self.builder.add_from_file(self._WINDOWS_GLADE)
def create_window(self):
# Get window and connect signals
self.main_window = self.builder.get_object("Main_Window")
self._set_window_data()
def _set_window_data(self):
self.main_window.set_icon_from_file(self._WINDOW_ICON)
screen = self.main_window.get_screen()
visual = screen.get_rgba_visual()
if visual != None and screen.is_composited():
self.main_window.set_visual(visual)
self.main_window.set_app_paintable(True)
self.main_window.connect("draw", self._area_draw)
# bind css file
cssProvider = Gtk.CssProvider()
cssProvider.load_from_path(self._CSS_FILE)
screen = Gdk.Screen.get_default()
styleContext = Gtk.StyleContext()
styleContext.add_provider_for_screen(screen, cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
def _area_draw(self, widget, cr):
cr.set_source_rgba(0, 0, 0, 0.54)
cr.set_operator(cairo.OPERATOR_SOURCE)
cr.paint()
cr.set_operator(cairo.OPERATOR_OVER)
def get_monitor_data(self):
screen = self.builder.get_object("Main_Window").get_screen()
monitors = []
for m in range(screen.get_n_monitors()):
monitors.append(screen.get_monitor_geometry(m))
for monitor in monitors:
print("{}x{}+{}+{}".format(monitor.width, monitor.height, monitor.x, monitor.y))
return monitors
def get_builder(self): return self.builder
def get_logger(self): return self.logger
def get_main_window(self): return self.main_window
def get_plugins_path(self): return self._PLUGINS_PATH
def get_success_color(self): return self._success_color
def get_warning_color(self): return self._warning_color
def get_error_color(self): return self._error_color

View File

@ -1,18 +0,0 @@
#!/bin/bash
# . CONFIG.sh
# set -o xtrace ## To debug scripts
# set -o errexit ## To exit on error
# set -o errunset ## To exit if a variable is referenced but not set
function main() {
SCRIPTPATH="$( cd "$(dirname "")" >/dev/null 2>&1 ; pwd -P )"
cd "${SCRIPTPATH}"
echo "Working Dir: " $(pwd)
source '/home/abaddon/Portable_Apps/py-venvs/gtk-apps-venv/venv/bin/activate'
python -m pudb $(pwd)/solarfm/__main__.py; bash
}
main "$@";

View File

@ -4,71 +4,73 @@ import builtins
# Lib imports
# Application imports
from utils.ipc_server import IPCServer
from controller import IPCServerMixin
class EventSystem(IPCServer):
""" Inheret IPCServerMixin. Create an pub/sub systems. """
class Builtins(IPCServerMixin):
"""Docstring for __builtins__ extender"""
def __init__(self):
super(EventSystem, self).__init__()
# NOTE: The format used is list of ['who', target, (data,)] Where:
# who is the sender or target ID and is used for context and control flow,
# method_target is the method to call,
# data is the method parameters OR message data to give
# NOTE: The format used is list of [type, target, data] Where:
# type is useful context for control flow,
# target is the method to call,
# data is the method parameters to give
# Where data may be any kind of data
self._gui_events = []
self._module_events = []
self._fm_events = []
self.is_ipc_alive = False
self.ipc_authkey = b'solarfm-ipc'
self.ipc_address = '127.0.0.1'
self.ipc_port = 4848
self.ipc_timeout = 15.0
# Makeshift "events" system FIFO
def _pop_gui_event(self) -> None:
# Makeshift fake "events" type system FIFO
def _pop_gui_event(self):
if len(self._gui_events) > 0:
return self._gui_events.pop(0)
return None
def _pop_module_event(self) -> None:
if len(self._module_events) > 0:
return self._module_events.pop(0)
def _pop_fm_event(self):
if len(self._fm_events) > 0:
return self._fm_events.pop(0)
return None
def push_gui_event(self, event: list) -> None:
def push_gui_event(self, event):
if len(event) == 3:
self._gui_events.append(event)
return None
raise Exception("Invald event format! Please do: ['sender_id': str, method_target: method, (data,): any]")
raise Exception("Invald event format! Please do: [type, target, data]")
def push_module_event(self, event: list) -> None:
def push_fm_event(self, event):
if len(event) == 3:
self._module_events.append(event)
self._fm_events.append(event)
return None
raise Exception("Invald event format! Please do: ['target_id': str, method_target: method, (data,): any]")
raise Exception("Invald event format! Please do: [type, target, data]")
def read_gui_event(self) -> list:
return self._gui_events[0] if self._gui_events else None
def read_gui_event(self):
return self._gui_events[0]
def read_module_event(self) -> list:
return self._module_events[0] if self._module_events else None
def read_fm_event(self):
return self._fm_events[0]
def consume_gui_event(self) -> list:
def consume_gui_event(self):
return self._pop_gui_event()
def consume_module_event(self) -> list:
return self._pop_module_event()
def consume_fm_event(self):
return self._pop_fm_event()
# NOTE: Just reminding myself we can add to builtins two different ways...
# __builtins__.update({"event_system": Builtins()})
builtins.app_name = "SolarFM"
builtins.event_system = EventSystem()
builtins.event_sleep_time = 0.05
builtins.trace_debug = False
builtins.event_system = Builtins()
builtins.event_sleep_time = 0.2
builtins.debug = False
builtins.trace_debug = False

View File

@ -1,3 +1,54 @@
"""
Base module
"""
# Python imports
import os, inspect, time
# Lib imports
# Application imports
from utils import Settings
from controller import Controller
from __builtins__ import Builtins
class Main(Builtins):
def __init__(self, args, unknownargs):
if not debug:
event_system.create_ipc_server()
time.sleep(0.2)
if not trace_debug:
if not event_system.is_ipc_alive:
if unknownargs:
for arg in unknownargs:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.send_ipc_message(message)
if args.new_tab and os.path.isdir(args.new_tab):
message = f"FILE|{args.new_tab}"
event_system.send_ipc_message(message)
raise Exception("IPC Server Exists: Will send path(s) to it and close...")
settings = Settings()
settings.create_window()
controller = Controller(args, unknownargs, settings)
if not controller:
raise Exception("Controller exited and doesn't exist...")
# Gets the methods from the classes and sets to handler.
# Then, builder connects to any signals it needs.
classes = [controller]
handlers = {}
for c in classes:
methods = None
try:
methods = inspect.getmembers(c, predicate=inspect.ismethod)
handlers.update(methods)
except Exception as e:
print(repr(e))
settings.builder.connect_signals(handlers)

View File

@ -15,12 +15,10 @@ gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from app import Application
from __init__ import Main
if __name__ == "__main__":
""" Set process title, get arguments, and create GTK main thread. """
try:
# import web_pdb
# web_pdb.set_trace()
@ -35,7 +33,7 @@ if __name__ == "__main__":
# Read arguments (If any...)
args, unknownargs = parser.parse_known_args()
Application(args, unknownargs)
Main(args, unknownargs)
Gtk.main()
except Exception as e:
traceback.print_exc()

View File

@ -1,55 +0,0 @@
# Python imports
import os, inspect, time
# Lib imports
# Application imports
from utils.settings import Settings
from core.controller import Controller
from __builtins__ import EventSystem
class Application(EventSystem):
""" Create Settings and Controller classes. Bind signal to Builder. Inherit from Builtins to bind global methods and classes. """
def __init__(self, args, unknownargs):
if not trace_debug:
event_system.create_ipc_listener()
time.sleep(0.05)
if not event_system.is_ipc_alive:
if unknownargs:
for arg in unknownargs:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.send_ipc_message(message)
if args.new_tab and os.path.isdir(args.new_tab):
message = f"FILE|{args.new_tab}"
event_system.send_ipc_message(message)
raise Exception("IPC Server Exists: Will send path(s) to it and close...\nNote: If no fm exists, remove /tmp/solarfm-ipc.sock")
settings = Settings()
settings.create_window()
controller = Controller(args, unknownargs, settings)
if not controller:
raise Exception("Controller exited and doesn't exist...")
# Gets the methods from the classes and sets to handler.
# Then, builder connects to any signals it needs.
classes = [controller]
handlers = {}
for c in classes:
methods = None
try:
methods = inspect.getmembers(c, predicate=inspect.ismethod)
handlers.update(methods)
except Exception as e:
print(repr(e))
settings.get_builder().connect_signals(handlers)

View File

@ -0,0 +1,162 @@
# Python imports
import sys, traceback, threading, inspect, os, time
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib
# Application imports
from .mixins import UIMixin
from .signals import IPCSignalsMixin, KeyboardSignalsMixin
from . import Controller_Data
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class Controller(UIMixin, KeyboardSignalsMixin, IPCSignalsMixin, Controller_Data):
def __init__(self, args, unknownargs, _settings):
sys.excepthook = self.custom_except_hook
self.setup_controller_data(_settings)
self.window.show()
self.generate_windows(self.state)
self.plugins.launch_plugins()
if not trace_debug:
self.gui_event_observer()
if unknownargs:
for arg in unknownargs:
if os.path.isdir(arg):
message = f"FILE|{arg}"
event_system.send_ipc_message(message)
if args.new_tab and os.path.isdir(args.new_tab):
message = f"FILE|{args.new_tab}"
event_system.send_ipc_message(message)
def tear_down(self, widget=None, eve=None):
event_system.send_ipc_message("close server")
self.window_controller.save_state()
time.sleep(event_sleep_time)
Gtk.main_quit()
@threaded
def gui_event_observer(self):
while True:
time.sleep(event_sleep_time)
event = event_system.consume_gui_event()
if event:
try:
type, target, data = event
method = getattr(self.__class__, target)
GLib.idle_add(method, *(self, *data,))
except Exception as e:
print(repr(e))
def custom_except_hook(self, exctype, value, _traceback):
trace = ''.join(traceback.format_tb(_traceback))
data = f"Exectype: {exctype} <--> Value: {value}\n\n{trace}\n\n\n\n"
start_itr = self.message_buffer.get_start_iter()
self.message_buffer.place_cursor(start_itr)
self.display_message(self.error, data)
def display_message(self, type, text, seconds=None):
self.message_buffer.insert_at_cursor(text)
self.message_widget.popup()
if seconds:
self.hide_message_timeout(seconds)
@threaded
def hide_message_timeout(self, seconds=3):
time.sleep(seconds)
GLib.idle_add(self.message_widget.popdown)
def save_debug_alerts(self, widget=None, eve=None):
start_itr, end_itr = self.message_buffer.get_bounds()
save_location_prompt = Gtk.FileChooserDialog("Choose Save Folder", self.window, \
action = Gtk.FileChooserAction.SAVE, \
buttons = (Gtk.STOCK_CANCEL, \
Gtk.ResponseType.CANCEL, \
Gtk.STOCK_SAVE, \
Gtk.ResponseType.OK))
text = self.message_buffer.get_text(start_itr, end_itr, False)
resp = save_location_prompt.run()
if (resp == Gtk.ResponseType.CANCEL) or (resp == Gtk.ResponseType.DELETE_EVENT):
pass
elif resp == Gtk.ResponseType.OK:
target = save_location_prompt.get_filename();
with open(target, "w") as f:
f.write(text)
save_location_prompt.destroy()
def set_arc_buffer_text(self, widget=None, eve=None):
id = widget.get_active_id()
self.arc_command_buffer.set_text(self.arc_commands[int(id)])
def clear_children(self, widget):
for child in widget.get_children():
widget.remove(child)
def get_current_state(self):
wid, tid = self.window_controller.get_active_data()
view = self.get_fm_window(wid).get_view_by_id(tid)
iconview = self.builder.get_object(f"{wid}|{tid}|iconview")
store = iconview.get_model()
return wid, tid, view, iconview, store
def do_action_from_menu_controls(self, widget, eventbutton):
action = widget.get_name()
self.ctrlDown = True
self.hide_context_menu()
self.hide_new_file_menu()
self.hide_edit_file_menu()
if action == "open":
self.open_files()
if action == "open_with":
self.show_appchooser_menu()
if action == "execute":
self.execute_files()
if action == "execute_in_terminal":
self.execute_files(in_terminal=True)
if action == "rename":
self.rename_files()
if action == "cut":
self.to_copy_files.clear()
self.cut_files()
if action == "copy":
self.to_cut_files.clear()
self.copy_files()
if action == "paste":
self.paste_files()
if action == "archive":
self.show_archiver_dialogue()
if action == "delete":
self.delete_files()
if action == "trash":
self.trash_files()
if action == "go_to_trash":
self.builder.get_object("path_entry").set_text(self.trash_files_path)
if action == "restore_from_trash":
self.restore_trash_files()
if action == "empty_trash":
self.empty_trash()
if action == "create":
self.create_files()
self.ctrlDown = False

View File

@ -0,0 +1,109 @@
# Python imports
import signal
# Lib imports
from gi.repository import GLib
# Application imports
from trasher.xdgtrash import XDGTrash
from shellfm import WindowController
from plugins import Plugins
class Controller_Data:
def has_method(self, o, name):
return callable(getattr(o, name, None))
def setup_controller_data(self, _settings):
self.trashman = XDGTrash()
self.window_controller = WindowController()
self.plugins = Plugins(_settings)
self.state = self.window_controller.load_state()
self.trashman.regenerate()
self.settings = _settings
self.builder = self.settings.get_builder()
self.logger = self.settings.get_logger()
self.window = self.settings.get_main_window()
self.window1 = self.builder.get_object("window_1")
self.window2 = self.builder.get_object("window_2")
self.window3 = self.builder.get_object("window_3")
self.window4 = self.builder.get_object("window_4")
self.message_widget = self.builder.get_object("message_widget")
self.message_view = self.builder.get_object("message_view")
self.message_buffer = self.builder.get_object("message_buffer")
self.arc_command_buffer = self.builder.get_object("arc_command_buffer")
self.warning_alert = self.builder.get_object("warning_alert")
self.edit_file_menu = self.builder.get_object("edit_file_menu")
self.file_exists_dialog = self.builder.get_object("file_exists_dialog")
self.exists_file_label = self.builder.get_object("exists_file_label")
self.exists_file_field = self.builder.get_object("exists_file_field")
self.path_menu = self.builder.get_object("path_menu")
self.exists_file_rename_bttn = self.builder.get_object("exists_file_rename_bttn")
self.bottom_size_label = self.builder.get_object("bottom_size_label")
self.bottom_file_count_label = self.builder.get_object("bottom_file_count_label")
self.bottom_path_label = self.builder.get_object("bottom_path_label")
self.trash_files_path = GLib.get_user_data_dir() + "/Trash/files"
self.trash_info_path = GLib.get_user_data_dir() + "/Trash/info"
# In compress commands:
# %n: First selected filename/dir to archive
# %N: All selected filenames/dirs to archive, or (with %O) a single filename
# %o: Resulting single archive file
# %O: Resulting archive per source file/directory (use changes %N meaning)
#
# In extract commands:
# %x: Archive file to extract
# %g: Unique extraction target filename with optional subfolder
# %G: Unique extraction target filename, never with subfolder
#
# In list commands:
# %x: Archive to list
#
# Plus standard bash variables are accepted.
self.arc_commands = [ '$(which 7za || echo 7zr) a %o %N',
'zip -r %o %N',
'rar a -r %o %N',
'tar -cvf %o %N',
'tar -cvjf %o %N',
'tar -cvzf %o %N',
'tar -cvJf %o %N',
'gzip -c %N > %O',
'xz -cz %N > %O'
]
self.notebooks = [self.window1, self.window2, self.window3, self.window4]
self.selected_files = []
self.to_copy_files = []
self.to_cut_files = []
self.single_click_open = False
self.is_pane1_hidden = False
self.is_pane2_hidden = False
self.is_pane3_hidden = False
self.is_pane4_hidden = False
self.override_drop_dest = None
self.is_searching = False
self.search_iconview = None
self.search_view = None
self.skip_edit = False
self.cancel_edit = False
self.ctrlDown = False
self.shiftDown = False
self.altDown = False
self.success = "#88cc27"
self.warning = "#ffa800"
self.error = "#ff0000"
self.window.connect("delete-event", self.tear_down)
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.tear_down)

View File

@ -1,5 +1,5 @@
# Python imports
import os, threading, time
import threading, socket, time
from multiprocessing.connection import Listener, Client
# Lib imports
@ -15,32 +15,11 @@ def threaded(fn):
class IPCServer:
""" Create a listener so that other SolarFM instances send requests back to existing instance. """
def __init__(self, conn_type="socket"):
self.is_ipc_alive = False
self._conn_type = conn_type
self.ipc_authkey = b'solarfm-ipc'
self.ipc_timeout = 15.0
if conn_type == "socket":
self.ipc_address = '/tmp/solarfm-ipc.sock'
else:
self.ipc_address = '127.0.0.1'
self.ipc_port = 4848
class IPCServerMixin:
@threaded
def create_ipc_server(self):
if self._conn_type == "socket":
if os.path.exists(self.ipc_address):
return
listener = Listener(address=self.ipc_address, family="AF_UNIX", authkey=self.ipc_authkey)
else:
listener = Listener((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey)
listener = Listener((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey)
self.is_ipc_alive = True
while True:
conn = listener.accept()
@ -55,7 +34,7 @@ class IPCServer:
if "FILE|" in msg:
file = msg.split("FILE|")[1].strip()
if file:
event_system.push_gui_event([None, "handle_file_from_ipc", (file,)])
event_system.push_gui_event([None, "handle_file_from_ipc", file])
conn.close()
break
@ -68,7 +47,7 @@ class IPCServer:
conn.close()
break
# NOTE: Not perfect but insures we don't lock up the connection for too long.
# NOTE: Not perfect but insures we don't lockup the connection for too long.
end_time = time.time()
if (end - start) > self.ipc_timeout:
conn.close()
@ -78,12 +57,7 @@ class IPCServer:
def send_ipc_message(self, message="Empty Data..."):
try:
if self._conn_type == "socket":
conn = Client(address=self.ipc_address, family="AF_UNIX", authkey=self.ipc_authkey)
else:
conn = Client((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey)
conn = Client((self.ipc_address, self.ipc_port), authkey=self.ipc_authkey)
conn.send(message)
conn.send('close connection')
except Exception as e:

View File

@ -0,0 +1,7 @@
"""
Gtk Bound Signal Module
"""
from .mixins import *
from .IPCServerMixin import IPCServerMixin
from .Controller_Data import Controller_Data
from .Controller import Controller

View File

@ -4,14 +4,14 @@
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk, Gdk
from gi.repository import Gtk, Gdk, Gio
# Application imports
class ShowHideMixin:
def show_messages_popup(self, type, text, seconds=None):
self.message_popup_widget.popup()
self.message_widget.popup()
def stop_file_searching(self, widget=None, eve=None):
self.is_searching = False
@ -48,7 +48,7 @@ class ShowHideMixin:
def show_about_page(self, widget=None, eve=None):
about_page = self.builder.get_object("about_page")
response = about_page.run()
if response in [Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT]:
if (response == Gtk.ResponseType.CANCEL) or (response == Gtk.ResponseType.DELETE_EVENT):
self.hide_about_page()
def hide_about_page(self, widget=None, eve=None):
@ -56,11 +56,11 @@ class ShowHideMixin:
def show_archiver_dialogue(self, widget=None, eve=None):
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
wid, tid = self.window_controller.get_active_data()
view = self.get_fm_window(wid).get_view_by_id(tid)
archiver_dialogue = self.builder.get_object("archiver_dialogue")
archiver_dialogue.set_action(Gtk.FileChooserAction.SAVE)
archiver_dialogue.set_current_folder(tab.get_current_directory())
archiver_dialogue.set_current_folder(view.get_current_directory())
archiver_dialogue.set_current_name("arc.7z")
response = archiver_dialogue.run()
@ -96,12 +96,6 @@ class ShowHideMixin:
dialog.response(Gtk.ResponseType.OK)
def show_plugins_popup(self, widget=None, eve=None):
self.builder.get_object("plugin_list").popup()
def hide_plugins_popup(self, widget=None, eve=None):
self.builder.get_object("plugin_list").hide()
def show_context_menu(self, widget=None, eve=None):
self.builder.get_object("context_menu").run()
@ -137,7 +131,7 @@ class ShowHideMixin:
def hide_edit_file_menu_enter_key(self, widget=None, eve=None):
keyname = Gdk.keyval_name(eve.keyval).lower()
if keyname in ["return", "enter"]:
if "return" in keyname or "enter" in keyname:
self.builder.get_object("edit_file_menu").hide()
def hide_edit_file_menu_skip(self, widget=None, eve=None):

View File

@ -0,0 +1,11 @@
# Python imports
# Gtk imports
# Application imports
from . import ShowHideMixin
from .ui import *
class UIMixin(WidgetFileActionMixin, PaneMixin, WindowMixin, ShowHideMixin):
pass

View File

@ -0,0 +1,2 @@
from .ShowHideMixin import ShowHideMixin
from .UIMixin import UIMixin

View File

@ -39,6 +39,8 @@ class PaneMixin:
def toggle_notebook_pane(self, widget, eve=None):
name = widget.get_name()
pane_index = int(name[-1])
pane = None
master_pane = self.builder.get_object("pane_master")
pane = self.builder.get_object("pane_top") if pane_index in [1, 2] else self.builder.get_object("pane_bottom")
@ -48,12 +50,16 @@ class PaneMixin:
self._save_state(state, pane_index)
return
child = pane.get_child1() if pane_index in [1, 3] else pane.get_child2()
child = None
if pane_index in [1, 3]:
child = pane.get_child1()
elif pane_index in [2, 4]:
child = pane.get_child2()
self.toggle_pane(child)
self._save_state(state, pane_index)
def _save_state(self, state, pane_index):
window = self.fm_controller.get_window_by_index(pane_index - 1)
window.set_is_hidden(state)
self.fm_controller.save_state()
window = self.window_controller.get_window_by_index(pane_index - 1)
window.isHidden = state
self.window_controller.save_state()

View File

@ -0,0 +1,205 @@
# Python imports
import os
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk, Gdk
# Application imports
from . import WidgetMixin
class TabMixin(WidgetMixin):
"""docstring for TabMixin"""
def create_tab(self, wid, path=None):
notebook = self.builder.get_object(f"window_{wid}")
path_entry = self.builder.get_object(f"path_entry")
view = self.window_controller.add_view_for_window_by_nickname(f"window_{wid}")
view.logger = self.logger
view.set_wid(wid)
if path: view.set_path(path)
tab = self.create_tab_widget(view)
scroll, store = self.create_grid_iconview_widget(view, wid)
# scroll, store = self.create_grid_treeview_widget(view, wid)
index = notebook.append_page(scroll, tab)
self.window_controller.set_active_data(wid, view.get_tab_id())
path_entry.set_text(view.get_current_directory())
notebook.show_all()
notebook.set_current_page(index)
ctx = notebook.get_style_context()
ctx.add_class("notebook-unselected-focus")
notebook.set_tab_reorderable(scroll, True)
self.load_store(view, store)
self.set_window_title()
self.set_file_watcher(view)
def close_tab(self, button, eve=None):
notebook = button.get_parent().get_parent()
tid = self.get_tab_id_from_tab_box(button.get_parent())
wid = int(notebook.get_name()[-1])
scroll = self.builder.get_object(f"{wid}|{tid}")
page = notebook.page_num(scroll)
view = self.get_fm_window(wid).get_view_by_id(tid)
watcher = view.get_dir_watcher()
watcher.cancel()
self.get_fm_window(wid).delete_view_by_id(tid)
notebook.remove_page(page)
self.window_controller.save_state()
self.set_window_title()
def on_tab_reorder(self, child, page_num, new_index):
wid, tid = page_num.get_name().split("|")
window = self.get_fm_window(wid)
view = None
for i, view in enumerate(window.views):
if view.id == tid:
_view = window.get_view_by_id(tid)
watcher = _view.get_dir_watcher()
watcher.cancel()
window.views.insert(new_index, window.views.pop(i))
view = window.get_view_by_id(tid)
self.set_file_watcher(view)
self.window_controller.save_state()
def on_tab_switch_update(self, notebook, content=None, index=None):
self.selected_files.clear()
wid, tid = content.get_children()[0].get_name().split("|")
self.window_controller.set_active_data(wid, tid)
self.set_path_text(wid, tid)
self.set_window_title()
def get_tab_id_from_tab_box(self, tab_box):
tid = tab_box.get_children()[2]
return tid.get_text()
def get_tab_label(self, notebook, iconview):
return notebook.get_tab_label(iconview.get_parent()).get_children()[0]
def get_tab_close(self, notebook, iconview):
return notebook.get_tab_label(iconview.get_parent()).get_children()[1]
def get_tab_iconview_from_notebook(self, notebook):
return notebook.get_children()[1].get_children()[0]
def refresh_tab(data=None):
wid, tid, view, iconview, store = self.get_current_state()
view.load_directory()
self.load_store(view, store)
def update_view(self, tab_label, view, store, wid, tid):
self.load_store(view, store)
self.set_path_text(wid, tid)
char_width = len(view.get_end_of_path())
tab_label.set_width_chars(char_width)
tab_label.set_label(view.get_end_of_path())
self.set_window_title()
self.set_file_watcher(view)
self.window_controller.save_state()
def do_action_from_bar_controls(self, widget, eve=None):
action = widget.get_name()
wid, tid = self.window_controller.get_active_data()
notebook = self.builder.get_object(f"window_{wid}")
store, tab_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
view = self.get_fm_window(wid).get_view_by_id(tid)
if action == "go_up":
view.pop_from_path()
if action == "go_home":
view.set_to_home()
if action == "refresh_view":
view.load_directory()
if action == "create_tab":
dir = view.get_current_directory()
self.create_tab(wid, dir)
self.window_controller.save_state()
return
if action == "path_entry":
focused_obj = self.window.get_focus()
dir = f"{view.get_current_directory()}/"
path = widget.get_text()
if isinstance(focused_obj, Gtk.Entry):
button_box = self.path_menu.get_children()[0].get_children()[0].get_children()[0]
query = widget.get_text().replace(dir, "")
files = view.files + view.hidden
self.clear_children(button_box)
show_path_menu = False
for file in files:
if os.path.isdir(f"{dir}{file}"):
if query.lower() in file.lower():
button = Gtk.Button(label=file)
button.show()
button.connect("clicked", self.set_path_entry)
button_box.add(button)
show_path_menu = True
if not show_path_menu:
self.path_menu.popdown()
else:
self.path_menu.popup()
widget.grab_focus_without_selecting()
widget.set_position(-1)
if path.endswith(".") or path == dir:
return
traversed = view.set_path(path)
if not traversed:
return
self.update_view(tab_label, view, store, wid, tid)
try:
widget.grab_focus_without_selecting()
widget.set_position(-1)
except Exception as e:
pass
def set_path_entry(self, button=None, eve=None):
wid, tid, view, iconview, store = self.get_current_state()
path = f"{view.get_current_directory()}/{button.get_label()}"
path_entry = self.builder.get_object("path_entry")
path_entry.set_text(path)
path_entry.grab_focus_without_selecting()
path_entry.set_position(-1)
self.path_menu.popdown()
def keyboard_close_tab(self):
wid, tid = self.window_controller.get_active_data()
notebook = self.builder.get_object(f"window_{wid}")
scroll = self.builder.get_object(f"{wid}|{tid}")
page = notebook.page_num(scroll)
view = self.get_fm_window(wid).get_view_by_id(tid)
watcher = view.get_dir_watcher()
watcher.cancel()
self.get_fm_window(wid).delete_view_by_id(tid)
notebook.remove_page(page)
self.window_controller.save_state()
self.set_window_title()
# File control events
def show_hide_hidden_files(self):
wid, tid = self.window_controller.get_active_data()
view = self.get_fm_window(wid).get_view_by_id(tid)
view.hide_hidden = not view.hide_hidden
view.load_directory()
self.builder.get_object("refresh_view").released()

View File

@ -1,5 +1,5 @@
# Python imports
import os, time, threading
import os
# Lib imports
import gi
@ -9,15 +9,9 @@ from gi.repository import Gtk, GObject, GLib, Gio
# Application imports
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class WidgetFileActionMixin:
"""docstring for WidgetFileActionMixin"""
def sizeof_fmt(self, num, suffix="B"):
for unit in ["", "K", "M", "G", "T", "Pi", "Ei", "Zi"]:
if abs(num) < 1024.0:
@ -40,73 +34,47 @@ class WidgetFileActionMixin:
return size
def set_file_watcher(self, tab):
if tab.get_dir_watcher():
watcher = tab.get_dir_watcher()
def set_file_watcher(self, view):
if view.get_dir_watcher():
watcher = view.get_dir_watcher()
watcher.cancel()
if debug:
print(f"Watcher Is Cancelled: {watcher.is_cancelled()}")
cur_dir = tab.get_current_directory()
cur_dir = view.get_current_directory()
# Temp updating too much with current events we are checking for.
# Seems to cause invalid iter errors in WidbetMixin > update_store
if cur_dir == "/tmp":
watcher = None
return
dir_watcher = Gio.File.new_for_path(cur_dir) \
.monitor_directory(Gio.FileMonitorFlags.WATCH_MOVES, Gio.Cancellable())
wid = tab.get_wid()
tid = tab.get_id()
wid = view.get_wid()
tid = view.get_tab_id()
dir_watcher.connect("changed", self.dir_watch_updates, (f"{wid}|{tid}",))
tab.set_dir_watcher(dir_watcher)
view.set_dir_watcher(dir_watcher)
# NOTE: Too lazy to impliment a proper update handler and so just regen store and update tab.
# Use a lock system to prevent too many update calls for certain instances but user can manually refresh if they have urgency
def dir_watch_updates(self, file_monitor, file, other_file=None, eve_type=None, data=None):
if eve_type in [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED,
Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN,
Gio.FileMonitorEvent.MOVED_OUT]:
Gio.FileMonitorEvent.MOVED_OUT]:
if debug:
print(eve_type)
if eve_type in [Gio.FileMonitorEvent.MOVED_IN, Gio.FileMonitorEvent.MOVED_OUT]:
self.update_on_soft_lock_end(data[0])
elif data[0] in self.soft_update_lock.keys():
self.soft_update_lock[data[0]]["last_update_time"] = time.time()
else:
self.soft_lock_countdown(data[0])
wid, tid = data[0].split("|")
notebook = self.builder.get_object(f"window_{wid}")
view = self.get_fm_window(wid).get_view_by_id(tid)
iconview = self.builder.get_object(f"{wid}|{tid}|iconview")
store = iconview.get_model()
_store, tab_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
@threaded
def soft_lock_countdown(self, tab_widget):
self.soft_update_lock[tab_widget] = { "last_update_time": time.time()}
view.load_directory()
self.load_store(view, store)
tab_label.set_label(view.get_end_of_path())
self.set_bottom_labels(view)
lock = True
while lock:
time.sleep(0.6)
last_update_time = self.soft_update_lock[tab_widget]["last_update_time"]
current_time = time.time()
if (current_time - last_update_time) > 0.6:
lock = False
self.soft_update_lock.pop(tab_widget, None)
GLib.idle_add(self.update_on_soft_lock_end, *(tab_widget,))
def update_on_soft_lock_end(self, tab_widget):
wid, tid = tab_widget.split("|")
notebook = self.builder.get_object(f"window_{wid}")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
icon_grid = self.builder.get_object(f"{wid}|{tid}|icon_grid")
store = icon_grid.get_model()
_store, tab_widget_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
tab.load_directory()
self.load_store(tab, store)
tab_widget_label.set_label(tab.get_end_of_path())
_wid, _tid, _tab, _icon_grid, _store = self.get_current_state()
if [wid, tid] in [_wid, _tid]:
self.set_bottom_labels(tab)
def popup_search_files(self, wid, keyname):
@ -118,43 +86,43 @@ class WidgetFileActionMixin:
def do_file_search(self, widget, eve=None):
query = widget.get_text()
self.search_icon_grid.unselect_all()
for i, file in enumerate(self.search_tab.get_files()):
if query and query in file[0].lower():
self.search_iconview.unselect_all()
for i, file in enumerate(self.search_view.files):
if query and query in file.lower():
path = Gtk.TreePath().new_from_indices([i])
self.search_icon_grid.select_path(path)
self.search_iconview.select_path(path)
items = self.search_icon_grid.get_selected_items()
items = self.search_iconview.get_selected_items()
if len(items) == 1:
self.search_icon_grid.scroll_to_path(items[0], True, 0.5, 0.5)
self.search_iconview.scroll_to_path(items[0], True, 0.5, 0.5)
def open_files(self):
wid, tid, tab, icon_grid, store = self.get_current_state()
wid, tid, view, iconview, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
for file in uris:
tab.open_file_locally(file)
view.open_file_locally(file)
def open_with_files(self, appchooser_widget):
wid, tid, tab, icon_grid, store = self.get_current_state()
wid, tid, view, iconview, store = self.get_current_state()
app_info = appchooser_widget.get_app_info()
uris = self.format_to_uris(store, wid, tid, self.selected_files)
tab.app_chooser_exec(app_info, uris)
view.app_chooser_exec(app_info, uris)
def execute_files(self, in_terminal=False):
wid, tid, tab, icon_grid, store = self.get_current_state()
wid, tid, view, iconview, store = self.get_current_state()
paths = self.format_to_uris(store, wid, tid, self.selected_files, True)
current_dir = tab.get_current_directory()
current_dir = view.get_current_directory()
command = None
for path in paths:
command = f"exec '{path}'" if not in_terminal else f"{tab.terminal_app} -e '{path}'"
tab.execute(command, start_dir=tab.get_current_directory(), use_os_system=False)
command = f"exec '{path}'" if not in_terminal else f"{view.terminal_app} -e '{path}'"
view.execute(command, start_dir=view.get_current_directory(), use_os_system=False)
def archive_files(self, archiver_dialogue):
wid, tid, tab, icon_grid, store = self.get_current_state()
wid, tid, view, iconview, store = self.get_current_state()
paths = self.format_to_uris(store, wid, tid, self.selected_files, True)
save_target = archiver_dialogue.get_filename();
@ -162,14 +130,14 @@ class WidgetFileActionMixin:
pre_command = self.arc_command_buffer.get_text(sItr, eItr, False)
pre_command = pre_command.replace("%o", save_target)
pre_command = pre_command.replace("%N", ' '.join(paths))
command = f"{tab.terminal_app} -e '{pre_command}'"
command = f"{view.terminal_app} -e '{pre_command}'"
tab.execute(command, start_dir=None, use_os_system=True)
view.execute(command, start_dir=None, use_os_system=True)
def rename_files(self):
rename_label = self.builder.get_object("file_to_rename_label")
rename_input = self.builder.get_object("new_rename_fname")
wid, tid, tab, icon_grid, store = self.get_current_state()
wid, tid, view, iconview, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
for uri in uris:
@ -186,7 +154,7 @@ class WidgetFileActionMixin:
break
rname_to = rename_input.get_text().strip()
target = f"{tab.get_current_directory()}/{rname_to}"
target = f"{view.get_current_directory()}/{rname_to}"
self.handle_files([uri], "rename", target)
@ -196,27 +164,27 @@ class WidgetFileActionMixin:
self.selected_files.clear()
def cut_files(self):
wid, tid, tab, icon_grid, store = self.get_current_state()
wid, tid, view, iconview, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
self.to_cut_files = uris
def copy_files(self):
wid, tid, tab, icon_grid, store = self.get_current_state()
wid, tid, view, iconview, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
self.to_copy_files = uris
def paste_files(self):
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
target = f"{tab.get_current_directory()}"
wid, tid = self.window_controller.get_active_data()
view = self.get_fm_window(wid).get_view_by_id(tid)
target = f"{view.get_current_directory()}"
if self.to_copy_files:
if len(self.to_copy_files) > 0:
self.handle_files(self.to_copy_files, "copy", target)
elif self.to_cut_files:
elif len(self.to_cut_files) > 0:
self.handle_files(self.to_cut_files, "move", target)
def delete_files(self):
wid, tid, tab, icon_grid, store = self.get_current_state()
wid, tid, view, iconview, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
response = None
@ -231,7 +199,7 @@ class WidgetFileActionMixin:
type = file.query_file_type(flags=Gio.FileQueryInfoFlags.NONE)
if type == Gio.FileType.DIRECTORY:
tab.delete_file( file.get_path() )
view.delete_file( file.get_path() )
else:
file.delete(cancellable=None)
else:
@ -239,13 +207,13 @@ class WidgetFileActionMixin:
def trash_files(self):
wid, tid, tab, icon_grid, store = self.get_current_state()
wid, tid, view, iconview, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
for uri in uris:
self.trashman.trash(uri, False)
def restore_trash_files(self):
wid, tid, tab, icon_grid, store = self.get_current_state()
wid, tid, view, iconview, store = self.get_current_state()
uris = self.format_to_uris(store, wid, tid, self.selected_files, True)
for uri in uris:
self.trashman.restore(filename=uri.split("/")[-1], verbose=False)
@ -259,9 +227,9 @@ class WidgetFileActionMixin:
file_name = fname_field.get_text().strip()
type = self.builder.get_object("context_menu_type_toggle").get_state()
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
target = f"{tab.get_current_directory()}"
wid, tid = self.window_controller.get_active_data()
view = self.get_fm_window(wid).get_view_by_id(tid)
target = f"{view.get_current_directory()}"
if file_name:
path = f"{target}/{file_name}"
@ -326,9 +294,9 @@ class WidgetFileActionMixin:
type = _file.query_file_type(flags=Gio.FileQueryInfoFlags.NONE)
if type == Gio.FileType.DIRECTORY:
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
tab.delete_file( _file.get_path() )
wid, tid = self.window_controller.get_active_data()
view = self.get_fm_window(wid).get_view_by_id(tid)
view.delete_file( _file.get_path() )
else:
_file.delete(cancellable=None)
@ -353,16 +321,16 @@ class WidgetFileActionMixin:
type = file.query_file_type(flags=Gio.FileQueryInfoFlags.NONE)
if type == Gio.FileType.DIRECTORY:
wid, tid = self.fm_controller.get_active_wid_and_tid()
tab = self.get_fm_window(wid).get_tab_by_id(tid)
wid, tid = self.window_controller.get_active_data()
view = self.get_fm_window(wid).get_view_by_id(tid)
fPath = file.get_path()
tPath = target.get_path()
state = True
if action == "copy":
tab.copy_file(fPath, tPath)
view.copy_file(fPath, tPath)
if action == "move" or action == "rename":
tab.move_file(fPath, tPath)
view.move_file(fPath, tPath)
else:
if action == "copy":
file.copy(target, flags=Gio.FileCopyFlags.BACKUP, cancellable=None)

View File

@ -0,0 +1,221 @@
# Python imports
import os, threading, subprocess, time
# Lib imports
import gi
gi.require_version("Gtk", "3.0")
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk, Gdk, GLib, Gio, GdkPixbuf
# Application imports
def threaded(fn):
def wrapper(*args, **kwargs):
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
return wrapper
class WidgetMixin:
def load_store(self, view, store, save_state=False):
store.clear()
dir = view.get_current_directory()
files = view.get_files()
for i, file in enumerate(files):
store.append([None, file[0]])
self.create_icon(i, view, store, dir, file[0])
# NOTE: Not likely called often from here but it could be useful
if save_state:
self.window_controller.save_state()
@threaded
def create_icon(self, i, view, store, dir, file):
icon = view.create_icon(dir, file)
fpath = f"{dir}/{file}"
GLib.idle_add(self.update_store, (i, store, icon, view, fpath,))
# NOTE: Might need to keep an eye on this throwing invalid iters when too
# many updates are happening to a folder. Example: /tmp
def update_store(self, item):
i, store, icon, view, fpath = item
itr = None
try:
itr = store.get_iter(i)
except Exception as e:
try:
time.sleep(0.2)
itr = store.get_iter(i)
except Exception as e:
print(":Invalid Itr detected: (Potential race condition...)")
print(f"Index Requested: {i}")
print(f"Store Size: {len(store)}")
return
if not icon:
icon = self.get_system_thumbnail(fpath, view.SYS_ICON_WH[0])
if not icon:
if fpath.endswith(".gif"):
icon = GdkPixbuf.PixbufAnimation.get_static_image(fpath)
else:
icon = GdkPixbuf.Pixbuf.new_from_file(view.DEFAULT_ICON)
store.set_value(itr, 0, icon)
def get_system_thumbnail(self, filename, size):
try:
if os.path.exists(filename):
gioFile = Gio.File.new_for_path(filename)
info = gioFile.query_info('standard::icon' , 0, Gio.Cancellable())
icon = info.get_icon().get_names()[0]
iconTheme = Gtk.IconTheme.get_default()
iconData = iconTheme.lookup_icon(icon , size , 0)
if iconData:
iconPath = iconData.get_filename()
return GdkPixbuf.Pixbuf.new_from_file(iconPath)
else:
return None
else:
return None
except Exception as e:
print("System icon generation issue:")
return None
def create_tab_widget(self, view):
tab = Gtk.ButtonBox()
label = Gtk.Label()
tid = Gtk.Label()
close = Gtk.Button()
icon = Gtk.Image(stock=Gtk.STOCK_CLOSE)
label.set_label(f"{view.get_end_of_path()}")
label.set_width_chars(len(view.get_end_of_path()))
label.set_xalign(0.0)
tid.set_label(f"{view.id}")
close.add(icon)
tab.add(label)
tab.add(close)
tab.add(tid)
close.connect("released", self.close_tab)
tab.show_all()
tid.hide()
return tab
def create_grid_iconview_widget(self, view, wid):
scroll = Gtk.ScrolledWindow()
grid = Gtk.IconView()
store = Gtk.ListStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str)
grid.set_model(store)
grid.set_pixbuf_column(0)
grid.set_text_column(1)
grid.set_item_orientation(1)
grid.set_selection_mode(3)
grid.set_item_width(96)
grid.set_item_padding(8)
grid.set_margin(12)
grid.set_row_spacing(18)
grid.set_columns(-1)
grid.set_spacing(12)
grid.set_column_spacing(18)
grid.connect("button_release_event", self.grid_icon_single_click)
grid.connect("item-activated", self.grid_icon_double_click)
# grid.connect("toggle-cursor-item", self.grid_cursor_toggled)
# grid.connect("notify", self.grid_cursor_toggled)
grid.connect("selection-changed", self.grid_set_selected_items)
grid.connect("drag-data-get", self.grid_on_drag_set)
grid.connect("drag-data-received", self.grid_on_drag_data_received)
grid.connect("drag-motion", self.grid_on_drag_motion)
URI_TARGET_TYPE = 80
uri_target = Gtk.TargetEntry.new('text/uri-list', Gtk.TargetFlags(0), URI_TARGET_TYPE)
targets = [ uri_target ]
action = Gdk.DragAction.COPY
grid.enable_model_drag_dest(targets, action)
grid.enable_model_drag_source(0, targets, action)
grid.show_all()
scroll.add(grid)
grid.set_name(f"{wid}|{view.id}")
scroll.set_name(f"{wid}|{view.id}")
self.builder.expose_object(f"{wid}|{view.id}|iconview", grid)
self.builder.expose_object(f"{wid}|{view.id}", scroll)
return scroll, store
def create_grid_treeview_widget(self, view, wid):
scroll = Gtk.ScrolledWindow()
grid = Gtk.TreeView()
store = Gtk.ListStore(GdkPixbuf.Pixbuf or GdkPixbuf.PixbufAnimation or None, str)
# store = Gtk.TreeStore(GdkPixbuf.Pixbuf or None, str)
column = Gtk.TreeViewColumn("Icons")
icon = Gtk.CellRendererPixbuf()
name = Gtk.CellRendererText()
selec = grid.get_selection()
grid.set_model(store)
selec.set_mode(3)
column.pack_start(icon, False)
column.pack_start(name, True)
column.add_attribute(icon, "pixbuf", 0)
column.add_attribute(name, "text", 1)
column.set_expand(False)
column.set_sizing(2)
column.set_min_width(120)
column.set_max_width(74)
grid.append_column(column)
grid.set_search_column(1)
grid.set_rubber_banding(True)
grid.set_headers_visible(False)
grid.set_enable_tree_lines(False)
grid.connect("button_release_event", self.grid_icon_single_click)
grid.connect("row-activated", self.grid_icon_double_click)
grid.connect("drag-data-get", self.grid_on_drag_set)
grid.connect("drag-data-received", self.grid_on_drag_data_received)
grid.connect("drag-motion", self.grid_on_drag_motion)
URI_TARGET_TYPE = 80
uri_target = Gtk.TargetEntry.new('text/uri-list', Gtk.TargetFlags(0), URI_TARGET_TYPE)
targets = [ uri_target ]
action = Gdk.DragAction.COPY
grid.enable_model_drag_dest(targets, action)
grid.enable_model_drag_source(0, targets, action)
grid.show_all()
scroll.add(grid)
grid.set_name(f"{wid}|{view.id}")
scroll.set_name(f"{wid}|{view.id}")
grid.columns_autosize()
self.builder.expose_object(f"{wid}|{view.id}", scroll)
return scroll, store
def get_store_and_label_from_notebook(self, notebook, _name):
icon_view = None
tab_label = None
store = None
for obj in notebook.get_children():
icon_view = obj.get_children()[0]
name = icon_view.get_name()
if name == _name:
store = icon_view.get_model()
tab_label = notebook.get_tab_label(obj).get_children()[0]
return store, tab_label

View File

@ -1,7 +1,7 @@
# Python imports
import copy
import traceback
from os.path import isdir
from os.path import isdir, isfile
# Lib imports
import gi
@ -9,59 +9,58 @@ gi.require_version('Gdk', '3.0')
from gi.repository import Gdk, Gio
# Application imports
from .tab_mixin import TabMixin
from . import TabMixin, WidgetMixin
class WindowMixin(TabMixin):
"""docstring for WindowMixin"""
def generate_windows(self, session_json = None):
if session_json:
for j, value in enumerate(session_json):
def generate_windows(self, data = None):
if data:
for j, value in enumerate(data):
i = j + 1
notebook_tggl_button = self.builder.get_object(f"tggl_notebook_{i}")
is_hidden = True if value[0]["window"]["isHidden"] == "True" else False
tabs = value[0]["window"]["tabs"]
self.fm_controller.create_window()
notebook_tggl_button.set_active(True)
isHidden = True if value[0]["window"]["isHidden"] == "True" else False
object = self.builder.get_object(f"tggl_notebook_{i}")
views = value[0]["window"]["views"]
self.window_controller.create_window()
object.set_active(True)
if tabs:
for tab in tabs:
self.create_new_tab_notebook(None, i, tab)
else:
self.create_new_tab_notebook(None, i, None)
for view in views:
self.create_new_view_notebook(None, i, view)
if is_hidden:
self.toggle_notebook_pane(notebook_tggl_button)
if isHidden:
self.toggle_notebook_pane(object)
try:
if not self.is_pane4_hidden:
icon_grid = self.window4.get_children()[-1].get_children()[0]
icon_view = self.window4.get_children()[1].get_children()[0]
icon_view.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE))
elif not self.is_pane3_hidden:
icon_grid = self.window3.get_children()[-1].get_children()[0]
icon_view = self.window3.get_children()[1].get_children()[0]
icon_view.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE))
elif not self.is_pane2_hidden:
icon_grid = self.window2.get_children()[-1].get_children()[0]
icon_view = self.window2.get_children()[1].get_children()[0]
icon_view.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE))
elif not self.is_pane1_hidden:
icon_grid = self.window1.get_children()[-1].get_children()[0]
icon_grid.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE))
icon_grid.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE))
icon_view = self.window1.get_children()[1].get_children()[0]
icon_view.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE))
except Exception as e:
print("\n: The saved session might be missing window data! :\nLocation: ~/.config/solarfm/session.json\nFix: Back it up and delete it to reset.\n")
print(repr(e))
else:
for j in range(0, 4):
i = j + 1
self.fm_controller.create_window()
self.create_new_tab_notebook(None, i, None)
self.window_controller.create_window()
self.create_new_view_notebook(None, i, None)
def get_fm_window(self, wid):
return self.fm_controller.get_window_by_nickname(f"window_{wid}")
return self.window_controller.get_window_by_nickname(f"window_{wid}")
def format_to_uris(self, store, wid, tid, treePaths, use_just_path=False):
tab = self.get_fm_window(wid).get_tab_by_id(tid)
dir = tab.get_current_directory()
view = self.get_fm_window(wid).get_view_by_id(tid)
dir = view.get_current_directory()
uris = []
for path in treePaths:
@ -79,10 +78,10 @@ class WindowMixin(TabMixin):
return uris
def set_bottom_labels(self, tab):
state = self.get_current_state()
selected_files = state.icon_grid.get_selected_items()
current_directory = tab.get_current_directory()
def set_bottom_labels(self, view):
_wid, _tid, _view, iconview, store = self.get_current_state()
selected_files = iconview.get_selected_items()
current_directory = view.get_current_directory()
path_file = Gio.File.new_for_path(current_directory)
mount_file = path_file.query_filesystem_info(attributes="filesystem::*", cancellable=None)
formatted_mount_free = self.sizeof_fmt( int(mount_file.get_attribute_as_string("filesystem::free")) )
@ -97,9 +96,9 @@ class WindowMixin(TabMixin):
# If something selected
self.bottom_size_label.set_label(f"{formatted_mount_free} free / {formatted_mount_size}")
self.bottom_path_label.set_label(tab.get_current_directory())
if selected_files:
uris = self.format_to_uris(state.store, state.wid, state.tid, selected_files, True)
self.bottom_path_label.set_label(view.get_current_directory())
if len(selected_files) > 0:
uris = self.format_to_uris(store, _wid, _tid, selected_files, True)
combined_size = 0
for uri in uris:
try:
@ -114,29 +113,29 @@ class WindowMixin(TabMixin):
formatted_size = self.sizeof_fmt(combined_size)
if tab.is_hiding_hidden():
self.bottom_path_label.set_label(f" {len(uris)} / {tab.get_files_count()} ({formatted_size})")
if view.hide_hidden:
self.bottom_path_label.set_label(f" {len(uris)} / {view.get_files_count()} ({formatted_size})")
else:
self.bottom_path_label.set_label(f" {len(uris)} / {tab.get_not_hidden_count()} ({formatted_size})")
self.bottom_path_label.set_label(f" {len(uris)} / {view.get_not_hidden_count()} ({formatted_size})")
return
# If nothing selected
if tab.is_hiding_hidden():
if tab.get_hidden_count() > 0:
self.bottom_file_count_label.set_label(f"{tab.get_not_hidden_count()} visible ({tab.get_hidden_count()} hidden)")
if view.hide_hidden:
if view.get_hidden_count() > 0:
self.bottom_file_count_label.set_label(f"{view.get_not_hidden_count()} visible ({view.get_hidden_count()} hidden)")
else:
self.bottom_file_count_label.set_label(f"{tab.get_files_count()} items")
self.bottom_file_count_label.set_label(f"{view.get_files_count()} items")
else:
self.bottom_file_count_label.set_label(f"{tab.get_files_count()} items")
self.bottom_file_count_label.set_label(f"{view.get_files_count()} items")
def set_window_title(self):
wid, tid = self.fm_controller.get_active_wid_and_tid()
wid, tid = self.window_controller.get_active_data()
notebook = self.builder.get_object(f"window_{wid}")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
dir = tab.get_current_directory()
view = self.get_fm_window(wid).get_view_by_id(tid)
dir = view.get_current_directory()
for _notebook in self.notebooks:
ctx = _notebook.get_style_context()
@ -147,75 +146,72 @@ class WindowMixin(TabMixin):
ctx.remove_class("notebook-unselected-focus")
ctx.add_class("notebook-selected-focus")
self.window.set_title(f"SolarFM ~ {dir}")
self.set_bottom_labels(tab)
self.window.set_title("SolarFM ~ " + dir)
self.set_bottom_labels(view)
def set_path_text(self, wid, tid):
path_entry = self.builder.get_object("path_entry")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
path_entry.set_text(tab.get_current_directory())
view = self.get_fm_window(wid).get_view_by_id(tid)
path_entry.set_text(view.get_current_directory())
def grid_set_selected_items(self, icons_grid):
self.selected_files = icons_grid.get_selected_items()
def grid_set_selected_items(self, iconview):
self.selected_files = iconview.get_selected_items()
def grid_cursor_toggled(self, icons_grid):
def grid_cursor_toggled(self, iconview):
print("wat...")
def grid_icon_single_click(self, icons_grid, eve):
def grid_icon_single_click(self, iconview, eve):
try:
self.path_menu.popdown()
wid, tid = icons_grid.get_name().split("|")
self.fm_controller.set_wid_and_tid(wid, tid)
wid, tid = iconview.get_name().split("|")
self.window_controller.set_active_data(wid, tid)
self.set_path_text(wid, tid)
self.set_window_title()
if eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 1: # l-click
if self.single_click_open: # FIXME: need to find a way to pass the model index
self.grid_icon_double_click(icons_grid)
self.grid_icon_double_click(iconview)
elif eve.type == Gdk.EventType.BUTTON_RELEASE and eve.button == 3: # r-click
self.show_context_menu()
except Exception as e:
print(repr(e))
self.display_message(self.error_color, f"{repr(e)}")
self.display_message(self.error, f"{repr(e)}")
def grid_icon_double_click(self, icons_grid, item, data=None):
def grid_icon_double_click(self, iconview, item, data=None):
try:
if self.ctrl_down and self.shift_down:
self.unset_keys_and_data()
if self.ctrlDown and self.shiftDown:
self.execute_files(in_terminal=True)
return
elif self.ctrl_down:
self.unset_keys_and_data()
elif self.ctrlDown:
self.execute_files()
return
state = self.get_current_state()
notebook = self.builder.get_object(f"window_{state.wid}")
tab_label = self.get_tab_label(notebook, icons_grid)
wid, tid, view, _iconview, store = self.get_current_state()
notebook = self.builder.get_object(f"window_{wid}")
tab_label = self.get_tab_label(notebook, iconview)
fileName = state.store[item][1]
dir = state.tab.get_current_directory()
fileName = store[item][1]
dir = view.get_current_directory()
file = f"{dir}/{fileName}"
if isdir(file):
state.tab.set_path(file)
self.update_tab(tab_label, state.tab, state.store, state.wid, state.tid)
view.set_path(file)
self.update_view(tab_label, view, store, wid, tid)
else:
self.open_files()
except Exception as e:
traceback.print_exc()
self.display_message(self.error_color, f"{repr(e)}")
self.display_message(self.error, f"{repr(e)}")
def grid_on_drag_set(self, icons_grid, drag_context, data, info, time):
action = icons_grid.get_name()
def grid_on_drag_set(self, iconview, drag_context, data, info, time):
action = iconview.get_name()
wid, tid = action.split("|")
store = icons_grid.get_model()
treePaths = icons_grid.get_selected_items()
store = iconview.get_model()
treePaths = iconview.get_selected_items()
# NOTE: Need URIs as URI format for DnD to work. Will strip 'file://'
# further down call chain when doing internal fm stuff.
uris = self.format_to_uris(store, wid, tid, treePaths)
@ -224,30 +220,30 @@ class WindowMixin(TabMixin):
data.set_uris(uris)
data.set_text(uris_text, -1)
def grid_on_drag_motion(self, icons_grid, drag_context, x, y, data):
current = '|'.join(self.fm_controller.get_active_wid_and_tid())
target = icons_grid.get_name()
def grid_on_drag_motion(self, iconview, drag_context, x, y, data):
current = '|'.join(self.window_controller.get_active_data())
target = iconview.get_name()
wid, tid = target.split("|")
store = icons_grid.get_model()
treePath = icons_grid.get_drag_dest_item().path
store = iconview.get_model()
treePath = iconview.get_drag_dest_item().path
if treePath:
uri = self.format_to_uris(store, wid, tid, treePath)[0].replace("file://", "")
self.override_drop_dest = uri if isdir(uri) else None
if target not in current:
self.fm_controller.set_wid_and_tid(wid, tid)
self.window_controller.set_active_data(wid, tid)
def grid_on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
if info == 80:
wid, tid = self.fm_controller.get_active_wid_and_tid()
wid, tid = self.window_controller.get_active_data()
notebook = self.builder.get_object(f"window_{wid}")
store, tab_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}")
tab = self.get_fm_window(wid).get_tab_by_id(tid)
view = self.get_fm_window(wid).get_view_by_id(tid)
uris = data.get_uris()
dest = f"{tab.get_current_directory()}" if not self.override_drop_dest else self.override_drop_dest
dest = f"{view.get_current_directory()}" if not self.override_drop_dest else self.override_drop_dest
if len(uris) == 0:
uris = data.get_text().split("\n")
@ -256,5 +252,5 @@ class WindowMixin(TabMixin):
self.move_files(uris, dest)
def create_new_tab_notebook(self, widget=None, wid=None, path=None):
self.create_tab(wid, None, path)
def create_new_view_notebook(self, widget=None, wid=None, path=None):
self.create_tab(wid, path)

Some files were not shown because too many files have changed in this diff Show More