Brand new run dialogue. This hasn't been updated all that much since the days of Strychnine. It's time for some modernizations. I think it's much improved, but if y'all hate it let me know and I'll either fix or revert it.

This commit is contained in:
Storm Dragon
2026-06-01 16:44:15 -04:00
parent 23a1ba93b8
commit e5bd73213d
7 changed files with 675 additions and 69 deletions
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -u
cacheFiles=()
while IFS= read -r stagedFile; do
case "$stagedFile" in
__pycache__/* | */__pycache__/* | *.pyc | *.pyo | *.pyd)
cacheFiles+=("$stagedFile")
;;
esac
done < <(git diff --cached --name-only --diff-filter=ACMR)
if [[ ${#cacheFiles[@]} -eq 0 ]]; then
exit 0
fi
printf 'Commit blocked: remove generated Python cache files from the index:\n' >&2
printf ' %s\n' "${cacheFiles[@]}" >&2
printf '\nUse git rm --cached -- <file> to unstage forced additions.\n' >&2
exit 1
+6 -1
View File
@@ -258,11 +258,16 @@ This audio feedback provides non-visual confirmation of actions and state change
Access applications in multiple ways:
- Applications menu: `@MODKEY@` + `F1`
- Run dialog (enter a command): `@MODKEY@` + `F2` or in Ratpoison mode, `!` (exclamation mark)
- Run dialog: `@MODKEY@` + `F2` or in Ratpoison mode, `!` (exclamation mark)
- Common applications have dedicated shortcuts in Ratpoison mode (see table above)
The applications menu is organized by categories similar to traditional desktop environments.
The run dialog suggests previous entries, installed applications, and commands
from your `PATH` as you type. Enter a path such as `~/Downloads/` to browse its
files and subdirectories. Use the up and down arrows to select a suggestion,
Tab to insert it into the entry, and Enter to open or run it.
*GNOME/MATE comparison:* Instead of clicking on application icons or using a start menu, I38 provides keyboard shortcuts to access applications.
+15 -1
View File
@@ -20,8 +20,12 @@ 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.
- python-i3ipc: For i3 integration scripts.
- rsync: For installing I38 scripts without generated Python cache files.
- yad: For screen reader accessible dialogs.
- xdg-utils: For opening files and URLs from the run dialog.
- xdotool: For X11 window interaction.
Optional features use these packages when installed:
@@ -35,7 +39,6 @@ Optional features use these packages when installed:
- pamixer: for the mute-unmute script
- pcmanfm: [optional] Graphical file manager.
- playerctl: music controls
- python-gobject: for applications menu.
- python-pillow: For OCR
- python-pytesseract: For OCR
- remind: [optional] For reminder notifications, Requires notify-daemon and notify-send for automatic reminders.
@@ -110,6 +113,17 @@ You can apply the same configuration to GTK2 apps. Create or edit ~/.gtkrc-2.0
- -x: Generate ~/.xinitrc and ~/.xprofile.
- -X: Generate ~/.xprofile only.
## Development
Enable the tracked pre-commit hooks once after cloning:
```bash
git config --local core.hooksPath .githooks
```
The pre-commit hook rejects staged Python cache files. The I38 script installer
also excludes and removes Python cache artifacts when copying `scripts/`.
## Ratpoison Mode
+46 -11
View File
@@ -21,13 +21,22 @@ i38CheckoutPath="${PWD}"
export DIALOGOPTS='--no-lines --visit-items'
# Check to make sure minimum requirements are installed.
for i in dialog jq yad xdotool ; do
declare -a missing=()
for i in dialog jq python3 rsync yad xdotool ; do
if ! command -v "$i" &> /dev/null ; then
missing+=("$i")
fi
done
if ! python3 -c 'import i3ipc' &> /dev/null ; then
missing+=("python-i3ipc")
if ! command -v xdg-open &> /dev/null ; then
missing+=("xdg-utils")
fi
if command -v python3 &> /dev/null ; then
if ! python3 -c 'import i3ipc' &> /dev/null ; then
missing+=("python-i3ipc")
fi
if ! python3 -c 'import gi; gi.require_version("Atk", "1.0"); gi.require_version("Gdk", "3.0"); gi.require_version("Gio", "2.0"); gi.require_version("GLib", "2.0"); gi.require_version("Gtk", "3.0"); from gi.repository import Atk, Gdk, Gio, GLib, Gtk' &> /dev/null ; then
missing+=("python-gobject")
fi
fi
if [[ ${#missing[@]} -gt 0 ]]; then
echo "Please install the following packages and run this script again:"
@@ -286,12 +295,22 @@ terminal_command() {
local terminalName="${terminalPath##*/}"
if [[ "$terminalName" == "xterm" ]]; then
printf '%s -T "I38 terminal" -e "fenrir -x"' "$terminalPath"
printf '%s -T "I38 terminal" -e "env ORC_CODE=backup fenrir -x"' "$terminalPath"
else
printf '%s' "$terminalPath"
fi
}
screen_reader_command() {
local screenReaderPath="$1"
if [[ "${screenReaderPath##*/}" == "cthulhu" ]]; then
printf 'env ORC_CODE=backup %s' "$screenReaderPath"
else
printf '%s' "$screenReaderPath"
fi
}
load_config() {
# Load existing configuration if available
if [[ -f "$configFile" ]]; then
@@ -476,6 +495,21 @@ apply_screenlock_pin() {
chmod 600 "$pinFile"
}
copy_scripts() {
mkdir -p "${i3Path}/scripts"
rsync -rv \
--exclude='__pycache__/' \
--exclude='*.pyc' \
--exclude='*.pyo' \
--exclude='*.pyd' \
scripts/ "${i3Path}/scripts/"
find "${i3Path}/scripts" -type f \
\( -name '*.pyc' -o -name '*.pyo' -o -name '*.pyd' \) \
-delete
find "${i3Path}/scripts" -depth -type d -name '__pycache__' \
-exec rm -rf -- {} +
}
update_scripts() {
local existingPinHash=""
local pinFile="${i3Path}/.screenpin"
@@ -487,7 +521,7 @@ update_scripts() {
source "$configFile"
existingPinHash="$screenlockPinHash"
fi
cp -rv scripts/ "${i3Path}/" | dialog --backtitle "I38" --progressbox "Updating scripts..." -1 -1
copy_scripts | dialog --backtitle "I38" --progressbox "Updating scripts..." -1 -1
write_i38_version
write_desktop_shortcuts_template
if [[ -n "$existingPinHash" ]]; then
@@ -827,7 +861,7 @@ if [[ -z "$dex" ]]; then
fi
fi
if [[ $dex -eq 0 ]] && [[ "$screenReader" != "none" ]] && [[ -n "$screenReader" ]]; then
dex -t "${XDG_CONFIG_HOME:-${HOME}/.config}/autostart" -c "$(command -v "$screenReader")"
dex -t "${XDG_CONFIG_HOME:-${HOME}/.config}/autostart" -c "$(screen_reader_command "$screenReader")"
fi
if [[ -z "$batteryAlert" ]]; then
if command -v acpi &> /dev/null ; then
@@ -937,6 +971,7 @@ if [[ $configLoaded -eq 0 || $configChanged -eq 1 ]]; then
fi
terminalExec="$(terminal_command "$terminalEmulator")"
screenReaderExec="$(screen_reader_command "$screenReader")"
if [[ -d "${i3Path}" ]]; then
yesno "This will replace your existing configuration at ${i3Path}. Do you want to continue?" || exit 0
@@ -951,7 +986,7 @@ write_i38_version
write_desktop_shortcuts_template
write_i38_xresources
# Move scripts into place
cp -rv scripts/ "${i3Path}/" | dialog --backtitle "I38" --progressbox "Moving scripts into place and writing config..." -1 -1
copy_scripts | dialog --backtitle "I38" --progressbox "Moving scripts into place and writing config..." -1 -1
apply_screenlock_pin
update_personal_customizations
@@ -989,7 +1024,7 @@ for_window [class="Wine"] floating enable
bindsym \$mod+Shift+F1 exec $webBrowser ${i3Path}/I38.html
# Run dialog
bindsym \$mod+F2 exec ${i3Path}/scripts/run_dialog.sh
bindsym \$mod+F2 exec ${i3Path}/scripts/run_dialog.py
# Bookmarks dialog
bindsym \$mod+Control+b exec ${i3Path}/scripts/bookmarks.sh
@@ -1287,7 +1322,7 @@ bindsym Mod1+Shift+r exec --no-startup-id ${i3Path}/scripts/slideshow_record_tog
bindsym apostrophe exec --no-startup-id ${i3Path}/scripts/window_list.sh, mode "default"
$(if command -v cthulhu &> /dev/null; then
echo "# Restart Cthulhu"
echo "bindsym Shift+c exec $(command -v cthulhu) --replace, mode \"default\""
echo "bindsym Shift+c exec env ORC_CODE=backup $(command -v cthulhu) --replace, mode \"default\""
fi)
$(if command -v orca &> /dev/null; then
echo "# Restart Orca"
@@ -1302,7 +1337,7 @@ bindsym Control+semicolon exec bash -c '$i3msg -t run_command reload && spd-say
# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)
bindsym Control+Shift+semicolon exec $i3msg -t run_command restart && spd-say -P important -Cw "I3 restarted.", mode "default"
# Run dialog with exclamation
bindsym Shift+exclam exec ${i3Path}/scripts/run_dialog.sh, mode "default"
bindsym Shift+exclam exec ${i3Path}/scripts/run_dialog.py, mode "default"
# exit i3 (logs you out of your X session)
bindsym \$mod+q exec bash -c 'yad --image "dialog-question" --title "I38" --button=yes:0 --button=no:1 --text "You pressed the exit shortcut. Do you really want to exit i3? This will end your X session." && $i3msg -t run_command exit'
bindsym Control+\$mod+q exec bash -c 'yad --image "dialog-question" --title "I38" --button=yes:0 --button=no:1 --text "You pressed the exit shortcut. Do you really want to exit i3? This will end your X session." && $i3msg -t run_command exit'
@@ -1375,7 +1410,7 @@ else
echo 'exec --no-startup-id x11bell play -nqV0 synth .1 sq norm -12'
fi
if [[ "$screenReader" != "none" ]] && [[ -n "$screenReader" ]]; then
echo "exec $screenReader"
echo "exec $screenReaderExec"
fi
fi)
+586
View File
@@ -0,0 +1,586 @@
#!/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 argparse
import os
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
from urllib.parse import urlparse
import gi
gi.require_version("Atk", "1.0")
gi.require_version("Gdk", "3.0")
gi.require_version("Gio", "2.0")
gi.require_version("GLib", "2.0")
gi.require_version("Gtk", "3.0")
from gi.repository import Atk, Gdk, Gio, GLib, Gtk
APP_NAME = "I38 Run Dialog"
MAX_HISTORY_ENTRIES = 50
MAX_RESULTS = 100
PATH_PREFIXES = ("/", "./", "../", "~/")
RESULT_FEEDBACK_DELAY_MS = 150
RESULT_SOUND = ["play", "-nqV0", "synth", "0.05", "saw", "300:1200", "vol", "0.20"]
@dataclass(frozen=True)
class Result:
kind: str
label: str
detail: str
value: str
app_id: str = ""
def state_dir():
value = os.environ.get("XDG_STATE_HOME")
if value:
return Path(value) / "I38"
return Path.home() / ".local" / "state" / "I38"
def history_path():
return state_dir() / "run-dialog-history"
def legacy_history_path():
return Path(__file__).resolve().parent / ".history"
def migrate_history():
path = history_path()
if path.exists() or not legacy_history_path().is_file():
return
path.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(legacy_history_path(), path)
def read_history():
try:
return [
line.strip()
for line in history_path().read_text(encoding="utf-8").splitlines()
if line.strip()
][:MAX_HISTORY_ENTRIES]
except OSError:
return []
def write_history(entry, items):
entry = entry.strip()
if not entry:
return
updated_items = [entry]
updated_items.extend(item for item in items if item != entry)
path = history_path()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
"\n".join(updated_items[:MAX_HISTORY_ENTRIES]) + "\n",
encoding="utf-8",
)
def path_commands():
commands = set()
for directory in os.environ.get("PATH", "").split(os.pathsep):
if not directory:
continue
try:
for item in os.scandir(directory):
if item.is_file() and os.access(item.path, os.X_OK):
commands.add(item.name)
except OSError:
continue
return sorted(commands, key=str.casefold)
def is_path_query(text):
return text.startswith(PATH_PREFIXES)
def path_results(text):
if not is_path_query(text):
return []
expanded_text = os.path.expanduser(text)
if expanded_text.endswith(os.sep):
directory = Path(expanded_text)
name_prefix = ""
display_parent = text
else:
directory = Path(expanded_text).parent
name_prefix = Path(expanded_text).name
display_parent = text[: len(text) - len(name_prefix)]
try:
items = list(os.scandir(directory))
except OSError:
return []
results = []
for item in items:
if not item.name.casefold().startswith(name_prefix.casefold()):
continue
value = f"{display_parent}{item.name}"
try:
if item.is_dir():
results.append(Result("directory", f"{item.name}/", "Directory", f"{value}/"))
elif item.is_file() and os.access(item.path, os.X_OK):
results.append(Result("file", item.name, "Executable file", value))
else:
results.append(Result("file", item.name, "File", value))
except OSError:
continue
return sorted(
results,
key=lambda item: (item.kind != "directory", item.label.casefold()),
)[:MAX_RESULTS]
def matches(query, value):
return query.casefold() in value.casefold()
def application_results(query, applications):
results = []
for app in applications:
name = app.get_display_name() or app.get_name()
executable = app.get_executable() or ""
if not matches(query, name) and not matches(query, executable):
continue
results.append(Result("application", name, f"Application: {executable}", name, app.get_id() or ""))
return sorted(results, key=lambda item: item.label.casefold())
def command_results(query, commands):
return [
Result("command", command, "Command", command)
for command in commands
if matches(query, command)
]
def history_results(query, items):
return [
Result("history", item, "History", item)
for item in items
if matches(query, item)
]
def merge_results(*groups):
results = []
seen = set()
for group in groups:
for result in group:
key = (result.kind, result.value, result.app_id)
if key in seen:
continue
seen.add(key)
results.append(result)
if len(results) >= MAX_RESULTS:
return results
return results
def normalized_path(text):
text = text.strip()
if len(text) >= 2 and text[0] == text[-1] and text[0] in ("'", '"'):
text = text[1:-1]
return Path(os.path.expanduser(text))
def raw_target_kind(text):
text = text.strip()
parsed = urlparse(text)
if parsed.scheme in ("ftp", "http", "https", "file", "mailto"):
return "uri"
if text.startswith("www."):
return "uri"
if text.startswith("man://"):
return "manual"
if normalized_path(text).exists():
return "path"
return "command"
def result_count_message(count):
return f"{count} suggestion" if count == 1 else f"{count} suggestions"
def play_result_sound():
try:
subprocess.Popen(
RESULT_SOUND,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
except OSError:
pass
class RunDialog(Gtk.Window):
def __init__(self):
super().__init__(title=APP_NAME)
self.set_default_size(720, 420)
self.set_border_width(12)
self.set_position(Gtk.WindowPosition.CENTER)
self.connect("destroy", Gtk.main_quit)
self.connect("key-press-event", self.on_window_key_press)
migrate_history()
self.history = read_history()
self.commands = path_commands()
self.applications = {
app.get_id(): app
for app in Gio.AppInfo.get_all()
if app.should_show() and app.get_id()
}
self.results = []
self.last_feedback_count = None
self.pending_feedback_count = None
self.feedback_timeout_id = None
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
self.add(main_box)
entry_label = Gtk.Label(label="_Command, application, or file:")
entry_label.set_xalign(0)
main_box.pack_start(entry_label, False, False, 0)
self.entry = Gtk.Entry()
self.entry.set_placeholder_text("Type a command, application name, or path")
self.entry.connect("changed", self.on_entry_changed)
self.entry.connect("activate", self.on_entry_activate)
self.entry.connect("key-press-event", self.on_entry_key_press)
entry_label.set_mnemonic_widget(self.entry)
main_box.pack_start(self.entry, False, False, 0)
self.store = Gtk.ListStore(str, str)
self.view = Gtk.TreeView(model=self.store)
self.view.set_headers_visible(True)
self.view.connect("row-activated", self.on_row_activated)
self.view.connect("key-press-event", self.on_view_key_press)
self.view.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
result_column = Gtk.TreeViewColumn("Result", Gtk.CellRendererText(), text=0)
detail_column = Gtk.TreeViewColumn("Type", Gtk.CellRendererText(), text=1)
self.view.append_column(result_column)
self.view.append_column(detail_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("run-dialog-results")
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)
cancel_button = Gtk.Button.new_with_mnemonic("_Cancel")
cancel_button.connect("clicked", lambda _button: self.close())
button_box.add(cancel_button)
button_box.add(open_button)
main_box.pack_start(button_box, False, False, 0)
window_accessible = self.get_accessible()
window_accessible.set_name(APP_NAME)
window_accessible.set_description(
"Type to find commands and applications. Type a path to browse files. "
"Use up and down arrows to select a result, Tab to insert a result, "
"and Enter to open it."
)
self.entry.get_accessible().set_name("Command, application, or file")
self.view.get_accessible().set_name("Run dialog suggestions")
self.view.get_accessible().set_role(Atk.Role.LIST)
self.status.get_accessible().set_name("Run dialog status")
self.refresh_results()
self.set_status("Type to search. Enter a path such as ~/doomtmp/ to browse files.")
GLib.idle_add(self.entry.grab_focus)
def refresh_results(self):
query = self.entry.get_text().strip()
if is_path_query(query):
self.results = path_results(query)
else:
self.results = merge_results(
history_results(query, self.history),
application_results(query, self.applications.values()),
command_results(query, self.commands),
)
self.store.clear()
for result in self.results:
self.store.append((result.label, result.detail))
if self.results:
self.view.get_selection().select_path(0)
self.schedule_result_feedback(len(self.results))
def set_status(self, message):
self.status.remove_all(self.status_context_id)
self.status.push(self.status_context_id, message)
def schedule_result_feedback(self, count):
if self.last_feedback_count is None:
self.last_feedback_count = count
return
if count == self.pending_feedback_count:
return
self.pending_feedback_count = count
if self.feedback_timeout_id is not None:
GLib.source_remove(self.feedback_timeout_id)
self.feedback_timeout_id = GLib.timeout_add(
RESULT_FEEDBACK_DELAY_MS,
self.announce_result_count,
)
def announce_result_count(self):
self.feedback_timeout_id = None
count = self.pending_feedback_count
if count == self.last_feedback_count:
return False
self.last_feedback_count = count
play_result_sound()
message = result_count_message(count)
self.set_status(message)
status_accessible = self.status.get_accessible()
status_accessible.emit("notification", message, Atk.Live.POLITE)
return False
def selected_result(self):
model, tree_iter = self.view.get_selection().get_selected()
if tree_iter is None:
return None
return self.results[model.get_path(tree_iter).get_indices()[0]]
def insert_selected_result(self):
result = self.selected_result()
if result is None:
return
self.entry.set_text(result.value)
self.entry.set_position(-1)
def activate(self, result=None):
if result is not None and result.kind == "directory":
self.entry.set_text(result.value)
self.entry.set_position(-1)
return
text = result.value if result is not None else self.entry.get_text().strip()
if not text:
return
write_history(text, self.history)
if result is not None and result.kind == "application":
self.launch_application(result)
return
self.launch_raw(text)
def launch_application(self, result):
app = self.applications.get(result.app_id)
if app is None:
self.show_error(f"Application is no longer available: {result.label}")
return
try:
app.launch([], None)
except GLib.Error as error:
self.show_error(f"Could not open {result.label}.\n\n{error.message}")
return
self.close()
def launch_raw(self, text):
kind = raw_target_kind(text)
if kind == "uri":
target = f"https://{text}" if text.startswith("www.") else text
command = ["xdg-open", target]
elif kind == "manual":
page = text.removeprefix("man://")
if not page:
self.show_error("Enter a manual page after man://")
return
command = [
"bash",
"-o",
"pipefail",
"-c",
'man -- "$1" | yad --text-info --show-cursor --button "Close:0" --title "I38 manual page" -',
"_",
page,
]
elif kind == "path":
path = normalized_path(text)
if path.is_file() and os.access(path, os.X_OK):
command = [str(path)]
else:
command = ["xdg-open", str(path)]
else:
command = ["bash", "-c", text]
self.start_command(command, text)
def start_command(self, command, description):
try:
process = subprocess.Popen(
command,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
start_new_session=True,
)
except OSError as error:
self.show_error(f"Could not open {description}.\n\n{error}")
return
self.hide()
GLib.timeout_add(250, self.check_process, process, description)
def check_process(self, process, description):
status = process.poll()
if status is None:
Gtk.main_quit()
return False
if status == 0:
Gtk.main_quit()
return False
error_text = process.stderr.read(2000).strip()
if not error_text:
error_text = "No additional error details were provided."
self.show_all()
self.entry.grab_focus()
self.show_error(f"Could not open {description}.\n\n{error_text}")
return False
def show_error(self, message):
dialog = Gtk.MessageDialog(
transient_for=self,
flags=Gtk.DialogFlags.MODAL,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.CLOSE,
text=message,
)
dialog.set_title(APP_NAME)
dialog.run()
dialog.destroy()
def on_entry_changed(self, _entry):
self.refresh_results()
def on_entry_activate(self, _entry):
self.activate(self.selected_result())
def on_open_clicked(self, _button):
self.activate(self.selected_result())
def on_row_activated(self, _view, path, _column):
self.activate(self.results[path.get_indices()[0]])
def on_entry_key_press(self, _entry, event):
if event.keyval in (Gdk.KEY_Down, Gdk.KEY_Up):
self.view.grab_focus()
if event.keyval == Gdk.KEY_Up and self.results:
self.view.get_selection().select_path(len(self.results) - 1)
return True
if event.keyval == Gdk.KEY_Tab:
self.insert_selected_result()
return True
return False
def on_view_key_press(self, _view, event):
if event.keyval == Gdk.KEY_BackSpace:
self.entry.grab_focus()
self.entry.set_position(-1)
self.entry.backspace()
return True
if event.keyval == Gdk.KEY_Tab:
self.insert_selected_result()
self.entry.grab_focus()
return True
if event.string and event.string.isprintable():
self.entry.grab_focus()
self.entry.set_position(-1)
self.entry.insert_text(event.string, -1)
return True
return False
def on_window_key_press(self, _window, event):
if event.keyval == Gdk.KEY_Escape:
self.close()
return True
return False
def run_self_test():
test_dir = Path(os.environ["I38_RUN_DIALOG_TEST_DIR"])
(test_dir / "folder").mkdir(parents=True)
(test_dir / "tool").write_text("#!/bin/sh\n", encoding="utf-8")
(test_dir / "note.txt").write_text("note\n", encoding="utf-8")
(test_dir / "tool").chmod(0o755)
results = path_results(f"{test_dir}/")
assert [result.label for result in results] == ["folder/", "note.txt", "tool"]
assert results[2].detail == "Executable file"
assert raw_target_kind("mailto:test@example.com") == "uri"
assert raw_target_kind("man://bash") == "manual"
assert raw_target_kind(str(test_dir / "note.txt")) == "path"
assert raw_target_kind('python3 "/tmp/file with spaces.py"') == "command"
assert merge_results(
[Result("history", "firefox", "History", "firefox")],
[Result("command", "firefox", "Command", "firefox")],
)[0].detail == "History"
assert result_count_message(0) == "0 suggestions"
assert result_count_message(1) == "1 suggestion"
assert result_count_message(12) == "12 suggestions"
print("run dialog self-test passed")
def main():
parser = argparse.ArgumentParser(description=APP_NAME)
parser.add_argument("--self-test", action="store_true")
args = parser.parse_args()
if args.self_test:
run_self_test()
return
window = RunDialog()
window.show_all()
Gtk.main()
if __name__ == "__main__":
main()
-55
View File
@@ -1,55 +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/>.
write_history(){
oldHistory="$(grep -Fvx -- "$txt" "${historyPath}/.history" | head -n 49)"
printf '%s\n%s\n' "$txt" "$oldHistory" | sed 's/^$//g' > "${historyPath}/.history"
}
historyPath="$(readlink -f "$0")"
historyPath="${historyPath%/*}"
if ! [[ -d "$historyPath" ]]; then
mkdir -p "$historyPath"
fi
if [[ -f "${historyPath}/.history" ]]; then
txt="$(yad --entry --editable --title "I38" --text "Execute program or enter file" --button "Open:0" --separator "\n" --rest "${historyPath}/.history")"
else
txt="$(yad --entry --title "I38" --text "Execute program or enter file" --button "Open:0")"
fi
if [[ -z "$txt" ]]; then
exit 0
fi
if [[ "$txt" =~ ^ftp://|^http://|^https://|^www.* ]]; then
xdg-open "$txt"
write_history
exit 0
fi
if [[ "$txt" =~ ^mailto://.* ]]; then
xdg-email "$txt"
write_history
exit 0
fi
if [[ "$txt" =~ ^man://.* ]]; then
eval "${txt/:\/\// }" | yad --text-info --show-cursor --button "Close:0" --title "I38" -
write_history
exit 0
fi
if command -v "$(echo "$txt" | cut -d " " -f1)" &> /dev/null ; then
eval "$txt" &
else
xdg-open "$txt" &
fi
write_history
exit 0
+1 -1
View File
@@ -97,7 +97,7 @@ if [ -n "$result" ]; then
if [ "$selectedReader" = "orca" ]; then
orca &
else
cthulhu &
ORC_CODE=backup cthulhu &
fi
fi