mirror of
https://github.com/RGBCube/serenity
synced 2025-07-26 07:17:35 +00:00
Userland: Add new app called Assistant
'Assistant' is similar to macOS spotlight where you can quickly open a text input, start typing, and hit 'enter' to launch apps or open directories.
This commit is contained in:
parent
d16db6a67c
commit
66c13edb98
9 changed files with 625 additions and 0 deletions
|
@ -4,3 +4,4 @@ SystemMonitor=SystemMonitor.af
|
||||||
Terminal=Terminal.af
|
Terminal=Terminal.af
|
||||||
FileManager=FileManager.af
|
FileManager=FileManager.af
|
||||||
TextEditor=TextEditor.af
|
TextEditor=TextEditor.af
|
||||||
|
Assistant=Assistant.af
|
||||||
|
|
4
Base/res/apps/Assistant.af
Normal file
4
Base/res/apps/Assistant.af
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[App]
|
||||||
|
Name=Assistant
|
||||||
|
Executable=/bin/Assistant
|
||||||
|
Category=Utilities
|
14
Userland/Applications/Assistant/CMakeLists.txt
Normal file
14
Userland/Applications/Assistant/CMakeLists.txt
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
serenity_component(
|
||||||
|
Assistant
|
||||||
|
RECOMMENDED
|
||||||
|
TARGETS Assistant
|
||||||
|
)
|
||||||
|
|
||||||
|
set(SOURCES
|
||||||
|
Providers.cpp
|
||||||
|
FuzzyMatch.cpp
|
||||||
|
main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
serenity_app(Assistant ICON app-run)
|
||||||
|
target_link_libraries(Assistant LibCore LibDesktop LibGUI LibJS LibThreading)
|
122
Userland/Applications/Assistant/FuzzyMatch.cpp
Normal file
122
Userland/Applications/Assistant/FuzzyMatch.cpp
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021, Spencer Dixon <spencercdixon@gmail.com>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "FuzzyMatch.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
namespace Assistant {
|
||||||
|
|
||||||
|
static constexpr const int RECURSION_LIMIT = 10;
|
||||||
|
static constexpr const int MAX_MATCHES = 256;
|
||||||
|
|
||||||
|
// Bonuses and penalties are used to build up a final score for the match.
|
||||||
|
static constexpr const int SEQUENTIAL_BONUS = 15; // bonus for adjacent matches (needle: 'ca', haystack: 'cat')
|
||||||
|
static constexpr const int SEPARATOR_BONUS = 30; // bonus if match occurs after a separator ('_' or ' ')
|
||||||
|
static constexpr const int CAMEL_BONUS = 30; // bonus if match is uppercase and prev is lower (needle: 'myF' haystack: '/path/to/myFile.txt')
|
||||||
|
static constexpr const int FIRST_LETTER_BONUS = 20; // bonus if the first letter is matched (needle: 'c' haystack: 'cat')
|
||||||
|
static constexpr const int LEADING_LETTER_PENALTY = -5; // penalty applied for every letter in str before the first match
|
||||||
|
static constexpr const int MAX_LEADING_LETTER_PENALTY = -15; // maximum penalty for leading letters
|
||||||
|
static constexpr const int UNMATCHED_LETTER_PENALTY = -1; // penalty for every letter that doesn't matter
|
||||||
|
|
||||||
|
static FuzzyMatchResult fuzzy_match_recursive(String const& needle, String const& haystack, size_t needle_idx, size_t haystack_idx,
|
||||||
|
u8 const* src_matches, u8* matches, int next_match, int& recursion_count)
|
||||||
|
{
|
||||||
|
int out_score = 0;
|
||||||
|
|
||||||
|
++recursion_count;
|
||||||
|
if (recursion_count >= RECURSION_LIMIT)
|
||||||
|
return { false, out_score };
|
||||||
|
|
||||||
|
if (needle.length() == needle_idx || haystack.length() == haystack_idx)
|
||||||
|
return { false, out_score };
|
||||||
|
|
||||||
|
bool had_recursive_match = false;
|
||||||
|
constexpr size_t recursive_match_limit = 256;
|
||||||
|
u8 best_recursive_matches[recursive_match_limit];
|
||||||
|
int best_recursive_score = 0;
|
||||||
|
|
||||||
|
bool first_match = true;
|
||||||
|
while (needle_idx < needle.length() && haystack_idx < haystack.length()) {
|
||||||
|
|
||||||
|
if (needle.substring(needle_idx, 1).to_lowercase() == haystack.substring(haystack_idx, 1).to_lowercase()) {
|
||||||
|
if (next_match >= MAX_MATCHES)
|
||||||
|
return { false, out_score };
|
||||||
|
|
||||||
|
if (first_match && src_matches) {
|
||||||
|
memcpy(matches, src_matches, next_match);
|
||||||
|
first_match = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
u8 recursive_matches[recursive_match_limit];
|
||||||
|
auto result = fuzzy_match_recursive(needle, haystack, needle_idx, haystack_idx + 1, matches, recursive_matches, next_match, recursion_count);
|
||||||
|
if (result.matched) {
|
||||||
|
if (!had_recursive_match || result.score > best_recursive_score) {
|
||||||
|
memcpy(best_recursive_matches, recursive_matches, recursive_match_limit);
|
||||||
|
best_recursive_score = result.score;
|
||||||
|
}
|
||||||
|
had_recursive_match = true;
|
||||||
|
matches[next_match++] = haystack_idx;
|
||||||
|
}
|
||||||
|
needle_idx++;
|
||||||
|
}
|
||||||
|
haystack_idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool matched = needle_idx == needle.length();
|
||||||
|
if (matched) {
|
||||||
|
out_score = 100;
|
||||||
|
|
||||||
|
int penalty = LEADING_LETTER_PENALTY * matches[0];
|
||||||
|
if (penalty < MAX_LEADING_LETTER_PENALTY)
|
||||||
|
penalty = MAX_LEADING_LETTER_PENALTY;
|
||||||
|
out_score += penalty;
|
||||||
|
|
||||||
|
int unmatched = haystack.length() - next_match;
|
||||||
|
out_score += UNMATCHED_LETTER_PENALTY * unmatched;
|
||||||
|
|
||||||
|
for (int i = 0; i < next_match; i++) {
|
||||||
|
u8 current_idx = matches[i];
|
||||||
|
|
||||||
|
if (i > 0) {
|
||||||
|
u8 previous_idx = matches[i - 1];
|
||||||
|
if (current_idx == previous_idx)
|
||||||
|
out_score += SEQUENTIAL_BONUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_idx > 0) {
|
||||||
|
auto current_character = haystack.substring(current_idx, 1);
|
||||||
|
auto neighbor_character = haystack.substring(current_idx - 1, 1);
|
||||||
|
|
||||||
|
if (neighbor_character != neighbor_character.to_uppercase() && current_character != current_character.to_lowercase())
|
||||||
|
out_score += CAMEL_BONUS;
|
||||||
|
|
||||||
|
if (neighbor_character == "_" || neighbor_character == " ")
|
||||||
|
out_score += SEPARATOR_BONUS;
|
||||||
|
} else {
|
||||||
|
out_score += FIRST_LETTER_BONUS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (had_recursive_match && (!matched || best_recursive_score > out_score)) {
|
||||||
|
memcpy(matches, best_recursive_matches, MAX_MATCHES);
|
||||||
|
out_score = best_recursive_score;
|
||||||
|
return { true, out_score };
|
||||||
|
} else if (matched) {
|
||||||
|
return { true, out_score };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { false, out_score };
|
||||||
|
}
|
||||||
|
|
||||||
|
FuzzyMatchResult fuzzy_match(String needle, String haystack)
|
||||||
|
{
|
||||||
|
int recursion_count = 0;
|
||||||
|
u8 matches[MAX_MATCHES];
|
||||||
|
return fuzzy_match_recursive(needle, haystack, 0, 0, nullptr, matches, 0, recursion_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
29
Userland/Applications/Assistant/FuzzyMatch.h
Normal file
29
Userland/Applications/Assistant/FuzzyMatch.h
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021, Spencer Dixon <spencercdixon@gmail.com>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/String.h>
|
||||||
|
#include <AK/Tuple.h>
|
||||||
|
|
||||||
|
namespace Assistant {
|
||||||
|
|
||||||
|
struct FuzzyMatchResult {
|
||||||
|
bool matched { false };
|
||||||
|
int score { 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
// This fuzzy_match algorithm is based off a similar algorithm used by Sublime Text. The key insight is that instead
|
||||||
|
// of doing a total in the distance between characters (I.E. Levenshtein Distance), we apply some meaningful heuristics
|
||||||
|
// related to our dataset that we're trying to match to build up a score. Scores can then be sorted and displayed
|
||||||
|
// with the highest at the top.
|
||||||
|
//
|
||||||
|
// Scores are not normalized between any values and have no particular meaning. The starting value is 100 and when we
|
||||||
|
// detect good indicators of a match we add to the score. When we detect bad indicators, we penalize the match and subtract
|
||||||
|
// from its score. Therefore, the longer the needle/haystack the greater the range of scores could be.
|
||||||
|
FuzzyMatchResult fuzzy_match(String needle, String haystack);
|
||||||
|
|
||||||
|
}
|
78
Userland/Applications/Assistant/Providers.cpp
Normal file
78
Userland/Applications/Assistant/Providers.cpp
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021, Spencer Dixon <spencercdixon@gmail.com>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Providers.h"
|
||||||
|
#include "FuzzyMatch.h"
|
||||||
|
#include <LibGUI/Clipboard.h>
|
||||||
|
#include <LibGUI/FileIconProvider.h>
|
||||||
|
#include <LibJS/Interpreter.h>
|
||||||
|
#include <LibJS/Lexer.h>
|
||||||
|
#include <LibJS/Parser.h>
|
||||||
|
#include <LibJS/Runtime/GlobalObject.h>
|
||||||
|
|
||||||
|
namespace Assistant {
|
||||||
|
|
||||||
|
void AppResult::activate() const
|
||||||
|
{
|
||||||
|
m_app_file->spawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalculatorResult::activate() const
|
||||||
|
{
|
||||||
|
GUI::Clipboard::the().set_plain_text(title());
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppProvider::query(String const& query, Function<void(Vector<NonnullRefPtr<Result>>)> on_complete)
|
||||||
|
{
|
||||||
|
if (query.starts_with("="))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Vector<NonnullRefPtr<Result>> results;
|
||||||
|
|
||||||
|
Desktop::AppFile::for_each([&](NonnullRefPtr<Desktop::AppFile> app_file) {
|
||||||
|
auto match_result = fuzzy_match(query, app_file->name());
|
||||||
|
if (!match_result.matched)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto icon = GUI::FileIconProvider::icon_for_executable(app_file->executable());
|
||||||
|
results.append(adopt_ref(*new AppResult(icon.bitmap_for_size(16), app_file->name(), app_file, match_result.score)));
|
||||||
|
});
|
||||||
|
|
||||||
|
on_complete(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalculatorProvider::query(String const& query, Function<void(Vector<NonnullRefPtr<Result>>)> on_complete)
|
||||||
|
{
|
||||||
|
if (!query.starts_with("="))
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto vm = JS::VM::create();
|
||||||
|
auto interpreter = JS::Interpreter::create<JS::GlobalObject>(*vm);
|
||||||
|
|
||||||
|
auto source_code = query.substring(1);
|
||||||
|
auto parser = JS::Parser(JS::Lexer(source_code));
|
||||||
|
auto program = parser.parse_program();
|
||||||
|
if (parser.has_errors())
|
||||||
|
return;
|
||||||
|
|
||||||
|
interpreter->run(interpreter->global_object(), *program);
|
||||||
|
if (interpreter->exception())
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto result = interpreter->vm().last_value();
|
||||||
|
String calculation;
|
||||||
|
if (!result.is_number()) {
|
||||||
|
calculation = "0";
|
||||||
|
} else {
|
||||||
|
calculation = result.to_string_without_side_effects();
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<NonnullRefPtr<Result>> results;
|
||||||
|
results.append(adopt_ref(*new CalculatorResult(calculation)));
|
||||||
|
on_complete(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
95
Userland/Applications/Assistant/Providers.h
Normal file
95
Userland/Applications/Assistant/Providers.h
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021, Spencer Dixon <spencercdixon@gmail.com>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/String.h>
|
||||||
|
#include <LibDesktop/AppFile.h>
|
||||||
|
#include <LibGUI/Desktop.h>
|
||||||
|
#include <LibJS/Interpreter.h>
|
||||||
|
#include <LibJS/Runtime/VM.h>
|
||||||
|
|
||||||
|
namespace Assistant {
|
||||||
|
|
||||||
|
class Result : public RefCounted<Result> {
|
||||||
|
public:
|
||||||
|
enum class Kind {
|
||||||
|
Unknown,
|
||||||
|
App,
|
||||||
|
Calculator,
|
||||||
|
};
|
||||||
|
|
||||||
|
virtual ~Result() = default;
|
||||||
|
|
||||||
|
virtual void activate() const = 0;
|
||||||
|
|
||||||
|
RefPtr<Gfx::Bitmap> bitmap() { return m_bitmap; }
|
||||||
|
String const& title() const { return m_title; }
|
||||||
|
Kind kind() const { return m_kind; }
|
||||||
|
int score() const { return m_score; }
|
||||||
|
bool equals(Result const& other) const
|
||||||
|
{
|
||||||
|
return title() == other.title() && kind() == other.kind();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Result(RefPtr<Gfx::Bitmap> bitmap, String title, int score = 0, Kind kind = Kind::Unknown)
|
||||||
|
: m_bitmap(move(bitmap))
|
||||||
|
, m_title(move(title))
|
||||||
|
, m_score(score)
|
||||||
|
, m_kind(kind)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
RefPtr<Gfx::Bitmap> m_bitmap;
|
||||||
|
String m_title;
|
||||||
|
int m_score { 0 };
|
||||||
|
Kind m_kind;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AppResult : public Result {
|
||||||
|
public:
|
||||||
|
AppResult(RefPtr<Gfx::Bitmap> bitmap, String title, NonnullRefPtr<Desktop::AppFile> af, int score)
|
||||||
|
: Result(move(bitmap), move(title), score, Kind::App)
|
||||||
|
, m_app_file(move(af))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
~AppResult() override = default;
|
||||||
|
void activate() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
NonnullRefPtr<Desktop::AppFile> m_app_file;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CalculatorResult : public Result {
|
||||||
|
public:
|
||||||
|
explicit CalculatorResult(String title)
|
||||||
|
: Result(GUI::Icon::default_icon("app-calculator").bitmap_for_size(16), move(title), 100, Kind::Calculator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
~CalculatorResult() override = default;
|
||||||
|
void activate() const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Provider {
|
||||||
|
public:
|
||||||
|
virtual ~Provider() = default;
|
||||||
|
|
||||||
|
virtual void query(const String&, Function<void(Vector<NonnullRefPtr<Result>>)> on_complete) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AppProvider : public Provider {
|
||||||
|
public:
|
||||||
|
void query(String const& query, Function<void(Vector<NonnullRefPtr<Result>>)> on_complete) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CalculatorProvider : public Provider {
|
||||||
|
public:
|
||||||
|
void query(String const& query, Function<void(Vector<NonnullRefPtr<Result>>)> on_complete) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
279
Userland/Applications/Assistant/main.cpp
Normal file
279
Userland/Applications/Assistant/main.cpp
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021, Spencer Dixon <spencercdixon@gmail.com>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Providers.h"
|
||||||
|
#include <AK/QuickSort.h>
|
||||||
|
#include <AK/String.h>
|
||||||
|
#include <LibGUI/Application.h>
|
||||||
|
#include <LibGUI/BoxLayout.h>
|
||||||
|
#include <LibGUI/Event.h>
|
||||||
|
#include <LibGUI/Icon.h>
|
||||||
|
#include <LibGUI/ImageWidget.h>
|
||||||
|
#include <LibGUI/Label.h>
|
||||||
|
#include <LibGUI/Painter.h>
|
||||||
|
#include <LibGUI/TextBox.h>
|
||||||
|
#include <LibGfx/Palette.h>
|
||||||
|
#include <LibThreading/Lock.h>
|
||||||
|
|
||||||
|
namespace Assistant {
|
||||||
|
|
||||||
|
struct AppState {
|
||||||
|
size_t selected_index { 0 };
|
||||||
|
Vector<NonnullRefPtr<Result>> results;
|
||||||
|
size_t visible_result_count { 0 };
|
||||||
|
|
||||||
|
Threading::Lock lock;
|
||||||
|
String last_query;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ResultRow final : public GUI::Widget {
|
||||||
|
C_OBJECT(ResultRow)
|
||||||
|
public:
|
||||||
|
ResultRow()
|
||||||
|
{
|
||||||
|
auto& layout = set_layout<GUI::HorizontalBoxLayout>();
|
||||||
|
layout.set_spacing(12);
|
||||||
|
layout.set_margins({ 4, 4, 4, 4 });
|
||||||
|
|
||||||
|
m_image = add<GUI::ImageWidget>();
|
||||||
|
|
||||||
|
m_label_container = add<GUI::Widget>();
|
||||||
|
m_label_container->set_layout<GUI::VerticalBoxLayout>();
|
||||||
|
m_label_container->set_fixed_height(30);
|
||||||
|
|
||||||
|
m_title = m_label_container->add<GUI::Label>();
|
||||||
|
m_title->set_text_alignment(Gfx::TextAlignment::CenterLeft);
|
||||||
|
|
||||||
|
set_shrink_to_fit(true);
|
||||||
|
set_fill_with_background_color(true);
|
||||||
|
set_global_cursor_tracking(true);
|
||||||
|
set_greedy_for_hits(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_image(RefPtr<Gfx::Bitmap> bitmap)
|
||||||
|
{
|
||||||
|
m_image->set_bitmap(bitmap);
|
||||||
|
}
|
||||||
|
void set_title(String text)
|
||||||
|
{
|
||||||
|
m_title->set_text(move(text));
|
||||||
|
}
|
||||||
|
void set_subtitle(String text)
|
||||||
|
{
|
||||||
|
if (!m_subtitle) {
|
||||||
|
m_subtitle = m_label_container->add<GUI::Label>();
|
||||||
|
m_subtitle->set_text_alignment(Gfx::TextAlignment::CenterLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_subtitle->set_text(move(text));
|
||||||
|
}
|
||||||
|
void set_is_highlighted(bool value)
|
||||||
|
{
|
||||||
|
if (m_is_highlighted == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_is_highlighted = value;
|
||||||
|
m_title->set_font_weight(value ? 700 : 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
Function<void()> on_selected;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void mousedown_event(GUI::MouseEvent&) override
|
||||||
|
{
|
||||||
|
set_background_role(ColorRole::MenuBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
void mouseup_event(GUI::MouseEvent&) override
|
||||||
|
{
|
||||||
|
set_background_role(ColorRole::NoRole);
|
||||||
|
on_selected();
|
||||||
|
}
|
||||||
|
|
||||||
|
void enter_event(Core::Event&) override
|
||||||
|
{
|
||||||
|
set_background_role(ColorRole::HoverHighlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
void leave_event(Core::Event&) override
|
||||||
|
{
|
||||||
|
set_background_role(ColorRole::NoRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
RefPtr<GUI::ImageWidget> m_image;
|
||||||
|
RefPtr<GUI::Widget> m_label_container;
|
||||||
|
RefPtr<GUI::Label> m_title;
|
||||||
|
RefPtr<GUI::Label> m_subtitle;
|
||||||
|
bool m_is_highlighted { false };
|
||||||
|
};
|
||||||
|
|
||||||
|
class Database {
|
||||||
|
public:
|
||||||
|
explicit Database(AppState& state)
|
||||||
|
: m_state(state)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Function<void(Vector<NonnullRefPtr<Result>>)> on_new_results;
|
||||||
|
|
||||||
|
void search(String const& query)
|
||||||
|
{
|
||||||
|
|
||||||
|
m_app_provider.query(query, [=, this](auto results) {
|
||||||
|
recv_results(query, results);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_calculator_provider.query(query, [=, this](auto results) {
|
||||||
|
recv_results(query, results);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void recv_results(String const& query, Vector<NonnullRefPtr<Result>> const& results)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Threading::Locker db_locker(m_lock);
|
||||||
|
auto it = m_result_cache.find(query);
|
||||||
|
if (it == m_result_cache.end()) {
|
||||||
|
m_result_cache.set(query, {});
|
||||||
|
}
|
||||||
|
it = m_result_cache.find(query);
|
||||||
|
|
||||||
|
for (auto& result : results) {
|
||||||
|
auto found = it->value.find_if([result](auto& other) {
|
||||||
|
return result->equals(other);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (found.is_end())
|
||||||
|
it->value.append(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Threading::Locker state_locker(m_state.lock);
|
||||||
|
auto new_results = m_result_cache.find(m_state.last_query);
|
||||||
|
if (new_results == m_result_cache.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
dual_pivot_quick_sort(new_results->value, 0, static_cast<int>(new_results->value.size() - 1), [](auto& a, auto& b) {
|
||||||
|
return a->score() > b->score();
|
||||||
|
});
|
||||||
|
|
||||||
|
on_new_results(new_results->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppState& m_state;
|
||||||
|
|
||||||
|
AppProvider m_app_provider;
|
||||||
|
CalculatorProvider m_calculator_provider;
|
||||||
|
|
||||||
|
Threading::Lock m_lock;
|
||||||
|
HashMap<String, Vector<NonnullRefPtr<Result>>> m_result_cache;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr size_t MAX_SEARCH_RESULTS = 6;
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
if (pledge("stdio recvfd sendfd rpath unix proc exec", nullptr) < 0) {
|
||||||
|
perror("pledge");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto app = GUI::Application::construct(argc, argv);
|
||||||
|
auto window = GUI::Window::construct();
|
||||||
|
|
||||||
|
Assistant::AppState app_state;
|
||||||
|
Assistant::Database db { app_state };
|
||||||
|
|
||||||
|
auto& container = window->set_main_widget<GUI::Widget>();
|
||||||
|
container.set_fill_with_background_color(true);
|
||||||
|
auto& layout = container.set_layout<GUI::VerticalBoxLayout>();
|
||||||
|
layout.set_margins({ 8, 8, 8, 0 });
|
||||||
|
|
||||||
|
auto& text_box = container.add<GUI::TextBox>();
|
||||||
|
auto& results_container = container.add<GUI::Widget>();
|
||||||
|
auto& results_layout = results_container.set_layout<GUI::VerticalBoxLayout>();
|
||||||
|
results_layout.set_margins({ 0, 10, 0, 10 });
|
||||||
|
|
||||||
|
auto mark_selected_item = [&]() {
|
||||||
|
for (size_t i = 0; i < app_state.visible_result_count; ++i) {
|
||||||
|
auto& row = dynamic_cast<Assistant::ResultRow&>(results_container.child_widgets()[i]);
|
||||||
|
row.set_is_highlighted(i == app_state.selected_index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
text_box.on_change = [&]() {
|
||||||
|
{
|
||||||
|
Threading::Locker locker(app_state.lock);
|
||||||
|
if (app_state.last_query == text_box.text())
|
||||||
|
return;
|
||||||
|
|
||||||
|
app_state.last_query = text_box.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
db.search(text_box.text());
|
||||||
|
};
|
||||||
|
text_box.on_return_pressed = [&]() {
|
||||||
|
app_state.results[app_state.selected_index]->activate();
|
||||||
|
exit(0);
|
||||||
|
};
|
||||||
|
text_box.on_up_pressed = [&]() {
|
||||||
|
if (app_state.selected_index == 0)
|
||||||
|
app_state.selected_index = app_state.visible_result_count - 1;
|
||||||
|
else if (app_state.selected_index > 0)
|
||||||
|
app_state.selected_index -= 1;
|
||||||
|
|
||||||
|
mark_selected_item();
|
||||||
|
};
|
||||||
|
text_box.on_down_pressed = [&]() {
|
||||||
|
if ((app_state.visible_result_count - 1) == app_state.selected_index)
|
||||||
|
app_state.selected_index = 0;
|
||||||
|
else if (app_state.visible_result_count > app_state.selected_index)
|
||||||
|
app_state.selected_index += 1;
|
||||||
|
|
||||||
|
mark_selected_item();
|
||||||
|
};
|
||||||
|
text_box.on_escape_pressed = []() {
|
||||||
|
exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
db.on_new_results = [&](auto results) {
|
||||||
|
app_state.selected_index = 0;
|
||||||
|
app_state.results = results;
|
||||||
|
app_state.visible_result_count = min(results.size(), MAX_SEARCH_RESULTS);
|
||||||
|
results_container.remove_all_children();
|
||||||
|
|
||||||
|
for (size_t i = 0; i < app_state.visible_result_count; ++i) {
|
||||||
|
auto result = app_state.results[i];
|
||||||
|
auto& match = results_container.add<Assistant::ResultRow>();
|
||||||
|
match.set_image(result->bitmap());
|
||||||
|
match.set_title(result->title());
|
||||||
|
match.on_selected = [result_copy = result]() {
|
||||||
|
result_copy->activate();
|
||||||
|
exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result->kind() == Assistant::Result::Kind::Calculator) {
|
||||||
|
match.set_subtitle("'Enter' will copy to clipboard");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mark_selected_item();
|
||||||
|
|
||||||
|
auto window_height = app_state.visible_result_count * 40 + text_box.height() + 28;
|
||||||
|
window->resize(GUI::Desktop::the().rect().width() / 3, window_height);
|
||||||
|
};
|
||||||
|
|
||||||
|
window->set_frameless(true);
|
||||||
|
window->resize(GUI::Desktop::the().rect().width() / 3, 46);
|
||||||
|
window->center_on_screen();
|
||||||
|
window->move_to(window->x(), window->y() - (GUI::Desktop::the().rect().height() * 0.33));
|
||||||
|
window->show();
|
||||||
|
|
||||||
|
return app->exec();
|
||||||
|
}
|
|
@ -9,6 +9,9 @@
|
||||||
#include <LibCore/ConfigFile.h>
|
#include <LibCore/ConfigFile.h>
|
||||||
#include <LibCore/DirIterator.h>
|
#include <LibCore/DirIterator.h>
|
||||||
#include <LibDesktop/AppFile.h>
|
#include <LibDesktop/AppFile.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <serenity.h>
|
||||||
|
#include <spawn.h>
|
||||||
|
|
||||||
namespace Desktop {
|
namespace Desktop {
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue