Compare commits
1 Commits
v1.0.0
...
d8c2a6604e
| Author | SHA1 | Date | |
|---|---|---|---|
| d8c2a6604e |
102
.github/workflows/build.yml
vendored
102
.github/workflows/build.yml
vendored
@@ -1,102 +0,0 @@
|
||||
name: "Build and Package"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
FORCE_COLOR: "1"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
package:
|
||||
name: Build and Package
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: [ "macOS" ]
|
||||
include:
|
||||
- target: "macOS"
|
||||
output-format: "app"
|
||||
runs-on: "macos-latest"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Install Python 3.13
|
||||
run: uv python install 3.13
|
||||
|
||||
- name: Setup Environment
|
||||
run: uv sync
|
||||
|
||||
- name: Build App
|
||||
run: |
|
||||
${{ matrix.briefcase-build-prefix }} \
|
||||
uv run briefcase build \
|
||||
${{ matrix.platform || matrix.target }} \
|
||||
${{ matrix.output-format }} \
|
||||
--test --no-input --log \
|
||||
${{ matrix.briefcase-args }} \
|
||||
${{ matrix.briefcase-build-args }}
|
||||
|
||||
- name: Package App
|
||||
run: |
|
||||
${{ matrix.briefcase-package-prefix }} \
|
||||
uv run briefcase package \
|
||||
${{ matrix.platform || matrix.target }} \
|
||||
${{ matrix.output-format }} \
|
||||
--update --adhoc-sign --no-input --log \
|
||||
${{ matrix.briefcase-args }} \
|
||||
${{ matrix.briefcase-package-args }}
|
||||
|
||||
- name: Upload App
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: App-${{ matrix.target }}
|
||||
path: dist
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Log
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Log-Failure-${{ matrix.target }}
|
||||
path: logs/*
|
||||
|
||||
|
||||
release:
|
||||
name: "Upload Release"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
needs:
|
||||
- package
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download macOS build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: App-macOS
|
||||
path: dist/
|
||||
|
||||
- name: Display all files
|
||||
run: ls -R
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: dist/*
|
||||
fail_on_unmatched_files: true
|
||||
41
README.md
41
README.md
@@ -1,42 +1,3 @@
|
||||
# illogical
|
||||
|
||||

|
||||
|
||||
The sane Logic Pro plugin manager Apple forgot to build.
|
||||
|
||||
## Features
|
||||
|
||||
- Browse and organize Audio Units plugins
|
||||
- "Uncategorized" folder
|
||||
- Tree-like view for categories
|
||||
- Vim-style keyboard navigation (j/k/h/l)
|
||||
- Backup and restore plugin configurations
|
||||
- Native Plug-in manager feel with liquid glass UI
|
||||
|
||||
## Install
|
||||
|
||||
**Homebrew** (coming soon)
|
||||
|
||||
**Manual**
|
||||
|
||||
1. Download the latest `.dmg` from [Releases](https://github.com/kotikotprojects/illogical/releases).
|
||||
2. Drag the app to the **Applications** folder.
|
||||
3. **Right-click** the app and select **Open**. (You only need to do this once).
|
||||
|
||||
⚠️ **If you see "App is damaged" error:**
|
||||
Run this command in Terminal:
|
||||
```bash
|
||||
xattr -cr /Applications/illogical.app
|
||||
```
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `J/K` | Navigate up/down |
|
||||
| `H/L` | Collapse/expand |
|
||||
| `Shift+V` | Visual line mode |
|
||||
| `Cmd+F` | Search |
|
||||
| `Cmd+B` | Backup |
|
||||
| `Cmd+Shift+R` | Restore |
|
||||
| `Cmd+Shift+/` | Show all shortcuts |
|
||||
The sane Logic Pro plugin manager Apple forgot to build
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
[tool.briefcase]
|
||||
project_name = "illogical"
|
||||
bundle = "com.kotikot.illogical"
|
||||
version = "1.0.0"
|
||||
version = "0.0.1"
|
||||
url = "https://dsp.kotikot.com/illogical"
|
||||
license.file = "LICENSE"
|
||||
author = "h"
|
||||
author_email = "h@kotikot.com"
|
||||
icon = "src/illogical/resources/icon"
|
||||
|
||||
[tool.briefcase.app.illogical]
|
||||
formal_name = "illogical"
|
||||
|
||||
2
src/illogical/resources/README
Normal file
2
src/illogical/resources/README
Normal file
@@ -0,0 +1,2 @@
|
||||
Put any application resources (e.g., icons and resources) here;
|
||||
they can be referenced in code as "resources/filename".
|
||||
Binary file not shown.
@@ -17,7 +17,6 @@ from illogical.ui.loading_overlay import LoadingOverlay
|
||||
from illogical.ui.menu_bar import MenuBar
|
||||
from illogical.ui.plugin_table import PluginTableView
|
||||
from illogical.ui.restore_backup_window import RestoreBackupWindow
|
||||
from illogical.ui.shortcuts_help_window import ShortcutsHelpWindow
|
||||
from illogical.ui.sidebar import Sidebar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -57,7 +56,6 @@ class MainWindow(QMainWindow):
|
||||
self._menu_bar.backup_now_triggered.connect(self._on_backup_now)
|
||||
self._menu_bar.restore_backup_triggered.connect(self._on_restore_backup)
|
||||
self._menu_bar.backup_settings_triggered.connect(self._on_backup_settings)
|
||||
self._menu_bar.shortcuts_help_triggered.connect(self._show_shortcuts_help)
|
||||
|
||||
def _setup_ui(self) -> None:
|
||||
self._central = QWidget()
|
||||
@@ -115,7 +113,6 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self._restore_window: RestoreBackupWindow | None = None
|
||||
self._settings_window: BackupSettingsWindow | None = None
|
||||
self._shortcuts_window: ShortcutsHelpWindow | None = None
|
||||
|
||||
def showEvent(self, event: QShowEvent) -> None: # noqa: N802
|
||||
super().showEvent(event)
|
||||
@@ -473,16 +470,6 @@ class MainWindow(QMainWindow):
|
||||
self._sidebar.clear_manufacturer_search()
|
||||
self._sidebar.select_show_all()
|
||||
|
||||
def _show_shortcuts_help(self) -> None:
|
||||
if self._shortcuts_window is not None:
|
||||
self._shortcuts_window.close()
|
||||
self._shortcuts_window = ShortcutsHelpWindow()
|
||||
self._shortcuts_window.destroyed.connect(self._on_shortcuts_window_destroyed)
|
||||
self._shortcuts_window.show_centered(self)
|
||||
|
||||
def _on_shortcuts_window_destroyed(self) -> None:
|
||||
self._shortcuts_window = None
|
||||
|
||||
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
|
||||
self._service.shutdown()
|
||||
self._backup_service.shutdown()
|
||||
|
||||
@@ -14,7 +14,6 @@ class MenuBar(QMenuBar):
|
||||
backup_now_triggered = Signal()
|
||||
restore_backup_triggered = Signal()
|
||||
backup_settings_triggered = Signal()
|
||||
shortcuts_help_triggered = Signal()
|
||||
|
||||
def __init__(self, main_window: QMainWindow | None = None) -> None:
|
||||
super().__init__()
|
||||
@@ -91,9 +90,4 @@ class MenuBar(QMenuBar):
|
||||
backup_menu.addAction(settings_action)
|
||||
|
||||
def _setup_help_menu(self) -> None:
|
||||
help_menu = self.addMenu("Help")
|
||||
|
||||
shortcuts_action = QAction("Keyboard Shortcuts", self)
|
||||
shortcuts_action.setShortcut(QKeySequence("Ctrl+Shift+/"))
|
||||
shortcuts_action.triggered.connect(self.shortcuts_help_triggered)
|
||||
help_menu.addAction(shortcuts_action)
|
||||
self.addMenu("Help")
|
||||
|
||||
@@ -35,18 +35,15 @@ if TYPE_CHECKING:
|
||||
|
||||
KVK_J = 0x26
|
||||
KVK_K = 0x28
|
||||
KVK_V = 0x09
|
||||
|
||||
|
||||
class _VimTableView(QTableView):
|
||||
enter_pressed = Signal()
|
||||
context_menu_requested = Signal(QPoint)
|
||||
visual_mode_changed = Signal(bool)
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._anchor_row: int | None = None
|
||||
self._visual_line_mode: bool = False
|
||||
self.setDragEnabled(True)
|
||||
self.setDragDropMode(QAbstractItemView.DragDropMode.DragOnly)
|
||||
self.setDefaultDropAction(Qt.DropAction.CopyAction)
|
||||
@@ -116,29 +113,7 @@ class _VimTableView(QTableView):
|
||||
index, self.selectionModel().SelectionFlag.NoUpdate
|
||||
)
|
||||
|
||||
def enter_visual_line_mode(self) -> None:
|
||||
if self._visual_line_mode:
|
||||
return
|
||||
self._visual_line_mode = True
|
||||
current = self.currentIndex()
|
||||
if current.isValid():
|
||||
self._anchor_row = current.row()
|
||||
self._select_row(current.row())
|
||||
self.visual_mode_changed.emit(True) # noqa: FBT003
|
||||
|
||||
def exit_visual_line_mode(self) -> None:
|
||||
if not self._visual_line_mode:
|
||||
return
|
||||
self._visual_line_mode = False
|
||||
self.visual_mode_changed.emit(False) # noqa: FBT003
|
||||
|
||||
def is_visual_line_mode(self) -> bool:
|
||||
return self._visual_line_mode
|
||||
|
||||
def mousePressEvent(self, event: QMouseEvent) -> None: # noqa: N802
|
||||
if self._visual_line_mode:
|
||||
self.exit_visual_line_mode()
|
||||
|
||||
index = self.indexAt(event.position().toPoint())
|
||||
if not index.isValid():
|
||||
super().mousePressEvent(event)
|
||||
@@ -179,7 +154,7 @@ class _VimTableView(QTableView):
|
||||
self._anchor_row = index.row()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802, C901, PLR0911, PLR0912
|
||||
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
|
||||
vk = event.nativeVirtualKey()
|
||||
key = event.key()
|
||||
mods = event.modifiers()
|
||||
@@ -187,19 +162,6 @@ class _VimTableView(QTableView):
|
||||
has_alt = bool(mods & Qt.KeyboardModifier.AltModifier)
|
||||
has_ctrl = bool(mods & Qt.KeyboardModifier.ControlModifier)
|
||||
|
||||
if key == Qt.Key.Key_Escape and self._visual_line_mode:
|
||||
self.exit_visual_line_mode()
|
||||
current = self.currentIndex()
|
||||
if current.isValid():
|
||||
self._select_row(current.row())
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if has_shift and vk == KVK_V:
|
||||
self.enter_visual_line_mode()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if has_ctrl and key == Qt.Key.Key_A:
|
||||
self._select_all_rows()
|
||||
event.accept()
|
||||
@@ -210,13 +172,11 @@ class _VimTableView(QTableView):
|
||||
event.accept()
|
||||
return
|
||||
|
||||
extend_selection = has_shift or self._visual_line_mode
|
||||
|
||||
if vk == KVK_J or key == Qt.Key.Key_Down:
|
||||
current = self.currentIndex()
|
||||
if current.row() < self.model().rowCount() - 1:
|
||||
new_row = current.row() + 1
|
||||
if extend_selection:
|
||||
if has_shift:
|
||||
self._extend_selection_to(new_row)
|
||||
else:
|
||||
self._select_row(new_row)
|
||||
@@ -226,7 +186,7 @@ class _VimTableView(QTableView):
|
||||
current = self.currentIndex()
|
||||
if current.row() > 0:
|
||||
new_row = current.row() - 1
|
||||
if extend_selection:
|
||||
if has_shift:
|
||||
self._extend_selection_to(new_row)
|
||||
else:
|
||||
self._select_row(new_row)
|
||||
@@ -256,7 +216,6 @@ class PluginTableView(QWidget):
|
||||
edit_requested = Signal(object, int, str)
|
||||
category_assignment_requested = Signal(list, str, bool)
|
||||
category_removal_requested = Signal(list)
|
||||
visual_mode_changed = Signal(bool)
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
@@ -326,7 +285,6 @@ class PluginTableView(QWidget):
|
||||
self._table.selectionModel().currentChanged.connect(self._on_current_changed)
|
||||
self._table.enter_pressed.connect(self._on_enter_pressed)
|
||||
self._table.context_menu_requested.connect(self._on_context_menu_requested)
|
||||
self._table.visual_mode_changed.connect(self.visual_mode_changed)
|
||||
|
||||
def _on_current_changed(self, current: QModelIndex, _previous: QModelIndex) -> None:
|
||||
if current.isValid():
|
||||
@@ -515,9 +473,6 @@ class PluginTableView(QWidget):
|
||||
if plugin:
|
||||
self.plugin_selected.emit(plugin)
|
||||
|
||||
def is_visual_line_mode(self) -> bool:
|
||||
return self._table.is_visual_line_mode()
|
||||
|
||||
def _on_search_escape(self) -> None:
|
||||
self._table.setFocus()
|
||||
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pyqt_liquidglass as glass
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtWidgets import QGridLayout, QHBoxLayout, QLabel, QVBoxLayout, QWidget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PySide6.QtGui import QKeyEvent
|
||||
|
||||
SHORTCUTS: dict[str, list[tuple[str, str]]] = {
|
||||
"Navigation": [
|
||||
("J / ↓", "Move down"),
|
||||
("K / ↑", "Move up"),
|
||||
("H / ←", "Collapse (categories)"),
|
||||
("L / →", "Expand (categories)"),
|
||||
("↩", "Enter category"),
|
||||
],
|
||||
"Visual Mode": [("⇧V", "Select lines"), ("Esc", "Exit mode")],
|
||||
"Quick Access": [
|
||||
("⌘1", "Show All"),
|
||||
("⌘2", "Uncategorized"),
|
||||
("⌘3", "Focus categories"),
|
||||
("⌘4", "Focus manufacturers"),
|
||||
("⌘F", "Search"),
|
||||
],
|
||||
"Actions": [
|
||||
("⌥↩", "Context menu"),
|
||||
("⌥⇧↑", "Move category up"),
|
||||
("⌥⇧↓", "Move category down"),
|
||||
("⌘A", "Select all"),
|
||||
],
|
||||
"Drag & Drop": [("⇧+Drop", "Add (don't move)")],
|
||||
"Backup": [("⌘B", "Create backup"), ("⌘⇧R", "Restore")],
|
||||
}
|
||||
|
||||
|
||||
class ShortcutsHelpWindow(QWidget):
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._setup_window()
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_window(self) -> None:
|
||||
self.setWindowTitle("Keyboard Shortcuts")
|
||||
self.setWindowFlags(Qt.WindowType.Tool | Qt.WindowType.WindowStaysOnTopHint)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
||||
|
||||
def _setup_ui(self) -> None:
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(40, 36, 40, 32)
|
||||
layout.setSpacing(24)
|
||||
|
||||
title = QLabel("Keyboard Shortcuts")
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
title.setStyleSheet("""
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: transparent;
|
||||
""")
|
||||
layout.addWidget(title)
|
||||
|
||||
grid = QGridLayout()
|
||||
grid.setSpacing(32)
|
||||
grid.setColumnStretch(0, 1)
|
||||
grid.setColumnStretch(1, 1)
|
||||
|
||||
sections = list(SHORTCUTS.items())
|
||||
for i, (section_name, shortcuts) in enumerate(sections):
|
||||
col = i % 2
|
||||
row = i // 2
|
||||
section_widget = self._create_section(section_name, shortcuts)
|
||||
grid.addWidget(section_widget, row, col, Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
layout.addLayout(grid)
|
||||
layout.addStretch()
|
||||
|
||||
hint = QLabel("Press Esc to close")
|
||||
hint.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
hint.setStyleSheet("""
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
background: transparent;
|
||||
""")
|
||||
layout.addWidget(hint)
|
||||
|
||||
def _create_section(self, title: str, shortcuts: list[tuple[str, str]]) -> QWidget:
|
||||
section = QWidget()
|
||||
section.setStyleSheet("background: transparent;")
|
||||
layout = QVBoxLayout(section)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(10)
|
||||
|
||||
header = QLabel(title)
|
||||
header.setStyleSheet("""
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background: transparent;
|
||||
padding-bottom: 4px;
|
||||
""")
|
||||
layout.addWidget(header)
|
||||
|
||||
for key, description in shortcuts:
|
||||
row = self._create_shortcut_row(key, description)
|
||||
layout.addWidget(row)
|
||||
|
||||
return section
|
||||
|
||||
def _create_shortcut_row(self, key: str, description: str) -> QWidget:
|
||||
row = QWidget()
|
||||
row.setStyleSheet("background: transparent;")
|
||||
layout = QHBoxLayout(row)
|
||||
layout.setContentsMargins(0, 4, 0, 4)
|
||||
layout.setSpacing(14)
|
||||
|
||||
key_label = QLabel(key)
|
||||
key_label.setStyleSheet("""
|
||||
font-size: 13px;
|
||||
font-family: 'SF Mono', 'Menlo', monospace;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 5px;
|
||||
padding: 5px 10px;
|
||||
""")
|
||||
|
||||
desc_label = QLabel(description)
|
||||
desc_label.setStyleSheet("""
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
background: transparent;
|
||||
""")
|
||||
|
||||
layout.addWidget(key_label)
|
||||
layout.addWidget(desc_label, 1)
|
||||
|
||||
return row
|
||||
|
||||
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
|
||||
if event.key() == Qt.Key.Key_Escape:
|
||||
self.close()
|
||||
event.accept()
|
||||
return
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def focusOutEvent(self, _event: object) -> None: # noqa: N802
|
||||
self.close()
|
||||
|
||||
def show_centered(self, parent: QWidget | None = None) -> None:
|
||||
glass.prepare_window_for_glass(self, frameless=True)
|
||||
self.adjustSize()
|
||||
self.show()
|
||||
self.activateWindow()
|
||||
self.setFocus()
|
||||
|
||||
if parent:
|
||||
parent_geo = parent.geometry()
|
||||
x = parent_geo.center().x() - self.width() // 2
|
||||
y = parent_geo.center().y() - self.height() // 2
|
||||
self.move(x, y)
|
||||
|
||||
QTimer.singleShot(0, self._apply_glass)
|
||||
|
||||
def _apply_glass(self) -> None:
|
||||
glass.apply_glass_to_window(
|
||||
self, options=glass.GlassOptions(corner_radius=16.0)
|
||||
)
|
||||
Reference in New Issue
Block a user