Skip to content

Commit

Permalink
support directory structure for Mac and Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
johnwangwyx committed May 19, 2024
1 parent d6f948e commit 41c3f10
Show file tree
Hide file tree
Showing 13 changed files with 119 additions and 34 deletions.
18 changes: 14 additions & 4 deletions .github/workflows/mac_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:

jobs:
build:
runs-on: macos-13
runs-on: macos-12
steps:
- uses: actions/checkout@v3
name: Check out the repository code
Expand All @@ -32,15 +32,25 @@ jobs:
- name: Build the executable with PyInstaller
run: |
pyinstaller --noconfirm --onedir --windowed --icon "./cmdcompass/static/icon.icns" --name "cmdCompass" \
--add-data "./cmdcompass/data/man_pages:data/man_pages" \
--add-data "./cmdcompass/data/man_pages:data" \
--add-data "./cmdcompass/static:static" \
--add-data "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/customtkinter:customtkinter" \
--collect-all "tkinterweb" \
--distpath "./dist" \
"./cmdcompass/main.py"
- name: Reorganize Files
run: |
mv dist/cmdCompass/_internal/static dist/
mv dist/cmdCompass/_internal/data dist/
rm -rf dist/cmdCompass/
- name: Create tar.gz Archive
run: |
tar -czvf dist/cmdCompass.tar.gz -C dist cmdCompass.app static data
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: executable
path: dist/*
name: cmdCompass-artifacts
path: dist/cmdCompass.tar.gz
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ cmdcompass/data/man_pages/html_download
cmdcompass/data/man_pages/man
cmdcompass/data/man_pages/tmp
build/
dist/
dist/
*.spec
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@
* Add a button to create a command for the current selected collection.
* Add extract_options() for command model class to extract options(with -- or -) as a list.
* Add unit tests for extract_options() and get_template_variables() for the command model class.
* Add GitHub Action to build executable on Mac

### CHANGED
* Use static images for some buttons
* Use static images for some buttons.
* GitHub Action for Mac build now build on MACOS 12 to support for more systems.

### DEBUGGED
* Fix `extract_options` that was unable to identify options start with `--` but contains `-` like `--convert-links`.

## Version 0.1.0 (Initial Development)

Expand Down
12 changes: 10 additions & 2 deletions cmdcompass/datamanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@
from cmdcompass.models.collection import Collection
from cmdcompass.models.command import Command
from cmdcompass.models.tag import Tag

from cmdcompass.utils.utils import get_current_working_dir
import sys
import os

if getattr(sys, 'frozen', False):
BASE_DIR = get_current_working_dir()
else:
BASE_DIR = "."
DATA_FILE = os.path.join(BASE_DIR, "data", "data.json")
class DataManager:
def __init__(self, data_file="./data/data.json"):
def __init__(self, data_file=DATA_FILE):
self.data_file = data_file
self.data: Dict[str, Collection] = {} # {collection_name: Collection object}
self.load_data()
Expand Down
3 changes: 2 additions & 1 deletion cmdcompass/gui/commandbodybox.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ def save_command(self):
self.save_button.configure(state="disabled", fg_color="gray")
self.main_window.refresh_command_list() # Refresh the command list
self.main_window.utility_box.set_command(self.main_window.selected_command)
self.main_window.man_page_box.set_man_page(self.main_window.selected_command)
if self.main_window.active_tab == "man_page":
self.main_window.man_page_box.set_man_page(self.main_window.selected_command)

def copy_command(self):
command_str = self.command_textbox.get("1.0", "end-1c")
Expand Down
14 changes: 10 additions & 4 deletions cmdcompass/gui/manpagebox.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import customtkinter as ctk
from tkinterweb import HtmlFrame
import os
from cmdcompass.utils.utils import get_command_name, highlight_options
from cmdcompass.utils.utils import get_command_name, highlight_options, get_current_working_dir
from cmdcompass.man_parser.loader import download_and_process_package
from cmdcompass.man_parser.html_coverter import OUTPUT_DIR
from cmdcompass.gui.progresswindow import ProgressWindow
from tkinter import ttk
import sys

HTML_CORE_DIR = os.path.join('.', 'data', 'man_pages', 'html_core')
if getattr(sys, 'frozen', False):
BASE_DIR = get_current_working_dir()
else:
BASE_DIR = "."
HTML_CORE_DIR = os.path.join(BASE_DIR, 'data', 'man_pages', 'html_core')


class ManPageBox(ctk.CTkFrame):
def __init__(self, master, dark_theme_enabled=False, **kwargs):
def __init__(self, master, **kwargs):
super().__init__(master, **kwargs)
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
Expand Down Expand Up @@ -52,6 +57,7 @@ def set_man_page(self, command):
else:
def download_and_update():
progress_window = self.create_progress_window()
progress_window.update_progress(HTML_CORE_DIR)
progress_window.update_progress(f"Downloading {command_name}...")
download_and_process_package(command_name, progress_window)
# After download and processing is complete, update the HTML
Expand All @@ -70,7 +76,7 @@ def download_and_update():
print(f"Error getting man page: {e}")
self.html_content = html_content
self.options = command.extract_options()
self.apply_with_highlight()
self.change_theme()
if self.options:
self.highlight_switch.grid(row=0, column=0, pady=(10, 0), sticky="w")
else:
Expand Down
12 changes: 11 additions & 1 deletion cmdcompass/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
from cmdcompass.gui.main_window import MainWindow
from cmdcompass.utils.utils import get_current_working_dir
import os
import platform

if __name__ == "__main__":
app = MainWindow()
app.lift()
app.attributes('-topmost', True)
app.after_idle(app.attributes, '-topmost', False)
app.iconbitmap(os.path.join(".", "static", "icon.ico"))
if platform.system() == "Windows":
icon_path = os.path.join(".", "static", "icon.ico")
elif platform.system() == "Darwin": # Darwin is the system name for macOS
icon_path = os.path.join(get_current_working_dir(), "static", "icon.icns")
else:
icon_path = None

if icon_path:
app.iconbitmap(icon_path)
app.mainloop()
26 changes: 23 additions & 3 deletions cmdcompass/man_parser/html_coverter.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import os
import subprocess
import platform
import sys
from cmdcompass.utils.utils import get_current_working_dir
import shutil
from pathlib import Path

WINDOWS_GROFF_BIN = os.path.join('.', 'data', 'bin', 'groff-1.22.4-w32-bin', 'bin', 'groff.exe')
OUTPUT_DIR = os.path.join('.', 'data', 'man_pages', 'html_download')
if getattr(sys, 'frozen', False):
BASE_DIR = get_current_working_dir()
else:
BASE_DIR = "."
WINDOWS_GROFF_BIN = os.path.join(BASE_DIR, 'data', 'bin', 'groff-1.22.4-w32-bin', 'bin', 'groff.exe')
OUTPUT_DIR = os.path.join(BASE_DIR, 'data', 'man_pages', 'html_download')
CREATE_NO_WINDOW = 0x08000000 # To used on Windows to not open any terminal window while using subprocess

def convert_man_pages(man_file, output_dir=OUTPUT_DIR):
Expand All @@ -16,7 +24,19 @@ def convert_man_pages(man_file, output_dir=OUTPUT_DIR):
if platform.system() == 'Windows':
groff_command = [WINDOWS_GROFF_BIN, '-mandoc', '-Thtml', man_file]
else:
groff_command = ['groff', '-mandoc', '-Thtml', man_file]
# Try to locate 'groff' using 'which' command
# As per https://pyinstaller.org/en/stable/common-issues-and-pitfalls.html#macos,
# If the app is run from Finder, the app may have a reduced set of PATH vars, which may not include groff
groff_path = shutil.which('groff')
if not groff_path:
# Fallback to common paths if not found
for path in ['/usr/local/bin/groff', '/opt/homebrew/bin/groff']:
if Path(path).exists():
groff_path = path
break
if groff_path is None:
raise FileNotFoundError("The 'groff' command is not available in the system PATH.")
groff_command = [groff_path, '-mandoc', '-Thtml', man_file]

base_name = os.path.basename(man_file) # Get the file name without the directory path
name_without_ext = os.path.splitext(base_name)[0] # Remove extension
Expand Down
16 changes: 11 additions & 5 deletions cmdcompass/man_parser/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@
import re
from sqlitedict import SqliteDict
from cmdcompass.man_parser.html_coverter import convert_man_pages
from cmdcompass.utils.utils import get_current_working_dir
import platform
import sys

ARCH = "binary-amd64"
DEB_PACKAGE_URL = f"https://ftp.debian.org/debian/dists/Debian12.5/main/{ARCH}/Packages.gz"

UNPACKING_DIR = os.path.join('.', 'data', 'man_pages', 'tmp')
MAN_PAGE_DIR = os.path.join('.', 'data', 'man_pages', 'man')
KV_DB_PATH = os.path.join('.', 'data', 'man_pages_kv.db')
if getattr(sys, 'frozen', False):
BASE_DIR = get_current_working_dir()
else:
BASE_DIR = "."
UNPACKING_DIR = os.path.join(BASE_DIR, 'data', 'man_pages', 'tmp')
MAN_PAGE_DIR = os.path.join(BASE_DIR, 'data', 'man_pages', 'man')
KV_DB_PATH = os.path.join(BASE_DIR, 'data', 'man_pages_kv.db')

DEB_DOWNLOAD_LINK = "http://ftp.ca.debian.org/debian"

Expand All @@ -23,7 +29,6 @@ def extract_deb(data_path, extract_to):
print(f"Extracting DEB package: {data_path} to {extract_to}")
if not os.path.exists(extract_to):
os.makedirs(extract_to)

# Open the DEB file as an AR archive
with arpy.Archive(data_path) as archive:
archive.read_all_headers()
Expand Down Expand Up @@ -105,10 +110,11 @@ def download_and_process_package(package_name, progress_window):
# Ensure destination folders exist
if not os.path.exists(UNPACKING_DIR):
os.makedirs(UNPACKING_DIR)

progress_window.update_progress(f"read from {KV_DB_PATH}")
with SqliteDict(KV_DB_PATH) as db:
if package_name in db:
try:
progress_window.update_progress(f"found")
# Note the db value can contain a list of package that includes the command man page.
# The value is in tuple of ("package_link", #number of man pages in this package) and ordered by the #number
# We can have a feature to allow user to choose which package to use
Expand Down
6 changes: 3 additions & 3 deletions cmdcompass/models/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,14 @@ def replace_var(match):
return re.sub(pattern, replace_var, self.command_str)

def extract_options(self):
command = self.command_str
options = set()
# Split the command into parts respecting quotes
parts = shlex.split(command)
parts = shlex.split(self.command_str)

for part in parts:
# Handle long-form options starting with --
if part.startswith('--'):
match = re.match(r'--(\w+)', part)
match = re.match(r'--([\w-]+)', part)
if match:
options.add(match.group(1))

Expand All @@ -51,3 +50,4 @@ def extract_options(self):
options.update(match.group(1))

return options

35 changes: 28 additions & 7 deletions cmdcompass/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
import os
from PIL import Image
import customtkinter as ctk
import sys

IMG_DIR = os.path.join(".", "static")
from pathlib import Path
import platform

IMG_DIR = "static"

def get_command_name(command_str):
# Regular expression to match environment variables with optional quotes around the value
Expand All @@ -19,8 +23,25 @@ def get_command_name(command_str):
raise ValueError(f"Unable to find the command name for {command_str}")


def get_current_working_dir():
# Get the working dir when app is built as an executable.
if getattr(sys, 'frozen', False) and platform.system() == "Darwin":
# sys.executable in a PyInstaller bundle on macOS points to the MacOS executable
# This resolves to MyApp.app/Contents/MacOS/MyApp.
# So when user lick on Myapp.app, the real executable is 3 directory down.
app_path = Path(sys.executable).resolve()
# Navigate three levels up to get to the .app's parent directory
application_root = app_path.parents[3]
else:
# If running as a normal script
application_root = "."

return application_root


def load_ctk_image(image_file_name, **kwargs):
image_path = os.path.join(IMG_DIR, image_file_name)
img_dir = os.path.join(get_current_working_dir(), IMG_DIR)
image_path = os.path.join(img_dir, image_file_name)
return ctk.CTkImage(light_image=Image.open(image_path), **kwargs)


Expand All @@ -33,17 +54,17 @@ def highlight_options(options, html, highlight_class="highlight"):

for option in options:
if len(option) == 1:
# For single-letter options, expect them to be followed by a specific character like "<"
pattern = r'(-{})<'.format(re.escape(option))
# For single-letter options, match both '-' and '&minus;'
pattern = r'([-&minus;]{})<'.format(re.escape(option))
# Replace the pattern in the HTML with a highlighted version using <span> with inline style
replacement = r'<span style="background-color:{};">\1</span><'.format(highlighted_color)
else:
# For multi-letter options, just match the option itself with "--"
pattern = r'(--{})'.format(re.escape(option))
# For multi-letter options, match both '--' and '&minus;&minus;' and handle hyphens in option names
option_pattern = re.escape(option).replace('-', '[-&minus;]')
pattern = r'([-&minus;][-&minus;]{})'.format(option_pattern)
# Replace the pattern in the HTML with a highlighted version using <span> with inline style
replacement = r'<span style="background-color:{};">\1</span>'.format(highlighted_color)

highlighted_html = re.sub(pattern, replacement, highlighted_html)

return highlighted_html

1 change: 0 additions & 1 deletion google312c553fd212bb76.html

This file was deleted.

2 changes: 1 addition & 1 deletion test/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def test_extract_options_wget(self):
comment="Download a website with wget",
tag_ids=["tag2"]
)
self.assertEqual(command.extract_options(), {'mirror', 'p', 'convert', 'P'})
self.assertEqual(command.extract_options(), {'mirror', 'p', 'convert-links', 'P'})

def test_extract_options_docker(self):
command = Command(
Expand Down

0 comments on commit 41c3f10

Please sign in to comment.