New updated desktop with more functionality.

This commit is contained in:
Storm Dragon committed 2026-09-29 02:41:37 -04:00
1 parent 6bd3b13284
commit cf837d5e02
5 files changed
+551 -133

No files matched your search

+1 -1
View File
@@ -345,7 +345,7 @@ This is only available if you chose multiple keyboard layouts during setup.
I38 uses minimal window decorations. Windows do not use the usual title bars with minimize and maximize buttons. Instead, windows fill their available space automatically, and most interactions are performed through keyboard shortcuts.
`@MODKEY@` + `Control` + `d`: Show desktop icons.<br>
`@MODKEY@` + `Control` + `d`: Open the I38 Desktop dialog. Use Up and Down to move through items, hold Shift with Up or Down to select multiple items, and press Control+Space to clear the selection. Type to search, press Enter to open the focused item, or press Shift+F10 for a context menu. Escape clears an active search and returns to the list; press it again to close the dialog. Right-click opens the context menu without clearing selected items. Delete permanently removes selected desktop files or folders after confirmation, including the contents of selected folders. Shortcut entries are kept. Use Tab and Shift+Tab to reach the search field and buttons.<br>
## Clipboard Management
+8 -2
View File
@@ -20,8 +20,8 @@ Required for the setup script and generated I38 configuration:
- i3-wm: The i3 window manager.
- jq: For reading i3 state.
- pandoc or markdown: To generate html files.
- python3: For core I38 scripts, including the run dialog.
- python-gobject: For the accessible GTK application menu and run dialog.
- python3 (3.6 or newer): For core I38 scripts, including the desktop and run dialogs.
- python-gobject: For the accessible GTK application menu, run dialog, and desktop dialog.
- python-i3ipc: For i3 integration scripts.
- rsync: For installing I38 scripts without generated Python cache files.
- yad: For screen reader accessible dialogs.
@@ -120,6 +120,12 @@ You can apply the same configuration to GTK2 apps. Create or edit ~/.gtkrc-2.0
Default configuration mode uses I38Lock when it is installed and supported by the screen-reader setup. In that case I38 locks on startup and autolocks after 5 minutes when `xprintidle` is available.
## Desktop Dialog
Press the configured desktop shortcut, Mod+Control+D (Super+Control+D or Alt+Control+D, depending on your setup), to open the I38 Desktop dialog. Use Up and Down to move through items, hold Shift with Up or Down to select multiple items, and press Control+Space to clear the selection. Type to search, and press Enter to open the focused item. Escape clears an active search and returns to the list; press it again to close the dialog. Use Tab and Shift+Tab to reach the search field and buttons. Press Shift+F10 for the context menu; this also opens with the right-click button without clearing selected items. Press Delete to permanently remove selected desktop files or folders after confirmation. Deleting a folder also deletes everything inside it. Shortcut entries are kept.
Quick-launch shortcut entries come from `~/.config/i3/desktop_shortcuts`. They are not deleted from disk by the desktop dialog.
## Dependency Helpers
Distro-specific dependency helpers live in `extra/`. On Arch Linux, run:
+6 -3
View File
@@ -33,6 +33,9 @@ if ! command -v xdg-open &> /dev/null ; then
missing+=("xdg-utils")
fi
if command -v python3 &> /dev/null ; then
if ! python3 -c 'import sys; sys.exit(sys.version_info < (3, 6))' &> /dev/null ; then
missing+=("python3>=3.6")
fi
if ! python3 -c 'import i3ipc' &> /dev/null ; then
missing+=("python-i3ipc")
fi
@@ -1306,8 +1309,8 @@ bindsym \$mod+F4 kill
# Applications menu
bindsym \$mod+F1 exec --no-startup-id "${i3Path}/scripts/menu.py"
# Desktop icons
bindsym \$mod+Control+d exec --no-startup-id ${i3Path}/scripts/desktop.sh
# Desktop dialog
bindsym \$mod+Control+d exec --no-startup-id ${i3Path}/scripts/desktop.py
# change focus
# Focus previous window (alt+shift+tab)
@@ -1639,7 +1642,7 @@ case "${fileBrowser##*/}" in
;;
esac
echo "exec_always --no-startup-id ${i3Path}/scripts/i38-clipboard.py --daemon"
echo "exec_always --no-startup-id ${i3Path}/scripts/desktop.sh"
echo "exec_always --no-startup-id ${i3Path}/scripts/desktop.py"
echo "exec_always --no-startup-id ${i3Path}/scripts/i38-update-check.sh"
if [[ $dex -eq 0 ]]; then
echo '# Start XDG autostart .desktop files using dex. See also'
+536
View File
@@ -0,0 +1,536 @@
#!/usr/bin/env python3
# This file is part of I38.
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
# either version 3 of the License, or (at your option) any later version.
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
import os
import re
import shlex
import shutil
import subprocess
from collections import namedtuple
from pathlib import Path
import gi
gi.require_version("Atk", "1.0")
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gio, GLib, Gtk
try:
gi.require_version("GioUnix", "2.0")
from gi.repository import GioUnix
DesktopAppInfo = GioUnix.DesktopAppInfo
except (ImportError, ValueError):
DesktopAppInfo = Gio.DesktopAppInfo
APP_NAME = "I38 Desktop"
DesktopItem = namedtuple("DesktopItem", ("name", "kind", "path", "command"))
def build_launch_command(command, workspace):
if not workspace or shutil.which("i3-msg") is None:
return command
if re.fullmatch(r"[0-9]+", workspace):
return f"i3-msg workspace number {workspace} && {command}"
return f"i3-msg workspace {shlex.quote(workspace)} && {command}"
def read_shortcuts():
config_home = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
shortcuts_file = config_home / "i3" / "desktop_shortcuts"
if not shortcuts_file.is_file():
return []
items = []
for raw_line in shortcuts_file.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
fields = line.split("|")
if len(fields) < 2:
continue
name = fields[0].strip()
command = fields[1].strip()
workspace = fields[2].strip() if len(fields) > 2 else ""
if not name or not command:
continue
items.append(
DesktopItem(
name=name,
kind="Shortcut",
path=None,
command=build_launch_command(command, workspace),
)
)
return items
def desktop_app_info(path):
try:
return DesktopAppInfo.new_from_filename(str(path))
except (TypeError, GLib.Error):
return None
def desktop_entry_name(path):
app_info = desktop_app_info(path)
if app_info is not None:
name = app_info.get_display_name()
if name:
return name
return path.name
def read_desktop_items():
desktop_path = Path.home() / "Desktop"
if not desktop_path.is_dir():
return []
directories = sorted(
(path for path in desktop_path.iterdir() if path.is_dir()),
key=lambda path: path.name.casefold(),
)
files = sorted(
(path for path in desktop_path.iterdir() if not path.is_dir()),
key=lambda path: path.name.casefold(),
)
items = []
for path in directories:
items.append(DesktopItem(path.name, "Folder", path, None))
for path in files:
kind = "Desktop entry" if path.suffix.lower() == ".desktop" else "File"
name = desktop_entry_name(path) if kind == "Desktop entry" else path.name
items.append(DesktopItem(name, kind, path, None))
return items
class DesktopWindow(Gtk.Window):
def __init__(self, items):
super().__init__(title=APP_NAME)
self.set_default_size(640, 420)
self.set_border_width(12)
self.set_position(Gtk.WindowPosition.CENTER)
self.set_skip_taskbar_hint(True)
self.set_skip_pager_hint(True)
self.connect("destroy", Gtk.main_quit)
self.connect("focus-out-event", self.on_focus_out)
self.suppress_focus_close = False
self.filter_text = ""
self.store = Gtk.ListStore(str, str, str, str)
self.filter_model = self.store.filter_new()
self.filter_model.set_visible_func(self.visible_item)
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
self.add(main_box)
search_label = Gtk.Label(label="_Search desktop items:")
search_label.set_xalign(0)
main_box.pack_start(search_label, False, False, 0)
self.search_entry = Gtk.SearchEntry()
self.search_entry.set_placeholder_text("Search desktop items")
self.search_entry.connect("changed", self.on_search_changed)
self.search_entry.connect("key-press-event", self.on_search_key_press)
search_label.set_mnemonic_widget(self.search_entry)
main_box.pack_start(self.search_entry, False, False, 0)
self.view = Gtk.TreeView(model=self.filter_model)
self.view.set_headers_visible(True)
self.view.set_enable_search(False)
self.view.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
self.view.connect("row-activated", self.on_row_activated)
self.view.connect("button-press-event", self.on_tree_button_press)
self.view.connect("key-press-event", self.on_tree_key_press)
name_column = Gtk.TreeViewColumn("Item", Gtk.CellRendererText(), text=0)
kind_column = Gtk.TreeViewColumn("Type", Gtk.CellRendererText(), text=1)
self.view.append_column(name_column)
self.view.append_column(kind_column)
scrolled = Gtk.ScrolledWindow()
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scrolled.add(self.view)
main_box.pack_start(scrolled, True, True, 0)
self.status = Gtk.Statusbar()
self.status_context_id = self.status.get_context_id("desktop-items")
main_box.pack_start(self.status, False, False, 0)
button_box = Gtk.ButtonBox(orientation=Gtk.Orientation.HORIZONTAL)
button_box.set_layout(Gtk.ButtonBoxStyle.END)
open_button = Gtk.Button.new_with_mnemonic("_Open")
open_button.connect("clicked", self.on_open_clicked)
delete_button = Gtk.Button.new_with_mnemonic("_Delete")
delete_button.connect("clicked", self.on_delete_clicked)
close_button = Gtk.Button.new_with_mnemonic("_Close")
close_button.connect("clicked", lambda _button: self.close())
button_box.add(close_button)
button_box.add(delete_button)
button_box.add(open_button)
main_box.pack_start(button_box, False, False, 0)
self.get_accessible().set_name(APP_NAME)
self.get_accessible().set_description(
"Select a desktop item and press Enter to open it. "
"Use Shift with Up or Down to select multiple items. "
"Press Control+Space to clear the selection. "
"Escape clears an active search, then closes the dialog. "
"Press Shift+F10 for the context menu or Delete to permanently remove selected items."
)
self.search_entry.get_accessible().set_name("Search desktop items")
self.view.get_accessible().set_name("Desktop items")
self.status.get_accessible().set_name("Desktop status")
self.load_items(items)
self.select_first_item()
itemCount = len(items)
self.set_status(f"{itemCount} {'item' if itemCount == 1 else 'items'} found.")
self.show_all()
self.view.grab_focus()
def load_items(self, items):
self.store.clear()
for item in items:
self.store.append(
(
item.name,
item.kind,
str(item.path) if item.path is not None else "",
item.command or "",
)
)
def visible_item(self, model, tree_iter, _data=None):
if not self.filter_text:
return True
name = model.get_value(tree_iter, 0).casefold()
kind = model.get_value(tree_iter, 1).casefold()
return self.filter_text in name or self.filter_text in kind
def select_first_item(self):
tree_iter = self.filter_model.get_iter_first()
if tree_iter is None:
self.view.get_selection().unselect_all()
return
self.view.set_cursor(self.filter_model.get_path(tree_iter), None, False)
def selected_items(self):
model, paths = self.view.get_selection().get_selected_rows()
items = []
for path in paths:
tree_iter = model.get_iter(path)
item_path = model.get_value(tree_iter, 2)
child_path = self.filter_model.convert_path_to_child_path(path)
items.append((
child_path.get_indices()[0],
path.to_string(),
DesktopItem(
name=model.get_value(tree_iter, 0),
kind=model.get_value(tree_iter, 1),
path=Path(item_path) if item_path else None,
command=model.get_value(tree_iter, 3) or None,
),
))
return items
def selected_item(self):
items = self.selected_items()
if not items:
return None
cursor_path, _column = self.view.get_cursor()
if cursor_path is not None:
for _index, path, item in items:
if path == cursor_path.to_string():
return item
return items[0][2]
def set_status(self, message):
self.status.remove_all(self.status_context_id)
self.status.push(self.status_context_id, message)
def show_error(self, message):
self.suppress_focus_close = True
dialog = Gtk.MessageDialog(
transient_for=self,
flags=Gtk.DialogFlags.MODAL,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text="I38 Desktop",
)
dialog.format_secondary_text(message)
dialog.run()
dialog.destroy()
GLib.idle_add(self.allow_focus_close)
def open_selected(self):
item = self.selected_item()
if item is None:
self.set_status("Select an item to open.")
return
try:
if item.command is not None:
subprocess.Popen(
["/usr/bin/env", "bash", "-c", item.command],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
elif item.path is not None and item.path.suffix.lower() == ".desktop":
app_info = desktop_app_info(item.path)
if app_info is None:
subprocess.Popen(
["xdg-open", str(item.path)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
elif not app_info.launch([], None):
self.show_error(f"Could not open {item.name}.")
return
elif item.path is not None:
subprocess.Popen(
["xdg-open", str(item.path)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
except (OSError, GLib.Error) as error:
self.show_error(f"Could not open {item.name}.\n\n{error}")
return
self.close()
def delete_selected(self):
selected = self.selected_items()
if not selected:
self.set_status("Select a desktop item to delete.")
return
deletable = [(index, item) for index, _path, item in selected if item.path is not None]
if not deletable:
self.set_status("Shortcut entries are configured in desktop_shortcuts.")
return
self.suppress_focus_close = True
dialog = Gtk.MessageDialog(
transient_for=self,
flags=Gtk.DialogFlags.MODAL,
message_type=Gtk.MessageType.QUESTION,
buttons=Gtk.ButtonsType.NONE,
text="Delete desktop item",
)
if len(deletable) > 1:
folder_selected = any(
item.path.is_dir() and not item.path.is_symlink()
for _index, item in deletable
)
message = f"Permanently delete {len(deletable)} selected desktop items"
if folder_selected:
message += " and everything inside the selected folders"
dialog.format_secondary_text(message + "?")
elif deletable[0][1].path.is_dir() and not deletable[0][1].path.is_symlink():
item = deletable[0][1]
dialog.format_secondary_text(
f"Permanently delete {item.name} and everything inside it?"
)
else:
dialog.format_secondary_text(f"Permanently delete {deletable[0][1].name}?")
dialog.add_buttons(
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
"_Delete", Gtk.ResponseType.OK,
)
dialog.set_default_response(Gtk.ResponseType.CANCEL)
response = dialog.run()
dialog.destroy()
GLib.idle_add(self.allow_focus_close)
if response != Gtk.ResponseType.OK:
return
deleted = []
failures = []
for index, item in deletable:
try:
if item.path.is_dir() and not item.path.is_symlink():
shutil.rmtree(item.path)
else:
item.path.unlink()
except OSError as error:
failures.append((item.name, error))
else:
deleted.append(index)
for index in sorted(deleted, reverse=True):
self.store.remove(self.store.get_iter_from_string(str(index)))
if deleted:
self.select_first_item()
if failures:
name, error = failures[0]
self.show_error(
f"Deleted {len(deleted)} items. Could not delete {len(failures)} items.\n\n"
f"{name}: {error}"
)
else:
count = len(deleted)
self.set_status(f"Deleted {count} {'item' if count == 1 else 'items'}.")
def show_context_menu(self, event=None):
item = self.selected_item()
if item is None:
self.set_status("Select an item to open its context menu.")
return True
menu = Gtk.Menu()
open_item = Gtk.MenuItem.new_with_mnemonic("_Open")
open_item.connect("activate", lambda _menu_item: self.open_selected())
menu.append(open_item)
if any(selected.path is not None for _index, _path, selected in self.selected_items()):
delete_item = Gtk.MenuItem.new_with_mnemonic("_Delete")
delete_item.connect("activate", lambda _menu_item: self.delete_selected())
menu.append(delete_item)
menu.attach_to_widget(self.view, None)
menu.show_all()
self.suppress_focus_close = True
menu.connect("deactivate", lambda _menu: GLib.idle_add(self.allow_focus_close))
if event is not None:
menu.popup_at_pointer(event)
else:
menu.popup_at_widget(
self.view,
Gdk.Gravity.CENTER,
Gdk.Gravity.CENTER,
None,
)
return True
def allow_focus_close(self):
self.suppress_focus_close = False
return False
def on_focus_out(self, _widget, _event):
if self.suppress_focus_close:
return False
self.close()
return False
def on_search_changed(self, _entry):
self.filter_text = self.search_entry.get_text().strip().casefold()
self.filter_model.refilter()
self.select_first_item()
visible_count = 0
tree_iter = self.filter_model.get_iter_first()
while tree_iter is not None:
visible_count += 1
tree_iter = self.filter_model.iter_next(tree_iter)
noun = "item" if visible_count == 1 else "items"
self.set_status(f"{visible_count} {noun} found.")
def clear_search_or_close(self):
if self.search_entry.get_text():
self.search_entry.set_text("")
self.view.grab_focus()
else:
self.close()
def on_search_key_press(self, _entry, event):
if event.keyval in (Gdk.KEY_Down, Gdk.KEY_KP_Down):
self.select_first_item()
self.view.grab_focus()
return True
if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
self.open_selected()
return True
if event.keyval == Gdk.KEY_Escape:
self.clear_search_or_close()
return True
return False
def on_tree_key_press(self, _view, event):
if event.keyval in (Gdk.KEY_space, Gdk.KEY_KP_Space) and event.state & Gdk.ModifierType.CONTROL_MASK:
self.view.get_selection().unselect_all()
self.set_status("Selection cleared.")
return True
if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
self.open_selected()
return True
if event.keyval == Gdk.KEY_F10 and event.state & Gdk.ModifierType.SHIFT_MASK:
return self.show_context_menu()
if event.keyval in (Gdk.KEY_Delete, Gdk.KEY_KP_Delete):
self.delete_selected()
return True
if event.keyval == Gdk.KEY_Escape:
self.clear_search_or_close()
return True
characterCode = Gdk.keyval_to_unicode(event.keyval)
if (characterCode >= 32 and not event.state &
(Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.MOD1_MASK |
Gdk.ModifierType.SUPER_MASK | Gdk.ModifierType.META_MASK)):
character = chr(characterCode)
self.search_entry.grab_focus()
self.search_entry.insert_text(character, -1)
self.search_entry.set_position(-1)
return True
return False
def on_tree_button_press(self, view, event):
if event.type != Gdk.EventType.BUTTON_PRESS or event.button != Gdk.BUTTON_SECONDARY:
return False
path_info = view.get_path_at_pos(int(event.x), int(event.y))
if path_info is not None:
path, column = path_info[0], path_info[1]
selection = view.get_selection()
selected_paths = selection.get_selected_rows()[1] if selection.path_is_selected(path) else []
view.set_cursor(path, column, False)
for selected_path in selected_paths:
selection.select_path(selected_path)
view.grab_focus()
return self.show_context_menu(event)
def on_row_activated(self, _view, _path, _column):
self.open_selected()
def on_open_clicked(self, _button):
self.open_selected()
def on_delete_clicked(self, _button):
self.delete_selected()
def main():
GLib.set_application_name(APP_NAME)
items = read_shortcuts() + read_desktop_items()
DesktopWindow(items)
Gtk.main()
if __name__ == "__main__":
main()
-127
View File
@@ -1,127 +0,0 @@
#!/usr/bin/env bash
# This file is part of I38.
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
# either version 3 of the License, or (at your option) any later version.
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
trim_whitespace() {
local text="$1"
text="${text#"${text%%[![:space:]]*}"}"
text="${text%"${text##*[![:space:]]}"}"
printf '%s' "$text"
}
get_shortcuts_file() {
local configHome="${XDG_CONFIG_HOME:-$HOME/.config}"
local candidatePath="${configHome}/i3/desktop_shortcuts"
if [[ -r "$candidatePath" ]]; then
printf '%s\n' "$candidatePath"
return 0
fi
return 1
}
build_launch_command() {
local shortcutCommand="$1"
local shortcutWorkspace="$2"
local workspaceEscaped=
if [[ -z "$shortcutWorkspace" ]]; then
printf '%s\n' "$shortcutCommand"
return 0
fi
if ! command -v i3-msg &> /dev/null; then
printf '%s\n' "$shortcutCommand"
return 0
fi
if [[ "$shortcutWorkspace" =~ ^[0-9]+$ ]]; then
printf 'i3-msg workspace number %s && %s\n' "$shortcutWorkspace" "$shortcutCommand"
return 0
fi
workspaceEscaped=$(printf '%q' "$shortcutWorkspace")
printf 'i3-msg workspace %s && %s\n' "$workspaceEscaped" "$shortcutCommand"
}
desktopPath="${HOME}/Desktop"
if [[ ! -d "$desktopPath" ]]; then
exit 0
fi
declare -a shortcutMenuList
shortcutsFile="$(get_shortcuts_file)"
if [[ -n "$shortcutsFile" ]]; then
# Optional quick-launch format:
# Pretty Name|command with args|workspace
# Firefox|/usr/bin/firefox|9
# Brave|/usr/bin/brave|
while IFS= read -r rawLine || [[ -n "$rawLine" ]]; do
line="$(trim_whitespace "$rawLine")"
if [[ -z "$line" ]] || [[ "$line" =~ ^# ]]; then
continue
fi
IFS='|' read -r shortcutName shortcutCommand shortcutWorkspace _ <<< "$line"
shortcutName="$(trim_whitespace "$shortcutName")"
shortcutCommand="$(trim_whitespace "$shortcutCommand")"
shortcutWorkspace="$(trim_whitespace "$shortcutWorkspace")"
if [[ -z "$shortcutName" ]] || [[ -z "$shortcutCommand" ]]; then
continue
fi
shortcutMenuList+=("$shortcutName" "$(build_launch_command "$shortcutCommand" "$shortcutWorkspace")")
done < "$shortcutsFile"
fi
# Discover directories and files on the desktop.
mapfile -t desktopDirs < <(find "$desktopPath" -mindepth 1 -maxdepth 1 -type d)
mapfile -t desktopFiles < <(find "$desktopPath" -mindepth 1 -maxdepth 1 -type f)
desktopItems=("${desktopDirs[@]}" "${desktopFiles[@]}")
# Build menu for yad.
declare -a desktopMenuList
for desktopItem in "${desktopItems[@]}"; do
case "$desktopItem" in
*.desktop)
desktopName="$(grep -m 1 "^Name=" "$desktopItem" | cut -f2- -d '=')"
if [[ -z "$desktopName" ]]; then
desktopName="${desktopItem##*/}"
fi
desktopMenuList+=("$desktopName" "gio launch \"$desktopItem\"")
;;
*)
desktopMenuList+=("${desktopItem##*/}" "/usr/bin/env xdg-open \"$desktopItem\"")
;;
esac
done
menuList=("${shortcutMenuList[@]}" "${desktopMenuList[@]}")
if [[ "${#menuList[@]}" -eq 0 ]]; then
exit 0
fi
if launch=$(yad --list \
--title="I38 Desktop" \
--column="Items" \
--column="Launch Command" \
--close-on-unfocus \
--hide-column=2 \
--search-column=1 \
--skip-taskbar \
"${menuList[@]}"); then
launch="${launch%|}"
launch="${launch##*|}"
eval "$launch"
fi