Skip to content

Commit 48246dc

Browse files
update text bounds calculations and add logger
1 parent f4599b0 commit 48246dc

File tree

7 files changed

+295
-39
lines changed

7 files changed

+295
-39
lines changed

data/be.alexandervanhee.gradia.desktop.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[Desktop Entry]
22
Name=Gradia
33
Comment=Make your images ready for the world
4-
Exec=gradia %f
4+
Exec=gradia
55
Icon=be.alexandervanhee.gradia
66
Terminal=false
77
Type=Application

gradia/backend/__init__.py

Whitespace-only changes.

gradia/backend/logger.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# Copyright (C) 2022 Gradience Team
2+
# Copyright 2023-2025, tfuxu <https://github.com/tfuxu>
3+
# Copyright (C) 2025 Alexander Vanhee
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
#
18+
# SPDX-License-Identifier: GPL-3.0-or-later
19+
20+
import sys
21+
import logging
22+
import traceback
23+
24+
class Logger(logging.getLoggerClass()):
25+
"""
26+
This is a wrapper of `logging` module. It provides
27+
custom formatting for log messages.
28+
29+
Attributes
30+
----------
31+
issue_footer_levels : list, optional
32+
Custom list of levels on which to show issue footer.
33+
[Allowed values: warning, error, traceback_error, critical]
34+
formatter : str, optional
35+
Custom formatter for the logger.
36+
"""
37+
38+
log_colors = {
39+
"debug": 32,
40+
"info": 36,
41+
"warning": 33,
42+
"error": 31,
43+
"critical": 41
44+
}
45+
46+
log_format = {
47+
'fmt': '%(message)s'
48+
}
49+
50+
issue_footer = "If you are reporting an issue, please copy the logs printed above to the issue body."
51+
52+
issue_footer_levels = [
53+
"error",
54+
"traceback_error",
55+
"critical"
56+
]
57+
58+
def __set_traceback_info(self, exception: BaseException | None = None) -> str:
59+
if not exception:
60+
exception = sys.exc_info()[1]
61+
traceback = self.get_traceback(exception)
62+
63+
message_head = "\n\t\033[1mTraceback:\033[0m"
64+
message_body = f"\n\033[90m{traceback}\033[0m"
65+
message_body = message_body.replace("\n", "\n\t\t")
66+
67+
return message_head + message_body
68+
69+
def __set_exception_info(self, exception: BaseException | None = None) -> str:
70+
if not exception:
71+
exception = sys.exc_info()[1]
72+
73+
message_head = "\n\t\033[1mException:\033[0m"
74+
message_body = f"\n{exception}"
75+
message_body = message_body.replace("\n", "\n\t\t")
76+
77+
return message_head + message_body
78+
79+
def __set_level_color(self, level: str, message: str) -> str:
80+
color_id = self.log_colors[level]
81+
82+
return f"[\033[1;{color_id}m{level.upper()}\033[0m] {message}"
83+
84+
def __init__(self, issue_footer_levels: list | None = None, fmt: str | None = None):
85+
"""
86+
The constructor for Logger class.
87+
"""
88+
89+
super().__init__(name="Gradia")
90+
91+
if issue_footer_levels:
92+
self.issue_footer_levels = issue_footer_levels
93+
94+
formatter = logging.Formatter(self.log_format["fmt"])
95+
if fmt:
96+
formatter = logging.Formatter(fmt)
97+
98+
self.root.setLevel(logging.DEBUG)
99+
100+
self.root.handlers = []
101+
102+
handler = logging.StreamHandler()
103+
handler.setFormatter(formatter)
104+
self.root.addHandler(handler)
105+
106+
def debug(self, message: str, *args, **kwargs) -> None:
107+
self.root.debug(self.__set_level_color("debug", str(message)))
108+
109+
def info(self, message: str, *args, **kwargs) -> None:
110+
self.root.info(self.__set_level_color("info", str(message)))
111+
112+
def warning(self, message: str, exception: BaseException | None = None,
113+
show_exception: bool = False, show_traceback: bool = False, *args, **kwargs) -> None:
114+
if show_exception:
115+
message += self.__set_exception_info(exception)
116+
if show_traceback:
117+
message += self.__set_traceback_info(exception)
118+
119+
self.root.warning(self.__set_level_color("warning", str(message)))
120+
if "warning" in self.issue_footer_levels:
121+
self.print_issue_footer()
122+
123+
def error(self, message: str, exception: BaseException | None = None,
124+
show_exception: bool = False, show_traceback: bool = False, *args, **kwargs) -> None:
125+
if show_exception:
126+
message += self.__set_exception_info(exception)
127+
if show_traceback:
128+
message += self.__set_traceback_info(exception)
129+
130+
self.root.error(self.__set_level_color("error", str(message)))
131+
if "error" in self.issue_footer_levels:
132+
self.print_issue_footer()
133+
134+
def traceback_error(self, message: str, exception: BaseException | None = None,
135+
show_exception: bool = False) -> None:
136+
if show_exception:
137+
message += self.__set_exception_info(exception)
138+
message += self.__set_traceback_info(exception)
139+
140+
self.root.error(self.__set_level_color("error", str(message)))
141+
if "traceback_error" in self.issue_footer_levels:
142+
self.print_issue_footer()
143+
144+
def critical(self, message: str, exception: BaseException | None = None,
145+
show_exception: bool = False, show_traceback: bool = True, *args, **kwargs) -> None:
146+
if show_exception:
147+
message += self.__set_exception_info(exception)
148+
if show_traceback:
149+
message += self.__set_traceback_info(exception)
150+
151+
self.root.critical(self.__set_level_color("critical", str(message)))
152+
if "critical" in self.issue_footer_levels:
153+
self.print_issue_footer()
154+
155+
def set_silent(self) -> None:
156+
self.root.handlers = []
157+
158+
def print_issue_footer(self) -> None:
159+
self.root.info(self.__set_level_color("info", self.issue_footer))
160+
161+
def get_traceback(self, exception: BaseException | None) -> str | None:
162+
if not exception:
163+
return
164+
165+
traceback_list = traceback.format_exception(exception)
166+
exception_tb = "".join(traceback_list)
167+
168+
return exception_tb
169+
170+
171+
"""
172+
Use for testing only.
173+
How to execute: python -m gradia.backend.logger
174+
"""
175+
# pylint: disable=W0719,W0718,W0707
176+
if __name__ == "__main__":
177+
logging = Logger()
178+
179+
logging.info("This is an information.")
180+
logging.debug("This is a debug message.")
181+
182+
try:
183+
raise ArithmeticError("Arithmetic Error")
184+
except Exception:
185+
try:
186+
raise Exception("General Exception")
187+
except Exception as e:
188+
logging.traceback_error("This is an test error.", exception=e, show_exception=True)
189+
190+
print(f"Retrieved traceback: {logging.get_traceback(e)}")

gradia/main.py

Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# along with this program. If not, see <https://www.gnu.org/licenses/>.
1515
#
1616
# SPDX-License-Identifier: GPL-3.0-or-later
17+
1718
import sys
1819
import os
1920
import tempfile
@@ -25,7 +26,9 @@
2526
from gi.repository import Adw, Gio
2627

2728
from gradia.ui.window import GradientWindow
29+
from gradia.backend.logger import Logger
2830

31+
logging = Logger()
2932

3033
class GradiaApp(Adw.Application):
3134
__gtype_name__ = "GradiaApp"
@@ -35,65 +38,90 @@ def __init__(self, version: str):
3538
application_id="be.alexandervanhee.gradia",
3639
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE | Gio.ApplicationFlags.HANDLES_OPEN
3740
)
38-
self.temp_dir = tempfile.mkdtemp()
3941
self.version = version
40-
self.init_with_screenshot = False
41-
self.file_to_open: Optional[str] = None
42+
self.screenshot_mode = False
43+
self.temp_dirs: list[str] = []
44+
45+
# Connect to shutdown signal for cleanup
46+
self.connect("shutdown", self.on_shutdown)
4247

4348
def do_command_line(self, command_line: Gio.ApplicationCommandLine) -> int:
4449
args = command_line.get_arguments()[1:]
4550

46-
self.init_with_screenshot = "--screenshot" in args
51+
logging.debug(f"Command line arguments: {args}")
52+
53+
self.screenshot_mode = "--screenshot" in args
54+
files_to_open = []
4755

48-
# Extract the first non-option argument
4956
for arg in args:
5057
if not arg.startswith("--"):
5158
try:
5259
file = Gio.File.new_for_commandline_arg(arg)
5360
path = file.get_path()
5461
if path:
55-
self.file_to_open = path
56-
break
62+
files_to_open.append(path)
63+
logging.debug(f"File to open detected: {path}")
64+
else:
65+
logging.warning(f"Argument {arg} does not have a valid path.")
5766
except Exception as e:
58-
print(f"Failed to parse file URI {arg}: {e}")
67+
logging.warning(f"Failed to parse file URI {arg}.", exception=e, show_exception=True)
68+
69+
if files_to_open:
70+
for path in files_to_open:
71+
self._open_window(path)
72+
else:
73+
self._open_window(None)
5974

60-
self.activate()
6175
return 0
6276

77+
def do_open(self, files: Sequence[Gio.File], hint: str):
78+
logging.debug(f"do_open called with files: {[file.get_path() for file in files]} and hint: {hint}")
79+
for file in files:
80+
path = file.get_path()
81+
if path:
82+
logging.debug(f"Opening file from do_open: {path}")
83+
self._open_window(path)
84+
6385
def do_activate(self):
64-
self.ui = GradientWindow(
65-
self.temp_dir,
86+
logging.debug("do_activate called")
87+
# Fallback if app is run without args and not via do_open/command_line
88+
self._open_window(None)
89+
90+
def _open_window(self, file_path: Optional[str]):
91+
logging.debug(f"Opening window with file_path={file_path}, screenshot_mode={self.screenshot_mode}")
92+
temp_dir = tempfile.mkdtemp()
93+
logging.debug(f"Created temp directory: {temp_dir}")
94+
self.temp_dirs.append(temp_dir)
95+
96+
window = GradientWindow(
97+
temp_dir=temp_dir,
6698
version=self.version,
6799
application=self,
68-
init_with_screenshot=self.init_with_screenshot,
69-
file_path=self.file_to_open
100+
init_with_screenshot=self.screenshot_mode,
101+
file_path=file_path
70102
)
71-
print(self.file_to_open)
72-
self.ui.build_ui()
73-
self.ui.show()
103+
window.build_ui()
104+
window.show()
74105

75-
def do_open(self, files: Sequence[Gio.File], hint: str):
76-
if files:
77-
path = files[0].get_path()
78-
if path:
79-
self.file_to_open = path
80-
self.activate()
81-
82-
def do_shutdown(self):
83-
try:
84-
if hasattr(self, 'temp_dir') and os.path.exists(self.temp_dir):
85-
shutil.rmtree(self.temp_dir)
86-
except Exception as e:
87-
print(f"Warning: Failed to clean up temporary directory: {e}")
88-
finally:
89-
Gio.Application.do_shutdown(self)
106+
def on_shutdown(self, application):
107+
logging.info("Application shutdown started, cleaning temp directories...")
108+
for temp_dir in self.temp_dirs:
109+
try:
110+
if os.path.exists(temp_dir):
111+
shutil.rmtree(temp_dir)
112+
logging.debug(f"Deleted temp dir: {temp_dir}")
113+
except Exception as e:
114+
logging.warning(f"Failed to clean up temp dir {temp_dir}.", exception=e, show_exception=True)
115+
logging.info("Cleanup complete.")
90116

91117

92118
def main(version: str):
93119
try:
120+
logging.info("Application starting...")
121+
94122
app = GradiaApp(version=version)
95123
return app.run(sys.argv)
96124
except Exception as e:
97-
print('Application closed with an exception:', e)
125+
logging.critical("Application closed with an exception.", exception=e, show_exception=True)
98126
return 1
99127

gradia/meson.build

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,5 @@ gradia_sources = [
4545
install_data(gradia_sources, install_dir: moduledir)
4646

4747
install_subdir('graphics', install_dir: moduledir)
48-
install_subdir('ui', install_dir: moduledir)
48+
install_subdir('ui', install_dir: moduledir)
49+
install_subdir('backend', install_dir: moduledir)

0 commit comments

Comments
 (0)