New updated desktop with more functionality.
This commit is contained in:
1 parent
6bd3b13284
commit
cf837d5e02
5 files changed
+551
-133
No files matched your search
Executable
+536
@@ -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()
|
||||
@@ -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
|
||||
Reference in new issue
Block a user