texcleaner 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. texcleaner-0.3.0/LICENSE +32 -0
  2. texcleaner-0.3.0/PKG-INFO +94 -0
  3. texcleaner-0.3.0/README.md +78 -0
  4. texcleaner-0.3.0/pyproject.toml +33 -0
  5. texcleaner-0.3.0/setup.cfg +4 -0
  6. texcleaner-0.3.0/texclean/__init__.py +24 -0
  7. texcleaner-0.3.0/texclean/__main__.py +4 -0
  8. texcleaner-0.3.0/texclean/app.py +308 -0
  9. texcleaner-0.3.0/texclean/scripts/changes/0-text.tex +225 -0
  10. texcleaner-0.3.0/texclean/scripts/changes/1-text.tex +216 -0
  11. texcleaner-0.3.0/texclean/scripts/changes/pyMergeChanges.py +305 -0
  12. texcleaner-0.3.0/texclean/scripts/trackchanges-py3/0readme-acceptchanges3.txt +10 -0
  13. texcleaner-0.3.0/texclean/scripts/trackchanges-py3/0readme-arxiv_latex_cleaner +19 -0
  14. texcleaner-0.3.0/texclean/scripts/trackchanges-py3/AcceptChanges3/__init__.py +1 -0
  15. texcleaner-0.3.0/texclean/scripts/trackchanges-py3/AcceptChanges3/__pycache__/__init__.cpython-312.pyc +0 -0
  16. texcleaner-0.3.0/texclean/scripts/trackchanges-py3/AcceptChanges3/__pycache__/consoleoutput.cpython-312.pyc +0 -0
  17. texcleaner-0.3.0/texclean/scripts/trackchanges-py3/AcceptChanges3/__pycache__/linesegment.cpython-312.pyc +0 -0
  18. texcleaner-0.3.0/texclean/scripts/trackchanges-py3/AcceptChanges3/consoleoutput.py +225 -0
  19. texcleaner-0.3.0/texclean/scripts/trackchanges-py3/AcceptChanges3/linesegment.py +54 -0
  20. texcleaner-0.3.0/texclean/scripts/trackchanges-py3/__pycache__/acceptchanges3.cpython-312.pyc +0 -0
  21. texcleaner-0.3.0/texclean/scripts/trackchanges-py3/acceptAll.sh +24 -0
  22. texcleaner-0.3.0/texclean/scripts/trackchanges-py3/acceptchanges3.py +353 -0
  23. texcleaner-0.3.0/texclean/version.py +12 -0
  24. texcleaner-0.3.0/texclean/wrapper.py +282 -0
  25. texcleaner-0.3.0/texcleaner.egg-info/PKG-INFO +94 -0
  26. texcleaner-0.3.0/texcleaner.egg-info/SOURCES.txt +28 -0
  27. texcleaner-0.3.0/texcleaner.egg-info/dependency_links.txt +1 -0
  28. texcleaner-0.3.0/texcleaner.egg-info/entry_points.txt +2 -0
  29. texcleaner-0.3.0/texcleaner.egg-info/requires.txt +6 -0
  30. texcleaner-0.3.0/texcleaner.egg-info/top_level.txt +1 -0
@@ -0,0 +1,32 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+ PREAMBLE
9
+
10
+ The GNU General Public License is a free, copyleft license for
11
+ software and other kinds of works.
12
+
13
+ The licenses for most software and other practical works are designed
14
+ to take away your freedom to share and change the works. By contrast,
15
+ the GNU General Public License is intended to guarantee your freedom to
16
+ share and change all versions of a program--to make sure it remains free
17
+ software for all its users.
18
+
19
+ TERMS AND CONDITIONS
20
+
21
+ This program is free software: you can redistribute it and/or modify
22
+ it under the terms of the GNU General Public License as published by
23
+ the Free Software Foundation, either version 3 of the License, or
24
+ (at your option) any later version.
25
+
26
+ This program is distributed in the hope that it will be useful,
27
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
28
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
+ GNU General Public License for more details.
30
+
31
+ You should have received a copy of the GNU General Public License
32
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: texcleaner
3
+ Version: 0.3.0
4
+ Summary: A GUI front-end for cleaning LaTeX track changes
5
+ Author: Dan Shim
6
+ License-Expression: GPL-3.0-or-later
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: arxiv-latex-cleaner
11
+ Provides-Extra: dev
12
+ Requires-Dist: build; extra == "dev"
13
+ Requires-Dist: twine; extra == "dev"
14
+ Requires-Dist: pytest; extra == "dev"
15
+ Dynamic: license-file
16
+
17
+ # TeXClean
18
+
19
+ A GUI front-end for cleaning LaTeX documents of track changes markup.
20
+
21
+ ## Features
22
+
23
+ TeXClean supports cleaning track changes from three different LaTeX packages:
24
+
25
+ 1. **TrackChanges** - For documents using `trackchanges.sty`
26
+ 2. **Changes** - For documents using `changes.sty`
27
+ 3. **arXiv** - For preparing documents for arXiv submission
28
+
29
+ ## Installation
30
+
31
+ ### From PyPI (Recommended)
32
+
33
+ ```bash
34
+ pip install texclean
35
+ ```
36
+
37
+ After installation, you can run the application with:
38
+
39
+ ```bash
40
+ texclean
41
+ ```
42
+
43
+ ### From Source
44
+
45
+ ```bash
46
+ git clone https://github.com/yourusername/texclean.git
47
+ cd texclean
48
+ pip install -e .
49
+ ```
50
+
51
+ ## Requirements
52
+
53
+ - Python 3.8+
54
+ - `arxiv-latex-cleaner` (installed automatically with pip)
55
+
56
+ ## Usage
57
+
58
+ 1. Run the application:
59
+
60
+ ```bash
61
+ texclean
62
+ ```
63
+
64
+ Or if running from source:
65
+
66
+ ```bash
67
+ python -m texclean.app
68
+ ```
69
+
70
+ 2. Select your input file or folder:
71
+ - For **TrackChanges** and **Changes**: Select a `.tex` file
72
+ - For **arXiv**: Select the project folder containing your LaTeX files
73
+
74
+ 3. Choose the cleaning option:
75
+ - **Remove TrackChanges**: Accepts all changes and removes annotations from `trackchanges.sty`
76
+ - **Remove Changes**: Accepts all changes from `changes.sty`
77
+ - **Clean for arXiv**: Removes comments, resizes images, and organizes files for submission
78
+
79
+ 4. Click **Clean** to process the file
80
+
81
+ ## Output
82
+
83
+ - **TrackChanges/Changes**: Creates a new file with `_no_track` or `_cleaned` suffix
84
+ - **arXiv**: Creates a new folder with `_arXiv` suffix
85
+
86
+ ## License
87
+
88
+ This project is licensed under the GNU General Public License v3.0 (GPL-3.0).
89
+
90
+ ## Acknowledgments
91
+
92
+ - `acceptchanges3.py` - Felix Salfner (GPL)
93
+ - `pyMergeChanges.py` - Y. Cui (GPL)
94
+ - `arxiv-latex-cleaner` - Google Research
@@ -0,0 +1,78 @@
1
+ # TeXClean
2
+
3
+ A GUI front-end for cleaning LaTeX documents of track changes markup.
4
+
5
+ ## Features
6
+
7
+ TeXClean supports cleaning track changes from three different LaTeX packages:
8
+
9
+ 1. **TrackChanges** - For documents using `trackchanges.sty`
10
+ 2. **Changes** - For documents using `changes.sty`
11
+ 3. **arXiv** - For preparing documents for arXiv submission
12
+
13
+ ## Installation
14
+
15
+ ### From PyPI (Recommended)
16
+
17
+ ```bash
18
+ pip install texclean
19
+ ```
20
+
21
+ After installation, you can run the application with:
22
+
23
+ ```bash
24
+ texclean
25
+ ```
26
+
27
+ ### From Source
28
+
29
+ ```bash
30
+ git clone https://github.com/yourusername/texclean.git
31
+ cd texclean
32
+ pip install -e .
33
+ ```
34
+
35
+ ## Requirements
36
+
37
+ - Python 3.8+
38
+ - `arxiv-latex-cleaner` (installed automatically with pip)
39
+
40
+ ## Usage
41
+
42
+ 1. Run the application:
43
+
44
+ ```bash
45
+ texclean
46
+ ```
47
+
48
+ Or if running from source:
49
+
50
+ ```bash
51
+ python -m texclean.app
52
+ ```
53
+
54
+ 2. Select your input file or folder:
55
+ - For **TrackChanges** and **Changes**: Select a `.tex` file
56
+ - For **arXiv**: Select the project folder containing your LaTeX files
57
+
58
+ 3. Choose the cleaning option:
59
+ - **Remove TrackChanges**: Accepts all changes and removes annotations from `trackchanges.sty`
60
+ - **Remove Changes**: Accepts all changes from `changes.sty`
61
+ - **Clean for arXiv**: Removes comments, resizes images, and organizes files for submission
62
+
63
+ 4. Click **Clean** to process the file
64
+
65
+ ## Output
66
+
67
+ - **TrackChanges/Changes**: Creates a new file with `_no_track` or `_cleaned` suffix
68
+ - **arXiv**: Creates a new folder with `_arXiv` suffix
69
+
70
+ ## License
71
+
72
+ This project is licensed under the GNU General Public License v3.0 (GPL-3.0).
73
+
74
+ ## Acknowledgments
75
+
76
+ - `acceptchanges3.py` - Felix Salfner (GPL)
77
+ - `pyMergeChanges.py` - Y. Cui (GPL)
78
+ - `arxiv-latex-cleaner` - Google Research
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "texcleaner"
7
+ dynamic = ["version"]
8
+ description = "A GUI front-end for cleaning LaTeX track changes"
9
+ readme = "README.md"
10
+ license = "GPL-3.0-or-later"
11
+ authors = [
12
+ {name = "Dan Shim"}
13
+ ]
14
+ requires-python = ">=3.8"
15
+ dependencies = [
16
+ "arxiv-latex-cleaner"
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "build",
22
+ "twine",
23
+ "pytest"
24
+ ]
25
+
26
+ [project.scripts]
27
+ texclean = "texclean.app:main"
28
+
29
+ [tool.setuptools]
30
+ package-data = {texclean = ["scripts/**/*.py", "scripts/**/*"]}
31
+
32
+ [tool.setuptools.dynamic]
33
+ version = {attr = "texclean.version.__version__"}
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,24 @@
1
+ """
2
+ TeXClean - A GUI front-end for cleaning LaTeX track changes.
3
+
4
+ Supports cleaning track changes from:
5
+ - trackchanges.sty
6
+ - changes.sty
7
+ - arXiv submission preparation
8
+ """
9
+
10
+ from .version import __version__
11
+ from .wrapper import (
12
+ clean_trackchanges,
13
+ clean_changes,
14
+ clean_arxiv,
15
+ generate_output_filename
16
+ )
17
+
18
+ __all__ = [
19
+ "clean_trackchanges",
20
+ "clean_changes",
21
+ "clean_arxiv",
22
+ "generate_output_filename",
23
+ "__version__",
24
+ ]
@@ -0,0 +1,4 @@
1
+ from .app import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ app.py - GUI front-end for cleaning LaTeX track changes
4
+
5
+ A simple Tkinter application to clean LaTeX documents of track changes markup
6
+ from TrackChanges, Changes, or arXiv submission packages.
7
+ """
8
+
9
+ import os
10
+ import threading
11
+ import tkinter as tk
12
+ from tkinter import ttk, filedialog, messagebox, scrolledtext
13
+
14
+ from .wrapper import (
15
+ clean_trackchanges,
16
+ clean_changes,
17
+ clean_arxiv,
18
+ generate_output_filename,
19
+ detect_cleaning_module
20
+ )
21
+ from .version import __version__
22
+
23
+
24
+ class TeXCleanApp:
25
+ def __init__(self, root):
26
+ self.root = root
27
+ self.root.title(f"TeXClean v{__version__} - LaTeX Track Changes Cleaner")
28
+ self.root.geometry("700x500")
29
+ self.root.resizable(True, True)
30
+
31
+ self.input_path = tk.StringVar()
32
+
33
+ self.create_widgets()
34
+
35
+ def create_widgets(self):
36
+ self.notebook = ttk.Notebook(self.root)
37
+ self.notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
38
+
39
+ self.trackchanges_tab = ttk.Frame(self.notebook)
40
+ self.arxiv_tab = ttk.Frame(self.notebook)
41
+
42
+ self.notebook.add(self.trackchanges_tab, text="Track Changes Cleaner")
43
+ self.notebook.add(self.arxiv_tab, text="arXiv Cleaner")
44
+
45
+ self._create_trackchanges_tab()
46
+ self._create_arxiv_tab()
47
+
48
+ def _create_trackchanges_tab(self):
49
+ main_frame = ttk.Frame(self.trackchanges_tab, padding="10")
50
+ main_frame.pack(fill=tk.BOTH, expand=True)
51
+
52
+ title_label = ttk.Label(
53
+ main_frame,
54
+ text="Clean LaTeX Track Changes",
55
+ font=("TkDefaultFont", 14, "bold")
56
+ )
57
+ title_label.pack(pady=(0, 20))
58
+
59
+ input_frame = ttk.LabelFrame(main_frame, text="Input File", padding="5")
60
+ input_frame.pack(fill=tk.X, pady=5)
61
+ input_frame.columnconfigure(0, weight=1)
62
+
63
+ self.trackchanges_entry = ttk.Entry(input_frame, textvariable=self.input_path)
64
+ self.trackchanges_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
65
+
66
+ browse_btn = ttk.Button(input_frame, text="Browse...", command=self._browse_trackchanges_input)
67
+ browse_btn.pack(side=tk.RIGHT)
68
+
69
+ helper_label = ttk.Label(
70
+ input_frame,
71
+ text="Select a .tex file",
72
+ font=("TkDefaultFont", 9, "italic"),
73
+ foreground="gray"
74
+ )
75
+ helper_label.pack(anchor=tk.W, pady=(2, 0))
76
+
77
+ self.trackchanges_clean_btn = ttk.Button(
78
+ main_frame,
79
+ text="Detect & Clean",
80
+ command=self._start_trackchanges_cleaning
81
+ )
82
+ self.trackchanges_clean_btn.pack(pady=20)
83
+
84
+ log_label = ttk.Label(main_frame, text="Log Output:", font=("TkDefaultFont", 10, "bold"))
85
+ log_label.pack(anchor=tk.W, pady=(10, 5))
86
+
87
+ self.trackchanges_log = scrolledtext.ScrolledText(
88
+ main_frame,
89
+ height=12,
90
+ width=80,
91
+ state=tk.DISABLED,
92
+ wrap=tk.WORD
93
+ )
94
+ self.trackchanges_log.pack(fill=tk.BOTH, expand=True, pady=5)
95
+
96
+ self.trackchanges_progress = ttk.Progressbar(main_frame, mode="indeterminate")
97
+ self.trackchanges_progress.pack(fill=tk.X, pady=5)
98
+
99
+ def _create_arxiv_tab(self):
100
+ main_frame = ttk.Frame(self.arxiv_tab, padding="10")
101
+ main_frame.pack(fill=tk.BOTH, expand=True)
102
+
103
+ title_label = ttk.Label(
104
+ main_frame,
105
+ text="Clean for arXiv Submission",
106
+ font=("TkDefaultFont", 14, "bold")
107
+ )
108
+ title_label.pack(pady=(0, 20))
109
+
110
+ input_frame = ttk.LabelFrame(main_frame, text="Project Folder", padding="5")
111
+ input_frame.pack(fill=tk.X, pady=5)
112
+ input_frame.columnconfigure(0, weight=1)
113
+
114
+ self.arxiv_entry = ttk.Entry(input_frame)
115
+ self.arxiv_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
116
+
117
+ browse_btn = ttk.Button(input_frame, text="Browse...", command=self._browse_arxiv_input)
118
+ browse_btn.pack(side=tk.RIGHT)
119
+
120
+ helper_label = ttk.Label(
121
+ input_frame,
122
+ text="Select a project folder",
123
+ font=("TkDefaultFont", 9, "italic"),
124
+ foreground="gray"
125
+ )
126
+ helper_label.pack(anchor=tk.W, pady=(2, 0))
127
+
128
+ self.arxiv_clean_btn = ttk.Button(
129
+ main_frame,
130
+ text="Clean for arXiv",
131
+ command=self._start_arxiv_cleaning
132
+ )
133
+ self.arxiv_clean_btn.pack(pady=20)
134
+
135
+ log_label = ttk.Label(main_frame, text="Log Output:", font=("TkDefaultFont", 10, "bold"))
136
+ log_label.pack(anchor=tk.W, pady=(10, 5))
137
+
138
+ self.arxiv_log = scrolledtext.ScrolledText(
139
+ main_frame,
140
+ height=12,
141
+ width=80,
142
+ state=tk.DISABLED,
143
+ wrap=tk.WORD
144
+ )
145
+ self.arxiv_log.pack(fill=tk.BOTH, expand=True, pady=5)
146
+
147
+ self.arxiv_progress = ttk.Progressbar(main_frame, mode="indeterminate")
148
+ self.arxiv_progress.pack(fill=tk.X, pady=5)
149
+
150
+ def _browse_trackchanges_input(self):
151
+ file_path = filedialog.askopenfilename(
152
+ title="Select LaTeX File",
153
+ filetypes=[("LaTeX files", "*.tex"), ("All files", "*.*")]
154
+ )
155
+ if file_path:
156
+ self.input_path.set(file_path)
157
+
158
+ def _browse_arxiv_input(self):
159
+ folder = filedialog.askdirectory(title="Select LaTeX Project Folder")
160
+ if folder:
161
+ self.arxiv_entry.delete(0, tk.END)
162
+ self.arxiv_entry.insert(0, folder)
163
+
164
+ def _log(self, message, log_widget):
165
+ log_widget.config(state=tk.NORMAL)
166
+ log_widget.insert(tk.END, message)
167
+ log_widget.see(tk.END)
168
+ log_widget.config(state=tk.DISABLED)
169
+
170
+ def _clear_log(self, log_widget):
171
+ log_widget.config(state=tk.NORMAL)
172
+ log_widget.delete(1.0, tk.END)
173
+ log_widget.config(state=tk.DISABLED)
174
+
175
+ def _validate_tex_input(self, input_path):
176
+ if not input_path:
177
+ return False, "Please select an input file."
178
+ if not os.path.exists(input_path):
179
+ return False, f"Path not found: {input_path}"
180
+ if not os.path.isfile(input_path):
181
+ return False, "Error: This operation requires a .tex file."
182
+ if not input_path.lower().endswith(".tex"):
183
+ return False, "Error: The selected file must have a .tex extension."
184
+ return True, ""
185
+
186
+ def _validate_folder_input(self, folder_path):
187
+ if not folder_path:
188
+ return False, "Please select a project folder."
189
+ if not os.path.exists(folder_path):
190
+ return False, f"Folder not found: {folder_path}"
191
+ if not os.path.isdir(folder_path):
192
+ return False, "Error: This operation requires a folder."
193
+ return True, ""
194
+
195
+ def _start_trackchanges_cleaning(self):
196
+ input_path = self.input_path.get().strip()
197
+
198
+ valid, error_msg = self._validate_tex_input(input_path)
199
+ if not valid:
200
+ messagebox.showerror("Input Error", error_msg)
201
+ return
202
+
203
+ detected = detect_cleaning_module(input_path)
204
+
205
+ if detected is None:
206
+ result = messagebox.askyesno(
207
+ "No Track Changes Detected",
208
+ "No track changes markup (trackchanges or changes package) was detected in the file.\n\n"
209
+ "Would you like to try cleaning anyway?"
210
+ )
211
+ if not result:
212
+ return
213
+ detected = "trackchanges"
214
+ else:
215
+ module_name = "TrackChanges (trackchanges.sty)" if detected == "trackchanges" else "Changes (changes.sty)"
216
+ result = messagebox.askyesno(
217
+ "Confirm Detected Module",
218
+ f"Detected '{module_name}' signatures in the file.\n\n"
219
+ f"Is this correct and would you like to proceed with cleaning?"
220
+ )
221
+ if not result:
222
+ return
223
+
224
+ self._clear_log(self.trackchanges_log)
225
+ self.trackchanges_clean_btn.config(state=tk.DISABLED)
226
+ self.trackchanges_progress.start(10)
227
+ self._log(f"Detected module: {detected}\n", self.trackchanges_log)
228
+ self._log("Starting cleaning...\n", self.trackchanges_log)
229
+
230
+ thread = threading.Thread(
231
+ target=self._run_trackchanges_cleaning,
232
+ args=(input_path, detected)
233
+ )
234
+ thread.daemon = True
235
+ thread.start()
236
+
237
+ def _run_trackchanges_cleaning(self, input_path, detected_module):
238
+ def callback(message):
239
+ self.root.after(0, self._log, message, self.trackchanges_log)
240
+
241
+ try:
242
+ output_path = generate_output_filename(input_path, "-cleaned")
243
+
244
+ if detected_module == "trackchanges":
245
+ success, message = clean_trackchanges(input_path, output_path, callback)
246
+ else:
247
+ success, message = clean_changes(input_path, output_path, callback)
248
+
249
+ callback("\n" + message + "\n")
250
+
251
+ if success:
252
+ self.root.after(0, lambda: messagebox.showinfo("Success", message))
253
+ else:
254
+ self.root.after(0, lambda: messagebox.showerror("Error", message))
255
+
256
+ except Exception as e:
257
+ error_msg = f"Unexpected error: {str(e)}"
258
+ self.root.after(0, self._log, error_msg, self.trackchanges_log)
259
+ self.root.after(0, lambda: messagebox.showerror("Error", error_msg))
260
+
261
+ finally:
262
+ self.root.after(0, self.trackchanges_progress.stop)
263
+ self.root.after(0, lambda: self.trackchanges_clean_btn.config(state=tk.NORMAL))
264
+
265
+ def _start_arxiv_cleaning(self):
266
+ folder_path = self.arxiv_entry.get().strip()
267
+
268
+ valid, error_msg = self._validate_folder_input(folder_path)
269
+ if not valid:
270
+ messagebox.showerror("Input Error", error_msg)
271
+ return
272
+
273
+ self._clear_log(self.arxiv_log)
274
+ self.arxiv_clean_btn.config(state=tk.DISABLED)
275
+ self.arxiv_progress.start(10)
276
+
277
+ thread = threading.Thread(target=self._run_arxiv_cleaning, args=(folder_path,))
278
+ thread.daemon = True
279
+ thread.start()
280
+
281
+ def _run_arxiv_cleaning(self, folder_path):
282
+ def callback(message):
283
+ self.root.after(0, self._log, message, self.arxiv_log)
284
+
285
+ try:
286
+ success, message = clean_arxiv(folder_path, callback=callback)
287
+
288
+ callback("\n" + message + "\n")
289
+
290
+ if success:
291
+ self.root.after(0, lambda: messagebox.showinfo("Success", message))
292
+ else:
293
+ self.root.after(0, lambda: messagebox.showerror("Error", message))
294
+
295
+ except Exception as e:
296
+ error_msg = f"Unexpected error: {str(e)}"
297
+ self.root.after(0, self._log, error_msg, self.arxiv_log)
298
+ self.root.after(0, lambda: messagebox.showerror("Error", error_msg))
299
+
300
+ finally:
301
+ self.root.after(0, self.arxiv_progress.stop)
302
+ self.root.after(0, lambda: self.arxiv_clean_btn.config(state=tk.NORMAL))
303
+
304
+
305
+ def main():
306
+ root = tk.Tk()
307
+ app = TeXCleanApp(root)
308
+ root.mainloop()