aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mbgui/executor.py49
-rw-r--r--mbgui/main.py395
-rw-r--r--mbgui/main.ui56
-rw-r--r--mbgui/mblaze.py21
-rw-r--r--mbgui/tkapp.py59
-rw-r--r--setup.py3
6 files changed, 302 insertions, 281 deletions
diff --git a/mbgui/executor.py b/mbgui/executor.py
new file mode 100644
index 0000000..9f3b420
--- /dev/null
+++ b/mbgui/executor.py
@@ -0,0 +1,49 @@
+import collections
+import concurrent.futures
+import threading
+import typing
+
+import PySide6.QtCore
+
+
+class Executor(PySide6.QtCore.QObject):
+
+ def __init__(self):
+ super().__init__()
+ self._executor = concurrent.futures.ThreadPoolExecutor()
+ self._call_main_queue = collections.deque()
+ self._call_main_lock = threading.Lock()
+
+ def call_main(self,
+ fn: typing.Callable,
+ *args):
+ with self._call_main_lock:
+ self._call_main_queue.append((fn, args))
+
+ PySide6.QtCore.QMetaObject.invokeMethod(
+ self, "_on_main_call", PySide6.QtCore.Qt.QueuedConnection)
+
+ def call_worker(self,
+ fn: typing.Callable,
+ *args,
+ done_cb: typing.Callable = None,
+ ) -> concurrent.futures.Future:
+ future = self._executor.submit(fn, *args)
+
+ if done_cb:
+ future.add_done_callback(
+ lambda f: self.call_main(done_cb, f.result()))
+
+ return future
+
+ @PySide6.QtCore.Slot()
+ def _on_main_call(self):
+ with self._call_main_lock:
+ if not self._call_main_queue:
+ return
+
+ queue = self._call_main_queue
+ self._call_main_queue = collections.deque()
+
+ for fn, args in queue:
+ fn(*args)
diff --git a/mbgui/main.py b/mbgui/main.py
index a944ade..f6263c2 100644
--- a/mbgui/main.py
+++ b/mbgui/main.py
@@ -1,252 +1,227 @@
from pathlib import Path
-from tkinter import ttk
import sys
-import tkinter as tk
-import tkinter.font
+
+import PySide6.QtCore
+import PySide6.QtGui
+import PySide6.QtUiTools
+import PySide6.QtWidgets
from mbgui import mblaze
-from mbgui.tkapp import TkApp
+from mbgui.executor import Executor
-conf = {'fonts': {'regular': {'family': 'Inter',
- 'size': 12,
- 'weight': 'normal'},
- 'monospace': {'family': 'Roboto Mono',
- 'size': 12,
- 'weight': 'normal'}}}
+icons_path = Path(__file__).parent / 'icons'
+main_ui_path = Path(__file__).parent / 'main.ui'
def main():
- app = App(conf)
- app.run(sys.argv[1:])
-
-
-class App:
-
- def __init__(self, conf):
- self._directories = set()
- self._selected_directory = None
- self._selected_message = None
-
- self._app = TkApp()
-
- self._fonts = {name: tk.font.Font(name=f'{name}Font', **values)
- for name, values in conf['fonts'].items()}
-
- self._icons = {
- path.stem: tk.PhotoImage(file=str(path))
- for path in (Path(__file__).parent / 'icons').glob('*.png')}
-
- self._app.root.columnconfigure(0, weight=1)
- self._app.root.rowconfigure(0, weight=1)
-
- self._app.style.configure('Treeview', font=self._fonts['regular'])
-
- frame = ttk.Frame(self._app.root)
- frame.columnconfigure(0, weight=1)
- frame.rowconfigure(0, weight=1)
- frame.grid(row=0, column=0, sticky='nswe')
-
- horizontal_pane = ttk.Panedwindow(frame, orient="horizontal")
- horizontal_pane.grid(row=0, column=0, sticky='nswe')
-
- directories_frame = ttk.Frame(horizontal_pane)
- directories_frame.columnconfigure(0, weight=1)
- directories_frame.rowconfigure(0, weight=1)
- horizontal_pane.add(directories_frame, weight=1)
-
- self._directories_tree = ttk.Treeview(directories_frame,
- selectmode='browse',
- columns=('unseen', 'total'))
- self._directories_tree.column('unseen', width=32, anchor='e')
- self._directories_tree.column('total', width=32, anchor='e')
- self._directories_tree.bind('<<TreeviewSelect>>',
- self._on_directories_tree_select)
- self._directories_tree.grid(row=0, column=0, sticky='nswe')
-
- directories_scroll = ttk.Scrollbar(
- directories_frame, orient='vertical',
- command=self._directories_tree.yview)
- self._directories_tree.configure(yscrollcommand=directories_scroll.set)
- directories_scroll.grid(row=0, column=1, sticky='nswe')
-
- vertical_pane = ttk.Panedwindow(horizontal_pane, orient="vertical")
- horizontal_pane.add(vertical_pane, weight=2)
-
- messages_frame = ttk.Frame(vertical_pane)
- messages_frame.columnconfigure(0, weight=1)
- messages_frame.rowconfigure(0, weight=1)
- vertical_pane.add(messages_frame, weight=1)
-
- self._messages_tree = ttk.Treeview(messages_frame,
- selectmode='browse',
- columns=('sender', 'date'))
- self._messages_tree.column('sender', width=32, anchor='w')
- self._messages_tree.column('date', width=32, anchor='w')
- self._messages_tree.bind('<<TreeviewSelect>>',
- self._on_messages_tree_select)
- self._messages_tree.bind('<Double-Button-1>',
- self._on_messages_tree_double)
- self._messages_tree.grid(row=0, column=0, sticky='nswe')
-
- messages_scroll = ttk.Scrollbar(
- messages_frame, orient='vertical',
- command=self._messages_tree.yview)
- self._messages_tree.configure(yscrollcommand=messages_scroll.set)
- messages_scroll.grid(row=0, column=1, sticky='nswe')
-
- message_frame = ttk.Frame(vertical_pane)
- message_frame.columnconfigure(0, weight=1)
- message_frame.rowconfigure(0, weight=1)
- vertical_pane.add(message_frame, weight=1)
-
- self._message_text = tk.Text(message_frame,
- font=self._fonts['monospace'])
- self._message_text.grid(row=0, column=0, sticky='nswe')
-
- message_scroll_vertical = ttk.Scrollbar(
- message_frame, orient='vertical',
- command=self._message_text.yview)
- self._message_text.configure(
- yscrollcommand=message_scroll_vertical.set)
- message_scroll_vertical.grid(row=0, column=1, sticky='nswe')
-
- def run(self, args):
- self._get_directories(args)
- self._app.run()
- def _get_directories(self, paths):
- def on_done(f):
- self._app.call_main(self._on_directories, f.result())
+ def on_directories_changed(current, previous):
+ item = directories.model.itemFromIndex(current)
+ directory = item.data() if item else None
+ messages.set_directory(directory)
- f = self._app.call_worker(mblaze.get_directories, paths)
- f.add_done_callback(on_done)
+ def on_messages_changed(current, pevious):
+ item = messages.model.itemFromIndex(current)
+ path = item.data() if item else None
+ message.set_path(path)
- def _get_directory_unseen(self, path):
- def on_done(f):
- self._app.call_main(self._on_directory_unseen, path, f.result())
+ def on_messages_double_clicked(index):
+ item = messages.model.itemFromIndex(index)
+ path = item.data() if item else None
+ if path:
+ print(path)
- f = self._app.call_worker(mblaze.get_directory_unseen, path)
- f.add_done_callback(on_done)
+ app = PySide6.QtWidgets.QApplication(sys.argv)
- def _get_directory_total(self, path):
- def on_done(f):
- self._app.call_main(self._on_directory_total, path, f.result())
+ executor = Executor()
+ icons = {i.stem: PySide6.QtGui.QIcon(str(i))
+ for i in icons_path.glob('*.png')}
- f = self._app.call_worker(mblaze.get_directory_total, path)
- f.add_done_callback(on_done)
+ directories = Directories(executor, icons, sys.argv[1:])
+ messages = Messages(executor, icons)
+ message = Message(executor)
- def _get_messages(self, path):
- def on_done(f):
- self._app.call_main(self._on_messages, path, f.result())
+ loader = PySide6.QtUiTools.QUiLoader()
+ window = loader.load(main_ui_path)
- f = self._app.call_worker(mblaze.get_messages, path)
- f.add_done_callback(on_done)
+ font = PySide6.QtGui.QFontDatabase.systemFont(
+ PySide6.QtGui.QFontDatabase.FixedFont)
+ window.message.setFont(font)
- def _get_message(self, path):
- def on_done(f):
- self._app.call_main(self._on_message, path, f.result())
-
- f = self._app.call_worker(mblaze.get_message, path)
- f.add_done_callback(on_done)
-
- def _on_directories(self, directories):
- self._directories = set()
- directory_ids = self._directories_tree.get_children()
- if directory_ids:
- self._directories_tree.delete(*directory_ids)
-
- def add_directory(parent_id, directory):
- directory_id = (str(Path(parent_id) / directory.name) if parent_id
- else directory.name)
- if directory.is_leaf:
- self._directories.add(directory_id)
- icon = self._icons['inbox-16' if directory.is_leaf
+ window.vsplitter.setStretchFactor(0, 1)
+ window.vsplitter.setStretchFactor(1, 2)
+
+ window.directories.setModel(directories.model)
+ model = window.directories.selectionModel()
+ model.currentChanged.connect(on_directories_changed)
+
+ header = window.directories.header()
+ header.setSectionResizeMode(0, PySide6.QtWidgets.QHeaderView.Stretch)
+ header.setSectionResizeMode(1, PySide6.QtWidgets.QHeaderView.Interactive)
+ header.setSectionResizeMode(2, PySide6.QtWidgets.QHeaderView.Interactive)
+
+ window.messages.setModel(messages.model)
+ model = window.messages.selectionModel()
+ model.currentChanged.connect(on_messages_changed)
+ window.messages.doubleClicked.connect(on_messages_double_clicked)
+
+ header = window.messages.header()
+ header.setSectionResizeMode(0, PySide6.QtWidgets.QHeaderView.Stretch)
+ header.setSectionResizeMode(1, PySide6.QtWidgets.QHeaderView.Interactive)
+ header.setSectionResizeMode(2, PySide6.QtWidgets.QHeaderView.Interactive)
+
+ message.change.connect(window.message.setText)
+
+ window.show()
+
+ sys.exit(app.exec())
+
+
+class Directories:
+
+ def __init__(self, executor, icons, paths):
+ self._executor = executor
+ self._icons = icons
+ self._model = PySide6.QtGui.QStandardItemModel()
+ self._model.setHorizontalHeaderLabels(['Directory', 'Unseed', 'Total'])
+ self._get_directories(paths)
+
+ @property
+ def model(self) -> PySide6.QtCore.QAbstractItemModel:
+ return self._model
+
+ def _get_directories(self, paths):
+
+ def on_done(directories):
+ for directory in directories:
+ self._model.appendRow(create_row(directory))
+
+ def create_row(directory):
+ icon = self._icons['inbox-16' if directory.path
else 'folder-16']
- self._directories_tree.insert(parent_id, 'end',
- id=directory_id,
- text=f' {directory.name}',
- image=icon,
- values=('', ''),
- open=True)
- if directory.is_leaf:
- self._get_directory_unseen(directory_id)
- self._get_directory_total(directory_id)
+
+ items = [PySide6.QtGui.QStandardItem(icon, directory.name),
+ PySide6.QtGui.QStandardItem(''),
+ PySide6.QtGui.QStandardItem('')]
+
+ for item in items:
+ item.setData(directory.path)
+
+ if directory.path:
+ self._get_directory_unseen(items[1], directory.path)
+ self._get_directory_total(items[2], directory.path)
+
for child in directory.children:
- add_directory(directory_id, child)
-
- for directory in directories:
- add_directory('', directory)
-
- def _on_directory_unseen(self, directory, count):
- values = self._directories_tree.item(directory, 'values')
- values = str(count), values[1]
- self._directories_tree.item(directory, values=values)
-
- def _on_directory_total(self, directory, count):
- values = self._directories_tree.item(directory, 'values')
- values = values[0], str(count)
- self._directories_tree.item(directory, values=values)
-
- def _on_directories_tree_select(self, evt):
- selection = self._directories_tree.selection()
- directory = selection[0] if selection else None
- directory = directory if directory in self._directories else None
- if directory == self._selected_directory:
- return
+ items[0].appendRow(create_row(child))
- self._selected_directory = directory
+ return items
- message_ids = self._messages_tree.get_children()
- if message_ids:
- self._messages_tree.delete(*message_ids)
+ self._executor.call_worker(mblaze.get_directories, paths,
+ done_cb=on_done)
- if not directory:
- return
+ def _get_directory_unseen(self, item, path):
- self._get_messages(directory)
+ def on_done(count):
+ item.setText(str(count))
- def _on_messages(self, directory, messages):
- if directory != self._selected_directory:
+ self._executor.call_worker(mblaze.get_directory_unseen, path,
+ done_cb=on_done)
+
+ def _get_directory_total(self, item, path):
+
+ def on_done(count):
+ item.setText(str(count))
+
+ self._executor.call_worker(mblaze.get_directory_total, path,
+ done_cb=on_done)
+
+
+class Messages:
+
+ def __init__(self, executor: Executor, icons):
+ self._executor = executor
+ self._icons = icons
+ self._model = PySide6.QtGui.QStandardItemModel()
+ self._model.setHorizontalHeaderLabels(['Subject', 'Sender', 'Date'])
+ self._directory = None
+
+ @property
+ def model(self) -> PySide6.QtCore.QAbstractItemModel:
+ return self._model
+
+ def set_directory(self, directory: str):
+ if self._directory == directory:
return
- def add_message(parent_id, message):
+ self._directory = directory
+ self._model.setRowCount(0)
+
+ if directory:
+ self._get_messages(directory)
+
+ def _get_messages(self, directory):
+
+ def on_done(messages):
+ if self._directory != directory:
+ return
+
+ for message in messages:
+ self._model.appendRow(create_row(message))
+
+ def create_row(message):
icon = _status_icon(message.status)
- icon = self._icons[f'{icon}-16'] if icon else ''
- self._messages_tree.insert(parent_id, 'end',
- id=message.path,
- text=f' {message.subject}',
- image=icon,
- values=(message.sender, message.date))
+ icon = self._icons[f'{icon}-16'] if icon else None
+
+ items = [PySide6.QtGui.QStandardItem(icon, message.subject),
+ PySide6.QtGui.QStandardItem(message.sender),
+ PySide6.QtGui.QStandardItem(message.date)]
+
+ for item in items:
+ item.setData(message.path)
+
for child in message.children:
- add_message(message.path, child)
+ items[0].appendRow(create_row(child))
+
+ return items
- for message in messages:
- add_message('', message)
+ self._executor.call_worker(mblaze.get_messages, directory,
+ done_cb=on_done)
- def _on_messages_tree_select(self, evt):
- selection = self._messages_tree.selection()
- message = selection[0] if selection else None
- if message == self._selected_message:
+ def _on_messages(self, directory, messages):
+ if directory != self._selected_directory:
return
- self._selected_message = message
- self._message_text.delete('1.0', 'end')
- if not message:
- return
+class Message(PySide6.QtCore.QObject):
- self._get_message(message)
+ change = PySide6.QtCore.Signal(str)
- def _on_messages_tree_double(self, evt):
- if self._selected_message:
- print(self._selected_message)
+ def __init__(self, executor):
+ super().__init__()
+ self._executor = executor
+ self._path = None
- def _on_message(self, message, text):
- if message != self._selected_message:
+ def set_path(self, path):
+ if self._path == path:
return
- self._message_text.insert('1.0', text)
+ self._path = path
+ self.change.emit('')
+
+ if path:
+ self._get_message(path)
+
+ def _get_message(self, path):
+
+ def on_done(text):
+ if self._path != path:
+ return
+
+ self.change.emit(text)
+
+ self._executor.call_worker(mblaze.get_message, path,
+ done_cb=on_done)
def _status_icon(status):
diff --git a/mbgui/main.ui b/mbgui/main.ui
new file mode 100644
index 0000000..a675f4c
--- /dev/null
+++ b/mbgui/main.ui
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>windows</class>
+ <widget class="QMainWindow" name="windows">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>MainWindow</string>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QSplitter" name="vsplitter">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <widget class="QTreeView" name="directories">
+ <property name="editTriggers">
+ <set>QAbstractItemView::NoEditTriggers</set>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ <widget class="QSplitter" name="hsplitter">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <widget class="QTreeView" name="messages">
+ <property name="editTriggers">
+ <set>QAbstractItemView::NoEditTriggers</set>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ <widget class="QTextEdit" name="message">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/mbgui/mblaze.py b/mbgui/mblaze.py
index 89841b3..4dae52c 100644
--- a/mbgui/mblaze.py
+++ b/mbgui/mblaze.py
@@ -14,8 +14,8 @@ class Status(enum.Enum):
class Directory(typing.NamedTuple):
+ path: typing.Optional[str]
name: str
- is_leaf: bool
children: typing.List['Directory']
@@ -30,7 +30,7 @@ class Message(typing.NamedTuple):
def get_directories(roots: typing.Iterable[str]) -> typing.List[Directory]:
paths = _cmd(['mdirs', '-a', *roots])
- directories = (_get_directory(Path(path).parts) for path in paths)
+ directories = (_get_directory(path, Path(path).parts) for path in paths)
directories = _group_directories(directories)
directories = [_reduce_directory(directory) for directory in directories]
return directories
@@ -73,12 +73,11 @@ def _cmd(args, stdin_lines=[]):
return stdout.split('\n')
-def _get_directory(path_parts):
+def _get_directory(path, path_parts):
name, path_parts = path_parts[0], path_parts[1:]
- is_leaf = not path_parts
- children = [_get_directory(path_parts)] if path_parts else []
- return Directory(name=name,
- is_leaf=is_leaf,
+ children = [_get_directory(path, path_parts)] if path_parts else []
+ return Directory(path=(path if not path_parts else None),
+ name=name,
children=children)
@@ -88,16 +87,16 @@ def _group_directories(directories):
name_directories.setdefault(i.name, []).append(i)
for name, directories in name_directories.items():
- is_leaf = any(i.is_leaf for i in directories)
+ path = next((i.path for i in directories if i.path), None)
children = list(_group_directories(
itertools.chain.from_iterable(i.children for i in directories)))
- yield Directory(name=name,
- is_leaf=is_leaf,
+ yield Directory(path=path,
+ name=name,
children=children)
def _reduce_directory(directory):
- while not directory.is_leaf and len(directory.children) == 1:
+ while not directory.path and len(directory.children) == 1:
child = directory.children[0]
directory = child._replace(name=str(Path(directory.name) / child.name))
diff --git a/mbgui/tkapp.py b/mbgui/tkapp.py
deleted file mode 100644
index fe16545..0000000
--- a/mbgui/tkapp.py
+++ /dev/null
@@ -1,59 +0,0 @@
-from tkinter import ttk
-import collections
-import concurrent.futures
-import threading
-import tkinter as tk
-import typing
-
-
-class TkApp:
-
- def __init__(self):
- self._root = tk.Tk()
- self._style = ttk.Style(self._root)
- self._executor = concurrent.futures.ThreadPoolExecutor()
- self._call_main_queue = collections.deque()
- self._call_main_lock = threading.Lock()
-
- self._root.bind('<<AppCall>>', self._on_app_call)
-
- @property
- def root(self) -> tk.Tk:
- return self._root
-
- @property
- def style(self) -> ttk.Style:
- return self._style
-
- def call_main(self,
- fn: typing.Callable,
- *args,
- **kwargs):
- with self._call_main_lock:
- self._call_main_queue.append((fn, args, kwargs))
- self._root.event_generate('<<AppCall>>')
-
- def call_worker(self,
- fn: typing.Callable,
- *args,
- **kwargs
- ) -> concurrent.futures.Future:
- return self._executor.submit(fn, *args, **kwargs)
-
- def run(self):
- try:
- self._root.mainloop()
-
- finally:
- self._executor.shutdown()
-
- def _on_app_call(self, evt):
- with self._call_main_lock:
- if not self._call_main_queue:
- return
-
- queue = self._call_main_queue
- self._call_main_queue = collections.deque()
-
- for fn, args, kwargs in queue:
- fn(*args, **kwargs)
diff --git a/setup.py b/setup.py
index 3e29191..64c4efd 100644
--- a/setup.py
+++ b/setup.py
@@ -7,12 +7,13 @@ readme = (Path(__file__).parent / 'README.rst').read_text()
setup(
name='mbgui',
- version='0.0.1',
+ version='0.1.0',
description='Maildir GUI based on mblaze',
long_description=readme,
long_description_content_type='text/x-rst',
url='https://github.com/bozokopic/mbgui',
packages=['mbgui'],
+ install_requires=['PySide6'],
package_data={'mbgui': ['icons/*.png']},
license='GPLv3',
classifiers=[