1050 lines
40 KiB
Python
1050 lines
40 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright (C) 2018 Andy Stewart
|
|
#
|
|
# Author: Andy Stewart <lazycat.manatee@gmail.com>
|
|
# Maintainer: Andy Stewart <lazycat.manatee@gmail.com>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from PyQt5.QtCore import QUrl, QThread, QMimeDatabase, QFileSystemWatcher
|
|
from PyQt5.QtGui import QColor, QIcon
|
|
from PyQt5 import QtCore, QtWidgets
|
|
from core.webengine import BrowserBuffer
|
|
from pathlib import Path
|
|
from functools import cmp_to_key
|
|
from core.utils import eval_in_emacs, PostGui, get_emacs_vars, interactive, message_to_emacs, get_emacs_func_result
|
|
import codecs
|
|
import os
|
|
import json
|
|
import shutil
|
|
import time
|
|
import copy
|
|
import subprocess
|
|
|
|
def get_fd_command():
|
|
if shutil.which("fd"):
|
|
return "fd"
|
|
elif shutil.which("fdfind"):
|
|
return "fdfind"
|
|
else:
|
|
return ""
|
|
|
|
class AppBuffer(BrowserBuffer):
|
|
def __init__(self, buffer_id, url, arguments):
|
|
BrowserBuffer.__init__(self, buffer_id, url, arguments, False)
|
|
|
|
self.arguments = arguments
|
|
|
|
self.vue_files = []
|
|
self.vue_current_index = 0
|
|
|
|
self.search_regex = ""
|
|
self.search_start_index = 0
|
|
|
|
self.load_index_html(__file__)
|
|
|
|
self.show_hidden_file = None
|
|
self.show_preview = None
|
|
self.show_icon = None
|
|
self.hide_preview_by_width = False
|
|
|
|
self.new_select_file = None
|
|
self.inhibit_mark_change_file = False
|
|
|
|
self.search_files = []
|
|
self.search_files_index = 0
|
|
|
|
self.file_changed_wacher = QFileSystemWatcher()
|
|
self.file_changed_wacher.directoryChanged.connect(lambda path: self.refresh())
|
|
|
|
self.mime_db = QMimeDatabase()
|
|
self.icon_cache_dir = os.path.join(os.path.dirname(__file__,), "src", "assets", "icon_cache")
|
|
if not os.path.exists(self.icon_cache_dir):
|
|
os.makedirs(self.icon_cache_dir)
|
|
|
|
self.preview_file = None
|
|
self.fetch_preview_info_threads = []
|
|
self.search_file_threads = []
|
|
self.fetch_git_log_threads = []
|
|
|
|
def monitor_current_dir(self):
|
|
if len(self.file_changed_wacher.directories()) > 0:
|
|
self.file_changed_wacher.removePaths(self.file_changed_wacher.directories())
|
|
self.file_changed_wacher.addPath(self.url)
|
|
|
|
def init_app(self):
|
|
self.init_vars()
|
|
|
|
if self.arguments != "":
|
|
if self.arguments.startswith("search:"):
|
|
self.search_regex = self.arguments.split("search:")[1]
|
|
if self.search_regex != "":
|
|
self.search_directory(self.url, self.search_regex)
|
|
else:
|
|
self.change_directory(self.url, "")
|
|
elif self.arguments.startswith("jump:"):
|
|
jump_file = self.arguments.split("jump:")[1]
|
|
self.change_directory(self.url, jump_file)
|
|
else:
|
|
self.change_directory(self.url, "")
|
|
|
|
def init_first_file_preview(self):
|
|
if self.file_infos == []:
|
|
self.update_preview("")
|
|
else:
|
|
self.update_preview(self.file_infos[self.select_index]["path"])
|
|
|
|
def init_vars(self):
|
|
(directory_color, symlink_color, header_color, mark_color, search_match_color, search_keyword_color) = get_emacs_func_result(
|
|
"get-emacs-face-foregrounds",
|
|
["font-lock-builtin-face",
|
|
"font-lock-keyword-face",
|
|
"font-lock-function-name-face",
|
|
"error",
|
|
"font-lock-string-face",
|
|
"warning"])
|
|
|
|
(self.show_hidden_file, self.show_preview, self.show_icon) = get_emacs_vars([
|
|
"eaf-file-manager-show-hidden-file",
|
|
"eaf-file-manager-show-preview",
|
|
"eaf-file-manager-show-icon"])
|
|
|
|
if self.theme_mode == "dark":
|
|
if self.theme_background_color == "#000000":
|
|
select_color = "#333333"
|
|
else:
|
|
select_color = QColor(self.theme_background_color).darker(120).name()
|
|
else:
|
|
if self.theme_background_color == "#FFFFFF":
|
|
select_color = "#EEEEEE"
|
|
else:
|
|
select_color = QColor(self.theme_background_color).darker(110).name()
|
|
|
|
self.buffer_widget.eval_js('''init(\"{}\", \"{}\", \"{}\", \"{}\", \"{}\", \"{}\", \"{}\", \"{}\", \"{}\", \"{}\", \"{}\", \"{}\", \"{}\")'''.format(
|
|
self.theme_background_color, self.theme_foreground_color, header_color, directory_color, symlink_color, mark_color, select_color, search_match_color, search_keyword_color,
|
|
self.icon_cache_dir, os.path.sep,
|
|
"true" if self.show_preview else "false",
|
|
"true" if self.show_icon else "false",
|
|
self.theme_mode))
|
|
|
|
@PostGui()
|
|
def handle_append_search(self, file_paths, first_search):
|
|
self.buffer_widget.eval_js('''appendSearch({});'''.format(
|
|
json.dumps(list(map(lambda file_path: self.get_file_info(file_path, self.url), file_paths)))))
|
|
|
|
if first_search:
|
|
self.update_preview(file_paths[0])
|
|
|
|
@PostGui()
|
|
def handle_finish_search(self, search_dir, search_regex, match_number):
|
|
self.buffer_widget.eval_js('''finishSearch()''')
|
|
|
|
if match_number > 0:
|
|
message_to_emacs("Find {} files that matched '{}'".format(match_number, search_regex))
|
|
else:
|
|
message_to_emacs("No file matched '{}'".format(search_regex))
|
|
|
|
def search_directory(self, dir, search_regex):
|
|
self.url = dir
|
|
|
|
fd_command = get_fd_command()
|
|
|
|
if fd_command != "":
|
|
self.buffer_widget.eval_js('''initSearch(\"{}\", \"{}\");'''.format(dir, "{} {}".format(fd_command, search_regex)))
|
|
thread = FdSearchThread(os.path.expanduser(dir), search_regex, self.filter_file)
|
|
else:
|
|
self.buffer_widget.eval_js('''initSearch(\"{}\", \"{}\");'''.format(dir, search_regex))
|
|
thread = PythonSearchThread(os.path.expanduser(dir), search_regex, self.filter_file)
|
|
thread.append_search.connect(self.handle_append_search)
|
|
thread.finish_search.connect(self.handle_finish_search)
|
|
self.search_file_threads.append(thread)
|
|
thread.start()
|
|
|
|
def get_file_mime(self, file_path):
|
|
if os.path.isdir(file_path):
|
|
return "directory"
|
|
else:
|
|
file_info = QtCore.QFileInfo(file_path)
|
|
if file_path.endswith(".vue"):
|
|
return "text-plain"
|
|
else:
|
|
return self.mime_db.mimeTypeForFile(file_info).name().replace("/", "-")
|
|
|
|
def generate_file_icon(self, file_path):
|
|
file_mime = self.get_file_mime(file_path)
|
|
icon_name = "{}.{}".format(file_mime, "png")
|
|
icon_path = os.path.join(self.icon_cache_dir, icon_name)
|
|
|
|
if not os.path.exists(icon_path):
|
|
if file_mime == "directory":
|
|
icon = QIcon.fromTheme("folder")
|
|
else:
|
|
icon = QIcon.fromTheme(file_mime, QIcon("text-plain"))
|
|
|
|
# If nothing match, icon size is empty.
|
|
# Then we use fallback icon.
|
|
if icon.availableSizes() == []:
|
|
icon = QIcon.fromTheme("text-plain")
|
|
|
|
icon.pixmap(64, 64).save(icon_path)
|
|
|
|
return icon_name
|
|
|
|
def get_file_info(self, file_path, current_dir = False):
|
|
file_type = ""
|
|
file_size = ""
|
|
|
|
if os.path.isfile(file_path):
|
|
file_type = "file"
|
|
file_size = self.file_size_format(os.path.getsize(file_path))
|
|
elif os.path.isdir(file_path):
|
|
file_type = "directory"
|
|
file_size = str(self.get_dir_file_number(file_path))
|
|
elif os.path.islink(file_path):
|
|
file_type = "symlink"
|
|
file_size = "1"
|
|
|
|
if current_dir:
|
|
current_dir = os.path.abspath(current_dir)
|
|
name = os.path.abspath(file_path).replace(current_dir, "", 1)[1:]
|
|
else:
|
|
name = os.path.basename(file_path)
|
|
|
|
file_info = {
|
|
"path": file_path,
|
|
"name": name,
|
|
"type": file_type,
|
|
"size": file_size,
|
|
"mark": "",
|
|
"match": "",
|
|
"icon": self.generate_file_icon(file_path),
|
|
"mtime": self.get_file_mtime(file_path)
|
|
}
|
|
|
|
return file_info
|
|
|
|
def get_file_mtime(self, file_path):
|
|
try:
|
|
return os.path.getmtime(file_path)
|
|
except:
|
|
return 0
|
|
|
|
def get_file_infos(self, path):
|
|
file_infos = []
|
|
for p in Path(os.path.expanduser(path)).glob("*"):
|
|
if self.filter_file(p.name):
|
|
file_infos.append(self.get_file_info(str(p.absolute())))
|
|
|
|
file_infos.sort(key=cmp_to_key(self.file_compare))
|
|
|
|
return file_infos
|
|
|
|
def filter_file(self, file_name):
|
|
return self.show_hidden_file or (not file_name.startswith("."))
|
|
|
|
def file_size_format(self, num, suffix='B'):
|
|
for unit in ['','K','M','G','T','P','E','Z']:
|
|
if abs(num) < 1024.0:
|
|
return "%3.1f%s%s" % (num, unit, suffix)
|
|
num /= 1024.0
|
|
return "%.1f%s%s" % (num, 'Yi', suffix)
|
|
|
|
def get_dir_file_number(self, dir):
|
|
try:
|
|
return len(list(filter(lambda f: self.filter_file(f), (os.listdir(dir)))))
|
|
except PermissionError:
|
|
return 0
|
|
|
|
def file_compare(self, a, b):
|
|
type_sort_weights = ["directory", "file", "symlink", ""]
|
|
|
|
a_type_weights = type_sort_weights.index(a["type"])
|
|
b_type_weights = type_sort_weights.index(b["type"])
|
|
|
|
if a_type_weights == b_type_weights:
|
|
if a["name"] < b["name"]:
|
|
return -1
|
|
elif a["name"] > b["name"]:
|
|
return 1
|
|
else:
|
|
return 0
|
|
else:
|
|
return a_type_weights - b_type_weights
|
|
|
|
@QtCore.pyqtSlot(str, str)
|
|
def change_directory(self, dir, current_dir):
|
|
self.url = dir
|
|
|
|
self.monitor_current_dir()
|
|
|
|
eval_in_emacs('eaf--change-default-directory', [self.buffer_id, dir])
|
|
self.change_title(os.path.basename(dir))
|
|
|
|
self.file_infos = self.get_file_infos(dir)
|
|
|
|
self.select_index = 0
|
|
|
|
if current_dir != "":
|
|
files = list(map(lambda file: file["path"], self.file_infos))
|
|
self.select_index = files.index(current_dir) if current_dir in files else 0
|
|
|
|
self.buffer_widget.eval_js('''changePath(\"{}\", {}, {});'''.format(
|
|
self.url,
|
|
json.dumps(self.file_infos),
|
|
self.select_index))
|
|
|
|
if len(self.file_infos) > 0:
|
|
self.init_first_file_preview()
|
|
|
|
self.fetch_git_log()
|
|
|
|
def fetch_git_log(self):
|
|
thread = GitCommitThread(self.url)
|
|
thread.fetch_command_result.connect(self.update_git_log)
|
|
self.fetch_git_log_threads.append(thread)
|
|
thread.start()
|
|
|
|
@PostGui()
|
|
def update_git_log(self, log):
|
|
self.buffer_widget.eval_js('''updateGitLog(\"{}\");'''.format(log))
|
|
|
|
@QtCore.pyqtSlot(str)
|
|
def change_up_directory(self, file):
|
|
current_dir = os.path.dirname(file)
|
|
up_directory_path = str(Path(current_dir).parent.absolute())
|
|
|
|
if file == self.url:
|
|
# If current directory is empty directory, file will same as current directory.
|
|
self.change_directory(current_dir, file)
|
|
elif up_directory_path != current_dir:
|
|
self.change_directory(up_directory_path, current_dir)
|
|
else:
|
|
eval_in_emacs("message", ["Already in root directory"])
|
|
|
|
@QtCore.pyqtSlot(str)
|
|
def update_preview(self, file):
|
|
if self.show_preview:
|
|
self.preview_file = file
|
|
|
|
thread = FetchPreviewInfoThread(file, self.get_preview_file, self.get_file_infos, self.get_file_mime)
|
|
thread.fetch_finish.connect(self.update_preview_info)
|
|
|
|
self.fetch_preview_info_threads.append(thread)
|
|
thread.start()
|
|
|
|
def get_preview_file(self):
|
|
return self.preview_file
|
|
|
|
def update_preview_info(self, file, file_type, file_infos):
|
|
self.buffer_widget.eval_js('''setPreview(\"{}\", \"{}\", {});'''.format(file, file_type, file_infos))
|
|
|
|
@interactive
|
|
def search_file(self):
|
|
self.search_start_index = self.vue_current_index
|
|
self.send_input_message("Search: ", "search_file", "search")
|
|
|
|
@interactive
|
|
def delete_selected_files(self):
|
|
if len(self.vue_get_mark_files()) == 0:
|
|
message_to_emacs("No deletions requested")
|
|
else:
|
|
self.send_input_message("Are you sure you want to delete selected files? ", "delete_file", "yes-or-no")
|
|
|
|
@interactive
|
|
def delete_current_file(self):
|
|
self.send_input_message("Are you sure you want to delete current file? ", "delete_current_file", "yes-or-no")
|
|
|
|
@interactive
|
|
def new_file(self):
|
|
if self.search_regex == "":
|
|
self.send_input_message("Create file: ", "create_file")
|
|
else:
|
|
message_to_emacs("Search page not support new file opeartion.")
|
|
|
|
@interactive
|
|
def new_directory(self):
|
|
if self.search_regex == "":
|
|
self.send_input_message("Create directory: ", "create_directory")
|
|
else:
|
|
message_to_emacs("Search page not support new directory opeartion.")
|
|
|
|
@interactive
|
|
def copy_dir_path(self):
|
|
eval_in_emacs("kill-new", [self.url])
|
|
message_to_emacs("Copy '{}'".format(self.url))
|
|
|
|
@interactive
|
|
def move_current_or_mark_file(self):
|
|
mark_number = len(self.vue_get_mark_files())
|
|
|
|
destination_path = os.path.join(get_emacs_func_result("eaf-file-browser-get-destination-path", []), "")
|
|
|
|
if mark_number > 0:
|
|
self.move_files = self.vue_get_mark_files()
|
|
self.send_input_message("Move mark files to: ", "move_files", "file", destination_path)
|
|
else:
|
|
self.move_file = self.vue_get_select_file()
|
|
if self.move_file != None:
|
|
self.send_input_message("Move '{}' to: ".format(self.move_file["name"]), "move_file", "file", destination_path)
|
|
|
|
@interactive
|
|
def copy_current_or_mark_file(self):
|
|
mark_number = len(self.vue_get_mark_files())
|
|
|
|
destination_path = os.path.join(get_emacs_func_result("eaf-file-browser-get-destination-path", []), "")
|
|
|
|
if mark_number > 0:
|
|
self.copy_files = self.vue_get_mark_files()
|
|
self.send_input_message("Copy mark files to: ", "copy_files", "file", destination_path)
|
|
else:
|
|
self.copy_file = self.vue_get_select_file()
|
|
if self.copy_file != None:
|
|
self.send_input_message("Copy '{}' to: ".format(self.copy_file["name"]), "copy_file", "file", destination_path)
|
|
|
|
@interactive
|
|
def batch_rename(self):
|
|
directory = os.path.basename(os.path.normpath(self.url))
|
|
|
|
all_files = []
|
|
marked_files = []
|
|
|
|
for id, f in enumerate(self.vue_get_all_files()):
|
|
f["id"] = id
|
|
all_files.append(f)
|
|
if f["mark"] == "mark":
|
|
marked_files.append(f)
|
|
|
|
self.batch_rename_files = all_files
|
|
|
|
pending_files = marked_files
|
|
if len(pending_files) == 0:
|
|
pending_files = all_files
|
|
|
|
output = []
|
|
for f in pending_files:
|
|
output.append([len(pending_files), f["id"], f["path"], f["name"], f["type"]])
|
|
eval_in_emacs("eaf-file-manager-rename-edit-buffer", [self.buffer_id, directory, json.dumps(output)])
|
|
|
|
@interactive
|
|
def toggle_hidden_file(self):
|
|
if self.show_hidden_file:
|
|
message_to_emacs("Hide hidden file")
|
|
else:
|
|
self.inhibit_mark_change_file = True
|
|
message_to_emacs("Show hidden file")
|
|
|
|
self.show_hidden_file = not self.show_hidden_file
|
|
|
|
self.refresh()
|
|
|
|
@interactive
|
|
def toggle_preview(self):
|
|
if self.show_preview:
|
|
message_to_emacs("Hide file preview.")
|
|
else:
|
|
message_to_emacs("Show file preview.")
|
|
|
|
self.show_preview = not self.show_preview
|
|
|
|
self.buffer_widget.eval_js('''setPreviewOption(\"{}\")'''.format("true" if self.show_preview else "false"))
|
|
|
|
if self.show_preview:
|
|
current_file = self.vue_get_select_file()
|
|
if current_file != None:
|
|
self.update_preview(current_file["path"])
|
|
|
|
@interactive
|
|
def find_files(self):
|
|
fd_command = get_fd_command()
|
|
if fd_command != "":
|
|
self.send_input_message("Find file with '{}': ".format(fd_command), "find_files", "string")
|
|
else:
|
|
self.send_input_message("Find file with '*?[]' glob pattern: ", "find_files", "string")
|
|
|
|
@interactive
|
|
def refresh_dir(self):
|
|
self.refresh()
|
|
message_to_emacs("Refresh current directory done.")
|
|
|
|
@interactive
|
|
def open_current_file_in_new_tab(self):
|
|
current_file = self.vue_get_select_file()
|
|
if current_file != None:
|
|
eval_in_emacs("eaf-open-in-file-manager", [current_file["path"]])
|
|
|
|
@interactive
|
|
def open_file(self):
|
|
self.send_input_message("Open file: ", "open_file", "file", self.url)
|
|
|
|
@interactive
|
|
def mark_file_by_extension(self):
|
|
self.send_input_message("Mark file by extension: ", "mark_file_by_extension", "string")
|
|
|
|
def refresh(self):
|
|
if not self.inhibit_mark_change_file:
|
|
old_file_info_dict = {}
|
|
for file_info in self.file_infos:
|
|
old_file_info_dict[file_info["path"]] = file_info
|
|
|
|
if self.new_select_file != None:
|
|
# Select new file if self.new_select_file is not None.
|
|
self.change_directory(self.url, self.new_select_file)
|
|
self.new_select_file = None
|
|
else:
|
|
current_file = self.vue_get_select_file()
|
|
if current_file != None:
|
|
self.change_directory(self.url, current_file["path"])
|
|
|
|
if self.inhibit_mark_change_file:
|
|
self.inherit_mark_change_file = False
|
|
else:
|
|
change_file_indexes = []
|
|
for index, new_file in enumerate(self.file_infos):
|
|
if new_file["path"] in old_file_info_dict:
|
|
if new_file["mtime"] != old_file_info_dict[new_file["path"]]["mtime"]:
|
|
change_file_indexes.append(index)
|
|
else:
|
|
change_file_indexes.append(index)
|
|
self.buffer_widget.eval_js("markChangeFiles({});".format(change_file_indexes))
|
|
|
|
self.fetch_git_log()
|
|
|
|
def batch_rename_confirm(self, new_file_string):
|
|
new_files = json.loads(new_file_string)
|
|
|
|
for [total, id, path, old_file_name, new_file_name] in new_files:
|
|
file_dir = os.path.dirname(path)
|
|
# when run find_files, old and new file name may include "/" or "\".
|
|
old_file_path = os.path.join(file_dir, os.path.basename(old_file_name))
|
|
new_file_path = os.path.join(file_dir, os.path.basename(new_file_name))
|
|
|
|
os.rename(old_file_path, new_file_path)
|
|
|
|
for i, f in enumerate(self.batch_rename_files):
|
|
if f["id"] == id:
|
|
self.batch_rename_files[i]["name"] = new_file_name
|
|
self.batch_rename_files[i]["path"] = new_file_path
|
|
break
|
|
|
|
self.buffer_widget.eval_js('''renameFiles({})'''.format(json.dumps(self.batch_rename_files)))
|
|
|
|
def handle_input_response(self, callback_tag, result_content):
|
|
if callback_tag == "delete_file":
|
|
self.handle_delete_file()
|
|
elif callback_tag == "delete_current_file":
|
|
self.handle_delete_current_file()
|
|
elif callback_tag == "rename_file":
|
|
self.handle_rename_file(result_content)
|
|
elif callback_tag == "create_file":
|
|
self.handle_create_file(result_content)
|
|
elif callback_tag == "create_directory":
|
|
self.handle_create_directory(result_content)
|
|
elif callback_tag == "move_file":
|
|
self.handle_move_file(result_content)
|
|
elif callback_tag == "move_files":
|
|
self.handle_move_files(result_content)
|
|
elif callback_tag == "copy_file":
|
|
self.handle_copy_file(result_content)
|
|
elif callback_tag == "copy_files":
|
|
self.handle_copy_files(result_content)
|
|
elif callback_tag == "open_link":
|
|
self.handle_open_link(result_content)
|
|
elif callback_tag == "find_files":
|
|
self.handle_find_files(result_content)
|
|
elif callback_tag == "open_file":
|
|
self.handle_open_file(result_content)
|
|
elif callback_tag == "search_file":
|
|
self.handle_search_file(result_content)
|
|
elif callback_tag == "mark_file_by_extension":
|
|
self.handle_mark_file_by_extension(result_content)
|
|
|
|
def cancel_input_response(self, callback_tag):
|
|
''' Cancel input message.'''
|
|
if callback_tag == "open_link":
|
|
self.buffer_widget.cleanup_links_dom()
|
|
elif callback_tag == "search_file":
|
|
self.buffer_widget.eval_js('''selectFileByIndex({})'''.format(self.search_start_index))
|
|
self.buffer_widget.eval_js('''setSearchMatchFiles({})'''.format(json.dumps([])))
|
|
|
|
def handle_search_forward(self, callback_tag):
|
|
if callback_tag == "search_file":
|
|
if len(self.search_files) > 0:
|
|
if self.search_files_index >= len(self.search_files) - 1:
|
|
self.search_files_index = 0
|
|
else:
|
|
self.search_files_index += 1
|
|
|
|
self.buffer_widget.eval_js('''selectFileByIndex({})'''.format(self.search_files[self.search_files_index][0]))
|
|
|
|
def handle_search_backward(self, callback_tag):
|
|
if callback_tag == "search_file":
|
|
if len(self.search_files) > 0:
|
|
if self.search_files_index <= 0:
|
|
self.search_files_index = len(self.search_files) - 1
|
|
else:
|
|
self.search_files_index -= 1
|
|
|
|
self.buffer_widget.eval_js('''selectFileByIndex({})'''.format(self.search_files[self.search_files_index][0]))
|
|
|
|
def handle_search_finish(self, callback_tag):
|
|
if callback_tag == "search_file":
|
|
self.buffer_widget.eval_js('''setSearchMatchFiles({})'''.format(json.dumps([])))
|
|
|
|
def delete_files(self, file_infos):
|
|
for file_info in file_infos:
|
|
self.delete_file(file_info)
|
|
|
|
def delete_file(self, file_info):
|
|
if file_info["type"] == "file":
|
|
os.remove(file_info["path"])
|
|
elif file_info["type"] == "directory":
|
|
shutil.rmtree(file_info["path"])
|
|
|
|
def vue_get_mark_files(self):
|
|
return list(filter(lambda file: file["mark"] == "mark", self.vue_files)).copy()
|
|
|
|
def vue_get_file_next_to_last_mark(self):
|
|
mark_indexes = []
|
|
for id, f in enumerate(self.vue_files):
|
|
if f["mark"] == "mark":
|
|
mark_indexes.append(id)
|
|
|
|
reverse_mark_indexes = copy.deepcopy(mark_indexes)
|
|
reverse_mark_indexes.reverse()
|
|
for i, mark_id in enumerate(reverse_mark_indexes):
|
|
if i < len(reverse_mark_indexes) - 1:
|
|
if reverse_mark_indexes[i] - reverse_mark_indexes[i + 1] > 1:
|
|
return self.vue_files[reverse_mark_indexes[i] - 1]
|
|
|
|
if mark_indexes[0] > 0:
|
|
return self.vue_files[mark_indexes[0] - 1]
|
|
elif mark_indexes[-1] < len(self.vue_files) - 1:
|
|
return self.vue_files[mark_indexes[-1] + 1]
|
|
else:
|
|
return None
|
|
|
|
def vue_get_all_files(self):
|
|
return self.vue_files.copy()
|
|
|
|
def vue_get_select_file(self):
|
|
try:
|
|
return copy.deepcopy(self.vue_files[self.vue_current_index])
|
|
except:
|
|
return None
|
|
|
|
@QtCore.pyqtSlot(list)
|
|
def vue_update_files(self, vue_files):
|
|
self.vue_files = vue_files
|
|
|
|
@QtCore.pyqtSlot(int)
|
|
def vue_update_current_index(self, inex):
|
|
self.vue_current_index = inex
|
|
|
|
@QtCore.pyqtSlot(str)
|
|
def rename_file(self, file_path):
|
|
self.rename_file_path = file_path
|
|
self.rename_file_name = os.path.basename(file_path)
|
|
self.send_input_message("Rename file name '{}' to: ".format(self.rename_file_name), "rename_file", "string", self.rename_file_name)
|
|
|
|
def handle_delete_file(self):
|
|
next_to_file = self.vue_get_file_next_to_last_mark()
|
|
if next_to_file != None:
|
|
self.new_select_file = next_to_file["path"]
|
|
|
|
self.delete_files(self.vue_get_mark_files())
|
|
self.buffer_widget.eval_js("removeMarkFiles();")
|
|
|
|
message_to_emacs("Delete selected files success.")
|
|
|
|
def handle_delete_current_file(self):
|
|
file_info = self.vue_get_select_file()
|
|
if file_info != None:
|
|
if self.vue_current_index > 0:
|
|
self.new_select_file = self.vue_files[self.vue_current_index - 1]["path"]
|
|
|
|
self.delete_file(file_info)
|
|
self.buffer_widget.eval_js("removeSelectFile();")
|
|
|
|
message_to_emacs("Delete file {} success.".format(file_info["path"]))
|
|
|
|
def handle_rename_file(self, new_file_name):
|
|
if new_file_name == self.rename_file_name:
|
|
message_to_emacs("Same as original name, the file name remains unchanged.")
|
|
elif new_file_name in os.listdir(os.path.dirname(self.rename_file_path)):
|
|
self.send_input_message("File name '{}' exists, choose different name: ".format(self.rename_file_name), "rename_file", "string", new_file_name)
|
|
else:
|
|
try:
|
|
new_file_path = os.path.join(os.path.dirname(self.rename_file_path), new_file_name)
|
|
self.new_select_file = new_file_path
|
|
|
|
os.rename(self.rename_file_path, new_file_path)
|
|
|
|
self.buffer_widget.eval_js("rename(\"{}\", \"{}\", \"{}\");".format(self.rename_file_path, new_file_path, new_file_name))
|
|
|
|
message_to_emacs("Rename to '{}'".format(new_file_name))
|
|
except:
|
|
import traceback
|
|
message_to_emacs("Error in rename file: " + str(traceback.print_exc()))
|
|
|
|
def handle_create_file(self, new_file):
|
|
if new_file in os.listdir(self.url):
|
|
self.send_input_message("File '{}' exists, choose different name: ".format(new_file), "create_file")
|
|
else:
|
|
self.inhibit_mark_change_file = True
|
|
|
|
new_file_path = os.path.join(self.url, new_file)
|
|
self.new_select_file = new_file_path # make sure select new file
|
|
|
|
with open(new_file_path, "a"):
|
|
os.utime(new_file_path)
|
|
|
|
self.buffer_widget.eval_js('''addNewFile({})'''.format(json.dumps(self.get_file_info(new_file_path))))
|
|
|
|
def handle_create_directory(self, new_directory):
|
|
if new_directory in os.listdir(self.url):
|
|
self.send_input_message("Directory '{}' exists, choose different name: ".format(new_directory), "create_directory")
|
|
else:
|
|
self.inhibit_mark_change_file = True
|
|
|
|
new_directory_path = os.path.join(self.url, new_directory)
|
|
self.new_select_file = new_directory_path # make sure select new directory
|
|
|
|
os.makedirs(new_directory_path)
|
|
|
|
self.buffer_widget.eval_js('''addNewDirectory({})'''.format(json.dumps(self.get_file_info(new_directory_path))))
|
|
|
|
def handle_move_file(self, new_file):
|
|
if new_file == self.url:
|
|
message_to_emacs("The directory has not changed, file '{}' not moved.".format(self.move_file["name"]))
|
|
else:
|
|
try:
|
|
if self.vue_current_index > 0:
|
|
self.new_select_file = self.vue_files[self.vue_current_index - 1]["path"]
|
|
|
|
shutil.move(self.move_file["path"], new_file)
|
|
self.buffer_widget.eval_js("removeSelectFile();")
|
|
|
|
message_to_emacs("Move '{}' to '{}'".format(self.move_file["name"], new_file))
|
|
except:
|
|
import traceback
|
|
message_to_emacs("Error in move file: " + str(traceback.print_exc()))
|
|
|
|
def handle_move_files(self, new_dir):
|
|
if new_dir == self.url:
|
|
message_to_emacs("The directory has not changed, mark files not moved.")
|
|
elif os.path.isdir(new_dir):
|
|
try:
|
|
next_to_file = self.vue_get_file_next_to_last_mark()
|
|
if next_to_file != None:
|
|
self.new_select_file = next_to_file["path"]
|
|
|
|
for move_file in self.move_files:
|
|
shutil.move(move_file["path"], new_dir)
|
|
|
|
self.buffer_widget.eval_js("removeMarkFiles();")
|
|
|
|
message_to_emacs("Move mark files to '{}'".format(new_dir))
|
|
except:
|
|
import traceback
|
|
message_to_emacs("Error in move files: " + str(traceback.print_exc()))
|
|
else:
|
|
message_to_emacs("'{}' is not directory, abandon movement.")
|
|
|
|
def handle_copy_file(self, new_file):
|
|
if new_file == self.url:
|
|
message_to_emacs("The directory has not changed, file '{}' not copyd.".format(self.copy_file["name"]))
|
|
else:
|
|
try:
|
|
if os.path.isdir(self.copy_file["path"]):
|
|
shutil.copytree(src=self.copy_file["path"], dst=new_file, dirs_exist_ok=True)
|
|
else:
|
|
shutil.copy(self.copy_file["path"], new_file)
|
|
|
|
self.refresh()
|
|
|
|
message_to_emacs("Copy '{}' to '{}'".format(self.copy_file["name"], new_file))
|
|
except:
|
|
import traceback
|
|
message_to_emacs("Error in copy file: " + str(traceback.print_exc()))
|
|
|
|
def handle_copy_files(self, new_dir):
|
|
if new_dir == self.url:
|
|
message_to_emacs("The directory has not changed, mark files not copyd.")
|
|
elif os.path.isdir(new_dir):
|
|
try:
|
|
for copy_file in self.copy_files:
|
|
if os.path.isdir(copy_file["path"]):
|
|
shutil.copytree(src=copy_file["path"], dst=new_dir, dirs_exist_ok=True)
|
|
else:
|
|
shutil.copy(copy_file["path"], new_dir)
|
|
|
|
message_to_emacs("Copy mark files to '{}'".format(new_dir))
|
|
except:
|
|
import traceback
|
|
message_to_emacs("Error in copy files: " + str(traceback.print_exc()))
|
|
else:
|
|
message_to_emacs("'{}' is not directory, abandon copy.")
|
|
|
|
def handle_open_link(self, result_content):
|
|
marker = result_content.strip()
|
|
file_name = self.buffer_widget.execute_js("Marker.getMarkerText('%s')" % str(marker))
|
|
class_name = self.buffer_widget.execute_js("Marker.getMarkerClass('%s')" % str(marker))
|
|
|
|
if class_name == "eaf-file-manager-file-name":
|
|
self.buffer_widget.eval_js('''openFileByName(\"{}\")'''.format(file_name))
|
|
elif class_name == "eaf-file-manager-preview-file-name":
|
|
self.buffer_widget.eval_js('''openPreviewFileByName(\"{}\")'''.format(file_name))
|
|
|
|
self.buffer_widget.cleanup_links_dom()
|
|
|
|
def handle_find_files(self, regex):
|
|
eval_in_emacs("eaf-open", [self.url, "file-manager", "search:{}".format(regex), "always-new"])
|
|
|
|
def handle_open_file(self, new_file):
|
|
if os.path.exists(new_file):
|
|
if os.path.isfile(new_file):
|
|
eval_in_emacs("find-file", [new_file])
|
|
elif os.path.isdir(new_file):
|
|
eval_in_emacs("eaf-open-in-file-manager", [new_file])
|
|
else:
|
|
message_to_emacs("File '{}' not exists".format(new_file))
|
|
|
|
def handle_search_file(self, search_string):
|
|
if search_string == "":
|
|
self.buffer_widget.eval_js('''selectFileByIndex({})'''.format(self.search_start_index))
|
|
self.buffer_widget.eval_js('''setSearchMatchFiles({})'''.format(json.dumps([])))
|
|
else:
|
|
in_minibuffer = get_emacs_func_result("minibufferp", [])
|
|
|
|
if in_minibuffer:
|
|
|
|
all_files = list(map(self.pick_search_string, self.vue_get_all_files()))
|
|
self.search_files = list(filter(
|
|
lambda args: not False in list(map(lambda str: self.is_file_match(args[1], str), search_string.split())),
|
|
enumerate(all_files)
|
|
))
|
|
self.search_files_index = 0
|
|
|
|
self.buffer_widget.eval_js('''setSearchMatchFiles({})'''.format(json.dumps(
|
|
list(map(lambda args: args[0], self.search_files))
|
|
)))
|
|
|
|
if len(self.search_files) > 0:
|
|
return self.buffer_widget.eval_js('''selectFileByIndex({})'''.format(self.search_files[self.search_files_index][0]))
|
|
|
|
# Notify user if no match file found.
|
|
eval_in_emacs("message", ["Did not find a matching file"])
|
|
else:
|
|
message_to_emacs("Select file: {}".format(self.vue_files[self.vue_current_index]['name']))
|
|
self.buffer_widget.eval_js('''setSearchMatchFiles({})'''.format(json.dumps([])))
|
|
|
|
def handle_mark_file_by_extension(self, extension):
|
|
self.buffer_widget.eval_js('''markFileByExtension(\"{}\")'''.format(extension))
|
|
|
|
def is_file_match(self, file, search_word):
|
|
return ((len(search_word) > 0 and search_word[0] != "!" and search_word.lower() in file.lower()) or
|
|
(len(search_word) > 0 and search_word[0] == "!" and (not search_word.lower()[1:] in file.lower())))
|
|
|
|
def marker_offset_x(self):
|
|
if self.show_icon:
|
|
return -28
|
|
else:
|
|
return -16
|
|
|
|
def marker_offset_y(self):
|
|
return 4
|
|
|
|
def pick_search_string(self, file):
|
|
from pypinyin import pinyin, Style
|
|
|
|
file_name = file["name"]
|
|
|
|
if self.is_contains_chinese(file_name):
|
|
return ''.join(list(map(lambda x: x[0], pinyin(file_name, style=Style.FIRST_LETTER))))
|
|
else:
|
|
return file_name
|
|
|
|
def is_contains_chinese(self, string):
|
|
for _char in string:
|
|
if '\u4e00' <= _char <= '\u9fa5':
|
|
return True
|
|
return False
|
|
|
|
def destroy_buffer(self):
|
|
''' Destroy buffer.'''
|
|
for search_file_thread in self.search_file_threads:
|
|
if search_file_thread.isRunning():
|
|
search_file_thread.quit()
|
|
search_file_thread.wait()
|
|
|
|
for fetch_preview_info_thread in self.fetch_preview_info_threads:
|
|
if fetch_preview_info_thread.isRunning():
|
|
fetch_preview_info_thread.quit()
|
|
fetch_preview_info_thread.wait()
|
|
|
|
if self.buffer_widget is not None:
|
|
self.buffer_widget.deleteLater()
|
|
|
|
def resize_view(self):
|
|
(frame_width, _) = get_emacs_func_result("eaf-get-render-size", [])
|
|
if self.buffer_widget.width() <= int(frame_width) / 2:
|
|
if self.show_preview:
|
|
self.buffer_widget.eval_js('''setPreviewOption(\"{}\")'''.format("false"))
|
|
self.hide_preview_by_width = True
|
|
else:
|
|
if self.show_preview and self.hide_preview_by_width:
|
|
self.buffer_widget.eval_js('''setPreviewOption(\"{}\")'''.format("true"))
|
|
self.hide_preview_by_width = False
|
|
|
|
class FetchPreviewInfoThread(QThread):
|
|
|
|
fetch_finish = QtCore.pyqtSignal(str, str, str)
|
|
|
|
def __init__(self, file, get_preview_file_callback, get_files_callback, get_file_mime_callback):
|
|
QThread.__init__(self)
|
|
|
|
self.file = file
|
|
self.get_preview_file_callback = get_preview_file_callback
|
|
self.get_files_callback = get_files_callback
|
|
self.get_file_mime_callback = get_file_mime_callback
|
|
|
|
def run(self):
|
|
# Wait 300 milliseconds, if current preview file is changed, stop fetch thread.
|
|
time.sleep(0.3)
|
|
if self.get_preview_file_callback() == self.file:
|
|
path = ""
|
|
file_type = ""
|
|
file_infos = []
|
|
|
|
if self.file != "":
|
|
path = Path(self.file)
|
|
|
|
if path.is_file():
|
|
file_type = "file"
|
|
file_infos = [{
|
|
"mime": self.get_file_mime_callback(str(path.absolute())),
|
|
"size": os.path.getsize(str(path.absolute()))
|
|
}]
|
|
elif path.is_dir():
|
|
file_type = "directory"
|
|
file_infos = self.get_files_callback(self.file)
|
|
elif path.is_symlink():
|
|
file_type = "symlink"
|
|
|
|
self.fetch_finish.emit(self.file, file_type, json.dumps(file_infos))
|
|
|
|
class GitCommitThread(QThread):
|
|
|
|
fetch_command_result = QtCore.pyqtSignal(str)
|
|
|
|
def __init__(self, current_dir):
|
|
QThread.__init__(self)
|
|
|
|
self.current_dir = current_dir
|
|
|
|
def get_command_result(self, command_string):
|
|
process = subprocess.Popen(command_string, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
process.wait()
|
|
result = process.stdout.readline().decode("utf-8")
|
|
return os.linesep.join([s for s in result.splitlines() if s])
|
|
|
|
def run(self):
|
|
git_log = ""
|
|
git_last_commit = self.get_command_result("cd {}; git log -1 --oneline".format(self.current_dir))
|
|
|
|
if git_last_commit != "" and not git_last_commit.startswith("fatal"):
|
|
git_current_branch = self.get_command_result("cd {}; git branch --show-current".format(self.current_dir))
|
|
git_log = "[{}] {}".format(git_current_branch, git_last_commit)
|
|
|
|
self.fetch_command_result.emit(git_log)
|
|
|
|
class FileSearchThread(QThread):
|
|
|
|
append_search = QtCore.pyqtSignal(list, bool)
|
|
finish_search = QtCore.pyqtSignal(str, str, int)
|
|
|
|
def __init__(self, search_dir, search_regex, filter_file_callback):
|
|
QThread.__init__(self)
|
|
|
|
self.search_dir = search_dir
|
|
self.search_regex = search_regex
|
|
self.filter_file_callback = filter_file_callback
|
|
|
|
self.start_time = time.time()
|
|
self.search_send_duration = 0.3
|
|
self.first_search = True
|
|
self.file_paths = []
|
|
self.match_number = 0
|
|
|
|
def run(self):
|
|
pass
|
|
|
|
def send_files(self):
|
|
if len(self.file_paths) > 0:
|
|
self.append_search.emit(self.file_paths, self.first_search)
|
|
self.first_search = False
|
|
|
|
self.file_paths = []
|
|
|
|
class PythonSearchThread(FileSearchThread):
|
|
|
|
def __init__(self, search_dir, search_regex, filter_file_callback):
|
|
FileSearchThread.__init__(self, search_dir, search_regex, filter_file_callback)
|
|
|
|
def run(self):
|
|
for p in Path(self.search_dir).rglob(self.search_regex):
|
|
if self.filter_file_callback(p.name):
|
|
self.file_paths.append(str(p.absolute()))
|
|
self.match_number += 1
|
|
|
|
if (time.time() - self.start_time) > self.search_send_duration:
|
|
self.send_files()
|
|
self.start_time = time.time()
|
|
|
|
self.send_files()
|
|
|
|
self.finish_search.emit(self.search_dir, self.search_regex, self.match_number)
|
|
|
|
class FdSearchThread(FileSearchThread):
|
|
|
|
def __init__(self, search_dir, search_regex, filter_file_callback):
|
|
FileSearchThread.__init__(self, search_dir, search_regex, filter_file_callback)
|
|
|
|
def run(self):
|
|
fd_command = get_fd_command()
|
|
|
|
process = subprocess.Popen("{} -c never --search-path '{}' {}".format(fd_command, self.search_dir, self.search_regex),
|
|
shell=True,
|
|
stderr=subprocess.PIPE,
|
|
stdout=subprocess.PIPE)
|
|
while True:
|
|
status = process.poll()
|
|
if status is not None:
|
|
break
|
|
|
|
output = process.stdout.readline()
|
|
if output:
|
|
self.file_paths.append(output.strip().decode("utf-8"))
|
|
self.match_number += 1
|
|
|
|
if (time.time() - self.start_time) > self.search_send_duration:
|
|
self.send_files()
|
|
self.start_time = time.time()
|
|
|
|
self.send_files()
|
|
|
|
self.finish_search.emit(self.search_dir, "{} {}".format(get_fd_command(), self.search_regex), self.match_number)
|