Skip to content

Commit bf86188

Browse files
authored
Merge pull request #7 from imoize/feat/app-chooser
Add app chooser feature and improvement
2 parents 7f08171 + 7302279 commit bf86188

File tree

25 files changed

+1354
-645
lines changed

25 files changed

+1354
-645
lines changed

@types/types.d.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
export interface SchemaType {
2-
'settings-version': number;
3-
'submenu': boolean;
4-
'editors': string[];
2+
settingsVersion: number;
3+
submenu: boolean;
4+
applications: string[];
55
}
66

77
export interface Application {
8-
id: number;
8+
id: string;
9+
appId: string;
910
name: string;
10-
enable?: boolean;
11-
native?: string[];
12-
flatpak?: string[];
13-
arguments?: string[];
14-
supports_files?: boolean;
11+
icon: string;
12+
pinned: boolean;
13+
multipleFiles: boolean;
14+
multipleFolders: boolean;
15+
packageType: 'Flatpak' | 'AppImage' | 'Native';
16+
mimeTypes?: string[];
17+
enable: boolean;
1518
}
1619

1720
export interface ValidationResult {

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ UI_FILES := $(patsubst resources/ui/%.blp,src/ui/%.ui,$(BLP_FILES))
77
UI_SRC := $(shell find src/ui -name '*.ui')
88
UI_DST := $(patsubst src/ui/%,dist/ui/%,$(UI_SRC))
99

10-
.PHONY: all build build-ui pot pot-merge mo pack install test test-shell remove clean
10+
.PHONY: all build build-ui pot pot-merge mo pack install test test-py test-shell remove clean
1111

1212
all: pack
1313

@@ -73,6 +73,7 @@ pack: build schemas/gschemas.compiled copy-ui mo
7373
@cp metadata.json dist/
7474
@cp -r schemas dist/
7575
@cp -r nautilus-extension/* dist/
76+
@cp -r resources/ui/icons dist/ui/
7677
@(cd dist && zip ../$(UUID).shell-extension.zip -9r .)
7778

7879
install: pack
@@ -83,6 +84,11 @@ test: pack
8384
@cp -r dist $(HOME)/.local/share/gnome-shell/extensions/$(UUID)
8485
gnome-extensions prefs $(UUID)
8586

87+
test-py:
88+
@rm -rf $(HOME)/.local/share/gnome-shell/extensions/$(UUID)/Flickernaut
89+
@rm -rf $(HOME)/.local/share/gnome-shell/extensions/$(UUID)/nautilus-flickernaut.py
90+
@cp -r nautilus-extension/* $(HOME)/.local/share/gnome-shell/extensions/$(UUID)
91+
8692
test-shell:
8793
@env GNOME_SHELL_SLOWDOWN_FACTOR=2 \
8894
MUTTER_DEBUG_DUMMY_MODE_SPECS=1500x1000 \
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import os
2+
import shlex
3+
from gi.repository import GLib, Gio # type: ignore
4+
from .logger import get_logger
5+
6+
logger = get_logger(__name__)
7+
8+
9+
class Launcher:
10+
"""Handles launching a desktop application."""
11+
12+
def __init__(self, app_info: Gio.DesktopAppInfo, app_id: str, name: str) -> None:
13+
self.app_id = app_id
14+
self.name = name
15+
self._app_info = app_info
16+
self._launch_method = "none"
17+
self._run_command = ()
18+
self._commandline = self._get_commandline(app_info)
19+
self._set_launch_command()
20+
21+
logger.debug(f"launcher method: {self._launch_method}")
22+
logger.debug(f"commandline: {self._commandline}")
23+
24+
def _get_commandline(self, app_info: Gio.DesktopAppInfo) -> list[str]:
25+
"""Get the commandline from the app_info, handling special cases."""
26+
executable = os.path.basename(app_info.get_executable()) or ""
27+
28+
bin_path = GLib.find_program_in_path(executable)
29+
if not bin_path:
30+
return []
31+
32+
cmd = app_info.get_commandline() or ""
33+
34+
# Split commandline into tokens while respecting quotes
35+
tokens = shlex.split(cmd)
36+
37+
# Placeholder tokens
38+
placeholders = {
39+
"%f",
40+
"%F",
41+
"%u",
42+
"%U",
43+
"%d",
44+
"%D",
45+
"%n",
46+
"%N",
47+
"%k",
48+
"%v",
49+
"%m",
50+
"%i",
51+
"%c",
52+
"%r",
53+
"@@u",
54+
"@@",
55+
"@",
56+
}
57+
filtered = [
58+
t for t in tokens if t not in placeholders and not t.startswith("%")
59+
]
60+
61+
if bin_path and filtered:
62+
filtered[0] = bin_path
63+
64+
return filtered
65+
66+
def _set_launch_command(self) -> None:
67+
"""Determine the best launch command for the application."""
68+
# 1. Try Gio.AppInfo.launch_uris first
69+
if self._app_info:
70+
self._launch_method = "gio-launch"
71+
self._run_command = ()
72+
return
73+
74+
# 2. Fallback to gtk-launch if gio-launch is not available
75+
bin_path = GLib.find_program_in_path("gtk-launch")
76+
if bin_path and os.path.isfile(bin_path):
77+
desktop_id = (
78+
self._app_info.get_id()[:-8]
79+
if self._app_info.get_id().endswith(".desktop")
80+
else self._app_info.get_id()
81+
)
82+
self._launch_method = "gtk-launch"
83+
self._run_command = (bin_path, desktop_id)
84+
return
85+
86+
# 3. Fallback to commandline if other methods are not available
87+
if self._commandline:
88+
self._launch_method = "commandline"
89+
self._run_command = tuple(self._commandline)
90+
return
91+
92+
self._run_command = ()
93+
self._launch_method = "none"
94+
self._init_failed = True
95+
96+
def launch(self, paths: list[str]) -> bool:
97+
"""Launch the application based _launch_method."""
98+
if self._launch_method == "gio-launch" and self._app_info:
99+
try:
100+
logger.debug(f"Launching {self.name} with gio-launch: {paths}")
101+
ctx = None
102+
self._app_info.launch_uris_async(paths, ctx)
103+
return True
104+
except Exception as e:
105+
logger.error(
106+
f"Failed to launch {self.name} with Gio.AppInfo.launch_uris: {e}"
107+
)
108+
return False
109+
110+
elif self._launch_method == "gtk-launch":
111+
try:
112+
command = list(self._run_command) + list(paths)
113+
logger.debug(f"Launching {self.name}: {command}")
114+
pid, *_ = GLib.spawn_async(command)
115+
GLib.spawn_close_pid(pid)
116+
return True
117+
except Exception as e:
118+
logger.error(f"Failed to launch {self.name} with gtk-launch: {e}")
119+
return False
120+
121+
elif self._launch_method == "commandline":
122+
try:
123+
command = list(self._run_command) + list(paths)
124+
logger.debug(f"Launching {self.name} with commandline: {command}")
125+
pid, *_ = GLib.spawn_async(command)
126+
GLib.spawn_close_pid(pid)
127+
return True
128+
except Exception as e:
129+
logger.error(f"Failed to launch {self.name} with commandline: {e}")
130+
return False
131+
132+
logger.error(f"No valid launch method for {self.app_id}")
133+
return False
134+
135+
@property
136+
def run_command(self) -> tuple[str, ...]:
137+
return self._run_command
138+
139+
def __str__(self) -> str:
140+
return f"Launcher({self.name}, method={self._launch_method}, cmd={self._run_command})"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import logging
2+
3+
# Set to True for development only
4+
FLICKERNAUT_DEBUG: bool = False
5+
6+
7+
class FlickernautFormatter(logging.Formatter):
8+
def format(self, record):
9+
record.msg = f"[Flickernaut] [{record.levelname}] : {record.msg}"
10+
return super().format(record)
11+
12+
13+
def get_logger(name: str) -> logging.Logger:
14+
logger = logging.getLogger(name)
15+
if not logger.hasHandlers():
16+
handler = logging.StreamHandler()
17+
handler.setFormatter(FlickernautFormatter())
18+
logger.addHandler(handler)
19+
logger.setLevel(logging.DEBUG if FLICKERNAUT_DEBUG else logging.WARNING)
20+
return logger

0 commit comments

Comments
 (0)