Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8aea6b0
Create pytest.yml
Adam-Color Apr 17, 2026
09efcd8
Update pytest.yml
Adam-Color Apr 17, 2026
dcfe088
Update pytest.yml
Adam-Color Apr 17, 2026
9d09ae4
Update pytest.yml
Adam-Color Apr 17, 2026
4e8dbed
Add 'Working pytest workflow' to TODO list
Adam-Color Apr 17, 2026
b096a79
Remove pytesting
Adam-Color Apr 18, 2026
11f8ae2
Linux compatability fix
Adam-Color Apr 18, 2026
d687ee2
Merge branch 'Develop' of https://github.com/Adam-Color/AppUsageGUI i…
Adam-Color Apr 18, 2026
b400175
switched to isDark()
Adam-Color Apr 18, 2026
44d4c4b
fix icon for linux
Adam-Color Apr 18, 2026
6a599ed
fix photoimage
Adam-Color Apr 18, 2026
6b57927
added force darkmode setting
Adam-Color Apr 18, 2026
f968b38
force_dark now requires restart
Adam-Color Apr 18, 2026
7015c6e
copyright info
Adam-Color Apr 19, 2026
e9cb82a
Update build.py
Adam-Color Apr 19, 2026
50e9c44
Update build.py
Adam-Color Apr 20, 2026
341f9fe
show_logs now just uses OS default log view flow
Adam-Color Apr 20, 2026
4df7c3d
fix darkmode support on windows
Adam-Color Apr 20, 2026
a3bb92b
Session and project name now displayed in TrackerWindow
Adam-Color Apr 24, 2026
bf9ae1a
Update save_window.py
Adam-Color Apr 24, 2026
bb4fcef
finalized new SaveWindow buttons
Adam-Color Apr 24, 2026
8a63701
Colorize most used buttons
Adam-Color Apr 24, 2026
2e6efc9
SessionsWindow now sorts session by last stop time by default
Adam-Color Apr 24, 2026
b2a2276
Removed button highlighting on macos
Adam-Color Apr 26, 2026
762ff5a
Update save_window.py
Adam-Color Apr 26, 2026
b197a91
added more highlighted buttons
Adam-Color Apr 26, 2026
c845196
versioning for 1.10.0
Adam-Color Apr 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
![version](https://img.shields.io/badge/Version-1.9.1-white.svg)
![version](https://img.shields.io/badge/Version-1.10.0-white.svg)
![license](https://img.shields.io/badge/License-GPL%20v3-blue.svg)
![python](https://img.shields.io/badge/Python-3.14t-green.svg)

Expand All @@ -19,6 +19,7 @@ To install, follow the instructions for your platform found here:

* Find a better way to filter out non-GUI apps
* Add integrations with professional applications
* Working pytest workflow
* Full linux support with packages

## Building
Expand Down
30 changes: 16 additions & 14 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,19 @@ def build_executable():

icon_file = "src/core/resources/icon.ico" if os.name == 'nt' else "src/core/resources/icon.icns"
version = get_version()

# Set environment variable in the current process
os.environ['PYTHONOPTIMIZE'] = '1'

print(f"Building {PROJECT_NAME} v{version}...")

windows_only_1 = '--collect-submodules pywinauto' if os.name == 'nt' else ""
version_file = ""

# On Windows, create a version file for the .exe
if os.name == 'nt':
version_file = create_version_file(version)

run_command(
f'{python_executable} -m PyInstaller -D --clean --name {PROJECT_NAME} '
f'--noconfirm '
Expand All @@ -56,8 +56,10 @@ def build_executable():
f'--collect-submodules pynput '
f'--collect-submodules requests '
f'--collect-submodules urllib3 '
f'--collect-submodules darkdetect '
f'--hidden-import=PIL.Image '
f'--hidden-import=PIL.ImageTk '
f'--hidden-import=PIL._tkinter_finder '
f'--collect-submodules pyperclip '
f'--exclude-module tkinter.test '
f'--exclude-module tkinter.demos '
Expand All @@ -71,7 +73,7 @@ def build_executable():
f'--add-data "src/_logging.py:." '
f'{ENTRY_POINT}'
)

# Clean up version file if created
if version_file and os.path.exists('version_info.txt'):
os.remove('version_info.txt')
Expand All @@ -81,11 +83,11 @@ def create_version_file(version):
version_parts = version.split('.')
while len(version_parts) < 4:
version_parts.append('0')

version_str = '.'.join(version_parts[:4])
# Convert to tuple format
version_tuple = tuple(int(x) for x in version_parts[:4])

version_content = f'''# UTF-8
#
# For more details about fixed file info 'ffi' see:
Expand All @@ -105,11 +107,11 @@ def create_version_file(version):
),
kids=[StringFileInfo(
[StringTable(u'040904B0',
[StringTable(u'CompanyName', u''),
[StringTable(u'CompanyName', u'Adam Color'),
StringTable(u'FileDescription', u'{PROJECT_NAME}'),
StringTable(u'FileVersion', u'{version_str}'),
StringTable(u'InternalName', u'{PROJECT_NAME}'),
StringTable(u'LegalCopyright', u''),
StringTable(u'LegalCopyright', u'Copyright © 2026 Adam Blair-Smith'),
StringTable(u'OriginalFilename', u'{PROJECT_NAME}.exe'),
StringTable(u'ProductName', u'{PROJECT_NAME}'),
StringTable(u'ProductVersion', u'{version_str}')])]),
Expand All @@ -127,7 +129,7 @@ def create_macos_app_bundle_info():
"""Create Info.plist for macOS .app bundle."""
version = get_version()
bundle_id = "com.appusagegui.app"

info_plist = f'''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
Expand All @@ -153,17 +155,17 @@ def create_macos_app_bundle_info():
<key>NSHighResolutionCapable</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2026. All rights reserved.</string>
<string>Copyright © 2026 Adam Blair-Smith. All rights reserved.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
'''

app_bundle = os.path.join(DIST_DIR, f"{PROJECT_NAME}.app")
contents_dir = os.path.join(app_bundle, "Contents")
os.makedirs(contents_dir, exist_ok=True)

info_plist_path = os.path.join(contents_dir, "Info.plist")
with open(info_plist_path, 'w') as f:
f.write(info_plist)
Expand Down
2 changes: 1 addition & 1 deletion dev/windows_installer.iss
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

#define MyAppName "AppUsageGUI"
#define MyAppPublisher "Adam Blair-Smith"
#define MyAppVersion "1.9.1"
#define MyAppVersion "1.10.0"
#define MyAppURL "https://github.com/Adam-Color/AppUsageGUI"
#define MyAppExeName "AppUsageGUI.exe"
#define MyInstallerName "AppUsageGUI_v" + MyAppVersion + "_WINDOWS_setup"
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ rubicon-objc==0.5.0
pyinstaller==6.19.0
requests==2.33.0
pillow==12.2.0
darkdetect
2 changes: 1 addition & 1 deletion src/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.9.1"
__version__ = "1.10.0"
110 changes: 8 additions & 102 deletions src/core/gui_root.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from tkinter import ttk
from core.utils.tk_utils import messagebox
import platform
import subprocess

from _version import __version__ as version
from _path import resource_path
Expand Down Expand Up @@ -193,109 +194,14 @@ def update_and_check(self, _=None):
messagebox.showinfo("Update", "No new updates available.")

def show_logs(self, _=None):
"""Display logs in a scrollable window with fixed footer buttons."""
# Prevent duplicate windows
if hasattr(self, "log_window") and self.log_window and self.log_window.winfo_exists():
self.log_window.lift()
self.log_window.focus_force()
return

win = tk.Toplevel(self.parent)
self.log_window = win # keep reference
win.title("Application Logs")
win.geometry("600x600")
win.transient(self.parent)
win.after(10, lambda: center_relative_to_parent(win, _main_window))

# Use grid instead of pack for better control
win.rowconfigure(0, weight=1)
win.columnconfigure(0, weight=1)

frame = ttk.Frame(win, padding=(8, 8, 8, 8))
frame.grid(row=0, column=0, sticky="nsew")
frame.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)

win.columnconfigure(0, weight=1)

frame = ttk.Frame(win, padding=(8, 8, 8, 8))
frame.grid(row=0, column=0, sticky="nsew")
frame.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)

# Header and text setup
header = (
f"=== {self.parent.title()} ===\n"
f"Python: {sys.version.split('(')[0]}\n"
f"Platform: {platform.system()} ({platform.machine()})\n"
f"{'=' * 21}\n"
f"NOTE: logs window only refreshes when reopened.\n\n"
)

# Read logs from the log file if available
log_contents = ""
"""Display logs in default file manager."""
if hasattr(self, "log_file_path") and os.path.exists(self.log_file_path):
try:
with open(self.log_file_path, "r") as log_file:
log_contents = log_file.read()
except Exception as e:
log_contents = f"(Failed to read log file: {e})\n"

# Combine header and logs
initial_text = header + (log_contents or "(No logs yet)\n")

text_box = tk.Text(frame, wrap="word")
text_box.insert("1.0", initial_text)
text_box.config(state="disabled")
text_box.grid(row=0, column=0, sticky="nsew")

scrollbar = ttk.Scrollbar(frame, orient="vertical", command=text_box.yview)
scrollbar.grid(row=0, column=1, sticky="ns")
text_box.config(yscrollcommand=scrollbar.set)

# Buttons at bottom (separate frame)
btn_frame = ttk.Frame(win)
btn_frame.grid(row=1, column=0, sticky="ew", pady=(6, 8))
btn_frame.columnconfigure(0, weight=1)
btn_frame.columnconfigure(1, weight=1)

def logs():
"""Read the log file and return its contents."""
try:
if os.path.exists(self.log_file_path):
with open(self.log_file_path, "r") as log_file:
return log_file.read()
else:
return "(Log file not found)"
except Exception as e:
return f"(Failed to read log file: {e})"

def copy_logs():
"""Copy the current logs to the clipboard."""
import pyperclip # type: ignore
try:
log_text = header + logs()
pyperclip.copy(log_text)
messagebox.showinfo("Copy Logs", "Logs copied to clipboard.")
except Exception as e:
messagebox.showerror("Copy Logs", f"Failed to copy logs: {e}")

ttk.Button(btn_frame, text="Copy Logs", command=copy_logs).grid(row=0, column=0, sticky="w", padx=(8, 0))
ttk.Button(btn_frame, text="Close", command=win.destroy).grid(row=0, column=1, sticky="e", padx=(0, 8))

# Live Updating
def refresh_logs():
"""Refresh the logs displayed in the logs window."""
try:
new_text = header + logs() # Use logs() to get log content
text_box.config(state="normal")
text_box.delete(1.0, "end") # Clear existing content
text_box.insert("end", new_text) # Insert new content
text_box.config(state="disabled") # Make it read-only
except Exception as e:
print(f"Failed to refresh logs: {e}")

refresh_logs()
if sys.platform == "win32":
os.startfile(self.log_file_path)
elif sys.platform == "darwin": # macOS
subprocess.call(('open', self.log_file_path))
else: # Linux/Unix
subprocess.call(('xdg-open', self.log_file_path))

def init_screens(self):
"""Pass the logic_controller when initializing screens"""
Expand Down
6 changes: 5 additions & 1 deletion src/core/logic/file_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ def save_session_data(self, data):
data["created_date"] = datetime.now().isoformat()
data["last_modified"] = datetime.now().isoformat()

logger.info(f"Saving session data: {data}")
logger.info("Saving session data...")

self.data = pickle.dumps(data)
logger.info(f"Data saved: {len(self.data)} bytes")

# Determine save directory based on project
if self.current_project:
Expand Down Expand Up @@ -161,6 +162,9 @@ def set_file_name(self, file_name):

def get_file_name(self):
return self.file_name

def get_project_name(self):
return self.current_project

def get_session_names(self):
return self.session_names
Expand Down
10 changes: 8 additions & 2 deletions src/core/screens/create_session_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,13 @@ def __init__(self, parent, controller, logic_controller):
button_frame.pack(fill="x", padx=20, pady=20)

# Confirm session name entry
confirm_button = tk.Button(button_frame, text="Create Session", command=self.on_confirm, width=15, height=1)
confirm_button = tk.Button(
button_frame,
text="Create Session",
command=self.on_confirm,
width=15, height=1,
bg="#0985d9"
)
confirm_button.pack(side="left", padx=5)

# Cancel button
Expand Down Expand Up @@ -137,7 +143,7 @@ def create_new_project(self):
vcmd = (dialog.register(validate_name), '%P')
project_name_var = tk.StringVar()
project_name_entry = tk.Entry(dialog, textvariable=project_name_var,
validate="key", validatecommand=vcmd, width=30,
validate="key", validatecommand=vcmd, width=30,
borderwidth=3, relief="groove")
project_name_entry.pack(pady=5)

Expand Down
3 changes: 2 additions & 1 deletion src/core/screens/projects_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ def __init__(self, parent, controller, logic_controller):

# Create new project button
create_button = tk.Button(button_frame, text="Create New Project",
command=self.create_project, width=20)
command=self.create_project, width=20,
bg="#0985d9")
create_button.pack(side="left", padx=5)


Expand Down
37 changes: 27 additions & 10 deletions src/core/screens/save_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,44 @@ def __init__(self, parent, controller, logic_controller):
tk.Frame.__init__(self, parent)
self.controller = controller
self.logic = logic_controller

# display the page label
self.page_label = tk.Label(self, text="Would you like to save the recorded data?")
self.page_label = tk.Label(
self,
text="\n\n\nWould you like to save the recorded data?",
font=("Arial", 20, "bold")
)
self.page_label.pack(pady=5)

button_frame = tk.Frame(self)
button_frame.pack(pady=5)

# display the yes/no buttons
button_yes = tk.Button(self, text="Yes", command=self.save)
button_yes.pack(pady=2)
button_no = tk.Button(self, text="No", command=self.dont_save)
button_no.pack(pady=5)
button_yes = tk.Button(button_frame, text="Yes",
command=self.save,
font=("Arial", 20),
width=4, height=1,
bg="#0985d9"
)
button_yes.pack(side='right', padx=5)
self.bind("<Return>", lambda event: self.save())
button_no = tk.Button(button_frame, text="No",
command=self.dont_save,
font=("Arial", 20),
width=4, height=1
)
button_no.pack(side='left', padx=5)

back_button = tk.Button(self, text="Main Menu", command=self.dont_save)
back_button.pack(pady=5, side='bottom')

def save(self):
time.sleep(0.3)

# Stop the time tracker before saving to ensure we have proper stop times
if self.logic.time_tracker.is_running():
self.logic.time_tracker.stop()

if self.logic.file_handler.get_continuing_session():
# Continuing an existing session - update it
session_time = self.logic.time_tracker.get_total_time()
Expand Down Expand Up @@ -83,15 +100,15 @@ def save(self):
logger.info(f"Session data: {data}")

self.logic.file_handler.save_session_data(data)

# Load the saved session data and show session total window
session_name = self.logic.file_handler.get_file_name()
project_name = self.logic.file_handler.get_current_project()
self.logic.file_handler.load_session_data(session_name, project_name)
self.controller.frames['SessionTotalWindow'].total_session_time_thread.start()
self.controller.frames['SessionTotalWindow'].update_total_time()
self.controller.show_frame("SessionTotalWindow")

def dont_save(self):
"""confirm data deletion"""
ans = messagebox.askyesno("AppUsageGUI", "Are you sure you don't want to save?")
Expand Down
3 changes: 2 additions & 1 deletion src/core/screens/select_app_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ def __init__(self, parent, controller, logic_controller):
text="Select",
command=self.select_app,
width=15, height=2,
font=("Arial", 10, "bold")
font=("Arial", 10, "bold"),
bg="#0985d9"
)
select_button.pack(side="right", padx=20)

Expand Down
Loading
Loading