mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-17 16:54:01 -04:00
launcher: open files through the mime handler so terminal apps get a terminal
fixes #3518
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
package mime
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/dankgo/paths"
|
||||
)
|
||||
|
||||
const noGlobs = "__NOGLOBS__"
|
||||
|
||||
type globRule struct {
|
||||
weight int
|
||||
mimeType string
|
||||
pattern string
|
||||
caseSensitive bool
|
||||
}
|
||||
|
||||
func (r globRule) literal() bool {
|
||||
return !strings.ContainsAny(r.pattern, "*?[")
|
||||
}
|
||||
|
||||
func (r globRule) matches(name string) bool {
|
||||
pattern, candidate := r.pattern, name
|
||||
if !r.caseSensitive {
|
||||
pattern, candidate = strings.ToLower(pattern), strings.ToLower(name)
|
||||
}
|
||||
if r.literal() {
|
||||
return pattern == candidate
|
||||
}
|
||||
ok, err := path.Match(pattern, candidate)
|
||||
return err == nil && ok
|
||||
}
|
||||
|
||||
func (r globRule) outranks(other globRule) bool {
|
||||
if r.literal() != other.literal() {
|
||||
return r.literal()
|
||||
}
|
||||
if r.weight != other.weight {
|
||||
return r.weight > other.weight
|
||||
}
|
||||
return len(r.pattern) > len(other.pattern)
|
||||
}
|
||||
|
||||
func mimeDataDirs() []string {
|
||||
dirs := []string{filepath.Join(paths.XDGDataHome(), "mime")}
|
||||
env := os.Getenv("XDG_DATA_DIRS")
|
||||
if env == "" {
|
||||
env = "/usr/local/share:/usr/share"
|
||||
}
|
||||
for d := range strings.SplitSeq(env, ":") {
|
||||
d = strings.TrimSpace(d)
|
||||
if d == "" {
|
||||
continue
|
||||
}
|
||||
dirs = append(dirs, filepath.Join(d, "mime"))
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
func parseGlobLine(line string) (globRule, bool) {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || line[0] == '#' {
|
||||
return globRule{}, false
|
||||
}
|
||||
fields := strings.SplitN(line, ":", 4)
|
||||
if len(fields) < 3 || fields[1] == "" || fields[2] == "" {
|
||||
return globRule{}, false
|
||||
}
|
||||
weight, err := strconv.Atoi(fields[0])
|
||||
if err != nil {
|
||||
return globRule{}, false
|
||||
}
|
||||
return globRule{
|
||||
weight: weight,
|
||||
mimeType: fields[1],
|
||||
pattern: fields[2],
|
||||
caseSensitive: len(fields) == 4 && fields[3] == "cs",
|
||||
}, true
|
||||
}
|
||||
|
||||
func loadGlobRules() []globRule {
|
||||
var rules []globRule
|
||||
cleared := map[string]bool{}
|
||||
for _, dir := range mimeDataDirs() {
|
||||
data, err := os.ReadFile(filepath.Join(dir, "globs2"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
clearedHere := map[string]bool{}
|
||||
for line := range strings.Lines(string(data)) {
|
||||
rule, ok := parseGlobLine(line)
|
||||
if !ok || cleared[rule.mimeType] {
|
||||
continue
|
||||
}
|
||||
if rule.pattern == noGlobs {
|
||||
clearedHere[rule.mimeType] = true
|
||||
continue
|
||||
}
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
for mimeType := range clearedHere {
|
||||
cleared[mimeType] = true
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
func mimeTypeForName(name string, rules []globRule) string {
|
||||
var best *globRule
|
||||
for i := range rules {
|
||||
rule := &rules[i]
|
||||
if !rule.matches(name) {
|
||||
continue
|
||||
}
|
||||
if best == nil || rule.outranks(*best) {
|
||||
best = rule
|
||||
}
|
||||
}
|
||||
if best == nil {
|
||||
return ""
|
||||
}
|
||||
return best.mimeType
|
||||
}
|
||||
|
||||
func MimeTypeForPath(filePath string) string {
|
||||
if info, err := os.Stat(filePath); err == nil && info.IsDir() {
|
||||
return "inode/directory"
|
||||
}
|
||||
return mimeTypeForName(filepath.Base(filePath), loadGlobRules())
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package mime
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeGlobs(t *testing.T, dataDir, content string) {
|
||||
t.Helper()
|
||||
dir := filepath.Join(dataDir, "mime")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "globs2"), []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMimeTypeForPathGlobRanking(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
system := t.TempDir()
|
||||
t.Setenv("XDG_DATA_HOME", home)
|
||||
t.Setenv("XDG_DATA_DIRS", system)
|
||||
writeGlobs(t, system, `# generated
|
||||
50:text/x-makefile:makefile
|
||||
50:application/gzip:*.gz
|
||||
50:application/x-compressed-tar:*.tar.gz
|
||||
50:text/x-c++src:*.C:cs
|
||||
50:text/x-csrc:*.c
|
||||
55:text/x-diff:*.patch
|
||||
50:text/x-patch:*.patch
|
||||
50:text/plain:*.txt
|
||||
50:application/json:*.json
|
||||
`)
|
||||
writeGlobs(t, home, `0:application/json:__NOGLOBS__
|
||||
50:text/x-myjson:*.json
|
||||
`)
|
||||
|
||||
dir := t.TempDir()
|
||||
cases := []struct{ path, want string }{
|
||||
{"/home/u/test.txt", "text/plain"},
|
||||
{"/home/u/Makefile", "text/x-makefile"},
|
||||
{"/home/u/data.tar.gz", "application/x-compressed-tar"},
|
||||
{"/home/u/main.C", "text/x-c++src"},
|
||||
{"/home/u/main.c", "text/x-csrc"},
|
||||
{"/home/u/fix.patch", "text/x-diff"},
|
||||
{"/home/u/NOTES.TXT", "text/plain"},
|
||||
{"/home/u/config.json", "text/x-myjson"},
|
||||
{"/home/u/unknown.zzz", ""},
|
||||
{dir, "inode/directory"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := MimeTypeForPath(c.path); got != c.want {
|
||||
t.Errorf("%s: got %q, want %q", c.path, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func HandleRequest(conn *ipc.ConnWriter, req ipc.Request) {
|
||||
}
|
||||
|
||||
func handleGetDefault(conn *ipc.ConnWriter, req ipc.Request) {
|
||||
mimeType, err := mimeParam(req.Params, "mimeType")
|
||||
mimeType, err := requestedMime(req.Params)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
@@ -152,6 +152,21 @@ func handleQueryDefaults(conn *ipc.ConnWriter, req ipc.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func requestedMime(p map[string]any) (string, error) {
|
||||
if _, ok := p["mimeType"]; ok {
|
||||
return mimeParam(p, "mimeType")
|
||||
}
|
||||
filePath, err := params.StringNonEmpty(p, "path")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("missing 'mimeType' or 'path' parameter")
|
||||
}
|
||||
mimeType := MimeTypeForPath(filePath)
|
||||
if mimeType == "" {
|
||||
return "", fmt.Errorf("no mime type matches %q", filePath)
|
||||
}
|
||||
return mimeType, nil
|
||||
}
|
||||
|
||||
func mimeParam(p map[string]any, key string) (string, error) {
|
||||
raw, err := params.StringNonEmpty(p, key)
|
||||
if err != nil {
|
||||
|
||||
@@ -845,6 +845,11 @@ Item {
|
||||
onApplicationSelected: (app, filePath) => {
|
||||
if (!app)
|
||||
return;
|
||||
const entry = SessionService.resolveDesktopId(app.appId);
|
||||
if (entry) {
|
||||
SessionService.launchDesktopEntry(entry, false, [filePath]);
|
||||
return;
|
||||
}
|
||||
let cmd = app.exec || "";
|
||||
const escapedPath = shellEscape(filePath);
|
||||
const escapedUri = shellEscape("file://" + filePath);
|
||||
|
||||
@@ -79,10 +79,7 @@ Item {
|
||||
return false;
|
||||
}
|
||||
|
||||
let entry = DesktopEntries.heuristicLookup(desktopId);
|
||||
if (!entry && desktopId.endsWith(".desktop")) {
|
||||
entry = DesktopEntries.heuristicLookup(desktopId.slice(0, -8));
|
||||
}
|
||||
const entry = SessionService.resolveDesktopId(desktopId);
|
||||
if (!entry) {
|
||||
log.warn("Default app desktop entry not found:", desktopId, "for:", appName);
|
||||
return false;
|
||||
|
||||
@@ -2307,16 +2307,13 @@ Item {
|
||||
}
|
||||
|
||||
function openFile(path) {
|
||||
if (!path)
|
||||
return;
|
||||
Qt.openUrlExternally("file://" + path);
|
||||
SessionService.openPath(path);
|
||||
}
|
||||
|
||||
function openFolder(path) {
|
||||
if (!path)
|
||||
return;
|
||||
var folder = path.substring(0, path.lastIndexOf("/"));
|
||||
Qt.openUrlExternally("file://" + folder);
|
||||
SessionService.openPath(path.substring(0, path.lastIndexOf("/")));
|
||||
}
|
||||
|
||||
function openTerminal(path) {
|
||||
|
||||
@@ -293,11 +293,41 @@ Singleton {
|
||||
return env;
|
||||
}
|
||||
|
||||
function launchDesktopEntry(desktopEntry, useNvidia) {
|
||||
function resolveDesktopId(desktopId) {
|
||||
if (!desktopId)
|
||||
return null;
|
||||
const entry = DesktopEntries.heuristicLookup(desktopId);
|
||||
if (entry || !desktopId.endsWith(".desktop"))
|
||||
return entry;
|
||||
return DesktopEntries.heuristicLookup(desktopId.slice(0, -8));
|
||||
}
|
||||
|
||||
// xdg-open's generic opener ignores Terminal=true, so resolve the handler here and launch it ourselves
|
||||
function openPath(path) {
|
||||
if (!path)
|
||||
return;
|
||||
const fallback = () => Qt.openUrlExternally("file://" + path);
|
||||
if (!DMSService.isConnected) {
|
||||
fallback();
|
||||
return;
|
||||
}
|
||||
DMSService.sendRequest("mime.getDefault", {
|
||||
"path": path
|
||||
}, response => {
|
||||
const entry = resolveDesktopId(response?.result?.desktopId);
|
||||
if (!entry) {
|
||||
fallback();
|
||||
return;
|
||||
}
|
||||
launchDesktopEntry(entry, false, [path]);
|
||||
});
|
||||
}
|
||||
|
||||
function launchDesktopEntry(desktopEntry, useNvidia, args) {
|
||||
if (!desktopEntry || !desktopEntry.command)
|
||||
return;
|
||||
CompositorService.closeNiriOverviewOnWindowFocus();
|
||||
let cmd = desktopEntry.command;
|
||||
let cmd = args?.length ? [...desktopEntry.command, ...args] : desktopEntry.command;
|
||||
|
||||
const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || "";
|
||||
const override = SessionData.getAppOverride(appId);
|
||||
|
||||
Reference in New Issue
Block a user