commit e9e53b2866596f0c7289e3580de84644f2c6d152 Author: punix Date: Thu Jun 5 21:11:22 2025 +0200 first commit of lxtools installer diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0b46ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +debug.log +.venv +.venv.bak +.idea +.vscode +__pycache__ diff --git a/Changelog b/Changelog new file mode 100644 index 0000000..1df4d59 --- /dev/null +++ b/Changelog @@ -0,0 +1,11 @@ +Changelog for LXTools installer + +## [Unreleased] + + - + + ### Added +4-06-2025 + + - Create LXTools installer for simple install Apps by git.ilunix.de + diff --git a/TK-Themes/LICENSE b/TK-Themes/LICENSE new file mode 100644 index 0000000..0212030 --- /dev/null +++ b/TK-Themes/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 rdbende + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/TK-Themes/theme/dark.tcl b/TK-Themes/theme/dark.tcl new file mode 100644 index 0000000..2ad09db --- /dev/null +++ b/TK-Themes/theme/dark.tcl @@ -0,0 +1,539 @@ +# Copyright (c) 2021 rdbende + +# inspired by rdbende modified azure to water by Désire Werner Menrath polunga40@unity-mail.de 2024 + +package require Tk 8.6 + +namespace eval ttk::theme::water-dark { + variable version 2.0 + package provide ttk::theme::water-dark $version + + ttk::style theme create water-dark -parent clam -settings { + proc load_images {imgdir} { + variable I + foreach file [glob -directory $imgdir *.png] { + set img [file tail [file rootname $file]] + set I($img) [image create photo -file $file -format png] + } + } + + load_images [file join [file dirname [info script]] dark] + + array set colors { + -fg "#ffffff" + -bg "#333333" + -disabledfg "#aaaaaa" + -disabledbg "#737373" + -selectfg "#ffffff" + -selectbg "#007fff" + } + + ttk::style layout TButton { + Button.button -children { + Button.padding -children { + Button.label -side left -expand true + } + } + } + + ttk::style layout Toolbutton { + Toolbutton.button -children { + Toolbutton.padding -children { + Toolbutton.label -side left -expand true + } + } + } + + ttk::style layout TMenubutton { + Menubutton.button -children { + Menubutton.padding -children { + Menubutton.indicator -side right + Menubutton.label -side right -expand true + } + } + } + + ttk::style layout TOptionMenu { + OptionMenu.button -children { + OptionMenu.padding -children { + OptionMenu.indicator -side right + OptionMenu.label -side right -expand true + } + } + } + + ttk::style layout Accent.TButton { + AccentButton.button -children { + AccentButton.padding -children { + AccentButton.label -side left -expand true + } + } + } + + ttk::style layout TCheckbutton { + Checkbutton.button -children { + Checkbutton.padding -children { + Checkbutton.indicator -side left + Checkbutton.label -side right -expand true + } + } + } + + ttk::style layout Switch.TCheckbutton { + Switch.button -children { + Switch.padding -children { + Switch.indicator -side left + Switch.label -side right -expand true + } + } + } + + ttk::style layout Toggle.TButton { + ToggleButton.button -children { + ToggleButton.padding -children { + ToggleButton.label -side left -expand true + } + } + } + + ttk::style layout TRadiobutton { + Radiobutton.button -children { + Radiobutton.padding -children { + Radiobutton.indicator -side left + Radiobutton.label -side right -expand true + } + } + } + + ttk::style layout Vertical.TScrollbar { + Vertical.Scrollbar.trough -sticky ns -children { + Vertical.Scrollbar.thumb -expand true + } + } + + ttk::style layout Horizontal.TScrollbar { + Horizontal.Scrollbar.trough -sticky ew -children { + Horizontal.Scrollbar.thumb -expand true + } + } + + ttk::style layout TCombobox { + Combobox.field -sticky nswe -children { + Combobox.padding -expand true -sticky nswe -children { + Combobox.textarea -sticky nswe + } + } + Combobox.button -side right -sticky ns -children { + Combobox.arrow -sticky nsew + } + } + + ttk::style layout TSpinbox { + Spinbox.field -sticky nsew -children { + Spinbox.padding -expand true -sticky nswe -children { + Spinbox.textarea -sticky nswe + } + + } + Spinbox.button -side right -sticky ns -children { + null -side right -children { + Spinbox.uparrow -side top + Spinbox.downarrow -side bottom + } + } + } + + ttk::style layout Horizontal.TSeparator { + Horizontal.separator -sticky nswe + } + + ttk::style layout Vertical.TSeparator { + Vertical.separator -sticky nswe + } + + ttk::style layout Horizontal.Tick.TScale { + Horizontal.TickScale.trough -sticky ew -children { + Horizontal.TickScale.slider -sticky w + } + } + + ttk::style layout Vertical.Tick.TScale { + Vertical.TickScale.trough -sticky ns -children { + Vertical.TickScale.slider -sticky n + } + } + + ttk::style layout Card.TFrame { + Card.field { + Card.padding -expand 1 + } + } + + ttk::style layout TLabelframe { + Labelframe.border { + Labelframe.padding -expand 1 -children { + Labelframe.label -side right + } + } + } + + ttk::style layout TNotebook.Tab { + Notebook.tab -children { + Notebook.padding -side top -children { + Notebook.label -side top -sticky {} + } + } + } + + ttk::style layout Treeview.Item { + Treeitem.padding -sticky nswe -children { + Treeitem.indicator -side left -sticky {} + Treeitem.image -side left -sticky {} + Treeitem.text -side left -sticky {} + } + } + + + # Elements + + # Button + ttk::style configure TButton -padding {8 4 8 4} -width -10 -anchor center + + ttk::style element create Button.button image \ + [list $I(box-basic) \ + {selected disabled} $I(box-basic) \ + disabled $I(box-basic) \ + pressed $I(box-basic) \ + selected $I(box-basic) \ + active $I(button-hover) \ + focus $I(button-hover) \ + ] -border 4 -sticky ewns + + # Toolbutton + ttk::style configure Toolbutton -padding {8 4 8 4} -width -5 -anchor center + + ttk::style element create Toolbutton.button image \ + [list $I(empty) \ + {selected disabled} $I(empty) \ + disabled $I(empty) \ + pressed $I(rect-basic) \ + selected $I(rect-basic) \ + active $I(rect-basic) \ + ] -border 4 -sticky ewns + + # Menubutton + ttk::style configure TMenubutton -padding {8 4 4 4} + + ttk::style element create Menubutton.button \ + image [list $I(rect-basic) \ + disabled $I(rect-basic) \ + pressed $I(rect-basic) \ + active $I(button-hover) \ + ] -border 4 -sticky ewns + + ttk::style element create Menubutton.indicator \ + image [list $I(down) \ + active $I(down) \ + pressed $I(down) \ + disabled $I(down) \ + ] -width 15 -sticky e + + # OptionMenu + ttk::style configure TOptionMenu -padding {8 4 4 4} + + ttk::style element create OptionMenu.button \ + image [list $I(rect-basic) \ + disabled $I(rect-basic) \ + pressed $I(rect-basic) \ + active $I(button-hover) \ + ] -border 4 -sticky ewns + + ttk::style element create OptionMenu.indicator \ + image [list $I(down) \ + active $I(down) \ + pressed $I(down) \ + disabled $I(down) \ + ] -width 15 -sticky e + + # AccentButton + ttk::style configure Accent.TButton -padding {8 4 8 4} -width -10 -anchor center + + ttk::style element create AccentButton.button image \ + [list $I(rect-accent) \ + {selected disabled} $I(rect-accent-hover) \ + disabled $I(rect-accent-hover) \ + pressed $I(rect-accent) \ + selected $I(rect-accent) \ + active $I(rect-accent-hover) \ + focus $I(rect-accent-hover) \ + ] -border 4 -sticky ewns + + # Checkbutton + ttk::style configure TCheckbutton -padding 4 + + ttk::style element create Checkbutton.indicator image \ + [list $I(box-basic) \ + {alternate disabled} $I(check-tri-basic) \ + {selected disabled} $I(check-basic) \ + disabled $I(box-basic) \ + {pressed alternate} $I(check-tri-hover) \ + {active alternate} $I(check-tri-hover) \ + alternate $I(check-tri-accent) \ + {pressed selected} $I(check-hover) \ + {active selected} $I(check-hover) \ + selected $I(check-accent) \ + {pressed !selected} $I(rect-hover) \ + active $I(box-hover) \ + ] -width 26 -sticky w + + # Switch + ttk::style element create Switch.indicator image \ + [list $I(off-basic) \ + {selected disabled} $I(on-basic) \ + disabled $I(off-basic) \ + {pressed selected} $I(on-accent) \ + {active selected} $I(on-accent) \ + selected $I(on-accent) \ + {pressed !selected} $I(off-basic) \ + active $I(off-basic) \ + ] -width 46 -sticky w + + # ToggleButton + ttk::style configure Toggle.TButton -padding {8 4 8 4} -width -10 -anchor center + + ttk::style element create ToggleButton.button image \ + [list $I(rect-basic) \ + {selected disabled} $I(rect-accent-hover) \ + disabled $I(rect-basic) \ + {pressed selected} $I(rect-basic) \ + {active selected} $I(rect-accent) \ + selected $I(rect-accent) \ + {pressed !selected} $I(rect-accent) \ + active $I(rect-basic) \ + ] -border 4 -sticky ewns + + # Radiobutton + ttk::style configure TRadiobutton -padding 4 + + ttk::style element create Radiobutton.indicator image \ + [list $I(outline-basic) \ + {alternate disabled} $I(radio-tri-basic) \ + {selected disabled} $I(radio-basic) \ + disabled $I(outline-basic) \ + {pressed alternate} $I(radio-tri-hover) \ + {active alternate} $I(radio-tri-hover) \ + alternate $I(radio-tri-accent) \ + {pressed selected} $I(radio-hover) \ + {active selected} $I(radio-hover) \ + selected $I(radio-accent) \ + {pressed !selected} $I(circle-hover) \ + active $I(outline-hover) \ + ] -width 26 -sticky w + + # Scrollbar + ttk::style element create Horizontal.Scrollbar.trough image $I(hor-basic) \ + -sticky ew + + ttk::style element create Horizontal.Scrollbar.thumb \ + image [list $I(hor-accent) \ + disabled $I(hor-basic) \ + pressed $I(hor-hover) \ + active $I(hor-hover) \ + ] -sticky ew + + ttk::style element create Vertical.Scrollbar.trough image $I(vert-basic) \ + -sticky ns + + ttk::style element create Vertical.Scrollbar.thumb \ + image [list $I(vert-accent) \ + disabled $I(vert-basic) \ + pressed $I(vert-hover) \ + active $I(vert-hover) \ + ] -sticky ns + + # Scale + ttk::style element create Horizontal.Scale.trough image $I(scale-hor) \ + -border 5 -padding 0 + + ttk::style element create Horizontal.Scale.slider \ + image [list $I(circle-accent) \ + disabled $I(circle-basic) \ + pressed $I(circle-hover) \ + active $I(circle-hover) \ + ] -sticky {} + + ttk::style element create Vertical.Scale.trough image $I(scale-vert) \ + -border 5 -padding 0 + + ttk::style element create Vertical.Scale.slider \ + image [list $I(circle-accent) \ + disabled $I(circle-basic) \ + pressed $I(circle-hover) \ + active $I(circle-hover) \ + ] -sticky {} + + # Tickscale + ttk::style element create Horizontal.TickScale.trough image $I(scale-hor) \ + -border 5 -padding 0 + + ttk::style element create Horizontal.TickScale.slider \ + image [list $I(tick-hor-accent) \ + disabled $I(tick-hor-basic) \ + pressed $I(tick-hor-hover) \ + active $I(tick-hor-hover) \ + ] -sticky {} + + ttk::style element create Vertical.TickScale.trough image $I(scale-vert) \ + -border 5 -padding 0 + + ttk::style element create Vertical.TickScale.slider \ + image [list $I(tick-vert-accent) \ + disabled $I(tick-vert-basic) \ + pressed $I(tick-vert-hover) \ + active $I(tick-vert-hover) \ + ] -sticky {} + + # Progressbar + ttk::style element create Horizontal.Progressbar.trough image $I(hor-basic) \ + -sticky ew + + ttk::style element create Horizontal.Progressbar.pbar image $I(hor-accent) \ + -sticky ew + + ttk::style element create Vertical.Progressbar.trough image $I(vert-basic) \ + -sticky ns + + ttk::style element create Vertical.Progressbar.pbar image $I(vert-accent) \ + -sticky ns + + # Entry + ttk::style element create Entry.field \ + image [list $I(box-basic) \ + {focus hover} $I(box-accent) \ + invalid $I(box-invalid) \ + disabled $I(box-basic) \ + focus $I(box-accent) \ + hover $I(box-hover) \ + ] -border 5 -padding {8} -sticky news + + # Combobox + ttk::style map TCombobox -selectbackground [list \ + {!focus} $colors(-selectbg) \ + {readonly hover} $colors(-selectbg) \ + {readonly focus} $colors(-selectbg) \ + ] + + ttk::style map TCombobox -selectforeground [list \ + {!focus} $colors(-selectfg) \ + {readonly hover} $colors(-selectfg) \ + {readonly focus} $colors(-selectfg) \ + ] + + ttk::style element create Combobox.field \ + image [list $I(box-basic) \ + {readonly disabled} $I(rect-basic) \ + {readonly pressed} $I(rect-basic) \ + {readonly focus hover} $I(button-hover) \ + {readonly focus} $I(button-hover) \ + {readonly hover} $I(button-hover) \ + {focus hover} $I(box-accent) \ + readonly $I(rect-basic) \ + invalid $I(box-invalid) \ + disabled $I(box-basic) \ + focus $I(box-accent) \ + hover $I(box-hover) \ + ] -border 5 -padding {8} + + ttk::style element create Combobox.button \ + image [list $I(combo-button-basic) \ + {!readonly focus} $I(combo-button-focus) \ + {readonly focus} $I(combo-button-hover) \ + {readonly hover} $I(combo-button-hover) + ] -border 5 -padding {2 6 6 6} + + ttk::style element create Combobox.arrow image $I(down) \ + -width 15 -sticky e + + # Spinbox + ttk::style element create Spinbox.field \ + image [list $I(box-basic) \ + invalid $I(box-invalid) \ + disabled $I(box-basic) \ + focus $I(box-accent) \ + hover $I(box-hover) \ + ] -border 5 -padding {8} -sticky news + + ttk::style element create Spinbox.uparrow \ + image [list $I(up) \ + disabled $I(up) \ + pressed $I(up-accent) \ + active $I(up-accent) \ + ] -border 4 -width 15 -sticky e + + ttk::style element create Spinbox.downarrow \ + image [list $I(down) \ + disabled $I(down) \ + pressed $I(down-accent) \ + active $I(down-accent) \ + ] -border 4 -width 15 -sticky e + + ttk::style element create Spinbox.button \ + image [list $I(combo-button-basic) \ + {!readonly focus} $I(combo-button-focus) \ + {readonly focus} $I(combo-button-hover) \ + {readonly hover} $I(combo-button-hover) + ] -border 5 -padding {2 6 6 6} + + # Sizegrip + ttk::style element create Sizegrip.sizegrip image $I(size) \ + -sticky ewns + + # Separator + ttk::style element create Horizontal.separator image $I(separator) + + ttk::style element create Vertical.separator image $I(separator) + + # Card + ttk::style element create Card.field image $I(card) \ + -border 10 -padding 4 -sticky news + + # Labelframe + ttk::style element create Labelframe.border image $I(card) \ + -border 5 -padding 4 -sticky news + + # Notebook + ttk::style element create Notebook.client \ + image $I(notebook) -border 5 + + ttk::style element create Notebook.tab \ + image [list $I(tab-disabled) \ + selected $I(tab-basic) \ + active $I(tab-hover) \ + ] -border 5 -padding {14 4} + + # Treeview + ttk::style element create Treeview.field image $I(card) \ + -border 5 + + ttk::style element create Treeheading.cell \ + image [list $I(tree-basic) \ + pressed $I(tree-pressed) + ] -border 5 -padding 4 -sticky ewns + + ttk::style element create Treeitem.indicator \ + image [list $I(right) \ + user2 $I(empty) \ + user1 $I(down) \ + ] -width 26 -sticky {} + + ttk::style configure Treeview -background $colors(-bg) + ttk::style configure Treeview.Item -padding {2 0 0 0} + ttk::style map Treeview \ + -background [list selected $colors(-selectbg)] \ + -foreground [list selected $colors(-selectfg)] + + # Panedwindow + # Insane hack to remove clam's ugly sash + ttk::style configure Sash -gripcount 0 + } +} diff --git a/TK-Themes/theme/dark/box-accent.png b/TK-Themes/theme/dark/box-accent.png new file mode 100644 index 0000000..767a81b Binary files /dev/null and b/TK-Themes/theme/dark/box-accent.png differ diff --git a/TK-Themes/theme/dark/box-basic.png b/TK-Themes/theme/dark/box-basic.png new file mode 100644 index 0000000..0b28fed Binary files /dev/null and b/TK-Themes/theme/dark/box-basic.png differ diff --git a/TK-Themes/theme/dark/box-hover.png b/TK-Themes/theme/dark/box-hover.png new file mode 100644 index 0000000..3ca7c19 Binary files /dev/null and b/TK-Themes/theme/dark/box-hover.png differ diff --git a/TK-Themes/theme/dark/box-invalid.png b/TK-Themes/theme/dark/box-invalid.png new file mode 100644 index 0000000..f180e94 Binary files /dev/null and b/TK-Themes/theme/dark/box-invalid.png differ diff --git a/TK-Themes/theme/dark/button-hover.png b/TK-Themes/theme/dark/button-hover.png new file mode 100644 index 0000000..300eb09 Binary files /dev/null and b/TK-Themes/theme/dark/button-hover.png differ diff --git a/TK-Themes/theme/dark/card.png b/TK-Themes/theme/dark/card.png new file mode 100644 index 0000000..3978e9f Binary files /dev/null and b/TK-Themes/theme/dark/card.png differ diff --git a/TK-Themes/theme/dark/check-accent.png b/TK-Themes/theme/dark/check-accent.png new file mode 100644 index 0000000..7cabf6a Binary files /dev/null and b/TK-Themes/theme/dark/check-accent.png differ diff --git a/TK-Themes/theme/dark/check-basic.png b/TK-Themes/theme/dark/check-basic.png new file mode 100644 index 0000000..529718e Binary files /dev/null and b/TK-Themes/theme/dark/check-basic.png differ diff --git a/TK-Themes/theme/dark/check-hover.png b/TK-Themes/theme/dark/check-hover.png new file mode 100644 index 0000000..c96c80c Binary files /dev/null and b/TK-Themes/theme/dark/check-hover.png differ diff --git a/TK-Themes/theme/dark/check-tri-accent.png b/TK-Themes/theme/dark/check-tri-accent.png new file mode 100644 index 0000000..1d0c255 Binary files /dev/null and b/TK-Themes/theme/dark/check-tri-accent.png differ diff --git a/TK-Themes/theme/dark/check-tri-basic.png b/TK-Themes/theme/dark/check-tri-basic.png new file mode 100644 index 0000000..d0188f5 Binary files /dev/null and b/TK-Themes/theme/dark/check-tri-basic.png differ diff --git a/TK-Themes/theme/dark/check-tri-hover.png b/TK-Themes/theme/dark/check-tri-hover.png new file mode 100644 index 0000000..ae3e19f Binary files /dev/null and b/TK-Themes/theme/dark/check-tri-hover.png differ diff --git a/TK-Themes/theme/dark/circle-accent.png b/TK-Themes/theme/dark/circle-accent.png new file mode 100644 index 0000000..1b25b16 Binary files /dev/null and b/TK-Themes/theme/dark/circle-accent.png differ diff --git a/TK-Themes/theme/dark/circle-basic.png b/TK-Themes/theme/dark/circle-basic.png new file mode 100644 index 0000000..85f2bac Binary files /dev/null and b/TK-Themes/theme/dark/circle-basic.png differ diff --git a/TK-Themes/theme/dark/circle-hover.png b/TK-Themes/theme/dark/circle-hover.png new file mode 100644 index 0000000..b439cd6 Binary files /dev/null and b/TK-Themes/theme/dark/circle-hover.png differ diff --git a/TK-Themes/theme/dark/combo-button-basic.png b/TK-Themes/theme/dark/combo-button-basic.png new file mode 100644 index 0000000..4aef1b2 Binary files /dev/null and b/TK-Themes/theme/dark/combo-button-basic.png differ diff --git a/TK-Themes/theme/dark/combo-button-focus.png b/TK-Themes/theme/dark/combo-button-focus.png new file mode 100644 index 0000000..e37db17 Binary files /dev/null and b/TK-Themes/theme/dark/combo-button-focus.png differ diff --git a/TK-Themes/theme/dark/combo-button-hover.png b/TK-Themes/theme/dark/combo-button-hover.png new file mode 100644 index 0000000..493cf52 Binary files /dev/null and b/TK-Themes/theme/dark/combo-button-hover.png differ diff --git a/TK-Themes/theme/dark/down-accent.png b/TK-Themes/theme/dark/down-accent.png new file mode 100644 index 0000000..5bb987d Binary files /dev/null and b/TK-Themes/theme/dark/down-accent.png differ diff --git a/TK-Themes/theme/dark/down.png b/TK-Themes/theme/dark/down.png new file mode 100644 index 0000000..d83f92d Binary files /dev/null and b/TK-Themes/theme/dark/down.png differ diff --git a/TK-Themes/theme/dark/empty.png b/TK-Themes/theme/dark/empty.png new file mode 100644 index 0000000..202e3de Binary files /dev/null and b/TK-Themes/theme/dark/empty.png differ diff --git a/TK-Themes/theme/dark/hor-accent.png b/TK-Themes/theme/dark/hor-accent.png new file mode 100644 index 0000000..ab4b182 Binary files /dev/null and b/TK-Themes/theme/dark/hor-accent.png differ diff --git a/TK-Themes/theme/dark/hor-basic.png b/TK-Themes/theme/dark/hor-basic.png new file mode 100644 index 0000000..bbc2e50 Binary files /dev/null and b/TK-Themes/theme/dark/hor-basic.png differ diff --git a/TK-Themes/theme/dark/hor-hover.png b/TK-Themes/theme/dark/hor-hover.png new file mode 100644 index 0000000..56216f4 Binary files /dev/null and b/TK-Themes/theme/dark/hor-hover.png differ diff --git a/TK-Themes/theme/dark/notebook.png b/TK-Themes/theme/dark/notebook.png new file mode 100644 index 0000000..5b937c1 Binary files /dev/null and b/TK-Themes/theme/dark/notebook.png differ diff --git a/TK-Themes/theme/dark/off-basic.png b/TK-Themes/theme/dark/off-basic.png new file mode 100644 index 0000000..2ca539a Binary files /dev/null and b/TK-Themes/theme/dark/off-basic.png differ diff --git a/TK-Themes/theme/dark/on-accent.png b/TK-Themes/theme/dark/on-accent.png new file mode 100644 index 0000000..9d48eb6 Binary files /dev/null and b/TK-Themes/theme/dark/on-accent.png differ diff --git a/TK-Themes/theme/dark/on-basic.png b/TK-Themes/theme/dark/on-basic.png new file mode 100644 index 0000000..9dce6a5 Binary files /dev/null and b/TK-Themes/theme/dark/on-basic.png differ diff --git a/TK-Themes/theme/dark/outline-basic.png b/TK-Themes/theme/dark/outline-basic.png new file mode 100644 index 0000000..2f76a1b Binary files /dev/null and b/TK-Themes/theme/dark/outline-basic.png differ diff --git a/TK-Themes/theme/dark/outline-hover.png b/TK-Themes/theme/dark/outline-hover.png new file mode 100644 index 0000000..7ce5290 Binary files /dev/null and b/TK-Themes/theme/dark/outline-hover.png differ diff --git a/TK-Themes/theme/dark/radio-accent.png b/TK-Themes/theme/dark/radio-accent.png new file mode 100644 index 0000000..6535a97 Binary files /dev/null and b/TK-Themes/theme/dark/radio-accent.png differ diff --git a/TK-Themes/theme/dark/radio-basic.png b/TK-Themes/theme/dark/radio-basic.png new file mode 100644 index 0000000..f9b55a6 Binary files /dev/null and b/TK-Themes/theme/dark/radio-basic.png differ diff --git a/TK-Themes/theme/dark/radio-hover.png b/TK-Themes/theme/dark/radio-hover.png new file mode 100644 index 0000000..4f3eab8 Binary files /dev/null and b/TK-Themes/theme/dark/radio-hover.png differ diff --git a/TK-Themes/theme/dark/radio-tri-accent.png b/TK-Themes/theme/dark/radio-tri-accent.png new file mode 100644 index 0000000..b09b6ae Binary files /dev/null and b/TK-Themes/theme/dark/radio-tri-accent.png differ diff --git a/TK-Themes/theme/dark/radio-tri-basic.png b/TK-Themes/theme/dark/radio-tri-basic.png new file mode 100644 index 0000000..def9e27 Binary files /dev/null and b/TK-Themes/theme/dark/radio-tri-basic.png differ diff --git a/TK-Themes/theme/dark/radio-tri-hover.png b/TK-Themes/theme/dark/radio-tri-hover.png new file mode 100644 index 0000000..86f1b59 Binary files /dev/null and b/TK-Themes/theme/dark/radio-tri-hover.png differ diff --git a/TK-Themes/theme/dark/rect-accent-hover.png b/TK-Themes/theme/dark/rect-accent-hover.png new file mode 100644 index 0000000..bb49129 Binary files /dev/null and b/TK-Themes/theme/dark/rect-accent-hover.png differ diff --git a/TK-Themes/theme/dark/rect-accent.png b/TK-Themes/theme/dark/rect-accent.png new file mode 100644 index 0000000..a4821d1 Binary files /dev/null and b/TK-Themes/theme/dark/rect-accent.png differ diff --git a/TK-Themes/theme/dark/rect-basic.png b/TK-Themes/theme/dark/rect-basic.png new file mode 100644 index 0000000..8ab4d10 Binary files /dev/null and b/TK-Themes/theme/dark/rect-basic.png differ diff --git a/TK-Themes/theme/dark/rect-hover.png b/TK-Themes/theme/dark/rect-hover.png new file mode 100644 index 0000000..b9e4c35 Binary files /dev/null and b/TK-Themes/theme/dark/rect-hover.png differ diff --git a/TK-Themes/theme/dark/right.png b/TK-Themes/theme/dark/right.png new file mode 100644 index 0000000..bc840c2 Binary files /dev/null and b/TK-Themes/theme/dark/right.png differ diff --git a/TK-Themes/theme/dark/scale-hor.png b/TK-Themes/theme/dark/scale-hor.png new file mode 100644 index 0000000..570530e Binary files /dev/null and b/TK-Themes/theme/dark/scale-hor.png differ diff --git a/TK-Themes/theme/dark/scale-vert.png b/TK-Themes/theme/dark/scale-vert.png new file mode 100644 index 0000000..c6fcf6f Binary files /dev/null and b/TK-Themes/theme/dark/scale-vert.png differ diff --git a/TK-Themes/theme/dark/separator.png b/TK-Themes/theme/dark/separator.png new file mode 100644 index 0000000..411c970 Binary files /dev/null and b/TK-Themes/theme/dark/separator.png differ diff --git a/TK-Themes/theme/dark/size.png b/TK-Themes/theme/dark/size.png new file mode 100644 index 0000000..51c682e Binary files /dev/null and b/TK-Themes/theme/dark/size.png differ diff --git a/TK-Themes/theme/dark/tab-basic.png b/TK-Themes/theme/dark/tab-basic.png new file mode 100644 index 0000000..8c6d008 Binary files /dev/null and b/TK-Themes/theme/dark/tab-basic.png differ diff --git a/TK-Themes/theme/dark/tab-disabled.png b/TK-Themes/theme/dark/tab-disabled.png new file mode 100644 index 0000000..736c438 Binary files /dev/null and b/TK-Themes/theme/dark/tab-disabled.png differ diff --git a/TK-Themes/theme/dark/tab-hover.png b/TK-Themes/theme/dark/tab-hover.png new file mode 100644 index 0000000..213e82b Binary files /dev/null and b/TK-Themes/theme/dark/tab-hover.png differ diff --git a/TK-Themes/theme/dark/tick-hor-accent.png b/TK-Themes/theme/dark/tick-hor-accent.png new file mode 100644 index 0000000..3a90f28 Binary files /dev/null and b/TK-Themes/theme/dark/tick-hor-accent.png differ diff --git a/TK-Themes/theme/dark/tick-hor-basic.png b/TK-Themes/theme/dark/tick-hor-basic.png new file mode 100644 index 0000000..ee4b441 Binary files /dev/null and b/TK-Themes/theme/dark/tick-hor-basic.png differ diff --git a/TK-Themes/theme/dark/tick-hor-hover.png b/TK-Themes/theme/dark/tick-hor-hover.png new file mode 100644 index 0000000..bc859f4 Binary files /dev/null and b/TK-Themes/theme/dark/tick-hor-hover.png differ diff --git a/TK-Themes/theme/dark/tick-vert-accent.png b/TK-Themes/theme/dark/tick-vert-accent.png new file mode 100644 index 0000000..0c0ff98 Binary files /dev/null and b/TK-Themes/theme/dark/tick-vert-accent.png differ diff --git a/TK-Themes/theme/dark/tick-vert-basic.png b/TK-Themes/theme/dark/tick-vert-basic.png new file mode 100644 index 0000000..9e0a5ee Binary files /dev/null and b/TK-Themes/theme/dark/tick-vert-basic.png differ diff --git a/TK-Themes/theme/dark/tick-vert-hover.png b/TK-Themes/theme/dark/tick-vert-hover.png new file mode 100644 index 0000000..e9ff3a3 Binary files /dev/null and b/TK-Themes/theme/dark/tick-vert-hover.png differ diff --git a/TK-Themes/theme/dark/tree-basic.png b/TK-Themes/theme/dark/tree-basic.png new file mode 100644 index 0000000..c71808d Binary files /dev/null and b/TK-Themes/theme/dark/tree-basic.png differ diff --git a/TK-Themes/theme/dark/tree-pressed.png b/TK-Themes/theme/dark/tree-pressed.png new file mode 100644 index 0000000..41b65c6 Binary files /dev/null and b/TK-Themes/theme/dark/tree-pressed.png differ diff --git a/TK-Themes/theme/dark/up-accent.png b/TK-Themes/theme/dark/up-accent.png new file mode 100644 index 0000000..54a20f8 Binary files /dev/null and b/TK-Themes/theme/dark/up-accent.png differ diff --git a/TK-Themes/theme/dark/up.png b/TK-Themes/theme/dark/up.png new file mode 100644 index 0000000..069d440 Binary files /dev/null and b/TK-Themes/theme/dark/up.png differ diff --git a/TK-Themes/theme/dark/vert-accent.png b/TK-Themes/theme/dark/vert-accent.png new file mode 100644 index 0000000..495f804 Binary files /dev/null and b/TK-Themes/theme/dark/vert-accent.png differ diff --git a/TK-Themes/theme/dark/vert-basic.png b/TK-Themes/theme/dark/vert-basic.png new file mode 100644 index 0000000..4f6c46e Binary files /dev/null and b/TK-Themes/theme/dark/vert-basic.png differ diff --git a/TK-Themes/theme/dark/vert-hover.png b/TK-Themes/theme/dark/vert-hover.png new file mode 100644 index 0000000..142ccce Binary files /dev/null and b/TK-Themes/theme/dark/vert-hover.png differ diff --git a/TK-Themes/theme/light.tcl b/TK-Themes/theme/light.tcl new file mode 100644 index 0000000..dbd6dad --- /dev/null +++ b/TK-Themes/theme/light.tcl @@ -0,0 +1,539 @@ +# Copyright (c) 2021 rdbende + +# inspired by rdbende modified azure to water by Désire Werner Menrath polunga40@unity-mail.de 2024 + +package require Tk 8.6 + +namespace eval ttk::theme::water-light { + variable version 2.0 + package provide ttk::theme::water-light $version + + ttk::style theme create water-light -parent clam -settings { + proc load_images {imgdir} { + variable I + foreach file [glob -directory $imgdir *.png] { + set img [file tail [file rootname $file]] + set I($img) [image create photo -file $file -format png] + } + } + + load_images [file join [file dirname [info script]] light] + + array set colors { + -fg "#000000" + -bg "#ffffff" + -disabledfg "#737373" + -disabledbg "#ffffff" + -selectfg "#ffffff" + -selectbg "#007fff" + } + + ttk::style layout TButton { + Button.button -children { + Button.padding -children { + Button.label -side left -expand true + } + } + } + + ttk::style layout Toolbutton { + Toolbutton.button -children { + Toolbutton.padding -children { + Toolbutton.label -side left -expand true + } + } + } + + ttk::style layout TMenubutton { + Menubutton.button -children { + Menubutton.padding -children { + Menubutton.indicator -side right + Menubutton.label -side right -expand true + } + } + } + + ttk::style layout TOptionMenu { + OptionMenu.button -children { + OptionMenu.padding -children { + OptionMenu.indicator -side right + OptionMenu.label -side right -expand true + } + } + } + + ttk::style layout Accent.TButton { + AccentButton.button -children { + AccentButton.padding -children { + AccentButton.label -side left -expand true + } + } + } + + ttk::style layout TCheckbutton { + Checkbutton.button -children { + Checkbutton.padding -children { + Checkbutton.indicator -side left + Checkbutton.label -side right -expand true + } + } + } + + ttk::style layout Switch.TCheckbutton { + Switch.button -children { + Switch.padding -children { + Switch.indicator -side left + Switch.label -side right -expand true + } + } + } + + ttk::style layout Toggle.TButton { + ToggleButton.button -children { + ToggleButton.padding -children { + ToggleButton.label -side left -expand true + } + } + } + + ttk::style layout TRadiobutton { + Radiobutton.button -children { + Radiobutton.padding -children { + Radiobutton.indicator -side left + Radiobutton.label -side right -expand true + } + } + } + + ttk::style layout Vertical.TScrollbar { + Vertical.Scrollbar.trough -sticky ns -children { + Vertical.Scrollbar.thumb -expand true + } + } + + ttk::style layout Horizontal.TScrollbar { + Horizontal.Scrollbar.trough -sticky ew -children { + Horizontal.Scrollbar.thumb -expand true + } + } + + ttk::style layout TCombobox { + Combobox.field -sticky nswe -children { + Combobox.padding -expand true -sticky nswe -children { + Combobox.textarea -sticky nswe + } + } + Combobox.button -side right -sticky ns -children { + Combobox.arrow -sticky nsew + } + } + + ttk::style layout TSpinbox { + Spinbox.field -sticky nsew -children { + Spinbox.padding -expand true -sticky nswe -children { + Spinbox.textarea -sticky nswe + } + + } + Spinbox.button -side right -sticky ns -children { + null -side right -children { + Spinbox.uparrow -side top + Spinbox.downarrow -side bottom + } + } + } + + ttk::style layout Horizontal.TSeparator { + Horizontal.separator -sticky nswe + } + + ttk::style layout Vertical.TSeparator { + Vertical.separator -sticky nswe + } + + ttk::style layout Horizontal.Tick.TScale { + Horizontal.TickScale.trough -sticky ew -children { + Horizontal.TickScale.slider -sticky w + } + } + + ttk::style layout Vertical.Tick.TScale { + Vertical.TickScale.trough -sticky ns -children { + Vertical.TickScale.slider -sticky n + } + } + + ttk::style layout Card.TFrame { + Card.field { + Card.padding -expand 1 + } + } + + ttk::style layout TLabelframe { + Labelframe.border { + Labelframe.padding -expand 1 -children { + Labelframe.label -side right + } + } + } + + ttk::style layout TNotebook.Tab { + Notebook.tab -children { + Notebook.padding -side top -children { + Notebook.label -side top -sticky {} + } + } + } + + ttk::style layout Treeview.Item { + Treeitem.padding -sticky nswe -children { + Treeitem.indicator -side left -sticky {} + Treeitem.image -side left -sticky {} + Treeitem.text -side left -sticky {} + } + } + + + # Elements + + # Button + ttk::style configure TButton -padding {8 4 8 4} -width -10 -anchor center + + ttk::style element create Button.button image \ + [list $I(box-basic) \ + {selected disabled} $I(box-basic) \ + disabled $I(box-basic) \ + selected $I(box-basic) \ + pressed $I(box-basic) \ + active $I(button-hover) \ + focus $I(button-hover) \ + ] -border 4 -sticky ewns + + # Toolbutton + ttk::style configure Toolbutton -padding {8 4 8 4} -width -5 -anchor center + + ttk::style element create Toolbutton.button image \ + [list $I(empty) \ + {selected disabled} $I(empty) \ + disabled $I(empty) \ + selected $I(rect-basic) \ + pressed $I(rect-basic) \ + active $I(rect-basic) \ + ] -border 4 -sticky ewns + + # Menubutton + ttk::style configure TMenubutton -padding {8 4 4 4} + + ttk::style element create Menubutton.button \ + image [list $I(rect-basic) \ + disabled $I(rect-basic) \ + pressed $I(rect-basic) \ + active $I(button-hover) \ + ] -border 4 -sticky ewns + + ttk::style element create Menubutton.indicator \ + image [list $I(down) \ + active $I(down) \ + pressed $I(down) \ + disabled $I(down) \ + ] -width 15 -sticky e + + # OptionMenu + ttk::style configure TOptionMenu -padding {8 4 4 4} + + ttk::style element create OptionMenu.button \ + image [list $I(rect-basic) \ + disabled $I(rect-basic) \ + pressed $I(rect-basic) \ + active $I(button-hover) \ + ] -border 4 -sticky ewns + + ttk::style element create OptionMenu.indicator \ + image [list $I(down) \ + active $I(down) \ + pressed $I(down) \ + disabled $I(down) \ + ] -width 15 -sticky e + + # AccentButton + ttk::style configure Accent.TButton -padding {8 4 8 4} -width -10 -anchor center + + ttk::style element create AccentButton.button image \ + [list $I(rect-accent) \ + {selected disabled} $I(rect-accent-hover) \ + disabled $I(rect-accent-hover) \ + selected $I(rect-accent) \ + pressed $I(rect-accent) \ + active $I(rect-accent-hover) \ + focus $I(rect-accent-hover) \ + ] -border 4 -sticky ewns + + # Checkbutton + ttk::style configure TCheckbutton -padding 4 + + ttk::style element create Checkbutton.indicator image \ + [list $I(box-basic) \ + {alternate disabled} $I(check-tri-basic) \ + {selected disabled} $I(check-basic) \ + disabled $I(box-basic) \ + {pressed alternate} $I(check-tri-hover) \ + {active alternate} $I(check-tri-hover) \ + alternate $I(check-tri-accent) \ + {pressed selected} $I(check-hover) \ + {active selected} $I(check-hover) \ + selected $I(check-accent) \ + {pressed !selected} $I(rect-hover) \ + active $I(box-hover) \ + ] -width 26 -sticky w + + # Switch + ttk::style element create Switch.indicator image \ + [list $I(off-basic) \ + {selected disabled} $I(on-basic) \ + disabled $I(off-basic) \ + {pressed selected} $I(on-hover) \ + {active selected} $I(on-hover) \ + selected $I(on-accent) \ + {pressed !selected} $I(off-hover) \ + active $I(off-hover) \ + ] -width 46 -sticky w + + # ToggleButton + ttk::style configure Toggle.TButton -padding {8 4 8 4} -width -10 -anchor center + + ttk::style element create ToggleButton.button image \ + [list $I(rect-basic) \ + {selected disabled} $I(rect-accent-hover) \ + disabled $I(rect-basic) \ + {pressed selected} $I(rect-basic) \ + {active selected} $I(rect-accent) \ + selected $I(rect-accent) \ + {pressed !selected} $I(rect-accent) \ + active $I(rect-basic) \ + ] -border 4 -sticky ewns + + # Radiobutton + ttk::style configure TRadiobutton -padding 4 + + ttk::style element create Radiobutton.indicator image \ + [list $I(outline-basic) \ + {alternate disabled} $I(radio-tri-basic) \ + {selected disabled} $I(radio-basic) \ + disabled $I(outline-basic) \ + {pressed alternate} $I(radio-tri-hover) \ + {active alternate} $I(radio-tri-hover) \ + alternate $I(radio-tri-accent) \ + {pressed selected} $I(radio-hover) \ + {active selected} $I(radio-hover) \ + selected $I(radio-accent) \ + {pressed !selected} $I(circle-hover) \ + active $I(outline-hover) \ + ] -width 26 -sticky w + + # Scrollbar + ttk::style element create Horizontal.Scrollbar.trough image $I(hor-basic) \ + -sticky ew + + ttk::style element create Horizontal.Scrollbar.thumb \ + image [list $I(hor-accent) \ + disabled $I(hor-basic) \ + pressed $I(hor-hover) \ + active $I(hor-hover) \ + ] -sticky ew + + ttk::style element create Vertical.Scrollbar.trough image $I(vert-basic) \ + -sticky ns + + ttk::style element create Vertical.Scrollbar.thumb \ + image [list $I(vert-accent) \ + disabled $I(vert-basic) \ + pressed $I(vert-hover) \ + active $I(vert-hover) \ + ] -sticky ns + + # Scale + ttk::style element create Horizontal.Scale.trough image $I(scale-hor) \ + -border 5 -padding 0 + + ttk::style element create Horizontal.Scale.slider \ + image [list $I(circle-accent) \ + disabled $I(circle-basic) \ + pressed $I(circle-hover) \ + active $I(circle-hover) \ + ] -sticky {} + + ttk::style element create Vertical.Scale.trough image $I(scale-vert) \ + -border 5 -padding 0 + + ttk::style element create Vertical.Scale.slider \ + image [list $I(circle-accent) \ + disabled $I(circle-basic) \ + pressed $I(circle-hover) \ + active $I(circle-hover) \ + ] -sticky {} + + # Tickscale + ttk::style element create Horizontal.TickScale.trough image $I(scale-hor) \ + -border 5 -padding 0 + + ttk::style element create Horizontal.TickScale.slider \ + image [list $I(tick-hor-accent) \ + disabled $I(tick-hor-basic) \ + pressed $I(tick-hor-hover) \ + active $I(tick-hor-hover) \ + ] -sticky {} + + ttk::style element create Vertical.TickScale.trough image $I(scale-vert) \ + -border 5 -padding 0 + + ttk::style element create Vertical.TickScale.slider \ + image [list $I(tick-vert-accent) \ + disabled $I(tick-vert-basic) \ + pressed $I(tick-vert-hover) \ + active $I(tick-vert-hover) \ + ] -sticky {} + + # Progressbar + ttk::style element create Horizontal.Progressbar.trough image $I(hor-basic) \ + -sticky ew + + ttk::style element create Horizontal.Progressbar.pbar image $I(hor-accent) \ + -sticky ew + + ttk::style element create Vertical.Progressbar.trough image $I(vert-basic) \ + -sticky ns + + ttk::style element create Vertical.Progressbar.pbar image $I(vert-accent) \ + -sticky ns + + # Entry + ttk::style element create Entry.field \ + image [list $I(box-basic) \ + {focus hover} $I(box-accent) \ + invalid $I(box-invalid) \ + disabled $I(box-basic) \ + focus $I(box-accent) \ + hover $I(box-hover) \ + ] -border 5 -padding {8} -sticky news + + # Combobox + ttk::style map TCombobox -selectbackground [list \ + {!focus} $colors(-selectbg) \ + {readonly hover} $colors(-selectbg) \ + {readonly focus} $colors(-selectbg) \ + ] + + ttk::style map TCombobox -selectforeground [list \ + {!focus} $colors(-selectfg) \ + {readonly hover} $colors(-selectfg) \ + {readonly focus} $colors(-selectfg) \ + ] + + ttk::style element create Combobox.field \ + image [list $I(box-basic) \ + {readonly disabled} $I(rect-basic) \ + {readonly pressed} $I(rect-basic) \ + {readonly focus hover} $I(button-hover) \ + {readonly focus} $I(button-hover) \ + {readonly hover} $I(button-hover) \ + {focus hover} $I(box-accent) \ + readonly $I(rect-basic) \ + invalid $I(box-invalid) \ + disabled $I(box-basic) \ + focus $I(box-accent) \ + hover $I(box-hover) \ + ] -border 5 -padding {8} + + ttk::style element create Combobox.button \ + image [list $I(combo-button-basic) \ + {!readonly focus} $I(combo-button-focus) \ + {readonly focus} $I(combo-button-hover) \ + {readonly hover} $I(combo-button-hover) + ] -border 5 -padding {2 6 6 6} + + ttk::style element create Combobox.arrow image $I(down) \ + -width 15 -sticky e + + # Spinbox + ttk::style element create Spinbox.field \ + image [list $I(box-basic) \ + invalid $I(box-invalid) \ + disabled $I(box-basic) \ + focus $I(box-accent) \ + hover $I(box-hover) \ + ] -border 5 -padding {8} -sticky news + + ttk::style element create Spinbox.uparrow \ + image [list $I(up) \ + disabled $I(up) \ + pressed $I(up-accent) \ + active $I(up-accent) \ + ] -border 4 -width 15 -sticky e + + ttk::style element create Spinbox.downarrow \ + image [list $I(down) \ + disabled $I(down) \ + pressed $I(down-accent) \ + active $I(down-accent) \ + ] -border 4 -width 15 -sticky e + + ttk::style element create Spinbox.button \ + image [list $I(combo-button-basic) \ + {!readonly focus} $I(combo-button-focus) \ + {readonly focus} $I(combo-button-hover) \ + {readonly hover} $I(combo-button-hover) + ] -border 5 -padding {2 6 6 6} + + # Sizegrip + ttk::style element create Sizegrip.sizegrip image $I(size) \ + -sticky ewns + + # Separator + ttk::style element create Horizontal.separator image $I(separator) + + ttk::style element create Vertical.separator image $I(separator) + + # Card + ttk::style element create Card.field image $I(card) \ + -border 10 -padding 4 -sticky news + + # Labelframe + ttk::style element create Labelframe.border image $I(card) \ + -border 5 -padding 4 -sticky news + + # Notebook + ttk::style element create Notebook.client \ + image $I(notebook) -border 5 + + ttk::style element create Notebook.tab \ + image [list $I(tab-disabled) \ + selected $I(tab-basic) \ + active $I(tab-hover) \ + ] -border 5 -padding {14 4} + + # Treeview + ttk::style element create Treeview.field image $I(card) \ + -border 5 + + ttk::style element create Treeheading.cell \ + image [list $I(tree-basic) \ + pressed $I(tree-pressed) + ] -border 5 -padding 4 -sticky ewns + + ttk::style element create Treeitem.indicator \ + image [list $I(right) \ + user2 $I(empty) \ + user1 $I(down) \ + ] -width 26 -sticky {} + + ttk::style configure Treeview -background $colors(-bg) + ttk::style configure Treeview.Item -padding {2 0 0 0} + ttk::style map Treeview \ + -background [list selected #ccc] \ + -foreground [list selected $colors(-fg)] + + # Panedwindow + # Insane hack to remove clam's ugly sash + ttk::style configure Sash -gripcount 0 + } +} diff --git a/TK-Themes/theme/light/box-accent.png b/TK-Themes/theme/light/box-accent.png new file mode 100644 index 0000000..70c1e38 Binary files /dev/null and b/TK-Themes/theme/light/box-accent.png differ diff --git a/TK-Themes/theme/light/box-basic.png b/TK-Themes/theme/light/box-basic.png new file mode 100644 index 0000000..090a00b Binary files /dev/null and b/TK-Themes/theme/light/box-basic.png differ diff --git a/TK-Themes/theme/light/box-hover.png b/TK-Themes/theme/light/box-hover.png new file mode 100644 index 0000000..e691da4 Binary files /dev/null and b/TK-Themes/theme/light/box-hover.png differ diff --git a/TK-Themes/theme/light/box-invalid.png b/TK-Themes/theme/light/box-invalid.png new file mode 100644 index 0000000..1f16f5c Binary files /dev/null and b/TK-Themes/theme/light/box-invalid.png differ diff --git a/TK-Themes/theme/light/button-hover.png b/TK-Themes/theme/light/button-hover.png new file mode 100644 index 0000000..13366eb Binary files /dev/null and b/TK-Themes/theme/light/button-hover.png differ diff --git a/TK-Themes/theme/light/card.png b/TK-Themes/theme/light/card.png new file mode 100644 index 0000000..09152f5 Binary files /dev/null and b/TK-Themes/theme/light/card.png differ diff --git a/TK-Themes/theme/light/check-accent.png b/TK-Themes/theme/light/check-accent.png new file mode 100644 index 0000000..67de0a6 Binary files /dev/null and b/TK-Themes/theme/light/check-accent.png differ diff --git a/TK-Themes/theme/light/check-basic.png b/TK-Themes/theme/light/check-basic.png new file mode 100644 index 0000000..0c00612 Binary files /dev/null and b/TK-Themes/theme/light/check-basic.png differ diff --git a/TK-Themes/theme/light/check-hover.png b/TK-Themes/theme/light/check-hover.png new file mode 100644 index 0000000..3811696 Binary files /dev/null and b/TK-Themes/theme/light/check-hover.png differ diff --git a/TK-Themes/theme/light/check-tri-accent.png b/TK-Themes/theme/light/check-tri-accent.png new file mode 100644 index 0000000..c02f75d Binary files /dev/null and b/TK-Themes/theme/light/check-tri-accent.png differ diff --git a/TK-Themes/theme/light/check-tri-basic.png b/TK-Themes/theme/light/check-tri-basic.png new file mode 100644 index 0000000..e92bea5 Binary files /dev/null and b/TK-Themes/theme/light/check-tri-basic.png differ diff --git a/TK-Themes/theme/light/check-tri-hover.png b/TK-Themes/theme/light/check-tri-hover.png new file mode 100644 index 0000000..d611d76 Binary files /dev/null and b/TK-Themes/theme/light/check-tri-hover.png differ diff --git a/TK-Themes/theme/light/circle-accent.png b/TK-Themes/theme/light/circle-accent.png new file mode 100644 index 0000000..b2202e2 Binary files /dev/null and b/TK-Themes/theme/light/circle-accent.png differ diff --git a/TK-Themes/theme/light/circle-basic.png b/TK-Themes/theme/light/circle-basic.png new file mode 100644 index 0000000..b16202e Binary files /dev/null and b/TK-Themes/theme/light/circle-basic.png differ diff --git a/TK-Themes/theme/light/circle-hover.png b/TK-Themes/theme/light/circle-hover.png new file mode 100644 index 0000000..6ea1803 Binary files /dev/null and b/TK-Themes/theme/light/circle-hover.png differ diff --git a/TK-Themes/theme/light/combo-button-basic.png b/TK-Themes/theme/light/combo-button-basic.png new file mode 100644 index 0000000..b7daa04 Binary files /dev/null and b/TK-Themes/theme/light/combo-button-basic.png differ diff --git a/TK-Themes/theme/light/combo-button-focus.png b/TK-Themes/theme/light/combo-button-focus.png new file mode 100644 index 0000000..521aef0 Binary files /dev/null and b/TK-Themes/theme/light/combo-button-focus.png differ diff --git a/TK-Themes/theme/light/combo-button-hover.png b/TK-Themes/theme/light/combo-button-hover.png new file mode 100644 index 0000000..1d06c57 Binary files /dev/null and b/TK-Themes/theme/light/combo-button-hover.png differ diff --git a/TK-Themes/theme/light/down-accent.png b/TK-Themes/theme/light/down-accent.png new file mode 100644 index 0000000..5bb987d Binary files /dev/null and b/TK-Themes/theme/light/down-accent.png differ diff --git a/TK-Themes/theme/light/down.png b/TK-Themes/theme/light/down.png new file mode 100644 index 0000000..1fd7e4f Binary files /dev/null and b/TK-Themes/theme/light/down.png differ diff --git a/TK-Themes/theme/light/empty.png b/TK-Themes/theme/light/empty.png new file mode 100644 index 0000000..202e3de Binary files /dev/null and b/TK-Themes/theme/light/empty.png differ diff --git a/TK-Themes/theme/light/hor-accent.png b/TK-Themes/theme/light/hor-accent.png new file mode 100644 index 0000000..20ea079 Binary files /dev/null and b/TK-Themes/theme/light/hor-accent.png differ diff --git a/TK-Themes/theme/light/hor-basic.png b/TK-Themes/theme/light/hor-basic.png new file mode 100644 index 0000000..eb18d1e Binary files /dev/null and b/TK-Themes/theme/light/hor-basic.png differ diff --git a/TK-Themes/theme/light/hor-hover.png b/TK-Themes/theme/light/hor-hover.png new file mode 100644 index 0000000..4d6c0f1 Binary files /dev/null and b/TK-Themes/theme/light/hor-hover.png differ diff --git a/TK-Themes/theme/light/notebook.png b/TK-Themes/theme/light/notebook.png new file mode 100644 index 0000000..430d9f2 Binary files /dev/null and b/TK-Themes/theme/light/notebook.png differ diff --git a/TK-Themes/theme/light/off-basic.png b/TK-Themes/theme/light/off-basic.png new file mode 100644 index 0000000..cf383f0 Binary files /dev/null and b/TK-Themes/theme/light/off-basic.png differ diff --git a/TK-Themes/theme/light/off-hover.png b/TK-Themes/theme/light/off-hover.png new file mode 100644 index 0000000..893b39c Binary files /dev/null and b/TK-Themes/theme/light/off-hover.png differ diff --git a/TK-Themes/theme/light/on-accent.png b/TK-Themes/theme/light/on-accent.png new file mode 100644 index 0000000..31ee2fc Binary files /dev/null and b/TK-Themes/theme/light/on-accent.png differ diff --git a/TK-Themes/theme/light/on-basic.png b/TK-Themes/theme/light/on-basic.png new file mode 100644 index 0000000..23ec72f Binary files /dev/null and b/TK-Themes/theme/light/on-basic.png differ diff --git a/TK-Themes/theme/light/on-hover.png b/TK-Themes/theme/light/on-hover.png new file mode 100644 index 0000000..d55f7ae Binary files /dev/null and b/TK-Themes/theme/light/on-hover.png differ diff --git a/TK-Themes/theme/light/outline-basic.png b/TK-Themes/theme/light/outline-basic.png new file mode 100644 index 0000000..2e79874 Binary files /dev/null and b/TK-Themes/theme/light/outline-basic.png differ diff --git a/TK-Themes/theme/light/outline-hover.png b/TK-Themes/theme/light/outline-hover.png new file mode 100644 index 0000000..502915d Binary files /dev/null and b/TK-Themes/theme/light/outline-hover.png differ diff --git a/TK-Themes/theme/light/radio-accent.png b/TK-Themes/theme/light/radio-accent.png new file mode 100644 index 0000000..4daef1d Binary files /dev/null and b/TK-Themes/theme/light/radio-accent.png differ diff --git a/TK-Themes/theme/light/radio-basic.png b/TK-Themes/theme/light/radio-basic.png new file mode 100644 index 0000000..793531e Binary files /dev/null and b/TK-Themes/theme/light/radio-basic.png differ diff --git a/TK-Themes/theme/light/radio-hover.png b/TK-Themes/theme/light/radio-hover.png new file mode 100644 index 0000000..d6faa74 Binary files /dev/null and b/TK-Themes/theme/light/radio-hover.png differ diff --git a/TK-Themes/theme/light/radio-tri-accent.png b/TK-Themes/theme/light/radio-tri-accent.png new file mode 100644 index 0000000..806d575 Binary files /dev/null and b/TK-Themes/theme/light/radio-tri-accent.png differ diff --git a/TK-Themes/theme/light/radio-tri-basic.png b/TK-Themes/theme/light/radio-tri-basic.png new file mode 100644 index 0000000..da85d03 Binary files /dev/null and b/TK-Themes/theme/light/radio-tri-basic.png differ diff --git a/TK-Themes/theme/light/radio-tri-hover.png b/TK-Themes/theme/light/radio-tri-hover.png new file mode 100644 index 0000000..ded14a6 Binary files /dev/null and b/TK-Themes/theme/light/radio-tri-hover.png differ diff --git a/TK-Themes/theme/light/rect-accent-hover.png b/TK-Themes/theme/light/rect-accent-hover.png new file mode 100644 index 0000000..5daa96a Binary files /dev/null and b/TK-Themes/theme/light/rect-accent-hover.png differ diff --git a/TK-Themes/theme/light/rect-accent.png b/TK-Themes/theme/light/rect-accent.png new file mode 100644 index 0000000..8b3f822 Binary files /dev/null and b/TK-Themes/theme/light/rect-accent.png differ diff --git a/TK-Themes/theme/light/rect-basic.png b/TK-Themes/theme/light/rect-basic.png new file mode 100644 index 0000000..239ca31 Binary files /dev/null and b/TK-Themes/theme/light/rect-basic.png differ diff --git a/TK-Themes/theme/light/rect-hover.png b/TK-Themes/theme/light/rect-hover.png new file mode 100644 index 0000000..9252c4f Binary files /dev/null and b/TK-Themes/theme/light/rect-hover.png differ diff --git a/TK-Themes/theme/light/right.png b/TK-Themes/theme/light/right.png new file mode 100644 index 0000000..8122cc9 Binary files /dev/null and b/TK-Themes/theme/light/right.png differ diff --git a/TK-Themes/theme/light/scale-hor.png b/TK-Themes/theme/light/scale-hor.png new file mode 100644 index 0000000..d11f508 Binary files /dev/null and b/TK-Themes/theme/light/scale-hor.png differ diff --git a/TK-Themes/theme/light/scale-vert.png b/TK-Themes/theme/light/scale-vert.png new file mode 100644 index 0000000..f78595d Binary files /dev/null and b/TK-Themes/theme/light/scale-vert.png differ diff --git a/TK-Themes/theme/light/separator.png b/TK-Themes/theme/light/separator.png new file mode 100644 index 0000000..7bffc9a Binary files /dev/null and b/TK-Themes/theme/light/separator.png differ diff --git a/TK-Themes/theme/light/size.png b/TK-Themes/theme/light/size.png new file mode 100644 index 0000000..bde3ade Binary files /dev/null and b/TK-Themes/theme/light/size.png differ diff --git a/TK-Themes/theme/light/tab-basic.png b/TK-Themes/theme/light/tab-basic.png new file mode 100644 index 0000000..365fdff Binary files /dev/null and b/TK-Themes/theme/light/tab-basic.png differ diff --git a/TK-Themes/theme/light/tab-disabled.png b/TK-Themes/theme/light/tab-disabled.png new file mode 100644 index 0000000..eeee518 Binary files /dev/null and b/TK-Themes/theme/light/tab-disabled.png differ diff --git a/TK-Themes/theme/light/tab-hover.png b/TK-Themes/theme/light/tab-hover.png new file mode 100644 index 0000000..5003806 Binary files /dev/null and b/TK-Themes/theme/light/tab-hover.png differ diff --git a/TK-Themes/theme/light/tick-hor-accent.png b/TK-Themes/theme/light/tick-hor-accent.png new file mode 100644 index 0000000..aa32534 Binary files /dev/null and b/TK-Themes/theme/light/tick-hor-accent.png differ diff --git a/TK-Themes/theme/light/tick-hor-basic.png b/TK-Themes/theme/light/tick-hor-basic.png new file mode 100644 index 0000000..ba727d7 Binary files /dev/null and b/TK-Themes/theme/light/tick-hor-basic.png differ diff --git a/TK-Themes/theme/light/tick-hor-hover.png b/TK-Themes/theme/light/tick-hor-hover.png new file mode 100644 index 0000000..cb66b59 Binary files /dev/null and b/TK-Themes/theme/light/tick-hor-hover.png differ diff --git a/TK-Themes/theme/light/tick-vert-accent.png b/TK-Themes/theme/light/tick-vert-accent.png new file mode 100644 index 0000000..53c102d Binary files /dev/null and b/TK-Themes/theme/light/tick-vert-accent.png differ diff --git a/TK-Themes/theme/light/tick-vert-basic.png b/TK-Themes/theme/light/tick-vert-basic.png new file mode 100644 index 0000000..a58440d Binary files /dev/null and b/TK-Themes/theme/light/tick-vert-basic.png differ diff --git a/TK-Themes/theme/light/tick-vert-hover.png b/TK-Themes/theme/light/tick-vert-hover.png new file mode 100644 index 0000000..18cbec5 Binary files /dev/null and b/TK-Themes/theme/light/tick-vert-hover.png differ diff --git a/TK-Themes/theme/light/tree-basic.png b/TK-Themes/theme/light/tree-basic.png new file mode 100644 index 0000000..755062b Binary files /dev/null and b/TK-Themes/theme/light/tree-basic.png differ diff --git a/TK-Themes/theme/light/tree-pressed.png b/TK-Themes/theme/light/tree-pressed.png new file mode 100644 index 0000000..ee28416 Binary files /dev/null and b/TK-Themes/theme/light/tree-pressed.png differ diff --git a/TK-Themes/theme/light/up-accent.png b/TK-Themes/theme/light/up-accent.png new file mode 100644 index 0000000..54a20f8 Binary files /dev/null and b/TK-Themes/theme/light/up-accent.png differ diff --git a/TK-Themes/theme/light/up.png b/TK-Themes/theme/light/up.png new file mode 100644 index 0000000..16568f2 Binary files /dev/null and b/TK-Themes/theme/light/up.png differ diff --git a/TK-Themes/theme/light/vert-accent.png b/TK-Themes/theme/light/vert-accent.png new file mode 100644 index 0000000..58b0c32 Binary files /dev/null and b/TK-Themes/theme/light/vert-accent.png differ diff --git a/TK-Themes/theme/light/vert-basic.png b/TK-Themes/theme/light/vert-basic.png new file mode 100644 index 0000000..d5f61ec Binary files /dev/null and b/TK-Themes/theme/light/vert-basic.png differ diff --git a/TK-Themes/theme/light/vert-hover.png b/TK-Themes/theme/light/vert-hover.png new file mode 100644 index 0000000..bfdc9d0 Binary files /dev/null and b/TK-Themes/theme/light/vert-hover.png differ diff --git a/TK-Themes/water.tcl b/TK-Themes/water.tcl new file mode 100755 index 0000000..ac96081 --- /dev/null +++ b/TK-Themes/water.tcl @@ -0,0 +1,88 @@ +# Copyright © 2021 rdbende +# inspired by rdbende modified azure to water by Désire Werner Menrath polunga40@unity-mail.de 2024 + +source [file join [file dirname [info script]] theme light.tcl] +source [file join [file dirname [info script]] theme dark.tcl] + +option add *tearOff 0 + +proc set_theme {mode} { + if {$mode == "dark"} { + ttk::style theme use "water-dark" + + array set colors { + -fg "#ffffff" + -bg "#333333" + -disabledfg "#ffffff" + -disabledbg "#737373" + -selectfg "#000000" + -selectbg "#00c4ff" + } + + ttk::style configure . \ + -background $colors(-bg) \ + -foreground $colors(-fg) \ + -troughcolor $colors(-bg) \ + -focuscolor $colors(-selectbg) \ + -selectbackground $colors(-selectbg) \ + -selectforeground $colors(-selectfg) \ + -insertcolor $colors(-fg) \ + -insertwidth 1 \ + -fieldbackground $colors(-selectbg) \ + -font {"Segoe Ui" 10} \ + -borderwidth 1 \ + -relief flat + + tk_setPalette background [ttk::style lookup . -background] \ + foreground [ttk::style lookup . -foreground] \ + highlightColor [ttk::style lookup . -focuscolor] \ + selectBackground [ttk::style lookup . -selectbackground] \ + selectForeground [ttk::style lookup . -selectforeground] \ + activeBackground [ttk::style lookup . -selectbackground] \ + activeForeground [ttk::style lookup . -selectforeground] + + ttk::style map . -foreground [list disabled $colors(-disabledfg)] + + option add *font [ttk::style lookup . -font] + option add *Menu.selectcolor $colors(-fg) + + } elseif {$mode == "light"} { + ttk::style theme use "water-light" + + array set colors { + -fg "#000000" + -bg "#ffffff" + -disabledfg "#737373" + -disabledbg "#ffffff" + -selectfg "#000000" + -selectbg "#00c4ff" + } + + ttk::style configure . \ + -background $colors(-bg) \ + -foreground $colors(-fg) \ + -troughcolor $colors(-bg) \ + -focuscolor $colors(-selectbg) \ + -selectbackground $colors(-selectbg) \ + -selectforeground $colors(-selectfg) \ + -insertcolor $colors(-fg) \ + -insertwidth 1 \ + -fieldbackground $colors(-selectbg) \ + -font {"Segoe Ui" 10} \ + -borderwidth 1 \ + -relief flat + + tk_setPalette background [ttk::style lookup . -background] \ + foreground [ttk::style lookup . -foreground] \ + highlightColor [ttk::style lookup . -focuscolor] \ + selectBackground [ttk::style lookup . -selectbackground] \ + selectForeground [ttk::style lookup . -selectforeground] \ + activeBackground [ttk::style lookup . -selectbackground] \ + activeForeground [ttk::style lookup . -selectforeground] + + ttk::style map . -foreground [list disabled $colors(-disabledfg)] + + option add *font [ttk::style lookup . -font] + option add *Menu.selectcolor $colors(-fg) + } +} diff --git a/common_tools.py b/common_tools.py new file mode 100755 index 0000000..ae495a1 --- /dev/null +++ b/common_tools.py @@ -0,0 +1,573 @@ +""" Classes Method and Functions for lx Apps """ + +import logging +import signal +import base64 +from subprocess import CompletedProcess, run +import re +import sys +import shutil +import tkinter as tk +from typing import Optional, Dict, Any, NoReturn +from pathlib import Path +from tkinter import ttk, Toplevel + + +class CryptoUtil: + """ + This class is for the creation of the folders and files + required by Wire-Py, as well as for decryption + the tunnel from the user's home directory + """ + + @staticmethod + def decrypt(user, path) -> None: + """ + Starts SSL dencrypt + """ + if len([file.stem for file in path.glob("*.dat")]) == 0: + pass + else: + process: CompletedProcess[str] = run( + ["pkexec", "/usr/local/bin/ssl_decrypt.py", "--user", user], + capture_output=True, + text=True, + check=False, + ) + + # Output from Openssl Error + if process.stderr: + logging.error(process.stderr, exc_info=True) + + if process.returncode == 0: + logging.info("Files successfully decrypted...", exc_info=True) + else: + logging.error( + f"Error process decrypt: Code {process.returncode}", exc_info=True + ) + + @staticmethod + def encrypt(user) -> None: + """ + Starts SSL encryption + """ + process: CompletedProcess[str] = run( + ["pkexec", "/usr/local/bin/ssl_encrypt.py", "--user", user], + capture_output=True, + text=True, + check=False, + ) + + # Output from Openssl Error + if process.stderr: + logging.error(process.stderr, exc_info=True) + + if process.returncode == 0: + logging.info("Files successfully encrypted...", exc_info=True) + else: + logging.error( + f"Error process encrypt: Code {process.returncode}", exc_info=True + ) + + @staticmethod + def find_key(key: str = "") -> bool: + """ + Checks if the private key already exists in the system using an external script. + Returns True only if the full key is found exactly (no partial match). + """ + process: CompletedProcess[str] = run( + ["pkexec", "/usr/local/bin/match_found.py", key], + capture_output=True, + text=True, + check=False, + ) + if "True" in process.stdout: + return True + elif "False" in process.stdout: + return False + logging.error( + f"Unexpected output from the external script:\nSTDOUT: {process.stdout}\nSTDERR: {process.stderr}", + exc_info=True, + ) + return False + + @staticmethod + def is_valid_base64(key: str) -> bool: + """ + Validates if the input is a valid Base64 string (WireGuard private key format). + Returns True only for non-empty strings that match the expected length. + """ + # Check for empty string + if not key or key.strip() == "": + return False + + # Regex pattern to validate Base64: [A-Za-z0-9+/]+={0,2} + base64_pattern = r"^[A-Za-z0-9+/]+={0,2}$" + if not re.match(base64_pattern, key): + return False + + try: + # Decode and check length (WireGuard private keys are 32 bytes long) + decoded = base64.b64decode(key) + if len(decoded) != 32: # 32 bytes = 256 bits + return False + except Exception as e: + logging.error(f"Error on decode Base64: {e}", exc_info=True) + return False + + return True + + +class LxTools: + """ + Class LinuxTools methods that can also be used for other apps + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + @staticmethod + def center_window_cross_platform(window, width, height): + """ + Centers a window on the primary monitor in a way that works on both X11 and Wayland + + Args: + window: The tkinter window to center + width: Window width + height: Window height + """ + # Calculate the position before showing the window + + # First attempt: Try to use GDK if available (works on both X11 and Wayland) + try: + import gi + + gi.require_version("Gdk", "3.0") + from gi.repository import Gdk + + display = Gdk.Display.get_default() + monitor = display.get_primary_monitor() or display.get_monitor(0) + geometry = monitor.get_geometry() + scale_factor = monitor.get_scale_factor() + + # Calculate center position on the primary monitor + x = geometry.x + (geometry.width - width // scale_factor) // 2 + y = geometry.y + (geometry.height - height // scale_factor) // 2 + + # Set window geometry + window.geometry(f"{width}x{height}+{x}+{y}") + return + except (ImportError, AttributeError): + pass + + # Second attempt: Try xrandr for X11 + try: + import subprocess + + output = subprocess.check_output( + ["xrandr", "--query"], universal_newlines=True + ) + + # Parse the output to find the primary monitor + primary_info = None + for line in output.splitlines(): + if "primary" in line: + parts = line.split() + for part in parts: + if "x" in part and "+" in part: + primary_info = part + break + break + + if primary_info: + # Parse the geometry: WIDTH x HEIGHT+X+Y + geometry = primary_info.split("+") + dimensions = geometry[0].split("x") + primary_width = int(dimensions[0]) + primary_height = int(dimensions[1]) + primary_x = int(geometry[1]) + primary_y = int(geometry[2]) + + # Calculate center position on the primary monitor + x = primary_x + (primary_width - width) // 2 + y = primary_y + (primary_height - height) // 2 + + # Set window geometry + window.geometry(f"{width}x{height}+{x}+{y}") + return + except (ImportError, IndexError, ValueError): + pass + + # Final fallback: Use standard Tkinter method + screen_width = window.winfo_screenwidth() + screen_height = window.winfo_screenheight() + + # Try to make an educated guess for multi-monitor setups + # If screen width is much larger than height, assume multiple monitors side by side + if ( + screen_width > screen_height * 1.8 + ): # Heuristic for detecting multiple monitors + # Assume the primary monitor is on the left half + screen_width = screen_width // 2 + + x = (screen_width - width) // 2 + y = (screen_height - height) // 2 + window.geometry(f"{width}x{height}+{x}+{y}") + + @staticmethod + def clean_files(tmp_dir: Path = None, file: Path = None) -> None: + """ + Deletes temporary files and directories for cleanup when exiting the application. + + This method safely removes an optional directory defined by `AppConfig.TEMP_DIR` + and a single file to free up resources at the end of the program's execution. + All operations are performed securely, and errors such as `FileNotFoundError` + are ignored if the target files or directories do not exist. + :param tmp_dir: (Path, optional): Path to the temporary directory that should be deleted. + If `None`, the value of `AppConfig.TEMP_DIR` is used. + :param file: (Path, optional): Path to the file that should be deleted. + If `None`, no additional file will be deleted. + + Returns: + None: The method does not return any value. + """ + + if tmp_dir is not None: + shutil.rmtree(tmp_dir, ignore_errors=True) + try: + if file is not None: + Path.unlink(file) + + except FileNotFoundError: + pass + + @staticmethod + def msg_window( + image_path: Path, + image_path2: Path, + w_title: str, + w_txt: str, + txt2: Optional[str] = None, + com: Optional[str] = None, + ) -> None: + """ + Creates message windows + + :param image_path2: + :param image_path: + AppConfig.IMAGE_PATHS["icon_info"] = Image for TK window which is displayed to the left of the text + AppConfig.IMAGE_PATHS["icon_vpn"] = Image for Task Icon + :argument w_title = Windows Title + :argument w_txt = Text for Tk Window + :argument txt2 = Text for Button two + :argument com = function for Button command + """ + msg: tk.Toplevel = tk.Toplevel() + msg.resizable(width=False, height=False) + msg.title(w_title) + msg.configure(pady=15, padx=15) + + # load first image for a window + try: + msg.img = tk.PhotoImage(file=image_path) + msg.i_window = tk.Label(msg, image=msg.img) + except Exception as e: + logging.error(f"Error on load Window Image: {e}", exc_info=True) + msg.i_window = tk.Label(msg, text="Image not found") + + label: tk.Label = tk.Label(msg, text=w_txt) + label.grid(column=1, row=0) + + if txt2 is not None and com is not None: + label.config(font=("Ubuntu", 11), padx=15, justify="left") + msg.i_window.grid(column=0, row=0, sticky="nw") + button2: ttk.Button = ttk.Button( + msg, text=f"{txt2}", command=com, padding=4 + ) + button2.grid(column=0, row=1, sticky="e", columnspan=2) + button: ttk.Button = ttk.Button( + msg, text="OK", command=msg.destroy, padding=4 + ) + button.grid(column=0, row=1, sticky="w", columnspan=2) + else: + label.config(font=("Ubuntu", 11), padx=15) + msg.i_window.grid(column=0, row=0) + button: ttk.Button = ttk.Button( + msg, text="OK", command=msg.destroy, padding=4 + ) + button.grid(column=0, columnspan=2, row=1) + + try: + icon = tk.PhotoImage(file=image_path2) + msg.iconphoto(True, icon) + except Exception as e: + logging.error(f"Error loading the window icon: {e}", exc_info=True) + + msg.columnconfigure(0, weight=1) + msg.rowconfigure(0, weight=1) + msg.winfo_toplevel() + + @staticmethod + def sigi(file_path: Optional[Path] = None, file: Optional[Path] = None) -> None: + """ + Function for cleanup after a program interruption + + :param file: Optional - File to be deleted + :param file_path: Optional - Directory to be deleted + """ + + def signal_handler(signum: int, frame: Any) -> NoReturn: + """ + Determines clear text names for signal numbers and handles signals + + Args: + signum: The signal number + frame: The current stack frame + + Returns: + NoReturn since the function either exits the program or continues execution + """ + + signals_to_names_dict: Dict[int, str] = dict( + (getattr(signal, n), n) + for n in dir(signal) + if n.startswith("SIG") and "_" not in n + ) + + signal_name: str = signals_to_names_dict.get( + signum, f"Unnamed signal: {signum}" + ) + + # End program for certain signals, report to others only reception + if signum in (signal.SIGINT, signal.SIGTERM): + exit_code: int = 1 + logging.error( + f"\nSignal {signal_name} {signum} received. => Aborting with exit code {exit_code}.", + exc_info=True, + ) + LxTools.clean_files(file_path, file) + logging.info("Breakdown by user...") + sys.exit(exit_code) + else: + logging.info(f"Signal {signum} received and ignored.") + LxTools.clean_files(file_path, file) + logging.error("Process unexpectedly ended...") + + # Register signal handlers for various signals + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGHUP, signal_handler) + + +# ConfigManager with caching +class ConfigManager: + """ + Universal class for managing configuration files with caching support. + + This class provides a general solution to load, save, and manage configuration + files across different projects. It uses a caching system to optimize access efficiency. + The `init()` method initializes the configuration file path, while `load()` and `save()` + synchronize data between the file and internal memory structures. + + Key Features: + - Caching to minimize I/O operations. + - Default values for missing or corrupted configuration files. + - Reusability across different projects and use cases. + + The class is designed for central application configuration management, working closely + with `ThemeManager` to dynamically manage themes or other settings. + """ + + _config = None + _config_file = None + + @classmethod + def init(cls, config_file): + """Initial the Configmanager with the given config file""" + cls._config_file = config_file + cls._config = None # Reset the cache + + @classmethod + def load(cls): + """Load the config file and return the config as dict""" + if not cls._config: + try: + lines = Path(cls._config_file).read_text(encoding="utf-8").splitlines() + cls._config = { + "updates": lines[1].strip(), + "theme": lines[3].strip(), + "tooltips": lines[5].strip() + == "True", # is converted here to boolean!!! + "autostart": lines[7].strip() if len(lines) > 7 else "off", + "logfile": lines[9].strip(), + } + except (IndexError, FileNotFoundError): + # DeDefault values in case of error + cls._config = { + "updates": "on", + "theme": "light", + "tooltips": "True", # Default Value as string! + "autostart": "off", + "logfile": LOG_FILE_PATH, + } + return cls._config + + @classmethod + def save(cls): + """Save the config to the config file""" + if cls._config: + lines = [ + "# Configuration\n", + f"{cls._config['updates']}\n", + "# Theme\n", + f"{cls._config['theme']}\n", + "# Tooltips\n", + f"{str(cls._config['tooltips'])}\n", + "# Autostart\n", + f"{cls._config['autostart']}\n", + "# Logfile\n", + f"{cls._config['logfile']}\n", + ] + Path(cls._config_file).write_text("".join(lines), encoding="utf-8") + + @classmethod + def set(cls, key, value): + """Sets a configuration value and saves the change""" + cls.load() + cls._config[key] = value + cls.save() + + @classmethod + def get(cls, key, default=None): + """Returns a configuration value""" + config = cls.load() + return config.get(key, default) + + +class ThemeManager: + """ + Class for central theme management and UI customization. + + This static class allows dynamic adjustment of the application's appearance. + The method `change_theme()` updates the current theme and saves + the selection in the configuration file via `ConfigManager`. + It ensures a consistent visual design across the entire project. + + Key Features: + - Central control over themes. + - Automatic saving of theme settings to the configuration file. + - Tight integration with `ConfigManager` for persistent storage of preferences. + + The class is designed to apply themes consistently throughout the application, + ensuring that changes are traceable and uniform across all parts of the project. + """ + + @staticmethod + def change_theme(root, theme_in_use, theme_name=None): + """Change application theme centrally""" + root.tk.call("set_theme", theme_in_use) + if theme_in_use == theme_name: + ConfigManager.set("theme", theme_in_use) + + +class Tooltip: + """Class for Tooltip + from common_tools.py import Tooltip + example: Tooltip(label, "Show tooltip on label") + example: Tooltip(button, "Show tooltip on button") + example: Tooltip(widget, "Text", state_var=tk.BooleanVar()) + example: Tooltip(widget, "Text", state_var=tk.BooleanVar(), x_offset=20, y_offset=10) + + info: label and button are parent widgets. + NOTE: When using with state_var, pass the tk.BooleanVar object directly, + NOT its value. For example: use state_var=my_bool_var, NOT state_var=my_bool_var.get() + """ + + def __init__( + self, + widget: Any, + text: str, + state_var: Optional[tk.BooleanVar] = None, + x_offset: int = 65, + y_offset: int = 40, + ) -> None: + """Tooltip Class""" + self.widget: Any = widget + self.text: str = text + self.tooltip_window: Optional[Toplevel] = None + self.state_var = state_var + self.x_offset = x_offset + self.y_offset = y_offset + + # Initial binding based on the current state + self.update_bindings() + + # Add trace to the state_var if provided + if self.state_var is not None: + self.state_var.trace_add("write", self.update_bindings) + + def update_bindings(self, *args) -> None: + """Updates the bindings based on the current state""" + # Remove existing bindings first + self.widget.unbind("") + self.widget.unbind("") + + # Add new bindings if tooltips are enabled + if self.state_var is None or self.state_var.get(): + self.widget.bind("", self.show_tooltip) + self.widget.bind("", self.hide_tooltip) + + def show_tooltip(self, event: Optional[Any] = None) -> None: + """Shows the tooltip""" + if self.tooltip_window or not self.text: + return + + x: int + y: int + cx: int + cy: int + + x, y, cx, cy = self.widget.bbox("insert") + x += self.widget.winfo_rootx() + self.x_offset + y += self.widget.winfo_rooty() + self.y_offset + + self.tooltip_window = tw = tk.Toplevel(self.widget) + tw.wm_overrideredirect(True) + tw.wm_geometry(f"+{x}+{y}") + + label: tk.Label = tk.Label( + tw, + text=self.text, + background="lightgreen", + foreground="black", + relief="solid", + borderwidth=1, + padx=5, + pady=5, + ) + label.grid() + + self.tooltip_window.after(2200, lambda: tw.destroy()) + + def hide_tooltip(self, event: Optional[Any] = None) -> None: + """Hides the tooltip""" + if self.tooltip_window: + self.tooltip_window.destroy() + self.tooltip_window = None + + +class LogConfig: + @staticmethod + def logger(file_path) -> None: + + file_handler = logging.FileHandler( + filename=f"{file_path}", + mode="a", + encoding="utf-8", + ) + formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") + file_handler.setFormatter(formatter) + file_handler.setLevel(logging.DEBUG) + + logger = logging.getLogger() + logger.addHandler(file_handler) diff --git a/lx-icons/128/download.png b/lx-icons/128/download.png new file mode 100644 index 0000000..1589350 Binary files /dev/null and b/lx-icons/128/download.png differ diff --git a/lx-icons/128/download_error.png b/lx-icons/128/download_error.png new file mode 100644 index 0000000..011c99e Binary files /dev/null and b/lx-icons/128/download_error.png differ diff --git a/lx-icons/128/error.png b/lx-icons/128/error.png new file mode 100644 index 0000000..ae580f8 Binary files /dev/null and b/lx-icons/128/error.png differ diff --git a/lx-icons/128/info.png b/lx-icons/128/info.png new file mode 100644 index 0000000..ccba611 Binary files /dev/null and b/lx-icons/128/info.png differ diff --git a/lx-icons/128/log.png b/lx-icons/128/log.png new file mode 100644 index 0000000..06de63c Binary files /dev/null and b/lx-icons/128/log.png differ diff --git a/lx-icons/128/wg_export.png b/lx-icons/128/wg_export.png new file mode 100644 index 0000000..787e894 Binary files /dev/null and b/lx-icons/128/wg_export.png differ diff --git a/lx-icons/128/wg_import.png b/lx-icons/128/wg_import.png new file mode 100644 index 0000000..a65f8b2 Binary files /dev/null and b/lx-icons/128/wg_import.png differ diff --git a/lx-icons/128/wg_msg.png b/lx-icons/128/wg_msg.png new file mode 100644 index 0000000..dcc04cd Binary files /dev/null and b/lx-icons/128/wg_msg.png differ diff --git a/lx-icons/128/wg_trash.png b/lx-icons/128/wg_trash.png new file mode 100644 index 0000000..16b06b1 Binary files /dev/null and b/lx-icons/128/wg_trash.png differ diff --git a/lx-icons/128/wg_vpn-start.png b/lx-icons/128/wg_vpn-start.png new file mode 100644 index 0000000..2555e35 Binary files /dev/null and b/lx-icons/128/wg_vpn-start.png differ diff --git a/lx-icons/128/wg_vpn-stop.png b/lx-icons/128/wg_vpn-stop.png new file mode 100644 index 0000000..ff02259 Binary files /dev/null and b/lx-icons/128/wg_vpn-stop.png differ diff --git a/lx-icons/128/wg_vpn.png b/lx-icons/128/wg_vpn.png new file mode 100644 index 0000000..e800c31 Binary files /dev/null and b/lx-icons/128/wg_vpn.png differ diff --git a/lx-icons/256/download.png b/lx-icons/256/download.png new file mode 100644 index 0000000..614bf40 Binary files /dev/null and b/lx-icons/256/download.png differ diff --git a/lx-icons/256/download_error.png b/lx-icons/256/download_error.png new file mode 100644 index 0000000..e75bb67 Binary files /dev/null and b/lx-icons/256/download_error.png differ diff --git a/lx-icons/256/error.png b/lx-icons/256/error.png new file mode 100644 index 0000000..7734e3a Binary files /dev/null and b/lx-icons/256/error.png differ diff --git a/lx-icons/256/info.png b/lx-icons/256/info.png new file mode 100644 index 0000000..8c1ca4f Binary files /dev/null and b/lx-icons/256/info.png differ diff --git a/lx-icons/256/log.png b/lx-icons/256/log.png new file mode 100644 index 0000000..3921edb Binary files /dev/null and b/lx-icons/256/log.png differ diff --git a/lx-icons/256/wg_export.png b/lx-icons/256/wg_export.png new file mode 100644 index 0000000..49fd9b2 Binary files /dev/null and b/lx-icons/256/wg_export.png differ diff --git a/lx-icons/256/wg_import.png b/lx-icons/256/wg_import.png new file mode 100644 index 0000000..03cc502 Binary files /dev/null and b/lx-icons/256/wg_import.png differ diff --git a/lx-icons/256/wg_msg.png b/lx-icons/256/wg_msg.png new file mode 100644 index 0000000..76f84f4 Binary files /dev/null and b/lx-icons/256/wg_msg.png differ diff --git a/lx-icons/256/wg_trash.png b/lx-icons/256/wg_trash.png new file mode 100644 index 0000000..6e21206 Binary files /dev/null and b/lx-icons/256/wg_trash.png differ diff --git a/lx-icons/256/wg_vpn-start.png b/lx-icons/256/wg_vpn-start.png new file mode 100644 index 0000000..17f7bf0 Binary files /dev/null and b/lx-icons/256/wg_vpn-start.png differ diff --git a/lx-icons/256/wg_vpn-stop.png b/lx-icons/256/wg_vpn-stop.png new file mode 100644 index 0000000..a99bc5b Binary files /dev/null and b/lx-icons/256/wg_vpn-stop.png differ diff --git a/lx-icons/256/wg_vpn.png b/lx-icons/256/wg_vpn.png new file mode 100644 index 0000000..2aa3c20 Binary files /dev/null and b/lx-icons/256/wg_vpn.png differ diff --git a/lx-icons/32/download.png b/lx-icons/32/download.png new file mode 100644 index 0000000..e209fbd Binary files /dev/null and b/lx-icons/32/download.png differ diff --git a/lx-icons/32/download_error.png b/lx-icons/32/download_error.png new file mode 100644 index 0000000..595d04d Binary files /dev/null and b/lx-icons/32/download_error.png differ diff --git a/lx-icons/32/error.png b/lx-icons/32/error.png new file mode 100644 index 0000000..18e6c64 Binary files /dev/null and b/lx-icons/32/error.png differ diff --git a/lx-icons/32/info.png b/lx-icons/32/info.png new file mode 100644 index 0000000..f11ca97 Binary files /dev/null and b/lx-icons/32/info.png differ diff --git a/lx-icons/32/log.png b/lx-icons/32/log.png new file mode 100644 index 0000000..ebc7be1 Binary files /dev/null and b/lx-icons/32/log.png differ diff --git a/lx-icons/32/wg_export.png b/lx-icons/32/wg_export.png new file mode 100644 index 0000000..81bb65b Binary files /dev/null and b/lx-icons/32/wg_export.png differ diff --git a/lx-icons/32/wg_import.png b/lx-icons/32/wg_import.png new file mode 100644 index 0000000..dbaa7e7 Binary files /dev/null and b/lx-icons/32/wg_import.png differ diff --git a/lx-icons/32/wg_msg.png b/lx-icons/32/wg_msg.png new file mode 100644 index 0000000..5bd0115 Binary files /dev/null and b/lx-icons/32/wg_msg.png differ diff --git a/lx-icons/32/wg_trash.png b/lx-icons/32/wg_trash.png new file mode 100644 index 0000000..57f604d Binary files /dev/null and b/lx-icons/32/wg_trash.png differ diff --git a/lx-icons/32/wg_vpn-start.png b/lx-icons/32/wg_vpn-start.png new file mode 100644 index 0000000..149142f Binary files /dev/null and b/lx-icons/32/wg_vpn-start.png differ diff --git a/lx-icons/32/wg_vpn-stop.png b/lx-icons/32/wg_vpn-stop.png new file mode 100644 index 0000000..f29cbe6 Binary files /dev/null and b/lx-icons/32/wg_vpn-stop.png differ diff --git a/lx-icons/32/wg_vpn.png b/lx-icons/32/wg_vpn.png new file mode 100644 index 0000000..55df4dd Binary files /dev/null and b/lx-icons/32/wg_vpn.png differ diff --git a/lx-icons/48/download.png b/lx-icons/48/download.png new file mode 100644 index 0000000..4302a7c Binary files /dev/null and b/lx-icons/48/download.png differ diff --git a/lx-icons/48/download_error.png b/lx-icons/48/download_error.png new file mode 100644 index 0000000..96ec900 Binary files /dev/null and b/lx-icons/48/download_error.png differ diff --git a/lx-icons/48/error.png b/lx-icons/48/error.png new file mode 100644 index 0000000..92a731a Binary files /dev/null and b/lx-icons/48/error.png differ diff --git a/lx-icons/48/info.png b/lx-icons/48/info.png new file mode 100644 index 0000000..52206e6 Binary files /dev/null and b/lx-icons/48/info.png differ diff --git a/lx-icons/48/log.png b/lx-icons/48/log.png new file mode 100644 index 0000000..971a013 Binary files /dev/null and b/lx-icons/48/log.png differ diff --git a/lx-icons/48/wg_export.png b/lx-icons/48/wg_export.png new file mode 100644 index 0000000..73fb2aa Binary files /dev/null and b/lx-icons/48/wg_export.png differ diff --git a/lx-icons/48/wg_import.png b/lx-icons/48/wg_import.png new file mode 100644 index 0000000..dd888b9 Binary files /dev/null and b/lx-icons/48/wg_import.png differ diff --git a/lx-icons/48/wg_msg.png b/lx-icons/48/wg_msg.png new file mode 100644 index 0000000..732eabb Binary files /dev/null and b/lx-icons/48/wg_msg.png differ diff --git a/lx-icons/48/wg_trash.png b/lx-icons/48/wg_trash.png new file mode 100644 index 0000000..3e1538c Binary files /dev/null and b/lx-icons/48/wg_trash.png differ diff --git a/lx-icons/48/wg_vpn-start.png b/lx-icons/48/wg_vpn-start.png new file mode 100644 index 0000000..e3f078d Binary files /dev/null and b/lx-icons/48/wg_vpn-start.png differ diff --git a/lx-icons/48/wg_vpn-stop.png b/lx-icons/48/wg_vpn-stop.png new file mode 100644 index 0000000..30aa9cc Binary files /dev/null and b/lx-icons/48/wg_vpn-stop.png differ diff --git a/lx-icons/48/wg_vpn.png b/lx-icons/48/wg_vpn.png new file mode 100644 index 0000000..cec4506 Binary files /dev/null and b/lx-icons/48/wg_vpn.png differ diff --git a/lx-icons/64/download.png b/lx-icons/64/download.png new file mode 100644 index 0000000..cc12d8a Binary files /dev/null and b/lx-icons/64/download.png differ diff --git a/lx-icons/64/download_error.png b/lx-icons/64/download_error.png new file mode 100644 index 0000000..0cd4161 Binary files /dev/null and b/lx-icons/64/download_error.png differ diff --git a/lx-icons/64/error.png b/lx-icons/64/error.png new file mode 100644 index 0000000..5ac3858 Binary files /dev/null and b/lx-icons/64/error.png differ diff --git a/lx-icons/64/info.png b/lx-icons/64/info.png new file mode 100644 index 0000000..4917814 Binary files /dev/null and b/lx-icons/64/info.png differ diff --git a/lx-icons/64/log.png b/lx-icons/64/log.png new file mode 100644 index 0000000..e1eb8dc Binary files /dev/null and b/lx-icons/64/log.png differ diff --git a/lx-icons/64/wg_export.png b/lx-icons/64/wg_export.png new file mode 100644 index 0000000..7d14d17 Binary files /dev/null and b/lx-icons/64/wg_export.png differ diff --git a/lx-icons/64/wg_import.png b/lx-icons/64/wg_import.png new file mode 100644 index 0000000..c9b0c85 Binary files /dev/null and b/lx-icons/64/wg_import.png differ diff --git a/lx-icons/64/wg_msg.png b/lx-icons/64/wg_msg.png new file mode 100644 index 0000000..d821d13 Binary files /dev/null and b/lx-icons/64/wg_msg.png differ diff --git a/lx-icons/64/wg_trash.png b/lx-icons/64/wg_trash.png new file mode 100644 index 0000000..1655e17 Binary files /dev/null and b/lx-icons/64/wg_trash.png differ diff --git a/lx-icons/64/wg_vpn-start.png b/lx-icons/64/wg_vpn-start.png new file mode 100644 index 0000000..3eeb910 Binary files /dev/null and b/lx-icons/64/wg_vpn-start.png differ diff --git a/lx-icons/64/wg_vpn-stop.png b/lx-icons/64/wg_vpn-stop.png new file mode 100644 index 0000000..03a5044 Binary files /dev/null and b/lx-icons/64/wg_vpn-stop.png differ diff --git a/lx-icons/64/wg_vpn.png b/lx-icons/64/wg_vpn.png new file mode 100644 index 0000000..d69ef68 Binary files /dev/null and b/lx-icons/64/wg_vpn.png differ diff --git a/lxtools_installer.py b/lxtools_installer.py new file mode 100644 index 0000000..8cb19e7 --- /dev/null +++ b/lxtools_installer.py @@ -0,0 +1,1264 @@ +#!/usr/bin/python3 +import gettext +import locale +import tkinter as tk +from tkinter import messagebox, ttk +import shutil +import os +import socket +import subprocess +import tempfile +import urllib.request +import zipfile +import json +from pathlib import Path + + +# ---------------------------- +# LXTools App Configuration +# ---------------------------- +class LXToolsAppConfig: + VERSION = "1.0.4" + APP_NAME = "LXTools Installer" + WINDOW_WIDTH = 500 + WINDOW_HEIGHT = 600 + + # Locale settings + LOCALE_DIR = Path("/usr/share/locale/") + + # Images and icons paths + IMAGE_PATHS = { + "icon_vpn": "./lx-icons/32/wg_vpn.png", + "icon_vpn2": "./lx-icons/48/wg_vpn.png", + "icon_msg": "./lx-icons/48/wg_msg.png", + "icon_info": "./lx-icons/64/info.png", + "icon_error": "./lx-icons/64/error.png", + "icon_log": "./lx-icons/32/log.png", + "icon_log2": "./lx-icons/48/log.png", + "icon_download": "./lx-icons/32/download.png", + "icon_download_error": "./lx-icons/32/download_error.png", + } + + # System-dependent paths + SYSTEM_PATHS = { + "tcl_path": "/usr/share/TK-Themes", + } + + # Download URLs + WIREPY_URL = "https://git.ilunix.de/punix/Wire-Py/archive/main.zip" + SHARED_LIBS_URL = "https://git.ilunix.de/punix/shared_libs/archive/main.zip" + + # API URLs for version checking + WIREPY_API_URL = "https://git.ilunix.de/api/v1/repos/punix/Wire-Py/releases" + SHARED_LIBS_API_URL = ( + "https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases" + ) + + # OS Detection List (order matters - specific first, generic last) + OS_DETECTION = [ + ("mint", "Linux Mint"), + ("pop", "Pop!_OS"), + ("manjaro", "Manjaro"), + ("garuda", "Garuda Linux"), + ("endeavouros", "EndeavourOS"), + ("fedora", "Fedora"), + ("tumbleweed", "SUSE Tumbleweed"), + ("leap", "SUSE Leap"), + ("suse", "openSUSE"), + ("arch", "Arch Linux"), + ("ubuntu", "Ubuntu"), + ("debian", "Debian"), + ] + + @staticmethod + def setup_translations(): + """Initialize translations and set the translation function""" + locale.bindtextdomain(LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR) + gettext.bindtextdomain(LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR) + gettext.textdomain(LXToolsAppConfig.APP_NAME) + return gettext.gettext + + +# Initialize translations +_ = LXToolsAppConfig.setup_translations() + + +# ---------------------------- +# Image Manager Class +# ---------------------------- +class ImageManager: + def __init__(self): + self.images = {} + + def load_image(self, image_key, fallback_paths=None): + """Load PNG image using tk.PhotoImage with fallback options""" + if image_key in self.images: + return self.images[image_key] + + # Primary path from config + primary_path = LXToolsAppConfig.IMAGE_PATHS.get(image_key) + paths_to_try = [] + + if primary_path: + paths_to_try.append(primary_path) + + # Add fallback paths + if fallback_paths: + paths_to_try.extend(fallback_paths) + + # Try to load image from paths + for path in paths_to_try: + try: + if os.path.exists(path): + photo = tk.PhotoImage(file=path) + self.images[image_key] = photo + return photo + except tk.TclError as e: + print(f"Failed to load image from {path}: {e}") + continue + + # Return None if no image found (we'll handle this in GUI) + return None + + +# ---------------------------- +# Gitea API Handler +# ---------------------------- +class GiteaUpdate: + @staticmethod + def api_down(url, current_version=""): + """Get latest version from Gitea API""" + try: + with urllib.request.urlopen(url) as response: + data = json.loads(response.read().decode()) + if data and len(data) > 0: + latest_version = data[0].get("tag_name", "Unknown") + return latest_version.lstrip("v") # Remove 'v' prefix if present + return "Unknown" + except Exception as e: + print(f"API Error: {e}") + return "Unknown" + + +# ---------------------------- +# OS Detection Class +# ---------------------------- +class OSDetector: + @staticmethod + def detect_os(): + """Detect operating system using ordered list""" + try: + with open("/etc/os-release", "r") as f: + content = f.read().lower() + + # Check each OS in order (specific first) + for keyword, os_name in LXToolsAppConfig.OS_DETECTION: + if keyword in content: + return os_name + + return "Unknown System" + except FileNotFoundError: + return "File not found" + + +# ---------------------------- +# Network Checker Class +# ---------------------------- +class NetworkChecker: + @staticmethod + def check_internet_connection(host="8.8.8.8", port=53, timeout=3): + """Check if internet connection is available""" + try: + socket.setdefaulttimeout(timeout) + socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) + return True + except socket.error: + return False + + @staticmethod + def check_repository_access(url="https://git.ilunix.de"): + """Check if repository is accessible""" + try: + urllib.request.urlopen(url, timeout=5) + return True + except: + return False + + +# ---------------------------- +# Application Configuration Class +# ---------------------------- +class AppConfig: + def __init__( + self, + key, + name, + files, + config, + desktop, + icon, + symlink, + config_dir, + log_file, + languages, + api_url, + icon_key, + policy_file=None, + ): + self.key = key + self.name = name + self.files = files + self.config = config + self.desktop = desktop + self.icon = icon + self.symlink = symlink + self.config_dir = config_dir + self.log_file = log_file + self.languages = languages + self.api_url = api_url + self.icon_key = icon_key # Key for ImageManager + self.policy_file = policy_file + + def is_installed(self): + """Check if application is installed""" + return os.path.exists(f"/usr/local/bin/{self.symlink}") + + def get_installed_version(self): + """Get installed version from config file""" + try: + config_file = f"/usr/lib/python3/dist-packages/shared_libs/{self.config}" + if os.path.exists(config_file): + with open(config_file, "r") as f: + content = f.read() + for line in content.split("\n"): + if "VERSION" in line and "=" in line: + return line.split("=")[1].strip().strip("\"'") + return "Unknown" + except: + return "Unknown" + + def get_latest_version(self): + """Get latest version from API""" + return GiteaUpdate.api_down(self.api_url) + + +# ---------------------------- +# Application Manager Class +# ---------------------------- +class AppManager: + def __init__(self): + self.apps = { + "wirepy": AppConfig( + key="wirepy", + name="Wire-Py", + files=[ + "wirepy.py", + "start_wg.py", + "ssl_encrypt.py", + "ssl_decrypt.py", + "match_found.py", + "tunnel.py", + ], + config="wp_app_config.py", + desktop="Wire-Py.desktop", + icon="wg_vpn.png", + symlink="wirepy", + config_dir="~/.config/wire_py", + log_file="~/.local/share/lxlogs/wirepy.log", + languages=["wirepy.mo"], + api_url=LXToolsAppConfig.WIREPY_API_URL, + icon_key="icon_vpn", + policy_file="org.sslcrypt.policy", + ), + "logviewer": AppConfig( + key="logviewer", + name="LogViewer", + files=["logviewer.py"], + config="logview_app_config.py", + desktop="LogViewer.desktop", + icon="log.png", + symlink="logviewer", + config_dir="~/.config/logviewer", + log_file="~/.local/share/lxlogs/logviewer.log", + languages=["logviewer.mo"], + api_url=LXToolsAppConfig.SHARED_LIBS_API_URL, + icon_key="icon_log", + policy_file=None, + ), + } + + self.shared_files = [ + "common_tools.py", + "file_and_dir_ensure.py", + "gitea.py", + "__init__.py", + "logview_app_config.py", + "logviewer.py", + ] + + def get_app(self, key): + """Get application configuration by key""" + return self.apps.get(key) + + def get_all_apps(self): + """Get all application configurations""" + return self.apps + + def check_other_apps_installed(self, exclude_key): + """Check if other apps are still installed""" + return any( + app.is_installed() for key, app in self.apps.items() if key != exclude_key + ) + + +# ---------------------------- +# Download Manager Class +# ---------------------------- +class DownloadManager: + @staticmethod + def download_and_extract( + url, extract_to, progress_callback=None, icon_callback=None + ): + """Download and extract ZIP file with icon status""" + try: + if progress_callback: + progress_callback(f"Downloading from {url}...") + + # Set download icon + if icon_callback: + icon_callback("downloading") + + with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file: + urllib.request.urlretrieve(url, tmp_file.name) + + if progress_callback: + progress_callback("Extracting files...") + + with zipfile.ZipFile(tmp_file.name, "r") as zip_ref: + zip_ref.extractall(extract_to) + + os.unlink(tmp_file.name) + + # Set success icon + if icon_callback: + icon_callback("success") + + return True + + except Exception as e: + if progress_callback: + progress_callback(f"Download failed: {str(e)}") + + # Set error icon + if icon_callback: + icon_callback("error") + + return False + + +# ---------------------------- +# System Manager Class +# ---------------------------- +class SystemManager: + @staticmethod + def create_directories(directories): + """Create system directories using pkexec""" + for directory in directories: + subprocess.run(["pkexec", "mkdir", "-p", directory], check=True) + + @staticmethod + def copy_file(src, dest, make_executable=False): + """Copy file using pkexec""" + subprocess.run(["pkexec", "cp", src, dest], check=True) + if make_executable: + subprocess.run(["pkexec", "chmod", "755", dest], check=True) + + @staticmethod + def copy_directory(src, dest): + """Copy directory using pkexec""" + subprocess.run(["pkexec", "cp", "-r", src, dest], check=True) + + @staticmethod + def remove_file(path): + """Remove file using pkexec""" + subprocess.run(["pkexec", "rm", "-f", path], check=False) + + @staticmethod + def remove_directory(path): + """Remove directory using pkexec""" + subprocess.run(["pkexec", "rm", "-rf", path], check=False) + + @staticmethod + def create_symlink(target, link_name): + """Create symbolic link using pkexec""" + subprocess.run(["pkexec", "rm", "-f", link_name], check=False) + subprocess.run(["pkexec", "ln", "-sf", target, link_name], check=True) + + @staticmethod + def create_ssl_key(pem_file): + """Create SSL key using pkexec""" + try: + subprocess.run( + ["pkexec", "openssl", "genrsa", "-out", pem_file, "4096"], check=True + ) + subprocess.run(["pkexec", "chmod", "600", pem_file], check=True) + return True + except subprocess.CalledProcessError: + return False + + +# ---------------------------- +# Installation Manager Class +# ---------------------------- +class InstallationManager: + def __init__(self, app_manager, progress_callback=None, icon_callback=None): + self.app_manager = app_manager + self.progress_callback = progress_callback + self.icon_callback = icon_callback + self.system_manager = SystemManager() + self.download_manager = DownloadManager() + + def update_progress(self, message): + """Update progress message""" + if self.progress_callback: + self.progress_callback(message) + + def install_app(self, app_key): + """Install or update application""" + app = self.app_manager.get_app(app_key) + if not app: + raise Exception(f"Unknown application: {app_key}") + + self.update_progress(f"Starting installation of {app.name}...") + + try: + # Create temporary directory + with tempfile.TemporaryDirectory() as temp_dir: + # Download application source + if app_key == "wirepy": + if not self.download_manager.download_and_extract( + LXToolsAppConfig.WIREPY_URL, + temp_dir, + self.update_progress, + self.icon_callback, + ): + raise Exception("Failed to download Wire-Py") + source_dir = os.path.join(temp_dir, "Wire-Py") + else: + if not self.download_manager.download_and_extract( + LXToolsAppConfig.SHARED_LIBS_URL, + temp_dir, + self.update_progress, + self.icon_callback, + ): + raise Exception("Failed to download LogViewer") + source_dir = os.path.join(temp_dir, "shared_libs") + + # Download shared libraries + shared_temp = os.path.join(temp_dir, "shared") + if not self.download_manager.download_and_extract( + LXToolsAppConfig.SHARED_LIBS_URL, + shared_temp, + self.update_progress, + self.icon_callback, + ): + raise Exception("Failed to download shared libraries") + shared_source = os.path.join(shared_temp, "shared_libs") + + # Create necessary directories + self.update_progress("Creating directories...") + directories = [ + "/usr/lib/python3/dist-packages/shared_libs", + "/usr/share/icons/lx-icons/48", + "/usr/share/icons/lx-icons/64", + "/usr/share/locale/de/LC_MESSAGES", + "/usr/share/applications", + "/usr/local/etc/ssl", + "/usr/share/polkit-1/actions", + ] + self.system_manager.create_directories(directories) + + # Install shared libraries + self.update_progress("Installing shared libraries...") + self._install_shared_libraries(shared_source) + + # Install application files + self.update_progress(f"Installing {app.name} files...") + self._install_app_files(app, source_dir) + + # Install additional resources + self._install_app_resources(app, source_dir) + + # Install policy file if exists + if app.policy_file: + self._install_policy_file(app, source_dir) + + # Create symlink + self.update_progress("Creating symlink...") + main_file = app.files[0] # First file is usually the main file + self.system_manager.create_symlink( + f"/usr/local/bin/{main_file}", f"/usr/local/bin/{app.symlink}" + ) + + # Special handling for Wire-Py SSL key + if app_key == "wirepy": + self._create_ssl_key() + + self.update_progress(f"{app.name} installation completed successfully!") + return True + + except subprocess.CalledProcessError as e: + self.update_progress("Error: pkexec command failed") + raise Exception( + f"Installation failed (pkexec): {e}\n\nPermission might have been denied." + ) + except Exception as e: + self.update_progress(f"Error: {str(e)}") + raise + + def _install_policy_file(self, app, source_dir): + """Install polkit policy file""" + if app.policy_file: + self.update_progress(f"Installing policy file {app.policy_file}...") + policy_src = os.path.join(source_dir, app.policy_file) + if os.path.exists(policy_src): + policy_dest = f"/usr/share/polkit-1/actions/{app.policy_file}" + self.system_manager.copy_file(policy_src, policy_dest) + self.update_progress( + f"Policy file {app.policy_file} installed successfully." + ) + else: + self.update_progress( + f"Warning: Policy file {app.policy_file} not found in source." + ) + + def _install_shared_libraries(self, shared_source): + """Install shared library files""" + for shared_file in self.app_manager.shared_files: + src = os.path.join(shared_source, shared_file) + if os.path.exists(src): + dest = f"/usr/lib/python3/dist-packages/shared_libs/{shared_file}" + self.system_manager.copy_file(src, dest) + + def _install_app_files(self, app, source_dir): + """Install application executable files""" + for app_file in app.files: + src = os.path.join(source_dir, app_file) + if os.path.exists(src): + dest = f"/usr/local/bin/{app_file}" + self.system_manager.copy_file(src, dest, make_executable=True) + + # Install app config + config_src = os.path.join(source_dir, app.config) + if os.path.exists(config_src): + config_dest = f"/usr/lib/python3/dist-packages/shared_libs/{app.config}" + self.system_manager.copy_file(config_src, config_dest) + + def _install_app_resources(self, app, source_dir): + """Install icons, desktop files, and language files""" + # Install icons + self.update_progress("Installing icons...") + icons_src = os.path.join(source_dir, "lx-icons") + if os.path.exists(icons_src): + # Copy all icon subdirectories + for item in os.listdir(icons_src): + item_path = os.path.join(icons_src, item) + if os.path.isdir(item_path): + dest_path = f"/usr/share/icons/lx-icons/{item}" + self.system_manager.copy_directory(item_path, dest_path) + + # Install desktop file + desktop_src = os.path.join(source_dir, app.desktop) + if os.path.exists(desktop_src): + self.system_manager.copy_file( + desktop_src, f"/usr/share/applications/{app.desktop}" + ) + + # Install language files + self.update_progress("Installing language files...") + lang_dir = os.path.join(source_dir, "languages", "de") + if os.path.exists(lang_dir): + for lang_file in app.languages: + lang_src = os.path.join(lang_dir, lang_file) + if os.path.exists(lang_src): + lang_dest = f"/usr/share/locale/de/LC_MESSAGES/{lang_file}" + self.system_manager.copy_file(lang_src, lang_dest) + + def _create_ssl_key(self): + """Create SSL key for Wire-Py""" + pem_file = "/usr/local/etc/ssl/pwgk.pem" + if not os.path.exists(pem_file): + self.update_progress("Creating SSL key...") + if not self.system_manager.create_ssl_key(pem_file): + self.update_progress( + "Warning: SSL key creation failed. OpenSSL might be missing." + ) + + def uninstall_app(self, app_key): + """Uninstall application""" + app = self.app_manager.get_app(app_key) + if not app: + raise Exception(f"Unknown application: {app_key}") + + if not app.is_installed(): + raise Exception(f"{app.name} is not installed.") + + try: + self.update_progress(f"Uninstalling {app.name}...") + + # Remove policy file if exists + if app.policy_file: + self.system_manager.remove_file( + f"/usr/share/polkit-1/actions/{app.policy_file}" + ) + + # Remove application files + for app_file in app.files: + self.system_manager.remove_file(f"/usr/local/bin/{app_file}") + + # Remove symlink + self.system_manager.remove_file(f"/usr/local/bin/{app.symlink}") + + # Remove app config + self.system_manager.remove_file( + f"/usr/lib/python3/dist-packages/shared_libs/{app.config}" + ) + + # Remove desktop file + self.system_manager.remove_file(f"/usr/share/applications/{app.desktop}") + + # Remove language files + for lang_file in app.languages: + self.system_manager.remove_file( + f"/usr/share/locale/de/LC_MESSAGES/{lang_file}" + ) + + # Remove user config directory + config_dir = os.path.expanduser(app.config_dir) + if os.path.exists(config_dir): + shutil.rmtree(config_dir) + + # Remove log file + log_file = os.path.expanduser(app.log_file) + if os.path.exists(log_file): + os.remove(log_file) + + # Check if other apps are still installed before removing shared resources + if not self.app_manager.check_other_apps_installed(app_key): + self.update_progress("Removing shared resources...") + self._remove_shared_resources() + + self.update_progress(f"{app.name} uninstalled successfully!") + return True + + except Exception as e: + self.update_progress(f"Error during uninstallation: {str(e)}") + raise + + def _remove_shared_resources(self): + """Remove shared resources when no apps are installed""" + # Remove shared libraries + for shared_file in self.app_manager.shared_files: + self.system_manager.remove_file( + f"/usr/lib/python3/dist-packages/shared_libs/{shared_file}" + ) + + # Remove icons and SSL directory + self.system_manager.remove_directory("/usr/share/icons/lx-icons") + self.system_manager.remove_directory("/usr/local/etc/ssl") + + # Remove shared_libs directory if empty + subprocess.run( + ["pkexec", "rmdir", "/usr/lib/python3/dist-packages/shared_libs"], + check=False, + ) + + +# ---------------------------- +# GUI Application Class (Erweiterte Version) +# ---------------------------- +class LXToolsGUI: + def __init__(self): + self.root = None + self.progress_label = None + self.download_icon_label = None + self.app_var = None + self.status_labels = {} + self.version_labels = {} + + # Initialize managers + self.app_manager = AppManager() + self.installation_manager = InstallationManager( + self.app_manager, self.update_progress, self.update_download_icon + ) + self.image_manager = ImageManager() + + # Detect OS + self.detected_os = OSDetector.detect_os() + + def create_gui(self): + """Create the main GUI""" + self.root = tk.Tk() + self.root.title(LXToolsAppConfig.APP_NAME) + self.root.geometry( + f"{LXToolsAppConfig.WINDOW_WIDTH}x{LXToolsAppConfig.WINDOW_HEIGHT}" + ) + + self.root.resizable(True, True) + + # Apply theme + try: + self.root.tk.call("source", "TK-Themes/water.tcl") + self.root.tk.call("set_theme", "light") + except tk.TclError as e: + print(f"Theme loading failed: {e}") + + # Create GUI components + self._create_header() + self._create_system_info() + self._create_app_selection() # Diese wird durch erweiterte Version ersetzt + self._create_progress_section() + self._create_buttons() + self._create_info_section() + self._check_system_requirements() + + # Configure responsive layout + self._configure_responsive_layout() + + # Initial status refresh + self.refresh_status() + + return self.root + + def _create_header(self): + """Create header section""" + header_frame = tk.Frame(self.root, bg="lightblue", height=60) + header_frame.pack(fill="x", padx=10, pady=10) + header_frame.pack_propagate(False) + + title_label = tk.Label( + header_frame, + text=LXToolsAppConfig.APP_NAME, + font=("Helvetica", 18, "bold"), + bg="lightblue", + ) + title_label.pack(expand=True) + + version_label = tk.Label( + header_frame, + text=f"v{LXToolsAppConfig.VERSION}", + font=("Helvetica", 10), + bg="lightblue", + ) + version_label.pack(side="bottom") + + def _create_system_info(self): + """Create system information section""" + info_frame = tk.Frame(self.root) + info_frame.pack(pady=5) + + os_info = tk.Label( + info_frame, + text=f"{_('Detected System')}: {self.detected_os}", + font=("Helvetica", 11), + ) + os_info.pack(pady=2) + + def _create_app_selection(self): + """Create application selection section with improved Grid layout""" + selection_frame = ttk.LabelFrame( + self.root, text=_("Select Application"), padding=15 + ) + selection_frame.pack(fill="both", expand=True, padx=15, pady=10) + + self.app_var = tk.StringVar() + + # Haupt-Container mit Scrollbar (falls mehr Apps hinzukommen) + canvas = tk.Canvas(selection_frame, highlightthickness=0) + scrollbar = ttk.Scrollbar( + selection_frame, orient="vertical", command=canvas.yview + ) + scrollable_frame = ttk.Frame(canvas) + + scrollable_frame.bind( + "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) + ) + + canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + + # Grid-Container für Apps + apps_container = tk.Frame(scrollable_frame) + apps_container.pack(fill="x", padx=5, pady=5) + + # Grid konfigurieren - 4 Spalten mit besserer Verteilung + apps_container.grid_columnconfigure(0, weight=0, minsize=60) # Icon + apps_container.grid_columnconfigure(1, weight=1, minsize=150) # App Name + apps_container.grid_columnconfigure(2, weight=0, minsize=120) # Status + apps_container.grid_columnconfigure(3, weight=0, minsize=150) # Version + + # Header-Zeile + header_font = ("Helvetica", 9, "bold") + tk.Label(apps_container, text="", font=header_font).grid( + row=0, column=0, sticky="w", padx=5, pady=2 + ) + tk.Label(apps_container, text=_("Application"), font=header_font).grid( + row=0, column=1, sticky="w", padx=5, pady=2 + ) + tk.Label(apps_container, text=_("Status"), font=header_font).grid( + row=0, column=2, sticky="w", padx=5, pady=2 + ) + tk.Label(apps_container, text=_("Version"), font=header_font).grid( + row=0, column=3, sticky="w", padx=5, pady=2 + ) + + # Trennlinie + separator = ttk.Separator(apps_container, orient="horizontal") + separator.grid(row=1, column=0, columnspan=4, sticky="ew", pady=5) + + row = 2 + for app_key, app in self.app_manager.get_all_apps().items(): + # Spalte 0: Icon + app_icon = self._load_app_icon(app) + if app_icon: + icon_label = tk.Label(apps_container, image=app_icon) + icon_label.grid(row=row, column=0, padx=5, pady=5, sticky="w") + icon_label.image = app_icon + else: + icon_text = "🔧" if app.icon_key == "icon_log" else "🔒" + icon_label = tk.Label( + apps_container, text=icon_text, font=("Helvetica", 16) + ) + icon_label.grid(row=row, column=0, padx=5, pady=5, sticky="w") + + # Spalte 1: Radio button mit App-Name + radio = ttk.Radiobutton( + apps_container, text=app.name, variable=self.app_var, value=app_key + ) + radio.grid(row=row, column=1, padx=5, pady=5, sticky="w") + + # Spalte 2: Status + status_label = tk.Label(apps_container, text="", font=("Helvetica", 9)) + status_label.grid(row=row, column=2, padx=5, pady=5, sticky="w") + self.status_labels[app_key] = status_label + + # Spalte 3: Version Info + version_label = tk.Label( + apps_container, text="", font=("Helvetica", 8), fg="gray" + ) + version_label.grid(row=row, column=3, padx=5, pady=5, sticky="w") + self.version_labels[app_key] = version_label + + row += 1 + + # Pack canvas and scrollbar + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + + # Mouse wheel scrolling + def _on_mousewheel(event): + canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + + canvas.bind_all("", _on_mousewheel) + + def _load_app_icon(self, app): + """Load icon for application using tk.PhotoImage""" + fallback_paths = [ + f"lx-icons/48/{app.icon}", + f"icons/{app.icon}", + f"./lx-icons/48/{app.icon}", + f"./icons/48/{app.icon}", + ( + f"lx-icons/48/wg_vpn.png" + if app.icon_key == "icon_vpn" + else f"lx-icons/48/log.png" + ), + ] + + return self.image_manager.load_image( + app.icon_key, fallback_paths=fallback_paths + ) + + def _create_progress_section(self): + """Create progress section with download icon using Grid""" + progress_frame = ttk.LabelFrame(self.root, text=_("Progress"), padding=10) + progress_frame.pack(fill="x", padx=15, pady=10) + + # Container für Icon und Progress mit Grid + progress_container = tk.Frame(progress_frame) + progress_container.pack(fill="x") + + # Grid konfigurieren + progress_container.grid_columnconfigure(1, weight=1) + + # Download Icon (Spalte 0) + self.download_icon_label = tk.Label( + progress_container, + text="", + width=3, + height=2, + relief="flat", + anchor="center", + ) + self.download_icon_label.grid(row=0, column=0, padx=(0, 10), pady=2, sticky="w") + + # Progress Text (Spalte 1) + self.progress_label = tk.Label( + progress_container, + text=_("Ready for installation..."), + font=("Helvetica", 10), + fg="blue", + anchor="w", + wraplength=400, + ) + self.progress_label.grid(row=0, column=1, pady=2, sticky="ew") + + # Initial icon laden (neutral) + self._reset_download_icon() + + def _configure_responsive_layout(self): + """Configure responsive layout for window resizing""" + + def on_window_resize(event): + # Adjust wraplength for progress label based on window width + if self.progress_label and event.widget == self.root: + new_width = max(300, event.width - 150) + self.progress_label.config(wraplength=new_width) + + self.root.bind("", on_window_resize) + + def _reset_download_icon(self): + """Reset download icon to neutral state""" + icon = self.image_manager.load_image( + "icon_download", + fallback_paths=["lx-icons/32/download.png", "./lx-icons/32/download.png"], + ) + if icon: + self.download_icon_label.config(image=icon, text="", compound="center") + self.download_icon_label.image = icon + else: + self.download_icon_label.config(text="📥", font=("Helvetica", 14), image="") + + def update_download_icon(self, status): + """Update download icon based on status""" + if not self.download_icon_label: + return + + if status == "downloading": + icon = self.image_manager.load_image( + "icon_download", + fallback_paths=[ + "lx-icons/32/download.png", + "./lx-icons/32/download.png", + ], + ) + if icon: + self.download_icon_label.config(image=icon, text="", compound="center") + self.download_icon_label.image = icon + else: + self.download_icon_label.config( + text="⬇️", font=("Helvetica", 14), image="" + ) + + elif status == "error": + icon = self.image_manager.load_image( + "icon_download_error", + fallback_paths=[ + "lx-icons/32/download_error.png", + "./lx-icons/32/download_error.png", + "/home/punix/Pyapps/installer-appimage/lx-icons/32/download_error.png", + ], + ) + if icon: + self.download_icon_label.config(image=icon, text="", compound="center") + self.download_icon_label.image = icon + else: + self.download_icon_label.config( + text="❌", font=("Helvetica", 14), image="" + ) + + elif status == "success": + icon = self.image_manager.load_image( + "icon_download", + fallback_paths=[ + "lx-icons/32/download.png", + "./lx-icons/32/download.png", + ], + ) + if icon: + self.download_icon_label.config(image=icon, text="", compound="center") + self.download_icon_label.image = icon + else: + self.download_icon_label.config( + text="✅", font=("Helvetica", 14), image="" + ) + + self.download_icon_label.update() + + def _create_buttons(self): + """Create button section using Grid""" + button_frame = tk.Frame(self.root) + button_frame.pack(pady=15) + + # Grid für Buttons - 3 Spalten + button_frame.grid_columnconfigure(0, weight=1) + button_frame.grid_columnconfigure(1, weight=1) + button_frame.grid_columnconfigure(2, weight=1) + + # Configure button styles + style = ttk.Style() + style.configure("Install.TButton", foreground="green") + style.configure("Uninstall.TButton", foreground="red") + style.configure("Refresh.TButton", foreground="blue") + + install_btn = ttk.Button( + button_frame, + text=_("Install / Update"), + command=self.install_app, + style="Install.TButton", + ) + install_btn.grid(row=0, column=0, padx=8, sticky="ew") + + uninstall_btn = ttk.Button( + button_frame, + text=_("Uninstall"), + command=self.uninstall_app, + style="Uninstall.TButton", + ) + uninstall_btn.grid(row=0, column=1, padx=8, sticky="ew") + + refresh_btn = ttk.Button( + button_frame, + text=_("Refresh Status"), + command=self.refresh_status, + style="Refresh.TButton", + ) + refresh_btn.grid(row=0, column=2, padx=8, sticky="ew") + + def _create_info_section(self): + """Create information section""" + info_text = tk.Label( + self.root, + text=_( + "Notes:\n" + "• Applications are downloaded automatically from the repository\n" + "• Root privileges are requested via pkexec when needed\n" + "• Shared libraries are managed automatically\n" + "• User configuration files are preserved during updates\n" + "• Policy files for pkexec are installed automatically" + ), + font=("Helvetica", 9), + fg="gray", + wraplength=450, + justify="left", + ) + info_text.pack(pady=15, padx=20) + + def _check_system_requirements(self): + """Check system requirements""" + try: + subprocess.run(["which", "pkexec"], check=True, capture_output=True) + except subprocess.CalledProcessError: + warning_label = tk.Label( + self.root, + text=_("⚠️ WARNING: pkexec is not available! Installation will fail."), + font=("Helvetica", 10, "bold"), + fg="red", + ) + warning_label.pack(pady=5) + + def update_progress(self, message): + """Update progress label""" + if self.progress_label: + self.progress_label.config(text=message) + self.progress_label.update() + + def refresh_status(self): + """Refresh application status and version information""" + self.update_progress(_("Refreshing status and checking versions...")) + self._reset_download_icon() + + for app_key, app in self.app_manager.get_all_apps().items(): + status_label = self.status_labels[app_key] + version_label = self.version_labels[app_key] + + if app.is_installed(): + installed_version = app.get_installed_version() + status_label.config( + text=f"✅ {_('Installed')} (v{installed_version})", fg="green" + ) + + # Get latest version from API + try: + latest_version = app.get_latest_version() + if latest_version != "Unknown": + if installed_version != latest_version: + version_label.config( + text=f"{_('Latest')}: v{latest_version} ({_('Update available')})", + fg="orange", + ) + else: + version_label.config( + text=f"{_('Latest')}: v{latest_version} ({_('Up to date')})", + fg="green", + ) + else: + version_label.config( + text=f"{_('Latest')}: {_('Unknown')}", fg="gray" + ) + except: + version_label.config( + text=f"{_('Latest')}: {_('Check failed')}", fg="gray" + ) + else: + status_label.config(text=f"❌ {_('Not installed')}", fg="red") + + # Still show latest available version + try: + latest_version = app.get_latest_version() + if latest_version != "Unknown": + version_label.config( + text=f"{_('Available')}: v{latest_version}", fg="blue" + ) + else: + version_label.config( + text=f"{_('Available')}: {_('Unknown')}", fg="gray" + ) + except: + version_label.config( + text=f"{_('Available')}: {_('Check failed')}", fg="gray" + ) + + self.update_progress(_("Status refresh completed.")) + + def install_app(self): + """Handle install button click""" + selected_app = self.app_var.get() + if not selected_app: + messagebox.showwarning( + _("Warning"), _("Please select an application to install.") + ) + return + + # Check internet connection + if not NetworkChecker.check_internet_connection(): + self.update_download_icon("error") + messagebox.showerror( + _("Network Error"), + _( + "No internet connection available.\nPlease check your network connection." + ), + ) + return + + if not NetworkChecker.check_repository_access(): + self.update_download_icon("error") + messagebox.showerror( + _("Repository Error"), + _("Cannot access repository.\nPlease try again later."), + ) + return + + # Reset download icon + self._reset_download_icon() + app = self.app_manager.get_app(selected_app) + + # Check if already installed + if app.is_installed(): + installed_version = app.get_installed_version() + latest_version = app.get_latest_version() + + dialog_text = ( + f"{app.name} {_('is already installed')}.\n\n" + f"{_('Installed version')}: v{installed_version}\n" + f"{_('Latest version')}: v{latest_version}\n\n" + f"{_('YES')} = {_('Update')} ({_('reinstall all files')})\n" + f"{_('NO')} = {_('Uninstall')}\n" + f"{_('Cancel')} = {_('Do nothing')}" + ) + + result = messagebox.askyesnocancel( + f"{app.name} {_('already installed')}", dialog_text + ) + + if result is None: # Cancel + self.update_progress(_("Installation cancelled.")) + return + elif not result: # Uninstall + self.uninstall_app(selected_app) + return + else: # Update + self.update_progress(_("Updating application...")) + + try: + self.installation_manager.install_app(selected_app) + messagebox.showinfo( + _("Success"), + f"{app.name} {_('has been successfully installed/updated')}.", + ) + self.refresh_status() + except Exception as e: + # Bei Fehler Error-Icon anzeigen + self.update_download_icon("error") + messagebox.showerror(_("Error"), f"{_('Installation failed')}: {e}") + + def uninstall_app(self, app_key=None): + """Handle uninstall button click""" + if app_key is None: + app_key = self.app_var.get() + + if not app_key: + messagebox.showwarning( + _("Warning"), _("Please select an application to uninstall.") + ) + return + + app = self.app_manager.get_app(app_key) + + if not app.is_installed(): + messagebox.showinfo(_("Info"), f"{app.name} {_('is not installed')}.") + return + + result = messagebox.askyesno( + _("Confirm Uninstall"), + f"{_('Are you sure you want to uninstall')} {app.name}?\n\n" + f"{_('This will remove all application files and user configurations')}.", + ) + if not result: + return + + try: + self.installation_manager.uninstall_app(app_key) + messagebox.showinfo( + _("Success"), f"{app.name} {_('has been successfully uninstalled')}." + ) + self.refresh_status() + except Exception as e: + messagebox.showerror(_("Error"), f"{_('Uninstallation failed')}: {e}") + + def run(self): + """Start the GUI application""" + root = self.create_gui() + root.mainloop() + + +# ---------------------------- +# Main Application Entry Point +# ---------------------------- +def main(): + """Main function to start the application""" + try: + # Create and run the GUI + app = LXToolsGUI() + app.run() + except KeyboardInterrupt: + print("\nApplication interrupted by user.") + except Exception as e: + print(f"Fatal error: {e}") + messagebox.showerror( + _("Fatal Error"), f"{_('Application failed to start')}: {e}" + ) + + +if __name__ == "__main__": + main() diff --git a/lxtools_installerv2.py b/lxtools_installerv2.py new file mode 100755 index 0000000..0ff6bc2 --- /dev/null +++ b/lxtools_installerv2.py @@ -0,0 +1,1584 @@ +#!/usr/bin/python3 +import tkinter as tk +from tkinter import messagebox, ttk +import os +import subprocess +import urllib.request +import json +import tempfile +import zipfile +import shutil +import sys +import socket + +# Add current directory to path for imports +current_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, current_dir) + +# Try to import common_tools, but provide fallback +try: + from common_tools import LxTools, center_window_cross_platform + + USE_LXTOOLS = True + print("Using local common_tools from LxTools project") +except ImportError: + try: + from shared_libs.common_tools import LxTools, center_window_cross_platform + + USE_LXTOOLS = True + print("Using shared_libs.common_tools") + except ImportError: + print("Warning: common_tools not found, using integrated methods") + USE_LXTOOLS = False + + +# ---------------------------- +# App Configuration Class (korrigiert) +# ---------------------------- +class LXToolsAppConfig: + VERSION = "1.0.8" + APP_NAME = "LX Tools Installer" + WINDOW_WIDTH = 650 + WINDOW_HEIGHT = 600 + DEBUG_WINDOW_HEIGHT = 700 + + # LxTools Installer eigene Ressourcen + WORK_DIR = os.path.dirname(os.path.abspath(__file__)) + ICONS_DIR = os.path.join(WORK_DIR, "lx-icons") + THEMES_DIR = os.path.join(WORK_DIR, "TK-Themes") + + # Download URLs für alle installierbaren Projekte + PROJECTS = { + "wirepy": { + "name": "Wire-Py", + "description": "🔐 WireGuard VPN Manager", + "download_url": "https://git.ilunix.de/punix/Wire-Py/archive/main.zip", + "api_url": "https://git.ilunix.de/api/v1/repos/punix/Wire-Py/releases", + "archive_folder": "Wire-Py", + "icon_key": "wirepy_icon", + }, + "logviewer": { + "name": "LogViewer", + "description": "📋 System Log Viewer", + "download_url": "https://git.ilunix.de/punix/shared_libs/archive/main.zip", + "api_url": "https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases", + "archive_folder": "shared_libs", + "icon_key": "logviewer_icon", + }, + } + + # Shared Libraries (für alle Projekte benötigt) + SHARED_LIBS_URL = "https://git.ilunix.de/punix/shared_libs/archive/main.zip" + SHARED_LIBS_API_URL = ( + "https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases" + ) + + @staticmethod + def get_icon_path(icon_key): + """Get icon path with fallbacks - KORRIGIERT""" + base_paths = [ + LXToolsAppConfig.ICONS_DIR, + os.path.join(LXToolsAppConfig.WORK_DIR, "lx-icons"), + "./lx-icons", + "/usr/share/icons/lx-icons", + ] + + icon_mapping = { + "app_icon": ["64/download.png", "48/download.png", "32/download.png"], + "wirepy_icon": ["32/wg_vpn.png", "48/wg_vpn.png"], + "logviewer_icon": ["32/log.png", "48/log.png"], + "download_icon": ["32/download.png", "48/download.png"], + "download_error_icon": ["32/download_error.png", "48/error.png"], + "success_icon": ["32/download.png", "48/download.png"], + } + + if icon_key not in icon_mapping: + return None + + for base_path in base_paths: + if not os.path.exists(base_path): + continue + for icon_file in icon_mapping[icon_key]: + full_path = os.path.join(base_path, icon_file) + if os.path.exists(full_path): + print(f"Found icon: {full_path}") + return full_path + + print(f"Icon not found: {icon_key}") + return None + + +# ---------------------------- +# Integrierte LxTools Methoden (korrigiert) +# ---------------------------- +class IntegratedLxTools: + @staticmethod + def center_window_cross_platform(window): + """Center window on screen - works with multiple monitors""" + window.update_idletasks() + + # Get window dimensions + window_width = window.winfo_reqwidth() + window_height = window.winfo_reqheight() + + # Get screen dimensions + screen_width = window.winfo_screenwidth() + screen_height = window.winfo_screenheight() + + # Calculate position + pos_x = (screen_width // 2) - (window_width // 2) + pos_y = (screen_height // 2) - (window_height // 2) + + # Ensure window is not positioned off-screen + pos_x = max(0, pos_x) + pos_y = max(0, pos_y) + + window.geometry(f"{window_width}x{window_height}+{pos_x}+{pos_y}") + + @staticmethod + def msg_window(parent, title, message, msg_type="info", width=400, height=200): + """Custom message window with proper centering""" + msg_win = tk.Toplevel(parent) + msg_win.title(title) + msg_win.geometry(f"{width}x{height}") + msg_win.transient(parent) + msg_win.grab_set() + + # Configure grid + msg_win.grid_columnconfigure(0, weight=1) + msg_win.grid_rowconfigure(0, weight=1) + msg_win.grid_rowconfigure(1, weight=0) + + # Message frame + msg_frame = ttk.Frame(msg_win, padding=20) + msg_frame.grid(row=0, column=0, sticky="nsew") + msg_frame.grid_columnconfigure(0, weight=1) + msg_frame.grid_rowconfigure(0, weight=1) + + # Message text + msg_label = tk.Label( + msg_frame, + text=message, + wraplength=width - 40, + justify="left", + font=("Helvetica", 10), + ) + msg_label.grid(row=0, column=0, sticky="nsew") + + # Button frame + btn_frame = ttk.Frame(msg_win) + btn_frame.grid(row=1, column=0, sticky="ew", padx=20, pady=(0, 20)) + btn_frame.grid_columnconfigure(0, weight=1) + + # OK Button + ok_btn = ttk.Button(btn_frame, text="OK", command=msg_win.destroy) + ok_btn.grid(row=0, column=0) + + # Center the window + IntegratedLxTools.center_window_cross_platform(msg_win) + + # Focus + msg_win.focus_set() + ok_btn.focus_set() + + return msg_win + + +# ---------------------------- +# Theme Manager Class (korrigiert) +# ---------------------------- +class ThemeManager: + @staticmethod + def apply_light_theme(root): + """Apply light theme using your working method""" + try: + # Verwende TK-Themes aus dem aktuellen LxTools Projekt-Ordner + theme_dir = LXToolsAppConfig.THEMES_DIR + water_theme_path = os.path.join(theme_dir, "water.tcl") + + print(f"Looking for theme at: {water_theme_path}") + + if os.path.exists(water_theme_path): + try: + # DEINE funktionierende Methode: + root.tk.call("source", water_theme_path) + root.tk.call("set_theme", "light") + print("Successfully applied water theme with set_theme light") + return True + except tk.TclError as e: + print(f"Theme loading failed: {e}") + + # Fallback: Versuche ohne set_theme + try: + root.tk.call("source", water_theme_path) + style = ttk.Style() + available_themes = style.theme_names() + print(f"Available themes: {available_themes}") + + # Versuche verschiedene Theme-Namen + for theme_name in ["water", "Water", "light", "awlight"]: + if theme_name in available_themes: + style.theme_use(theme_name) + print(f"Applied theme: {theme_name}") + return True + + except Exception as e2: + print(f"Fallback theme loading failed: {e2}") + else: + print(f"Theme file not found: {water_theme_path}") + print(f"Current working directory: {os.getcwd()}") + print(f"Theme directory exists: {os.path.exists(theme_dir)}") + if os.path.exists(theme_dir): + print(f"Files in theme directory: {os.listdir(theme_dir)}") + + # System theme fallback + try: + style = ttk.Style() + if "clam" in style.theme_names(): + style.theme_use("clam") + print("Using fallback theme: clam") + return True + except: + pass + + except Exception as e: + print(f"Theme loading completely failed: {e}") + + return False + + +# ---------------------------- +# Image Manager Class (korrigiert) +# ---------------------------- +class ImageManager: + def __init__(self): + self.images = {} + + def load_image(self, icon_key, fallback_paths=None): + """Load PNG image using tk.PhotoImage with fallback options""" + if icon_key in self.images: + return self.images[icon_key] + + # Get primary path from config + primary_path = LXToolsAppConfig.get_icon_path(icon_key) + paths_to_try = [] + + if primary_path: + paths_to_try.append(primary_path) + + # Add fallback paths + if fallback_paths: + paths_to_try.extend(fallback_paths) + + # Try to load image from paths + for path in paths_to_try: + try: + if os.path.exists(path): + photo = tk.PhotoImage(file=path) + self.images[icon_key] = photo + print(f"Successfully loaded image: {path}") + return photo + except tk.TclError as e: + print(f"Failed to load image from {path}: {e}") + continue + + # Return None if no image found + print(f"No image found for key: {icon_key}") + return None + + +# ---------------------------- +# OS Detection Class (korrigiert) +# ---------------------------- +class OSDetector: + OS_DETECTION = [ + ("mint", "Linux Mint"), + ("pop", "Pop!_OS"), + ("manjaro", "Manjaro"), + ("garuda", "Garuda Linux"), + ("endeavouros", "EndeavourOS"), + ("fedora", "Fedora"), + ("tumbleweed", "SUSE Tumbleweed"), + ("leap", "SUSE Leap"), + ("suse", "openSUSE"), + ("arch", "Arch Linux"), + ("ubuntu", "Ubuntu"), + ("debian", "Debian"), + ] + + @staticmethod + def detect_os(): + """Detect operating system using ordered list""" + try: + with open("/etc/os-release", "r") as f: + content = f.read().lower() + + # Check each OS in order (specific first) + for keyword, os_name in OSDetector.OS_DETECTION: + if keyword in content: + return os_name + + return "Unknown System" + except FileNotFoundError: + return "File not found" + + +# ---------------------------- +# Network Checker Class (korrigiert) +# ---------------------------- +class NetworkChecker: + @staticmethod + def check_internet(host="8.8.8.8", port=53, timeout=3): # ← Korrigierter Name + """Check if internet connection is available""" + try: + socket.setdefaulttimeout(timeout) + socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) + return True + except socket.error: + return False + + @staticmethod + def check_repository(url="https://git.ilunix.de"): # ← Korrigierter Name + """Check if repository is accessible""" + try: + urllib.request.urlopen(url, timeout=5) + return True + except: + return False + + +# ---------------------------- +# App Manager Class (korrigiert) +# ---------------------------- +class AppManager: + def __init__(self): + self.projects = LXToolsAppConfig.PROJECTS + + def get_all_projects(self): + """Get all project configurations""" + return self.projects + + def get_project_info(self, project_key): + """Get project information by key""" + return self.projects.get(project_key) + + def is_installed(self, project_key): + """Check if project is installed""" + if project_key == "wirepy": + return os.path.exists("/usr/local/bin/wirepy") + elif project_key == "logviewer": + return os.path.exists("/usr/local/bin/logviewer") + else: + return os.path.exists(f"/usr/local/bin/{project_key}") + + def get_installed_version(self, project_key): + """Get installed version from config file""" + try: + if project_key == "wirepy": + config_file = ( + "/usr/lib/python3/dist-packages/shared_libs/wp_app_config.py" + ) + elif project_key == "logviewer": + config_file = ( + "/usr/lib/python3/dist-packages/shared_libs/logview_app_config.py" + ) + else: + config_file = f"/usr/lib/python3/dist-packages/shared_libs/{project_key}_app_config.py" + + if os.path.exists(config_file): + with open(config_file, "r") as f: + content = f.read() + for line in content.split("\n"): + if "VERSION" in line and "=" in line: + version = line.split("=")[1].strip().strip("\"'") + return version + return "Unknown" + except Exception as e: + print(f"Error getting version for {project_key}: {e}") + return "Unknown" + + def get_latest_version(self, project_key): + """Get latest version from API - KORRIGIERT""" + try: + project_info = self.get_project_info(project_key) + if not project_info: + return "Unknown" + + with urllib.request.urlopen( + project_info["api_url"], timeout=10 + ) as response: + data = json.loads(response.read().decode()) + if data and len(data) > 0: + latest_version = data[0].get("tag_name", "Unknown") + return latest_version.lstrip("v") + return "Unknown" # ← FIX: Korrigierte Syntax + except Exception as e: + print(f"API Error for {project_key}: {e}") + return "Unknown" # ← FIX: Korrigierte Syntax + + +# ---------------------------- +# Installation Manager Class (korrigiert) +# ---------------------------- +class InstallationManager: + def __init__( + self, + app_manager, + progress_callback=None, + icon_callback=None, + debug_callback=None, + ): + self.app_manager = app_manager + self.progress_callback = progress_callback + self.icon_callback = icon_callback + self.debug_callback = debug_callback + + def install_project(self, project_key): + """Install any project generically""" + project_info = self.app_manager.get_project_info(project_key) + if not project_info: + raise Exception(f"Unknown project: {project_key}") + + self.update_progress(f"Starting {project_info['name']} installation...") + self.update_icon("downloading") + + try: + with tempfile.TemporaryDirectory() as temp_dir: + # Download project + self.update_progress(f"Downloading {project_info['name']}...") + if not self._download_and_extract( + project_info["download_url"], temp_dir + ): + raise Exception(f"Failed to download {project_info['name']}") + + # Download shared libs + self.update_progress("Downloading shared libraries...") + shared_temp = os.path.join(temp_dir, "shared") + if not self._download_and_extract( + LXToolsAppConfig.SHARED_LIBS_URL, shared_temp + ): + raise Exception("Failed to download shared libraries") + + # Create installation script + self.update_progress("Preparing installation...") + script_path = self._create_install_script( + project_key, project_info, temp_dir + ) + + # Execute installation + self.update_progress("Installing...") + self._execute_install_script(script_path) + + self.update_progress(f"{project_info['name']} installation completed!") + self.update_icon("success") + return True + + except Exception as e: + self.update_icon("error") + raise Exception(f"Installation failed: {e}") + + def _download_and_extract(self, url, extract_to): + """Download and extract ZIP file""" + try: + with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file: + urllib.request.urlretrieve(url, tmp_file.name) + + with zipfile.ZipFile(tmp_file.name, "r") as zip_ref: + zip_ref.extractall(extract_to) + + os.unlink(tmp_file.name) + return True + + except Exception as e: + self.debug_log(f"Download failed: {e}") + return False + + def _create_install_script(self, project_key, project_info, temp_dir): + """Create installation script for any project""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) as f: + + project_source = os.path.join(temp_dir, project_info["archive_folder"]) + shared_source = os.path.join(temp_dir, "shared", "shared_libs") + + if project_key == "wirepy": + script_content = f"""#!/bin/bash +set -e + +echo "=== {project_info['name']} Installation Script ===" + +# Create directories +mkdir -p /usr/lib/python3/dist-packages/shared_libs +mkdir -p /usr/share/icons/lx-icons +mkdir -p /usr/share/locale/de/LC_MESSAGES +mkdir -p /usr/share/applications +mkdir -p /usr/local/etc/ssl +mkdir -p /usr/share/polkit-1/actions + +# Install shared libraries +echo "Installing shared libraries..." +if [ -d "{shared_source}" ]; then + cp -f "{shared_source}"/*.py /usr/lib/python3/dist-packages/shared_libs/ 2>/dev/null || true +fi + +# Install Wire-Py files +echo "Installing Wire-Py files..." +cp -f "{project_source}/wirepy.py" /usr/local/bin/ +cp -f "{project_source}/start_wg.py" /usr/local/bin/ +cp -f "{project_source}/ssl_encrypt.py" /usr/local/bin/ +cp -f "{project_source}/ssl_decrypt.py" /usr/local/bin/ +cp -f "{project_source}/match_found.py" /usr/local/bin/ +cp -f "{project_source}/tunnel.py" /usr/local/bin/ 2>/dev/null || true + +# Make executable +chmod 755 /usr/local/bin/wirepy.py +chmod 755 /usr/local/bin/start_wg.py +chmod 755 /usr/local/bin/ssl_encrypt.py +chmod 755 /usr/local/bin/ssl_decrypt.py +chmod 755 /usr/local/bin/match_found.py +chmod 755 /usr/local/bin/tunnel.py 2>/dev/null || true + +# Install config +cp -f "{project_source}/wp_app_config.py" /usr/lib/python3/dist-packages/shared_libs/ + +# Install icons +echo "Installing icons..." +if [ -d "{project_source}/lx-icons" ]; then + cp -rf "{project_source}/lx-icons"/* /usr/share/icons/lx-icons/ +fi + +# Install desktop file +if [ -f "{project_source}/Wire-Py.desktop" ]; then + cp -f "{project_source}/Wire-Py.desktop" /usr/share/applications/ +fi + +# Install policy file +if [ -f "{project_source}/org.sslcrypt.policy" ]; then + cp -f "{project_source}/org.sslcrypt.policy" /usr/share/polkit-1/actions/ +fi + +# Install language files +echo "Installing language files..." +if [ -d "{project_source}/languages/de" ]; then + cp -f "{project_source}/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true +fi + +# Create symlink +rm -f /usr/local/bin/wirepy +ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy + +# Create SSL key if not exists +if [ ! -f "/usr/local/etc/ssl/pwgk.pem" ]; then + echo "Creating SSL key..." + openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096 2>/dev/null || echo "Warning: SSL key creation failed" + chmod 600 /usr/local/etc/ssl/pwgk.pem 2>/dev/null || true +fi + +echo "=== {project_info['name']} installation completed ===" +""" + + elif project_key == "logviewer": + script_content = f"""#!/bin/bash +set -e + +echo "=== {project_info['name']} Installation Script ===" + +# Create directories +mkdir -p /usr/lib/python3/dist-packages/shared_libs +mkdir -p /usr/share/icons/lx-icons +mkdir -p /usr/share/locale/de/LC_MESSAGES +mkdir -p /usr/share/applications + +# Install shared libraries +echo "Installing shared libraries..." +if [ -d "{shared_source}" ]; then + cp -f "{shared_source}"/*.py /usr/lib/python3/dist-packages/shared_libs/ 2>/dev/null || true +fi + +# Install LogViewer +echo "Installing LogViewer..." +cp -f "{shared_source}/logviewer.py" /usr/local/bin/ +chmod 755 /usr/local/bin/logviewer.py + +# Install config +cp -f "{shared_source}/logview_app_config.py" /usr/lib/python3/dist-packages/shared_libs/ + +# Install icons (if available) +if [ -d "{shared_source}/lx-icons" ]; then + cp -rf "{shared_source}/lx-icons"/* /usr/share/icons/lx-icons/ 2>/dev/null || true +fi + +# Install desktop file (if available) +if [ -f "{shared_source}/LogViewer.desktop" ]; then + cp -f "{shared_source}/LogViewer.desktop" /usr/share/applications/ +fi + +# Install language files (if available) +if [ -d "{shared_source}/languages/de" ]; then + cp -f "{shared_source}/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true +fi + +# Create symlink +rm -f /usr/local/bin/logviewer +ln -sf /usr/local/bin/logviewer.py /usr/local/bin/logviewer + +echo "=== {project_info['name']} installation completed ===" +""" + + else: + # Generisches Script für zukünftige Projekte + script_content = f"""#!/bin/bash +set -e + +echo "=== {project_info['name']} Installation Script ===" + +# Create directories +mkdir -p /usr/lib/python3/dist-packages/shared_libs +mkdir -p /usr/share/icons/lx-icons +mkdir -p /usr/share/locale/de/LC_MESSAGES +mkdir -p /usr/share/applications + +# Install shared libraries +echo "Installing shared libraries..." +if [ -d "{shared_source}" ]; then + cp -f "{shared_source}"/*.py /usr/lib/python3/dist-packages/shared_libs/ 2>/dev/null || true +fi + +# Install project files (generic approach) +echo "Installing {project_info['name']} files..." +if [ -f "{project_source}/{project_key}.py" ]; then + cp -f "{project_source}/{project_key}.py" /usr/local/bin/ + chmod 755 /usr/local/bin/{project_key}.py + + # Create symlink + rm -f /usr/local/bin/{project_key} + ln -sf /usr/local/bin/{project_key}.py /usr/local/bin/{project_key} +fi + +# Install config (if exists) +if [ -f "{project_source}/{project_key}_app_config.py" ]; then + cp -f "{project_source}/{project_key}_app_config.py" /usr/lib/python3/dist-packages/shared_libs/ +fi + +# Install icons (if available) +if [ -d "{project_source}/lx-icons" ]; then + cp -rf "{project_source}/lx-icons"/* /usr/share/icons/lx-icons/ 2>/dev/null || true +fi + +# Install desktop file (if available) +if [ -f "{project_source}/{project_info['name']}.desktop" ]; then + cp -f "{project_source}/{project_info['name']}.desktop" /usr/share/applications/ +fi + +# Install language files (if available) +if [ -d "{project_source}/languages/de" ]; then + cp -f "{project_source}/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true +fi + +echo "=== {project_info['name']} installation completed ===" +""" + + f.write(script_content) + script_path = f.name + + # Make script executable + os.chmod(script_path, 0o755) + self.debug_log(f"Created install script: {script_path}") + return script_path + + def _execute_install_script(self, script_path): + """Execute installation script with pkexec""" + try: + result = subprocess.run( + ["pkexec", "bash", script_path], + capture_output=True, + text=True, + timeout=120, + ) + + if result.returncode != 0: + error_msg = f"Installation script failed: {result.stderr}" + self.debug_log(f"ERROR: {error_msg}") + self.debug_log(f"STDOUT: {result.stdout}") + raise Exception(error_msg) + + self.debug_log("Installation script output:") + self.debug_log(result.stdout) + + except subprocess.TimeoutExpired: + raise Exception("Installation timed out") + except subprocess.CalledProcessError as e: + raise Exception(f"Installation script failed: {e}") + + def update_progress(self, message): + if self.progress_callback: + self.progress_callback(message) + self.debug_log(f"Progress: {message}") + + def update_icon(self, status): + if self.icon_callback: + self.icon_callback(status) + + def debug_log(self, message): + if self.debug_callback: + self.debug_callback(message) + print(message) + + +# ---------------------------- +# Uninstallation Manager Class (korrigiert) +# ---------------------------- +class UninstallationManager: + def __init__(self, app_manager, progress_callback=None, debug_callback=None): + self.app_manager = app_manager + self.progress_callback = progress_callback + self.debug_callback = debug_callback + + def uninstall_project(self, project_key): + """Uninstall any project generically""" + project_info = self.app_manager.get_project_info(project_key) + if not project_info: + raise Exception(f"Unknown project: {project_key}") + + if not self.app_manager.is_installed(project_key): + raise Exception(f"{project_info['name']} is not installed.") + + self.update_progress(f"Starting {project_info['name']} uninstallation...") + + try: + # Create uninstallation script + script_path = self._create_uninstall_script(project_key, project_info) + + # Execute uninstallation + self.update_progress("Executing uninstallation...") + self._execute_uninstall_script(script_path) + + # Remove user config directories + self._cleanup_user_files(project_key) + + self.update_progress(f"{project_info['name']} uninstalled successfully!") + return True + + except Exception as e: + self.update_progress(f"Error during uninstallation: {str(e)}") + raise + + def _create_uninstall_script(self, project_key, project_info): + """Create uninstallation script""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) as f: + + if project_key == "wirepy": + script_content = f"""#!/bin/bash +set -e + +echo "=== {project_info['name']} Uninstallation Script ===" + +# Remove Wire-Py files +rm -f /usr/local/bin/wirepy.py +rm -f /usr/local/bin/start_wg.py +rm -f /usr/local/bin/ssl_encrypt.py +rm -f /usr/local/bin/ssl_decrypt.py +rm -f /usr/local/bin/match_found.py +rm -f /usr/local/bin/tunnel.py +rm -f /usr/local/bin/wirepy + +# Remove config +rm -f /usr/lib/python3/dist-packages/shared_libs/wp_app_config.py + +# Remove desktop file +rm -f /usr/share/applications/Wire-Py.desktop + +# Remove policy file +rm -f /usr/share/polkit-1/actions/org.sslcrypt.policy + +# Remove language files +rm -f /usr/share/locale/de/LC_MESSAGES/wirepy.mo + +# Check if other projects are still installed +OTHER_PROJECTS_INSTALLED=false +if [ -f "/usr/local/bin/logviewer" ]; then + OTHER_PROJECTS_INSTALLED=true +fi + +# Remove shared resources only if no other projects +if [ "$OTHER_PROJECTS_INSTALLED" = false ]; then + echo "No other LX projects found, removing shared resources..." + rm -rf /usr/share/icons/lx-icons + rm -rf /usr/local/etc/ssl + rmdir /usr/lib/python3/dist-packages/shared_libs 2>/dev/null || true +fi + +echo "=== {project_info['name']} uninstallation completed ===" +""" + + elif project_key == "logviewer": + script_content = f"""#!/bin/bash +set -e + +echo "=== {project_info['name']} Uninstallation Script ===" + +# Remove LogViewer files +rm -f /usr/local/bin/logviewer.py +rm -f /usr/local/bin/logviewer + +# Remove config +rm -f /usr/lib/python3/dist-packages/shared_libs/logview_app_config.py + +# Remove desktop file +rm -f /usr/share/applications/LogViewer.desktop + +# Remove language files +rm -f /usr/share/locale/de/LC_MESSAGES/logviewer.mo + +# Check if other projects are still installed +OTHER_PROJECTS_INSTALLED=false +if [ -f "/usr/local/bin/wirepy" ]; then + OTHER_PROJECTS_INSTALLED=true +fi + +# Remove shared resources only if no other projects +if [ "$OTHER_PROJECTS_INSTALLED" = false ]; then + echo "No other LX projects found, removing shared resources..." + rm -rf /usr/share/icons/lx-icons + rmdir /usr/lib/python3/dist-packages/shared_libs 2>/dev/null || true +fi + +echo "=== {project_info['name']} uninstallation completed ===" +""" + + else: + # Generisches Uninstall-Script + script_content = f"""#!/bin/bash +set -e + +echo "=== {project_info['name']} Uninstallation Script ===" + +# Remove project files +rm -f /usr/local/bin/{project_key}.py +rm -f /usr/local/bin/{project_key} + +# Remove config +rm -f /usr/lib/python3/dist-packages/shared_libs/{project_key}_app_config.py + +# Remove desktop file +rm -f /usr/share/applications/{project_info['name']}.desktop + +echo "=== {project_info['name']} uninstallation completed ===" +""" + + f.write(script_content) + script_path = f.name + + # Make script executable + os.chmod(script_path, 0o755) + self.debug_log(f"Created uninstall script: {script_path}") + return script_path + + def _execute_uninstall_script(self, script_path): + """Execute uninstallation script with pkexec""" + try: + result = subprocess.run( + ["pkexec", "bash", script_path], + capture_output=True, + text=True, + timeout=60, + ) + + if result.returncode != 0: + error_msg = f"Uninstallation script failed: {result.stderr}" + self.debug_log(f"ERROR: {error_msg}") + self.debug_log(f"STDOUT: {result.stdout}") + raise Exception(error_msg) + + self.debug_log("Uninstallation script output:") + self.debug_log(result.stdout) + + except subprocess.TimeoutExpired: + raise Exception("Uninstallation timed out") + except subprocess.CalledProcessError as e: + raise Exception(f"Uninstallation script failed: {e}") + + def _cleanup_user_files(self, project_key): + """Clean up user configuration files""" + try: + if project_key == "wirepy": + config_dir = os.path.expanduser("~/.config/wire_py") + log_file = os.path.expanduser("~/.local/share/lxlogs/wirepy.log") + elif project_key == "logviewer": + config_dir = os.path.expanduser("~/.config/logviewer") + log_file = os.path.expanduser("~/.local/share/lxlogs/logviewer.log") + else: + config_dir = os.path.expanduser(f"~/.config/{project_key}") + log_file = os.path.expanduser( + f"~/.local/share/lxlogs/{project_key}.log" + ) + + # Remove user config directory + if os.path.exists(config_dir): + shutil.rmtree(config_dir) + self.debug_log(f"Removed user config: {config_dir}") + + # Remove log file + if os.path.exists(log_file): + os.remove(log_file) + self.debug_log(f"Removed log file: {log_file}") + + except Exception as e: + self.debug_log(f"Warning: Could not clean up user files: {e}") + + def update_progress(self, message): + if self.progress_callback: + self.progress_callback(message) + self.debug_log(f"Progress: {message}") + + def debug_log(self, message): + if self.debug_callback: + self.debug_callback(message) + print(message) + + +# ---------------------------- +# Main GUI Application Class (korrigiert) +# ---------------------------- +class LXToolsGUI: + def __init__(self): + self.root = None + self.progress_label = None + self.download_icon_label = None + self.project_var = None + self.status_labels = {} + self.version_labels = {} + self.debug_text = None + self.show_debug = False + + # Initialize managers + self.app_manager = AppManager() + self.installation_manager = InstallationManager( + self.app_manager, + self.update_progress, + self.update_download_icon, + self.debug_log, + ) + self.uninstallation_manager = UninstallationManager( + self.app_manager, self.update_progress, self.debug_log + ) + self.image_manager = ImageManager() + + # Detect OS + self.detected_os = OSDetector.detect_os() + + def create_gui(self): + """Create the main GUI""" + self.root = tk.Tk() + self.root.title(f"{LXToolsAppConfig.APP_NAME} v{LXToolsAppConfig.VERSION}") + + # Set window size + window_height = ( + LXToolsAppConfig.DEBUG_WINDOW_HEIGHT + if self.show_debug + else LXToolsAppConfig.WINDOW_HEIGHT + ) + self.root.geometry(f"{LXToolsAppConfig.WINDOW_WIDTH}x{window_height}") + + # Apply theme + ThemeManager.apply_light_theme(self.root) + + # Configure main grid + self.root.grid_columnconfigure(0, weight=1) + self.root.grid_rowconfigure(0, weight=0) # Header + self.root.grid_rowconfigure(1, weight=0) # OS Info + self.root.grid_rowconfigure(2, weight=1) # Projects + self.root.grid_rowconfigure(3, weight=0) # Progress + self.root.grid_rowconfigure(4, weight=0) # Buttons + if self.show_debug: + self.root.grid_rowconfigure(5, weight=1) # Debug + + # Create GUI sections + self._create_header_section() + self._create_os_info_section() + self._create_projects_section() + self._create_progress_section() + self._create_buttons_section() + + if self.show_debug: + self._create_debug_section() + + # Load app icon + self._load_app_icon() + + # Center window + if USE_LXTOOLS: + center_window_cross_platform(self.root) + else: + IntegratedLxTools.center_window_cross_platform(self.root) + + # Initial status refresh + self.root.after(100, self.refresh_status) + + return self.root + + def _create_header_section(self): + """Create header section with title and version""" + header_frame = ttk.Frame(self.root, padding=15) + header_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 0)) + header_frame.grid_columnconfigure(0, weight=1) + + # Title + title_label = tk.Label( + header_frame, + text=f"🚀 {LXToolsAppConfig.APP_NAME}", + font=("Helvetica", 16, "bold"), + fg="#2E86AB", + ) + title_label.grid(row=0, column=0, sticky="w") + + # Version + version_label = tk.Label( + header_frame, + text=f"Version {LXToolsAppConfig.VERSION}", + font=("Helvetica", 10), + fg="gray", + ) + version_label.grid(row=1, column=0, sticky="w") + + def _create_os_info_section(self): + """Create OS information section""" + os_frame = ttk.LabelFrame(self.root, text="System Information", padding=10) + os_frame.grid(row=1, column=0, sticky="ew", padx=15, pady=10) + os_frame.grid_columnconfigure(1, weight=1) + + # OS Detection + tk.Label(os_frame, text="Detected OS:", font=("Helvetica", 10, "bold")).grid( + row=0, column=0, sticky="w", padx=(0, 10) + ) + tk.Label(os_frame, text=self.detected_os, font=("Helvetica", 10)).grid( + row=0, column=1, sticky="w" + ) + + # Working Directory + tk.Label(os_frame, text="Working Dir:", font=("Helvetica", 10, "bold")).grid( + row=1, column=0, sticky="w", padx=(0, 10) + ) + tk.Label(os_frame, text=LXToolsAppConfig.WORK_DIR, font=("Helvetica", 9)).grid( + row=1, column=1, sticky="w" + ) + + def _create_projects_section(self): + """Create projects selection and status section""" + projects_frame = ttk.LabelFrame( + self.root, text="Available Projects", padding=10 + ) + projects_frame.grid(row=2, column=0, sticky="nsew", padx=15, pady=(0, 10)) + projects_frame.grid_columnconfigure(0, weight=1) + projects_frame.grid_rowconfigure(1, weight=1) + + # Project selection + selection_frame = ttk.Frame(projects_frame) + selection_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10)) + selection_frame.grid_columnconfigure(1, weight=1) + + tk.Label( + selection_frame, text="Select Project:", font=("Helvetica", 10, "bold") + ).grid(row=0, column=0, sticky="w", padx=(0, 10)) + + self.project_var = tk.StringVar() + project_combo = ttk.Combobox( + selection_frame, + textvariable=self.project_var, + values=list(self.app_manager.get_all_projects().keys()), + state="readonly", + width=20, + ) + project_combo.grid(row=0, column=1, sticky="w") + project_combo.bind("<>", self._on_project_selected) + + # Projects status frame with scrollbar + status_container = ttk.Frame(projects_frame) + status_container.grid(row=1, column=0, sticky="nsew") + status_container.grid_columnconfigure(0, weight=1) + status_container.grid_rowconfigure(0, weight=1) + + # Canvas for scrolling + canvas = tk.Canvas(status_container, height=200) + scrollbar = ttk.Scrollbar( + status_container, orient="vertical", command=canvas.yview + ) + scrollable_frame = ttk.Frame(canvas) + + scrollable_frame.bind( + "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) + ) + + canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + + canvas.grid(row=0, column=0, sticky="nsew") + scrollbar.grid(row=0, column=1, sticky="ns") + + # Project status entries + scrollable_frame.grid_columnconfigure(1, weight=1) + + row = 0 + for project_key, project_info in self.app_manager.get_all_projects().items(): + # Project icon and name + project_frame = ttk.Frame(scrollable_frame, padding=5) + project_frame.grid(row=row, column=0, columnspan=3, sticky="ew", pady=2) + project_frame.grid_columnconfigure(1, weight=1) + + # Try to load project icon + icon = self.image_manager.load_image(project_info["icon_key"]) + if icon: + icon_label = tk.Label(project_frame, image=icon) + icon_label.image = icon # Keep reference + icon_label.grid(row=0, column=0, rowspan=2, padx=(0, 10)) + else: + # Fallback emoji + emoji_map = {"wirepy": "🔐", "logviewer": "📋"} + emoji = emoji_map.get(project_key, "📦") + tk.Label(project_frame, text=emoji, font=("Helvetica", 16)).grid( + row=0, column=0, rowspan=2, padx=(0, 10) + ) + + # Project name and description + tk.Label( + project_frame, + text=f"{project_info['name']} - {project_info['description']}", + font=("Helvetica", 11, "bold"), + ).grid(row=0, column=1, sticky="w") + + # Status label + status_label = tk.Label( + project_frame, text="Checking...", font=("Helvetica", 9) + ) + status_label.grid(row=1, column=1, sticky="w") + self.status_labels[project_key] = status_label + + # Version label + version_label = tk.Label(project_frame, text="", font=("Helvetica", 9)) + version_label.grid(row=2, column=1, sticky="w") + self.version_labels[project_key] = version_label + + row += 1 + + def _create_progress_section(self): + """Create progress section with download icon""" + progress_frame = ttk.LabelFrame(self.root, text="Progress", padding=10) + progress_frame.grid(row=3, column=0, sticky="ew", padx=15, pady=(0, 10)) + progress_frame.grid_columnconfigure(1, weight=1) + + # Download icon + self.download_icon_label = tk.Label(progress_frame, text="", width=4) + self.download_icon_label.grid(row=0, column=0, padx=(0, 10)) + + # Progress text + self.progress_label = tk.Label( + progress_frame, + text="Ready for installation...", + font=("Helvetica", 10), + fg="blue", + anchor="w", + ) + self.progress_label.grid(row=0, column=1, sticky="ew") + + # Reset download icon + self._reset_download_icon() + + def _create_buttons_section(self): + """Create buttons section""" + buttons_frame = ttk.Frame(self.root, padding=10) + buttons_frame.grid(row=4, column=0, sticky="ew", padx=15, pady=(0, 10)) + buttons_frame.grid_columnconfigure(0, weight=1) + + # Button container + btn_container = ttk.Frame(buttons_frame) + btn_container.grid(row=0, column=0) + + # Install/Update button + install_btn = ttk.Button( + btn_container, + text="📥 Install/Update", + command=self.install_project, + width=15, + ) + install_btn.grid(row=0, column=0, padx=(0, 10)) + + # Uninstall button + uninstall_btn = ttk.Button( + btn_container, text="🗑️ Uninstall", command=self.uninstall_project, width=15 + ) + uninstall_btn.grid(row=0, column=1, padx=(0, 10)) + + # Refresh button + refresh_btn = ttk.Button( + btn_container, text="Refresh", command=self.refresh_status, width=15 + ) + refresh_btn.grid(row=0, column=2, padx=(0, 10)) + + # Debug toggle button + debug_btn = ttk.Button( + btn_container, text="Debug", command=self.toggle_debug, width=15 + ) + debug_btn.grid(row=0, column=3) + + def _create_debug_section(self): + """Create debug section""" + debug_frame = ttk.LabelFrame(self.root, text="Debug Output", padding=10) + debug_frame.grid(row=5, column=0, sticky="nsew", padx=15, pady=(0, 10)) + debug_frame.grid_columnconfigure(0, weight=1) + debug_frame.grid_rowconfigure(0, weight=1) + + # Debug text with scrollbar + debug_container = ttk.Frame(debug_frame) + debug_container.grid(row=0, column=0, sticky="nsew") + debug_container.grid_columnconfigure(0, weight=1) + debug_container.grid_rowconfigure(0, weight=1) + + self.debug_text = tk.Text( + debug_container, height=8, font=("Courier", 9), bg="#f8f8f8", fg="#333333" + ) + self.debug_text.grid(row=0, column=0, sticky="nsew") + + # Scrollbar for debug text + debug_scrollbar = ttk.Scrollbar( + debug_container, orient="vertical", command=self.debug_text.yview + ) + debug_scrollbar.grid(row=0, column=1, sticky="ns") + self.debug_text.configure(yscrollcommand=debug_scrollbar.set) + + # Clear debug button + clear_debug_btn = ttk.Button( + debug_frame, text="Clear Debug", command=self.clear_debug + ) + clear_debug_btn.grid(row=1, column=0, sticky="e", pady=(5, 0)) + + def _load_app_icon(self): + """Load application icon""" + try: + icon_path = LXToolsAppConfig.get_icon_path("app_icon") + if icon_path and os.path.exists(icon_path): + icon = tk.PhotoImage(file=icon_path) + self.root.iconphoto(False, icon) + print(f"App icon loaded: {icon_path}") + else: + print("App icon not found, using default") + except Exception as e: + print(f"Failed to load app icon: {e}") + + def _reset_download_icon(self): + """Reset download icon to neutral state""" + icon = self.image_manager.load_image("download_icon") + if icon: + self.download_icon_label.config(image=icon, text="") + self.download_icon_label.image = icon + else: + # Fallback to emoji + self.download_icon_label.config(text="📥", font=("Helvetica", 16)) + + def _on_project_selected(self, event=None): + """Handle project selection change""" + selected = self.project_var.get() + if selected: + self.update_progress(f"Selected: {selected}") + + def update_download_icon(self, status): + """Update download icon based on status""" + if not self.download_icon_label: + return + + if status == "downloading": + icon = self.image_manager.load_image("download_icon") + if icon: + self.download_icon_label.config(image=icon, text="") + self.download_icon_label.image = icon + else: + self.download_icon_label.config(text="⬇️", font=("Helvetica", 16)) + + elif status == "error": + icon = self.image_manager.load_image("download_error_icon") + if icon: + self.download_icon_label.config(image=icon, text="") + self.download_icon_label.image = icon + else: + self.download_icon_label.config(text="❌", font=("Helvetica", 16)) + + elif status == "success": + icon = self.image_manager.load_image("success_icon") + if icon: + self.download_icon_label.config(image=icon, text="") + self.download_icon_label.image = icon + else: + self.download_icon_label.config(text="✅", font=("Helvetica", 16)) + + self.download_icon_label.update() + + def update_progress(self, message): + """Update progress message""" + if self.progress_label: + self.progress_label.config(text=message) + self.progress_label.update() + print(f"Progress: {message}") + + def debug_log(self, message): + """Add message to debug log""" + if self.debug_text: + self.debug_text.insert(tk.END, f"{message}\n") + self.debug_text.see(tk.END) + self.debug_text.update() + print(f"Debug: {message}") + + def clear_debug(self): + """Clear debug text""" + if self.debug_text: + self.debug_text.delete(1.0, tk.END) + + def toggle_debug(self): + """Toggle debug window visibility""" + self.show_debug = not self.show_debug + + # Recreate window with new size + if self.root: + self.root.destroy() + + # Create new window + self.create_gui() + self.root.mainloop() + + def refresh_status(self): + """Refresh application status and version information""" + self.update_progress("Refreshing status and checking versions...") + self._reset_download_icon() + + for project_key, project_info in self.app_manager.get_all_projects().items(): + status_label = self.status_labels[project_key] + version_label = self.version_labels[project_key] + + if self.app_manager.is_installed(project_key): + installed_version = self.app_manager.get_installed_version(project_key) + status_label.config( + text=f"✅ Installed (v{installed_version})", fg="green" + ) + + # Get latest version from API + try: + latest_version = self.app_manager.get_latest_version(project_key) + if latest_version != "Unknown": + if installed_version != latest_version: + version_label.config( + text=f"Latest: v{latest_version} (Update available)", + fg="orange", + ) + else: + version_label.config( + text=f"Latest: v{latest_version} (Up to date)", + fg="green", + ) + else: + version_label.config(text=f"Latest: Unknown", fg="gray") + except Exception as e: + version_label.config(text=f"Latest: Check failed", fg="gray") + self.debug_log(f"Version check failed for {project_key}: {e}") + else: + status_label.config(text=f"❌ Not installed", fg="red") + + # Still show latest available version + try: + latest_version = self.app_manager.get_latest_version(project_key) + if latest_version != "Unknown": + version_label.config( + text=f"Available: v{latest_version}", fg="blue" + ) + else: + version_label.config(text=f"Available: Unknown", fg="gray") + except Exception as e: + version_label.config(text=f"Available: Check failed", fg="gray") + self.debug_log(f"Version check failed for {project_key}: {e}") + + self.update_progress("Status refresh completed.") + + def install_project(self): + """Handle install button click""" + selected_project = self.project_var.get() + if not selected_project: + messagebox.showwarning("Warning", "Please select a project to install.") + return + + # Check internet connection + if not NetworkChecker.check_internet(): + self.update_download_icon("error") + messagebox.showerror( + "Network Error", + "No internet connection available.\nPlease check your network connection.", + ) + return + + if not NetworkChecker.check_repository(): + self.update_download_icon("error") + messagebox.showerror( + "Repository Error", "Cannot access repository.\nPlease try again later." + ) + return + + # Reset download icon + self._reset_download_icon() + project_info = self.app_manager.get_project_info(selected_project) + + # Check if already installed + if self.app_manager.is_installed(selected_project): + installed_version = self.app_manager.get_installed_version(selected_project) + latest_version = self.app_manager.get_latest_version(selected_project) + + dialog_text = ( + f"{project_info['name']} is already installed.\n\n" + f"Installed version: v{installed_version}\n" + f"Latest version: v{latest_version}\n\n" + f"YES = Update (reinstall all files)\n" + f"NO = Uninstall\n" + f"Cancel = Do nothing" + ) + + result = messagebox.askyesnocancel( + f"{project_info['name']} already installed", dialog_text + ) + + if result is None: # Cancel + self.update_progress("Installation cancelled.") + return + elif not result: # Uninstall + self.uninstall_project(selected_project) + return + else: # Update + self.update_progress("Updating application...") + + try: + self.installation_manager.install_project(selected_project) + + # Success message + if USE_LXTOOLS: + LxTools.msg_window( + self.root, + "Success", + f"{project_info['name']} has been successfully installed/updated.", + "info", + ) + else: + messagebox.showinfo( + "Success", + f"{project_info['name']} has been successfully installed/updated.", + ) + + self.refresh_status() + + except Exception as e: + # Show error icon + self.update_download_icon("error") + + error_msg = f"Installation failed: {e}" + self.debug_log(f"ERROR: {error_msg}") + + if USE_LXTOOLS: + LxTools.msg_window( + self.root, "Error", error_msg, "error", width=500, height=300 + ) + else: + messagebox.showerror("Error", error_msg) + + def uninstall_project(self, project_key=None): + """Handle uninstall button click""" + if project_key is None: + project_key = self.project_var.get() + + if not project_key: + messagebox.showwarning("Warning", "Please select a project to uninstall.") + return + + project_info = self.app_manager.get_project_info(project_key) + + if not self.app_manager.is_installed(project_key): + messagebox.showinfo("Info", f"{project_info['name']} is not installed.") + return + + result = messagebox.askyesno( + "Confirm Uninstall", + f"Are you sure you want to uninstall {project_info['name']}?\n\n" + f"This will remove all application files and user configurations.", + ) + if not result: + return + + try: + self.uninstallation_manager.uninstall_project(project_key) + + # Success message + if USE_LXTOOLS: + LxTools.msg_window( + self.root, + "Success", + f"{project_info['name']} has been successfully uninstalled.", + "info", + ) + else: + messagebox.showinfo( + "Success", + f"{project_info['name']} has been successfully uninstalled.", + ) + + self.refresh_status() + + except Exception as e: + error_msg = f"Uninstallation failed: {e}" + self.debug_log(f"ERROR: {error_msg}") + + if USE_LXTOOLS: + LxTools.msg_window( + self.root, "Error", error_msg, "error", width=500, height=300 + ) + else: + messagebox.showerror("Error", error_msg) + + def run(self): + """Start the GUI application""" + try: + print(f"Starting {LXToolsAppConfig.APP_NAME} v{LXToolsAppConfig.VERSION}") + print(f"Working directory: {LXToolsAppConfig.WORK_DIR}") + print(f"Icons directory: {LXToolsAppConfig.ICONS_DIR}") + print(f"Using LxTools: {USE_LXTOOLS}") + + root = self.create_gui() + root.mainloop() + + except KeyboardInterrupt: + print("\nApplication interrupted by user.") + except Exception as e: + print(f"Fatal error: {e}") + if self.root: + messagebox.showerror("Fatal Error", f"Application failed to start: {e}") + + +# ---------------------------- +# Main Application Entry Point +# ---------------------------- +def main(): + """Main function to start the application""" + try: + # Check if running as root (not recommended) + if os.geteuid() == 0: + print("Warning: Running as root is not recommended!") + print("The installer will use pkexec for privilege escalation when needed.") + + # Create and run the GUI + app = LXToolsGUI() + app.run() + + except KeyboardInterrupt: + print("\nApplication interrupted by user.") + except Exception as e: + print(f"Fatal error: {e}") + try: + import tkinter.messagebox as mb + + mb.showerror("Fatal Error", f"Application failed to start: {e}") + except: + pass # If even tkinter fails, just print + + +if __name__ == "__main__": + main() diff --git a/lxtools_installerv3.py b/lxtools_installerv3.py new file mode 100755 index 0000000..e69c346 --- /dev/null +++ b/lxtools_installerv3.py @@ -0,0 +1,1719 @@ +#!/usr/bin/python3 +################### Teil 1 - Imports und Basis-Konfiguration ################### +import gettext +import locale +import tkinter as tk +from tkinter import messagebox, ttk +import shutil +import os +import socket +import subprocess +import tempfile +import urllib.request +import zipfile +import json +import stat +from pathlib import Path +from datetime import datetime + + +################### Teil 2 - LXTools App Configuration ################### +class LXToolsAppConfig: + VERSION = "1.0.9" + APP_NAME = "LX Tools Installer" + WINDOW_WIDTH = 600 + WINDOW_HEIGHT = 700 + + # Working directory + WORK_DIR = os.getcwd() + ICONS_DIR = os.path.join(WORK_DIR, "lx-icons") + THEMES_DIR = os.path.join(WORK_DIR, "TK-Themes") + + # Locale settings + LOCALE_DIR = Path("/usr/share/locale/") + + # Download URLs + WIREPY_URL = "https://git.ilunix.de/punix/Wire-Py/archive/main.zip" + SHARED_LIBS_URL = "https://git.ilunix.de/punix/shared_libs/archive/main.zip" + + # API URLs for version checking + WIREPY_API_URL = "https://git.ilunix.de/api/v1/repos/punix/Wire-Py/releases" + SHARED_LIBS_API_URL = ( + "https://git.ilunix.de/api/v1/repos/punix/shared_libs/releases" + ) + + # Project configurations + PROJECTS = { + "wirepy": { + "name": "Wire-Py", + "description": "WireGuard VPN Manager with GUI", + "download_url": WIREPY_URL, + "api_url": WIREPY_API_URL, + "icon_key": "icon_vpn", + "main_executable": "wirepy.py", + "symlink_name": "wirepy", + "config_file": "wp_app_config.py", + "desktop_file": "Wire-Py.desktop", + "policy_file": "org.sslcrypt.policy", + "requires_ssl": True, + }, + "logviewer": { + "name": "LogViewer", + "description": "System Log Viewer with GUI", + "download_url": SHARED_LIBS_URL, + "api_url": SHARED_LIBS_API_URL, + "icon_key": "icon_log", + "main_executable": "logviewer.py", + "symlink_name": "logviewer", + "config_file": "logview_app_config.py", + "desktop_file": "LogViewer.desktop", + "policy_file": None, + "requires_ssl": False, + }, + } + + # OS Detection List (order matters - specific first, generic last) + OS_DETECTION = [ + ("mint", "Linux Mint"), + ("pop", "Pop!_OS"), + ("manjaro", "Manjaro"), + ("garuda", "Garuda Linux"), + ("endeavouros", "EndeavourOS"), + ("fedora", "Fedora"), + ("tumbleweed", "SUSE Tumbleweed"), + ("leap", "SUSE Leap"), + ("suse", "openSUSE"), + ("arch", "Arch Linux"), + ("ubuntu", "Ubuntu"), + ("debian", "Debian"), + ] + + # Package manager commands for TKinter installation + TKINTER_INSTALL_COMMANDS = { + "Ubuntu": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], + "Debian": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], + "Linux Mint": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], + "Pop!_OS": ["apt", "update", "&&", "apt", "install", "-y", "python3-tk"], + "Fedora": ["dnf", "install", "-y", "tkinter"], + "Arch Linux": ["pacman", "-S", "--noconfirm", "tk"], + "Manjaro": ["pacman", "-S", "--noconfirm", "tk"], + "Garuda Linux": ["pacman", "-S", "--noconfirm", "tk"], + "EndeavourOS": ["pacman", "-S", "--noconfirm", "tk"], + "openSUSE": ["zypper", "install", "-y", "python3-tk"], + "SUSE Tumbleweed": ["zypper", "install", "-y", "python3-tk"], + "SUSE Leap": ["zypper", "install", "-y", "python3-tk"], + } + + @staticmethod + def setup_translations(): + """Initialize translations and set the translation function""" + try: + locale.bindtextdomain( + LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR + ) + gettext.bindtextdomain( + LXToolsAppConfig.APP_NAME, LXToolsAppConfig.LOCALE_DIR + ) + gettext.textdomain(LXToolsAppConfig.APP_NAME) + except: + pass + return gettext.gettext + + +# Initialize translations +_ = LXToolsAppConfig.setup_translations() + + +################### Teil 3 - OS Detection und System Manager ################### +class OSDetector: + @staticmethod + def detect_os(): + """Detect operating system using ordered list""" + try: + with open("/etc/os-release", "r") as f: + content = f.read().lower() + + # Check each OS in order (specific first) + for keyword, os_name in LXToolsAppConfig.OS_DETECTION: + if keyword in content: + return os_name + + return "Unknown System" + except FileNotFoundError: + return "File not found" + + @staticmethod + def check_tkinter_available(): + """Check if tkinter is available""" + try: + import tkinter + + return True + except ImportError: + return False + + @staticmethod + def install_tkinter(): + """Install tkinter based on detected OS""" + detected_os = OSDetector.detect_os() + + if detected_os in LXToolsAppConfig.TKINTER_INSTALL_COMMANDS: + commands = LXToolsAppConfig.TKINTER_INSTALL_COMMANDS[detected_os] + + print(f"Installing tkinter for {detected_os}...") + print(f"Command: {' '.join(commands)}") + + try: + # Use pkexec for privilege escalation + full_command = ["pkexec", "bash", "-c", " ".join(commands)] + result = subprocess.run( + full_command, capture_output=True, text=True, timeout=300 + ) + + if result.returncode == 0: + print("TKinter installation completed successfully!") + return True + else: + print(f"TKinter installation failed: {result.stderr}") + return False + + except subprocess.TimeoutExpired: + print("TKinter installation timed out") + return False + except Exception as e: + print(f"Error installing tkinter: {e}") + return False + else: + print(f"No tkinter installation command defined for {detected_os}") + return False + + +class SystemManager: + @staticmethod + def create_directories(directories): + """Create system directories using pkexec""" + for directory in directories: + subprocess.run(["pkexec", "mkdir", "-p", directory], check=True) + + @staticmethod + def copy_file(src, dest, make_executable=False): + """Copy file using pkexec""" + subprocess.run(["pkexec", "cp", src, dest], check=True) + if make_executable: + subprocess.run(["pkexec", "chmod", "755", dest], check=True) + + @staticmethod + def copy_directory(src, dest): + """Copy directory using pkexec""" + subprocess.run(["pkexec", "cp", "-r", src, dest], check=True) + + @staticmethod + def remove_file(path): + """Remove file using pkexec""" + subprocess.run(["pkexec", "rm", "-f", path], check=False) + + @staticmethod + def remove_directory(path): + """Remove directory using pkexec""" + subprocess.run(["pkexec", "rm", "-rf", path], check=False) + + @staticmethod + def create_symlink(target, link_name): + """Create symbolic link using pkexec""" + subprocess.run(["pkexec", "rm", "-f", link_name], check=False) + subprocess.run(["pkexec", "ln", "-sf", target, link_name], check=True) + + @staticmethod + def create_ssl_key(pem_file): + """Create SSL key using pkexec""" + try: + subprocess.run( + ["pkexec", "openssl", "genrsa", "-out", pem_file, "4096"], check=True + ) + subprocess.run(["pkexec", "chmod", "600", pem_file], check=True) + return True + except subprocess.CalledProcessError: + return False + + +################### Teil 4 - Image Manager und Gitea API ################### +class ImageManager: + def __init__(self): + self.images = {} + + def load_image(self, image_key, fallback_paths=None): + """Load PNG image using tk.PhotoImage with fallback options""" + if image_key in self.images: + return self.images[image_key] + + # Define image paths based on key + image_paths = { + "app_icon": [ + "./lx-icons/48/wg_vpn.png", + "/usr/share/icons/lx-icons/48/wg_vpn.png", + ], + "download_icon": [ + "./lx-icons/32/download.png", + "/usr/share/icons/lx-icons/32/download.png", + ], + "download_error_icon": [ + "./lx-icons/32/download_error.png", + "/usr/share/icons/lx-icons/32/download_error.png", + ], + "success_icon": [ + "./lx-icons/32/download.png", + "/usr/share/icons/lx-icons/32/download.png", + ], + "icon_vpn": [ + "./lx-icons/48/wg_vpn.png", + "/usr/share/icons/lx-icons/48/wg_vpn.png", + ], + "icon_log": [ + "./lx-icons/48/log.png", + "/usr/share/icons/lx-icons/48/log.png", + ], + } + + # Get paths to try + paths_to_try = image_paths.get(image_key, []) + + # Add fallback paths if provided + if fallback_paths: + paths_to_try.extend(fallback_paths) + + # Try to load image from paths + for path in paths_to_try: + try: + if os.path.exists(path): + photo = tk.PhotoImage(file=path) + self.images[image_key] = photo + return photo + except tk.TclError as e: + print(f"Failed to load image from {path}: {e}") + continue + + # Return None if no image found + return None + + +class GiteaUpdate: + @staticmethod + def api_down(url, current_version=""): + """Get latest version from Gitea API""" + try: + with urllib.request.urlopen(url, timeout=10) as response: + data = json.loads(response.read().decode()) + if data and len(data) > 0: + latest_version = data[0].get("tag_name", "Unknown") + return latest_version.lstrip("v") # Remove 'v' prefix if present + return "Unknown" + except Exception as e: + print(f"API Error: {e}") + return "Unknown" + + +class NetworkChecker: + @staticmethod + def check_internet(host="8.8.8.8", port=53, timeout=3): + """Check if internet connection is available""" + try: + socket.setdefaulttimeout(timeout) + socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) + return True + except socket.error: + return False + + @staticmethod + def check_repository(url="https://git.ilunix.de", timeout=5): + """Check if repository is accessible""" + try: + urllib.request.urlopen(url, timeout=timeout) + return True + except: + return False + + +################### Teil 5 - Download Manager ################### +class DownloadManager: + @staticmethod + def download_and_extract(url, extract_to, progress_callback=None): + """Download and extract ZIP file""" + try: + if progress_callback: + progress_callback(f"Downloading from {url}...") + + with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file: + urllib.request.urlretrieve(url, tmp_file.name) + + if progress_callback: + progress_callback("Extracting files...") + + with zipfile.ZipFile(tmp_file.name, "r") as zip_ref: + zip_ref.extractall(extract_to) + + os.unlink(tmp_file.name) + return True + + except Exception as e: + if progress_callback: + progress_callback(f"Download failed: {str(e)}") + return False + + +################### Teil 6 - App Manager ################### +class AppManager: + def __init__(self): + self.projects = LXToolsAppConfig.PROJECTS + + def get_project_info(self, project_key): + """Get project information by key""" + return self.projects.get(project_key) + + def get_all_projects(self): + """Get all project configurations""" + return self.projects + + def is_installed(self, project_key): + """Check if project is installed with better detection""" + if project_key == "wirepy": + # Check for wirepy symlink + return os.path.exists("/usr/local/bin/wirepy") and os.path.islink( + "/usr/local/bin/wirepy" + ) + + elif project_key == "logviewer": + # Check for logviewer symlink AND executable file + symlink_exists = os.path.exists("/usr/local/bin/logviewer") + executable_exists = os.path.exists( + "/usr/lib/python3/dist-packages/shared_libs/logviewer.py" + ) + executable_is_executable = False + + if executable_exists: + try: + # Check if file is executable + file_stat = os.stat( + "/usr/lib/python3/dist-packages/shared_libs/logviewer.py" + ) + executable_is_executable = bool(file_stat.st_mode & stat.S_IEXEC) + except: + executable_is_executable = False + + # LogViewer is installed if symlink exists AND executable file exists AND is executable + is_installed = ( + symlink_exists and executable_exists and executable_is_executable + ) + + # Debug logging + print(f"LogViewer installation check:") + print(f" Symlink exists: {symlink_exists}") + print(f" Executable exists: {executable_exists}") + print(f" Is executable: {executable_is_executable}") + print(f" Final result: {is_installed}") + + return is_installed + + return False + + def get_installed_version(self, project_key): + """Get installed version from config file""" + try: + if project_key == "wirepy": + config_file = ( + "/usr/lib/python3/dist-packages/shared_libs/wp_app_config.py" + ) + elif project_key == "logviewer": + config_file = ( + "/usr/lib/python3/dist-packages/shared_libs/logview_app_config.py" + ) + else: + return "Unknown" + + if os.path.exists(config_file): + with open(config_file, "r") as f: + content = f.read() + for line in content.split("\n"): + if "VERSION" in line and "=" in line: + version = line.split("=")[1].strip().strip("\"'") + return version + return "Unknown" + except Exception as e: + print(f"Error getting version for {project_key}: {e}") + return "Unknown" + + def get_latest_version(self, project_key): + """Get latest version from API""" + project_info = self.get_project_info(project_key) + if not project_info: + return "Unknown" + + return GiteaUpdate.api_down(project_info["api_url"]) + + def check_other_apps_installed(self, exclude_key): + """Check if other apps are still installed""" + return any( + self.is_installed(key) for key in self.projects.keys() if key != exclude_key + ) + + +################### Teil 7 - Installation Manager ################### +class InstallationManager: + def __init__( + self, app_manager, progress_callback=None, icon_callback=None, log_callback=None + ): + self.app_manager = app_manager + self.progress_callback = progress_callback + self.icon_callback = icon_callback + self.log_callback = log_callback + self.system_manager = SystemManager() + self.download_manager = DownloadManager() + + def install_project(self, project_key): + """Install or update project""" + project_info = self.app_manager.get_project_info(project_key) + if not project_info: + raise Exception(f"Unknown project: {project_key}") + + self.update_progress(f"Starting installation of {project_info['name']}...") + self.log(f"=== Installing {project_info['name']} ===") + + try: + # Create installation script + script_content = self._create_install_script(project_key) + + # Execute installation + self._execute_install_script(script_content) + + self.update_progress( + f"{project_info['name']} installation completed successfully!" + ) + self.log(f"=== {project_info['name']} installed successfully ===") + + # Set success icon + self.update_icon("success") + + except Exception as e: + self.log(f"ERROR: Installation failed: {e}") + self.update_icon("error") + raise Exception(f"Installation failed: {e}") + + def _create_install_script(self, project_key): + """Create installation script based on project""" + if project_key == "wirepy": + return self._create_wirepy_install_script() + elif project_key == "logviewer": + return self._create_logviewer_install_script() + else: + raise Exception(f"Unknown project: {project_key}") + + def _create_wirepy_install_script(self): + """Create Wire-Py installation script""" + script = f"""#!/bin/bash +set -e + +echo "=== Wire-Py Installation ===" + +# Create necessary directories +mkdir -p /usr/lib/python3/dist-packages/shared_libs +mkdir -p /usr/share/icons/lx-icons +mkdir -p /usr/share/locale/de/LC_MESSAGES +mkdir -p /usr/share/applications +mkdir -p /usr/local/etc/ssl +mkdir -p /usr/share/polkit-1/actions +mkdir -p /usr/share/TK-Themes + +# Download and extract Wire-Py +cd /tmp +rm -rf wirepy_install +mkdir wirepy_install +cd wirepy_install + +echo "Downloading Wire-Py..." +wget -q "{LXToolsAppConfig.WIREPY_URL}" -O wirepy.zip +unzip -q wirepy.zip +WIREPY_DIR=$(find . -name "Wire-Py" -type d | head -1) + +echo "Downloading shared libraries..." +wget -q "{LXToolsAppConfig.SHARED_LIBS_URL}" -O shared.zip +unzip -q shared.zip +SHARED_DIR=$(find . -name "shared_libs" -type d | head -1) + +# Install Wire-Py files +echo "Installing Wire-Py executables..." +for file in wirepy.py start_wg.py ssl_encrypt.py ssl_decrypt.py match_found.py tunnel.py; do + if [ -f "$WIREPY_DIR/$file" ]; then + cp "$WIREPY_DIR/$file" /usr/local/bin/ + chmod 755 /usr/local/bin/$file + echo "Installed $file" + fi +done + +# Install config +if [ -f "$WIREPY_DIR/wp_app_config.py" ]; then + cp "$WIREPY_DIR/wp_app_config.py" /usr/lib/python3/dist-packages/shared_libs/ + echo "Installed wp_app_config.py" +fi + +# Install shared libraries +echo "Installing shared libraries..." +for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py logviewer.py; do + if [ -f "$SHARED_DIR/$file" ]; then + cp "$SHARED_DIR/$file" /usr/lib/python3/dist-packages/shared_libs/ + echo "Installed shared lib: $file" + fi +done + +# Install icons +if [ -d "$WIREPY_DIR/lx-icons" ]; then + echo "Installing icons..." + cp -r "$WIREPY_DIR/lx-icons"/* /usr/share/icons/lx-icons/ +fi + +# Install TK-Themes +if [ -d "$WIREPY_DIR/TK-Themes" ]; then + echo "Installing TK-Themes..." + cp -r "$WIREPY_DIR/TK-Themes"/* /usr/share/TK-Themes/ +fi + +# Install desktop file +if [ -f "$WIREPY_DIR/Wire-Py.desktop" ]; then + cp "$WIREPY_DIR/Wire-Py.desktop" /usr/share/applications/ + echo "Installed desktop file" +fi + +# Install language files +if [ -d "$WIREPY_DIR/languages/de" ]; then + echo "Installing language files..." + cp "$WIREPY_DIR/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true +fi + +# Install policy file +if [ -f "$WIREPY_DIR/org.sslcrypt.policy" ]; then + cp "$WIREPY_DIR/org.sslcrypt.policy" /usr/share/polkit-1/actions/ + echo "Installed policy file" +fi + +# Create symlink +ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy +echo "Created symlink" + +# Create SSL key if not exists +if [ ! -f /usr/local/etc/ssl/pwgk.pem ]; then + echo "Creating SSL key..." + openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096 + chmod 600 /usr/local/etc/ssl/pwgk.pem +fi + +# Cleanup +cd /tmp +rm -rf wirepy_install + +echo "Wire-Py installation completed!" +""" + return script + + ################### Teil 8 - Installation Manager LogViewer Script ################### + def _create_logviewer_install_script(self): + """Create LogViewer installation script""" + script = f"""#!/bin/bash +set -e + +echo "=== LogViewer Installation ===" + +# Create necessary directories +mkdir -p /usr/lib/python3/dist-packages/shared_libs +mkdir -p /usr/share/icons/lx-icons +mkdir -p /usr/share/locale/de/LC_MESSAGES +mkdir -p /usr/share/applications +mkdir -p /usr/share/TK-Themes + +# Download and extract shared libraries (contains LogViewer) +cd /tmp +rm -rf logviewer_install +mkdir logviewer_install +cd logviewer_install + +echo "Downloading LogViewer and shared libraries..." +wget -q "{LXToolsAppConfig.SHARED_LIBS_URL}" -O shared.zip +unzip -q shared.zip +SHARED_DIR=$(find . -name "shared_libs" -type d | head -1) + +# Check if TK-Themes exists, if not download Wire-Py for themes +if [ ! -d "/usr/share/TK-Themes" ] || [ -z "$(ls -A /usr/share/TK-Themes 2>/dev/null)" ]; then + echo "TK-Themes not found, downloading from Wire-Py..." + wget -q "{LXToolsAppConfig.WIREPY_URL}" -O wirepy.zip + unzip -q wirepy.zip + WIREPY_DIR=$(find . -name "Wire-Py" -type d | head -1) + + if [ -d "$WIREPY_DIR/TK-Themes" ]; then + echo "Installing TK-Themes..." + cp -r "$WIREPY_DIR/TK-Themes"/* /usr/share/TK-Themes/ + fi + + # Also install icons from Wire-Py if not present + if [ -d "$WIREPY_DIR/lx-icons" ] && [ ! -d "/usr/share/icons/lx-icons" ]; then + echo "Installing icons from Wire-Py..." + cp -r "$WIREPY_DIR/lx-icons"/* /usr/share/icons/lx-icons/ + fi +fi + +# Install shared libraries +echo "Installing shared libraries..." +for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do + if [ -f "$SHARED_DIR/$file" ]; then + cp "$SHARED_DIR/$file" /usr/lib/python3/dist-packages/shared_libs/ + echo "Installed shared lib: $file" + fi +done + +# Install LogViewer executable +if [ -f "$SHARED_DIR/logviewer.py" ]; then + cp "$SHARED_DIR/logviewer.py" /usr/lib/python3/dist-packages/shared_libs/ + chmod 755 /usr/lib/python3/dist-packages/shared_libs/logviewer.py + echo "Installed logviewer.py (executable)" +fi + +# Create LogViewer desktop file +cat > /usr/share/applications/LogViewer.desktop << 'EOF' +[Desktop Entry] +Version=1.0 +Type=Application +Name=LogViewer +Comment=System Log Viewer +Exec=/usr/local/bin/logviewer +Icon=/usr/share/icons/lx-icons/48/log.png +Terminal=false +Categories=System;Utility; +StartupNotify=true +EOF + +echo "Created LogViewer desktop file" + +# Create symlink for LogViewer +ln -sf /usr/lib/python3/dist-packages/shared_libs/logviewer.py /usr/local/bin/logviewer +echo "Created LogViewer symlink" + +# Install language files if available +if [ -d "$SHARED_DIR/languages/de" ]; then + echo "Installing language files..." + cp "$SHARED_DIR/languages/de"/*.mo /usr/share/locale/de/LC_MESSAGES/ 2>/dev/null || true +fi + +# Cleanup +cd /tmp +rm -rf logviewer_install + +echo "LogViewer installation completed!" +""" + return script + + def _execute_install_script(self, script_content): + """Execute installation script with pkexec""" + try: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".sh", delete=False + ) as script_file: + script_file.write(script_content) + script_file.flush() + + # Make script executable + os.chmod(script_file.name, 0o755) + self.log(f"Created install script: {script_file.name}") + + # Execute with pkexec + result = subprocess.run( + ["pkexec", "bash", script_file.name], + capture_output=True, + text=True, + timeout=300, # 5 minutes timeout + ) + + # Log output + if result.stdout: + self.log(f"STDOUT: {result.stdout}") + if result.stderr: + self.log(f"STDERR: {result.stderr}") + + # Clean up + os.unlink(script_file.name) + + if result.returncode != 0: + raise Exception(f"Installation script failed: {result.stderr}") + + except subprocess.TimeoutExpired: + raise Exception("Installation timed out") + except subprocess.CalledProcessError as e: + raise Exception(f"Installation script failed: {e}") + + def update_progress(self, message): + if self.progress_callback: + self.progress_callback(message) + + def update_icon(self, status): + if self.icon_callback: + self.icon_callback(status) + + def log(self, message): + if self.log_callback: + self.log_callback(message) + + +################### Teil 9 - Uninstallation Manager ################### +class UninstallationManager: + def __init__(self, app_manager, progress_callback=None, log_callback=None): + self.app_manager = app_manager + self.progress_callback = progress_callback + self.log_callback = log_callback + + def uninstall_project(self, project_key): + """Uninstall project""" + project_info = self.app_manager.get_project_info(project_key) + if not project_info: + raise Exception(f"Unknown project: {project_key}") + + if not self.app_manager.is_installed(project_key): + raise Exception(f"{project_info['name']} is not installed.") + + self.update_progress(f"Uninstalling {project_info['name']}...") + self.log(f"=== Uninstalling {project_info['name']} ===") + + try: + # Create uninstallation script + script_content = self._create_uninstall_script(project_key) + + # Execute uninstallation + self._execute_uninstall_script(script_content) + + self.update_progress(f"{project_info['name']} uninstalled successfully!") + self.log(f"=== {project_info['name']} uninstalled successfully ===") + + except Exception as e: + self.log(f"ERROR: Uninstallation failed: {e}") + raise Exception(f"Uninstallation failed: {e}") + + def _create_uninstall_script(self, project_key): + """Create uninstallation script based on project""" + if project_key == "wirepy": + return self._create_wirepy_uninstall_script() + elif project_key == "logviewer": + return self._create_logviewer_uninstall_script() + else: + raise Exception(f"Unknown project: {project_key}") + + def _create_wirepy_uninstall_script(self): + """Create Wire-Py uninstallation script""" + script = """#!/bin/bash +set -e + +echo "=== Wire-Py Uninstallation ===" + +# Remove Wire-Py executables +echo "Removing Wire-Py executables..." +for file in wirepy.py start_wg.py ssl_encrypt.py ssl_decrypt.py match_found.py tunnel.py; do + rm -f /usr/local/bin/$file + echo "Removed $file" +done + +# Remove symlink +rm -f /usr/local/bin/wirepy +echo "Removed wirepy symlink" + +# Remove config +rm -f /usr/lib/python3/dist-packages/shared_libs/wp_app_config.py +echo "Removed wp_app_config.py" + +# Remove desktop file +rm -f /usr/share/applications/Wire-Py.desktop +echo "Removed desktop file" + +# Remove policy file +rm -f /usr/share/polkit-1/actions/org.sslcrypt.policy +echo "Removed policy file" + +# Remove language files +rm -f /usr/share/locale/de/LC_MESSAGES/wirepy.mo +echo "Removed language files" + +# Remove user config directory +if [ -d "$HOME/.config/wire_py" ]; then + rm -rf "$HOME/.config/wire_py" + echo "Removed user config directory" +fi + +# Remove log file +rm -f "$HOME/.local/share/lxlogs/wirepy.log" +echo "Removed log file" + +# Check if LogViewer is still installed before removing shared resources +if [ ! -f /usr/local/bin/logviewer ] || [ ! -L /usr/local/bin/logviewer ]; then + echo "No other LX apps found, removing shared resources..." + rm -rf /usr/share/icons/lx-icons + rm -rf /usr/share/TK-Themes + rm -rf /usr/local/etc/ssl + + # Remove shared libs + for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py logviewer.py; do + rm -f /usr/lib/python3/dist-packages/shared_libs/$file + done + + # Try to remove shared_libs directory if empty + rmdir /usr/lib/python3/dist-packages/shared_libs 2>/dev/null || true +else + echo "LogViewer still installed, keeping shared resources" +fi + +echo "Wire-Py uninstallation completed!" +""" + return script + + def _create_logviewer_uninstall_script(self): + """Create LogViewer uninstallation script""" + script = """#!/bin/bash +set -e + +echo "=== LogViewer Uninstallation ===" + +# Remove LogViewer symlink +rm -f /usr/local/bin/logviewer +echo "Removed logviewer symlink" + +# Remove desktop file +rm -f /usr/share/applications/LogViewer.desktop +echo "Removed desktop file" + +# Remove language files +rm -f /usr/share/locale/de/LC_MESSAGES/logviewer.mo +echo "Removed language files" + +# Remove user config directory +if [ -d "$HOME/.config/logviewer" ]; then + rm -rf "$HOME/.config/logviewer" + echo "Removed user config directory" +fi + +# Remove log file +rm -f "$HOME/.local/share/lxlogs/logviewer.log" +echo "Removed log file" + +# Check if Wire-Py is still installed before removing shared resources +if [ ! -f /usr/local/bin/wirepy ] || [ ! -L /usr/local/bin/wirepy ]; then + echo "No other LX apps found, removing shared resources..." + rm -rf /usr/share/icons/lx-icons + rm -rf /usr/share/TK-Themes + + # Remove shared libs (but keep logviewer.py if we're only uninstalling logviewer) + for file in common_tools.py file_and_dir_ensure.py gitea.py __init__.py logview_app_config.py; do + rm -f /usr/lib/python3/dist-packages/shared_libs/$file + done + + # Remove logviewer.py last + rm -f /usr/lib/python3/dist-packages/shared_libs/logviewer.py + + # Try to remove shared_libs directory if empty + rmdir /usr/lib/python3/dist-packages/shared_libs 2>/dev/null || true +else + echo "Wire-Py still installed, keeping shared resources" + # Only remove logviewer-specific files + rm -f /usr/lib/python3/dist-packages/shared_libs/logview_app_config.py + rm -f /usr/lib/python3/dist-packages/shared_libs/logviewer.py +fi + +echo "LogViewer uninstallation completed!" +""" + return script + + ################### Teil 10 - Uninstallation Manager Helper Methods ################### + def _execute_uninstall_script(self, script_content): + """Execute uninstallation script with pkexec""" + try: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".sh", delete=False + ) as script_file: + script_file.write(script_content) + script_file.flush() + + # Make script executable + os.chmod(script_file.name, 0o755) + self.log(f"Created uninstall script: {script_file.name}") + + # Execute with pkexec + result = subprocess.run( + ["pkexec", "bash", script_file.name], + capture_output=True, + text=True, + timeout=120, + ) + + # Log output + if result.stdout: + self.log(f"STDOUT: {result.stdout}") + if result.stderr: + self.log(f"STDERR: {result.stderr}") + + # Clean up + os.unlink(script_file.name) + + if result.returncode != 0: + raise Exception(f"Uninstallation script failed: {result.stderr}") + + except subprocess.TimeoutExpired: + raise Exception("Uninstallation timed out") + except subprocess.CalledProcessError as e: + raise Exception(f"Uninstallation script failed: {e}") + + def update_progress(self, message): + if self.progress_callback: + self.progress_callback(message) + + def log(self, message): + if self.log_callback: + self.log_callback(message) + + +################### Teil 11 - LXToolsGUI Klasse (Hauptklasse) ################### +class LXToolsGUI: + def __init__(self): + self.root = None + self.notebook = None + self.progress_label = None + self.download_icon_label = None + self.log_text = None + self.selected_project = None + self.project_frames = {} + self.status_labels = {} + self.version_labels = {} + + # Managers + self.app_manager = AppManager() + self.installation_manager = InstallationManager( + self.app_manager, + self.update_progress, + self.update_download_icon, + self.log_message, + ) + self.uninstallation_manager = UninstallationManager( + self.app_manager, self.update_progress, self.log_message + ) + self.image_manager = ImageManager() + + # Detect OS + self.detected_os = OSDetector.detect_os() + + # Color scheme + self.colors = { + "bg": "#f8f9fa", + "card_bg": "#ffffff", + "hover_bg": "#e3f2fd", + "selected_bg": "#bbdefb", + "progress_bg": "#f8f9fa", + "text": "#2c3e50", + "accent": "#3498db", + } + + def create_gui(self): + """Create the main GUI""" + self.root = tk.Tk() + self.root.title(f"{LXToolsAppConfig.APP_NAME} v{LXToolsAppConfig.VERSION}") + self.root.geometry( + f"{LXToolsAppConfig.WINDOW_WIDTH}x{LXToolsAppConfig.WINDOW_HEIGHT}" + ) + self.root.configure(bg=self.colors["bg"]) + + # Try to set icon + try: + icon = self.image_manager.load_image("app_icon") + if icon: + self.root.iconphoto(False, icon) + except: + pass + + # Create header + self._create_header() + + # Create notebook (tabs) + self.notebook = ttk.Notebook(self.root) + self.notebook.pack(fill="both", expand=True, padx=15, pady=(0, 10)) + + # Create tabs + self._create_projects_tab() + self._create_log_tab() + + # Create progress section + self._create_progress_section() + + # Create buttons + self._create_modern_buttons() + + # Initial status refresh + self.root.after(100, self.refresh_status) + + return self.root + + def _create_header(self): + """Create header section""" + header_frame = tk.Frame(self.root, bg=self.colors["bg"]) + header_frame.pack(fill="x", padx=15, pady=15) + + # Title + title_label = tk.Label( + header_frame, + text=f"{LXToolsAppConfig.APP_NAME} v{LXToolsAppConfig.VERSION}", + font=("Helvetica", 16, "bold"), + bg=self.colors["bg"], + fg=self.colors["text"], + ) + title_label.pack() + + # OS info + os_label = tk.Label( + header_frame, + text=f"Detected OS: {self.detected_os}", + font=("Helvetica", 10), + bg=self.colors["bg"], + fg="#7f8c8d", + ) + os_label.pack() + + ################### Teil 12 - GUI Projects Tab ################### + def _create_projects_tab(self): + """Create projects tab with project cards""" + projects_frame = ttk.Frame(self.notebook) + self.notebook.add(projects_frame, text="Projects") + + # Scrollable frame + canvas = tk.Canvas(projects_frame, bg=self.colors["bg"]) + scrollbar = ttk.Scrollbar( + projects_frame, orient="vertical", command=canvas.yview + ) + scrollable_frame = tk.Frame(canvas, bg=self.colors["bg"]) + + scrollable_frame.bind( + "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) + ) + + canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + + # Create project cards + for project_key, project_info in self.app_manager.get_all_projects().items(): + self._create_project_card(scrollable_frame, project_key, project_info) + + def _create_project_card(self, parent, project_key, project_info): + """Create a project card""" + # Main card frame + card_frame = tk.Frame(parent, bg=self.colors["bg"]) + card_frame.pack(fill="x", padx=10, pady=5) + + # Content frame (the actual card) + content_frame = tk.Frame( + card_frame, bg=self.colors["card_bg"], relief="solid", bd=1 + ) + content_frame.pack(fill="x", padx=5, pady=2) + + # Store frame reference + self.project_frames[project_key] = card_frame + + # Make entire card clickable + self._make_clickable(content_frame, content_frame, project_key) + + # Header with icon and title + header_frame = tk.Frame(content_frame, bg=self.colors["card_bg"]) + header_frame.pack(fill="x", padx=15, pady=(15, 5)) + + # Icon + icon_label = tk.Label(header_frame, bg=self.colors["card_bg"]) + icon_label.pack(side="left", padx=(0, 10)) + + # Try to load project icon + icon = self.image_manager.load_image( + project_info.get("icon_key", "default_icon") + ) + if icon: + icon_label.config(image=icon) + icon_label.image = icon + else: + # Use emoji based on project + if project_key == "wirepy": + icon_label.config(text="🔒", font=("Helvetica", 24)) + elif project_key == "logviewer": + icon_label.config(text="📋", font=("Helvetica", 24)) + else: + icon_label.config(text="📦", font=("Helvetica", 24)) + + # Title and description + title_frame = tk.Frame(header_frame, bg=self.colors["card_bg"]) + title_frame.pack(side="left", fill="x", expand=True) + + title_label = tk.Label( + title_frame, + text=project_info["name"], + font=("Helvetica", 14, "bold"), + bg=self.colors["card_bg"], + fg=self.colors["text"], + anchor="w", + ) + title_label.pack(fill="x") + + desc_label = tk.Label( + title_frame, + text=project_info["description"], + font=("Helvetica", 10), + bg=self.colors["card_bg"], + fg="#7f8c8d", + anchor="w", + wraplength=300, + ) + desc_label.pack(fill="x") + + # Status section + status_frame = tk.Frame(content_frame, bg=self.colors["card_bg"]) + status_frame.pack(fill="x", padx=15, pady=(5, 15)) + + # Status label + status_label = tk.Label( + status_frame, + text="❓ Checking...", + font=("Helvetica", 10), + bg=self.colors["card_bg"], + fg="#95a5a6", + anchor="w", + ) + status_label.pack(fill="x") + + # Version label + version_label = tk.Label( + status_frame, + text="Version: Checking...", + font=("Helvetica", 9), + bg=self.colors["card_bg"], + fg="#95a5a6", + anchor="w", + ) + version_label.pack(fill="x") + + # Store label references + self.status_labels[project_key] = status_label + self.version_labels[project_key] = version_label + + # Make all elements clickable + for widget in [ + header_frame, + title_frame, + title_label, + desc_label, + status_frame, + status_label, + version_label, + ]: + self._make_clickable(widget, content_frame, project_key) + + # Make icon clickable too + self._make_clickable(icon_label, content_frame, project_key) + + ################### Teil 13 - GUI Card Interaction Methods ################### + def _make_clickable(self, widget, main_frame, project_key): + """Make widget clickable with hover effects""" + + def on_click(event): + self.select_project(project_key) + + def on_enter(event): + if self.selected_project == project_key: + main_frame.config(bg=self.colors["selected_bg"]) + self._update_frame_children_bg(main_frame, self.colors["selected_bg"]) + else: + main_frame.config(bg=self.colors["hover_bg"]) + self._update_frame_children_bg(main_frame, self.colors["hover_bg"]) + + def on_leave(event): + if self.selected_project == project_key: + main_frame.config(bg=self.colors["selected_bg"]) + self._update_frame_children_bg(main_frame, self.colors["selected_bg"]) + else: + main_frame.config(bg=self.colors["card_bg"]) + self._update_frame_children_bg(main_frame, self.colors["card_bg"]) + + widget.bind("", on_click) + widget.bind("", on_enter) + widget.bind("", on_leave) + + def _update_frame_children_bg(self, frame, bg_color): + """Recursively update background color of all children""" + try: + for child in frame.winfo_children(): + if isinstance(child, (tk.Frame, tk.Label)): + child.config(bg=bg_color) + if isinstance(child, tk.Frame): + self._update_frame_children_bg(child, bg_color) + except tk.TclError: + # Ignore color errors + pass + + def select_project(self, project_key): + """Select a project""" + # Reset previous selection + if self.selected_project and self.selected_project in self.project_frames: + old_frame = self.project_frames[self.selected_project] + old_content = old_frame.winfo_children()[0] # content_frame + old_content.config(bg=self.colors["card_bg"]) + self._update_frame_children_bg(old_content, self.colors["card_bg"]) + + # Set new selection + self.selected_project = project_key + if project_key in self.project_frames: + new_frame = self.project_frames[project_key] + new_content = new_frame.winfo_children()[0] # content_frame + new_content.config(bg=self.colors["selected_bg"]) + self._update_frame_children_bg(new_content, self.colors["selected_bg"]) + + project_info = self.app_manager.get_project_info(project_key) + self.log_message(f"Selected project: {project_info['name']}") + + ################### Teil 14 - GUI Log Tab und Progress Section ################### + def _create_log_tab(self): + """Create log tab""" + log_frame = ttk.Frame(self.notebook) + self.notebook.add(log_frame, text="Installation Log") + + # Log text with scrollbar + log_container = tk.Frame(log_frame) + log_container.pack(fill="both", expand=True, padx=10, pady=10) + + self.log_text = tk.Text( + log_container, + wrap=tk.WORD, + font=("Consolas", 9), + bg="#1e1e1e", + fg="#d4d4d4", + insertbackground="white", + selectbackground="#264f78", + ) + + log_scrollbar = ttk.Scrollbar( + log_container, orient="vertical", command=self.log_text.yview + ) + self.log_text.configure(yscrollcommand=log_scrollbar.set) + + self.log_text.pack(side="left", fill="both", expand=True) + log_scrollbar.pack(side="right", fill="y") + + # Log controls + log_controls = tk.Frame(log_frame) + log_controls.pack(fill="x", padx=10, pady=(0, 10)) + + # Clear log button + clear_log_btn = ttk.Button( + log_controls, text="Clear Log", command=self.clear_log + ) + clear_log_btn.pack(side="right") + + # Initial log message + self.log_message( + f"=== {LXToolsAppConfig.APP_NAME} v{LXToolsAppConfig.VERSION} ===" + ) + self.log_message(f"Working directory: {LXToolsAppConfig.WORK_DIR}") + self.log_message(f"Icons directory: {LXToolsAppConfig.ICONS_DIR}") + self.log_message(f"Detected OS: {self.detected_os}") + self.log_message("Ready for installation...") + + def _create_progress_section(self): + """Create progress section with download icon""" + progress_frame = ttk.LabelFrame(self.root, text="Progress", padding=10) + progress_frame.pack(fill="x", padx=15, pady=10) + + # Container for Icon and Progress + progress_container = tk.Frame(progress_frame) + progress_container.pack(fill="x") + + # Download Icon (left) + self.download_icon_label = tk.Label(progress_container, text="", width=4) + self.download_icon_label.pack(side="left", padx=(0, 10)) + + # Progress Text (right, expandable) + self.progress_label = tk.Label( + progress_container, + text="Ready for installation...", + font=("Helvetica", 10), + fg="blue", + anchor="w", + ) + self.progress_label.pack(side="left", fill="x", expand=True) + + # Initial icon load (neutral) + self._reset_download_icon() + + ################### Teil 15 - GUI Buttons and Icon Management ################### + def _create_modern_buttons(self): + """Create modern styled buttons""" + button_frame = tk.Frame(self.root, bg=self.colors["bg"]) + button_frame.pack(fill="x", padx=15, pady=(0, 15)) + + # Button style configuration + style = ttk.Style() + + # Install button (green) + style.configure( + "Install.TButton", foreground="#27ae60", font=("Helvetica", 10, "bold") + ) + style.map( + "Install.TButton", + foreground=[("active", "#2ecc71"), ("pressed", "#1e8449")], + ) + + # Uninstall button (red) + style.configure( + "Uninstall.TButton", foreground="#e74c3c", font=("Helvetica", 10, "bold") + ) + style.map( + "Uninstall.TButton", + foreground=[("active", "#ec7063"), ("pressed", "#c0392b")], + ) + + # Refresh button (blue) + style.configure( + "Refresh.TButton", foreground="#3498db", font=("Helvetica", 10, "bold") + ) + style.map( + "Refresh.TButton", + foreground=[("active", "#5dade2"), ("pressed", "#2980b9")], + ) + + # Create buttons + install_btn = ttk.Button( + button_frame, + text="Install/Update Selected", + command=self.install_selected, + style="Install.TButton", + ) + install_btn.pack(side="left", padx=(0, 10)) + + uninstall_btn = ttk.Button( + button_frame, + text="Uninstall Selected", + command=self.uninstall_selected, + style="Uninstall.TButton", + ) + uninstall_btn.pack(side="left", padx=(0, 10)) + + refresh_btn = ttk.Button( + button_frame, + text="Refresh Status", + command=self.refresh_status, + style="Refresh.TButton", + ) + refresh_btn.pack(side="right") + + def update_download_icon(self, status): + """Update download icon based on status""" + if not self.download_icon_label: + return + + if status == "downloading": + icon = self.image_manager.load_image("download_icon") + if icon: + self.download_icon_label.config(image=icon, text="") + self.download_icon_label.image = icon + else: + self.download_icon_label.config(text="⬇️", font=("Helvetica", 16)) + + elif status == "error": + icon = self.image_manager.load_image("download_error_icon") + if icon: + self.download_icon_label.config(image=icon, text="") + self.download_icon_label.image = icon + else: + self.download_icon_label.config(text="❌", font=("Helvetica", 16)) + + elif status == "success": + icon = self.image_manager.load_image("success_icon") + if icon: + self.download_icon_label.config(image=icon, text="") + self.download_icon_label.image = icon + else: + self.download_icon_label.config(text="✅", font=("Helvetica", 16)) + + self.download_icon_label.update() + + def _reset_download_icon(self): + """Reset download icon to neutral state""" + icon = self.image_manager.load_image("download_icon") + if icon: + self.download_icon_label.config(image=icon, text="") + self.download_icon_label.image = icon + else: + self.download_icon_label.config(text="📥", font=("Helvetica", 16)) + + ################### Teil 16 - GUI Status Management ################### + def refresh_status(self): + """Refresh application status and version information""" + self.update_progress("Refreshing status and checking versions...") + self._reset_download_icon() + self.log_message("=== Refreshing Status ===") + + for project_key, project_info in self.app_manager.get_all_projects().items(): + status_label = self.status_labels[project_key] + version_label = self.version_labels[project_key] + + self.log_message(f"Checking {project_info['name']}...") + + if self.app_manager.is_installed(project_key): + installed_version = self.app_manager.get_installed_version(project_key) + status_label.config( + text=f"✅ Installed (v{installed_version})", fg="green" + ) + self.log_message( + f"{project_info['name']}: Installed v{installed_version}" + ) + + # Get latest version from API + try: + latest_version = self.app_manager.get_latest_version(project_key) + if latest_version != "Unknown": + if installed_version != latest_version: + version_label.config( + text=f"Latest: v{latest_version} (Update available)", + fg="orange", + ) + self.log_message( + f"{project_info['name']}: Update available v{latest_version}" + ) + else: + version_label.config( + text=f"Latest: v{latest_version} (Up to date)", + fg="green", + ) + self.log_message(f"{project_info['name']}: Up to date") + else: + version_label.config(text="Latest: Unknown", fg="gray") + self.log_message( + f"{project_info['name']}: Could not check latest version" + ) + except Exception as e: + version_label.config(text="Latest: Check failed", fg="gray") + self.log_message( + f"{project_info['name']}: Version check failed: {e}" + ) + else: + status_label.config(text="❌ Not installed", fg="red") + self.log_message(f"{project_info['name']}: Not installed") + + # Still show latest available version + try: + latest_version = self.app_manager.get_latest_version(project_key) + if latest_version != "Unknown": + version_label.config( + text=f"Available: v{latest_version}", fg="blue" + ) + self.log_message( + f"{project_info['name']}: Available v{latest_version}" + ) + else: + version_label.config(text="Available: Unknown", fg="gray") + except Exception as e: + version_label.config(text="Available: Check failed", fg="gray") + self.log_message( + f"{project_info['name']}: Version check failed: {e}" + ) + + self.update_progress("Status refresh completed.") + self.log_message("=== Status refresh completed ===") + + ################### Teil 17 - GUI Action Methods ################### + def install_selected(self): + """Handle install button click""" + if not self.selected_project: + messagebox.showwarning("Warning", "Please select a project to install.") + return + + # Check internet connection + if not NetworkChecker.check_internet(): + self.update_download_icon("error") + messagebox.showerror( + "Network Error", + "No internet connection available.\nPlease check your network connection.", + ) + return + + if not NetworkChecker.check_repository(): + self.update_download_icon("error") + messagebox.showerror( + "Repository Error", "Cannot access repository.\nPlease try again later." + ) + return + + # Reset download icon + self._reset_download_icon() + project_info = self.app_manager.get_project_info(self.selected_project) + + # Check if already installed + if self.app_manager.is_installed(self.selected_project): + installed_version = self.app_manager.get_installed_version( + self.selected_project + ) + latest_version = self.app_manager.get_latest_version(self.selected_project) + + dialog_text = ( + f"{project_info['name']} is already installed.\n\n" + f"Installed version: v{installed_version}\n" + f"Latest version: v{latest_version}\n\n" + f"YES = Update (reinstall all files)\n" + f"NO = Uninstall\n" + f"Cancel = Do nothing" + ) + + result = messagebox.askyesnocancel( + f"{project_info['name']} already installed", dialog_text + ) + + if result is None: # Cancel + self.update_progress("Installation cancelled.") + return + elif not result: # Uninstall + self.uninstall_selected() + return + else: # Update + self.update_progress("Updating application...") + + try: + self.update_download_icon("downloading") + self.installation_manager.install_project(self.selected_project) + self.update_download_icon("success") + messagebox.showinfo( + "Success", + f"{project_info['name']} has been successfully installed/updated.", + ) + self.refresh_status() + except Exception as e: + self.update_download_icon("error") + messagebox.showerror("Error", f"Installation failed: {e}") + + def uninstall_selected(self): + """Handle uninstall button click""" + if not self.selected_project: + messagebox.showwarning("Warning", "Please select a project to uninstall.") + return + + project_info = self.app_manager.get_project_info(self.selected_project) + + if not self.app_manager.is_installed(self.selected_project): + messagebox.showinfo("Info", f"{project_info['name']} is not installed.") + return + + result = messagebox.askyesno( + "Confirm Uninstall", + f"Are you sure you want to uninstall {project_info['name']}?\n\n" + f"This will remove all application files and user configurations.", + ) + if not result: + return + + try: + self.uninstallation_manager.uninstall_project(self.selected_project) + messagebox.showinfo( + "Success", f"{project_info['name']} has been successfully uninstalled." + ) + self.refresh_status() + except Exception as e: + messagebox.showerror("Error", f"Uninstallation failed: {e}") + + ################### Teil 18 - GUI Helper Methods ################### + def update_progress(self, message): + """Update progress message""" + if self.progress_label: + self.progress_label.config(text=message) + self.progress_label.update() + print(f"Progress: {message}") + + def log_message(self, message): + """Add message to log""" + if self.log_text: + timestamp = datetime.now().strftime("%H:%M:%S") + log_entry = f"[{timestamp}] {message}\n" + self.log_text.insert(tk.END, log_entry) + self.log_text.see(tk.END) + self.log_text.update() + print(f"Log: {message}") + + def clear_log(self): + """Clear the log""" + if self.log_text: + self.log_text.delete(1.0, tk.END) + self.log_message("Log cleared") + + def run(self): + """Start the GUI application""" + root = self.create_gui() + root.mainloop() + + +################### Teil 19 - TKinter Check und Main Function ################### +def check_and_install_tkinter(): + """Check if tkinter is available and install if needed""" + if not OSDetector.check_tkinter_available(): + print("TKinter is not available on this system.") + detected_os = OSDetector.detect_os() + print(f"Detected OS: {detected_os}") + + response = input("Would you like to install TKinter? (y/n): ").lower().strip() + if response in ["y", "yes"]: + print("Installing TKinter...") + if OSDetector.install_tkinter(): + print("TKinter installed successfully!") + print("Please restart the application.") + return False + else: + print("Failed to install TKinter.") + print("Please install TKinter manually:") + + if detected_os in ["Ubuntu", "Debian", "Linux Mint", "Pop!_OS"]: + print("sudo apt update && sudo apt install python3-tk") + elif detected_os == "Fedora": + print("sudo dnf install tkinter") + elif detected_os in [ + "Arch Linux", + "Manjaro", + "Garuda Linux", + "EndeavourOS", + ]: + print("sudo pacman -S tk") + elif "SUSE" in detected_os or "openSUSE" in detected_os: + print("sudo zypper install python3-tk") + else: + print("Please check your distribution's package manager.") + + return False + else: + print("TKinter is required to run this application.") + return False + + return True + + +def main(): + """Main function to start the application""" + print(f"=== {LXToolsAppConfig.APP_NAME} v{LXToolsAppConfig.VERSION} ===") + print(f"Working directory: {os.getcwd()}") + + # Check and install tkinter if needed + if not check_and_install_tkinter(): + return + + try: + # Create and run the GUI + app = LXToolsGUI() + app.run() + except KeyboardInterrupt: + print("\nApplication interrupted by user.") + except Exception as e: + print(f"Fatal error: {e}") + try: + messagebox.showerror("Fatal Error", f"Application failed to start: {e}") + except: + pass + + +if __name__ == "__main__": + main()