davatorium / rofi

Rofi: A window switcher, application launcher and dmenu replacement
https://davatorium.github.io/rofi/
Other
12.93k stars 609 forks source link

[REQUEST] Prefix sorting #1983

Open BhasherBEL opened 2 months ago

BhasherBEL commented 2 months ago

Before opening a feature request

What is the user problem or growth opportunity you want to see solved?

When looking for an application, we usually search with it prefix. By example, when searching "O", "Onlyoffice" should be shown before "firefox". Thanks to https://github.com/davatorium/rofi/pull/1237, it's possible to match by prefix. But sometimes, we also search with another word. One good example is "DB Browser for SQLite". It would be nice to be able to sort by prefix, rather than match by it. That way, both uses cases works, and the arguments used in https://github.com/davatorium/rofi/pull/1237 are still true.

How do you know that this problem exists today? Why is this important?

I'm using rofi many times a day, and while it's awesome, there is sometimes too much friction, when there is a lot of applications.

Who will benefit from it?

Probably everybody, as it's a very common use case to start with the prefix.

Rofi version (rofi -v)

1.7.5+wayland3

Configuration

https://gist.github.com/BhasherBEL/674b236b9ae37a5734378b12b7fcdd62

Additional information

No response

DaveDavenport commented 2 months ago

What exactly do you mean with 'prefix' sorting? sort it alpha numeric on the full string? so it starts with a.... b..... etc.?

Something like:

diff --git a/include/settings.h b/include/settings.h
index 1af9b952..6a803441 100644
--- a/include/settings.h
+++ b/include/settings.h
@@ -46,7 +46,7 @@ typedef enum {
 /**
  * Possible sorting methods for listview.
  */
-typedef enum { SORT_NORMAL = 0, SORT_FZF = 1 } SortingMethod;
+typedef enum { SORT_NORMAL = 0, SORT_FZF = 1, SORT_ALNUM = 2 } SortingMethod;

 /**
  * Settings structure holding all (static) configurable options.
diff --git a/source/helper.c b/source/helper.c
index 53f366bf..0777cbde 100644
--- a/source/helper.c
+++ b/source/helper.c
@@ -653,6 +653,8 @@ int config_sanity_check(void) {
       config.sorting_method_enum = SORT_NORMAL;
     } else if (g_strcmp0(config.sorting_method, "fzf") == 0) {
       config.sorting_method_enum = SORT_FZF;
+    } else if (g_strcmp0(config.sorting_method, "alnum") == 0) {
+      config.sorting_method_enum = SORT_ALNUM;
     } else {
       g_string_append_printf(
           msg,
diff --git a/source/view.c b/source/view.c
index aac8c22e..a942f082 100644
--- a/source/view.c
+++ b/source/view.c
@@ -211,6 +211,28 @@ static int lev_sort(const void *p1, const void *p2, void *arg) {

   return distances[*a] - distances[*b];
 }
+static int alnum_sort(const void *p1, const void *p2, void *arg) {
+  const int *a = p1;
+  const int *b = p2;
+  RofiViewState *state = arg;
+
+  char *str_a = mode_get_completion(state->sw, *a);
+  char *str_b = mode_get_completion(state->sw, *b);
+
+  if (str_a == NULL && str_b == NULL) {
+    return 0;
+  } else if (str_a != NULL && str_b == NULL) {
+    g_free(str_a);
+    return -1;
+  } else if (str_a == NULL && str_b != NULL) {
+    g_free(str_b);
+    return -1;
+  }
+  int retv = g_utf8_collate(str_a, str_b);
+  g_free(str_a);
+  g_free(str_b);
+  return retv;
+}

 /**
  * Stores a screenshot of Rofi at that point in time.
@@ -765,6 +787,9 @@ static void filter_elements(thread_state *ts,
           t->state->distance[i] =
               rofi_scorer_fuzzy_evaluate(t->pattern, t->plen, str, slen);
           break;
+        case SORT_ALNUM:
+          t->state->distance[i] = 0;
+          break;
         case SORT_NORMAL:
         default:
           t->state->distance[i] = levenshtein(t->pattern, t->plen, str, slen);
@@ -1486,8 +1511,12 @@ static gboolean rofi_view_refilter_real(RofiViewState *state) {
       j += states[i].count;
     }
     if (config.sort) {
-      g_qsort_with_data(state->line_map, j, sizeof(int), lev_sort,
-                        state->distance);
+      if (config.sorting_method_enum == SORT_ALNUM) {
+        g_qsort_with_data(state->line_map, j, sizeof(int), alnum_sort, state);
+      } else {
+        g_qsort_with_data(state->line_map, j, sizeof(int), lev_sort,
+                          state->distance);
+      }
     }

     // Cleanup + bookkeeping.

-sorting-method alnum -sort

BhasherBEL commented 2 months ago

Not 100% sure about what g_utf8_collate do here.

The idea here is basically to prioritize entries where the match is at the beginning of the string. By example, when searching for "s", here is what happens now: image

With this sorting method, Signal would be sorted first, because the match "s" is at the beginning.

DaveDavenport commented 2 months ago

What about using the fzf sorting method, as that put more weight on matching at the start?

rofi -sort -sorting-method fzf rofi-2024-05-29-1907-00000

BhasherBEL commented 2 months ago

It's a partial improvement, as it put more weight on smaller names. So from my experiments, it can both move them forward if they are small, but backward if the name is long.

DaveDavenport commented 2 months ago

Do you know of a weighting algorithm that does what you want?

BhasherBEL commented 2 months ago

I don't know if there is any algorithm that do that directly, but probably that a custom comparator could do it quite easily. I'm not used to C, but I could try to write an example if you want, maybe in a higher level language.

DaveDavenport commented 2 months ago

Yeah, a high level algo would be useful.. In my experience it sounds easier then it is, for us (knowing what we are looking for ) it sounds obvious, but it often isn't to implement.

With the current information, I don't know what would fit your requirements.

Currently rofi works with 'distances', the lower the distance the higher it is in the list.

BhasherBEL commented 2 months ago

No problem, converting words and ideas into code is indeed not an easy task.

If I stick to a high level language such as python, I would write this kind of comparator function, with a and b the variables to compare, input what the users typed, and distance the current distance-based algorithm.

As we can see, it's not so much a new algorithm, but rather a "shortcut" for the case where an entry starts with the input.

def compare(a: str, b: str, input: str):
    if a.startswith(input) and not b.startswith(input):
        return 1
    elif b.startswith(input) and not a.startswith(input):
        return -1
    else:
        return distance(a, b, index)
DaveDavenport commented 2 months ago

This would most likely favour shorter words just like the fzf algorithm.

DaveDavenport commented 2 months ago
diff --git a/include/settings.h b/include/settings.h
index 1af9b952..6a803441 100644
--- a/include/settings.h
+++ b/include/settings.h
@@ -46,7 +46,7 @@ typedef enum {
 /**
  * Possible sorting methods for listview.
  */
-typedef enum { SORT_NORMAL = 0, SORT_FZF = 1 } SortingMethod;
+typedef enum { SORT_NORMAL = 0, SORT_FZF = 1, SORT_ALNUM = 2 } SortingMethod;

 /**
  * Settings structure holding all (static) configurable options.
diff --git a/source/helper.c b/source/helper.c
index 53f366bf..0777cbde 100644
--- a/source/helper.c
+++ b/source/helper.c
@@ -653,6 +653,8 @@ int config_sanity_check(void) {
       config.sorting_method_enum = SORT_NORMAL;
     } else if (g_strcmp0(config.sorting_method, "fzf") == 0) {
       config.sorting_method_enum = SORT_FZF;
+    } else if (g_strcmp0(config.sorting_method, "alnum") == 0) {
+      config.sorting_method_enum = SORT_ALNUM;
     } else {
       g_string_append_printf(
           msg,
diff --git a/source/view.c b/source/view.c
index aac8c22e..c611e3fe 100644
--- a/source/view.c
+++ b/source/view.c
@@ -211,6 +211,38 @@ static int lev_sort(const void *p1, const void *p2, void *arg) {

   return distances[*a] - distances[*b];
 }
+static int alnum_sort(const void *p1, const void *p2, void *arg) {
+  const int *a = p1;
+  const int *b = p2;
+  RofiViewState *state = arg;
+  int *distances = state->distance;
+
+  char *str_a = mode_get_completion(state->sw, *a);
+  char *str_b = mode_get_completion(state->sw, *b);
+
+  if (str_a == NULL && str_b == NULL) {
+    return 0;
+  } else if (str_a != NULL && str_b == NULL) {
+    g_free(str_a);
+    return -1;
+  } else if (str_a == NULL && str_b != NULL) {
+    g_free(str_b);
+    return -1;
+  }
+
+  char *str = state->text->text;
+  size_t l = strlen(str);
+  if (strncasecmp(str_a, str, l) == 0 && strncasecmp(str_b, str, l) != 0) {
+    return -1;
+  } else if (strncasecmp(str_b, str, l) == 0 &&
+             strncasecmp(str_a, str, l) != 0) {
+    return 1;
+  }
+  int retv = distances[*a] - distances[*b];
+  g_free(str_a);
+  g_free(str_b);
+  return retv;
+}

 /**
  * Stores a screenshot of Rofi at that point in time.
@@ -765,9 +797,13 @@ static void filter_elements(thread_state *ts,
           t->state->distance[i] =
               rofi_scorer_fuzzy_evaluate(t->pattern, t->plen, str, slen);
           break;
+        case SORT_ALNUM:
+          t->state->distance[i] = levenshtein(t->pattern, t->plen, str, slen);
+          break;
         case SORT_NORMAL:
         default:
           t->state->distance[i] = levenshtein(t->pattern, t->plen, str, slen);
+          printf("%d %s\n", t->state->distance[i], str);
           break;
         }
         g_free(str);
@@ -1486,8 +1522,12 @@ static gboolean rofi_view_refilter_real(RofiViewState *state) {
       j += states[i].count;
     }
     if (config.sort) {
-      g_qsort_with_data(state->line_map, j, sizeof(int), lev_sort,
-                        state->distance);
+      if (config.sorting_method_enum == SORT_ALNUM) {
+        g_qsort_with_data(state->line_map, j, sizeof(int), alnum_sort, state);
+      } else {
+        g_qsort_with_data(state->line_map, j, sizeof(int), lev_sort,
+                          state->distance);
+      }
     }

     // Cleanup + bookkeeping.
BhasherBEL commented 2 months ago

I tried your patch, but wasn't able to see any difference when using rofi -modes run -show run -sorting-method alnum compared to the default option. Do you see any difference on your side?

In this example, I would except Thunderbird to be ranked way higher than it actually is, as it starts with the user input.

image

DaveDavenport commented 2 months ago

Pass -sort to enable sorting?

DaveDavenport commented 2 months ago

rofi-2024-06-02-1800-00000

BhasherBEL commented 2 months ago

Ok, feel a bit stupid now :sweat_smile: Thank you a lot, that was exactly what I was looking for!

Any plan to merge this into rofi itself?

DaveDavenport commented 2 months ago

I need to see how to do this utf-8 safe, but I don't see a reason to not merge it.