From 0dfd727222e5bc9fb7f6b1325b016d6d24dd1957 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 05:33:39 +0900 Subject: [PATCH 01/84] change: use enum for popup mode --- helpers/ui.py | 70 ++++++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/helpers/ui.py b/helpers/ui.py index 9508760..cb5542d 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -19,7 +19,7 @@ gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango from enum import Enum -# 5.5.0 +# 5.6.0 app_name = "DZGUI" start_time = 0 @@ -297,25 +297,25 @@ def process_shell_return_code(transient_parent, msg, code, original_input): #TODO: add logger output to each case 0: # success with notice popup - spawn_dialog(transient_parent, msg, "NOTIFY") + spawn_dialog(transient_parent, msg, Popup.NOTIFY) case 1: # error with notice popup if msg == "": msg = "Something went wrong" - spawn_dialog(transient_parent, msg, "NOTIFY") + spawn_dialog(transient_parent, msg, Popup.NOTIFY) case 2: # warn and recurse (e.g. validation failed) - spawn_dialog(transient_parent, msg, "NOTIFY") + spawn_dialog(transient_parent, msg, Popup.NOTIFY) treeview = transient_parent.grid.scrollable_treelist.treeview process_tree_option(original_input, treeview) case 4: # for BM only - spawn_dialog(transient_parent, msg, "NOTIFY") + spawn_dialog(transient_parent, msg, Popup.NOTIFY) treeview = transient_parent.grid.scrollable_treelist.treeview process_tree_option(["Options", "Change Battlemetrics API key"], treeview) case 5: # for steam only - spawn_dialog(transient_parent, msg, "NOTIFY") + spawn_dialog(transient_parent, msg, Popup.NOTIFY) treeview = transient_parent.grid.scrollable_treelist.treeview process_tree_option(["Options", "Change Steam API key"], treeview) case 6: @@ -330,17 +330,17 @@ def process_shell_return_code(transient_parent, msg, code, original_input): config_vals.append(i) tooltip = format_metadata(col) transient_parent.grid.update_statusbar(tooltip) - spawn_dialog(transient_parent, msg, "NOTIFY") + spawn_dialog(transient_parent, msg, Popup.NOTIFY) return case 100: # final handoff before launch - final_conf = spawn_dialog(transient_parent, msg, "CONFIRM") + final_conf = spawn_dialog(transient_parent, msg, Popup.CONFIRM) treeview = transient_parent.grid.scrollable_treelist.treeview if final_conf == 1 or final_conf is None: return process_tree_option(["Handshake", ""], treeview) case 255: - spawn_dialog(transient_parent, "Update complete. Please close DZGUI and restart.", "NOTIFY") + spawn_dialog(transient_parent, "Update complete. Please close DZGUI and restart.", Popup.NOTIFY) Gtk.main_quit() @@ -369,7 +369,7 @@ def process_tree_option(input, treeview): proc = call_out(transient_parent, subproc, args) GLib.idle_add(_load) if bool is True: - wait_dialog = GenericDialog(transient_parent, msg, "WAIT") + wait_dialog = GenericDialog(transient_parent, msg, Popup.WAIT) wait_dialog.show_all() thread = threading.Thread(target=_background, args=(subproc, args, wait_dialog)) thread.start() @@ -432,7 +432,7 @@ def process_tree_option(input, treeview): link_label = "Open Battlemetrics API page" prompt = "Enter new API key" - user_entry = EntryDialog(transient_parent, prompt, "ENTRY", link_label) + user_entry = EntryDialog(transient_parent, prompt, Popup.ENTRY, link_label) res = user_entry.get_input() if res is None: logger.info("User aborted entry dialog") @@ -711,7 +711,7 @@ class TreeView(Gtk.TreeView): iter = self.get_current_iter() server_store.remove(iter) msg = proc.stdout - res = spawn_dialog(parent, msg, "NOTIFY") + res = spawn_dialog(parent, msg, Popup.NOTIFY) case "Remove from history": record = "%s:%s" %(self.get_column_at_index(7), self.get_column_at_index(8)) call_out(parent, context_menu_label, record) @@ -734,16 +734,16 @@ class TreeView(Gtk.TreeView): conf_msg = "Really delete the mod '%s'?" %(value) success_msg = "Successfully deleted the mod '%s'." %(value) fail_msg = "An error occurred during deletion. Aborting." - res = spawn_dialog(parent, conf_msg, "CONFIRM") + res = spawn_dialog(parent, conf_msg, Popup.CONFIRM) symlink = self.get_column_at_index(1) dir = self.get_column_at_index(2) if res == 0: proc = call_out(parent, "delete", symlink, dir) if proc.returncode == 0: - spawn_dialog(parent, success_msg, "NOTIFY") + spawn_dialog(parent, success_msg, Popup.NOTIFY) self._update_quad_column("List installed mods") else: - spawn_dialog(parent, fail_msg, "NOTIFY") + spawn_dialog(parent, fail_msg, Popup.NOTIFY) case "Open in Steam Workshop": record = self.get_column_at_index(2) call_out(parent, "open_workshop_page", record) @@ -805,7 +805,7 @@ class TreeView(Gtk.TreeView): diff = now - then cooldown = 30 - math.floor(diff) if ((start_time > 0) and (now - then) < 30): - spawn_dialog(parent, "Global refresh cooldown not met. Wait %s second(s)." %(str(cooldown)), "NOTIFY") + spawn_dialog(parent, "Global refresh cooldown not met. Wait %s second(s)." %(str(cooldown)), Popup.NOTIFY) return start_time = now @@ -935,7 +935,7 @@ class TreeView(Gtk.TreeView): wait_dialog.destroy() parent = self.get_outer_window() - wait_dialog = GenericDialog(parent, "Refreshing player count", "WAIT") + wait_dialog = GenericDialog(parent, "Refreshing player count", Popup.WAIT) wait_dialog.show_all() select = self.get_selection() sels = select.get_selected_rows() @@ -971,7 +971,7 @@ class TreeView(Gtk.TreeView): No servers returned. Possible network issue or API key on cooldown? Return to the main menu, wait 60s, and try again. If this issue persists, your API key may be defunct.""" - spawn_dialog(self.get_outer_window(), textwrap.dedent(api_warn_msg), "NOTIFY") + spawn_dialog(self.get_outer_window(), textwrap.dedent(api_warn_msg), Popup.NOTIFY) grid = self.get_outer_grid() right_panel = grid.right_panel @@ -1097,7 +1097,7 @@ class TreeView(Gtk.TreeView): return mode = mode + ":" + port - wait_dialog = GenericDialog(transient_parent, "Fetching server metadata", "WAIT") + wait_dialog = GenericDialog(transient_parent, "Fetching server metadata", Popup.WAIT) wait_dialog.show_all() thread = threading.Thread(target=self._background, args=(wait_dialog, mode)) thread.start() @@ -1153,12 +1153,12 @@ class TreeView(Gtk.TreeView): data = call_out(self, "show_log") res = parse_log_rows(data) if res == 1: - spawn_dialog(self.get_outer_window(), "Failed to load log file, possibly corrupted", "NOTIFY") + spawn_dialog(self.get_outer_window(), "Failed to load log file, possibly corrupted", Popup.NOTIFY) return transient_parent = self.get_outer_window() - wait_dialog = GenericDialog(transient_parent, "Checking mods", "WAIT") + wait_dialog = GenericDialog(transient_parent, "Checking mods", Popup.WAIT) wait_dialog.show_all() thread = threading.Thread(target=self._background_quad, args=(wait_dialog, mode)) thread.start() @@ -1181,7 +1181,7 @@ class TreeView(Gtk.TreeView): qport = self.get_column_at_index(8) record = "%s:%s" %(addr, str(qport)) - wait_dialog = GenericDialog(transient_parent, "Querying server and aligning mods", "WAIT") + wait_dialog = GenericDialog(transient_parent, "Querying server and aligning mods", Popup.WAIT) wait_dialog.show_all() thread = threading.Thread(target=self._background_connection, args=(wait_dialog, record)) thread.start() @@ -1205,7 +1205,7 @@ class TreeView(Gtk.TreeView): if chosen_row == "Server browser": cooldown = call_out(self, "test_cooldown", "", "") if cooldown.returncode == 1: - spawn_dialog(self.get_outer_window(), cooldown.stdout, "NOTIFY") + spawn_dialog(self.get_outer_window(), cooldown.stdout, Popup.NOTIFY) return 1 for check in checks: toggle_signal(filters_vbox, check, '_on_check_toggle', False) @@ -1323,7 +1323,7 @@ def filter_servers(transient_parent, filters_vbox, treeview, context): toggle_signal(filters_vbox, filters_vbox, '_on_button_release', False) toggle_signal(filters_vbox, filters_vbox.maps_combo, '_on_map_changed', False) - dialog = GenericDialog(transient_parent, "Filtering results", "WAIT") + dialog = GenericDialog(transient_parent, "Filtering results", Popup.WAIT) dialog.show_all() server_store.clear() @@ -1343,6 +1343,12 @@ class Port(Enum): DEFAULT = 1 CUSTOM = 2 +class Popup(Enum): + WAIT = 1 + NOTIFY = 2 + CONFIRM = 3 + ENTRY = 4 + class GenericDialog(Gtk.MessageDialog): def __init__(self, parent, text, mode): @@ -1351,19 +1357,19 @@ class GenericDialog(Gtk.MessageDialog): return True match mode: - case "WAIT": + case Popup.WAIT: dialog_type = Gtk.MessageType.INFO button_type = Gtk.ButtonsType.NONE header_text = "Please wait" - case "NOTIFY": + case Popup.NOTIFY: dialog_type = Gtk.MessageType.INFO button_type = Gtk.ButtonsType.OK header_text = "Notice" - case "CONFIRM": + case Popup.CONFIRM: dialog_type = Gtk.MessageType.QUESTION button_type = Gtk.ButtonsType.OK_CANCEL header_text = "Confirmation" - case "ENTRY": + case Popup.ENTRY: dialog_type = Gtk.MessageType.QUESTION button_type = Gtk.ButtonsType.OK_CANCEL header_text = "User input required" @@ -1384,7 +1390,7 @@ class GenericDialog(Gtk.MessageDialog): modal=True, ) - if mode == "WAIT": + if mode == Popup.WAIT: dialogBox = self.get_content_area() spinner = Gtk.Spinner() dialogBox.pack_end(spinner, False, False, 0) @@ -1598,7 +1604,7 @@ class PingDialog(GenericDialog): dialogBox = self.get_content_area() self.set_default_response(Gtk.ResponseType.OK) self.set_size_request(500, 200) - wait_dialog = GenericDialog(parent, "Checking ping", "WAIT") + wait_dialog = GenericDialog(parent, "Checking ping", Popup.WAIT) wait_dialog.show_all() thread = threading.Thread(target=self._background, args=(wait_dialog, parent, record)) thread.start() @@ -1642,7 +1648,7 @@ class ModDialog(GenericDialog): column.set_sort_column_id(i) dialogBox.pack_end(self.scrollable, True, True, 0) - wait_dialog = GenericDialog(parent, "Fetching modlist", "WAIT") + wait_dialog = GenericDialog(parent, "Fetching modlist", Popup.WAIT) wait_dialog.show_all() thread = threading.Thread(target=self._background, args=(wait_dialog, parent, record)) thread.start() @@ -1651,7 +1657,7 @@ class ModDialog(GenericDialog): def _load(): dialog.destroy() if data.returncode == 1: - spawn_dialog(parent, "Server has no mods installed or is unsupported in this mode", "NOTIFY") + spawn_dialog(parent, "Server has no mods installed or is unsupported in this mode", Popup.NOTIFY) return self.show_all() self.set_markup("Modlist (%s mods)" %(mod_count)) From 031df3a62f931cc71e9dbf6a73ab41c6f976afc6 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 05:35:50 +0900 Subject: [PATCH 02/84] fix: batch MSQ queries --- helpers/funcs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/helpers/funcs b/helpers/funcs index b69640f..b0c57d7 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -1,6 +1,6 @@ #!/usr/bin/env bash set -o pipefail -version=5.5.0 +version=5.6.0 #CONSTANTS aid=221100 @@ -38,6 +38,7 @@ lock_file="$state_path/$prefix.lock" #CACHE cache_dir="$HOME/.cache/$app_name" _cache_servers="$cache_dir/$prefix.servers" +_cache_temp="$cache_dir/$prefix.temp" _cache_my_servers="$cache_dir/$prefix.my_servers" _cache_history="$cache_dir/$prefix.history" _cache_launch="$cache_dir/$prefix.launch_mods" @@ -380,9 +381,30 @@ get_dist(){ fi } get_remote_servers(){ - local limit=20000 - local url="https://api.steampowered.com/IGameServersService/GetServerList/v1/?filter=\appid\221100&limit=$limit&key=$steam_api" - curl -Ls "$url" | jq -r '.response.servers' + params=( + "\\nor\1\map\chernarusplus\\nor\1\map\sakhal" + "\map\chernarusplus\empty\1" + "\map\chernarusplus\noplayers\1" + "\map\\sakhal" + ) + local limit=10000 + local url="https://api.steampowered.com/IGameServersService/GetServerList/v1/?" + + _fetch(){ + local param="$1" + curl -LsG "$url" \ + -d filter="\appid\221100${param}" \ + -d limit=$limit \ + -d key=$steam_api \ + | jq -M -r '.response.servers' + + } + + for ((i=0; i <${#params[@]}; i++ )); do + _fetch "${params[$i]}" > $_cache_temp.${i} + done + + jq -n '[ [inputs]|add ].[]' $_cache_temp.* && rm $_cache_temp.* } get_unique_maps(){ shift From 1eed01623a97637e9e8369fcbfa52f4854c71270 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 05:53:44 +0900 Subject: [PATCH 03/84] chore: clarify timeout message --- helpers/funcs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/funcs b/helpers/funcs index b0c57d7..59c3718 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -671,7 +671,7 @@ test_ping(){ local qport="$2" local res res=$(ping -c1 -4 -W0.5 $1 | grep time= | awk -F= '{print $4}') - [[ ! $? -eq 0 ]] && res="Unreachable" + [[ ! $? -eq 0 ]] && res="Timed out" printf "%s" "$res" } show_server_modlist(){ From 50d6bdf74f44ba2a912dc088abe505bf4109cf3f Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 06:19:37 +0900 Subject: [PATCH 04/84] feat: add pluralize func --- helpers/ui.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/helpers/ui.py b/helpers/ui.py index cb5542d..ae27bd4 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -169,6 +169,14 @@ status_tooltip = { "Hall of fame ⧉": "A list of significant contributors and testers", } +def pluralize(plural, count): + suffix = plural[-2:] + if suffix == "es": + base = plural[:-2] + return f"%s{'es'[:2*count^2]}" %(base) + else: + base = plural[:-1] + return f"%s{'s'[:count^1]}" %(base) def format_ping(ping): ms = " | Ping: %s" %(ping) @@ -997,7 +1005,8 @@ class TreeView(Gtk.TreeView): self.set_model(mod_store) self.grab_focus() size = locale.format_string('%.3f', total_size, grouping=True) - grid.update_statusbar("Found %s mods taking up %s MiB" %(f'{total_mods:n}', size)) + pretty = pluralize("mods", total_mods) + grid.update_statusbar(f"Found {total_mods:n} {pretty} taking up {size} MiB") toggle_signal(self, self, '_on_keypress', True) grid = self.get_outer_grid() @@ -1286,16 +1295,10 @@ def format_metadata(row_sel): return prefix -def format_tooltip(sum, hits): - if hits == 1: - hit_suffix = "match" - else: - hit_suffix = "matches" - if sum == 1: - player_suffix = "player" - else: - player_suffix = "players" - tooltip = "Found %s %s with %s %s" %(f'{hits:n}', hit_suffix, f'{sum:n}', player_suffix) +def format_tooltip(players, hits): + hits_pretty = pluralize("matches", hits) + players_pretty = pluralize("players", players) + tooltip = f"Found {hits:n} {hits_pretty} with {players:n} {players_pretty}" return tooltip From 365b455b3c4c3595d4db280575af8d1c9a4e7022 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:34:05 +0900 Subject: [PATCH 05/84] feat: bulk mod deletion --- helpers/ui.py | 166 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 150 insertions(+), 16 deletions(-) diff --git a/helpers/ui.py b/helpers/ui.py index ae27bd4..99d8102 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -48,12 +48,14 @@ default_tooltip = "Select a row to see its detailed description" server_tooltip = [None, None] user_path = os.path.expanduser('~') +cache_path = '%s/.cache/dzgui' %(user_path) state_path = '%s/.local/state/dzgui' %(user_path) helpers_path = '%s/.local/share/dzgui/helpers' %(user_path) log_path = '%s/logs' %(state_path) changelog_path = '%s/CHANGELOG.md' %(state_path) geometry_path = '%s/dzg.cols.json' %(state_path) funcs = '%s/funcs' %(helpers_path) +mods_temp_file = '%s/dzg.mods_temp' %(cache_path) logger = logging.getLogger(__name__) log_file = '%s/DZGUI_DEBUG.log' %(log_path) @@ -169,6 +171,32 @@ status_tooltip = { "Hall of fame ⧉": "A list of significant contributors and testers", } +def relative_widget(child): + # get target widget relative to source widget + # chiefly used for transient modals and parent member functions + # when calling non-adjacent widgets + # positions are relative to grid members + grid = child.get_parent().get_parent() + treeview = grid.scrollable_treelist.treeview + outer = grid.get_parent() + + widgets = { + 'grid': grid, + 'treeview': treeview, + 'outer': outer + } + + supported = [ + "ModSelectionPanel", + "ButtonBox", + "TreeView" + ] + + if child.__class__.__name__ not in supported: + raise Exception("Unsupported child widget") + + return widgets + def pluralize(plural, count): suffix = plural[-2:] if suffix == "es": @@ -571,10 +599,17 @@ class ButtonBox(Gtk.Box): def _update_single_column(self, context): logger.info("Returning from multi-column view to monocolumn view for the context '%s'" %(context)) - treeview = self.get_treeview() + widgets = relative_widget(self) + + # only applicable when returning from mod list + grid = widgets["grid"] + grid.right_panel.remove(grid.sel_panel) right_panel = self.get_parent() right_panel.set_filter_visibility(False) + treeview = widgets["treeview"] + treeview.set_selection_mode(Gtk.SelectionMode.SINGLE) + """Block maps combo when returning to main menu""" toggle_signal(right_panel.filters_vbox, right_panel.filters_vbox.maps_combo, '_on_map_changed', False) right_panel.filters_vbox.keyword_entry.set_text("") @@ -749,14 +784,30 @@ class TreeView(Gtk.TreeView): proc = call_out(parent, "delete", symlink, dir) if proc.returncode == 0: spawn_dialog(parent, success_msg, Popup.NOTIFY) - self._update_quad_column("List installed mods") + self.update_quad_column("List installed mods") else: spawn_dialog(parent, fail_msg, Popup.NOTIFY) case "Open in Steam Workshop": record = self.get_column_at_index(2) call_out(parent, "open_workshop_page", record) + def toggle_selection(self, bool): + l = len(mod_store) + match bool: + case True: + for i in range (0, l): + path = Gtk.TreePath(i) + it = mod_store.get_iter(path) + self.get_selection().select_path(path) + case False: + for i in range (0, l): + path = Gtk.TreePath(i) + it = mod_store.get_iter(path) + self.get_selection().unselect_path(path) + def _on_button_release(self, widget, event): + if event.type is Gdk.EventType.BUTTON_RELEASE and event.button != 3: + return try: pathinfo = self.get_path_at_pos(event.x, event.y) if pathinfo is None: @@ -766,8 +817,6 @@ class TreeView(Gtk.TreeView): except AttributeError: pass - if event.type is Gdk.EventType.BUTTON_RELEASE and event.button != 3: - return context = self.get_first_col() self.menu = Gtk.Menu() @@ -861,8 +910,6 @@ class TreeView(Gtk.TreeView): self.emit("on_distcalc_started") self.current_proc = CalcDist(self, addr, self.queue, cache) self.current_proc.start() - elif None: - return else: tooltip = format_metadata(row_sel) grid.update_statusbar(tooltip) @@ -1004,16 +1051,28 @@ class TreeView(Gtk.TreeView): dialog.destroy() self.set_model(mod_store) self.grab_focus() - size = locale.format_string('%.3f', total_size, grouping=True) - pretty = pluralize("mods", total_mods) - grid.update_statusbar(f"Found {total_mods:n} {pretty} taking up {size} MiB") + if abort is False: + size = locale.format_string('%.3f', total_size, grouping=True) + pretty = pluralize("mods", total_mods) + grid.update_statusbar(f"Found {total_mods:n} {pretty} taking up {size} MiB") + #2024-11-12 + toggle_signal(self, self.selected_row, '_on_tree_selection_changed', True) toggle_signal(self, self, '_on_keypress', True) grid = self.get_outer_grid() right_panel = grid.right_panel + abort = False right_panel.set_filter_visibility(False) data = call_out(self, "list_mods", mode) + + # suppress errors if no mods available on system + if data.returncode == 1: + abort = True + GLib.idle_add(load) + spawn_dialog(self.get_outer_window(), data.stdout, Popup.NOTIFY) + return 1 + result = parse_mod_rows(data) total_size = result[0] total_mods = result[1] @@ -1091,6 +1150,8 @@ class TreeView(Gtk.TreeView): selected_map.clear() selected_map.append("Map=All maps") + self.set_selection_mode(Gtk.SelectionMode.SINGLE) + for check in checks: toggle_signal(self.get_outer_grid().right_panel.filters_vbox, check, '_on_check_toggle', True) toggle_signal(self, self, '_on_keypress', True) @@ -1130,8 +1191,12 @@ class TreeView(Gtk.TreeView): cell.set_property('text', formatted) return - def _update_quad_column(self, mode): - # toggle_signal(self, self.selected_row, '_on_tree_selection_changed', False) + def set_selection_mode(self, mode): + sel = self.get_selection() + sel.set_mode(mode) + + def update_quad_column(self, mode): + toggle_signal(self, self.selected_row, '_on_tree_selection_changed', False) for column in self.get_columns(): self.remove_column(column) @@ -1141,6 +1206,10 @@ class TreeView(Gtk.TreeView): if mode == "List installed mods": cols = mod_cols self.set_model(mod_store) + # attach button panel + grid = self.get_parent().get_parent() + grid.right_panel.pack_start(grid.sel_panel, False, False, 0) + grid.show_all() else: cols = log_cols self.set_model(log_store) @@ -1152,12 +1221,10 @@ class TreeView(Gtk.TreeView): if i == 3: column.set_cell_data_func(renderer, self._format_float, func_data=None) column.set_sort_column_id(i) - #if (column_title == "Name"): - # column.set_fixed_width(600) self.append_column(column) if mode == "List installed mods": - pass + self.set_selection_mode(Gtk.SelectionMode.MULTIPLE) else: data = call_out(self, "show_log") res = parse_log_rows(data) @@ -1236,7 +1303,7 @@ class TreeView(Gtk.TreeView): self.grab_focus() elif chosen_row == "List installed mods" or chosen_row == "Show debug log": toggle_signal(self, self.selected_row, '_on_tree_selection_changed', False) - self._update_quad_column(chosen_row) + self.update_quad_column(chosen_row) toggle_signal(self, self.selected_row, '_on_tree_selection_changed', True) elif any(map(context.__contains__, valid_contexts)): # implies activated row on any server list subcontext @@ -1747,7 +1814,7 @@ class Grid(Gtk.Grid): self.scrollable_treelist.set_vexpand(True) self.right_panel = RightPanel(is_steam_deck) - + self.sel_panel = ModSelectionPanel() self.bar = Gtk.Statusbar() self.scrollable_treelist.treeview.connect("on_distcalc_started", self._on_calclat_started) @@ -1853,6 +1920,73 @@ class App(Gtk.Application): self.win.halt_proc_and_quit(self, None) +class ModSelectionPanel(Gtk.Box): + def __init__(self): + super().__init__(spacing=6) + self.set_orientation(Gtk.Orientation.VERTICAL) + + labels = [ + "Select all", + "Unselect all", + "Delete selected" + ] + + for l in labels: + button = Gtk.Button(label=l) + button.set_margin_start(10) + button.set_margin_end(10) + button.connect("clicked", self._on_button_clicked) + self.pack_start(button, False, True, 0) + + def _on_button_clicked(self, button): + label = button.get_label() + widgets = relative_widget(self) + parent = widgets["outer"] + treeview = widgets["treeview"] + match label: + case "Select all": + treeview.toggle_selection(True) + case "Unselect all": + treeview.toggle_selection(False) + case "Delete selected": + (model, pathlist) = treeview.get_selection().get_selected_rows() + ct = len(pathlist) + if ct < 1: + return + + self._iterate_mod_deletion(model, pathlist, ct) + + def _iterate_mod_deletion(self, model, pathlist, ct): + # hedge against large number of arguments + widgets = relative_widget(self) + parent = widgets["outer"] + treeview = widgets["treeview"] + + pretty = pluralize("mods", ct) + conf_msg = f"You are going to delete {ct} {pretty}. Proceed?" + success_msg = f"Successfully deleted {ct} {pretty}." + fail_msg = "An error occurred during deletion. Aborting." + + res = spawn_dialog(parent, conf_msg, Popup.CONFIRM) + if res != 0: + return + + mods = [] + for i in pathlist: + it = model.get_iter(i) + symlink = model.get_value(it, 1) + path = model.get_value(it, 2) + concat = symlink + " " + path + "\n" + mods.append(concat) + with open(mods_temp_file, "w") as outfile: + outfile.writelines(mods) + proc = call_out(parent, "delete") + if proc.returncode == 0: + spawn_dialog(parent, success_msg, Popup.NOTIFY) + treeview.update_quad_column("List installed mods") + else: + spawn_dialog(parent, fail_msg, Popup.NOTIFY) + class FilterPanel(Gtk.Box): def __init__(self): super().__init__(spacing=6) From fbc500066f4558958f4a189df0a5eedc6d60a328 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:34:25 +0900 Subject: [PATCH 06/84] fix: focus first row --- helpers/ui.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/helpers/ui.py b/helpers/ui.py index 99d8102..1b55aa9 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -969,6 +969,11 @@ class TreeView(Gtk.TreeView): else: return False + def _focus_first_row(self): + path = Gtk.TreePath(0) + it = mod_store.get_iter(path) + self.get_selection().select_path(path) + def get_column_at_index(self, index): select = self.get_selection() sels = select.get_selected_rows() @@ -1058,6 +1063,7 @@ class TreeView(Gtk.TreeView): #2024-11-12 toggle_signal(self, self.selected_row, '_on_tree_selection_changed', True) toggle_signal(self, self, '_on_keypress', True) + self._focus_first_row() grid = self.get_outer_grid() right_panel = grid.right_panel From c254de2aec6f8c4dd2fbbb6174b951984d301bf0 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:42:01 +0900 Subject: [PATCH 07/84] chore: add comments --- helpers/ui.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/helpers/ui.py b/helpers/ui.py index 1b55aa9..16cd43e 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -172,10 +172,11 @@ status_tooltip = { } def relative_widget(child): - # get target widget relative to source widget - # chiefly used for transient modals and parent member functions - # when calling non-adjacent widgets - # positions are relative to grid members + # returns collection of outer widgets relative to source widget + # chiefly used for transient modals and accessing non-adjacent widget methods + # positions are always relative to grid sub-children + # containers and nested buttons should never need to call this function directly + grid = child.get_parent().get_parent() treeview = grid.scrollable_treelist.treeview outer = grid.get_parent() @@ -187,9 +188,9 @@ def relative_widget(child): } supported = [ - "ModSelectionPanel", - "ButtonBox", - "TreeView" + "ModSelectionPanel", # Grid < RightPanel < ModSelectionPanel + "ButtonBox", # Grid < RightPanel < ButtonBox + "TreeView" # Grid < ScrollableTree < TreeView ] if child.__class__.__name__ not in supported: @@ -590,6 +591,7 @@ class ButtonBox(Gtk.Box): button.set_size_request(10, 10) else: button.set_size_request(50,50) + #TODO: explore a more intuitive way of highlighting the active context button.set_opacity(0.6) self.buttons.append(button) button.connect("clicked", self._on_selection_button_clicked) From d6d8dde2aad09d0e2aa01cabeffa87b6b4f45599 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:43:23 +0900 Subject: [PATCH 08/84] feat: iterate mod deletion --- helpers/funcs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/helpers/funcs b/helpers/funcs index 59c3718..9c42da7 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -38,6 +38,7 @@ lock_file="$state_path/$prefix.lock" #CACHE cache_dir="$HOME/.cache/$app_name" _cache_servers="$cache_dir/$prefix.servers" +_cache_mods_temp="$cache_dir/$prefix.mods_temp" _cache_temp="$cache_dir/$prefix.temp" _cache_my_servers="$cache_dir/$prefix.my_servers" _cache_history="$cache_dir/$prefix.history" @@ -580,12 +581,23 @@ parse_server_json(){ } delete_local_mod(){ shift - local symlink="$1" - local dir="$2" - [[ ! -d $workshop_dir/$dir ]] && return 1 - [[ ! -L $game_dir/$symlink ]] && return 1 - #SC2115 - rm -rf "${workshop_dir:?}/$dir" && unlink "$game_dir/$symlink" || return 1 + if [[ -z $1 ]]; then + # use multi mode + readarray -t symlinks < <(awk '{print $1}' $_cache_mods_temp) + readarray -t ids < <(awk '{print $2}' $_cache_mods_temp) + rm "$_cache_mods_temp" + else + local symlink="$1" + local dir="$2" + readarray -t symlinks <<< "$symlink" + readarray -t ids <<< "$dir" + fi + for ((i=0; i<${#symlinks[@]}; i++)); do + [[ ! -d $workshop_dir/${ids[$i]} ]] && return 1 + [[ ! -L $game_dir/${symlinks[$i]} ]] && return 1 + #SC2115 + rm -rf "${workshop_dir:?}/${ids[$i]}" && unlink "$game_dir/${symlinks[$i]}" || return 1 + done } test_cooldown(){ [[ ! -f $_cache_cooldown ]] && return 0 From 2087f4ffa8d4797d563ca5c5989a241db311460b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:52:49 +0900 Subject: [PATCH 09/84] chore: update changelog --- CHANGELOG.md | 10 ++++++++++ dzgui.sh | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0982e75..45f0ba0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [5.6.0-beta.1] 2024-11-12 +### Added +- Bulk delete mods (via 'List installed mods' list). Not compatible with Manual Mod install mode +### Fixed +- Fix for server list truncation causing some servers to not appear in results +- Suppress signal emission when switching menu contexts +- Focus first row when opening mods list +### Changed +- Clarify some error messages and normalize text formatting + ## [5.5.0-beta.5] 2024-11-03 ### Changed - Use updated A2S_RULES logic diff --git a/dzgui.sh b/dzgui.sh index 5a0bac8..3330fc8 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.5.0-beta.5 +version=5.6.0-beta.1 #CONSTANTS aid=221100 @@ -569,10 +569,10 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="dd7aa34df1d374739127cca3033a3f67" + ["ui.py"]="680ff0e4071681f26409fa3592a41e46" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="d8ae2662fbc3c62bdb5a51dec1935705" + ["funcs"]="fa5eb43c454e6bf2903e94884fe64644" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" From 732bc918a76faae2a21c6a134a17c6dcd15ba4eb Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sat, 16 Nov 2024 07:42:52 +0900 Subject: [PATCH 10/84] fix: align local mod signatures --- dzgui.sh | 17 +++++++++++++++-- helpers/funcs | 12 +++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/dzgui.sh b/dzgui.sh index 3330fc8..5af16f5 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.1 +version=5.6.0-beta.2 #CONSTANTS aid=221100 @@ -572,7 +572,7 @@ fetch_helpers_by_sum(){ ["ui.py"]="680ff0e4071681f26409fa3592a41e46" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="fa5eb43c454e6bf2903e94884fe64644" + ["funcs"]="acd5d85b27141082b25e07138f8b5b54" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" @@ -862,6 +862,18 @@ legacy_cols(){ < $cols_file jq '.cols += { "Queue": 120 }' > $cols_file.new && mv $cols_file.new $cols_file } +stale_mod_signatures(){ + local workshop_dir="$steam_path/steamapps/workshop/content/$aid" + if [[ -d $workshop_dir ]]; then + readarray -t old_mod_ids < <(awk -F, '{print $1}' $versions_file) + for ((i=0; i<${#old_mod_ids[@]}; ++i)); do + if [[ ! -d $workshop_dir/${old_mod_ids[$i]} ]]; then + "$HOME/.local/share/$app_name/helpers/funcs" "align_local" "${old_mod_ids[$i]}" + fi + done + fi + +} initial_setup(){ setup_dirs setup_state_files @@ -882,6 +894,7 @@ initial_setup(){ steam_deps migrate_files stale_symlinks + stale_mod_signatures local_latlon is_steam_running is_dzg_downloading diff --git a/helpers/funcs b/helpers/funcs index 9c42da7..0375292 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -117,6 +117,7 @@ declare -A funcs=( ["start_cooldown"]="start_cooldown" ["list_mods"]="list_mods" ["delete"]="delete_local_mod" +["align_local"]="align_versions_file" ["show_server_modlist"]="show_server_modlist" ["test_ping"]="test_ping" ["is_in_favs"]="is_in_favs" @@ -339,7 +340,7 @@ list_mods(){ fi } installed_mods(){ - ls -1 "$workshop_dir" + find "$workshop_dir" -maxdepth 1 -mindepth 1 -printf "%f\n" } local_latlon(){ local url="http://ip-api.com/json/$local_ip" @@ -579,6 +580,14 @@ parse_server_json(){ "\(.addr|split(":")[1])" ' | sort -k1 } +align_versions_file(){ + shift + local mod="$1" + [[ ! -f $versions_file ]] && return + < "$versions_file" awk -F, -v var="$mod" '$1 != var' > $versions_file.new && + mv $versions_file.new $versions_file + logger INFO "Removed local signatures for the mod '$mod'" +} delete_local_mod(){ shift if [[ -z $1 ]]; then @@ -597,6 +606,7 @@ delete_local_mod(){ [[ ! -L $game_dir/${symlinks[$i]} ]] && return 1 #SC2115 rm -rf "${workshop_dir:?}/${ids[$i]}" && unlink "$game_dir/${symlinks[$i]}" || return 1 + align_versions_file "align" "${ids[$i]}" done } test_cooldown(){ From 32a43cd7875c51227fef6a75b4a772f063c6d58b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sat, 16 Nov 2024 11:37:02 +0900 Subject: [PATCH 11/84] chore: update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f0ba0..daeff58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [5.6.0-beta.2] 2024-11-15 +### Fixed +- Clean up local mod signatures from versions file when deleting mods + ## [5.6.0-beta.1] 2024-11-12 ### Added - Bulk delete mods (via 'List installed mods' list). Not compatible with Manual Mod install mode From 8b9f751ff1852713dc9da9edf45e27e36b94045d Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 19 Nov 2024 21:49:59 +0900 Subject: [PATCH 12/84] fix: error handling for no local mods --- CHANGELOG.md | 8 +++++ dzgui.sh | 8 +++-- helpers/funcs | 57 ++++++++++++++++++++++++---------- helpers/ui.py | 85 ++++++++++++++++++++++++++++++++------------------- 4 files changed, 108 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daeff58..cd3f24c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [5.6.0-beta.3] 2024-11-18 +### Fixed +- Improved handling for cases where there are no locally installed mods +- Set up mod symlinks at boot, rather than only on server connect +- Prevent context menus from opening when table is empty +- When reloading table in-place, prevent duplicate panel elements from being added if already present +- Clean up signal emission + ## [5.6.0-beta.2] 2024-11-15 ### Fixed - Clean up local mod signatures from versions file when deleting mods diff --git a/dzgui.sh b/dzgui.sh index 5af16f5..7f1ab2f 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -569,10 +569,10 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="680ff0e4071681f26409fa3592a41e46" + ["ui.py"]="4663cdda7bf91a0c594103d6f4382f15" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="acd5d85b27141082b25e07138f8b5b54" + ["funcs"]="5eae515ea2cac2ab38212a529415e86b" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" @@ -874,6 +874,9 @@ stale_mod_signatures(){ fi } +create_new_links(){ + "$HOME/.local/share/$app_name/helpers/funcs" "update_symlinks" +} initial_setup(){ setup_dirs setup_state_files @@ -895,6 +898,7 @@ initial_setup(){ migrate_files stale_symlinks stale_mod_signatures + create_new_links local_latlon is_steam_running is_dzg_downloading diff --git a/helpers/funcs b/helpers/funcs index 0375292..09037f6 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -116,7 +116,7 @@ declare -A funcs=( ["query_config"]="query_config" ["start_cooldown"]="start_cooldown" ["list_mods"]="list_mods" -["delete"]="delete_local_mod" +["Delete selected mods"]="delete_local_mod" ["align_local"]="align_versions_file" ["show_server_modlist"]="show_server_modlist" ["test_ping"]="test_ping" @@ -131,6 +131,7 @@ declare -A funcs=( ["Handshake"]="final_handshake" ["get_player_count"]="get_player_count" ["lan_scan"]="lan_scan" +["update_symlinks"]="update_symlinks" ) lan_scan(){ @@ -588,19 +589,34 @@ align_versions_file(){ mv $versions_file.new $versions_file logger INFO "Removed local signatures for the mod '$mod'" } +pluralize(){ + local plural="$1" + local count="$2" + local mod + local suffix + local base + local ct + local s + + if [[ "${plural: -2}" == "es" ]]; then + base="${plural::-2}" + suffix="${plural: -2}" + ct=$((count^2)) + [[ $ct -ne 3 ]] && s="$suffix" + else + base="${plural::-1}" + suffix="${plural: -1}" + ct=$((count^1)) + [[ $ct -gt 0 ]] && s="$suffix" + fi + + printf "%s%s" "$base" "$s" +} delete_local_mod(){ shift - if [[ -z $1 ]]; then - # use multi mode - readarray -t symlinks < <(awk '{print $1}' $_cache_mods_temp) - readarray -t ids < <(awk '{print $2}' $_cache_mods_temp) - rm "$_cache_mods_temp" - else - local symlink="$1" - local dir="$2" - readarray -t symlinks <<< "$symlink" - readarray -t ids <<< "$dir" - fi + readarray -t symlinks < <(awk '{print $1}' $_cache_mods_temp) + readarray -t ids < <(awk '{print $2}' $_cache_mods_temp) + rm "$_cache_mods_temp" for ((i=0; i<${#symlinks[@]}; i++)); do [[ ! -d $workshop_dir/${ids[$i]} ]] && return 1 [[ ! -L $game_dir/${symlinks[$i]} ]] && return 1 @@ -608,16 +624,17 @@ delete_local_mod(){ rm -rf "${workshop_dir:?}/${ids[$i]}" && unlink "$game_dir/${symlinks[$i]}" || return 1 align_versions_file "align" "${ids[$i]}" done + printf "Successfully deleted %s %s." "${#symlinks[@]}" "$(pluralize "mods" ${#symlinks[@]})" + return 95 } test_cooldown(){ [[ ! -f $_cache_cooldown ]] && return 0 local old_time=$(< $_cache_cooldown) local cur_time=$(date +%s) local delta=$(($cur_time - $old_time)) - local suffix="seconds" if [[ $delta -lt 60 ]]; then local remains=$((60 - $delta)) - [[ $remains -eq 1 ]] && suffix="second" + local suffix=$(pluralize "seconds" $remains) printf "Global API cooldown in effect. Please wait %s %s." "$remains" "$suffix" exit 1 fi @@ -1104,7 +1121,11 @@ legacy_symlinks(){ unlink "$d" fi done - for d in "$workshop_dir"/*; do + readarray -t mod_dirs < <(find "$workshop_dir" -maxdepth 1 -mindepth 1 -type d) + [[ ${#mod_dirs[@]} -eq 0 ]] && return + for d in "${mod_dirs[@]}"; do + # suppress errors if mods are downloading at boot + [[ ! -f "$d/meta.cpp" ]] && continue local id=$(awk -F"= " '/publishedid/ {print $2}' "$d"/meta.cpp | awk -F\; '{print $1}') local encoded_id=$(echo "$id" | awk '{printf("%c",$1)}' | base64 | sed 's/\//_/g; s/=//g; s/+/]/g') if [[ -h "$game_dir/@$encoded_id" ]]; then @@ -1113,7 +1134,11 @@ legacy_symlinks(){ done } symlinks(){ - for d in "$workshop_dir"/*; do + readarray -t mod_dirs < <(find "$workshop_dir" -maxdepth 1 -mindepth 1 -type d) + [[ ${#mod_dirs[@]} -eq 0 ]] && return + for d in "${mod_dirs[@]}"; do + # suppress errors if mods are downloading at boot + [[ ! -f "$d/meta.cpp" ]] && continue id=$(awk -F"= " '/publishedid/ {print $2}' "$d"/meta.cpp | awk -F\; '{print $1}') encoded_id=$(encode "$id") link="@$encoded_id" diff --git a/helpers/ui.py b/helpers/ui.py index 16cd43e..a6ba4ea 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -330,8 +330,8 @@ def spawn_dialog(transient_parent, msg, mode): def process_shell_return_code(transient_parent, msg, code, original_input): + logger.info("Processing return code '%s' for the input '%s', returned message '%s'" %(code, original_input, msg)) match code: - #TODO: add logger output to each case 0: # success with notice popup spawn_dialog(transient_parent, msg, Popup.NOTIFY) @@ -369,6 +369,13 @@ def process_shell_return_code(transient_parent, msg, code, original_input): transient_parent.grid.update_statusbar(tooltip) spawn_dialog(transient_parent, msg, Popup.NOTIFY) return + case 95: + # reload mods list + spawn_dialog(transient_parent, msg, Popup.NOTIFY) + treeview = transient_parent.grid.scrollable_treelist.treeview + # re-block this signal before redrawing table contents + toggle_signal(treeview, treeview, '_on_keypress', False) + treeview.update_quad_column("List installed mods") case 100: # final handoff before launch final_conf = spawn_dialog(transient_parent, msg, Popup.CONFIRM) @@ -427,6 +434,8 @@ def process_tree_option(input, treeview): else: # non-blocking subprocess subprocess.Popen(['/usr/bin/env', 'bash', funcs, "Open link", command]) + case "Delete selected mods": + call_on_thread(True, context, "Deleting mods", command) case "Handshake": call_on_thread(True, context, "Waiting for DayZ", command) case _: @@ -605,7 +614,9 @@ class ButtonBox(Gtk.Box): # only applicable when returning from mod list grid = widgets["grid"] - grid.right_panel.remove(grid.sel_panel) + grid_last_child = grid.right_panel.get_children()[-1] + if isinstance(grid_last_child, ModSelectionPanel): + grid.right_panel.remove(grid.sel_panel) right_panel = self.get_parent() right_panel.set_filter_visibility(False) @@ -780,15 +791,15 @@ class TreeView(Gtk.TreeView): success_msg = "Successfully deleted the mod '%s'." %(value) fail_msg = "An error occurred during deletion. Aborting." res = spawn_dialog(parent, conf_msg, Popup.CONFIRM) - symlink = self.get_column_at_index(1) - dir = self.get_column_at_index(2) if res == 0: - proc = call_out(parent, "delete", symlink, dir) - if proc.returncode == 0: - spawn_dialog(parent, success_msg, Popup.NOTIFY) - self.update_quad_column("List installed mods") - else: - spawn_dialog(parent, fail_msg, Popup.NOTIFY) + mods = [] + symlink = self.get_column_at_index(1) + dir = self.get_column_at_index(2) + concat = symlink + " " + dir + "\n" + mods.append(concat) + with open(mods_temp_file, "w") as outfile: + outfile.writelines(mods) + process_tree_option(["Delete selected mods", ""], self) case "Open in Steam Workshop": record = self.get_column_at_index(2) call_out(parent, "open_workshop_page", record) @@ -851,6 +862,11 @@ class TreeView(Gtk.TreeView): self.menu.show_all() if event.type is Gdk.EventType.KEY_PRESS and event.keyval is Gdk.KEY_l: + sel = self.get_selection() + sels = sel.get_selected_rows() + (model, pathlist) = sels + if len(pathlist) < 1: + return self.menu.popup_at_widget(widget, Gdk.Gravity.CENTER, Gdk.Gravity.WEST) else: self.menu.popup_at_pointer(event) @@ -973,8 +989,11 @@ class TreeView(Gtk.TreeView): def _focus_first_row(self): path = Gtk.TreePath(0) - it = mod_store.get_iter(path) - self.get_selection().select_path(path) + try: + it = mod_store.get_iter(path) + self.get_selection().select_path(path) + except ValueError: + pass def get_column_at_index(self, index): select = self.get_selection() @@ -1056,31 +1075,42 @@ class TreeView(Gtk.TreeView): def _background_quad(self, dialog, mode): def load(): dialog.destroy() + # detach button panel if store is empty + if isinstance(panel_last_child, ModSelectionPanel): + if total_mods == 0: + right_panel.remove(grid.sel_panel) + grid.show_all() + right_panel.set_filter_visibility(False) self.set_model(mod_store) self.grab_focus() - if abort is False: - size = locale.format_string('%.3f', total_size, grouping=True) - pretty = pluralize("mods", total_mods) - grid.update_statusbar(f"Found {total_mods:n} {pretty} taking up {size} MiB") + size = locale.format_string('%.3f', total_size, grouping=True) + pretty = pluralize("mods", total_mods) + grid.update_statusbar(f"Found {total_mods:n} {pretty} taking up {size} MiB") #2024-11-12 toggle_signal(self, self.selected_row, '_on_tree_selection_changed', True) toggle_signal(self, self, '_on_keypress', True) self._focus_first_row() + if total_mods == 0: + spawn_dialog(self.get_outer_window(), data.stdout, Popup.NOTIFY) - grid = self.get_outer_grid() + widgets = relative_widget(self) + grid = widgets["grid"] right_panel = grid.right_panel - - abort = False - right_panel.set_filter_visibility(False) data = call_out(self, "list_mods", mode) + panel_last_child = right_panel.get_children()[-1] # suppress errors if no mods available on system if data.returncode == 1: - abort = True + total_mods = 0 + total_size = 0 GLib.idle_add(load) - spawn_dialog(self.get_outer_window(), data.stdout, Popup.NOTIFY) return 1 + # attach button panel only if missing (prevents duplication when reloading in-place) + if not isinstance(panel_last_child, ModSelectionPanel): + right_panel.pack_start(grid.sel_panel, False, False, 0) + grid.show_all() + right_panel.set_filter_visibility(False) result = parse_mod_rows(data) total_size = result[0] total_mods = result[1] @@ -1214,10 +1244,6 @@ class TreeView(Gtk.TreeView): if mode == "List installed mods": cols = mod_cols self.set_model(mod_store) - # attach button panel - grid = self.get_parent().get_parent() - grid.right_panel.pack_start(grid.sel_panel, False, False, 0) - grid.show_all() else: cols = log_cols self.set_model(log_store) @@ -1988,12 +2014,7 @@ class ModSelectionPanel(Gtk.Box): mods.append(concat) with open(mods_temp_file, "w") as outfile: outfile.writelines(mods) - proc = call_out(parent, "delete") - if proc.returncode == 0: - spawn_dialog(parent, success_msg, Popup.NOTIFY) - treeview.update_quad_column("List installed mods") - else: - spawn_dialog(parent, fail_msg, Popup.NOTIFY) + process_tree_option(["Delete selected mods", ""], treeview) class FilterPanel(Gtk.Box): def __init__(self): From 651b50ade2df8c8c05d7f8b20fc4177022499165 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 19 Nov 2024 21:51:43 +0900 Subject: [PATCH 13/84] chore: unify time delta methods --- dzgui.sh | 2 +- helpers/ui.py | 16 +++++----------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/dzgui.sh b/dzgui.sh index 7f1ab2f..d95bb3a 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -569,7 +569,7 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="4663cdda7bf91a0c594103d6f4382f15" + ["ui.py"]="353e6fcb8d2c674a3720c0ab8a2b3fd6" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" ["funcs"]="5eae515ea2cac2ab38212a529415e86b" diff --git a/helpers/ui.py b/helpers/ui.py index a6ba4ea..6ef843d 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -12,7 +12,6 @@ import subprocess import sys import textwrap import threading -import time locale.setlocale(locale.LC_ALL, '') gi.require_version("Gtk", "3.0") @@ -22,7 +21,6 @@ from enum import Enum # 5.6.0 app_name = "DZGUI" -start_time = 0 cache = {} config_vals = [] @@ -874,15 +872,11 @@ class TreeView(Gtk.TreeView): def refresh_player_count(self): parent = self.get_outer_window() - global start_time - then = start_time - now = time.monotonic() - diff = now - then - cooldown = 30 - math.floor(diff) - if ((start_time > 0) and (now - then) < 30): - spawn_dialog(parent, "Global refresh cooldown not met. Wait %s second(s)." %(str(cooldown)), Popup.NOTIFY) - return - start_time = now + cooldown = call_out(self, "test_cooldown", "", "") + if cooldown.returncode == 1: + spawn_dialog(self.get_outer_window(), cooldown.stdout, Popup.NOTIFY) + return 1 + call_out(self, "start_cooldown", "", "") thread = threading.Thread(target=self._background_player_count, args=()) thread.start() From 3e9c5a96d90833ddea05a0d78090a2f24673270a Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 19 Nov 2024 21:51:59 +0900 Subject: [PATCH 14/84] chore: bump version --- dzgui.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui.sh b/dzgui.sh index d95bb3a..2f3ea7a 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.2 +version=5.6.0-beta.3 #CONSTANTS aid=221100 From 9fdc123e2c11e28f1fcc985e5ca80ac8db4e7094 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 21 Nov 2024 17:28:36 +0900 Subject: [PATCH 15/84] feat: header bar --- CHANGELOG.md | 10 + dzgui.sh | 8 +- helpers/funcs | 7 +- helpers/ui.py | 876 +++++++++++++++++++++++++++++++++++--------------- 4 files changed, 642 insertions(+), 259 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3f24c..fe598b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [5.6.0-beta.4] 2024-11-20 +### Added +- Application header bar and controls +- Menu context subtitle in header bar +### Changed +- Refactor control flow for more robust contextual parsing +- Stop sending modal dialog hints to outer window +### Fixed +- Favorite server message not updating correctly + ## [5.6.0-beta.3] 2024-11-18 ### Fixed - Improved handling for cases where there are no locally installed mods diff --git a/dzgui.sh b/dzgui.sh index 2f3ea7a..454b930 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.3 +version=5.6.0-beta.4 #CONSTANTS aid=221100 @@ -569,10 +569,10 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="353e6fcb8d2c674a3720c0ab8a2b3fd6" + ["ui.py"]="27ef5c9b811011521c81985ee2b32bb4" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="5eae515ea2cac2ab38212a529415e86b" + ["funcs"]="75afe0be7e73af2fb6a7e423b5ac9159" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" @@ -886,7 +886,7 @@ initial_setup(){ watcher_deps check_architecture test_connection - fetch_helpers > >(pdialog "Checking helper files") +# fetch_helpers > >(pdialog "Checking helper files") varcheck source "$config_file" lock diff --git a/helpers/funcs b/helpers/funcs index 09037f6..6ebacfd 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -115,19 +115,19 @@ declare -A funcs=( ["test_cooldown"]="test_cooldown" ["query_config"]="query_config" ["start_cooldown"]="start_cooldown" -["list_mods"]="list_mods" +["List installed mods"]="list_mods" ["Delete selected mods"]="delete_local_mod" ["align_local"]="align_versions_file" ["show_server_modlist"]="show_server_modlist" ["test_ping"]="test_ping" ["is_in_favs"]="is_in_favs" ["show_log"]="show_log" -["gen_log"]="generate_log" +["Output system info to log file"]="generate_log" ["open_workshop_page"]="open_workshop_page" ["Add to my servers"]="update_favs_from_table" ["Remove from my servers"]="update_favs_from_table" ["Remove from history"]="remove_from_history" -["force_update"]="force_update" +["Force update local mods"]="force_update" ["Handshake"]="final_handshake" ["get_player_count"]="get_player_count" ["lan_scan"]="lan_scan" @@ -236,6 +236,7 @@ add_record(){ fav_label=$(<<< "$res" jq -r '.[].name') fav_server="$record" update_config + echo "Updated favorite server to '$fav_server' ($fav_label)" return 90 ;; "Add server by ID") diff --git a/helpers/ui.py b/helpers/ui.py index 6ef843d..123962f 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -21,7 +21,6 @@ from enum import Enum # 5.6.0 app_name = "DZGUI" - cache = {} config_vals = [] stored_keys = [] @@ -86,41 +85,6 @@ log_cols = [ "Traceback", "Message" ] -connect = [ - ("Server browser",), - ("My saved servers",), - ("Quick-connect to favorite server",), - ("Recent servers",), - ("Connect by IP",), - ("Connect by ID",), - ("Scan LAN servers",) -] -manage = [ - ("Add server by IP",), - ("Add server by ID",), - ("Change favorite server",), -] -options = [ - ("List installed mods",), - ("Toggle release branch",), - ("Toggle mod install mode",), - ("Toggle Steam/Flatpak",), - ("Toggle DZGUI fullscreen boot",), - ("Change player name",), - ("Change Steam API key",), - ("Change Battlemetrics API key",), - ("Force update local mods",), - ("Output system info to log file",) -] -help = [ - ("View changelog",), - ("Show debug log",), - ("Help file ⧉",), - ("Report a bug ⧉",), - ("Forum ⧉",), - ("Sponsor ⧉",), - ("Hall of fame ⧉",), -] filters = { "1PP": True, "3PP": True, @@ -132,42 +96,338 @@ filters = { "Non-ASCII": False, "Duplicate": False } -side_buttons = [ - "Main menu", - "Manage", - "Options", - "Help", - "Exit" -] -status_tooltip = { - "Server browser": "Used to browse the global server list", - "My saved servers": "Browse your saved servers. Unreachable/offline servers will be excluded", - "Quick-connect to favorite server": "Connect to your favorite server", - "Recent servers": "Shows the last 10 servers you connected to (includes attempts)", - "Connect by IP": "Connect to a server by IP", - "Connect by ID": "Connect to a server by Battlemetrics ID", - "Scan LAN servers": "Search for servers on your local network", - "Add server by IP": "Add a server by IP", - "Add server by ID": "Add a server by Battlemetrics ID", - "Change favorite server": "Update your quick-connect server", - "List installed mods": "Browse a list of locally-installed mods", - "Toggle release branch": "Switch between stable and testing branches", - "Toggle mod install mode": "Switch between manual and auto mod installation", - "Toggle Steam/Flatpak": "Switch the preferred client to use for launching DayZ", - "Toggle DZGUI fullscreen boot": "Whether to start DZGUI as a maximized window (desktop only)", - "Change player name": "Update your in-game name (required by some servers)", - "Change Steam API key": "Can be used if you revoked an old API key", - "Change Battlemetrics API key": "Can be used if you revoked an old API key", - "Force update local mods": "Synchronize the signatures of all local mods with remote versions (experimental)", - "Output system info to log file": "Generates a system log for troubleshooting", - "View changelog": "Opens the DZGUI changelog in a dialog window", - "Show debug log": "Read the DZGUI log generated since startup", - "Help file ⧉": "Opens the DZGUI documentation in a browser", - "Report a bug ⧉": "Opens the DZGUI issue tracker in a browser", - "Forum ⧉": "Opens the DZGUI discussion forum in a browser", - "Sponsor ⧉": "Sponsor the developer of DZGUI", - "Hall of fame ⧉": "A list of significant contributors and testers", -} + + +class EnumWithAttrs(Enum): + + def __new__(cls, *args, **kwds): + value = len(cls.__members__) + 1 + obj = object.__new__(cls) + obj._value_ = value + return obj + def __init__(self, a): + self.dict = a + + +class RowType(EnumWithAttrs): + @classmethod + def str2rowtype(cls, str): + for member in cls: + if str == member.dict["label"]: + return member + return RowType.DYNAMIC + + DYNAMIC = { + "label": None, + "tooltip": None, + } + HANDSHAKE = { + "label": "Handshake", + "tooltip": None, + "wait_msg": "Waiting for DayZ" + } + DELETE_SELECTED = { + "label": "Delete selected mods", + "tooltip": None, + "wait_msg": "Deleting mods" + } + SERVER_BROWSER = { + "label": "Server browser", + "tooltip": "Used to browse the global server list", + } + SAVED_SERVERS = { + "label": "My saved servers", + "tooltip": "Browse your saved servers. Unreachable/offline servers will be excluded", + } + QUICK_CONNECT = { + "label": "Quick-connect to favorite server", + "tooltip": "Connect to your favorite server", + "wait_msg": "Working", + "default": "unset", + "alt": None, + "val": "fav_label" + } + RECENT_SERVERS = { + "label": "Recent servers", + "tooltip": "Shows the last to servers you connected to (includes attempts)", + } + CONN_BY_IP = { + "label": "Connect by IP", + "tooltip": "Connect to a server by IP", + "prompt": "Enter IP in IP:Queryport format (e.g. 192.168.1.1:27016)", + "link_label": None, + } + CONN_BY_ID = { + "label": "Connect by ID", + "tooltip": "Connect to a server by Battlemetrics ID", + "prompt": "Enter server ID", + "link_label": "Open Battlemetrics", + } + SCAN_LAN = { + "label": "Scan LAN servers", + "tooltip": "Search for servers on your local network" + } + ADD_BY_IP = { + "label": "Add server by IP", + "tooltip": "Add a server by IP", + "prompt": "Enter IP in IP:Queryport format (e.g. 192.168.1.1:27016)", + "link_label": None, + } + ADD_BY_ID = { + "label": "Add server by ID", + "tooltip": "Add a server by Battlemetrics ID", + "prompt": "Enter server ID", + "link_label": "Open Battlemetrics", + } + CHNG_FAV = { + "label": "Change favorite server", + "tooltip": "Update your quick-connect server", + "prompt": "Enter IP in IP:Queryport format (e.g. 192.168.1.1:27016)", + "link_label": None, + "alt": None, + "default": "unset", + "val": "fav_label" + } + LIST_MODS = { + "label": "List installed mods", + "tooltip": "Browse a list of locally-installed mods", + "quad_label": "Mods" + } + TGL_BRANCH = { + "label": "Toggle release branch", + "tooltip": "Switch between stable and testing branches", + "default": None, + "val": "branch" + } + TGL_INSTALL = { + "label": "Toggle mod install mode", + "tooltip": "Switch between manual and auto mod installation", + "default": "manual", + "alt": "auto", + "val": "auto_install" + } + TGL_STEAM = { + "label": "Toggle Steam/Flatpak", + "tooltip": "Switch the preferred client to use for launching DayZ", + "alt": None, + "default": None, + "val": "preferred_client" + } + TGL_FULLSCREEN = { + "label": "Toggle DZGUI fullscreen boot", + "tooltip": "Whether to start DZGUI as a maximized window (desktop only)", + "alt": "true", + "default": "false", + "val": "fullscreen" + } + CHNG_PLAYER = { + "label": "Change player name", + "tooltip": "Update your in-game name (required by some servers)", + "prompt": "Enter new nickname", + "link_label": None, + "alt": None, + "default": None, + "val": "name" + } + CHNG_STEAM_API = { + "label": "Change Steam API key", + "tooltip": "Can be used if you revoked an old API key", + "prompt": "Enter new API key", + "link_label": "Open Steam API page", + } + CHNG_BM_API = { + "label": "Change Battlemetrics API key", + "tooltip": "Can be used if you revoked an old API key", + "link_label": "Open Battlemetrics API page", + "prompt": "Enter new API key", + } + FORCE_UPDATE = { + "label": "Force update local mods", + "tooltip": "Synchronize the signatures of all local mods with remote versions (experimental)", + "wait_msg": "Updating mods" + } + DUMP_LOG = { + "label": "Output system info to log file", + "tooltip": "Dump diagnostic data for troubleshooting", + "wait_msg": "Generating log" + } + CHANGELOG = { + "label": "View changelog", + "tooltip": "Opens the DZGUI changelog in a dialog window" + } + SHOW_LOG = { + "label": "Show debug log", + "tooltip": "Read the DZGUI log generated since startup", + "quad_label": "Debug log" + } + DOCS = { + "label": "Help file ⧉", + "tooltip": "Opens the DZGUI documentation in a browser" + } + BUGS = { + "label": "Report a bug ⧉", + "tooltip": "Opens the DZGUI issue tracker in a browser" + } + FORUM = { + "label": "Forum ⧉", + "tooltip": "Opens the DZGUI discussion forum in a browser" + } + SPONSOR = { + "label": "Sponsor ⧉", + "tooltip": "Sponsor the developer of DZGUI" + } + HOF = { + "label": "Hall of fame ⧉", + "tooltip": "A list of significant contributors and testers" + } + + +class WindowContext(EnumWithAttrs): + @classmethod + def row2con(cls, row): + m = None + for member in cls: + if row in member.dict["rows"]: + m = member + elif row in member.dict["called_by"]: + m = member + else: + continue + return m + + + MAIN_MENU = { + "label": "", + "rows": [ + RowType.SERVER_BROWSER, + RowType.SAVED_SERVERS, + RowType.QUICK_CONNECT, + RowType.RECENT_SERVERS, + RowType.CONN_BY_IP, + RowType.CONN_BY_ID, + RowType.SCAN_LAN + ], + "called_by": [] + } + MANAGE = { + "label": "Manage", + "rows": [ + RowType.ADD_BY_IP, + RowType.ADD_BY_ID, + RowType.CHNG_FAV + ], + "called_by": [] + } + OPTIONS = { + "label": "Options", + "rows":[ + RowType.LIST_MODS, + RowType.TGL_BRANCH, + RowType.TGL_INSTALL, + RowType.TGL_STEAM, + RowType.TGL_FULLSCREEN, + RowType.CHNG_PLAYER, + RowType.CHNG_STEAM_API, + RowType.CHNG_BM_API, + RowType.FORCE_UPDATE, + RowType.DUMP_LOG + ], + "called_by": [] + } + HELP = { + "label": "Help", + "rows":[ + RowType.CHANGELOG, + RowType.SHOW_LOG, + RowType.DOCS, + RowType.BUGS, + RowType.FORUM, + RowType.SPONSOR, + RowType.HOF + ], + "called_by": [] + } + # inner server contexts + TABLE_API = { + "label": "", + "rows": [], + "called_by": [ + RowType.SERVER_BROWSER + ], + } + TABLE_SERVER = { + "label": "", + "rows": [], + "called_by": [ + RowType.SAVED_SERVERS, + RowType.RECENT_SERVERS, + RowType.SCAN_LAN + ], + } + TABLE_MODS = { + "label": "", + "rows": [], + "called_by": [ + RowType.LIST_MODS, + ], + } + TABLE_LOG = { + "label": "", + "rows": [], + "called_by": [ + RowType.SHOW_LOG + ], + } + + +class WidgetType(Enum): + OUTER_WIN = 1 + TREEVIEW = 2 + GRID = 3 + RIGHT_PANEL = 4 + MOD_PANEL = 5 + FILTER_PANEL = 6 + + +class Port(Enum): + DEFAULT = 1 + CUSTOM = 2 + + +class Popup(Enum): + WAIT = 1 + NOTIFY = 2 + CONFIRM = 3 + ENTRY = 4 + + +class ButtonType(EnumWithAttrs): + MAIN_MENU = {"label": "Main menu", + "opens": WindowContext.MAIN_MENU + } + MANAGE = {"label": "Manage", + "opens": WindowContext.MANAGE + } + OPTIONS = {"label": "Options", + "opens": WindowContext.OPTIONS + } + HELP = {"label": "Help", + "opens": WindowContext.HELP + } + EXIT = {"label": "Exit", + "opens": None + } + + +class EnumeratedButton(Gtk.Button): + @GObject.Property + def button_type(self): + return self._button_type + + @button_type.setter + def button_type(self, value): + self._button_type = value + def relative_widget(child): # returns collection of outer widgets relative to source widget @@ -196,6 +456,7 @@ def relative_widget(child): return widgets + def pluralize(plural, count): suffix = plural[-2:] if suffix == "es": @@ -205,10 +466,12 @@ def pluralize(plural, count): base = plural[:-1] return f"%s{'s'[:count^1]}" %(base) + def format_ping(ping): ms = " | Ping: %s" %(ping) return ms + def format_distance(distance): if distance == "Unknown": distance = "| Distance: %s" %(distance) @@ -237,6 +500,7 @@ def parse_modlist_rows(data): modlist_store.append(row) return hits + def parse_log_rows(data): lines = data.stdout.splitlines() reader = csv.reader(lines, delimiter=delimiter) @@ -247,6 +511,7 @@ def parse_log_rows(data): for row in rows: log_store.append(row) + def parse_mod_rows(data): # GTK pads trailing zeroes on floats # https://stackoverflow.com/questions/26827434/gtk-cellrenderertext-with-format @@ -347,12 +612,13 @@ def process_shell_return_code(transient_parent, msg, code, original_input): # for BM only spawn_dialog(transient_parent, msg, Popup.NOTIFY) treeview = transient_parent.grid.scrollable_treelist.treeview - process_tree_option(["Options", "Change Battlemetrics API key"], treeview) + process_tree_option([treeview.view, RowType.CHNG_BM_API], treeview) case 5: # for steam only + # deprecated, Steam is mandatory now spawn_dialog(transient_parent, msg, Popup.NOTIFY) treeview = transient_parent.grid.scrollable_treelist.treeview - process_tree_option(["Options", "Change Steam API key"], treeview) + process_tree_option([treeview.view, RowType.CHNG_STEAM_API], treeview) case 6: # return silently pass @@ -373,14 +639,14 @@ def process_shell_return_code(transient_parent, msg, code, original_input): treeview = transient_parent.grid.scrollable_treelist.treeview # re-block this signal before redrawing table contents toggle_signal(treeview, treeview, '_on_keypress', False) - treeview.update_quad_column("List installed mods") + treeview.update_quad_column(RowType.LIST_MODS) case 100: # final handoff before launch final_conf = spawn_dialog(transient_parent, msg, Popup.CONFIRM) treeview = transient_parent.grid.scrollable_treelist.treeview if final_conf == 1 or final_conf is None: return - process_tree_option(["Handshake", ""], treeview) + process_tree_option([treeview.view, RowType.HANDSHAKE], treeview) case 255: spawn_dialog(transient_parent, "Update complete. Please close DZGUI and restart.", Popup.NOTIFY) Gtk.main_quit() @@ -389,22 +655,22 @@ def process_shell_return_code(transient_parent, msg, code, original_input): def process_tree_option(input, treeview): context = input[0] command = input[1] + cmd_string = command.dict["label"] logger.info("Parsing tree option '%s' for the context '%s'" %(command, context)) - transient_parent = treeview.get_outer_window() - toggle_contexts = [ - "Toggle mod install mode", - "Toggle release branch", - "Toggle Steam/Flatpak", - "Toggle DZGUI fullscreen boot" - ] + widgets = relative_widget(treeview) + transient_parent = widgets["outer"] + grid = widgets["grid"] def call_on_thread(bool, subproc, msg, args): def _background(subproc, args, dialog): def _load(): wait_dialog.destroy() out = proc.stdout.splitlines() - msg = out[-1] + try: + msg = out[-1] + except: + msg = '' rc = proc.returncode logger.info("Subprocess returned code %s with message '%s'" %(rc, msg)) process_shell_return_code(transient_parent, msg, rc, input) @@ -423,68 +689,82 @@ def process_tree_option(input, treeview): msg = out[-1] process_shell_return_code(transient_parent, msg, rc, input) - match context: - case "Help": - if command == "View changelog": - diag = ChangelogDialog(transient_parent, '', "Changelog -- content can be scrolled") + # help pages + if context == WindowContext.HELP: + match command: + case RowType.CHANGELOG: + diag = ChangelogDialog(transient_parent) diag.run() diag.destroy() - else: - # non-blocking subprocess - subprocess.Popen(['/usr/bin/env', 'bash', funcs, "Open link", command]) - case "Delete selected mods": - call_on_thread(True, context, "Deleting mods", command) - case "Handshake": - call_on_thread(True, context, "Waiting for DayZ", command) - case _: - if command == "Output system info to log file": - call_on_thread(True, "gen_log", "Generating log", "") - elif command == "Force update local mods": - call_on_thread(True, "force_update", "Updating mods", "") - elif command == "Quick-connect to favorite server": - call_on_thread(True, command, "Working", "") - elif command in toggle_contexts: - if command == "Toggle release branch": - call_on_thread(False, "toggle", "Updating DZGUI branch", command) - else: - proc = call_out(transient_parent, "toggle", command) - grid = treeview.get_parent().get_parent() - grid.update_right_statusbar() - tooltip = format_metadata(command) - transient_parent.grid.update_statusbar(tooltip) - else: - # This branch is only used by interactive dialogs - match command: - case "Connect by IP" | "Add server by IP" | "Change favorite server": - flag = True - link_label = "" - prompt = "Enter IP in IP:Queryport format (e.g. 192.168.1.1:27016)" - case "Connect by ID" | "Add server by ID": - flag = True - link_label = "Open Battlemetrics" - prompt = "Enter server ID" - case "Change player name": - flag = False - link_label = "" - prompt = "Enter new nickname" - case "Change Steam API key": - flag = True - link_label = "Open Steam API page" - prompt = "Enter new API key" - case "Change Battlemetrics API key": - flag = True - link_label = "Open Battlemetrics API page" - prompt = "Enter new API key" + case _: + base_cmd = "Open link" + arg_string = cmd_string + subprocess.Popen(['/usr/bin/env', 'bash', funcs, base_cmd, arg_string]) + pass + return - user_entry = EntryDialog(transient_parent, prompt, Popup.ENTRY, link_label) - res = user_entry.get_input() - if res is None: - logger.info("User aborted entry dialog") - return - logger.info("User entered: '%s'" %(res)) + # config metadata toggles + toggle_commands = [ + RowType.TGL_INSTALL, + RowType.TGL_BRANCH, + RowType.TGL_STEAM, + RowType.TGL_FULLSCREEN + ] - call_on_thread(flag, command, "Working", res) + if command in toggle_commands: + match command: + case RowType.TGL_BRANCH: + wait_msg = "Updating DZGUI branch" + call_on_thread(False, "toggle", wait_msg, cmd_string) + case _: + proc = call_out(transient_parent, "toggle", cmd_string) + grid.update_right_statusbar() + tooltip = format_metadata(command.dict["label"]) + transient_parent.grid.update_statusbar(tooltip) + return + # entry dialogs + interactive_commands = [ + RowType.CONN_BY_IP, + RowType.CONN_BY_ID, + RowType.ADD_BY_IP, + RowType.ADD_BY_ID, + RowType.CHNG_FAV, + RowType.CHNG_PLAYER, + RowType.CHNG_STEAM_API, + RowType.CHNG_BM_API + ] + + if command in interactive_commands: + prompt = command.dict["prompt"] + flag = True + link_label = command.dict["link_label"] + wait_msg = "Working" + + user_entry = EntryDialog(transient_parent, prompt, Popup.ENTRY, link_label) + res = user_entry.get_input() + + if res is None: + logger.info("User aborted entry dialog") + return + logger.info("User entered: '%s'" %(res)) + + if command == RowType.CHNG_PLAYER: flag = False + call_on_thread(flag, cmd_string, wait_msg, res) + return + + # standalone commands + misc_commands = [ + RowType.DELETE_SELECTED, + RowType.HANDSHAKE, + RowType.DUMP_LOG, + RowType.FORCE_UPDATE, + RowType.QUICK_CONNECT + ] + if command in misc_commands: + wait_msg = command.dict["wait_msg"] + call_on_thread(True, cmd_string, wait_msg, '') + return def reinit_checks(): toggled_checks.clear() @@ -498,18 +778,30 @@ def reinit_checks(): class OuterWindow(Gtk.Window): + @GObject.Property + def widget_type(self): + return self._widget_type + + @widget_type.setter + def widget_type(self, value): + self._widget_type = value + def __init__(self, is_steam_deck, is_game_mode): super().__init__(title=app_name) - self.connect("delete-event", self.halt_proc_and_quit) - # Deprecated in GTK 4.0 - self.set_border_width(10) - self.set_type_hint(Gdk.WindowTypeHint.DIALOG) + self.hb = AppHeaderBar() + # steam deck taskbar may occlude elements + if is_steam_deck is False: + self.set_titlebar(self.hb) + + self.set_property("widget_type", WidgetType.OUTER_WIN) + + self.connect("delete-event", self.halt_proc_and_quit) + self.set_border_width(10) + + #app > win > grid > scrollable > treeview [row/server/mod store] + #app > win > grid > vbox > buttonbox > filterpanel > combo [map store] - """ - app > win > grid > scrollable > treeview [row/server/mod store] - app > win > grid > vbox > buttonbox > filterpanel > combo [map store] - """ self.grid = Grid(is_steam_deck) self.add(self.grid) if is_game_mode is True: @@ -521,6 +813,7 @@ class OuterWindow(Gtk.Window): # Hide FilterPanel on main menu self.show_all() self.grid.right_panel.set_filter_visibility(False) + self.grid.sel_panel.set_visible(False) self.grid.scrollable_treelist.treeview.grab_focus() def halt_proc_and_quit(self, window, event): @@ -592,8 +885,11 @@ class ButtonBox(Gtk.Box): set_surrounding_margins(self, 10) self.buttons = list() - for side_button in side_buttons: - button = Gtk.Button(label=side_button) + self.is_steam_deck = is_steam_deck + + for side_button in ButtonType: + button = EnumeratedButton(label=side_button.dict["label"]) + button.set_property("button_type", side_button) if is_steam_deck is True: button.set_size_request(10, 10) else: @@ -614,14 +910,14 @@ class ButtonBox(Gtk.Box): grid = widgets["grid"] grid_last_child = grid.right_panel.get_children()[-1] if isinstance(grid_last_child, ModSelectionPanel): - grid.right_panel.remove(grid.sel_panel) + grid.sel_panel.set_visible(False) right_panel = self.get_parent() right_panel.set_filter_visibility(False) treeview = widgets["treeview"] treeview.set_selection_mode(Gtk.SelectionMode.SINGLE) - """Block maps combo when returning to main menu""" + # Block maps combo when returning to main menu toggle_signal(right_panel.filters_vbox, right_panel.filters_vbox.maps_combo, '_on_map_changed', False) right_panel.filters_vbox.keyword_entry.set_text("") keyword_filter.clear() @@ -630,38 +926,49 @@ class ButtonBox(Gtk.Box): for column in treeview.get_columns(): treeview.remove_column(column) - for i, column_title in enumerate([context]): + # used as a convenience for Steam Deck if it has no titlebar + for i, column_title in enumerate([context.dict["label"]]): renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn(column_title, renderer, text=i) treeview.append_column(column) - self._populate(context) + + if self.is_steam_deck is False: + treeview.set_headers_visible(False) + + self._populate(context.dict["opens"]) toggle_signal(treeview, treeview, '_on_keypress', False) treeview.set_model(row_store) treeview.grab_focus() def _populate(self, context): - match context: - case 'Manage': array_context = manage - case 'Main menu': array_context = connect - case 'Options': array_context = options - case 'Help': array_context = help - row_store.clear() - status = array_context[0][0] - treeview = self.get_treeview() - grid = self.get_parent().get_parent() + widgets = relative_widget(self) + treeview = widgets["treeview"] + grid = widgets["grid"] + window = widgets["outer"] - for items in array_context: - row_store.append(list(items)) - grid.update_statusbar(status_tooltip[status]) + # set global window context + treeview.view = context + + row_store.clear() + array = context.dict["rows"] + + window.hb.set_subtitle(context.dict["label"]) + + for item in array: + label = item.dict["label"] + tooltip = item.dict["tooltip"] + t = (label, ) + row_store.append(t) + grid.update_statusbar(tooltip) treeview.grab_focus() def _on_selection_button_clicked(self, button): treeview = self.get_treeview() toggle_signal(treeview, treeview.selected_row, '_on_tree_selection_changed', False) - context = button.get_label() + context = button.get_property("button_type") logger.info("User clicked '%s'" %(context)) - if context == "Exit": + if context == ButtonType.EXIT: logger.info("Normal user exit") Gtk.main_quit() return @@ -676,9 +983,10 @@ class ButtonBox(Gtk.Box): button.set_opacity(1.0) for col in cols: - col.set_title(context) + col.set_title(context.dict["label"]) - self._populate(context) + # get destination WindowContext enum from button + self._populate(context.dict["opens"]) toggle_signal(treeview, treeview.selected_row, '_on_tree_selection_changed', True) @@ -714,10 +1022,20 @@ class CalcDist(multiprocessing.Process): class TreeView(Gtk.TreeView): __gsignals__ = {"on_distcalc_started": (GObject.SignalFlags.RUN_FIRST, None, ())} + @GObject.Property + def widget_type(self): + return self._widget_type + + @widget_type.setter + def widget_type(self, value): + self._widget_type = value def __init__(self, is_steam_deck): super().__init__() + self.set_property("widget_type", WidgetType.TREEVIEW) + self.view = WindowContext.MAIN_MENU + self.queue = multiprocessing.Queue() self.current_proc = None @@ -726,8 +1044,10 @@ class TreeView(Gtk.TreeView): self.set_search_column(-1) # Populate model with initial context - for rows in connect: - row_store.append(list(rows)) + for row in WindowContext.MAIN_MENU.dict["rows"]: + label = row.dict["label"] + t = (label,) + row_store.append(t) self.set_model(row_store) for i, column_title in enumerate( @@ -737,6 +1057,8 @@ class TreeView(Gtk.TreeView): column = Gtk.TreeViewColumn(column_title, renderer, text=i) self.append_column(column) + if is_steam_deck is False: + self.set_headers_visible(False) self.connect("row-activated", self._on_row_activated) self.connect("key-press-event", self._on_keypress) self.connect("key-press-event", self._on_keypress_main_menu) @@ -751,6 +1073,7 @@ class TreeView(Gtk.TreeView): self.current_proc.terminate() def _on_menu_click(self, menu_item): + #TODO: context menus use old stringwise parsing parent = self.get_outer_window() context = self.get_first_col() value = self.get_column_at_index(0) @@ -797,7 +1120,7 @@ class TreeView(Gtk.TreeView): mods.append(concat) with open(mods_temp_file, "w") as outfile: outfile.writelines(mods) - process_tree_option(["Delete selected mods", ""], self) + process_tree_option([self.view, RowType.DELETE_SELECTED], self) case "Open in Steam Workshop": record = self.get_column_at_index(2) call_out(parent, "open_workshop_page", record) @@ -832,12 +1155,19 @@ class TreeView(Gtk.TreeView): self.menu = Gtk.Menu() mod_context_items = ["Open in Steam Workshop", "Delete mod"] - subcontext_items = {"Server browser": ["Add to my servers", "Copy IP to clipboard", "Show server-side mods", "Refresh player count"], - "My saved servers": ["Remove from my servers", "Copy IP to clipboard", "Show server-side mods", "Refresh player count"], - "Recent servers": ["Add to my servers", "Remove from history", "Copy IP to clipboard", "Show server-side mods", "Refresh player count"], + subcontext_items = { + "Server browser": + ["Add to my servers", "Copy IP to clipboard", "Show server-side mods", "Refresh player count"], + "My saved servers": + ["Remove from my servers", "Copy IP to clipboard", "Show server-side mods", "Refresh player count"], + "Recent servers": + ["Add to my servers", "Remove from history", "Copy IP to clipboard", "Show server-side mods", "Refresh player count"], } # submenu hierarchy https://stackoverflow.com/questions/52847909/how-to-add-a-sub-menu-to-a-gtk-menu - if context == "Mod": + + if self.view == WindowContext.TABLE_LOG: + return + if self.view == WindowContext.TABLE_MODS: items = mod_context_items subcontext = "List installed mods" elif "Name" in context: @@ -908,7 +1238,7 @@ class TreeView(Gtk.TreeView): if self.current_proc and self.current_proc.is_alive(): self.current_proc.terminate() - if "Name" in context: + if self.view == WindowContext.TABLE_API or self.view == WindowContext.TABLE_SERVER: addr = self.get_column_at_index(7) if addr is None: return @@ -1067,20 +1397,24 @@ class TreeView(Gtk.TreeView): GLib.idle_add(loadTable) def _background_quad(self, dialog, mode): + # currently only used by list mods method def load(): dialog.destroy() - # detach button panel if store is empty + # suppress button panel if store is empty if isinstance(panel_last_child, ModSelectionPanel): if total_mods == 0: - right_panel.remove(grid.sel_panel) + # nb. do not forcibly remove previously added widgets + grid.sel_panel.set_visible(False) grid.show_all() right_panel.set_filter_visibility(False) + + grid.sel_panel.set_visible(True) self.set_model(mod_store) self.grab_focus() size = locale.format_string('%.3f', total_size, grouping=True) pretty = pluralize("mods", total_mods) grid.update_statusbar(f"Found {total_mods:n} {pretty} taking up {size} MiB") - #2024-11-12 + toggle_signal(self, self.selected_row, '_on_tree_selection_changed', True) toggle_signal(self, self, '_on_keypress', True) self._focus_first_row() @@ -1090,7 +1424,7 @@ class TreeView(Gtk.TreeView): widgets = relative_widget(self) grid = widgets["grid"] right_panel = grid.right_panel - data = call_out(self, "list_mods", mode) + data = call_out(self, mode.dict["label"], '') panel_last_child = right_panel.get_children()[-1] # suppress errors if no mods available on system @@ -1100,11 +1434,9 @@ class TreeView(Gtk.TreeView): GLib.idle_add(load) return 1 - # attach button panel only if missing (prevents duplication when reloading in-place) + # show button panel missing (prevents duplication when reloading in-place) if not isinstance(panel_last_child, ModSelectionPanel): - right_panel.pack_start(grid.sel_panel, False, False, 0) - grid.show_all() - right_panel.set_filter_visibility(False) + grid.sel_panel.set_visible(True) result = parse_mod_rows(data) total_size = result[0] total_mods = result[1] @@ -1121,6 +1453,7 @@ class TreeView(Gtk.TreeView): title = col.get_title() size = col.get_width() + # steam deck column title workaround if "Name" in title: title = "Name" @@ -1140,6 +1473,8 @@ class TreeView(Gtk.TreeView): def _update_multi_column(self, mode): # Local server lists may have different filter toggles from remote list # FIXME: tree selection updates twice here. attach signal later + self.set_headers_visible(True) + toggle_signal(self, self.selected_row, '_on_tree_selection_changed', False) for column in self.get_columns(): self.remove_column(column) @@ -1175,8 +1510,14 @@ class TreeView(Gtk.TreeView): self.append_column(column) - self.update_first_col(mode) - transient_parent = self.get_outer_window() + self.update_first_col(mode.dict["label"]) + + widgets = relative_widget(self) + grid = widgets["grid"] + window = widgets["outer"] + window.hb.set_subtitle(mode.dict["label"]) + + transient_parent = window # Reset map selection selected_map.clear() @@ -1188,20 +1529,21 @@ class TreeView(Gtk.TreeView): toggle_signal(self.get_outer_grid().right_panel.filters_vbox, check, '_on_check_toggle', True) toggle_signal(self, self, '_on_keypress', True) - if mode == "Scan LAN servers": + string = mode.dict["label"] + if mode == RowType.SCAN_LAN: lan_dialog = LanButtonDialog(self.get_outer_window()) port = lan_dialog.get_selected_port() if port is None: grid = self.get_outer_grid() right_panel = grid.right_panel vbox = right_panel.button_vbox - vbox._update_single_column("Main menu") + vbox._update_single_column(ButtonType.MAIN_MENU) return - mode = mode + ":" + port + string = string + ":" + port wait_dialog = GenericDialog(transient_parent, "Fetching server metadata", Popup.WAIT) wait_dialog.show_all() - thread = threading.Thread(target=self._background, args=(wait_dialog, mode)) + thread = threading.Thread(target=self._background, args=(wait_dialog, string)) thread.start() def update_first_col(self, title): @@ -1232,10 +1574,11 @@ class TreeView(Gtk.TreeView): for column in self.get_columns(): self.remove_column(column) + self.set_headers_visible(True) mod_store.clear() log_store.clear() - if mode == "List installed mods": + if mode == RowType.LIST_MODS: cols = mod_cols self.set_model(mod_store) else: @@ -1245,24 +1588,33 @@ class TreeView(Gtk.TreeView): for i, column_title in enumerate(cols): renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn(column_title, renderer, text=i) - if mode == "List installed mods": + if mode == RowType.LIST_MODS: if i == 3: column.set_cell_data_func(renderer, self._format_float, func_data=None) column.set_sort_column_id(i) self.append_column(column) - if mode == "List installed mods": + widgets = relative_widget(self) + grid = widgets["grid"] + window = widgets["outer"] + try: + window.hb.set_subtitle(mode.dict["quad_label"]) + except KeyError: + window.hb.set_subtitle(mode.dict["label"]) + + if mode == RowType.LIST_MODS: self.set_selection_mode(Gtk.SelectionMode.MULTIPLE) else: + # short circuit and jump to debug log data = call_out(self, "show_log") res = parse_log_rows(data) + toggle_signal(self, self, '_on_keypress', True) if res == 1: spawn_dialog(self.get_outer_window(), "Failed to load log file, possibly corrupted", Popup.NOTIFY) return - transient_parent = self.get_outer_window() - wait_dialog = GenericDialog(transient_parent, "Checking mods", Popup.WAIT) + wait_dialog = GenericDialog(window, "Checking mods", Popup.WAIT) wait_dialog.show_all() thread = threading.Thread(target=self._background_quad, args=(wait_dialog, mode)) thread.start() @@ -1293,23 +1645,58 @@ class TreeView(Gtk.TreeView): def _on_row_activated(self, treeview, tree_iter, col): context = self.get_first_col() chosen_row = self.get_column_at_index(0) - output = context, chosen_row - if context == "Mod" or context == "Timestamp": + + # recycled from ModDialog + if self.view == WindowContext.TABLE_MODS: + select = treeview.get_selection() + sels = select.get_selected_rows() + (model, pathlist) = sels + if len(pathlist) < 1: + return + path = pathlist[0] + tree_iter = model.get_iter(path) + mod_id = model.get_value(tree_iter, 2) + base_cmd = "open_workshop_page" + subprocess.Popen(['/usr/bin/env', 'bash', funcs, base_cmd, mod_id]) return + + dynamic_contexts = [ + WindowContext.TABLE_LOG, + WindowContext.TABLE_SERVER, + WindowContext.TABLE_API + ] + + # if already in table, the row selection is arbitrary + if self.view in dynamic_contexts: + cr = RowType.DYNAMIC + else: + cr = RowType.str2rowtype(chosen_row) + wc = WindowContext.row2con(cr) + self.view = wc + + output = self.view, cr logger.info("User selected '%s' for the context '%s'" %(chosen_row, context)) + if self.view == WindowContext.TABLE_LOG and cr == RowType.DYNAMIC: + return + outer = self.get_outer_window() right_panel = outer.grid.right_panel filters_vbox = right_panel.filters_vbox - valid_contexts = ["Server browser", "My saved servers", "Recent servers", "Scan LAN servers"] - if chosen_row in valid_contexts: - # server contexts share the same model type + server_contexts = [ + RowType.SCAN_LAN, + RowType.SERVER_BROWSER, + RowType.RECENT_SERVERS, + RowType.SAVED_SERVERS + ] - if chosen_row == "Server browser": + # server contexts share the same model type + if cr in server_contexts: + if cr == RowType.SERVER_BROWSER: cooldown = call_out(self, "test_cooldown", "", "") if cooldown.returncode == 1: - spawn_dialog(self.get_outer_window(), cooldown.stdout, Popup.NOTIFY) + spawn_dialog(outer, cooldown.stdout, Popup.NOTIFY) return 1 for check in checks: toggle_signal(filters_vbox, check, '_on_check_toggle', False) @@ -1320,7 +1707,7 @@ class TreeView(Gtk.TreeView): if check.get_label() not in toggled_checks: toggled_checks.append(check.get_label()) check.set_active(True) - self._update_multi_column(chosen_row) + self._update_multi_column(cr) map_store.clear() map_store.append(["All maps"]) @@ -1329,12 +1716,13 @@ class TreeView(Gtk.TreeView): toggle_signal(filters_vbox, filters_vbox.maps_combo, '_on_map_changed', True) toggle_signal(self, self.selected_row, '_on_tree_selection_changed', True) self.grab_focus() - elif chosen_row == "List installed mods" or chosen_row == "Show debug log": + return + + if self.view == WindowContext.TABLE_MODS or self.view == WindowContext.TABLE_LOG: toggle_signal(self, self.selected_row, '_on_tree_selection_changed', False) - self.update_quad_column(chosen_row) + self.update_quad_column(cr) toggle_signal(self, self.selected_row, '_on_tree_selection_changed', True) - elif any(map(context.__contains__, valid_contexts)): - # implies activated row on any server list subcontext + elif self.view == WindowContext.TABLE_SERVER or self.view == WindowContext.TABLE_API: self._attempt_connection() else: # implies any other non-server option selected from main menu @@ -1342,7 +1730,10 @@ class TreeView(Gtk.TreeView): def format_metadata(row_sel): - prefix = status_tooltip[row_sel] + for i in RowType: + if i.dict["label"] == row_sel: + row = i + prefix = i.dict["tooltip"] vals = { "branch": config_vals[0], "debug": config_vals[1], @@ -1352,38 +1743,19 @@ def format_metadata(row_sel): "preferred_client": config_vals[5], "fullscreen": config_vals[6] } - match row_sel: - case "Quick-connect to favorite server" | "Change favorite server": - default = "unset" - val = "fav_label" - case "Change player name": - val = "name" - case "Toggle mod install mode": - default = "manual" - alt = "auto" - val = "auto_install" - case "Toggle debug mode": - default = "normal" - alt = "debug" - val = "debug" - case "Toggle release branch": - val = "branch" - case "Toggle Steam/Flatpak": - val = "preferred_client" - case "Toggle DZGUI fullscreen boot": - default = "false" - alt = "true" - val = "fullscreen" - case _: - return prefix - + try: + alt = row.dict["alt"] + default = row.dict["default"] + val = row.dict["val"] + except KeyError: + return prefix try: cur_val = vals[val] if cur_val == "": - return "%s | Current: %s" %(prefix, default) + return "%s | Current: '%s'" %(prefix, default) # TODO: migrate to human readable config values elif cur_val == "1": - return "%s | Current: %s" %(prefix, alt) + return "%s | Current: '%s'" %(prefix, alt) else: return "%s | Current: '%s'" %(prefix, cur_val) except KeyError: @@ -1433,19 +1805,9 @@ class AppHeaderBar(Gtk.HeaderBar): def __init__(self): super().__init__() self.props.title = app_name - self.set_decoration_layout("menu:minimize,maximize,close") + self.set_decoration_layout(":minimize,maximize,close") self.set_show_close_button(True) -class Port(Enum): - # Contains enums for LanButtonDialog ports - DEFAULT = 1 - CUSTOM = 2 - -class Popup(Enum): - WAIT = 1 - NOTIFY = 2 - CONFIRM = 3 - ENTRY = 4 class GenericDialog(Gtk.MessageDialog): def __init__(self, parent, text, mode): @@ -1564,6 +1926,7 @@ class LanButtonDialog(Gtk.Window): def get_active_button(): return self.buttonBox.active_button + class LanDialog(Gtk.MessageDialog): # Custom dialog class that performs integer validation and blocks input if invalid port # Returns None if user cancels the dialog @@ -1637,8 +2000,11 @@ class LanDialog(Gtk.MessageDialog): return True return False -def ChangelogDialog(parent, text, mode): +def ChangelogDialog(parent): + + text = '' + mode = "Changelog -- content can be scrolled" dialog = GenericDialog(parent, text, mode) dialogBox = dialog.get_content_area() dialog.set_default_response(Gtk.ResponseType.OK) @@ -1722,6 +2088,7 @@ class PingDialog(GenericDialog): data = call_out(parent, "test_ping", ip, qport) GLib.idle_add(_load) + class ModDialog(GenericDialog): def __init__(self, parent, text, mode, record): super().__init__(parent, text, mode) @@ -1784,6 +2151,7 @@ class ModDialog(GenericDialog): mod_id = model.get_value(tree_iter, 1) subprocess.Popen(['/usr/bin/env', 'bash', funcs, "open_workshop_page", mod_id]) + class EntryDialog(GenericDialog): def __init__(self, parent, text, mode, link): super().__init__(parent, text, mode) @@ -1803,7 +2171,7 @@ class EntryDialog(GenericDialog): self.userEntry.set_activates_default(True) self.dialogBox.pack_start(self.userEntry, False, False, 0) - if link != "": + if link is not None: button = Gtk.Button(label=link) button.set_margin_start(60) button.set_margin_end(60) @@ -1843,6 +2211,8 @@ class Grid(Gtk.Grid): self.right_panel = RightPanel(is_steam_deck) self.sel_panel = ModSelectionPanel() + self.right_panel.pack_start(self.sel_panel, False, False, 0) + self.show_all() self.bar = Gtk.Statusbar() self.scrollable_treelist.treeview.connect("on_distcalc_started", self._on_calclat_started) @@ -2008,7 +2378,8 @@ class ModSelectionPanel(Gtk.Box): mods.append(concat) with open(mods_temp_file, "w") as outfile: outfile.writelines(mods) - process_tree_option(["Delete selected mods", ""], treeview) + process_tree_option([treeview.view, RowType.DELETE_SELECTED], treeview) + class FilterPanel(Gtk.Box): def __init__(self): @@ -2189,6 +2560,7 @@ class FilterPanel(Gtk.Box): filter_servers(transient_parent, self, treeview, context) self.maps_entry.set_text(selection) + def main(): def usage(): From fbb133eeb2f12383725d85125c6ccb02f0ddcd89 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 21 Nov 2024 17:34:47 +0900 Subject: [PATCH 16/84] chore: uncomment --- dzgui.sh | 4 ++-- helpers/ui.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/dzgui.sh b/dzgui.sh index 454b930..f13c3d2 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -569,7 +569,7 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="27ef5c9b811011521c81985ee2b32bb4" + ["ui.py"]="b58e0d80cb3e1b927e66866a3911a860" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" ["funcs"]="75afe0be7e73af2fb6a7e423b5ac9159" @@ -886,7 +886,7 @@ initial_setup(){ watcher_deps check_architecture test_connection -# fetch_helpers > >(pdialog "Checking helper files") + fetch_helpers > >(pdialog "Checking helper files") varcheck source "$config_file" lock diff --git a/helpers/ui.py b/helpers/ui.py index 123962f..114c898 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -1228,11 +1228,13 @@ class TreeView(Gtk.TreeView): return index def _on_tree_selection_changed(self, selection): + # no statusbar queue on quad tables + if self.view == WindowContext.TABLE_MODS or context == "Timestamp": + return + grid = self.get_outer_grid() context = self.get_first_col() row_sel = self.get_column_at_index(0) - if context == "Mod" or context == "Timestamp": - return logger.info("Tree selection for context '%s' changed to '%s'" %(context, row_sel)) if self.current_proc and self.current_proc.is_alive(): From e9cd4572330d2c8e5bdb786eeaa7136342af8e6a Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:06:01 +0900 Subject: [PATCH 17/84] fix: set window context earlier --- dzgui.sh | 2 +- helpers/ui.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dzgui.sh b/dzgui.sh index f13c3d2..b53700e 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -569,7 +569,7 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="b58e0d80cb3e1b927e66866a3911a860" + ["ui.py"]="549b0415af9ccad6a882b640615b9a22" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" ["funcs"]="75afe0be7e73af2fb6a7e423b5ac9159" diff --git a/helpers/ui.py b/helpers/ui.py index 114c898..1bc315d 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -1229,13 +1229,13 @@ class TreeView(Gtk.TreeView): def _on_tree_selection_changed(self, selection): # no statusbar queue on quad tables - if self.view == WindowContext.TABLE_MODS or context == "Timestamp": - return grid = self.get_outer_grid() context = self.get_first_col() row_sel = self.get_column_at_index(0) logger.info("Tree selection for context '%s' changed to '%s'" %(context, row_sel)) + if self.view == WindowContext.TABLE_MODS or context == "Timestamp": + return if self.current_proc and self.current_proc.is_alive(): self.current_proc.terminate() From 08af994361d6dec73a6eaa5e78699196c2c16391 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 21 Nov 2024 23:40:17 +0900 Subject: [PATCH 18/84] feat: highlight stale mods (#162) --- CHANGELOG.md | 6 ++++ dzgui.sh | 6 ++-- helpers/funcs | 16 +++++++++++ helpers/ui.py | 78 +++++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 92 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe598b9..b979cc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [5.6.0-beta.5] 2024-11-21 +### Added +- Highlight stale mods in mods list +### Fixed +- Duplicate dialog title on Steam Deck + ## [5.6.0-beta.4] 2024-11-20 ### Added - Application header bar and controls diff --git a/dzgui.sh b/dzgui.sh index b53700e..1b41411 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.4 +version=5.6.0-beta.5 #CONSTANTS aid=221100 @@ -569,10 +569,10 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="549b0415af9ccad6a882b640615b9a22" + ["ui.py"]="15fd3fc32e96db8345c5a3ac8564ecc2" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="75afe0be7e73af2fb6a7e423b5ac9159" + ["funcs"]="d1f0d32e3ad34a34d561305ffa597e79" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" diff --git a/helpers/funcs b/helpers/funcs index 6ebacfd..4642378 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -39,6 +39,7 @@ lock_file="$state_path/$prefix.lock" cache_dir="$HOME/.cache/$app_name" _cache_servers="$cache_dir/$prefix.servers" _cache_mods_temp="$cache_dir/$prefix.mods_temp" +_cache_stale_mods_temp="$cache_dir/$prefix.stale_mods_temp" _cache_temp="$cache_dir/$prefix.temp" _cache_my_servers="$cache_dir/$prefix.my_servers" _cache_history="$cache_dir/$prefix.history" @@ -95,6 +96,7 @@ else fi declare -A funcs=( +["Highlight stale"]="find_stale_mods" ["My servers"]="dump_servers" ["Change player name"]="update_config_val" ["Change Steam API key"]="update_config_val" @@ -144,6 +146,20 @@ lan_scan(){ printf "%s\n" "$res" fi } +find_stale_mods(){ + local res + local mods=() + > $_cache_stale_mods_temp + for i in "${ip_list[@]}"; do + local ip=$(<<< "$i" awk -F: '{print $1}') + local qport=$(<<< "$i" awk -F: '{print $3}') + res=$(a2s $ip $qport rules) + if [[ -n $res ]]; then + printf "%s\n" "$res" >> $_cache_stale_mods_temp + fi + done + return 99 +} get_player_count(){ shift local res diff --git a/helpers/ui.py b/helpers/ui.py index 1bc315d..1270ace 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -34,8 +34,8 @@ checks = list() map_store = Gtk.ListStore(str) row_store = Gtk.ListStore(str) modlist_store = Gtk.ListStore(str, str, str) -#cf. mod_cols -mod_store = Gtk.ListStore(str, str, str, float) +#cf. mod_cols, last column holds hex color +mod_store = Gtk.ListStore(str, str, str, float, str) #cf. log_cols log_store = Gtk.ListStore(str, str, str, str) #cf. browser_cols @@ -53,6 +53,7 @@ changelog_path = '%s/CHANGELOG.md' %(state_path) geometry_path = '%s/dzg.cols.json' %(state_path) funcs = '%s/funcs' %(helpers_path) mods_temp_file = '%s/dzg.mods_temp' %(cache_path) +stale_mods_temp_file = '%s/dzg.stale_mods_temp' %(cache_path) logger = logging.getLogger(__name__) log_file = '%s/DZGUI_DEBUG.log' %(log_path) @@ -77,7 +78,8 @@ mod_cols = [ "Mod", "Symlink", "Dir", - "Size (MiB)" + "Size (MiB)", + "Color" ] log_cols = [ "Timestamp", @@ -121,6 +123,11 @@ class RowType(EnumWithAttrs): "label": None, "tooltip": None, } + HIGHLIGHT = { + "label": "Highlight stale", + "tooltip": None, + "wait_msg": "Looking for stale mods" + } HANDSHAKE = { "label": "Handshake", "tooltip": None, @@ -519,8 +526,9 @@ def parse_mod_rows(data): lines = data.stdout.splitlines() hits = len(lines) reader = csv.reader(lines, delimiter=delimiter) + # Nonetype inherits default GTK color try: - rows = [[row[0], row[1], row[2], locale.atof(row[3], func=float)] for row in reader if row] + rows = [[row[0], row[1], row[2], locale.atof(row[3], func=float), None] for row in reader if row] except IndexError: return 1 for row in rows: @@ -640,6 +648,10 @@ def process_shell_return_code(transient_parent, msg, code, original_input): # re-block this signal before redrawing table contents toggle_signal(treeview, treeview, '_on_keypress', False) treeview.update_quad_column(RowType.LIST_MODS) + case 99: + # highlight stale mods + panel = transient_parent.grid.sel_panel + panel.colorize_cells(msg, True) case 100: # final handoff before launch final_conf = spawn_dialog(transient_parent, msg, Popup.CONFIRM) @@ -690,6 +702,10 @@ def process_tree_option(input, treeview): process_shell_return_code(transient_parent, msg, rc, input) # help pages + if context == WindowContext.TABLE_MODS and command == RowType.HIGHLIGHT: + wait_msg = command.dict["wait_msg"] + call_on_thread(True, cmd_string, wait_msg, '') + return if context == WindowContext.HELP: match command: case RowType.CHANGELOG: @@ -1589,12 +1605,14 @@ class TreeView(Gtk.TreeView): for i, column_title in enumerate(cols): renderer = Gtk.CellRendererText() - column = Gtk.TreeViewColumn(column_title, renderer, text=i) + column = Gtk.TreeViewColumn(column_title, renderer, text=i, foreground=4) if mode == RowType.LIST_MODS: if i == 3: column.set_cell_data_func(renderer, self._format_float, func_data=None) column.set_sort_column_id(i) - self.append_column(column) + # hidden color property column + if i != 4: + self.append_column(column) widgets = relative_widget(self) grid = widgets["grid"] @@ -1848,7 +1866,7 @@ class GenericDialog(Gtk.MessageDialog): text=header_text, secondary_text=textwrap.fill(text, 50), buttons=button_type, - title=app_name, + title="DZGUI - Dialog", modal=True, ) @@ -1939,7 +1957,7 @@ class LanDialog(Gtk.MessageDialog): buttons=Gtk.ButtonsType.OK_CANCEL, text=text, secondary_text="Select the query port", - title=app_name, + title="DZGUI - Dialog", modal=True, ) @@ -2328,9 +2346,12 @@ class ModSelectionPanel(Gtk.Box): labels = [ "Select all", "Unselect all", - "Delete selected" + "Delete selected", + "Highlight stale" ] + self.active_button = None + for l in labels: button = Gtk.Button(label=l) button.set_margin_start(10) @@ -2339,22 +2360,57 @@ class ModSelectionPanel(Gtk.Box): self.pack_start(button, False, True, 0) def _on_button_clicked(self, button): + self.active_button = button label = button.get_label() widgets = relative_widget(self) parent = widgets["outer"] treeview = widgets["treeview"] + (model, pathlist) = treeview.get_selection().get_selected_rows() match label: case "Select all": treeview.toggle_selection(True) case "Unselect all": treeview.toggle_selection(False) case "Delete selected": - (model, pathlist) = treeview.get_selection().get_selected_rows() ct = len(pathlist) if ct < 1: return - self._iterate_mod_deletion(model, pathlist, ct) + case "Highlight stale": + process_tree_option([treeview.view, RowType.HIGHLIGHT], treeview) + case "Unhighlight stale": + self.colorize_cells(None, False) + + def colorize_cells(self, mods, bool): + def _colorize(path, color): + mod_store[path][4] = color + + widgets = relative_widget(self) + parent = widgets["outer"] + treeview = widgets["treeview"] + (model, pathlist) = treeview.get_selection().get_selected_rows() + + if bool is False: + default = None + for i in range (0, len(mod_store)): + path = Gtk.TreePath(i) + it = mod_store.get_iter(path) + _colorize(path, None) + self.active_button.set_label("Highlight stale") + return + + with open(stale_mods_temp_file, "r") as infile: + lines = [line.rstrip('\n') for line in infile] + + for i in range (0, len(mod_store)): + red = "#FF0000" + path = Gtk.TreePath(i) + it = mod_store.get_iter(path) + if model.get_value(it, 2) not in lines: + _colorize(path, red) + treeview.toggle_selection(False) + self.active_button.set_label("Unhighlight stale") + def _iterate_mod_deletion(self, model, pathlist, ct): # hedge against large number of arguments From 6e375d3f241695c57f3055a382ea97bb5217f5cd Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 21 Nov 2024 23:58:38 +0900 Subject: [PATCH 19/84] fix: drop unused parameter --- helpers/funcs | 1 + helpers/ui.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/helpers/funcs b/helpers/funcs index 4642378..6f2404d 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -158,6 +158,7 @@ find_stale_mods(){ printf "%s\n" "$res" >> $_cache_stale_mods_temp fi done + printf "" return 99 } get_player_count(){ diff --git a/helpers/ui.py b/helpers/ui.py index 1270ace..5753fa8 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -651,7 +651,7 @@ def process_shell_return_code(transient_parent, msg, code, original_input): case 99: # highlight stale mods panel = transient_parent.grid.sel_panel - panel.colorize_cells(msg, True) + panel.colorize_cells(True) case 100: # final handoff before launch final_conf = spawn_dialog(transient_parent, msg, Popup.CONFIRM) @@ -2379,9 +2379,9 @@ class ModSelectionPanel(Gtk.Box): case "Highlight stale": process_tree_option([treeview.view, RowType.HIGHLIGHT], treeview) case "Unhighlight stale": - self.colorize_cells(None, False) + self.colorize_cells(False) - def colorize_cells(self, mods, bool): + def colorize_cells(self, bool): def _colorize(path, color): mod_store[path][4] = color From 885c3bc7e78f79392ce1ad561d75596be0253424 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 21 Nov 2024 23:58:47 +0900 Subject: [PATCH 20/84] chore: update checksums --- dzgui.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dzgui.sh b/dzgui.sh index 1b41411..b7291d1 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -569,10 +569,10 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="15fd3fc32e96db8345c5a3ac8564ecc2" + ["ui.py"]="a5896c88510e8b4b2f1fec06aed0c96a" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="d1f0d32e3ad34a34d561305ffa597e79" + ["funcs"]="9a0ad94df22e24fe41e93a689de7d03e" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" From e28a75cfd93e43b2c16cffe836db204b4a727bf9 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 28 Nov 2024 17:43:03 +0900 Subject: [PATCH 21/84] fix: installed mods race condition (#167) --- CHANGELOG.md | 4 ++++ dzgui.sh | 4 ++-- helpers/ui.py | 25 +++++++++++++------------ 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b979cc0..bf502c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [5.6.0-beta.6] 2024-11-28 +### Fixed +- Race condition when checking for installed mods + ## [5.6.0-beta.5] 2024-11-21 ### Added - Highlight stale mods in mods list diff --git a/dzgui.sh b/dzgui.sh index b7291d1..f5f2dd1 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.5 +version=5.6.0-beta.6 #CONSTANTS aid=221100 @@ -569,7 +569,7 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="a5896c88510e8b4b2f1fec06aed0c96a" + ["ui.py"]="1c6e5b996eccd891a3e56930e28246da" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" ["funcs"]="9a0ad94df22e24fe41e93a689de7d03e" diff --git a/helpers/ui.py b/helpers/ui.py index 5753fa8..779ac15 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -1421,12 +1421,12 @@ class TreeView(Gtk.TreeView): # suppress button panel if store is empty if isinstance(panel_last_child, ModSelectionPanel): if total_mods == 0: - # nb. do not forcibly remove previously added widgets + # do not forcibly remove previously added widgets when reloading in-place grid.sel_panel.set_visible(False) - grid.show_all() right_panel.set_filter_visibility(False) + else: + grid.sel_panel.set_visible(True) - grid.sel_panel.set_visible(True) self.set_model(mod_store) self.grab_focus() size = locale.format_string('%.3f', total_size, grouping=True) @@ -1437,6 +1437,7 @@ class TreeView(Gtk.TreeView): toggle_signal(self, self, '_on_keypress', True) self._focus_first_row() if total_mods == 0: + logger.info("Nothing to do, spawning notice dialog") spawn_dialog(self.get_outer_window(), data.stdout, Popup.NOTIFY) widgets = relative_widget(self) @@ -1447,18 +1448,18 @@ class TreeView(Gtk.TreeView): # suppress errors if no mods available on system if data.returncode == 1: + logger.info("Failed to find mods on local system") total_mods = 0 total_size = 0 GLib.idle_add(load) - return 1 - - # show button panel missing (prevents duplication when reloading in-place) - if not isinstance(panel_last_child, ModSelectionPanel): - grid.sel_panel.set_visible(True) - result = parse_mod_rows(data) - total_size = result[0] - total_mods = result[1] - GLib.idle_add(load) + else: + # show button panel missing (prevents duplication when reloading in-place) + if not isinstance(panel_last_child, ModSelectionPanel): + grid.sel_panel.set_visible(True) + result = parse_mod_rows(data) + total_size = result[0] + total_mods = result[1] + GLib.idle_add(load) def _on_col_width_changed(self, col, width): From 94f9ac52c513cb7ccf4a38380c75ed9acb628df3 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 28 Nov 2024 22:37:15 +0900 Subject: [PATCH 22/84] chore: add logging --- CHANGELOG.md | 4 ++++ dzgui.sh | 2 +- helpers/ui.py | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf502c6..9878f31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [5.6.0-beta.7] 2024-11-28 +### Changed +- Add additional logging when fetching installed mods + ## [5.6.0-beta.6] 2024-11-28 ### Fixed - Race condition when checking for installed mods diff --git a/dzgui.sh b/dzgui.sh index f5f2dd1..de06cac 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.6 +version=5.6.0-beta.7 #CONSTANTS aid=221100 diff --git a/helpers/ui.py b/helpers/ui.py index 779ac15..af1b553 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -1459,6 +1459,9 @@ class TreeView(Gtk.TreeView): result = parse_mod_rows(data) total_size = result[0] total_mods = result[1] + logger.info("Found mods on local system") + logger.info("Total mod size: %s" %(total_size)) + logger.info("Total mod count: %s" %(total_mods)) GLib.idle_add(load) def _on_col_width_changed(self, col, width): From d5162d6d9e06f5b3f0f151dc242812050d5ae86b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 28 Nov 2024 23:25:28 +0900 Subject: [PATCH 23/84] fix: normalize user locale (#168) --- CHANGELOG.md | 4 ++++ dzgui.sh | 4 ++-- helpers/funcs | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9878f31..5a47dd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [5.6.0-beta.8] 2024-11-28 +### Fixed +- Normalize user locale when parsing floats + ## [5.6.0-beta.7] 2024-11-28 ### Changed - Add additional logging when fetching installed mods diff --git a/dzgui.sh b/dzgui.sh index de06cac..abe92bf 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.7 +version=5.6.0-beta.8 #CONSTANTS aid=221100 @@ -572,7 +572,7 @@ fetch_helpers_by_sum(){ ["ui.py"]="1c6e5b996eccd891a3e56930e28246da" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="9a0ad94df22e24fe41e93a689de7d03e" + ["funcs"]="20f25bd68cad369696a5b7ab8d8c543d" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" diff --git a/helpers/funcs b/helpers/funcs index 6f2404d..8f213ba 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -354,7 +354,7 @@ list_mods(){ base_dir=$(basename $(readlink -f $game_dir/$symlink)) size=$(du -s "$(readlink -f "$game_dir/$symlink")" | awk '{print $1}') size=$(python3 -c "n=($size/1024) +.005; print(round(n,4))") - printf "%s$sep%s$sep%s$sep%3.3f\n" "$name" "$symlink" "$base_dir" "$size" + LC_ALL=C printf "%s$sep%s$sep%s$sep%3.3f\n" "$name" "$symlink" "$base_dir" "$size" done | sort -k1 fi } From a94859a1572194343759b0866bf6c7314808ac43 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 3 Dec 2024 20:12:18 +0900 Subject: [PATCH 24/84] change: use LC_NUMERIC --- CHANGELOG.md | 4 ++++ dzgui.sh | 4 ++-- helpers/funcs | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a47dd6..b52b56a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [5.6.0-beta.9] 2024-12-03 +### Fixed +- Normalize user locale when parsing floats + ## [5.6.0-beta.8] 2024-11-28 ### Fixed - Normalize user locale when parsing floats diff --git a/dzgui.sh b/dzgui.sh index abe92bf..5ac52ad 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.8 +version=5.6.0-beta.9 #CONSTANTS aid=221100 @@ -572,7 +572,7 @@ fetch_helpers_by_sum(){ ["ui.py"]="1c6e5b996eccd891a3e56930e28246da" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="20f25bd68cad369696a5b7ab8d8c543d" + ["funcs"]="d98d8626a1d61b2d5947b53155a14928" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" diff --git a/helpers/funcs b/helpers/funcs index 8f213ba..f8f0c2f 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -354,7 +354,7 @@ list_mods(){ base_dir=$(basename $(readlink -f $game_dir/$symlink)) size=$(du -s "$(readlink -f "$game_dir/$symlink")" | awk '{print $1}') size=$(python3 -c "n=($size/1024) +.005; print(round(n,4))") - LC_ALL=C printf "%s$sep%s$sep%s$sep%3.3f\n" "$name" "$symlink" "$base_dir" "$size" + LC_NUMERIC=C printf "%s$sep%s$sep%s$sep%3.3f\n" "$name" "$symlink" "$base_dir" "$size" done | sort -k1 fi } From 6b669e5c2144399a3d393d93e954ab45493acba3 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 3 Dec 2024 21:41:54 +0900 Subject: [PATCH 25/84] fix: reset window context on cooldown --- helpers/ui.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helpers/ui.py b/helpers/ui.py index af1b553..9b51720 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -1721,6 +1721,8 @@ class TreeView(Gtk.TreeView): cooldown = call_out(self, "test_cooldown", "", "") if cooldown.returncode == 1: spawn_dialog(outer, cooldown.stdout, Popup.NOTIFY) + # reset context to main menu if navigation was blocked + self.view = WindowContext.MAIN_MENU return 1 for check in checks: toggle_signal(filters_vbox, check, '_on_check_toggle', False) From 0fdc132873dfcd85eda3e3d58d29b059f80ba23e Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:03:04 +0900 Subject: [PATCH 26/84] fix: array syntax --- helpers/funcs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helpers/funcs b/helpers/funcs index f8f0c2f..d538109 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -930,7 +930,7 @@ remove_from_favs(){ break fi done - if [[ ${#ip_list} -gt 0 ]]; then + if [[ ${#ip_list[@]} -gt 0 ]]; then readarray -t ip_list < <(printf "%s\n" "${ip_list[@]}") fi update_config @@ -938,6 +938,7 @@ remove_from_favs(){ local cache="$(< "$_cache_my_servers")" <<< "$cache" grep -v -P "$r$" > $_cache_my_servers logger INFO "Removed the record $record from saved servers" + echo "Removed $record from saved servers" return 90 } update_favs_from_table(){ From ac3055ce39f2d777dfcf2b53d7b21a02e5f226fe Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:04:00 +0900 Subject: [PATCH 27/84] fix: non blocking workshop open --- helpers/funcs | 2 +- helpers/ui.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/helpers/funcs b/helpers/funcs index d538109..8d4faec 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -1020,7 +1020,7 @@ open_workshop_page(){ shift local id="$1" local workshop_uri="steam://url/CommunityFilePage/$id" - $steam_cmd "$workshop_uri" $id + $steam_cmd "$workshop_uri" $id & } open_link(){ shift diff --git a/helpers/ui.py b/helpers/ui.py index 9b51720..e64caee 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -1139,7 +1139,8 @@ class TreeView(Gtk.TreeView): process_tree_option([self.view, RowType.DELETE_SELECTED], self) case "Open in Steam Workshop": record = self.get_column_at_index(2) - call_out(parent, "open_workshop_page", record) + base_cmd = "open_workshop_page" + subprocess.Popen(['/usr/bin/env', 'bash', funcs, base_cmd, record]) def toggle_selection(self, bool): l = len(mod_store) From ef3fffca47a179dbf87d664fdb1a511be79b1311 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:04:29 +0900 Subject: [PATCH 28/84] change: use printf --- helpers/funcs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/funcs b/helpers/funcs index 8d4faec..5df388a 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -343,7 +343,7 @@ list_mods(){ local base_dir local size if [[ -z $(installed_mods) ]] || [[ -z $(find $workshop_dir -maxdepth 2 -name "*.cpp" | grep .cpp) ]]; then - echo "No mods currently installed or incorrect path set." + printf "No mods currently installed or incorrect path set." logger WARN "Found no locally installed mods" return 1 else From eaa6c1154323c417af7d8ba02e6b4714509247a8 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:06:46 +0900 Subject: [PATCH 29/84] fix: typo --- helpers/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/ui.py b/helpers/ui.py index e64caee..fa57d14 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -156,7 +156,7 @@ class RowType(EnumWithAttrs): } RECENT_SERVERS = { "label": "Recent servers", - "tooltip": "Shows the last to servers you connected to (includes attempts)", + "tooltip": "Shows the last 10 servers you connected to (includes attempts)", } CONN_BY_IP = { "label": "Connect by IP", From a4f076fbdb6ab12485246df7f73d931d4c3cc0e9 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:07:16 +0900 Subject: [PATCH 30/84] chore: clarify comments --- helpers/ui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/helpers/ui.py b/helpers/ui.py index fa57d14..ff5a1ac 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -1090,6 +1090,7 @@ class TreeView(Gtk.TreeView): def _on_menu_click(self, menu_item): #TODO: context menus use old stringwise parsing + # use enumerated contexts parent = self.get_outer_window() context = self.get_first_col() value = self.get_column_at_index(0) From 5619d55840e1bbeb563006c642e36a780d99e377 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:09:25 +0900 Subject: [PATCH 31/84] fix: update statusbar when removing rows --- helpers/ui.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/helpers/ui.py b/helpers/ui.py index ff5a1ac..a573d80 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -1261,8 +1261,11 @@ class TreeView(Gtk.TreeView): if self.view == WindowContext.TABLE_API or self.view == WindowContext.TABLE_SERVER: addr = self.get_column_at_index(7) if addr is None: + server_tooltip[0] = format_tooltip() + grid.update_statusbar(server_tooltip[0]) return if addr in cache: + server_tooltip[0] = format_tooltip() dist = format_distance(cache[addr][0]) ping = format_ping(cache[addr][1]) @@ -1390,7 +1393,7 @@ class TreeView(Gtk.TreeView): self.grab_focus() for column in self.get_columns(): column.connect("notify::width", self._on_col_width_changed) - if hits == 0: + if len(server_store) == 0: call_out(self, "start_cooldown", "", "") api_warn_msg = """\ No servers returned. Possible network issue or API key on cooldown? @@ -1405,10 +1408,8 @@ class TreeView(Gtk.TreeView): data = call_out(self, "dump_servers", mode, *filters) toggle_signal(self, self.selected_row, '_on_tree_selection_changed', False) - row_metadata = parse_server_rows(data) - sum = row_metadata[0] - hits = row_metadata[1] - server_tooltip[0] = format_tooltip(sum, hits) + parse_server_rows(data) + server_tooltip[0] = format_tooltip() grid.update_statusbar(server_tooltip[0]) map_data = call_out(self, "get_unique_maps", mode) @@ -1790,7 +1791,11 @@ def format_metadata(row_sel): return prefix -def format_tooltip(players, hits): +def format_tooltip(): + hits = len(server_store) + players = 0 + for row in server_store: + players+= row[4] hits_pretty = pluralize("matches", hits) players_pretty = pluralize("players", players) tooltip = f"Found {hits:n} {hits_pretty} with {players:n} {players_pretty}" @@ -1800,10 +1805,8 @@ def format_tooltip(players, hits): def filter_servers(transient_parent, filters_vbox, treeview, context): def filter(dialog): def clear_and_destroy(): - row_metadata = parse_server_rows(data) - sum = row_metadata[0] - hits = row_metadata[1] - server_tooltip[0] = format_tooltip(sum, hits) + parse_server_rows(data) + server_tooltip[0] = format_tooltip() transient_parent.grid.update_statusbar(server_tooltip[0]) toggle_signal(treeview, treeview.selected_row, '_on_tree_selection_changed', True) @@ -2279,6 +2282,7 @@ class Grid(Gtk.Grid): self.scrollable_treelist.treeview.terminate_process() def _on_calclat_started(self, treeview): + server_tooltip[0] = format_tooltip() server_tooltip[1] = server_tooltip[0] + "| Distance: calculating..." self.update_statusbar(server_tooltip[1]) From 75e247b83156da8b59ef8936cc54bc93a20ec504 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:10:08 +0900 Subject: [PATCH 32/84] fix: remove return values from parse_server_rows() --- helpers/ui.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/helpers/ui.py b/helpers/ui.py index a573d80..7dea8e5 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -539,19 +539,14 @@ def parse_mod_rows(data): def parse_server_rows(data): - sum = 0 lines = data.stdout.splitlines() reader = csv.reader(lines, delimiter=delimiter) - hits = len(lines) try: rows = [[row[0], row[1], row[2], row[3], int(row[4]), int(row[5]), int(row[6]), row[7], int(row[8])] for row in reader if row] except IndexError: return 1 for row in rows: server_store.append(row) - players = int(row[4]) - sum += players - return [sum, hits] def query_config(widget, key=""): From 43a639005f3fcb3b88ccf8d99e0dd1621179ade6 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:10:31 +0900 Subject: [PATCH 33/84] chore: move comments --- helpers/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/ui.py b/helpers/ui.py index 7dea8e5..7d42ae2 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -2420,7 +2420,6 @@ class ModSelectionPanel(Gtk.Box): def _iterate_mod_deletion(self, model, pathlist, ct): - # hedge against large number of arguments widgets = relative_widget(self) parent = widgets["outer"] treeview = widgets["treeview"] @@ -2441,6 +2440,7 @@ class ModSelectionPanel(Gtk.Box): path = model.get_value(it, 2) concat = symlink + " " + path + "\n" mods.append(concat) + # hedge against large number of arguments passed to shell with open(mods_temp_file, "w") as outfile: outfile.writelines(mods) process_tree_option([treeview.view, RowType.DELETE_SELECTED], treeview) From 13c6813c8e0d9fe9a1ab8cdb3eee7c78b4bf7919 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:11:39 +0900 Subject: [PATCH 34/84] feat: resolve ip when saving records --- helpers/ui.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/helpers/ui.py b/helpers/ui.py index 7d42ae2..652ea92 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -123,6 +123,11 @@ class RowType(EnumWithAttrs): "label": None, "tooltip": None, } + RESOLVE_IP = { + "label": "Resolve IP", + "tooltip": None, + "wait_msg": "Resolving remote IP" + } HIGHLIGHT = { "label": "Highlight stale", "tooltip": None, @@ -696,6 +701,11 @@ def process_tree_option(input, treeview): msg = out[-1] process_shell_return_code(transient_parent, msg, rc, input) + if command == RowType.RESOLVE_IP: + record = "%s:%s" %(treeview.get_column_at_index(7), treeview.get_column_at_index(8)) + wait_msg = command.dict["wait_msg"] + call_on_thread(True, cmd_string, wait_msg, record) + return # help pages if context == WindowContext.TABLE_MODS and command == RowType.HIGHLIGHT: wait_msg = command.dict["wait_msg"] @@ -1095,12 +1105,10 @@ class TreeView(Gtk.TreeView): match context_menu_label: case "Add to my servers" | "Remove from my servers": record = "%s:%s" %(self.get_column_at_index(7), self.get_column_at_index(8)) - proc = call_out(parent, context_menu_label, record) + process_tree_option([self.view, RowType.RESOLVE_IP], self) if context == "Name (My saved servers)": iter = self.get_current_iter() server_store.remove(iter) - msg = proc.stdout - res = spawn_dialog(parent, msg, Popup.NOTIFY) case "Remove from history": record = "%s:%s" %(self.get_column_at_index(7), self.get_column_at_index(8)) call_out(parent, context_menu_label, record) From a46c2e17abf16132768ca47723358ee19b1c87ff Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:12:20 +0900 Subject: [PATCH 35/84] chore: remove unused values --- helpers/ui.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/helpers/ui.py b/helpers/ui.py index 652ea92..cd61034 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -1152,12 +1152,10 @@ class TreeView(Gtk.TreeView): case True: for i in range (0, l): path = Gtk.TreePath(i) - it = mod_store.get_iter(path) self.get_selection().select_path(path) case False: for i in range (0, l): path = Gtk.TreePath(i) - it = mod_store.get_iter(path) self.get_selection().unselect_path(path) def _on_button_release(self, widget, event): From 6d332f613e3f254773b7318119f52950b461c603 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:13:29 +0900 Subject: [PATCH 36/84] chore: guard clause --- helpers/ui.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/helpers/ui.py b/helpers/ui.py index cd61034..7d1f81e 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -1132,15 +1132,16 @@ class TreeView(Gtk.TreeView): success_msg = "Successfully deleted the mod '%s'." %(value) fail_msg = "An error occurred during deletion. Aborting." res = spawn_dialog(parent, conf_msg, Popup.CONFIRM) - if res == 0: - mods = [] - symlink = self.get_column_at_index(1) - dir = self.get_column_at_index(2) - concat = symlink + " " + dir + "\n" - mods.append(concat) - with open(mods_temp_file, "w") as outfile: - outfile.writelines(mods) - process_tree_option([self.view, RowType.DELETE_SELECTED], self) + if res != 0: + return + mods = [] + symlink = self.get_column_at_index(1) + dir = self.get_column_at_index(2) + concat = symlink + " " + dir + "\n" + mods.append(concat) + with open(mods_temp_file, "w") as outfile: + outfile.writelines(mods) + process_tree_option([self.view, RowType.DELETE_SELECTED], self) case "Open in Steam Workshop": record = self.get_column_at_index(2) base_cmd = "open_workshop_page" From 336b9882820057093ef40c9e90e3ef0e1f342ffc Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:15:45 +0900 Subject: [PATCH 37/84] fix: untoggle highlight button (#169, 170) --- helpers/ui.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/helpers/ui.py b/helpers/ui.py index 7d1f81e..0747b7a 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -642,16 +642,35 @@ def process_shell_return_code(transient_parent, msg, code, original_input): spawn_dialog(transient_parent, msg, Popup.NOTIFY) return case 95: - # reload mods list + # successful mod deletion spawn_dialog(transient_parent, msg, Popup.NOTIFY) treeview = transient_parent.grid.scrollable_treelist.treeview + grid = treeview.get_parent().get_parent() + (model, pathlist) = treeview.get_selection().get_selected_rows() + for p in reversed(pathlist): + it = model.get_iter(p) + model.remove(it) + total_size = 0 + total_mods = len(model) + for row in model: + total_size += row[3] + size = locale.format_string('%.3f', total_size, grouping=True) + pretty = pluralize("mods", total_mods) + grid.update_statusbar(f"Found {total_mods:n} {pretty} taking up {size} MiB") + # untoggle selection for visibility of other stale rows + treeview.toggle_selection(False) + case 96: + # unsuccessful mod deletion + spawn_dialog(transient_parent, msg, Popup.NOTIFY) # re-block this signal before redrawing table contents + treeview = transient_parent.grid.scrollable_treelist.treeview toggle_signal(treeview, treeview, '_on_keypress', False) treeview.update_quad_column(RowType.LIST_MODS) case 99: # highlight stale mods panel = transient_parent.grid.sel_panel panel.colorize_cells(True) + panel.toggle_select_stale_button(True) case 100: # final handoff before launch final_conf = spawn_dialog(transient_parent, msg, Popup.CONFIRM) @@ -1431,6 +1450,7 @@ class TreeView(Gtk.TreeView): right_panel.set_filter_visibility(False) else: grid.sel_panel.set_visible(True) + grid.sel_panel.initialize() self.set_model(mod_store) self.grab_focus() @@ -2373,6 +2393,19 @@ class ModSelectionPanel(Gtk.Box): button.connect("clicked", self._on_button_clicked) self.pack_start(button, False, True, 0) + + def initialize(self): + l = len(self.get_children()) + last = self.get_children()[l-1] + last_label = last.get_label() + for i in self.get_children(): + match i.get_label(): + case "Select stale": + i.destroy() + case "Unhighlight stale": + i.set_label("Highlight stale") + + def _on_button_clicked(self, button): self.active_button = button label = button.get_label() @@ -2394,6 +2427,22 @@ class ModSelectionPanel(Gtk.Box): process_tree_option([treeview.view, RowType.HIGHLIGHT], treeview) case "Unhighlight stale": self.colorize_cells(False) + self._remove_last_button() + case "Select stale": + for i in range (0, len(mod_store)): + if mod_store[i][4] == "#FF0000": + path = Gtk.TreePath(i) + treeview.get_selection().select_path(path) + + + def toggle_select_stale_button(self, bool): + if bool is True: + button = Gtk.Button(label="Select stale") + button.set_margin_start(10) + button.set_margin_end(10) + button.connect("clicked", self._on_button_clicked) + self.pack_start(button, False, True, 0) + self.show_all() def colorize_cells(self, bool): def _colorize(path, color): From 8fdc29d47df45c8099b3d3149db934dd62c49911 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:23:51 +0900 Subject: [PATCH 38/84] chore: bump version --- CHANGELOG.md | 8 ++++++++ dzgui.sh | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b52b56a..45c7290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [5.6.0-beta.10] 2024-12-04 +### Fixed +- Untoggle highlight button when repopulating mod list +- Resolve remote IP when saving records for game servers with multiple hosts +- Update statusbar when removing servers from list/repopulating +### Added: +- "Select stale" button to bulk select mods marked as obsolete + ## [5.6.0-beta.9] 2024-12-03 ### Fixed - Normalize user locale when parsing floats diff --git a/dzgui.sh b/dzgui.sh index 5ac52ad..766915b 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.9 +version=5.6.0-beta.10 #CONSTANTS aid=221100 @@ -569,10 +569,10 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="1c6e5b996eccd891a3e56930e28246da" + ["ui.py"]="3b90cd522e52131e7ae396671e1c1ad2" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="d98d8626a1d61b2d5947b53155a14928" + ["funcs"]="62f6b3fb2dcb56a78b7642c0f0aa7abe" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" From 8dc9ad3313860d0d6d56d374157bfd0f0f4e1113 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sat, 7 Dec 2024 14:02:50 +0900 Subject: [PATCH 39/84] fix: add missing function definition --- CHANGELOG.md | 4 ++++ dzgui.sh | 4 ++-- helpers/ui.py | 9 +++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45c7290..3288630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [5.6.0-beta.11] 2024-12-07 +### Fixed +- Add missing function definition + ## [5.6.0-beta.10] 2024-12-04 ### Fixed - Untoggle highlight button when repopulating mod list diff --git a/dzgui.sh b/dzgui.sh index 766915b..2a8d4e2 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.10 +version=5.6.0-beta.11 #CONSTANTS aid=221100 @@ -569,7 +569,7 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="3b90cd522e52131e7ae396671e1c1ad2" + ["ui.py"]="be3da1e542d14105f4358dd38901e25a" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" ["funcs"]="62f6b3fb2dcb56a78b7642c0f0aa7abe" diff --git a/helpers/ui.py b/helpers/ui.py index 0747b7a..b5f01c0 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -2435,6 +2435,15 @@ class ModSelectionPanel(Gtk.Box): treeview.get_selection().select_path(path) + def _remove_last_button(self): + children = self.get_children() + l = len(children) + tip = children[l-1] + label = tip.get_label() + if label == "Select stale": + tip.destroy() + + def toggle_select_stale_button(self, bool): if bool is True: button = Gtk.Button(label="Select stale") From eaf9d5ad3ee7b7a418fb5d6228a85ce35bcb9508 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:54:44 +0900 Subject: [PATCH 40/84] fix: change remote URLs --- README.md | 7 ++----- dzgui.sh | 16 ++++++++-------- helpers/funcs | 6 +++--- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 5732eb9..dae89ad 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ ## What this is -DZGUI is a GUI version of [DZTUI](https://github.com/aclist/dztui/tree/dztui) for Linux. - -Note: development of DZTUI has stopped and has been replaced with DZGUI. - -DZGUI allows you to connect to both official and modded/community DayZ servers on Linux and provides a graphical interface for doing so. This overcomes certain limitations in the Linux client and helps prepare the game to launch by doing the following: +DZGUI allows you to connect to both official and modded/community DayZ servers on Linux and provides a graphical interface for doing so. This overcomes certain limitations in the Linux client and +helps prepare the game to launch by doing the following: 1. Search for and display server metadata in a table (server name, player count, ping, current gametime, distance, IP) 2. Add/delete/manage favorite servers by IP or ID diff --git a/dzgui.sh b/dzgui.sh index 2a8d4e2..8885bc9 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -56,11 +56,11 @@ func_helper="$helpers_path/funcs" #URLS author="aclist" -repo="dztui" -url_prefix="https://raw.githubusercontent.com/$author/$repo" +repo="dzgui" +url_prefix="https://codeberg.org/$author/$repo/raw/branch" stable_url="$url_prefix/dzgui" testing_url="$url_prefix/testing" -releases_url="https://github.com/$author/$repo/releases/download/browser" +releases_url="https://codeberg.org/$author/$repo/releases/download/browser" km_helper_url="$releases_url/latlon" geo_file_url="$releases_url/ips.csv.gz" @@ -373,7 +373,7 @@ dl_changelog(){ local mdbranch [[ $branch == "stable" ]] && mdbranch="dzgui" [[ $branch == "testing" ]] && mdbranch="testing" - local md="https://raw.githubusercontent.com/$author/dztui/${mdbranch}/CHANGELOG.md" + local md="https://codeberg.org/$author/dzgui/raw/branch/${mdbranch}/CHANGELOG.md" curl -Ls "$md" > "$state_path/CHANGELOG.md" } test_display_mode(){ @@ -535,7 +535,7 @@ fetch_dzq(){ return 0 fi local sha=3088bbfb147b77bc7b6a9425581b439889ff3f7f - local author="aclist" + local author="yepoleb" local repo="dayzquery" local url="https://raw.githubusercontent.com/$author/$repo/$sha/dayzquery.py" curl -Ls "$url" > "$file" @@ -572,11 +572,11 @@ fetch_helpers_by_sum(){ ["ui.py"]="be3da1e542d14105f4358dd38901e25a" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="62f6b3fb2dcb56a78b7642c0f0aa7abe" + ["funcs"]="0cbec29142f4fcb03b2bd8367fe365e4" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" - local repo="dztui" + local repo="dzgui" local realbranch local file local sum @@ -596,7 +596,7 @@ fetch_helpers_by_sum(){ file="$i" sum="${sums[$i]}" full_path="$helpers_path/$file" - url="https://raw.githubusercontent.com/$author/$repo/$realbranch/helpers/$file" + url="https://codeberg.org/$author/$repo/raw/branch/$realbranch/helpers/$file" if [[ -f "$full_path" ]] && [[ $(get_hash "$full_path") == $sum ]]; then logger INFO "$file is current" else diff --git a/helpers/funcs b/helpers/funcs index 5df388a..e8745f6 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -72,10 +72,10 @@ author="aclist" repo="dztui" gh_prefix="https://github.com" issues_url="$gh_prefix/$author/$repo/issues" -url_prefix="https://raw.githubusercontent.com/$author/$repo" +url_prefix="https://codeberg.org/$author/$repo/branch" stable_url="$url_prefix/dzgui" testing_url="$url_prefix/testing" -releases_url="$gh_prefix/$author/$repo/releases/download/browser" +releases_url="https://codeberg.org/$author/$repo/releases/download/browser" km_helper_url="$releases_url/latlon" db_file="$releases_url/ips.csv.gz" sums_url="$stable_url/helpers/sums.md5" @@ -852,7 +852,7 @@ dl_changelog(){ local file="CHANGELOG.md" [[ $branch == "stable" ]] && mdbranch="dzgui" [[ $branch == "testing" ]] && mdbranch="testing" - local md="https://raw.githubusercontent.com/$author/$repo/${mdbranch}/$file" + local md="https://codeberg.org/$author/$repo/raw/branch/${mdbranch}/$file" curl -Ls "$md" > "$state_path/$file" } toggle(){ From f503c89c2b2fac160dd37cf23615b65d7d7f3a69 Mon Sep 17 00:00:00 2001 From: aclist Date: Thu, 12 Dec 2024 00:58:56 +0000 Subject: [PATCH 41/84] fix: line wrapping --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index dae89ad..8290e55 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ ## What this is -DZGUI allows you to connect to both official and modded/community DayZ servers on Linux and provides a graphical interface for doing so. This overcomes certain limitations in the Linux client and -helps prepare the game to launch by doing the following: +DZGUI allows you to connect to both official and modded/community DayZ servers on Linux and provides a graphical interface for doing so. This overcomes certain limitations in the Linux client and helps prepare the game to launch by doing the following: 1. Search for and display server metadata in a table (server name, player count, ping, current gametime, distance, IP) 2. Add/delete/manage favorite servers by IP or ID From 1da2e7292820ffb28ad900c0d9212b75ec0f206d Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:27:38 +0900 Subject: [PATCH 42/84] fix: update installscript --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index a56422e..1e07a07 100644 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -curl "https://raw.githubusercontent.com/aclist/dztui/dzgui/dzgui.sh" > dzgui.sh +curl "https://codeberg.org/aclist/dzgui/raw/branch/dzgui/dzgui.sh" > dzgui.sh chmod +x dzgui.sh xdg_file="$HOME/.local/share/applications/dzgui.desktop" share="$HOME/.local/share/dzgui" From 0197e0f92103f70a2855dc8aa2f6f7a31f7d19f6 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:08:07 +0900 Subject: [PATCH 43/84] fix: conditional remote url logic --- dzgui.sh | 60 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/dzgui.sh b/dzgui.sh index 8885bc9..45f5663 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -54,16 +54,18 @@ km_helper="$helpers_path/latlon" sums_path="$helpers_path/sums.md5" func_helper="$helpers_path/funcs" -#URLS +#REMOTE +remote_host=gh author="aclist" -repo="dzgui" -url_prefix="https://codeberg.org/$author/$repo/raw/branch" +repo="dztui" +url_prefix="https://raw.githubusercontent.com/$author/$repo" stable_url="$url_prefix/dzgui" testing_url="$url_prefix/testing" -releases_url="https://codeberg.org/$author/$repo/releases/download/browser" +releases_url="https://github.com/$author/$repo/releases/download/browser" km_helper_url="$releases_url/latlon" geo_file_url="$releases_url/ips.csv.gz" + set_im_module(){ #TODO: drop pending SteamOS changes pgrep -a gamescope | grep -q "generate-drm-mode" @@ -312,13 +314,14 @@ check_unmerged(){ check_version(){ local version_url=$(format_version_url) local upstream=$(curl -Ls "$version_url" | awk -F= '/^version=/ {print $2}') + #2024-12-12: do not clobber local version if unreachable + [[ -z $upstream ]] && return logger INFO "Local branch: '$branch', local version: $version" if [[ $branch == "stable" ]]; then version_url="$stable_url/dzgui.sh" elif [[ $branch == "testing" ]]; then version_url="$testing_url/dzgui.sh" fi - local upstream=$(curl -Ls "$version_url" | awk -F= '/^version=/ {print $2}') [[ ! -f "$freedesktop_path/$app_name.desktop" ]] && freedesktop_dirs if [[ $version == $upstream ]]; then logger INFO "Local version is same as upstream" @@ -371,9 +374,14 @@ prompt_dl(){ } dl_changelog(){ local mdbranch + local md [[ $branch == "stable" ]] && mdbranch="dzgui" [[ $branch == "testing" ]] && mdbranch="testing" - local md="https://codeberg.org/$author/dzgui/raw/branch/${mdbranch}/CHANGELOG.md" + if [[ $remote_host == "gh" ]]; then + md="https://raw.githubusercontent.com/$author/$repo/${mdbranch}/CHANGELOG.md" + else + md="https://codeberg.org/$author/$repo/raw/branch/${mdbranch}/CHANGELOG.md" + fi curl -Ls "$md" > "$state_path/CHANGELOG.md" } test_display_mode(){ @@ -596,7 +604,13 @@ fetch_helpers_by_sum(){ file="$i" sum="${sums[$i]}" full_path="$helpers_path/$file" - url="https://codeberg.org/$author/$repo/raw/branch/$realbranch/helpers/$file" + + if [[ $remote_host == "gh" ]]; then + url="https://raw.githubusercontent.com/$author/$repo/$realbranch/helpers/$file" + else + url="https://codeberg.org/$author/$repo/raw/branch/$realbranch/helpers/$file" + fi + if [[ -f "$full_path" ]] && [[ $(get_hash "$full_path") == $sum ]]; then logger INFO "$file is current" else @@ -845,14 +859,32 @@ is_steam_running(){ return 0 fi } +get_response_code(){ + local url="$1" + curl -Ls -I -o /dev/null -w "%{http_code}" "$url" +} test_connection(){ - ping -c1 -4 github.com 1>/dev/null 2>&1 - if [[ ! $? -eq 0 ]]; then - raise_error_and_quit "No connection could be established to the remote server (github.com)." - fi - ping -c1 -4 api.steampowered.com 1>/dev/null 2>&1 - if [[ ! $? -eq 0 ]]; then - raise_error_and_quit "No connection could be established to the remote server (steampowered.com)." + source "$config_file" + declare -A hr + local res1 + local res2 + local str="No connection could be established to the remote server" + hr=( + ["steampowered.com"]="https://api.steampowered.com/IGameServersService/GetServerList/v1/?key=$steam_api" + ["github.com"]="https://github.com/$author" + ["codeberg.org"]="https://codeberg.org/$author" + ) + res=$(get_response_code "${hr["github.com"]}") + [[ $res -ne 200 ]] && remote_host=cb + res=$(get_response_code "${hr["codeberg.org"]}") + [[ $res -ne 200 ]] && raise_error_and_quit "$str (${hr["codeberg.org"]})" + # steam API is mandatory + res=$(get_response_code "${hr["steampowered.com"]}") + [[ $res -ne 200 ]] && raise_error_and_quit "$str ${hr["steampowered.com"]}" + + if [[ $remote_host == "cb" ]]; then + url_prefix="https://codeberg.org/$author/$repo/raw/branch" + releases_url="https://codeberg.org/$author/$repo/releases/download/browser" fi } legacy_cols(){ From a079d554bcdb5a44d9a6aa6c7a333a7fcac173fa Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:13:48 +0900 Subject: [PATCH 44/84] chore: bump version --- dzgui.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui.sh b/dzgui.sh index 45f5663..1cbd63e 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.11 +version=5.6.0-beta.12 #CONSTANTS aid=221100 From 7c5bd76e8f73831500056634dc34d8fcc08561ec Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:16:46 +0900 Subject: [PATCH 45/84] fix: test connection control flow --- dzgui.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dzgui.sh b/dzgui.sh index 1cbd63e..3067c93 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -874,14 +874,18 @@ test_connection(){ ["github.com"]="https://github.com/$author" ["codeberg.org"]="https://codeberg.org/$author" ) - res=$(get_response_code "${hr["github.com"]}") - [[ $res -ne 200 ]] && remote_host=cb - res=$(get_response_code "${hr["codeberg.org"]}") - [[ $res -ne 200 ]] && raise_error_and_quit "$str (${hr["codeberg.org"]})" # steam API is mandatory res=$(get_response_code "${hr["steampowered.com"]}") [[ $res -ne 200 ]] && raise_error_and_quit "$str ${hr["steampowered.com"]}" + res=$(get_response_code "${hr["github.com"]}") + if [[ $res -ne 200 ]]; then + logger WARN "Remote host '${hr["github.com"]}' unreachable', trying fallback" + remote_host=cb + res=$(get_response_code "${hr["codeberg.org"]}") + [[ $res -ne 200 ]] && raise_error_and_quit "$str (${hr["codeberg.org"]})" + fi + if [[ $remote_host == "cb" ]]; then url_prefix="https://codeberg.org/$author/$repo/raw/branch" releases_url="https://codeberg.org/$author/$repo/releases/download/browser" From ea0d94a2fcf24bf5f35febeeb7853f301b67c395 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:25:39 +0900 Subject: [PATCH 46/84] fix: interpolate old vars --- dzgui.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dzgui.sh b/dzgui.sh index 3067c93..fca682b 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -889,6 +889,11 @@ test_connection(){ if [[ $remote_host == "cb" ]]; then url_prefix="https://codeberg.org/$author/$repo/raw/branch" releases_url="https://codeberg.org/$author/$repo/releases/download/browser" + # 2024-12-12: interpolate variables again + stable_url="$url_prefix/dzgui" + testing_url="$url_prefix/testing" + km_helper_url="$releases_url/latlon" + geo_file_url="$releases_url/ips.csv.gz" fi } legacy_cols(){ From 4b95033c1978398cd565c152a64717138cb8dc92 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:31:32 +0900 Subject: [PATCH 47/84] chore: add comments --- dzgui.sh | 2 +- helpers/funcs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dzgui.sh b/dzgui.sh index fca682b..13bb058 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -580,7 +580,7 @@ fetch_helpers_by_sum(){ ["ui.py"]="be3da1e542d14105f4358dd38901e25a" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="0cbec29142f4fcb03b2bd8367fe365e4" + ["funcs"]="bdf2b0d71622de2a5762005290879b3d" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" diff --git a/helpers/funcs b/helpers/funcs index e8745f6..42cdd49 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -70,6 +70,8 @@ game_dir="$steam_path/steamapps/common/DayZ" #URLS author="aclist" repo="dztui" +#TODO: this is hardcoded +#2024-12-12 gh_prefix="https://github.com" issues_url="$gh_prefix/$author/$repo/issues" url_prefix="https://codeberg.org/$author/$repo/branch" From 1be68e7be3cf2835db15bfbfce8efc145b5d23a6 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:38:16 +0900 Subject: [PATCH 48/84] fix: enable redirects --- dzgui.sh | 2 +- install.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dzgui.sh b/dzgui.sh index 13bb058..855aaf9 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.12 +version=5.6.0-beta.13 #CONSTANTS aid=221100 diff --git a/install.sh b/install.sh index 1e07a07..22ec6ab 100644 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -curl "https://codeberg.org/aclist/dzgui/raw/branch/dzgui/dzgui.sh" > dzgui.sh +curl -L "https://codeberg.org/aclist/dzgui/raw/branch/dzgui/dzgui.sh" > dzgui.sh chmod +x dzgui.sh xdg_file="$HOME/.local/share/applications/dzgui.desktop" share="$HOME/.local/share/dzgui" From 12b1c392dfc912779847e64a9dd032db3a2558ed Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:50:18 +0900 Subject: [PATCH 49/84] fix: initial setup --- dzgui.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dzgui.sh b/dzgui.sh index 855aaf9..aa02198 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.13 +version=5.6.0-beta.14 #CONSTANTS aid=221100 @@ -874,9 +874,11 @@ test_connection(){ ["github.com"]="https://github.com/$author" ["codeberg.org"]="https://codeberg.org/$author" ) - # steam API is mandatory - res=$(get_response_code "${hr["steampowered.com"]}") - [[ $res -ne 200 ]] && raise_error_and_quit "$str ${hr["steampowered.com"]}" + # steam API is mandatory, except on initial setup + if [[ -n $steam_api ]]; then + res=$(get_response_code "${hr["steampowered.com"]}") + [[ $res -ne 200 ]] && raise_error_and_quit "$str ("steampowered.com")" + fi res=$(get_response_code "${hr["github.com"]}") if [[ $res -ne 200 ]]; then @@ -885,7 +887,6 @@ test_connection(){ res=$(get_response_code "${hr["codeberg.org"]}") [[ $res -ne 200 ]] && raise_error_and_quit "$str (${hr["codeberg.org"]})" fi - if [[ $remote_host == "cb" ]]; then url_prefix="https://codeberg.org/$author/$repo/raw/branch" releases_url="https://codeberg.org/$author/$repo/releases/download/browser" From 62e6da2abb9c3edb8479e5ab6d803a3662c45116 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:08:57 +0900 Subject: [PATCH 50/84] fix: url syntax --- dzgui.sh | 4 ++-- helpers/funcs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) mode change 100755 => 100644 dzgui.sh diff --git a/dzgui.sh b/dzgui.sh old mode 100755 new mode 100644 index aa02198..82a97d2 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.14 +version=5.6.0-beta.15 #CONSTANTS aid=221100 @@ -580,7 +580,7 @@ fetch_helpers_by_sum(){ ["ui.py"]="be3da1e542d14105f4358dd38901e25a" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="bdf2b0d71622de2a5762005290879b3d" + ["funcs"]="bfe629ed9481627317b49a8f9c6fad24" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" diff --git a/helpers/funcs b/helpers/funcs index 42cdd49..8e06c1a 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -74,7 +74,7 @@ repo="dztui" #2024-12-12 gh_prefix="https://github.com" issues_url="$gh_prefix/$author/$repo/issues" -url_prefix="https://codeberg.org/$author/$repo/branch" +url_prefix="https://codeberg.org/$author/$repo/raw/branch" stable_url="$url_prefix/dzgui" testing_url="$url_prefix/testing" releases_url="https://codeberg.org/$author/$repo/releases/download/browser" @@ -854,6 +854,8 @@ dl_changelog(){ local file="CHANGELOG.md" [[ $branch == "stable" ]] && mdbranch="dzgui" [[ $branch == "testing" ]] && mdbranch="testing" + #TODO: this is hardcoded + #2024-12-12 local md="https://codeberg.org/$author/$repo/raw/branch/${mdbranch}/$file" curl -Ls "$md" > "$state_path/$file" } From c5785d185796b8b066d28d3fe9af2420484abed7 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:31:58 +0900 Subject: [PATCH 51/84] docs: update docs link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8290e55..d6cf770 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Other options include the ability to connect by IP or ID or set a favorite serv ## Setup and usage -Refer to the [manual](https://aclist.github.io/dzgui/dzgui.html) for installation and setup instructions, a feature-by-feature breakdown, and Steam integration tutorials. +Refer to the [manual](https://aclist.codeberg.page) for installation and setup instructions, a feature-by-feature breakdown, and Steam integration tutorials. ![Alt text](/images/example.png) From cbecf569ad217a26de24e3c8272947ad484b399e Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 13 Dec 2024 09:40:46 +0900 Subject: [PATCH 52/84] fix: add fallbacks and remote resource checks --- CHANGELOG.md | 6 ++++++ dzgui.sh | 28 ++++++++++++++++++---------- helpers/funcs | 47 ++++++++++++++++++++++++++++++++++++----------- 3 files changed, 60 insertions(+), 21 deletions(-) mode change 100644 => 100755 dzgui.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 3288630..6d02bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [5.6.0-beta.16] 2024-12-13 +### Fixed +- Add remote resource health checks before downloading updates +### Added +- Add fallback repository + ## [5.6.0-beta.11] 2024-12-07 ### Fixed - Add missing function definition diff --git a/dzgui.sh b/dzgui.sh old mode 100644 new mode 100755 index 82a97d2..6f5f8d1 --- a/dzgui.sh +++ b/dzgui.sh @@ -314,8 +314,8 @@ check_unmerged(){ check_version(){ local version_url=$(format_version_url) local upstream=$(curl -Ls "$version_url" | awk -F= '/^version=/ {print $2}') - #2024-12-12: do not clobber local version if unreachable - [[ -z $upstream ]] && return + local res=$(get_response_code "$version_url") + [[ $res -ne 200 ]] && raise_error_and_quit "Remote resource unavailable: '$version_url'" logger INFO "Local branch: '$branch', local version: $version" if [[ $branch == "stable" ]]; then version_url="$stable_url/dzgui.sh" @@ -377,11 +377,7 @@ dl_changelog(){ local md [[ $branch == "stable" ]] && mdbranch="dzgui" [[ $branch == "testing" ]] && mdbranch="testing" - if [[ $remote_host == "gh" ]]; then - md="https://raw.githubusercontent.com/$author/$repo/${mdbranch}/CHANGELOG.md" - else - md="https://codeberg.org/$author/$repo/raw/branch/${mdbranch}/CHANGELOG.md" - fi + local md="$url_prefix/${mdbranch}/$file" curl -Ls "$md" > "$state_path/CHANGELOG.md" } test_display_mode(){ @@ -523,6 +519,7 @@ get_hash(){ md5sum "$1" | awk '{print $1}' } fetch_a2s(){ + # this file is currently monolithic [[ -d $helpers_path/a2s ]] && { logger INFO "A2S helper is current"; return 0; } local sha=c7590ffa9a6d0c6912e17ceeab15b832a1090640 local author="yepoleb" @@ -530,6 +527,8 @@ fetch_a2s(){ local url="https://github.com/$author/$repo/tarball/$sha" local prefix="${author^}-$repo-${sha:0:7}" local file="$prefix.tar.gz" + local res=$(get_response_code "$url") + [[ $res -ne 200 ]] && raise_error_and_quit "Remote resource unavailable: '$file'" curl -Ls "$url" > "$helpers_path/$file" tar xf "$helpers_path/$file" -C "$helpers_path" "$prefix/a2s" --strip=1 rm "$helpers_path/$file" @@ -546,6 +545,8 @@ fetch_dzq(){ local author="yepoleb" local repo="dayzquery" local url="https://raw.githubusercontent.com/$author/$repo/$sha/dayzquery.py" + local res=$(get_response_code "$url") + [[ $res -ne 200 ]] && raise_error_and_quit "Remote resource unavailable: 'dayzquery.py'" curl -Ls "$url" > "$file" logger INFO "Updated DZQ to sha '$sha'" } @@ -580,11 +581,11 @@ fetch_helpers_by_sum(){ ["ui.py"]="be3da1e542d14105f4358dd38901e25a" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="bfe629ed9481627317b49a8f9c6fad24" + ["funcs"]="2a27ab36d5f8071fe53bf8dfd3b8d88d" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" - local repo="dzgui" + local repo="dztui" local realbranch local file local sum @@ -615,6 +616,8 @@ fetch_helpers_by_sum(){ logger INFO "$file is current" else logger WARN "File '$full_path' checksum != '$sum'" + local res=$(get_response_code "$url") + [[ $res -ne 200 ]] && raise_error_and_quit "Remote resource unavailable: '$url'" curl -Ls "$url" > "$full_path" if [[ ! $? -eq 0 ]]; then raise_error_and_quit "Failed to fetch the file '$file'. Possible timeout?" @@ -635,11 +638,15 @@ fetch_geo_file(){ local km_sum="b038fdb8f655798207bd28de3a004706" local gzip="$helpers_path/ips.csv.gz" if [[ ! -f $geo_file ]] || [[ $(get_hash $geo_file) != $geo_sum ]]; then + local res=$(get_response_code "$geo_file_url") + [[ $res -ne 200 ]] && raise_error_and_quit "Remote resource unavailable: '$geo_file_url'" curl -Ls "$geo_file_url" > "$gzip" #force overwrite gunzip -f "$gzip" fi if [[ ! -f $km_helper ]] || [[ $(get_hash $km_helper) != $km_sum ]]; then + local res=$(get_response_code "$km_helper_url") + [[ $res -ne 200 ]] && raise_error_and_quit "Remote resource unavailable: '$km_helper_url'" curl -Ls "$km_helper_url" > "$km_helper" chmod +x "$km_helper" fi @@ -887,10 +894,11 @@ test_connection(){ res=$(get_response_code "${hr["codeberg.org"]}") [[ $res -ne 200 ]] && raise_error_and_quit "$str (${hr["codeberg.org"]})" fi + logger INFO "Set remote host to '${hr["codeberg.org"]}'" + remote_host=cb if [[ $remote_host == "cb" ]]; then url_prefix="https://codeberg.org/$author/$repo/raw/branch" releases_url="https://codeberg.org/$author/$repo/releases/download/browser" - # 2024-12-12: interpolate variables again stable_url="$url_prefix/dzgui" testing_url="$url_prefix/testing" km_helper_url="$releases_url/latlon" diff --git a/helpers/funcs b/helpers/funcs index 8e06c1a..3681c01 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -70,18 +70,12 @@ game_dir="$steam_path/steamapps/common/DayZ" #URLS author="aclist" repo="dztui" -#TODO: this is hardcoded -#2024-12-12 gh_prefix="https://github.com" issues_url="$gh_prefix/$author/$repo/issues" -url_prefix="https://codeberg.org/$author/$repo/raw/branch" +url_prefix="https://raw.githubusercontent.com/$author/$repo" stable_url="$url_prefix/dzgui" testing_url="$url_prefix/testing" -releases_url="https://codeberg.org/$author/$repo/releases/download/browser" -km_helper_url="$releases_url/latlon" -db_file="$releases_url/ips.csv.gz" -sums_url="$stable_url/helpers/sums.md5" -#TODO: move adoc to index +releases_url="$gh_prefix/$author/$repo/releases/download/browser" help_url="https://$author.github.io/dzgui/dzgui" forum_url="$gh_prefix/$author/$repo/discussions" sponsor_url="$gh_prefix/sponsors/$author" @@ -829,7 +823,40 @@ format_version_url(){ esac echo "$version_url" } +get_response_code(){ + local url="$1" + curl -Ls -I -o /dev/null -w "%{http_code}" "$url" +} +test_connection(){ + source "$config_file" + declare -A hr + local res1 + local res2 + local str="No connection could be established to the remote server" + hr=( + ["github.com"]="https://github.com/$author" + ["codeberg.org"]="https://codeberg.org/$author" + ) + res=$(get_response_code "${hr["github.com"]}") + if [[ $res -ne 200 ]]; then + logger WARN "Remote host '${hr["github.com"]}' unreachable', trying fallback" + remote_host=cb + res=$(get_response_code "${hr["codeberg.org"]}") + if [[ $res -ne 200 ]]; then + printf "Failed to fetch new version. Rolling back" + return 1 + fi + fi + logger INFO "Set remote host to '${hr["codeberg.org"]}'" + if [[ $remote_host == "cb" ]]; then + url_prefix="https://codeberg.org/$author/$repo/raw/branch" + releases_url="https://codeberg.org/$author/$repo/releases/download/browser" + stable_url="$url_prefix/dzgui" + testing_url="$url_prefix/testing" + fi +} download_new_version(){ + test_connection local version_url="$(format_version_url)" mv "$src_path" "$src_path.old" curl -Ls "$version_url" > "$src_path" @@ -854,9 +881,7 @@ dl_changelog(){ local file="CHANGELOG.md" [[ $branch == "stable" ]] && mdbranch="dzgui" [[ $branch == "testing" ]] && mdbranch="testing" - #TODO: this is hardcoded - #2024-12-12 - local md="https://codeberg.org/$author/$repo/raw/branch/${mdbranch}/$file" + local md="$url_prefix/${mdbranch}/$file" curl -Ls "$md" > "$state_path/$file" } toggle(){ From d4d0593927678da56e797191af2e4f23618e45a1 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 13 Dec 2024 09:49:39 +0900 Subject: [PATCH 53/84] fix: add fallback logic to installscript --- install.sh | 47 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) mode change 100644 => 100755 install.sh diff --git a/install.sh b/install.sh old mode 100644 new mode 100755 index 22ec6ab..13f96cc --- a/install.sh +++ b/install.sh @@ -1,8 +1,41 @@ #!/usr/bin/env bash -curl -L "https://codeberg.org/aclist/dzgui/raw/branch/dzgui/dzgui.sh" > dzgui.sh -chmod +x dzgui.sh -xdg_file="$HOME/.local/share/applications/dzgui.desktop" -share="$HOME/.local/share/dzgui" -[[ -f $xdg_file ]] && rm $xdg_file -[[ -d $share ]] && rm -rf "$share" -./dzgui.sh +get_response_code(){ + local url="$1" + curl -Ls -I -o /dev/null -w "%{http_code}" "$url" +} +abort(){ + printf "Remote resource not available. Try again later.\n" + exit 1 +} +fetch(){ + local file="dzgui.sh" + local author="aclist" + local repo="dztui" + local branch="dzgui" + local url + local res + gh_url="https://raw.githubusercontent.com/$author/$repo/$branch/$file" + cb_url="https://codeberg.org/$author/$repo/raw/branch/$branch/$file" + + url="$gh_url" + printf "Checking the remote resource at '%s'\n" "$url" + res=$(get_response_code "$url") + if [[ $res -ne 200 ]]; then + url="$cb_url" + printf "Checking the remote resource at '%s'\n" "$url" + res=$(get_response_code "$url") + if [[ $res -ne 200 ]]; then + abort + fi + fi + + curl -L "$url" > dzgui.sh + chmod +x dzgui.sh + xdg_file="$HOME/.local/share/applications/dzgui.desktop" + share="$HOME/.local/share/dzgui" + [[ -f $xdg_file ]] && rm $xdg_file + [[ -d $share ]] && rm -rf "$share" + ./dzgui.sh +} + +fetch From 5d6cbaa1a5c8b7e79de64d9ad6fb720c97df89e4 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 13 Dec 2024 09:51:31 +0900 Subject: [PATCH 54/84] chore: bump version --- dzgui.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui.sh b/dzgui.sh index 6f5f8d1..b5af209 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.15 +version=5.6.0-beta.16 #CONSTANTS aid=221100 From 4da78d33f350dc8fd364c0c285d7fd4acd2f2616 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 13 Dec 2024 09:58:44 +0900 Subject: [PATCH 55/84] fix: clean up URLs --- dzgui.sh | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/dzgui.sh b/dzgui.sh index b5af209..a92e175 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -606,11 +606,7 @@ fetch_helpers_by_sum(){ sum="${sums[$i]}" full_path="$helpers_path/$file" - if [[ $remote_host == "gh" ]]; then - url="https://raw.githubusercontent.com/$author/$repo/$realbranch/helpers/$file" - else - url="https://codeberg.org/$author/$repo/raw/branch/$realbranch/helpers/$file" - fi + url="${url_prefix}/$realbranch/helpers/$file" if [[ -f "$full_path" ]] && [[ $(get_hash "$full_path") == $sum ]]; then logger INFO "$file is current" @@ -895,7 +891,6 @@ test_connection(){ [[ $res -ne 200 ]] && raise_error_and_quit "$str (${hr["codeberg.org"]})" fi logger INFO "Set remote host to '${hr["codeberg.org"]}'" - remote_host=cb if [[ $remote_host == "cb" ]]; then url_prefix="https://codeberg.org/$author/$repo/raw/branch" releases_url="https://codeberg.org/$author/$repo/releases/download/browser" From 3fce2cc3ad710781029453da458d60ae6d45b9fe Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 13 Dec 2024 10:18:56 +0900 Subject: [PATCH 56/84] fix: return codes --- dzgui.sh | 2 +- helpers/funcs | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/dzgui.sh b/dzgui.sh index a92e175..07c06f8 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -581,7 +581,7 @@ fetch_helpers_by_sum(){ ["ui.py"]="be3da1e542d14105f4358dd38901e25a" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="2a27ab36d5f8071fe53bf8dfd3b8d88d" + ["funcs"]="10c7d9cb9fbb792626ec9e7a4a788ba5" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" diff --git a/helpers/funcs b/helpers/funcs index 3681c01..82ab6ce 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -843,7 +843,6 @@ test_connection(){ remote_host=cb res=$(get_response_code "${hr["codeberg.org"]}") if [[ $res -ne 200 ]]; then - printf "Failed to fetch new version. Rolling back" return 1 fi fi @@ -857,6 +856,12 @@ test_connection(){ } download_new_version(){ test_connection + rc=$? + if [[ $rc -eq 1 ]]; then + printf "Remote resource unavailable. Aborting" + logger WARN "Remote resource unavailable" + return 1 + fi local version_url="$(format_version_url)" mv "$src_path" "$src_path.old" curl -Ls "$version_url" > "$src_path" @@ -896,6 +901,7 @@ toggle(){ fi update_config download_new_version + [[ $? -eq 1 ]] && return 1 return 255 ;; Toggle[[:space:]]mod[[:space:]]install[[:space:]]mode) From e6b5e40bb2bc5e30288f01e6922488de5001290d Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 15 Dec 2024 08:57:25 +0900 Subject: [PATCH 57/84] chore: add logging --- CHANGELOG.md | 4 ++++ dzgui.sh | 6 +++--- helpers/funcs | 6 ++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d02bc4..8caec7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [5.6.0-beta.17] 2024-12-14 +### Added +- Additional logging + ## [5.6.0-beta.16] 2024-12-13 ### Fixed - Add remote resource health checks before downloading updates diff --git a/dzgui.sh b/dzgui.sh index 07c06f8..86c478e 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.16 +version=5.6.0-beta.17 #CONSTANTS aid=221100 @@ -483,7 +483,7 @@ stale_symlinks(){ local game_dir="$steam_path/steamapps/common/DayZ" for l in $(find "$game_dir" -xtype l); do logger DEBUG "Updating stale symlink '$l'" - unlink $l + unlink "$l" done } local_latlon(){ @@ -581,7 +581,7 @@ fetch_helpers_by_sum(){ ["ui.py"]="be3da1e542d14105f4358dd38901e25a" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="10c7d9cb9fbb792626ec9e7a4a788ba5" + ["funcs"]="37897aa36bc2fb6286cee02c8bb07258" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" diff --git a/helpers/funcs b/helpers/funcs index 82ab6ce..22c6d44 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -1170,19 +1170,24 @@ compare(){ echo "$diff" } legacy_symlinks(){ + logger INFO "Removing legacy symlinks" for d in "$game_dir"/*; do if [[ $d =~ @[0-9]+-.+ ]]; then + logger INFO "Unlinking $d" unlink "$d" fi done readarray -t mod_dirs < <(find "$workshop_dir" -maxdepth 1 -mindepth 1 -type d) + logger INFO "Read local mods into array with length: ${#mod_dirs[@]}" [[ ${#mod_dirs[@]} -eq 0 ]] && return + logger INFO "Removing legacy encoding format" for d in "${mod_dirs[@]}"; do # suppress errors if mods are downloading at boot [[ ! -f "$d/meta.cpp" ]] && continue local id=$(awk -F"= " '/publishedid/ {print $2}' "$d"/meta.cpp | awk -F\; '{print $1}') local encoded_id=$(echo "$id" | awk '{printf("%c",$1)}' | base64 | sed 's/\//_/g; s/=//g; s/+/]/g') if [[ -h "$game_dir/@$encoded_id" ]]; then + logger INFO "Unlinking $game_dir/@$encoded_id" unlink "$game_dir/@$encoded_id" fi done @@ -1190,6 +1195,7 @@ legacy_symlinks(){ symlinks(){ readarray -t mod_dirs < <(find "$workshop_dir" -maxdepth 1 -mindepth 1 -type d) [[ ${#mod_dirs[@]} -eq 0 ]] && return + logger INFO "Generating symlinks in new format" for d in "${mod_dirs[@]}"; do # suppress errors if mods are downloading at boot [[ ! -f "$d/meta.cpp" ]] && continue From 4d3d9bf0bb09f9ecb2af5fe46ee7ecfe5205bbbc Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 15 Dec 2024 09:08:35 +0900 Subject: [PATCH 58/84] feat: open workshop subscriptions --- CHANGELOG.md | 4 ++++ dzgui.sh | 4 ++-- helpers/funcs | 21 ++++++++++++++++++ helpers/ui.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8caec7d..71c19b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [5.6.0-beta.18] 2024-12-14 +### Added +- Open Steam workshop subscriptions + ## [5.6.0-beta.17] 2024-12-14 ### Added - Additional logging diff --git a/dzgui.sh b/dzgui.sh index 86c478e..ff58bfe 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -578,10 +578,10 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="be3da1e542d14105f4358dd38901e25a" + ["ui.py"]="24d0525e71c0725a1487926fae7330e4" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="37897aa36bc2fb6286cee02c8bb07258" + ["funcs"]="4c1480fcfae15c4bc1c02a337d1de488" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" diff --git a/helpers/funcs b/helpers/funcs index 22c6d44..1bf3d1a 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -104,6 +104,7 @@ declare -A funcs=( ["Connect by IP"]="validate_and_connect" ["Connect by ID"]="validate_and_connect" ["Connect from table"]="connect_from_table" +["find_id"]="find_id" ["toggle"]="toggle" ["Open link"]="open_link" ["filter"]="dump_servers" @@ -121,6 +122,7 @@ declare -A funcs=( ["is_in_favs"]="is_in_favs" ["show_log"]="show_log" ["Output system info to log file"]="generate_log" +["open_user_workshop"]="open_user_workshop" ["open_workshop_page"]="open_workshop_page" ["Add to my servers"]="update_favs_from_table" ["Remove from my servers"]="update_favs_from_table" @@ -332,6 +334,19 @@ is_in_favs(){ done return 1 } +find_id(){ + local file="$default_steam_path/config/loginusers.vdf" + [[ ! -f $file ]] && return 1 + local res=$(python3 $HOME/.local/share/dzgui/helpers/vdf2json.py \ + -i "$file" | jq -r '.users + |to_entries[] + |select(.value.MostRecent=="1") + |.key' + ) + [[ -z $res ]] && return 1 + printf "%s" "$res" + return 0 +} list_mods(){ local symlink local sep @@ -1051,6 +1066,12 @@ update_config_val(){ show_log(){ < "$debug_log" sed 's/Keyword␞/Keyword/' } +open_user_workshop(){ + shift + local id="$1" + url="https://steamcommunity.com/profiles/$id/myworkshopfiles/?appid=$aid&browsefilter=mysubscriptions" + $steam_cmd steam://openurl/$url & +} open_workshop_page(){ shift local id="$1" diff --git a/helpers/ui.py b/helpers/ui.py index b5f01c0..d741ee0 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -215,6 +215,7 @@ class RowType(EnumWithAttrs): "label": "Toggle mod install mode", "tooltip": "Switch between manual and auto mod installation", "default": "manual", + "link_label": "Open Steam Workshop", "alt": "auto", "val": "auto_install" } @@ -756,6 +757,29 @@ def process_tree_option(input, treeview): case RowType.TGL_BRANCH: wait_msg = "Updating DZGUI branch" call_on_thread(False, "toggle", wait_msg, cmd_string) + case RowType.TGL_INSTALL: + if query_config(None, "auto_install")[0] == "1": + proc = call_out(transient_parent, "toggle", cmd_string) + grid.update_right_statusbar() + tooltip = format_metadata(command.dict["label"]) + transient_parent.grid.update_statusbar(tooltip) + return + # manual -> auto mode + proc = call_out(transient_parent, "find_id", "") + if proc.returncode == 1: + link=None + uid=None + else: + link=command.dict["link_label"] + uid=proc.stdout + manual_sub_msg = """\ + When switching from MANUAL to AUTO mod install mode, + DZGUI will manage mod installation and deletion for you. + To prevent conflicts with Steam Workshop subscriptions and old mods from being downloaded + when Steam updates, you should unsubscribe from any existing Workshop mods you manually subscribed to. + Open your Profile > Workshop Items and select 'Unsubscribe from all' + on the right-hand side, then click OK below to enable AUTO mod install mode.""" + LinkDialog(transient_parent, textwrap.dedent(manual_sub_msg), Popup.NOTIFY, link, command, uid) case _: proc = call_out(transient_parent, "toggle", cmd_string) grid.update_right_statusbar() @@ -2205,6 +2229,43 @@ class ModDialog(GenericDialog): subprocess.Popen(['/usr/bin/env', 'bash', funcs, "open_workshop_page", mod_id]) +class LinkDialog(GenericDialog): + def __init__(self, parent, text, mode, link, command, uid=None): + super().__init__(parent, text, mode) + + self.dialog = GenericDialog(parent, text, mode) + self.dialogBox = self.dialog.get_content_area() + self.dialog.set_default_response(Gtk.ResponseType.OK) + self.dialog.set_size_request(500, 0) + + if link is not None: + button = Gtk.Button(label=link) + button.set_margin_start(60) + button.set_margin_end(60) + button.connect("clicked", self._on_button_clicked, uid) + self.dialogBox.pack_end(button, False, False, 0) + + self.dialog.show_all() + self.dialog.connect("response", self._on_dialog_response, parent, command) + + def _on_button_clicked(self, button, uid): + label = button.get_label() + subprocess.Popen(['/usr/bin/env', 'bash', funcs, "open_user_workshop", uid]) + + def _on_dialog_response(self, dialog, resp, parent, command): + match resp: + case Gtk.ResponseType.DELETE_EVENT: + return + case Gtk.ResponseType.OK: + self.dialog.destroy() + proc = call_out(parent, "toggle", command.dict["label"]) + parent.grid.update_right_statusbar() + tooltip = format_metadata(command.dict["label"]) + parent.grid.update_statusbar(tooltip) + + + + class EntryDialog(GenericDialog): def __init__(self, parent, text, mode, link): super().__init__(parent, text, mode) From 34b3d3bc8cec5b20e66273f17c5ba11ff68afa9e Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:49:55 +0900 Subject: [PATCH 59/84] chore: drop unused var --- dzgui.sh | 2 +- helpers/ui.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dzgui.sh b/dzgui.sh index ff58bfe..a7aabc6 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -578,7 +578,7 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="24d0525e71c0725a1487926fae7330e4" + ["ui.py"]="0e3845ef150ee9863f9160c62dbb24f9" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" ["funcs"]="4c1480fcfae15c4bc1c02a337d1de488" diff --git a/helpers/ui.py b/helpers/ui.py index d741ee0..793d552 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -2249,7 +2249,6 @@ class LinkDialog(GenericDialog): self.dialog.connect("response", self._on_dialog_response, parent, command) def _on_button_clicked(self, button, uid): - label = button.get_label() subprocess.Popen(['/usr/bin/env', 'bash', funcs, "open_user_workshop", uid]) def _on_dialog_response(self, dialog, resp, parent, command): From 6e3746e9b499390ab21b072de0896b9d24a4e072 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:21:57 +0900 Subject: [PATCH 60/84] fix: abort path discovery --- CHANGELOG.md | 5 +++++ dzgui.sh | 6 +++--- helpers/funcs | 12 ++++++++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71c19b2..56e0f91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ ## [5.6.0-beta.18] 2024-12-14 ### Added - Open Steam workshop subscriptions +### Fixed +- Empty dialog popups if user manually deletes local mods while application is running +- Abort DayZ path discovery if VDF if Steam files are not synched +### Changed +- Admonish user to restart Steam in error dialog if DayZ path could not be found ## [5.6.0-beta.17] 2024-12-14 ### Added diff --git a/dzgui.sh b/dzgui.sh index a7aabc6..05d9cee 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -581,7 +581,7 @@ fetch_helpers_by_sum(){ ["ui.py"]="0e3845ef150ee9863f9160c62dbb24f9" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="4c1480fcfae15c4bc1c02a337d1de488" + ["funcs"]="a9745fa5cc2f007bddf0d30032845fbd" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" @@ -747,7 +747,7 @@ find_library_folder(){ local search_path="$1" steam_path="$(python3 "$helpers_path/vdf2json.py" -i "$1/steamapps/libraryfolders.vdf" \ | jq -r '.libraryfolders[]|select(.apps|has("221100")).path')" - if [[ ! $? -eq 0 ]]; then + if [[ ! $? -eq 0 ]] || [[ -z $steam_path ]]; then logger WARN "Failed to parse Steam path using '$search_path'" return 1 fi @@ -800,7 +800,7 @@ create_config(){ find_library_folder "$default_steam_path" if [[ -z $steam_path ]]; then logger raise_error "Steam path was empty" - zenity --question --text="DayZ not found or not installed at the Steam library given." --ok-label="Choose path manually" --cancel-label="Exit" + zenity --question --text="DayZ not found or not installed at the Steam library given. NOTE: if you recently installed DayZ or moved its location, you MUST restart Steam first for these changes to synch." --ok-label="Choose path manually" --cancel-label="Exit" if [[ $? -eq 0 ]]; then logger INFO "User selected file picker" file_picker diff --git a/helpers/funcs b/helpers/funcs index 1bf3d1a..88bc082 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -353,12 +353,13 @@ list_mods(){ local name local base_dir local size + local mods if [[ -z $(installed_mods) ]] || [[ -z $(find $workshop_dir -maxdepth 2 -name "*.cpp" | grep .cpp) ]]; then printf "No mods currently installed or incorrect path set." logger WARN "Found no locally installed mods" return 1 else - for dir in $(find $game_dir/* -maxdepth 1 -type l); do + mods=$(for dir in $(find $game_dir/* -maxdepth 1 -type l); do symlink=$(basename $dir) sep="␞" name=$(awk -F\" '/name/ {print $2}' "${dir}/meta.cpp") @@ -366,7 +367,14 @@ list_mods(){ size=$(du -s "$(readlink -f "$game_dir/$symlink")" | awk '{print $1}') size=$(python3 -c "n=($size/1024) +.005; print(round(n,4))") LC_NUMERIC=C printf "%s$sep%s$sep%s$sep%3.3f\n" "$name" "$symlink" "$base_dir" "$size" - done | sort -k1 + done | sort -k1) + # user may have manually pruned mods out-of-band + # handle directory detritus but no actual mods + if [[ -z $mods ]]; then + printf "No mods currently installed or incorrect path set." + return 1 + fi + echo "$mods" fi } installed_mods(){ From c73120295ac42eb0c0efa950c1ccfe7f2336d695 Mon Sep 17 00:00:00 2001 From: jiriks74 Date: Mon, 9 Dec 2024 14:56:51 +0100 Subject: [PATCH 61/84] fix(checks): Check `vm.max_map_count` using `cat` This solves multiple issues: - We don't need `sudo` to check the value anymore - Some systems may not have `sysctl` available - IDK about desktops, but my Debian server doesn't have this command (for whatever reason) --- dzgui.sh | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/dzgui.sh b/dzgui.sh index 2a8d4e2..c02fa2f 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -402,13 +402,20 @@ check_architecture(){ } check_map_count(){ [[ $is_steam_deck -gt 0 ]] && return 0 - local count=1048576 + local map_count_file="/proc/sys/vm/max_map_count" + local min_count=1048576 local conf_file="/etc/sysctl.d/dayz.conf" - if [[ -f $conf_file ]]; then - logger DEBUG "System map count is already $count or higher" + local current_count + if [[ ! -f ${map_count_file} ]]; then + logger WARN "File '${map_count_file}' doesn't exist!" + return 1 + fi + current_count=$(cat ${map_count_file}) + if [[ $current_count -ge $min_count ]]; then + logger DEBUG "System map count is set to ${current_count}" return 0 fi - qdialog "sudo password required to check system vm map count." "OK" "Cancel" + qdialog "sudo password required to set system vm map count." "OK" "Cancel" if [[ $? -eq 0 ]]; then local pass logger INFO "Prompting user for sudo escalation" @@ -417,13 +424,11 @@ check_map_count(){ logger WARN "User aborted password prompt" return 1 fi - local ct=$(sudo -S <<< "$pass" sh -c "sysctl -q vm.max_map_count | awk -F'= ' '{print \$2}'") - logger DEBUG "Old map count is $ct" - local new_ct - [[ $ct -lt $count ]] && ct=$count - sudo -S <<< "$pass" sh -c "echo 'vm.max_map_count=$ct' > $conf_file" + logger DEBUG "Old map count is $current_count" + [[ $current_count -lt $min_count ]] && current_count=$min_count + sudo -S <<< "$pass" sh -c "echo 'vm.max_map_count=${current_count}' > $conf_file" sudo sysctl -p "$conf_file" - logger DEBUG "Updated map count to $count" + logger DEBUG "Updated map count to $min_count" else logger WARN "User aborted map count prompt" return 1 From a18d68776f0f45cbd999e1a23a518d37e58a2dfa Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:26:31 +0900 Subject: [PATCH 62/84] chore: add logging --- CHANGELOG.md | 3 ++- dzgui.sh | 4 ++-- helpers/funcs | 7 +++++-- helpers/ui.py | 2 -- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56e0f91..0aaa350 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ## [5.6.0-beta.18] 2024-12-14 ### Added -- Open Steam workshop subscriptions +- Open Steam workshop subscriptions dialog +- Additional logging ### Fixed - Empty dialog popups if user manually deletes local mods while application is running - Abort DayZ path discovery if VDF if Steam files are not synched diff --git a/dzgui.sh b/dzgui.sh index 05d9cee..ce0e203 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -578,10 +578,10 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="0e3845ef150ee9863f9160c62dbb24f9" + ["ui.py"]="99544ccef6060125509c4b689a808a15" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="a9745fa5cc2f007bddf0d30032845fbd" + ["funcs"]="98261fdba4323f77c6dd610c1efc4d11" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" diff --git a/helpers/funcs b/helpers/funcs index 88bc082..576b5c3 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -1202,7 +1202,7 @@ legacy_symlinks(){ logger INFO "Removing legacy symlinks" for d in "$game_dir"/*; do if [[ $d =~ @[0-9]+-.+ ]]; then - logger INFO "Unlinking $d" + logger INFO "Unlinking '$d'" unlink "$d" fi done @@ -1212,11 +1212,14 @@ legacy_symlinks(){ logger INFO "Removing legacy encoding format" for d in "${mod_dirs[@]}"; do # suppress errors if mods are downloading at boot + logger INFO "Testing directory '$d'" [[ ! -f "$d/meta.cpp" ]] && continue local id=$(awk -F"= " '/publishedid/ {print $2}' "$d"/meta.cpp | awk -F\; '{print $1}') + logger INFO "Given id is '$id'" local encoded_id=$(echo "$id" | awk '{printf("%c",$1)}' | base64 | sed 's/\//_/g; s/=//g; s/+/]/g') + logger INFO "Resolved id is '$encoded_id'" if [[ -h "$game_dir/@$encoded_id" ]]; then - logger INFO "Unlinking $game_dir/@$encoded_id" + logger INFO "Unlinking '$game_dir/@$encoded_id'" unlink "$game_dir/@$encoded_id" fi done diff --git a/helpers/ui.py b/helpers/ui.py index 793d552..38abd1b 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -2263,8 +2263,6 @@ class LinkDialog(GenericDialog): parent.grid.update_statusbar(tooltip) - - class EntryDialog(GenericDialog): def __init__(self, parent, text, mode, link): super().__init__(parent, text, mode) From 99d964bf449bbee524d67cbe719350b6929b8a2a Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 18 Dec 2024 18:22:41 +0900 Subject: [PATCH 63/84] chore: bump version --- CHANGELOG.md | 1 + dzgui.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aaa350..ea3c173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Fixed - Empty dialog popups if user manually deletes local mods while application is running - Abort DayZ path discovery if VDF if Steam files are not synched +- Avoid sudo escalation if system map count is sufficient (jiriks74) ### Changed - Admonish user to restart Steam in error dialog if DayZ path could not be found diff --git a/dzgui.sh b/dzgui.sh index 15da90a..8136155 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.17 +version=5.6.0-beta.18 #CONSTANTS aid=221100 From 4a953164122a3e55f51679ea82d1d404adf85502 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 18 Dec 2024 20:55:01 +0900 Subject: [PATCH 64/84] feat: redact usernames --- helpers/funcs | 1 + 1 file changed, 1 insertion(+) diff --git a/helpers/funcs b/helpers/funcs index 576b5c3..99bc569 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -739,6 +739,7 @@ logger(){ local self="${BASH_SOURCE[0]}" local caller="${FUNCNAME[1]}" local line="${BASH_LINENO[0]}" + self="$(<<< "$self" sed 's@\(/[^/]*/\)\([^/]*\)\(.*\)@\1REDACTED\3@g')" printf "%s␞%s␞%s::%s()::%s␞%s\n" "$date" "$tag" "$self" "$caller" "$line" "$string" >> "$debug_log" } test_ping(){ From 9b2ec79aa57ac41c9d5fa52b238b2d0c44b8fc97 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 18 Dec 2024 20:56:14 +0900 Subject: [PATCH 65/84] feat: redact usernames --- dzgui.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/dzgui.sh b/dzgui.sh index 8136155..0dd1df3 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -83,6 +83,7 @@ logger(){ local self="${BASH_SOURCE[0]}" local caller="${FUNCNAME[1]}" local line="${BASH_LINENO[0]}" + self="$(<<< "$self" sed 's@\(/[^/]*/\)\([^/]*\)\(.*\)@\1REDACTED\3@g')" printf "%s␞%s␞%s::%s()::%s␞%s\n" "$date" "$tag" "$self" "$caller" "$line" "$string" >> "$debug_log" } setup_dirs(){ From bdea37ddc447860c084c2a725ed0f258766b9ef5 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 18 Dec 2024 20:57:09 +0900 Subject: [PATCH 66/84] chore: cleanup --- helpers/funcs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/helpers/funcs b/helpers/funcs index 99bc569..8450450 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -337,8 +337,9 @@ is_in_favs(){ find_id(){ local file="$default_steam_path/config/loginusers.vdf" [[ ! -f $file ]] && return 1 - local res=$(python3 $HOME/.local/share/dzgui/helpers/vdf2json.py \ - -i "$file" | jq -r '.users + local res=$(python3 "$helpers_path/vdf2json.py" \ + -i "$file" \ + | jq -r '.users |to_entries[] |select(.value.MostRecent=="1") |.key' From 973e2dc100a5a82d803c735478285fcfe6732497 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:21:44 +0900 Subject: [PATCH 67/84] fix: shorten symlink check time --- CHANGELOG.md | 6 ++++++ dzgui.sh | 4 ++-- helpers/funcs | 39 ++++++++++++++++----------------------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea3c173..ecfc1b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [5.6.0-beta.19] 2024-12-18 +### Added +- Redact usernames in log files +### Fixed +- More performant symlink traversal when checking for legacy links + ## [5.6.0-beta.18] 2024-12-14 ### Added - Open Steam workshop subscriptions dialog diff --git a/dzgui.sh b/dzgui.sh index 0dd1df3..ac2de14 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.18 +version=5.6.0-beta.19 #CONSTANTS aid=221100 @@ -587,7 +587,7 @@ fetch_helpers_by_sum(){ ["ui.py"]="99544ccef6060125509c4b689a808a15" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="98261fdba4323f77c6dd610c1efc4d11" + ["funcs"]="05f104fcdf27222f04046d41ec48d692" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" diff --git a/helpers/funcs b/helpers/funcs index 8450450..5989793 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -1202,29 +1202,22 @@ compare(){ } legacy_symlinks(){ logger INFO "Removing legacy symlinks" - for d in "$game_dir"/*; do - if [[ $d =~ @[0-9]+-.+ ]]; then - logger INFO "Unlinking '$d'" - unlink "$d" - fi - done - readarray -t mod_dirs < <(find "$workshop_dir" -maxdepth 1 -mindepth 1 -type d) - logger INFO "Read local mods into array with length: ${#mod_dirs[@]}" - [[ ${#mod_dirs[@]} -eq 0 ]] && return - logger INFO "Removing legacy encoding format" - for d in "${mod_dirs[@]}"; do - # suppress errors if mods are downloading at boot - logger INFO "Testing directory '$d'" - [[ ! -f "$d/meta.cpp" ]] && continue - local id=$(awk -F"= " '/publishedid/ {print $2}' "$d"/meta.cpp | awk -F\; '{print $1}') - logger INFO "Given id is '$id'" - local encoded_id=$(echo "$id" | awk '{printf("%c",$1)}' | base64 | sed 's/\//_/g; s/=//g; s/+/]/g') - logger INFO "Resolved id is '$encoded_id'" - if [[ -h "$game_dir/@$encoded_id" ]]; then - logger INFO "Unlinking '$game_dir/@$encoded_id'" - unlink "$game_dir/@$encoded_id" - fi - done + + readarray -t stale_mod_dirs1 < <(find "$workshop_dir" -maxdepth 1 -mindepth 1 -type l -name '@?*-*') + logger INFO "Read local mods into array 1 with length: ${#stale_mod_dirs[@]}" + if [[ ${#stale_mod_dirs1} -ne 0 ]]; then + for d in "${stale_mod_dirs1[@]}"; do + unlink "$game_dir/$d" + done + fi + + readarray -t stale_mod_dirs2 < <(find "$workshop_dir" -maxdepth 1 -mindepth 1 -type l -name '@??') + logger INFO "Read local mods into array 2 with length: ${#stale_mod_dirs[@]}" + if [[ ${#stale_mod_dirs2} -eq 0 ]]; then + for d in "${stale_mod_dirs2[@]}"; do + unlink "$game_dir/$d" + done + fi } symlinks(){ readarray -t mod_dirs < <(find "$workshop_dir" -maxdepth 1 -mindepth 1 -type d) From 4e968e63e54cc333f399a74131a02d4b61343288 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 23 Dec 2024 20:58:21 +0900 Subject: [PATCH 68/84] fix: more performant link traversal --- CHANGELOG.md | 8 ++++++ dzgui.sh | 9 ++++-- helpers/funcs | 79 ++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 77 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecfc1b3..14a1345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [5.6.0-beta.20] 2024-12-23 +### Added +- Output real and resolved mod ids to logs (temporary) +- Added -steam launch parameter +### Fixed +- Only iterate on missing symlinks +- Move logging up + ## [5.6.0-beta.19] 2024-12-18 ### Added - Redact usernames in log files diff --git a/dzgui.sh b/dzgui.sh index ac2de14..0d62f18 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.19 +version=5.6.0-beta.20 #CONSTANTS aid=221100 @@ -587,7 +587,7 @@ fetch_helpers_by_sum(){ ["ui.py"]="99544ccef6060125509c4b689a808a15" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="05f104fcdf27222f04046d41ec48d692" + ["funcs"]="6371f34f2040de5df82bc9064ad4d26d" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" @@ -893,10 +893,10 @@ test_connection(){ if [[ $res -ne 200 ]]; then logger WARN "Remote host '${hr["github.com"]}' unreachable', trying fallback" remote_host=cb + logger INFO "Set remote host to '${hr["codeberg.org"]}'" res=$(get_response_code "${hr["codeberg.org"]}") [[ $res -ne 200 ]] && raise_error_and_quit "$str (${hr["codeberg.org"]})" fi - logger INFO "Set remote host to '${hr["codeberg.org"]}'" if [[ $remote_host == "cb" ]]; then url_prefix="https://codeberg.org/$author/$repo/raw/branch" releases_url="https://codeberg.org/$author/$repo/releases/download/browser" @@ -1004,6 +1004,9 @@ main(){ uninstall && exit 0 fi + if [[ $1 == "--steam" ]] || [[ $1 == "-s" ]]; then + export STEAM_LAUNCH=1 + fi set_im_module diff --git a/helpers/funcs b/helpers/funcs index 5989793..79e0953 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -29,6 +29,8 @@ prefix="dzg" log_path="$state_path/logs" debug_log="$log_path/DZGUI_DEBUG.log" system_log="$log_path/DZGUI_SYSTEM.log" +mod_log="$log_path/DZGUI_MODIDS.log" +meta_log="$log_path/DZGUI_META.log" #STATE FILES history_file="$state_path/$prefix.history" @@ -866,12 +868,12 @@ test_connection(){ if [[ $res -ne 200 ]]; then logger WARN "Remote host '${hr["github.com"]}' unreachable', trying fallback" remote_host=cb + logger INFO "Set remote host to '${hr["codeberg.org"]}'" res=$(get_response_code "${hr["codeberg.org"]}") if [[ $res -ne 200 ]]; then return 1 fi fi - logger INFO "Set remote host to '${hr["codeberg.org"]}'" if [[ $remote_host == "cb" ]]; then url_prefix="https://codeberg.org/$author/$repo/raw/branch" releases_url="https://codeberg.org/$author/$repo/releases/download/browser" @@ -1165,6 +1167,12 @@ $(print_ip_list | sed 's/"//g') Mods: $(list_mods | sed 's/^/\t/g') DOC + #2024-12-13 + find $workshop_dir -mindepth 1 -maxdepth 1 -type d | awk -F/ '{print $NF}' | sort > $mod_log + find $workshop_dir -mindepth 1 -name meta.cpp | while read -r line; do + cat "$line" | awk '/publishedid/ {print $3}' | sed 's/;//g;s/\r//g' + done | sort > $meta_log + #END printf "Wrote system log to %s" "$system_log" return 0 } @@ -1220,22 +1228,61 @@ legacy_symlinks(){ fi } symlinks(){ - readarray -t mod_dirs < <(find "$workshop_dir" -maxdepth 1 -mindepth 1 -type d) - [[ ${#mod_dirs[@]} -eq 0 ]] && return - logger INFO "Generating symlinks in new format" - for d in "${mod_dirs[@]}"; do - # suppress errors if mods are downloading at boot - [[ ! -f "$d/meta.cpp" ]] && continue - id=$(awk -F"= " '/publishedid/ {print $2}' "$d"/meta.cpp | awk -F\; '{print $1}') - encoded_id=$(encode "$id") - link="@$encoded_id" - if [[ -h "$game_dir/$link" ]]; then - logger INFO "Symlink already exists: '$link' for mod '$id'" - continue + _merge(){ + comm -23 <(printf "%s\n" "${mods[@]}" | sort) <(printf "%s\n" "${targets[@]}" | sort) + } + _pulse(){ + zenity --pulsate --progress --auto-close --no-cancel --title="DZGUI" + } + _create_links(){ + local arr=("$@") + local encoded_id + local link + local mod + for ((i=0; i<${#arr[@]}; i++)); do + encoded_id=$(encode "${arr[$i]}") + link="@$encoded_id" + mod="${arr[$i]}" + logger INFO "Creating link '$game_dir/$link' for '$workshop_dir/$mod'" + [[ $STEAM_LAUNCH -eq 1 ]] && echo "# Creating mod link $((i+1))/${#arr[@]}" + ln -s "$workshop_dir/$mod" "$game_dir/$link" + done + } + + readarray -t mods < <(find $workshop_dir -mindepth 1 -name meta.cpp | awk -F/ 'NF=NF-1{print $NF}') + readarray -t links < <(find $game_dir -type l) + + if [[ ${#mods[@]} -eq 0 ]]; then + logger INFO "No mods present, aborting" + return + fi + + if [[ ${#links[@]} -eq 0 ]]; then + logger INFO "No symlinks present in '$game_dir', creating them" + if [[ $STEAM_LAUNCH -eq 1 ]]; then + _create_links "${mods[@]}" > >(_pulse) + else + _create_links "${mods[@]}" fi - ln -fs "$d" "$game_dir/$link" - logger INFO "Created symlink '$link' for mod '$id'" - done + return + fi + + readarray -t targets < <(printf "%s\n" "${links[@]}" | xargs readlink -f | awk -F/ '{print $NF}') + readarray -t hits < <(_merge) + + if [[ ${#hits[@]} -eq 0 ]]; then + logger INFO "Symlinks are up to date, skipping" + return + + fi + + # update missing targets + logger INFO "Found ${#hits[@]} unlinked mods" + if [[ $STEAM_LAUNCH -eq 1 ]]; then + _create_links "${hits[@]}" > >(_pulse) + else + _create_links "${hits[@]}" + fi } update_history(){ local record="$1" From 3cd99911a61edfe51da64bc9ec1e62b2ffa30b5b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 23 Dec 2024 21:00:18 +0900 Subject: [PATCH 69/84] chore: remove stray line --- dzgui.sh | 2 +- helpers/funcs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dzgui.sh b/dzgui.sh index 0d62f18..a6181ac 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -587,7 +587,7 @@ fetch_helpers_by_sum(){ ["ui.py"]="99544ccef6060125509c4b689a808a15" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="6371f34f2040de5df82bc9064ad4d26d" + ["funcs"]="16caf08919f25b44340ed37de7c4265c" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" diff --git a/helpers/funcs b/helpers/funcs index 79e0953..95c0a3f 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -1273,7 +1273,6 @@ symlinks(){ if [[ ${#hits[@]} -eq 0 ]]; then logger INFO "Symlinks are up to date, skipping" return - fi # update missing targets From 3b5960cb16dffea6d7a19c4b01ebb3f8d6cdb902 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 23 Dec 2024 21:38:15 +0900 Subject: [PATCH 70/84] fix: enclose paths --- dzgui.sh | 2 +- helpers/funcs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dzgui.sh b/dzgui.sh index a6181ac..2d3b184 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -587,7 +587,7 @@ fetch_helpers_by_sum(){ ["ui.py"]="99544ccef6060125509c4b689a808a15" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="16caf08919f25b44340ed37de7c4265c" + ["funcs"]="5d69e8e3d7c3b3c499354b0b939ce76b" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" diff --git a/helpers/funcs b/helpers/funcs index 95c0a3f..ea965b3 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -1168,10 +1168,10 @@ $(print_ip_list | sed 's/"//g') $(list_mods | sed 's/^/\t/g') DOC #2024-12-13 - find $workshop_dir -mindepth 1 -maxdepth 1 -type d | awk -F/ '{print $NF}' | sort > $mod_log + find $workshop_dir -mindepth 1 -maxdepth 1 -type d | awk -F/ '{print $NF}' | sort > "$mod_log" find $workshop_dir -mindepth 1 -name meta.cpp | while read -r line; do cat "$line" | awk '/publishedid/ {print $3}' | sed 's/;//g;s/\r//g' - done | sort > $meta_log + done | sort > "$meta_log" #END printf "Wrote system log to %s" "$system_log" return 0 From 8817a5db23c61d8d8ce58950fe2ca5fb2686ee74 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 25 Dec 2024 14:22:39 +0900 Subject: [PATCH 71/84] chore: add YAML file --- .github/workflows/mirror.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/mirror.yml diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml new file mode 100644 index 0000000..b4cf79b --- /dev/null +++ b/.github/workflows/mirror.yml @@ -0,0 +1,18 @@ +name: Mirror to Codeberg + +on: + push: + workflow_dispatch: + +jobs: + mirror-to-codeberg: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: pixta-dev/repository-mirroring-action@v1 + if: ${{ vars.GIT_REMOTE != '' }} + with: + target_repo_url: ${{ vars.GIT_REMOTE }} + ssh_private_key: ${{ secrets.GIT_SSH_PRIVATE_KEY }} From 2158635e64eb58039622ec54dfb778d8274191ef Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:04:12 +0900 Subject: [PATCH 72/84] chore: add gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e92f57 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tags From 5da4d16ed8fa5f2ff9cbcb945195f0835f257f80 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:23:38 +0900 Subject: [PATCH 73/84] feat: add tooltips --- CHANGELOG.md | 15 ++++++++++++ README.md | 17 ++++++++------ dzgui.sh | 6 ++--- helpers/funcs | 23 ++++++++----------- helpers/ui.py | 63 +++++++++++++++++++++++++++++++-------------------- 5 files changed, 76 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14a1345..620e8f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [5.6.0-beta.21] 2024-01-06 +### Added +- Add in-app documentation link to Codeberg mirror +- Hover tooltips to most buttons +### Fixed +- Prevent ArrowUp/ArrowDown input when inside keyword field +### Changed +- Update forum URL +- Reword Help section links to include destination +- Update README.md +### Dropped +- Removed temporary mod ID output in debug logs +- Removed Hall of Fame section from button links, moved inside documentation +- Remove unused imports + ## [5.6.0-beta.20] 2024-12-23 ### Added - Output real and resolved mod ids to logs (temporary) diff --git a/README.md b/README.md index d6cf770..93f8559 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,19 @@ ## What this is -DZGUI allows you to connect to both official and modded/community DayZ servers on Linux and provides a graphical interface for doing so. This overcomes certain limitations in the Linux client and helps prepare the game to launch by doing the following: +DZGUI allows you to connect to both official and modded/community DayZ servers on Linux and provides a graphical interface for doing so. -1. Search for and display server metadata in a table (server name, player count, ping, current gametime, distance, IP) +This overcomes certain limitations in the Linux client and helps prepare the game to launch by providing the following: + +1. Search for and display server metadata in a table (server name, player count, ping, queue size, current gametime, distance, IP) 2. Add/delete/manage favorite servers by IP or ID -3. Find and prepare mods being requested by the server (choose from manual or automatic installation) -4. Concatenate launch options to pass to Steam - -Other options include the ability to connect by IP or ID or set a favorite server. +3. Quick-connect to favorite/recent servers +4. Find and prepare mods being requested by servers (choose from manual or automatic installation) +5. Bulk delete/update local mods +6. Concatenate launch options to pass to Steam +7. Connect to mod-enabled LAN servers ## Setup and usage -Refer to the [manual](https://aclist.codeberg.page) for installation and setup instructions, a feature-by-feature breakdown, and Steam integration tutorials. +Refer to the [manual](https://aclist.codeberg.page) for installation and setup instructions. ![Alt text](/images/example.png) diff --git a/dzgui.sh b/dzgui.sh index 2d3b184..e9e2a12 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0-beta.20 +version=5.6.0-beta.21 #CONSTANTS aid=221100 @@ -584,10 +584,10 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="99544ccef6060125509c4b689a808a15" + ["ui.py"]="a2f9134c9b415a2be1d54a7e91065ee0" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="5d69e8e3d7c3b3c499354b0b939ce76b" + ["funcs"]="b928622aa16e966a5098df02bce6dc3b" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" diff --git a/helpers/funcs b/helpers/funcs index ea965b3..b669fc5 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -79,7 +79,8 @@ stable_url="$url_prefix/dzgui" testing_url="$url_prefix/testing" releases_url="$gh_prefix/$author/$repo/releases/download/browser" help_url="https://$author.github.io/dzgui/dzgui" -forum_url="$gh_prefix/$author/$repo/discussions" +help_url2="https://$author.codeberg.page" +forum_url="https://old.reddit.com/r/dzgui" sponsor_url="$gh_prefix/sponsors/$author" battlemetrics_server_url="https://www.battlemetrics.com/servers/dayz" steam_api_url="https://steamcommunity.com/dev/apikey" @@ -1104,21 +1105,21 @@ open_link(){ "Open Battlemetrics API page") url="$battlemetrics_api_url" ;; - "Help file ⧉") + "Documentation/help files (GitHub) ⧉") url="$help_url" ;; - "Report a bug ⧉") + "Documentation/help files (Codeberg mirror) ⧉") + url="$help_url2" + ;; + "Report a bug (GitHub) ⧉") url="$issues_url" ;; - "Forum ⧉") + "DZGUI Subreddit ⧉") url="$forum_url" ;; - "Sponsor ⧉") + "Sponsor (GitHub) ⧉") url="$sponsor_url" ;; - "Hall of fame ⧉") - url="${help_url}#_hall_of_fame" - ;; esac #if [[ $is_steam_deck -eq 1 ]]; then @@ -1167,12 +1168,6 @@ $(print_ip_list | sed 's/"//g') Mods: $(list_mods | sed 's/^/\t/g') DOC - #2024-12-13 - find $workshop_dir -mindepth 1 -maxdepth 1 -type d | awk -F/ '{print $NF}' | sort > "$mod_log" - find $workshop_dir -mindepth 1 -name meta.cpp | while read -r line; do - cat "$line" | awk '/publishedid/ {print $3}' | sed 's/;//g;s/\r//g' - done | sort > "$meta_log" - #END printf "Wrote system log to %s" "$system_log" return 0 } diff --git a/helpers/ui.py b/helpers/ui.py index 38abd1b..02fe315 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -1,22 +1,21 @@ import csv -import gi import json import locale import logging -import math import multiprocessing import os -import re import signal import subprocess import sys import textwrap import threading +from enum import Enum locale.setlocale(locale.LC_ALL, '') + +import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango -from enum import Enum # 5.6.0 app_name = "DZGUI" @@ -274,25 +273,25 @@ class RowType(EnumWithAttrs): "quad_label": "Debug log" } DOCS = { - "label": "Help file ⧉", + "label": "Documentation/help files (GitHub) ⧉", + "tooltip": "Opens the DZGUI documentation in a browser" + } + DOCS_FALLBACK = { + "label": "Documentation/help files (Codeberg mirror) ⧉", "tooltip": "Opens the DZGUI documentation in a browser" } BUGS = { - "label": "Report a bug ⧉", + "label": "Report a bug (GitHub) ⧉", "tooltip": "Opens the DZGUI issue tracker in a browser" } FORUM = { - "label": "Forum ⧉", + "label": "DZGUI Subreddit ⧉", "tooltip": "Opens the DZGUI discussion forum in a browser" } SPONSOR = { - "label": "Sponsor ⧉", + "label": "Sponsor (GitHub) ⧉", "tooltip": "Sponsor the developer of DZGUI" } - HOF = { - "label": "Hall of fame ⧉", - "tooltip": "A list of significant contributors and testers" - } class WindowContext(EnumWithAttrs): @@ -353,10 +352,10 @@ class WindowContext(EnumWithAttrs): RowType.CHANGELOG, RowType.SHOW_LOG, RowType.DOCS, + RowType.DOCS_FALLBACK, RowType.BUGS, RowType.FORUM, RowType.SPONSOR, - RowType.HOF ], "called_by": [] } @@ -416,19 +415,24 @@ class Popup(Enum): class ButtonType(EnumWithAttrs): MAIN_MENU = {"label": "Main menu", - "opens": WindowContext.MAIN_MENU + "opens": WindowContext.MAIN_MENU, + "tooltip": "Search for and connect to servers" } MANAGE = {"label": "Manage", - "opens": WindowContext.MANAGE + "opens": WindowContext.MANAGE, + "tooltip": "Manage/add to saved servers" } OPTIONS = {"label": "Options", - "opens": WindowContext.OPTIONS + "opens": WindowContext.OPTIONS, + "tooltip": "Change settings, list local mods and\nother advanced options" } HELP = {"label": "Help", - "opens": WindowContext.HELP + "opens": WindowContext.HELP, + "tooltip": "Links to documentation" } EXIT = {"label": "Exit", - "opens": None + "opens": None, + "tooltip": "Quits the application" } @@ -906,12 +910,15 @@ class RightPanel(Gtk.Box): self.pack_start(self.filters_vbox, False, False, 0) self.debug_toggle = Gtk.ToggleButton(label="Debug mode") + self.debug_toggle.set_tooltip_text("Used to perform a dry run without\nactually connecting to a server") + if query_config(None, "debug")[0] == '1': self.debug_toggle.set_active(True) self.debug_toggle.connect("toggled", self._on_button_toggled, "Toggle debug mode") set_surrounding_margins(self.debug_toggle, 10) self.question_button = Gtk.Button(label="?") + self.question_button.set_tooltip_text("Opens the keybindings dialog") self.question_button.set_margin_top(10) self.question_button.set_margin_start(50) self.question_button.set_margin_end(50) @@ -954,6 +961,8 @@ class ButtonBox(Gtk.Box): for side_button in ButtonType: button = EnumeratedButton(label=side_button.dict["label"]) button.set_property("button_type", side_button) + button.set_tooltip_text(side_button.dict["tooltip"]) + if is_steam_deck is True: button.set_size_request(10, 10) else: @@ -2436,16 +2445,17 @@ class ModSelectionPanel(Gtk.Box): self.set_orientation(Gtk.Orientation.VERTICAL) labels = [ - "Select all", - "Unselect all", - "Delete selected", - "Highlight stale" + ["label": "Select all", "tooltip": "Bulk selects all mods"], + ["label": "Unselect all", "tooltip": "Bulk unselects all mods"], + ["label": "Delete selected", "tooltip": "Deletes selected mods from the system"], + ["label": "Highlight stale", "tooltip": "Shows locally-installed mods\nwhich are not used by any server\nin your Saved Servers"] ] self.active_button = None for l in labels: - button = Gtk.Button(label=l) + button = Gtk.Button(label=l["label"]) + button.set_tooltip_text(l["tooltip"]) button.set_margin_start(10) button.set_margin_end(10) button.connect("clicked", self._on_button_clicked) @@ -2505,6 +2515,7 @@ class ModSelectionPanel(Gtk.Box): def toggle_select_stale_button(self, bool): if bool is True: button = Gtk.Button(label="Select stale") + button.set_tooltip_text("Bulk selects all currently highlighted mods") button.set_margin_start(10) button.set_margin_end(10) button.connect("clicked", self._on_button_clicked) @@ -2521,7 +2532,6 @@ class ModSelectionPanel(Gtk.Box): (model, pathlist) = treeview.get_selection().get_selected_rows() if bool is False: - default = None for i in range (0, len(mod_store)): path = Gtk.TreePath(i) it = mod_store.get_iter(path) @@ -2540,6 +2550,7 @@ class ModSelectionPanel(Gtk.Box): _colorize(path, red) treeview.toggle_selection(False) self.active_button.set_label("Unhighlight stale") + self.active_button.set_tooltip_text("Clears highlights and reverts\nthe table to a default state") def _iterate_mod_deletion(self, model, pathlist, ct): @@ -2667,6 +2678,10 @@ class FilterPanel(Gtk.Box): match event.keyval: case Gdk.KEY_Escape: GLib.idle_add(self.restore_focus_to_treeview) + case Gdk.KEY_Up: + return True + case Gdk.KEY_Down: + return True case _: return False From 2b4649ac6e81d15b60e774ed6745b33c094998fd Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 6 Jan 2025 22:08:48 +0900 Subject: [PATCH 74/84] chore: drop docs --- docs/custom.css | 427 ------------------------ docs/dark.css | 120 ------- docs/dzgui.adoc | 608 ----------------------------------- docs/dzgui_dark.adoc | 608 ----------------------------------- docs/kb.adoc | 28 -- docs/kb_dark.adoc | 28 -- docs/kb_sections/dzg001.adoc | 11 - docs/kb_sections/dzg002.adoc | 5 - docs/kb_sections/dzg003.adoc | 7 - docs/kb_sections/dzg004.adoc | 7 - docs/kb_sections/dzg005.adoc | 7 - docs/kb_sections/dzg006.adoc | 11 - docs/kb_sections/dzg007.adoc | 17 - docs/kb_sections/dzg008.adoc | 7 - docs/kb_sections/dzg009.adoc | 5 - 15 files changed, 1896 deletions(-) delete mode 100644 docs/custom.css delete mode 100644 docs/dark.css delete mode 100644 docs/dzgui.adoc delete mode 100644 docs/dzgui_dark.adoc delete mode 100644 docs/kb.adoc delete mode 100644 docs/kb_dark.adoc delete mode 100644 docs/kb_sections/dzg001.adoc delete mode 100644 docs/kb_sections/dzg002.adoc delete mode 100644 docs/kb_sections/dzg003.adoc delete mode 100644 docs/kb_sections/dzg004.adoc delete mode 100644 docs/kb_sections/dzg005.adoc delete mode 100644 docs/kb_sections/dzg006.adoc delete mode 100644 docs/kb_sections/dzg007.adoc delete mode 100644 docs/kb_sections/dzg008.adoc delete mode 100644 docs/kb_sections/dzg009.adoc diff --git a/docs/custom.css b/docs/custom.css deleted file mode 100644 index d1f3eff..0000000 --- a/docs/custom.css +++ /dev/null @@ -1,427 +0,0 @@ -/*! Asciidoctor default stylesheet | MIT License | https://asciidoctor.org */ -/* Uncomment the following line when using as a custom stylesheet */ -/* @import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700"; */ -html{font-family:sans-serif;-webkit-text-size-adjust:100%} -h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 {color: #8a1414 !important} -a{background:none} -a:focus{outline:thin dotted} -a:active,a:hover{outline:0} -h1{font-size:2em;margin:.67em 0} -b,strong{font-weight:bold} -abbr{font-size:.9em} -abbr[title]{cursor:help;border-bottom:1px dotted #dddddf;text-decoration:none} -dfn{font-style:italic} -hr{height:0} -mark{background:#ff0;color:#000} -code,kbd,pre,samp{font-family:monospace;font-size:1em} -pre{white-space:pre-wrap} -q{quotes:"\201C" "\201D" "\2018" "\2019"} -small{font-size:80%} -sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} -sup{top:-.5em} -sub{bottom:-.25em} -img{border:0} -svg:not(:root){overflow:hidden} -figure{margin:0} -audio,video{display:inline-block} -audio:not([controls]){display:none;height:0} -fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em} -legend{border:0;padding:0} -button,input,select,textarea{font-family:inherit;font-size:100%;margin:0} -button,input{line-height:normal} -button,select{text-transform:none} -button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer} -button[disabled],html input[disabled]{cursor:default} -input[type=checkbox],input[type=radio]{padding:0} -button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0} -textarea{overflow:auto;vertical-align:top} -table{border-collapse:collapse;border-spacing:0} -*,::before,::after{box-sizing:border-box} -html,body{font-size:100%} -body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;line-height:1;position:relative;cursor:auto;-moz-tab-size:4;-o-tab-size:4;tab-size:4;word-wrap:anywhere;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased} -a:hover{cursor:pointer} -img,object,embed{max-width:100%;height:auto} -object,embed{height:100%} -img{-ms-interpolation-mode:bicubic} -.left{float:left!important} -.right{float:right!important} -.text-left{text-align:left!important} -.text-right{text-align:right!important} -.text-center{text-align:center!important} -.text-justify{text-align:justify!important} -.hide{display:none} -img,object,svg{display:inline-block;vertical-align:middle} -textarea{height:auto;min-height:50px} -select{width:100%} -.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em} -div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0} -a{color:#2156a5;text-decoration:underline;line-height:inherit} -a:hover,a:focus{color:#1d4b8f} -a img{border:0} -p{line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility} -p aside{font-size:.875em;line-height:1.35;font-style:italic} -h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em} -h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0} -h1{font-size:2.125em} -h2{font-size:1.6875em} -h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em} -h4,h5{font-size:1.125em} -h6{font-size:1em} -hr{border:solid #dddddf;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em} -em,i{font-style:italic;line-height:inherit} -strong,b{font-weight:bold;line-height:inherit} -small{font-size:60%;line-height:inherit} -code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)} -ul,ol,dl{line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} -ul,ol{margin-left:1.5em} -ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0} -ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit} -ul.square{list-style-type:square} -ul.circle{list-style-type:circle} -ul.disc{list-style-type:disc} -ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} -dl dt{margin-bottom:.3125em;font-weight:bold} -dl dd{margin-bottom:1.25em} -blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd} -blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)} -@media screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2} -h1{font-size:2.75em} -h2{font-size:2.3125em} -h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em} -h4{font-size:1.4375em}} -table{background:#fff;margin-bottom:1.25em;border:1px solid #dedede;word-wrap:normal} -table thead,table tfoot{background:#f7f8f7} -table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left} -table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)} -table tr.even,table tr.alt{background:#f8f8f7} -table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{line-height:1.6} -h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em} -h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400} -.center{margin-left:auto;margin-right:auto} -.stretch{width:100%} -.clearfix::before,.clearfix::after,.float-group::before,.float-group::after{content:" ";display:table} -.clearfix::after,.float-group::after{clear:both} -:not(pre).nobreak{word-wrap:normal} -:not(pre).nowrap{white-space:nowrap} -:not(pre).pre-wrap{white-space:pre-wrap} -:not(pre):not([class^=L])>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background:#f7f7f8;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed} -pre{color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;line-height:1.45;text-rendering:optimizeSpeed} -pre code,pre pre{color:inherit;font-size:inherit;line-height:inherit} -pre>code{display:block} -pre.nowrap,pre.nowrap pre{white-space:pre;word-wrap:normal} -em em{font-style:normal} -strong strong{font-weight:400} -.keyseq{color:rgba(51,51,51,.8)} -kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background:#f7f7f7;border:1px solid #ccc;border-radius:3px;box-shadow:0 1px 0 rgba(0,0,0,.2),inset 0 0 0 .1em #fff;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap} -.keyseq kbd:first-child{margin-left:0} -.keyseq kbd:last-child{margin-right:0} -.menuseq,.menuref{color:#000} -.menuseq b:not(.caret),.menuref{font-weight:inherit} -.menuseq{word-spacing:-.02em} -.menuseq b.caret{font-size:1.25em;line-height:.8} -.menuseq i.caret{font-weight:bold;text-align:center;width:.45em} -b.button::before,b.button::after{position:relative;top:-1px;font-weight:400} -b.button::before{content:"[";padding:0 3px 0 2px} -b.button::after{content:"]";padding:0 2px 0 3px} -p a>code:hover{color:rgba(0,0,0,.9)} -#header,#content,#footnotes,#footer{width:100%;margin:0 auto;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em} -#header::before,#header::after,#content::before,#content::after,#footnotes::before,#footnotes::after,#footer::before,#footer::after{content:" ";display:table} -#header::after,#content::after,#footnotes::after,#footer::after{clear:both} -#content{margin-top:1.25em} -#content::before{content:none} -#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0} -#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #dddddf} -#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #dddddf;padding-bottom:8px} -#header .details{border-bottom:1px solid #dddddf;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:flex;flex-flow:row wrap} -#header .details span:first-child{margin-left:-.125em} -#header .details span.email a{color:rgba(0,0,0,.85)} -#header .details br{display:none} -#header .details br+span::before{content:"\00a0\2013\00a0"} -#header .details br+span.author::before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)} -#header .details br+span#revremark::before{content:"\00a0|\00a0"} -#header #revnumber{text-transform:capitalize} -#header #revnumber::after{content:"\00a0"} -#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #dddddf;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem} -#toc{border-bottom:1px solid #e7e7e9;padding-bottom:.5em} -#toc>ul{margin-left:.125em} -#toc ul.sectlevel0>li>a{font-style:italic} -#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0} -#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none} -#toc li{line-height:1.3334;margin-top:.3334em} -#toc a{text-decoration:none} -#toc a:active{text-decoration:underline} -#toctitle{color:#7a2518;font-size:1.2em} -@media screen and (min-width:768px){#toctitle{font-size:1.375em} -body.toc2{padding-left:15em;padding-right:0} -#toc.toc2{margin-top:0!important;background:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #e7e7e9;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto} -#toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em} -#toc.toc2>ul{font-size:.9em;margin-bottom:0} -#toc.toc2 ul ul{margin-left:0;padding-left:1em} -#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em} -body.toc2.toc-right{padding-left:0;padding-right:15em} -body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #e7e7e9;left:auto;right:0}} -@media screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0} -#toc.toc2{width:20em} -#toc.toc2 #toctitle{font-size:1.375em} -#toc.toc2>ul{font-size:.95em} -#toc.toc2 ul ul{padding-left:1.25em} -body.toc2.toc-right{padding-left:0;padding-right:20em}} -#content #toc{border:1px solid #e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;border-radius:4px} -#content #toc>:first-child{margin-top:0} -#content #toc>:last-child{margin-bottom:0} -#footer{max-width:none;background:rgba(0,0,0,.8);padding:1.25em} -#footer-text{color:hsla(0,0%,100%,.8);line-height:1.44} -#content{margin-bottom:.625em} -.sect1{padding-bottom:.625em} -@media screen and (min-width:768px){#content{margin-bottom:1.25em} -.sect1{padding-bottom:1.25em}} -.sect1:last-child{padding-bottom:0} -.sect1+.sect1{border-top:1px solid #e7e7e9} -#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400} -#content h1>a.anchor::before,h2>a.anchor::before,h3>a.anchor::before,#toctitle>a.anchor::before,.sidebarblock>.content>.title>a.anchor::before,h4>a.anchor::before,h5>a.anchor::before,h6>a.anchor::before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em} -#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible} -#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none} -#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221} -details,.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em} -details{margin-left:1.25rem} -details>summary{cursor:pointer;display:block;position:relative;line-height:1.6;margin-bottom:.625rem;outline:none;-webkit-tap-highlight-color:transparent} -details>summary::-webkit-details-marker{display:none} -details>summary::before{content:"";border:solid transparent;border-left:solid;border-width:.3em 0 .3em .5em;position:absolute;top:.5em;left:-1.25rem;transform:translateX(15%)} -details[open]>summary::before{border:solid transparent;border-top:solid;border-width:.5em .3em 0;transform:translateY(15%)} -details>summary::after{content:"";width:1.25rem;height:1em;position:absolute;top:.3em;left:-1.25rem} -.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic} -table.tableblock.fit-content>caption.title{white-space:nowrap;width:0} -.paragraph.lead>p,#preamble>.sectionbody>[class=paragraph]:first-of-type p{font-size:1.21875em;line-height:1.6;color:rgba(0,0,0,.85)} -.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%} -.admonitionblock>table td.icon{text-align:center;width:80px} -.admonitionblock>table td.icon img{max-width:none} -.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase} -.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6);word-wrap:anywhere} -.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} -.exampleblock>.content{border:1px solid #e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;border-radius:4px} -.exampleblock>.content>:first-child{margin-top:0} -.exampleblock>.content>:last-child{margin-bottom:0} -.sidebarblock{border:1px solid #dbdbd6;margin-bottom:1.25em;padding:1.25em;background:#f3f3f2;border-radius:4px} -.sidebarblock>:first-child{margin-top:0} -.sidebarblock>:last-child{margin-bottom:0} -.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center} -.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} -.literalblock pre,.listingblock>.content>pre{border-radius:4px;overflow-x:auto;padding:1em;font-size:.8125em} -@media screen and (min-width:768px){.literalblock pre,.listingblock>.content>pre{font-size:.90625em}} -@media screen and (min-width:1280px){.literalblock pre,.listingblock>.content>pre{font-size:1em}} -.literalblock pre,.listingblock>.content>pre:not(.highlight),.listingblock>.content>pre[class=highlight],.listingblock>.content>pre[class^="highlight "]{background:#f7f7f8} -.literalblock.output pre{color:#f7f7f8;background:rgba(0,0,0,.9)} -.listingblock>.content{position:relative} -.listingblock code[data-lang]::before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:inherit;opacity:.5} -.listingblock:hover code[data-lang]::before{display:block} -.listingblock.terminal pre .command::before{content:attr(data-prompt);padding-right:.5em;color:inherit;opacity:.5} -.listingblock.terminal pre .command:not([data-prompt])::before{content:"$"} -.listingblock pre.highlightjs{padding:0} -.listingblock pre.highlightjs>code{padding:1em;border-radius:4px} -.listingblock pre.prettyprint{border-width:0} -.prettyprint{background:#f7f7f8} -pre.prettyprint .linenums{line-height:1.45;margin-left:2em} -pre.prettyprint li{background:none;list-style-type:inherit;padding-left:0} -pre.prettyprint li code[data-lang]::before{opacity:1} -pre.prettyprint li:not(:first-child) code[data-lang]::before{display:none} -table.linenotable{border-collapse:separate;border:0;margin-bottom:0;background:none} -table.linenotable td[class]{color:inherit;vertical-align:top;padding:0;line-height:inherit;white-space:normal} -table.linenotable td.code{padding-left:.75em} -table.linenotable td.linenos,pre.pygments .linenos{border-right:1px solid;opacity:.35;padding-right:.5em;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} -pre.pygments span.linenos{display:inline-block;margin-right:.75em} -.quoteblock{margin:0 1em 1.25em 1.5em;display:table} -.quoteblock:not(.excerpt)>.title{margin-left:-1.5em;margin-bottom:.75em} -.quoteblock blockquote,.quoteblock p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify} -.quoteblock blockquote{margin:0;padding:0;border:0} -.quoteblock blockquote::before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)} -.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0} -.quoteblock .attribution{margin-top:.75em;margin-right:.5ex;text-align:right} -.verseblock{margin:0 1em 1.25em} -.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans-serif;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility} -.verseblock pre strong{font-weight:400} -.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex} -.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic} -.quoteblock .attribution br,.verseblock .attribution br{display:none} -.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)} -.quoteblock.abstract blockquote::before,.quoteblock.excerpt blockquote::before,.quoteblock .quoteblock blockquote::before{display:none} -.quoteblock.abstract blockquote,.quoteblock.abstract p,.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{line-height:1.6;word-spacing:0} -.quoteblock.abstract{margin:0 1em 1.25em;display:block} -.quoteblock.abstract>.title{margin:0 0 .375em;font-size:1.15em;text-align:center} -.quoteblock.excerpt>blockquote,.quoteblock .quoteblock{padding:0 0 .25em 1em;border-left:.25em solid #dddddf} -.quoteblock.excerpt,.quoteblock .quoteblock{margin-left:0} -.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{color:inherit;font-size:1.0625rem} -.quoteblock.excerpt .attribution,.quoteblock .quoteblock .attribution{color:inherit;font-size:.85rem;text-align:left;margin-right:0} -p.tableblock:last-child{margin-bottom:0} -td.tableblock>.content{margin-bottom:1.25em;word-wrap:anywhere} -td.tableblock>.content>:last-child{margin-bottom:-1.25em} -table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede} -table.grid-all>*>tr>*{border-width:1px} -table.grid-cols>*>tr>*{border-width:0 1px} -table.grid-rows>*>tr>*{border-width:1px 0} -table.frame-all{border-width:1px} -table.frame-ends{border-width:1px 0} -table.frame-sides{border-width:0 1px} -table.frame-none>colgroup+*>:first-child>*,table.frame-sides>colgroup+*>:first-child>*{border-top-width:0} -table.frame-none>:last-child>:last-child>*,table.frame-sides>:last-child>:last-child>*{border-bottom-width:0} -table.frame-none>*>tr>:first-child,table.frame-ends>*>tr>:first-child{border-left-width:0} -table.frame-none>*>tr>:last-child,table.frame-ends>*>tr>:last-child{border-right-width:0} -table.stripes-all>*>tr,table.stripes-odd>*>tr:nth-of-type(odd),table.stripes-even>*>tr:nth-of-type(even),table.stripes-hover>*>tr:hover{background:#f8f8f7} -th.halign-left,td.halign-left{text-align:left} -th.halign-right,td.halign-right{text-align:right} -th.halign-center,td.halign-center{text-align:center} -th.valign-top,td.valign-top{vertical-align:top} -th.valign-bottom,td.valign-bottom{vertical-align:bottom} -th.valign-middle,td.valign-middle{vertical-align:middle} -table thead th,table tfoot th{font-weight:bold} -tbody tr th{background:#f7f8f7} -tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold} -p.tableblock>code:only-child{background:none;padding:0} -p.tableblock{font-size:1em} -ol{margin-left:1.75em} -ul li ol{margin-left:1.5em} -dl dd{margin-left:1.125em} -dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0} -li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em} -ul.checklist,ul.none,ol.none,ul.no-bullet,ol.no-bullet,ol.unnumbered,ul.unstyled,ol.unstyled{list-style-type:none} -ul.no-bullet,ol.no-bullet,ol.unnumbered{margin-left:.625em} -ul.unstyled,ol.unstyled{margin-left:0} -li>p:empty:only-child::before{content:"";display:inline-block} -ul.checklist>li>p:first-child{margin-left:-1em} -ul.checklist>li>p:first-child>.fa-square-o:first-child,ul.checklist>li>p:first-child>.fa-check-square-o:first-child{width:1.25em;font-size:.8em;position:relative;bottom:.125em} -ul.checklist>li>p:first-child>input[type=checkbox]:first-child{margin-right:.25em} -ul.inline{display:flex;flex-flow:row wrap;list-style:none;margin:0 0 .625em -1.25em} -ul.inline>li{margin-left:1.25em} -.unstyled dl dt{font-weight:400;font-style:normal} -ol.arabic{list-style-type:decimal} -ol.decimal{list-style-type:decimal-leading-zero} -ol.loweralpha{list-style-type:lower-alpha} -ol.upperalpha{list-style-type:upper-alpha} -ol.lowerroman{list-style-type:lower-roman} -ol.upperroman{list-style-type:upper-roman} -ol.lowergreek{list-style-type:lower-greek} -.hdlist>table,.colist>table{border:0;background:none} -.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none} -td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em} -td.hdlist1{font-weight:bold;padding-bottom:1.25em} -td.hdlist2{word-wrap:anywhere} -.literalblock+.colist,.listingblock+.colist{margin-top:-.5em} -.colist td:not([class]):first-child{padding:.4em .75em 0;line-height:1;vertical-align:top} -.colist td:not([class]):first-child img{max-width:none} -.colist td:not([class]):last-child{padding:.25em 0} -.thumb,.th{line-height:0;display:inline-block;border:4px solid #fff;box-shadow:0 0 0 1px #ddd} -.imageblock.left{margin:.25em .625em 1.25em 0} -.imageblock.right{margin:.25em 0 1.25em .625em} -.imageblock>.title{margin-bottom:0} -.imageblock.thumb,.imageblock.th{border-width:6px} -.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em} -.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0} -.image.left{margin-right:.625em} -.image.right{margin-left:.625em} -a.image{text-decoration:none;display:inline-block} -a.image object{pointer-events:none} -sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super} -sup.footnote a,sup.footnoteref a{text-decoration:none} -sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline} -#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em} -#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em;border-width:1px 0 0} -#footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;margin-bottom:.2em} -#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none;margin-left:-1.05em} -#footnotes .footnote:last-of-type{margin-bottom:0} -#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0} -div.unbreakable{page-break-inside:avoid} -.big{font-size:larger} -.small{font-size:smaller} -.underline{text-decoration:underline} -.overline{text-decoration:overline} -.line-through{text-decoration:line-through} -.aqua{color:#00bfbf} -.aqua-background{background:#00fafa} -.black{color:#000} -.black-background{background:#000} -.blue{color:#0000bf} -.blue-background{background:#0000fa} -.fuchsia{color:#bf00bf} -.fuchsia-background{background:#fa00fa} -.gray{color:#606060} -.gray-background{background:#7d7d7d} -.green{color:#006000} -.green-background{background:#007d00} -.lime{color:#00bf00} -.lime-background{background:#00fa00} -.maroon{color:#600000} -.maroon-background{background:#7d0000} -.navy{color:#000060} -.navy-background{background:#00007d} -.olive{color:#606000} -.olive-background{background:#7d7d00} -.purple{color:#600060} -.purple-background{background:#7d007d} -.red{color:#bf0000} -.red-background{background:#fa0000} -.silver{color:#909090} -.silver-background{background:#bcbcbc} -.teal{color:#006060} -.teal-background{background:#007d7d} -.white{color:#bfbfbf} -.white-background{background:#fafafa} -.yellow{color:#bfbf00} -.yellow-background{background:#fafa00} -span.icon>.fa{cursor:default} -a span.icon>.fa{cursor:inherit} -.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default} -.admonitionblock td.icon .icon-note::before{content:"\f05a";color:#19407c} -.admonitionblock td.icon .icon-tip::before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111} -.admonitionblock td.icon .icon-warning::before{content:"\f071";color:#bf6900} -.admonitionblock td.icon .icon-caution::before{content:"\f06d";color:#bf3400} -.admonitionblock td.icon .icon-important::before{content:"\f06a";color:#bf0000} -.conum[data-value]{display:inline-block;color:#fff!important;background:rgba(0,0,0,.8);border-radius:50%;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold} -.conum[data-value] *{color:#fff!important} -.conum[data-value]+b{display:none} -.conum[data-value]::after{content:attr(data-value)} -pre .conum[data-value]{position:relative;top:-.125em} -b.conum *{color:inherit!important} -.conum:not([data-value]):empty{display:none} -dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility} -h1,h2,p,td.content,span.alt,summary{letter-spacing:-.01em} -p strong,td.content strong,div.footnote strong{letter-spacing:-.005em} -p,blockquote,dt,td.content,span.alt,summary{font-size:1.0625rem} -p{margin-bottom:1.25rem} -.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em} -.exampleblock>.content{background:#fffef7;border-color:#e0e0dc;box-shadow:0 1px 4px #e0e0dc} -.print-only{display:none!important} -@page{margin:1.25cm .75cm} -@media print{*{box-shadow:none!important;text-shadow:none!important} -html{font-size:80%} -a{color:inherit!important;text-decoration:underline!important} -a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important} -a[href^="http:"]:not(.bare)::after,a[href^="https:"]:not(.bare)::after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em} -abbr[title]{border-bottom:1px dotted} -abbr[title]::after{content:" (" attr(title) ")"} -pre,blockquote,tr,img,object,svg{page-break-inside:avoid} -thead{display:table-header-group} -svg{max-width:100%} -p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3} -h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid} -#header,#content,#footnotes,#footer{max-width:none} -#toc,.sidebarblock,.exampleblock>.content{background:none!important} -#toc{border-bottom:1px solid #dddddf!important;padding-bottom:0!important} -body.book #header{text-align:center} -body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em} -body.book #header .details{border:0!important;display:block;padding:0!important} -body.book #header .details span:first-child{margin-left:0!important} -body.book #header .details br{display:block} -body.book #header .details br+span::before{content:none!important} -body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important} -body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always} -.listingblock code[data-lang]::before{display:block} -#footer{padding:0 .9375em} -.hide-on-print{display:none!important} -.print-only{display:block!important} -.hide-for-print{display:none!important} -.show-for-print{display:inherit!important}} -@media amzn-kf8,print{#header>h1:first-child{margin-top:1.25rem} -.sect1{padding:0!important} -.sect1+.sect1{border:0} -#footer{background:none} -#footer-text{color:rgba(0,0,0,.6);font-size:.9em}} -@media amzn-kf8{#header,#content,#footnotes,#footer{padding:0}} diff --git a/docs/dark.css b/docs/dark.css deleted file mode 100644 index 698689a..0000000 --- a/docs/dark.css +++ /dev/null @@ -1,120 +0,0 @@ -/* Asciidoctor default stylesheet | MIT License | https://asciidoctor.org */ - -@import url(//fonts.googleapis.com/css?family=Noto+Sans); -@import url(https://cdn.jsdelivr.net/gh/asciidoctor/asciidoctor@2.0/data/stylesheets/asciidoctor-default.css); /* Default asciidoc style framework - important */ - -/* CUSTOMISATIONS */ -/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */ -:root{ ---maincolor:#222222; ---primarycolor:#aaa; ---secondarycolor:#aaa; ---tertiarycolor:#aaa; ---sidebarbackground:#222222; ---linkcolor:#ecc89e; ---linkcoloralternate:#cbcbcb; ---white:#777777; ---codebg:#111; ---codefg:#ffffff; ---linkhover:#eb862f; -} - -/* Text styles */ - -body{font-family: "Noto Sans",sans-serif;background-color: var(--maincolor);color:var(--white);} - -h1{color:var(--primarycolor) !important;font-family:"Noto Sans",sans-serif;} -h2,h3,h4,h5,h6{color:var(--secondarycolor) !important;font-family:"Noto Sans",sans-serif;} -.title{color:var(--white) !important;font-family:"Noto Sans",sans-serif;font-style: normal; font-weight: normal;} -p{font-family: "Noto Sans",sans-serif ! important} -#toc.toc2 a:link{color:var(--linkcolor);} -#toc.toc2 {border-right: 1px solid #8e8e96} -blockquote{color:var(--tertiarycolor) !important} -.quoteblock{color:var(--white)} -code{color:var(--codefg);background-color: var(--codebg) !important} -td.tableblock{border:0 solid #a9a9a9} - - -/* Table styles */ -th{background-color: var(--maincolor);color:var(--primarycolor) !important;} -td{background-color: var(--maincolor);color: var(--primarycolor) !important} - - -#toc.toc2{background-color:var(--sidebarbackground);} -#toctitle{color:var(--white);} - -/* Responsiveness fixes */ -video { - max-width: 100%; -} - -@media all and (max-width: 600px) { - table { - width: 55vw!important; - font-size: 3vw; - } -} - -.exampleblock > .content { - background-color: var(--maincolor); -} - -a { - color: var(--linkcolor); -} - -a:hover,#toc.toc2 a:hover{ - color: var(--linkhover); -} -.admonitionblock td.icon .icon-tip::before { - text-shadow: none; - color: var(--white); -} -.admonitionblock td.icon .icon-note::before { - color: var(--tertiarycolor); -} -.admonitionblock td.icon .icon-important::before { - color: var(--linkcolor); -} -/*.admonitionblock td.icon .icon-caution::before { - color: var(--linkcoloralternate); -}*/ -.admonitionblock td.icon .icon-warning::before { - color: var(--primarycolor); -} - -#preamble > .sectionbody > .paragraph:first-of-type p { - color: var(--white); -} - -.quoteblock blockquote::before { - color: var(--primarycolor); -} -.quoteblock .attribution cite, .verseblock .attribution cite { - color: var(--white); -} -.verseblock pre { - color: var(--white); -} -.quoteblock blockquote, .quoteblock blockquote p { - color: var(--white); -} - -.sidebarblock { - background: var(--sidebarbackground); -} -.literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint { - background: var(--codebg); - color: var(--white); -} - -.literalblock pre, .listingblock>.content>pre:not(.highlight), .listingblock>.content>pre[class=highlight], .listingblock>.content>pre[class^="highlight "] { - background: var(--codebg); -} - -#header .details { - color: var(--white); -} -#header .details span.email a { - color: var(--linkcoloralternate); -} diff --git a/docs/dzgui.adoc b/docs/dzgui.adoc deleted file mode 100644 index e059491..0000000 --- a/docs/dzgui.adoc +++ /dev/null @@ -1,608 +0,0 @@ -:nofooter: -:toc: left -:stylesheet: custom.css - -= DZGUI documentation (v5.x.x) -DayZ server browser and mod manager for Linux | Last updated: {d} - -Click https://aclist.github.io/dzgui/dzgui_dark.html[here] for dark mode - -Looking for the DZGUI Knowledge Base? Click https://aclist.github.io/dzgui/kb.html[here] - -== What this is -A GUI version of https://github.com/aclist/dztui[DZTUI] for Linux. -Used to list official and community server details and quick connect to preferred servers -by staging mods and concatenating launch options automatically. - -Development on DZTUI (terminal client) has stopped at this time. -Instead, DZGUI brings numerous functionality and security improvements and is intended to be a more user-friendly, -turnkey solution for graphical desktop environments, and can also be used on the Steam Deck or similar devices. - -== Setup -=== Dependencies -If not already installed, the below can be found in your system's package manager. - -If any dependencies are missing when the application starts, it will warn you, so you need not take any preemptive measures here. - -All dependencies are installed out of the box on Steam Deck. - -- `curl` -- `jq` -- `zenity` -- `steam` -- `wmctrl` or `xdotool` -- `PyGObject` (`python-gobject`) - - -[NOTE] -If you are using a self-compiled version of jq (e.g. gentoo), it must be configured with support for oniguruma (this is the default setting on most distributions). - -=== Preparation -==== Step 1: Download DZGUI and make it executable - -**Automatic method: generic OS** - -Invoke the command below from a terminal: - -``` -curl -s "https://raw.githubusercontent.com/aclist/dztui/dzgui/install.sh" | bash -``` -**Automatic method: nix-based systems (contributed by lelgenio)** - -Follow the instructions at https://github.com/lelgenio/dzgui-nix to ingest the package and dependencies -into your system using flakes. - -**Manual method** - -``` -git clone https://github.com/aclist/dztui.git -chmod +x dzgui.sh -./dzgui.sh -``` - -==== Step 2: update the vm.max_map_count value - -On most modern distributions, it will seldom be necessary to update this value anymore, since it is set to a sufficiently large number for performance-intensive applications. - -**Automatic method:** - -This is handled automatically by DZGUI if you just choose to run the application out of the box. -You will be prompted for your sudo password in order to check whether the system map count is too small. -This is a one-time check that will not be triggered again once the map count is updated. - -[NOTE] -If you are using a Steam Deck, this step is not necessary. - -The process writes the count to the file `/etc/sysctl.d/dayz.conf`. - -If the system map count was lower than the threshold, it is updated to `1048576`. -If the system map count was already higher, that value is interpolated into this file for redundancy purposes and to avoid sudo escalation on subsequent launches of the application. - -[NOTE] -If, for reasons unrelated to DayZ, you choose at a later time to raise your system map count higher than it originally was and -you find that the count is not sticking, check for the presence of the `dayz.conf` file to see if it is taking precedence and delete it accordingly. - -If you used the automatic method, you can skip to <> below. - -**Manual method:** - -If you wish to update this value yourself without intervention from DZGUI, you have two options: - - -Invoke the command below for an **ephemeral change**. Note that if changing the map count on a one-time basis, it will revert to the old value after the system is rebooted. - - -``` -sudo sysctl -w vm.max_map_count=1048576 -``` - -Invoke the command below for a **persistent change**: -``` -echo 'vm.max_map_count=1048576' | sudo tee /etc/sysctl.d/dayz.conf -``` - -==== Step 3: Prepare a Steam account with a DayZ license -Enable a Proton version ≥ `6.8` (or use Proton Experimental) in the `Compatibility` field of the game's right-click options. As of this writing, any recent version of Proton should work, and it is encouraged to use the most recent one. - -=== API key & server IDs - -==== Steam Web API key (required) -1. Register for a https://steamcommunity.com/dev/apikey[Steam Web API key] (free) using your Steam account. You will be asked for a unique URL for your app when registering. -2. Since this key is for a personal use application and does not actually call back anywhere, set a generic local identifier here like "127.0.0.1" or some other name that is meaningful to you. -3. Once configured, you can insert this key in the app when launching it for the first time. - -[NOTE] -If you are confused about this requirement, please refer to DZGUI Knowledge Base article https://aclist.github.io/dzgui/kb.html#DZG-007[DZG-007] for additional information. - -==== BattleMetrics API key (optional) - -This key is optional. Using this key in conjunction with the above allows you to also connect to and query servers by numerical ID instead of by IP. See < Add server by ID, Add server by ID>>. - -1. Register for an API key at https://www.battlemetrics.com/account/register?after=%2Fdevelopers[BattleMetrics] (free). -2. From the **Personal Access Tokens** area, Select **New Token**. -3. Give the token any name in the field at the top. -4. Leave all options **unchecked** and scroll to the bottom, select **Create Token**. -5. Once configured, you can insert this key in the app when launching it for the first time (optional), or later on when using the connect/query by ID methods in the app for the first time. - -=== First-time launch - -It is always advised to have Steam running in the background. DZGUI is meant to run "on top of" Steam, and will warn you if Steam appears to not be running. - -DZGUI can be launched one of two ways. - -**From a terminal:** - -``` -./dzgui.sh -``` - -Launching from a terminal may give more verbose information in the event of a crash, and can be a good way of troubleshooting problems. - -**From the shortcut shipped with the application**: - -If you are using a desktop environment (DE) based on the Freedesktop specification, shortcuts will be installed for you. - -- One shortcut is located under the "Games" category of your system's applications list. -- The other is accessed via the "DZGUI" desktop icon (Steam Deck only) - -After launching the app, follow the menu prompts onscreen. You will be asked to provide the following: - -- Player name (a handle name that identifies your character; required by some servers) -- Steam API key (required) -- BM API key (optional) - -==== Steam path discovery - -DZGUI will now attempt to locate your default Steam installation and DayZ path. You *must* have DayZ installed in your Steam library in order to proceed. (It can be installed to any drive of your choosing.) - -If DZGUI cannot find Steam or cannot find DayZ installed at the detected Steam path, it will prompt you to manually specify the path to your Steam installation. - -Specify the root, top-level entry point to Steam, not DayZ. E.g., - -`/media/mydrive/Steam`, not `/media/mydrive/Steam/steamapps/common/DayZ` - -If your Steam installation is in a hidden folder but the file picker dialog does not show hidden folders, ensure that your GTK settings are set to show hidden files. - -For GTK 2, update the file `$HOME/.config/gtk-2.0/gtkfilechooser.ini` to contain this line: - -`ShowHidden=true` - -For GTK 3, invoke the command: - -`gsettings set org.gtk.Settings.FileChooser show-hidden true` - -=== Steam integration & artwork - -==== Adding DZGUI as a third-party app - -DZGUI can be added to Steam as a "non-Steam game" in order to facilitate integration with Steam Deck or desktop environments. - -1. Launch Steam in the "Large" (default) view. - -[NOTE] -Steam Deck: you must switch to "Desktop Mode" and launch Steam from the desktop. Steam Deck's Game Mode view has limited support for configuration of custom games. - -[start=2] -2. Select **Add a Game** > **Add a Non-Steam Game** from the lower left-hand corner of the Steam client. - -image::https://github.com/aclist/dztui/raw/dzgui/images/tutorial/01.png[01,500] - -[start=3] -3. Navigate to `$HOME/.local/share/applications/` and select `dzgui.desktop` -4. Select **Add Selected Programs** to add a shortcut to DZGUI. - -==== Artwork - -DZGUI also ships with Steam cover artwork. It is located under: - -``` -$HOME/.local/share/dzgui -``` - -The artwork consists of five parts: - -[%autowidth] -|=== -|Name|Description - -|Hero|a large horizontal banner used on the app's details page, and on landscape-orientation covers in the Recent Games section -|Icon|a square icon used for the tree/list view of the Steam library -|Grid|a vertical "box art" cover used on Steam library pages -|Logo|a transparent icon used to remove Steam's default app text -|dzgui|used by freedesktop shortcut to generate a desktop icon; not intended for manual use by the user -|=== - -===== Updating the artwork - -1. From the main library view, navigate to the app's details page and right-click the blank image header at the top. - -image::https://github.com/aclist/dztui/raw/dzgui/images/tutorial/03.png[03,700] - -[start=2] -2. Select **Set Custom Background** -3. Select to display **All Files** from the File type dropdown -4. Navigate to the artwork path described above and select `hero.png`. -5. Next, right-click the image background and select **Set Custom Logo**. - -image::https://github.com/aclist/dztui/raw/dzgui/images/tutorial/04.png[04,700] - -[start=5] -5. Navigate to the same path and select `logo.png`. Notice that this removes the redundant app name that occluded the image. - -image::https://github.com/aclist/dztui/raw/dzgui/images/tutorial/05.png[05,700] - -[start=6] -6. Next, navigate to your Library index (looks like a bookshelf of cover art) and find the DZGUI app. - -[start=7] -7. Right-click its cover and select **Manage** > **Set custom artwork**. - -image::https://github.com/aclist/dztui/raw/dzgui/images/tutorial/06.png[06,700] - -[start=8] -8. Navigate to the same path and select `grid.png`. The final result: - -image::https://github.com/aclist/dztui/raw/dzgui/images/tutorial/07.png[07,700] - -[start=9] -9. Right-click the DZGUI entry and select Properties to open the properties dialog. Next to the **Shortcut** field, you will see a small square box which represents the game's icon. Click this to open a file explorer and select `icon.png` from the path above. This will add a small icon to the list view. - -image::https://github.com/aclist/dztui/raw/dzgui/images/tutorial/08.png[08,700] - -[start=10] -10. After you launch DZGUI for the first time, you should return to the library view and select the Recent Games dropdown on the right-hand side. Steam shows a collection of box art based on categories like "Play Next", "Recent Games", etc. Look for a downward-pointing caret icon and click it, then select the "Recent Games" category. If DZGUI was the last item played, it will be shown with a landscape, rather than portrait, orientation cover, which is initially blank. To customize this cover, right click it and select `Manage > Set custom artwork`, then select the `hero.png` image again for this area. - -image::https://github.com/aclist/dztui/raw/dzgui/images/tutorial/09.png[09,700] - -==== Controller layout - -A controller layout for Steam Deck is available in the Steam community layouts section. Search for "DZGUI Official Config" to download it. This layout provides modal layers intended to facilitate interaction with the DZGUI interface, but does not attempt to customize in-game DayZ controls in a detailed fashion. - -Long-press the View button and Select button (☰) to toggle D-pad navigation. This creates an additional layer that lets you navigate through menus using the D-pad and A/B to respectively confirm selections and go back. Remember to toggle this layer off again after launching your game to revert back to the master layer. - - -=== Updating the app -If DZGUI detects a new upstream version, it will prompt you to download it automatically. -It backs up the original version before fetching the new one, then updates your config file with your existing values. Once finished, it will ask you to relaunch the app. - -If you decline to upgrade to the new version, DZGUI will continue to the main menu with the current version. - -[NOTE] -New versions may include changes to bugs that could prevent you from playing on certain servers. -Upgrading is always advised. - -If you experience a problem or need to restore the prior version of DZGUI and/or your configs, -it is enough to simply replace the new version with the old one and relaunch the app. -The file can be found at: - -``` -/