Merge remote-tracking branch 'origin/dev' into search-open-explorer-window-tweaks

# Conflicts:
#	Flow.Launcher/ViewModel/MainViewModel.cs
This commit is contained in:
Hongtao Zhang 2023-01-03 22:38:53 -05:00
commit f78c212c5e
No known key found for this signature in database
GPG key ID: 75F655B91C7AC9BB
477 changed files with 52410 additions and 34361 deletions

146
.editorconfig Normal file
View file

@ -0,0 +1,146 @@
# To learn more about .editorconfig see https://aka.ms/editorconfigdocs
###############################
# Core EditorConfig Options #
###############################
# All files
[*]
indent_style = space
# XML project files
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
indent_size = 2
# XML config files
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
indent_size = 2
# Code files
[*.{cs,csx,vb,vbx}]
indent_size = 4
insert_final_newline = true
charset = utf-8-bom
###############################
# .NET Coding Conventions #
###############################
[*.{cs,vb}]
# Organize usings
dotnet_sort_system_directives_first = true
# this. preferences
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_event = false:silent
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
dotnet_style_readonly_field = true:suggestion
# Expression-level preferences
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
###############################
# Naming Conventions #
###############################
# Style Definitions
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# Use PascalCase for constant fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 2
end_of_line = crlf
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_diagnostic.CA1416.severity = silent
###############################
# C# Coding Conventions #
###############################
[*.cs]
dotnet_diagnostics.VSTHRD200.severity = none # VSTHRD200: Use "Async" suffix for async methods
dotnet_analyzer_diagnostic.VSTHRD200.severity = none # VSTHRD200: Use "Async" suffix for async methods
# var preferences
csharp_style_var_for_built_in_types = true:silent
csharp_style_var_when_type_is_apparent = true:silent
csharp_style_var_elsewhere = true:silent
# Expression-bodied members
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
# Null-checking preferences
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
# Expression-level preferences
csharp_prefer_braces = true:silent
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
###############################
# C# Formatting Rules #
###############################
# New line preferences
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
# Space preferences
csharp_space_after_cast = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_around_binary_operators = before_and_after
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
# Wrapping preferences
csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true
csharp_using_directive_placement = outside_namespace:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_style_namespace_declarations = block_scoped:silent
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
###############################
# VB Coding Conventions #
###############################
[*.vb]
# Modifier preferences
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion

View file

@ -1,32 +0,0 @@
---
name: "\U0001F41E Bug report"
about: Create a bug report to help us improve Flow Launcher
title: "[Describe Your Bug]"
labels: 'bug'
assignees: ''
---
**Describe the bug/issue**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. ...
2. ...
3. ...
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Your System**
```
Windows build number: (run "ver" at a command prompt)
Flow Launcher version: (Settings => About)
```
**Flow Launcher Error Log**
<!--
The latest log file can be found here: %AppData%\FlowLauncher\Logs\<version>\<date>.txt
or for portable mode: %LocalAppData%\FlowLauncher\<App-Version>\UserData\Logs\<version>\<date>.txt
Drag and drop the log file below this comment.
-->

78
.github/ISSUE_TEMPLATE/bug-report.yaml vendored Normal file
View file

@ -0,0 +1,78 @@
name: "\U0001F41E Bug Report"
description: Create a bug report to help us improve Flow Launcher
title: "BUG: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: Thanks for taking the time to fill out this bug report!
- type: checkboxes
attributes:
label: Checks
options:
- label: >
I have checked that this issue has not already been reported.
- type: textarea
attributes:
label: Problem Description
description: A clear and concise description of what the problem is.
validations:
required: true
- type: textarea
attributes:
label: To Reproduce
description: Steps to reproduce the behavior.
value: >
1. ...
2. ...
3. ...
- type: textarea
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
- type: input
attributes:
label: Flow Launcher Version
description: Go to "Settings" => "About".
value: v1.8.3
- type: input
attributes:
label: Windows Build Number
description: Run "ver" at CMD (command prompt).
value: 10.0.19043.1288
- type: textarea
id: logs
attributes:
label: Error Log
description: >
Log file place:
- The latest version place: `%AppData%\FlowLauncher\Logs\<version>\<date>.txt`
- For portable mode: `%LocalAppData%\FlowLauncher\<App-Version>\UserData\Logs\<version>\<date>.txt`
value: >
<details>
```shell
Replace this line with the important log contents.
```
</details>
<!-- # Or drag and drop the log file and delete the above detail part. -->

17
.github/actions/spelling/README.md vendored Normal file
View file

@ -0,0 +1,17 @@
# check-spelling/check-spelling configuration
File | Purpose | Format | Info
-|-|-|-
[dictionary.txt](dictionary.txt) | Replacement dictionary (creating this file will override the default dictionary) | one word per line | [dictionary](https://github.com/check-spelling/check-spelling/wiki/Configuration#dictionary)
[allow.txt](allow.txt) | Add words to the dictionary | one word per line (only letters and `'`s allowed) | [allow](https://github.com/check-spelling/check-spelling/wiki/Configuration#allow)
[reject.txt](reject.txt) | Remove words from the dictionary (after allow) | grep pattern matching whole dictionary words | [reject](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-reject)
[excludes.txt](excludes.txt) | Files to ignore entirely | perl regular expression | [excludes](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-excludes)
[only.txt](only.txt) | Only check matching files (applied after excludes) | perl regular expression | [only](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-only)
[patterns.txt](patterns.txt) | Patterns to ignore from checked lines | perl regular expression (order matters, first match wins) | [patterns](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-patterns)
[candidate.patterns](candidate.patterns) | Patterns that might be worth adding to [patterns.txt](patterns.txt) | perl regular expression with optional comment block introductions (all matches will be suggested) | [candidates](https://github.com/check-spelling/check-spelling/wiki/Feature:-Suggest-patterns)
[line_forbidden.patterns](line_forbidden.patterns) | Patterns to flag in checked lines | perl regular expression (order matters, first match wins) | [patterns](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-patterns)
[expect.txt](expect.txt) | Expected words that aren't in the dictionary | one word per line (sorted, alphabetically) | [expect](https://github.com/check-spelling/check-spelling/wiki/Configuration#expect)
[advice.md](advice.md) | Supplement for GitHub comment when unrecognized words are found | GitHub Markdown | [advice](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice)
Note: you can replace any of these files with a directory by the same name (minus the suffix)
and then include multiple files inside that directory (with that suffix) to merge multiple files together.

25
.github/actions/spelling/advice.md vendored Normal file
View file

@ -0,0 +1,25 @@
<!-- See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice --> <!-- markdownlint-disable MD033 MD041 -->
<details><summary>If the flagged items are :exploding_head: false positives</summary>
If items relate to a ...
* binary file (or some other file you wouldn't want to check at all).
Please add a file path to the `excludes.txt` file matching the containing file.
File paths are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your files.
`^` refers to the file's path from the root of the repository, so `^README\.md$` would exclude [README.md](
../tree/HEAD/README.md) (on whichever branch you're using).
* well-formed pattern.
If you can write a [pattern](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns) that would match it,
try adding it to the `patterns.txt` file.
Patterns are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your lines.
Note that patterns can't match multiline strings.
</details>

4
.github/actions/spelling/allow.txt vendored Normal file
View file

@ -0,0 +1,4 @@
github
https
ssh
ubuntu

View file

@ -0,0 +1,522 @@
# marker to ignore all code on line
^.*/\* #no-spell-check-line \*/.*$
# marker for ignoring a comment to the end of the line
// #no-spell-check.*$
# patch hunk comments
^\@\@ -\d+(?:,\d+|) \+\d+(?:,\d+|) \@\@ .*
# git index header
index [0-9a-z]{7,40}\.\.[0-9a-z]{7,40}
# cid urls
(['"])cid:.*?\g{-1}
# data url in parens
\(data:[^)]*?(?:[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})[^)]*\)
# data url in quotes
([`'"])data:.*?(?:[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,}).*\g{-1}
# data url
data:[-a-zA-Z=;:/0-9+]*,\S*
# mailto urls
mailto:[-a-zA-Z=;:/?%&0-9+@.]{3,}
# magnet urls
magnet:[?=:\w]+
# magnet urls
"magnet:[^"]+"
# obs:
"obs:[^"]*"
# The `\b` here means a break, it's the fancy way to handle urls, but it makes things harder to read
# In this examples content, I'm using a number of different ways to match things to show various approaches
# asciinema
\basciinema\.org/a/[0-9a-zA-Z]+
# apple
\bdeveloper\.apple\.com/[-\w?=/]+
# Apple music
\bembed\.music\.apple\.com/fr/playlist/usr-share/[-\w.]+
# appveyor api
\bci\.appveyor\.com/api/projects/status/[0-9a-z]+
# appveyor project
\bci\.appveyor\.com/project/(?:[^/\s"]*/){2}builds?/\d+/job/[0-9a-z]+
# Amazon
# Amazon
\bamazon\.com/[-\w]+/(?:dp/[0-9A-Z]+|)
# AWS S3
\b\w*\.s3[^.]*\.amazonaws\.com/[-\w/&#%_?:=]*
# AWS execute-api
\b[0-9a-z]{10}\.execute-api\.[-0-9a-z]+\.amazonaws\.com\b
# AWS ELB
\b\w+\.[-0-9a-z]+\.elb\.amazonaws\.com\b
# AWS SNS
\bsns\.[-0-9a-z]+.amazonaws\.com/[-\w/&#%_?:=]*
# AWS VPC
vpc-\w+
# While you could try to match `http://` and `https://` by using `s?` in `https?://`, sometimes there
# YouTube url
\b(?:(?:www\.|)youtube\.com|youtu.be)/(?:channel/|embed/|user/|playlist\?list=|watch\?v=|v/|)[-a-zA-Z0-9?&=_%]*
# YouTube music
\bmusic\.youtube\.com/youtubei/v1/browse(?:[?&]\w+=[-a-zA-Z0-9?&=_]*)
# YouTube tag
<\s*youtube\s+id=['"][-a-zA-Z0-9?_]*['"]
# YouTube image
\bimg\.youtube\.com/vi/[-a-zA-Z0-9?&=_]*
# Google Accounts
\baccounts.google.com/[-_/?=.:;+%&0-9a-zA-Z]*
# Google Analytics
\bgoogle-analytics\.com/collect.[-0-9a-zA-Z?%=&_.~]*
# Google APIs
\bgoogleapis\.(?:com|dev)/[a-z]+/(?:v\d+/|)[a-z]+/[-@:./?=\w+|&]+
# Google Storage
\b[-a-zA-Z0-9.]*\bstorage\d*\.googleapis\.com(?:/\S*|)
# Google Calendar
\bcalendar\.google\.com/calendar(?:/u/\d+|)/embed\?src=[@./?=\w&%]+
\w+\@group\.calendar\.google\.com\b
# Google DataStudio
\bdatastudio\.google\.com/(?:(?:c/|)u/\d+/|)(?:embed/|)(?:open|reporting|datasources|s)/[-0-9a-zA-Z]+(?:/page/[-0-9a-zA-Z]+|)
# The leading `/` here is as opposed to the `\b` above
# ... a short way to match `https://` or `http://` since most urls have one of those prefixes
# Google Docs
/docs\.google\.com/[a-z]+/(?:ccc\?key=\w+|(?:u/\d+|d/(?:e/|)[0-9a-zA-Z_-]+/)?(?:edit\?[-\w=#.]*|/\?[\w=&]*|))
# Google Drive
\bdrive\.google\.com/(?:file/d/|open)[-0-9a-zA-Z_?=]*
# Google Groups
\bgroups\.google\.com/(?:(?:forum/#!|d/)(?:msg|topics?|searchin)|a)/[^/\s"]+/[-a-zA-Z0-9$]+(?:/[-a-zA-Z0-9]+)*
# Google Maps
\bmaps\.google\.com/maps\?[\w&;=]*
# Google themes
themes\.googleusercontent\.com/static/fonts/[^/\s"]+/v\d+/[^.]+.
# Google CDN
\bclients2\.google(?:usercontent|)\.com[-0-9a-zA-Z/.]*
# Goo.gl
/goo\.gl/[a-zA-Z0-9]+
# Google Chrome Store
\bchrome\.google\.com/webstore/detail/[-\w]*(?:/\w*|)
# Google Books
\bgoogle\.(?:\w{2,4})/books(?:/\w+)*\?[-\w\d=&#.]*
# Google Fonts
\bfonts\.(?:googleapis|gstatic)\.com/[-/?=:;+&0-9a-zA-Z]*
# Google Forms
\bforms\.gle/\w+
# Google Scholar
\bscholar\.google\.com/citations\?user=[A-Za-z0-9_]+
# Google Colab Research Drive
\bcolab\.research\.google\.com/drive/[-0-9a-zA-Z_?=]*
# GitHub SHAs (api)
\bapi.github\.com/repos(?:/[^/\s"]+){3}/[0-9a-f]+\b
# GitHub SHAs (markdown)
(?:\[`?[0-9a-f]+`?\]\(https:/|)/(?:www\.|)github\.com(?:/[^/\s"]+){2,}(?:/[^/\s")]+)(?:[0-9a-f]+(?:[-0-9a-zA-Z/#.]*|)\b|)
# GitHub SHAs
\bgithub\.com(?:/[^/\s"]+){2}[@#][0-9a-f]+\b
# GitHub wiki
\bgithub\.com/(?:[^/]+/){2}wiki/(?:(?:[^/]+/|)_history|[^/]+(?:/_compare|)/[0-9a-f.]{40,})\b
# githubusercontent
/[-a-z0-9]+\.githubusercontent\.com/[-a-zA-Z0-9?&=_\/.]*
# githubassets
\bgithubassets.com/[0-9a-f]+(?:[-/\w.]+)
# gist github
\bgist\.github\.com/[^/\s"]+/[0-9a-f]+
# git.io
\bgit\.io/[0-9a-zA-Z]+
# GitHub JSON
"node_id": "[-a-zA-Z=;:/0-9+]*"
# Contributor
\[[^\]]+\]\(https://github\.com/[^/\s"]+\)
# GHSA
GHSA(?:-[0-9a-z]{4}){3}
# GitLab commit
\bgitlab\.[^/\s"]*/\S+/\S+/commit/[0-9a-f]{7,16}#[0-9a-f]{40}\b
# GitLab merge requests
\bgitlab\.[^/\s"]*/\S+/\S+/-/merge_requests/\d+/diffs#[0-9a-f]{40}\b
# GitLab uploads
\bgitlab\.[^/\s"]*/uploads/[-a-zA-Z=;:/0-9+]*
# GitLab commits
\bgitlab\.[^/\s"]*/(?:[^/\s"]+/){2}commits?/[0-9a-f]+\b
# binanace
accounts.binance.com/[a-z/]*oauth/authorize\?[-0-9a-zA-Z&%]*
# bitbucket diff
\bapi\.bitbucket\.org/\d+\.\d+/repositories/(?:[^/\s"]+/){2}diff(?:stat|)(?:/[^/\s"]+){2}:[0-9a-f]+
# bitbucket repositories commits
\bapi\.bitbucket\.org/\d+\.\d+/repositories/(?:[^/\s"]+/){2}commits?/[0-9a-f]+
# bitbucket commits
\bbitbucket\.org/(?:[^/\s"]+/){2}commits?/[0-9a-f]+
# bit.ly
\bbit\.ly/\w+
# bitrise
\bapp\.bitrise\.io/app/[0-9a-f]*/[\w.?=&]*
# bootstrapcdn.com
\bbootstrapcdn\.com/[-./\w]+
# cdn.cloudflare.com
\bcdnjs\.cloudflare\.com/[./\w]+
# circleci
\bcircleci\.com/gh(?:/[^/\s"]+){1,5}.[a-z]+\?[-0-9a-zA-Z=&]+
# gitter
\bgitter\.im(?:/[^/\s"]+){2}\?at=[0-9a-f]+
# gravatar
\bgravatar\.com/avatar/[0-9a-f]+
# ibm
[a-z.]*ibm\.com/[-_#=:%!?~.\\/\d\w]*
# imgur
\bimgur\.com/[^.]+
# Internet Archive
\barchive\.org/web/\d+/(?:[-\w.?,'/\\+&%$#_:]*)
# discord
/discord(?:app\.com|\.gg)/(?:invite/)?[a-zA-Z0-9]{7,}
# Disqus
\bdisqus\.com/[-\w/%.()!?&=_]*
# medium link
\blink\.medium\.com/[a-zA-Z0-9]+
# medium
\bmedium\.com/\@?[^/\s"]+/[-\w]+
# microsoft
\b(?:https?://|)(?:(?:download\.visualstudio|docs|msdn2?|research)\.microsoft|blogs\.msdn)\.com/[-_a-zA-Z0-9()=./%]*
# powerbi
\bapp\.powerbi\.com/reportEmbed/[^"' ]*
# vs devops
\bvisualstudio.com(?::443|)/[-\w/?=%&.]*
# microsoft store
\bmicrosoft\.com/store/apps/\w+
# mvnrepository.com
\bmvnrepository\.com/[-0-9a-z./]+
# now.sh
/[0-9a-z-.]+\.now\.sh\b
# oracle
\bdocs\.oracle\.com/[-0-9a-zA-Z./_?#&=]*
# chromatic.com
/\S+.chromatic.com\S*[")]
# codacy
\bapi\.codacy\.com/project/badge/Grade/[0-9a-f]+
# compai
\bcompai\.pub/v1/png/[0-9a-f]+
# mailgun api
\.api\.mailgun\.net/v3/domains/[0-9a-z]+\.mailgun.org/messages/[0-9a-zA-Z=@]*
# mailgun
\b[0-9a-z]+.mailgun.org
# /message-id/
/message-id/[-\w@./%]+
# Reddit
\breddit\.com/r/[/\w_]*
# requestb.in
\brequestb\.in/[0-9a-z]+
# sched
\b[a-z0-9]+\.sched\.com\b
# Slack url
slack://[a-zA-Z0-9?&=]+
# Slack
\bslack\.com/[-0-9a-zA-Z/_~?&=.]*
# Slack edge
\bslack-edge\.com/[-a-zA-Z0-9?&=%./]+
# Slack images
\bslack-imgs\.com/[-a-zA-Z0-9?&=%.]+
# shields.io
\bshields\.io/[-\w/%?=&.:+;,]*
# stackexchange -- https://stackexchange.com/feeds/sites
\b(?:askubuntu|serverfault|stack(?:exchange|overflow)|superuser).com/(?:questions/\w+/[-\w]+|a/)
# Sentry
[0-9a-f]{32}\@o\d+\.ingest\.sentry\.io\b
# Twitter markdown
\[\@[^[/\]:]*?\]\(https://twitter.com/[^/\s"')]*(?:/status/\d+(?:\?[-_0-9a-zA-Z&=]*|)|)\)
# Twitter hashtag
\btwitter\.com/hashtag/[\w?_=&]*
# Twitter status
\btwitter\.com/[^/\s"')]*(?:/status/\d+(?:\?[-_0-9a-zA-Z&=]*|)|)
# Twitter profile images
\btwimg\.com/profile_images/[_\w./]*
# Twitter media
\btwimg\.com/media/[-_\w./?=]*
# Twitter link shortened
\bt\.co/\w+
# facebook
\bfburl\.com/[0-9a-z_]+
# facebook CDN
\bfbcdn\.net/[\w/.,]*
# facebook watch
\bfb\.watch/[0-9A-Za-z]+
# dropbox
\bdropbox\.com/sh?/[^/\s"]+/[-0-9A-Za-z_.%?=&;]+
# ipfs protocol
ipfs://[0-9a-z]*
# ipfs url
/ipfs/[0-9a-z]*
# w3
\bw3\.org/[-0-9a-zA-Z/#.]+
# loom
\bloom\.com/embed/[0-9a-f]+
# regex101
\bregex101\.com/r/[^/\s"]+/\d+
# figma
\bfigma\.com/file(?:/[0-9a-zA-Z]+/)+
# freecodecamp.org
\bfreecodecamp\.org/[-\w/.]+
# image.tmdb.org
\bimage\.tmdb\.org/[/\w.]+
# mermaid
\bmermaid\.ink/img/[-\w]+|\bmermaid-js\.github\.io/mermaid-live-editor/#/edit/[-\w]+
# Wikipedia
\ben\.wikipedia\.org/wiki/[-\w%.#]+
# gitweb
[^"\s]+/gitweb/\S+;h=[0-9a-f]+
# HyperKitty lists
/archives/list/[^@/]+\@[^/\s"]*/message/[^/\s"]*/
# lists
/thread\.html/[^"\s]+
# list-management
\blist-manage\.com/subscribe(?:[?&](?:u|id)=[0-9a-f]+)+
# kubectl.kubernetes.io/last-applied-configuration
"kubectl.kubernetes.io/last-applied-configuration": ".*"
# pgp
\bgnupg\.net/pks/lookup[?&=0-9a-zA-Z]*
# Spotify
\bopen\.spotify\.com/embed/playlist/\w+
# Mastodon
\bmastodon\.[-a-z.]*/(?:media/|\@)[?&=0-9a-zA-Z_]*
# scastie
\bscastie\.scala-lang\.org/[^/]+/\w+
# images.unsplash.com
\bimages\.unsplash\.com/(?:(?:flagged|reserve)/|)[-\w./%?=%&.;]+
# pastebin
\bpastebin\.com/[\w/]+
# heroku
\b\w+\.heroku\.com/source/archive/\w+
# quip
\b\w+\.quip\.com/\w+(?:(?:#|/issues/)\w+)?
# badgen.net
\bbadgen\.net/badge/[^")\]'\s]+
# statuspage.io
\w+\.statuspage\.io\b
# media.giphy.com
\bmedia\.giphy\.com/media/[^/]+/[\w.?&=]+
# tinyurl
\btinyurl\.com/\w+
# getopts
\bgetopts\s+(?:"[^"]+"|'[^']+')
# ANSI color codes
(?:\\(?:u00|x)1b|\x1b)\[\d+(?:;\d+|)m
# URL escaped characters
\%[0-9A-F][A-F]
# IPv6
\b(?:[0-9a-fA-F]{0,4}:){3,7}[0-9a-fA-F]{0,4}\b
# c99 hex digits (not the full format, just one I've seen)
0x[0-9a-fA-F](?:\.[0-9a-fA-F]*|)[pP]
# Punycode
\bxn--[-0-9a-z]+
# sha
sha\d+:[0-9]*[a-f]{3,}[0-9a-f]*
# sha-... -- uses a fancy capture
(['"]|&quot;)[0-9a-f]{40,}\g{-1}
# hex runs
\b[0-9a-fA-F]{16,}\b
# hex in url queries
=[0-9a-fA-F]*?(?:[A-F]{3,}|[a-f]{3,})[0-9a-fA-F]*?&
# ssh
(?:ssh-\S+|-nistp256) [-a-zA-Z=;:/0-9+]{12,}
# PGP
\b(?:[0-9A-F]{4} ){9}[0-9A-F]{4}\b
# GPG keys
\b(?:[0-9A-F]{4} ){5}(?: [0-9A-F]{4}){5}\b
# Well known gpg keys
.well-known/openpgpkey/[\w./]+
# uuid:
\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b
# hex digits including css/html color classes:
(?:[\\0][xX]|\\u|[uU]\+|#x?|\%23)[0-9_a-fA-FgGrR]*?[a-fA-FgGrR]{2,}[0-9_a-fA-FgGrR]*(?:[uUlL]{0,3}|u\d+)\b
# integrity
integrity="sha\d+-[-a-zA-Z=;:/0-9+]{40,}"
# https://www.gnu.org/software/groff/manual/groff.html
# man troff content
\\f[BCIPR]
# '
\\\(aq
# .desktop mime types
^MimeTypes?=.*$
# .desktop localized entries
^[A-Z][a-z]+\[[a-z]+\]=.*$
# Localized .desktop content
Name\[[^\]]+\]=.*
# IServiceProvider
\bI(?=(?:[A-Z][a-z]{2,})+\b)
# crypt
"\$2[ayb]\$.{56}"
# scrypt / argon
\$(?:scrypt|argon\d+[di]*)\$\S+
# Input to GitHub JSON
content: "[-a-zA-Z=;:/0-9+]*="
# Python stringprefix / binaryprefix
# Note that there's a high false positive rate, remove the `?=` and search for the regex to see if the matches seem like reasonable strings
(?<!')\b(?:B|BR|Br|F|FR|Fr|R|RB|RF|Rb|Rf|U|UR|Ur|b|bR|br|f|fR|fr|r|rB|rF|rb|rf|u|uR|ur)'(?:[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})
# Regular expressions for (P|p)assword
\([A-Z]\|[a-z]\)[a-z]+
# JavaScript regular expressions
# javascript test regex
/.*/[gim]*\.test\(
# javascript match regex
\.match\(/[^/\s"]*/[gim]*\s*
# javascript match regex
\.match\(/\\[b].*?/[gim]*\s*\)(?:;|$)
# javascript regex
^\s*/\\[b].*/[gim]*\s*(?:\)(?:;|$)|,$)
# javascript replace regex
\.replace\(/[^/\s"]*/[gim]*\s*,
# Go regular expressions
regexp?\.MustCompile\(`[^`]*`\)
# sed regular expressions
sed 's/(?:[^/]*?[a-zA-Z]{3,}[^/]*?/){2}
# go install
go install(?:\s+[a-z]+\.[-@\w/.]+)+
# kubernetes pod status lists
# https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase
\w+(?:-\w+)+\s+\d+/\d+\s+(?:Running|Pending|Succeeded|Failed|Unknown)\s+
# kubectl - pods in CrashLoopBackOff
\w+-[0-9a-f]+-\w+\s+\d+/\d+\s+CrashLoopBackOff\s+
# kubernetes object suffix
-[0-9a-f]{10}-\w{5}\s
# posthog secrets
posthog\.init\((['"])phc_[^"',]+\g{-1},
# xcode
# xcodeproject scenes
(?:Controller|ID|id)="\w{3}-\w{2}-\w{3}"
# xcode api botches
customObjectInstantitationMethod
# font awesome classes
\.fa-[-a-z0-9]+
# Update Lorem based on your content (requires `ge` and `w` from https://github.com/jsoref/spelling; and `review` from https://github.com/check-spelling/check-spelling/wiki/Looking-for-items-locally )
# grep '^[^#].*lorem' .github/actions/spelling/patterns.txt|perl -pne 's/.*i..\?://;s/\).*//' |tr '|' "\n"|sort -f |xargs -n1 ge|perl -pne 's/^[^:]*://'|sort -u|w|sed -e 's/ .*//'|w|review -
# Warning, while `(?i)` is very neat and fancy, if you have some binary files that aren't proper unicode, you might run into:
## Operation "substitution (s///)" returns its argument for non-Unicode code point 0x1C19AE (the code point will vary).
## You could manually change `(?i)X...` to use `[Xx]...`
## or you could add the files to your `excludes` file (a version after 0.0.19 should identify the file path)
# Lorem
(?:\w|\s|[,.])*\b(?i)(?:amet|consectetur|cursus|dolor|eros|ipsum|lacus|libero|ligula|lorem|magna|neque|nulla|suscipit|tempus)\b(?:\w|\s|[,.])*
# Non-English
[a-zA-Z]*[ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź][a-zA-Z]{3}[a-zA-ZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź]*
# French
# This corpus only had capital letters, but you probably want lowercase ones as well.
\b[LN]'+[a-z]{2,}\b
# latex
\\(?:n(?:ew|ormal|osub)|r(?:enew)|t(?:able(?:of|)|he|itle))(?=[a-z]+)
# the negative lookahead here is to allow catching 'templatesz' as a misspelling
# but to otherwise recognize a Windows path with \templates\foo.template or similar:
\\(?:necessary|r(?:eport|esolve[dr]?|esult)|t(?:arget|emplates?))(?![a-z])
# ignore long runs of a single character:
\b([A-Za-z])\g{-1}{3,}\b
# Note that the next example is no longer necessary if you are using
# to match a string starting with a `#`, use a character-class:
[#]backwards
# version suffix <word>v#
(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_]))
# Compiler flags (Scala)
(?:^|[\t ,>"'`=(])-J-[DPWXY](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,})
# Compiler flags
(?:^|[\t ,"'`=(])-[DPWXYLlf](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,})
# Compiler flags (linker)
,-B
# curl arguments
\b(?:\\n|)curl(?:\s+-[a-zA-Z]{1,2}\b)*(?:\s+-[a-zA-Z]{3,})(?:\s+-[a-zA-Z]+)*
# set arguments
\bset(?:\s+-[abefimouxE]{1,2})*\s+-[abefimouxE]{3,}(?:\s+-[abefimouxE]+)*
# tar arguments
\b(?:\\n|)g?tar(?:\.exe|)(?:(?:\s+--[-a-zA-Z]+|\s+-[a-zA-Z]+|\s[ABGJMOPRSUWZacdfh-pr-xz]+\b)(?:=[^ ]*|))+
# tput arguments -- https://man7.org/linux/man-pages/man5/terminfo.5.html -- technically they can be more than 5 chars long...
\btput\s+(?:(?:-[SV]|-T\s*\w+)\s+)*\w{3,5}\b
# macOS temp folders
/var/folders/\w\w/[+\w]+/(?:T|-Caches-)/

73
.github/actions/spelling/excludes.txt vendored Normal file
View file

@ -0,0 +1,73 @@
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-excludes
(?:^|/)(?i)COPYRIGHT
(?:^|/)(?i)LICEN[CS]E
(?:^|/)3rdparty/
(?:^|/)go\.sum$
(?:^|/)package(?:-lock|)\.json$
(?:^|/)vendor/
\.a$
\.ai$
\.avi$
\.bmp$
\.bz2$
\.class$
\.crt$
\.dll$
\.docx?$
\.drawio$
\.DS_Store$
\.eot$
\.exe$
\.gif$
\.gitattributes$
\.gitignore$
\.graffle$
\.gz$
\.icns$
\.ico$
\.jar$
\.jks$
\.jpe?g$
\.key$
\.lib$
\.lock$
\.map$
\.min\..
\.mod$
\.mp[34]$
\.o$
\.ocf$
\.otf$
\.pdf$
\.pem$
\.png$
\.psd$
\.pyc$
\.s$
\.svgz?$
\.tar$
\.tiff?$
\.ttf$
\.wav$
\.webm$
\.webp$
\.woff2?$
\.xlsx?$
\.zip$
^\.github/actions/spelling/
^\Q.github/workflows/spelling.yml\E$
# Custom
(?:^|/)Languages/(?!en\.xaml)
Scripts/
\.resx$
^\QPlugins/Flow.Launcher.Plugin.WindowsSettings/WindowsSettings.json\E$
^\QPlugins/Flow.Launcher.Plugin.WebSearch/setting.json\E$
(?:^|/)FodyWeavers\.xml
.editorconfig
ignore$
\.ps1$
\.yml$
\.sln$
\.csproj$
\.DotSettings$
\.targets$

64
.github/actions/spelling/expect.txt vendored Normal file
View file

@ -0,0 +1,64 @@
crowdin
DWM
workflows
wpf
actionkeyword
stackoverflow
Wox
flowlauncher
Fody
stackoverflow
IContext
IShell
IPlugin
appveyor
netflix
youtube
appdata
Prioritise
Segoe
Google
Customise
UWP
uwp
Bokmal
Bokm
uninstallation
uninstalling
voidtools
fullscreen
hotkeys
totalcmd
lnk
amazonaws
mscorlib
pythonw
dotnet
winget
jjw24
wolframalpha
gmail
duckduckgo
facebook
findicon
baidu
pls
websearch
qianlifeng
userdata
srchadmin
EWX
dlgtext
CMD
appref-ms
appref
TSource
runas
dpi
popup
ptr
pluginindicator
TobiasSekan
Img
img
resx

View file

@ -0,0 +1,62 @@
# reject `m_data` as there's a certain OS which has evil defines that break things if it's used elsewhere
# \bm_data\b
# If you have a framework that uses `it()` for testing and `fit()` for debugging a specific test,
# you might not want to check in code where you were debugging w/ `fit()`, in which case, you might want
# to use this:
#\bfit\(
# s.b. GitHub
#\bGithub\b
# s.b. GitLab
\bGitlab\b
# s.b. JavaScript
\bJavascript\b
# s.b. Microsoft
\bMicroSoft\b
# s.b. another
\ban[- ]other\b
# s.b. greater than
\bgreater then\b
# s.b. into
\sin to\s
# s.b. opt-in
\sopt in\s
# s.b. less than
\bless then\b
# s.b. otherwise
\bother[- ]wise\b
# s.b. nonexistent
\bnon existing\b
\b[Nn]o[nt][- ]existent\b
# s.b. preexisting
[Pp]re[- ]existing
# s.b. preempt
[Pp]re[- ]empt\b
# s.b. preemptively
[Pp]re[- ]emptively
# s.b. reentrancy
[Rr]e[- ]entrancy
# s.b. reentrant
[Rr]e[- ]entrant
# s.b. workaround(s)
\bwork[- ]arounds?\b
# Reject duplicate words
\s([A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})\s\g{-1}\s

117
.github/actions/spelling/patterns.txt vendored Normal file
View file

@ -0,0 +1,117 @@
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns
# Questionably acceptable forms of `in to`
# Personally, I prefer `log into`, but people object
# https://www.tprteaching.com/log-into-log-in-to-login/
\b[Ll]og in to\b
# acceptable duplicates
# ls directory listings
[-bcdlpsw](?:[-r][-w][-sx]){3}\s+\d+\s+(\S+)\s+\g{-1}\s+\d+\s+
# C types and repeated CSS values
\s(center|div|inherit|long|LONG|none|normal|solid|thin|transparent|very)(?: \g{-1})+\s
# go templates
\s(\w+)\s+\g{-1}\s+\`(?:graphql|json|yaml):
# javadoc / .net
(?:[\\@](?:groupname|param)|(?:public|private)(?:\s+static|\s+readonly)*)\s+(\w+)\s+\g{-1}\s
# Commit message -- Signed-off-by and friends
^\s*(?:(?:Based-on-patch|Co-authored|Helped|Mentored|Reported|Reviewed|Signed-off)-by|Thanks-to): (?:[^<]*<[^>]*>|[^<]*)\s*$
# Autogenerated revert commit message
^This reverts commit [0-9a-f]{40}\.$
# ignore long runs of a single character:
\b([A-Za-z])\g{-1}{3,}\b
# Automatically suggested patterns
# hit-count: 360 file-count: 108
# IServiceProvider
\bI(?=(?:[A-Z][a-z]{2,})+\b)
# hit-count: 297 file-count: 18
# uuid:
\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b
# hit-count: 138 file-count: 27
# hex digits including css/html color classes:
(?:[\\0][xX]|\\u|[uU]\+|#x?|\%23)[0-9_a-fA-FgGrR]*?[a-fA-FgGrR]{2,}[0-9_a-fA-FgGrR]*(?:[uUlL]{0,3}|u\d+)\b
# hit-count: 93 file-count: 28
# hex runs
\b[0-9a-fA-F]{16,}\b
# hit-count: 52 file-count: 3
# githubusercontent
/[-a-z0-9]+\.githubusercontent\.com/[-a-zA-Z0-9?&=_\/.]*
/[-a-z0-9]+\.github\.com/[-a-zA-Z0-9?&=_\/.]*
# hit-count: 24 file-count: 12
# GitHub SHAs (markdown)
(?:\[`?[0-9a-f]+`?\]\(https:/|)/(?:www\.|)github\.com(?:/[^/\s"]+){2,}(?:/[^/\s")]+)(?:[0-9a-f]+(?:[-0-9a-zA-Z/#.]*|)\b|)
# hit-count: 11 file-count: 10
# stackexchange -- https://stackexchange.com/feeds/sites
\b(?:askubuntu|serverfault|stack(?:exchange|overflow)|superuser).com/(?:questions/\w+/[-\w]+|a/)
# hit-count: 11 file-count: 8
# microsoft
\b(?:https?://|)(?:(?:download\.visualstudio|docs|msdn2?|research)\.microsoft|blogs\.msdn)\.com/[-_a-zA-Z0-9()=./%]*
# hit-count: 2 file-count: 2
# Twitter status
\btwitter\.com/[^/\s"')]*(?:/status/\d+(?:\?[-_0-9a-zA-Z&=]*|)|)
# hit-count: 2 file-count: 1
# discord
/discord(?:app\.com|\.gg)/(?:invite/)?[a-zA-Z0-9]{7,}
# hit-count: 1 file-count: 1
# appveyor api
\bci\.appveyor\.com/api/projects/status/[0-9a-z]+
# hit-count: 1 file-count: 1
# gist github
\bgist\.github\.com/[^/\s"]+/[0-9a-f]+
# Automatically suggested patterns
# hit-count: 21 file-count: 21
# w3
\bw3\.org/[-0-9a-zA-Z/#.]+
# hit-count: 6 file-count: 1
# shields.io
\bshields\.io/[-\w/%?=&.:+;,]*
# hit-count: 3 file-count: 2
# While you could try to match `http://` and `https://` by using `s?` in `https?://`, sometimes there
# YouTube url
\b(?:(?:www\.|)youtube\.com|youtu.be)/(?:channel/|embed/|user/|playlist\?list=|watch\?v=|v/|)[-a-zA-Z0-9?&=_%]*
# hit-count: 2 file-count: 2
# Google Maps
\bmaps\.google\.com/maps\?[\w&;=]*
# hit-count: 2 file-count: 2
# Contributor
\[[^\]]+\]\(https://github\.com/[^/\s"]+\)
# hit-count: 2 file-count: 1
# URL escaped characters
\%[0-9A-F][A-F]
# hit-count: 1 file-count: 1
# Compiler flags
(?:^|[\t ,"'`=(])-[DPWXYLlf](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,})
# Localization keys
#x:Key="[^"]+"
#{DynamicResource [^"]+}
# html tag
<\w+[^>]*>
</\w+[^>]*>
#http/https
(?:\b(?:https?|ftp|file)://)[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]

10
.github/actions/spelling/reject.txt vendored Normal file
View file

@ -0,0 +1,10 @@
^attache$
benefitting
occurences?
^dependan.*
^oer$
Sorce
^[Ss]pae.*
^untill$
^untilling$
^wether.*

21
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,21 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "nuget" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
ignore:
- dependency-name: "squirrel-windows"
reviewers:
- "jjw24"
- "taooceros"
- "JohnTheGr8"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

160
.github/workflows/spelling.yml vendored Normal file
View file

@ -0,0 +1,160 @@
name: Check Spelling
# Comment management is handled through a secondary job, for details see:
# https://github.com/check-spelling/check-spelling/wiki/Feature%3A-Restricted-Permissions
#
# `jobs.comment-push` runs when a push is made to a repository and the `jobs.spelling` job needs to make a comment
# (in odd cases, it might actually run just to collapse a comment, but that's fairly rare)
# it needs `contents: write` in order to add a comment.
#
# `jobs.comment-pr` runs when a pull_request is made to a repository and the `jobs.spelling` job needs to make a comment
# or collapse a comment (in the case where it had previously made a comment and now no longer needs to show a comment)
# it needs `pull-requests: write` in order to manipulate those comments.
# Updating pull request branches is managed via comment handling.
# For details, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-expect-list
#
# These elements work together to make it happen:
#
# `on.issue_comment`
# This event listens to comments by users asking to update the metadata.
#
# `jobs.update`
# This job runs in response to an issue_comment and will push a new commit
# to update the spelling metadata.
#
# `with.experimental_apply_changes_via_bot`
# Tells the action to support and generate messages that enable it
# to make a commit to update the spelling metadata.
#
# `with.ssh_key`
# In order to trigger workflows when the commit is made, you can provide a
# secret (typically, a write-enabled github deploy key).
#
# For background, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-with-deploy-key
on:
push:
branches:
- '**'
- '!l10n_dev'
tags-ignore:
- "**"
pull_request_target:
branches:
- '**'
# - '!l10n_dev'
tags-ignore:
- "**"
types:
- 'opened'
- 'reopened'
- 'synchronize'
# issue_comment:
# types:
# - 'created'
jobs:
spelling:
name: Check Spelling
permissions:
contents: read
pull-requests: read
actions: read
security-events: write
outputs:
followup: ${{ steps.spelling.outputs.followup }}
runs-on: ubuntu-latest
if: (contains(github.event_name, 'pull_request') && github.head_ref != 'l10n_dev') || github.event_name == 'push'
concurrency:
group: spelling-${{ github.event.pull_request.number || github.ref }}
# note: If you use only_check_changed_files, you do not want cancel-in-progress
cancel-in-progress: false
steps:
- name: check-spelling
id: spelling
uses: check-spelling/check-spelling@main
with:
suppress_push_for_open_pull_request: 1
checkout: true
check_file_names: 1
spell_check_this: check-spelling/spell-check-this@main
post_comment: 0
use_magic_file: 1
experimental_apply_changes_via_bot: 1
use_sarif: 0 # to show in pr page
extra_dictionary_limit: 10
check_commit_messages: commits title description
only_check_changed_files: 1
check_extra_dictionaries: ''
quit_without_error: true
extra_dictionaries:
cspell:software-terms/src/software-terms.txt
cspell:win32/src/win32.txt
cspell:php/php.txt
cspell:filetypes/filetypes.txt
cspell:csharp/csharp.txt
cspell:dotnet/dotnet.txt
cspell:python/src/python/python-lib.txt
cspell:aws/aws.txt
cspell:companies/src/companies.txt
warnings:
binary-file,deprecated-feature,large-file,limited-references,noisy-file,non-alpha-in-dictionary,unexpected-line-ending,whitespace-in-dictionary,minified-file,unsupported-configuration,unrecognized-spelling,no-newline-at-eof
comment-push:
name: Report (Push)
# If your workflow isn't running on push, you can remove this job
runs-on: ubuntu-latest
needs: spelling
permissions:
contents: write
if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name == 'push'
steps:
- name: comment
uses: check-spelling/check-spelling@main
with:
checkout: true
spell_check_this: check-spelling/spell-check-this@main
task: ${{ needs.spelling.outputs.followup }}
comment-pr:
name: Report (PR)
# If you workflow isn't running on pull_request*, you can remove this job
runs-on: ubuntu-latest
needs: spelling
permissions:
pull-requests: write
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
steps:
- name: comment
uses: check-spelling/check-spelling@main
with:
checkout: true
spell_check_this: check-spelling/spell-check-this@main
task: ${{ needs.spelling.outputs.followup }}
experimental_apply_changes_via_bot: 1
# update:
# name: Update PR
# permissions:
# contents: write
# pull-requests: write
# actions: read
# runs-on: ubuntu-latest
# if: ${{
# github.event_name == 'issue_comment' &&
# github.event.issue.pull_request &&
# contains(github.event.comment.body, '@check-spelling-bot apply')
# }}
# concurrency:
# group: spelling-update-${{ github.event.issue.number }}
# cancel-in-progress: false
# steps:
# - name: apply spelling updates
# uses: check-spelling/check-spelling@main
# with:
# experimental_apply_changes_via_bot: 1
# checkout: true
# ssh_key: "${{ secrets.CHECK_SPELLING }}"

View file

@ -13,7 +13,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v4
- uses: actions/stale@v7
with:
stale-issue-message: 'This issue is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
days-before-stale: 45

15
.github/workflows/winget.yml vendored Normal file
View file

@ -0,0 +1,15 @@
name: Publish to Winget
on:
release:
types: [released]
jobs:
publish:
# Action can only be run on windows
runs-on: windows-latest
steps:
- uses: vedantmgoyal2009/winget-releaser@v1
with:
identifier: Flow-Launcher.Flow-Launcher
token: ${{ secrets.WINGET_TOKEN }}

View file

@ -1,4 +1,4 @@
using Microsoft.Win32;
using Microsoft.Win32;
using Squirrel;
using System;
using System.IO;
@ -47,7 +47,7 @@ namespace Flow.Launcher.Core.Configuration
}
catch (Exception e)
{
Log.Exception("|Portable.DisablePortableMode|Error occured while disabling portable mode", e);
Log.Exception("|Portable.DisablePortableMode|Error occurred while disabling portable mode", e);
}
}
@ -71,7 +71,7 @@ namespace Flow.Launcher.Core.Configuration
}
catch (Exception e)
{
Log.Exception("|Portable.EnablePortableMode|Error occured while enabling portable mode", e);
Log.Exception("|Portable.EnablePortableMode|Error occurred while enabling portable mode", e);
}
}
@ -127,7 +127,7 @@ namespace Flow.Launcher.Core.Configuration
using (var portabilityUpdater = NewUpdateManager())
{
portabilityUpdater.CreateUninstallerRegistryEntry();
_ = portabilityUpdater.CreateUninstallerRegistryEntry();
}
}
@ -187,7 +187,7 @@ namespace Flow.Launcher.Core.Configuration
if (roamingLocationExists && portableLocationExists)
{
MessageBox.Show(string.Format("Flow Launcher detected your user data exists both in {0} and " +
"{1}. {2}{2}Please delete {1} in order to proceed. No changes have occured.",
"{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.",
DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine));
return false;

View file

@ -0,0 +1,234 @@
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
public abstract class AbstractPluginEnvironment
{
internal abstract string Language { get; }
internal abstract string EnvName { get; }
internal abstract string EnvPath { get; }
internal abstract string InstallPath { get; }
internal abstract string ExecutablePath { get; }
internal virtual string FileDialogFilter => string.Empty;
internal abstract string PluginsSettingsFilePath { get; set; }
internal List<PluginMetadata> PluginMetadataList;
internal PluginsSettings PluginSettings;
internal AbstractPluginEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings)
{
PluginMetadataList = pluginMetadataList;
PluginSettings = pluginSettings;
}
internal IEnumerable<PluginPair> Setup()
{
if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase)))
return new List<PluginPair>();
// TODO: Remove. This is backwards compatibility for 1.10.0 release- changed PythonEmbeded to Environments/Python
if (Language.Equals(AllowedLanguage.Python, StringComparison.OrdinalIgnoreCase))
{
FilesFolders.RemoveFolderIfExists(Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable"));
if (!string.IsNullOrEmpty(PluginSettings.PythonDirectory) && PluginSettings.PythonDirectory.StartsWith(Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable")))
{
InstallEnvironment();
PluginSettings.PythonDirectory = string.Empty;
}
}
if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath))
{
// Ensure latest only if user is using Flow's environment setup.
if (PluginsSettingsFilePath.StartsWith(EnvPath, StringComparison.OrdinalIgnoreCase))
EnsureLatestInstalled(ExecutablePath, PluginsSettingsFilePath, EnvPath);
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
}
if (MessageBox.Show($"Flow detected you have installed {Language} plugins, which " +
$"will require {EnvName} to run. Would you like to download {EnvName}? " +
Environment.NewLine + Environment.NewLine +
"Click no if it's already installed, " +
$"and you will be prompted to select the folder that contains the {EnvName} executable",
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No)
{
var msg = $"Please select the {EnvName} executable";
var selectedFile = string.Empty;
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
if (!string.IsNullOrEmpty(selectedFile))
PluginsSettingsFilePath = selectedFile;
// Nothing selected because user pressed cancel from the file dialog window
if (string.IsNullOrEmpty(selectedFile))
InstallEnvironment();
}
else
{
InstallEnvironment();
}
if (FilesFolders.FileExists(PluginsSettingsFilePath))
{
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
}
else
{
MessageBox.Show(
$"Unable to set {Language} executable path, please try from Flow's settings (scroll down to the bottom).");
Log.Error("PluginsLoader",
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
$"{Language}Environment");
return new List<PluginPair>();
}
}
internal abstract void InstallEnvironment();
private void EnsureLatestInstalled(string expectedPath, string currentPath, string installedDirPath)
{
if (expectedPath == currentPath)
return;
FilesFolders.RemoveFolderIfExists(installedDirPath);
InstallEnvironment();
}
internal abstract PluginPair CreatePluginPair(string filePath, PluginMetadata metadata);
private IEnumerable<PluginPair> SetPathForPluginPairs(string filePath, string languageToSet)
{
var pluginPairs = new List<PluginPair>();
foreach (var metadata in PluginMetadataList)
{
if (metadata.Language.Equals(languageToSet, StringComparison.OrdinalIgnoreCase))
pluginPairs.Add(CreatePluginPair(filePath, metadata));
}
return pluginPairs;
}
private string GetFileFromDialog(string title, string filter = "")
{
var dlg = new OpenFileDialog
{
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
Multiselect = false,
CheckFileExists = true,
CheckPathExists = true,
Title = title,
Filter = filter
};
var result = dlg.ShowDialog();
if (result == DialogResult.OK)
{
return dlg.FileName;
}
else
{
return string.Empty;
}
}
/// <summary>
/// After app updated while in portable mode or switched between portable/roaming mode,
/// need to update each plugin's executable path so user will not be prompted again to reinstall the environments.
/// </summary>
/// <param name="settings"></param>
public static void PreStartPluginExecutablePathUpdate(Settings settings)
{
if (DataLocation.PortableDataLocationInUse())
{
// When user is using portable but has moved flow to a different location
if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName)
&& !settings.PluginSettings.PythonExecutablePath.StartsWith(DataLocation.PortableDataPath))
{
settings.PluginSettings.PythonExecutablePath
= GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath);
}
if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName)
&& !settings.PluginSettings.NodeExecutablePath.StartsWith(DataLocation.PortableDataPath))
{
settings.PluginSettings.NodeExecutablePath
= GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath);
}
// When user has switched from roaming to portable
if (IsUsingRoamingPath(settings.PluginSettings.PythonExecutablePath))
{
settings.PluginSettings.PythonExecutablePath
= settings.PluginSettings.PythonExecutablePath.Replace(DataLocation.RoamingDataPath, DataLocation.PortableDataPath);
}
if (IsUsingRoamingPath(settings.PluginSettings.NodeExecutablePath))
{
settings.PluginSettings.NodeExecutablePath
= settings.PluginSettings.NodeExecutablePath.Replace(DataLocation.RoamingDataPath, DataLocation.PortableDataPath);
}
}
else
{
if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName))
settings.PluginSettings.PythonExecutablePath
= GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath);
if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName))
settings.PluginSettings.NodeExecutablePath
= GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath);
}
}
private static bool IsUsingPortablePath(string filePath, string pluginEnvironmentName)
{
if (string.IsNullOrEmpty(filePath))
return false;
// DataLocation.PortableDataPath returns the current portable path, this determines if an out
// of date path is also a portable path.
var portableAppEnvLocation = $"UserData\\{DataLocation.PluginEnvironments}\\{pluginEnvironmentName}";
return filePath.Contains(portableAppEnvLocation);
}
private static bool IsUsingRoamingPath(string filePath)
{
if (string.IsNullOrEmpty(filePath))
return false;
return filePath.StartsWith(DataLocation.RoamingDataPath);
}
private static string GetUpdatedEnvironmentPath(string filePath)
{
var index = filePath.IndexOf(DataLocation.PluginEnvironments);
// get the substring after "Environments" because we can not determine it dynamically
var ExecutablePathSubstring = filePath.Substring(index + DataLocation.PluginEnvironments.Count());
return $"{DataLocation.PluginEnvironmentsPath}{ExecutablePathSubstring}";
}
}
}

View file

@ -0,0 +1,14 @@
using System.Collections.Generic;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
internal class JavaScriptEnvironment : TypeScriptEnvironment
{
internal override string Language => AllowedLanguage.JavaScript;
internal JavaScriptEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
}
}

View file

@ -0,0 +1,48 @@
using Droplex;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using System.Collections.Generic;
using System.IO;
namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
internal class PythonEnvironment : AbstractPluginEnvironment
{
internal override string Language => AllowedLanguage.Python;
internal override string EnvName => DataLocation.PythonEnvironmentName;
internal override string EnvPath => Path.Combine(DataLocation.PluginEnvironmentsPath, EnvName);
internal override string InstallPath => Path.Combine(EnvPath, "PythonEmbeddable-v3.8.9");
internal override string ExecutablePath => Path.Combine(InstallPath, "pythonw.exe");
internal override string FileDialogFilter => "Python|pythonw.exe";
internal override string PluginsSettingsFilePath { get => PluginSettings.PythonExecutablePath; set => PluginSettings.PythonExecutablePath = value; }
internal PythonEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
internal override void InstallEnvironment()
{
FilesFolders.RemoveFolderIfExists(InstallPath);
// Python 3.8.9 is used for Windows 7 compatibility
DroplexPackage.Drop(App.python_3_8_9_embeddable, InstallPath).Wait();
PluginsSettingsFilePath = ExecutablePath;
}
internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata)
{
return new PluginPair
{
Plugin = new PythonPlugin(filePath),
Metadata = metadata
};
}
}
}

View file

@ -0,0 +1,44 @@
using System.Collections.Generic;
using Droplex;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin;
using System.IO;
using Flow.Launcher.Core.Plugin;
namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
internal class TypeScriptEnvironment : AbstractPluginEnvironment
{
internal override string Language => AllowedLanguage.TypeScript;
internal override string EnvName => DataLocation.NodeEnvironmentName;
internal override string EnvPath => Path.Combine(DataLocation.PluginEnvironmentsPath, EnvName);
internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0");
internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe");
internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; }
internal TypeScriptEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
internal override void InstallEnvironment()
{
FilesFolders.RemoveFolderIfExists(InstallPath);
DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait();
PluginsSettingsFilePath = ExecutablePath;
}
internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata)
{
return new PluginPair
{
Plugin = new NodePlugin(filePath),
Metadata = metadata
};
}
}
}

View file

@ -0,0 +1,24 @@
using Flow.Launcher.Plugin;
using System;
namespace Flow.Launcher.Core.ExternalPlugins
{
public class FlowPluginException : Exception
{
public PluginMetadata Metadata { get; set; }
public FlowPluginException(PluginMetadata metadata, Exception e) : base(e.Message, e)
{
Metadata = metadata;
}
public override string ToString()
{
return $@"{Metadata.Name} Exception:
Websites: {Metadata.Website}
Author: {Metadata.Author}
Version: {Metadata.Version}
{base.ToString()}";
}
}
}

View file

@ -29,13 +29,13 @@ namespace Flow.Launcher.Core.ExternalPlugins
var request = new HttpRequestMessage(HttpMethod.Get, manifestFileUrl);
request.Headers.Add("If-None-Match", latestEtag);
var response = await Http.SendAsync(request, token).ConfigureAwait(false);
using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.OK)
{
Log.Info($"|PluginsManifest.{nameof(UpdateManifestAsync)}|Fetched plugins from manifest repo");
var json = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false);
await using var json = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false);
UserPlugins = await JsonSerializer.DeserializeAsync<List<UserPlugin>>(json, cancellationToken: token).ConfigureAwait(false);
@ -56,4 +56,4 @@ namespace Flow.Launcher.Core.ExternalPlugins
}
}
}
}
}

View file

@ -1,4 +1,6 @@
namespace Flow.Launcher.Core.ExternalPlugins
using System;
namespace Flow.Launcher.Core.ExternalPlugins
{
public record UserPlugin
{
@ -12,5 +14,8 @@
public string UrlDownload { get; set; }
public string UrlSourceCode { get; set; }
public string IcoPath { get; set; }
public DateTime LatestReleaseDate { get; set; }
public DateTime DateAdded { get; set; }
}
}

View file

@ -53,10 +53,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Droplex" Version="1.4.1" />
<PackageReference Include="FSharp.Core" Version="5.0.2" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.1.3" />
<PackageReference Include="squirrel.windows" Version="1.5.2" />
<PackageReference Include="Droplex" Version="1.6.0" />
<PackageReference Include="FSharp.Core" Version="6.0.6" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.1" />
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
</ItemGroup>
<ItemGroup>

View file

@ -9,7 +9,6 @@ namespace Flow.Launcher.Core.Plugin
internal class ExecutablePlugin : JsonRPCPlugin
{
private readonly ProcessStartInfo _startInfo;
public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable;
public ExecutablePlugin(string filename)
{

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
namespace Flow.Launcher.Core.Plugin
{
@ -26,6 +27,8 @@ namespace Flow.Launcher.Core.Plugin
public string Name { get; set; }
public string Label { get; set; }
public string Description { get; set; }
public string urlLabel { get; set; }
public Uri url { get; set; }
public bool Validation { get; set; }
public List<string> Options { get; set; }
public string DefaultValue { get; set; }
@ -40,4 +43,4 @@ namespace Flow.Launcher.Core.Plugin
DefaultValue = this.DefaultValue;
}
}
}
}

View file

@ -1,33 +1,31 @@
using Accessibility;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using ICSharpCode.SharpZipLib.Zip;
using JetBrains.Annotations;
using Microsoft.IO;
using System.Text.Json.Serialization;
using System.Windows;
using System.Windows.Controls;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using CheckBox = System.Windows.Controls.CheckBox;
using Control = System.Windows.Controls.Control;
using Label = System.Windows.Controls.Label;
using Orientation = System.Windows.Controls.Orientation;
using TextBox = System.Windows.Controls.TextBox;
using UserControl = System.Windows.Controls.UserControl;
using System.Windows.Data;
using System.Windows.Documents;
using static System.Windows.Forms.LinkLabel;
using Droplex;
using System.Windows.Forms;
namespace Flow.Launcher.Core.Plugin
{
@ -40,10 +38,6 @@ namespace Flow.Launcher.Core.Plugin
protected PluginInitContext context;
public const string JsonRPC = "JsonRPC";
/// <summary>
/// The language this JsonRPCPlugin support
/// </summary>
public abstract string SupportedLanguage { get; set; }
protected abstract Task<Stream> RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default);
protected abstract string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default);
@ -69,7 +63,13 @@ namespace Flow.Launcher.Core.Plugin
private static readonly JsonSerializerOptions options = new()
{
PropertyNameCaseInsensitive = true,
#pragma warning disable SYSLIB0020
// IgnoreNullValues is obsolete, but the replacement JsonIgnoreCondition.WhenWritingNull still
// deserializes null, instead of ignoring it and leaving the default (empty list). We can change the behaviour
// to accept null and fallback to a default etc, or just keep IgnoreNullValues for now
// see: https://github.com/dotnet/runtime/issues/39152
IgnoreNullValues = true,
#pragma warning restore SYSLIB0020 // Type or member is obsolete
Converters =
{
new JsonObjectConverter()
@ -82,16 +82,19 @@ namespace Flow.Launcher.Core.Plugin
};
private Dictionary<string, object> Settings { get; set; }
private Dictionary<string, FrameworkElement> _settingControls = new();
private readonly Dictionary<string, FrameworkElement> _settingControls = new();
private async Task<List<Result>> DeserializedResultAsync(Stream output)
{
if (output == Stream.Null) return null;
await using (output)
{
if (output == Stream.Null) return null;
var queryResponseModel =
await JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output, options);
var queryResponseModel =
await JsonSerializer.DeserializeAsync<JsonRPCQueryResponseModel>(output, options);
return ParseResults(queryResponseModel);
return ParseResults(queryResponseModel);
}
}
private List<Result> DeserializedResult(string output)
@ -133,7 +136,7 @@ namespace Flow.Launcher.Core.Plugin
}
else
{
var actionResponse = await RequestAsync(result.JsonRPCAction);
await using var actionResponse = await RequestAsync(result.JsonRPCAction);
if (actionResponse.Length == 0)
{
@ -241,74 +244,55 @@ namespace Flow.Launcher.Core.Plugin
protected async Task<Stream> ExecuteAsync(ProcessStartInfo startInfo, CancellationToken token = default)
{
Process process = null;
using var exitTokenSource = new CancellationTokenSource();
using var process = Process.Start(startInfo);
if (process == null)
{
Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process");
return Stream.Null;
}
var sourceBuffer = BufferManager.GetStream();
using var errorBuffer = BufferManager.GetStream();
var sourceCopyTask = process.StandardOutput.BaseStream.CopyToAsync(sourceBuffer, token);
var errorCopyTask = process.StandardError.BaseStream.CopyToAsync(errorBuffer, token);
await using var registeredEvent = token.Register(() =>
{
if (!process.HasExited)
process.Kill();
sourceBuffer.Dispose();
});
try
{
process = Process.Start(startInfo);
if (process == null)
{
Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process");
return Stream.Null;
}
await using var source = process.StandardOutput.BaseStream;
var buffer = BufferManager.GetStream();
token.Register(() =>
{
// ReSharper disable once AccessToModifiedClosure
// Manually Check whether disposed
if (!exitTokenSource.IsCancellationRequested && !process.HasExited)
process.Kill();
});
try
{
// token expire won't instantly trigger the exception,
// manually kill process at before
await source.CopyToAsync(buffer, token);
}
catch (OperationCanceledException)
{
await buffer.DisposeAsync();
return Stream.Null;
}
buffer.Seek(0, SeekOrigin.Begin);
token.ThrowIfCancellationRequested();
if (buffer.Length == 0)
{
var errorMessage = process.StandardError.EndOfStream ?
"Empty JSONRPC Response" :
await process.StandardError.ReadToEndAsync();
throw new InvalidDataException($"{context.CurrentPluginMetadata.Name}|{errorMessage}");
}
if (!process.StandardError.EndOfStream)
{
using var standardError = process.StandardError;
var error = await standardError.ReadToEndAsync();
if (!string.IsNullOrEmpty(error))
{
Log.Error($"|{context.CurrentPluginMetadata.Name}.{nameof(ExecuteAsync)}|{error}");
}
}
return buffer;
// token expire won't instantly trigger the exception,
// manually kill process at before
await process.WaitForExitAsync(token);
await Task.WhenAll(sourceCopyTask, errorCopyTask);
}
finally
catch (OperationCanceledException)
{
exitTokenSource.Cancel();
process?.Dispose();
await sourceBuffer.DisposeAsync();
return Stream.Null;
}
switch (sourceBuffer.Length, errorBuffer.Length)
{
case (0, 0):
const string errorMessage = "Empty JSON-RPC Response.";
Log.Warn($"|{nameof(JsonRPCPlugin)}.{nameof(ExecuteAsync)}|{errorMessage}");
break;
case (_, not 0):
throw new InvalidDataException(Encoding.UTF8.GetString(errorBuffer.ToArray())); // The process has exited with an error message
}
sourceBuffer.Seek(0, SeekOrigin.Begin);
return sourceBuffer;
}
public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
{
var request = new JsonRPCRequestModel
@ -356,124 +340,240 @@ namespace Flow.Launcher.Core.Plugin
this.context = context;
await InitSettingAsync();
}
private static readonly Thickness settingControlMargin = new(10, 4, 10, 4);
private static readonly Thickness settingPanelMargin = new(15, 20, 15, 20);
private static readonly Thickness settingTextBlockMargin = new(10, 4, 10, 4);
private static readonly Thickness settingControlMargin = new(0, 9, 18, 9);
private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9);
private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0);
private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9);
private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9);
private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0);
private static readonly Thickness settingDescMargin = new(0, 2, 0, 0);
private static readonly Thickness settingSepMargin = new(0, 0, 0, 2);
private JsonRpcConfigurationModel _settingsTemplate;
public Control CreateSettingPanel()
{
if (Settings == null)
return new();
var settingWindow = new UserControl();
var mainPanel = new StackPanel
var mainPanel = new Grid
{
Margin = settingPanelMargin, Orientation = Orientation.Vertical
Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center
};
settingWindow.Content = mainPanel;
ColumnDefinition gridCol1 = new ColumnDefinition();
ColumnDefinition gridCol2 = new ColumnDefinition();
gridCol1.Width = new GridLength(70, GridUnitType.Star);
gridCol2.Width = new GridLength(30, GridUnitType.Star);
mainPanel.ColumnDefinitions.Add(gridCol1);
mainPanel.ColumnDefinitions.Add(gridCol2);
settingWindow.Content = mainPanel;
int rowCount = 0;
foreach (var (type, attribute) in _settingsTemplate.Body)
{
Separator sep = new Separator();
sep.VerticalAlignment = VerticalAlignment.Top;
sep.Margin = settingSepMargin;
sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */
var panel = new StackPanel
{
Orientation = Orientation.Horizontal, Margin = settingControlMargin
Orientation = Orientation.Vertical, VerticalAlignment = VerticalAlignment.Center,
Margin = settingLabelPanelMargin
};
RowDefinition gridRow = new RowDefinition();
mainPanel.RowDefinitions.Add(gridRow);
var name = new TextBlock()
{
Text = attribute.Label,
Width = 120,
VerticalAlignment = VerticalAlignment.Center,
Margin = settingControlMargin,
Margin = settingLabelMargin,
TextWrapping = TextWrapping.WrapWithOverflow
};
var desc = new TextBlock()
{
Text = attribute.Description, FontSize = 12,
VerticalAlignment = VerticalAlignment.Center,Margin = settingDescMargin,
TextWrapping = TextWrapping.WrapWithOverflow
};
desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B");
if (attribute.Description == null) /* if no description, hide */
desc.Visibility = Visibility.Collapsed;
if (type != "textBlock") /* if textBlock, hide desc */
{
panel.Children.Add(name);
panel.Children.Add(desc);
}
Grid.SetColumn(panel, 0);
Grid.SetRow(panel, rowCount);
FrameworkElement contentControl;
switch (type)
{
case "textBlock":
{
contentControl = new TextBlock
{
contentControl = new TextBlock
{
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
Margin = settingTextBlockMargin,
MaxWidth = 500,
TextWrapping = TextWrapping.WrapWithOverflow
};
Text = attribute.Description.Replace("\\r\\n", "\r\n"),
Margin = settingTextBlockMargin,
Padding = new Thickness(0,0,0,0),
HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
TextAlignment = TextAlignment.Left,
TextWrapping = TextWrapping.Wrap
};
Grid.SetColumn(contentControl, 0);
Grid.SetColumnSpan(contentControl, 2);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
}
case "input":
{
var textBox = new TextBox()
{
Text = Settings[attribute.Name] as string ?? string.Empty,
Margin = settingControlMargin,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
ToolTip = attribute.Description
};
textBox.TextChanged += (_, _) =>
{
Settings[attribute.Name] = textBox.Text;
};
contentControl = textBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
case "inputWithFileBtn":
{
var textBox = new TextBox()
{
Width = 300,
Margin = new Thickness(10, 0, 0, 0),
Text = Settings[attribute.Name] as string ?? string.Empty,
Margin = settingControlMargin,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
ToolTip = attribute.Description
};
textBox.TextChanged += (_, _) =>
{
Settings[attribute.Name] = textBox.Text;
};
contentControl = textBox;
var Btn = new System.Windows.Controls.Button()
{
Margin = new Thickness(10,0,0,0),
Content = "Browse"
};
var dockPanel = new DockPanel()
{
Margin = settingControlMargin
};
DockPanel.SetDock(Btn, Dock.Right);
dockPanel.Children.Add(Btn);
dockPanel.Children.Add(textBox);
contentControl = dockPanel;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
case "textarea":
{
var textBox = new TextBox()
{
var textBox = new TextBox()
{
Width = 300,
Height = 120,
Margin = settingControlMargin,
TextWrapping = TextWrapping.WrapWithOverflow,
AcceptsReturn = true,
Text = Settings[attribute.Name] as string ?? string.Empty,
ToolTip = attribute.Description
};
textBox.TextChanged += (sender, _) =>
{
Settings[attribute.Name] = ((TextBox)sender).Text;
};
contentControl = textBox;
Height = 120,
Margin = settingControlMargin,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.WrapWithOverflow,
AcceptsReturn = true,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
Text = Settings[attribute.Name] as string ?? string.Empty,
ToolTip = attribute.Description
};
textBox.TextChanged += (sender, _) =>
{
Settings[attribute.Name] = ((TextBox)sender).Text;
};
contentControl = textBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
}
case "passwordBox":
{
var passwordBox = new PasswordBox()
{
var passwordBox = new PasswordBox()
{
Width = 300,
Margin = settingControlMargin,
Password = Settings[attribute.Name] as string ?? string.Empty,
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
ToolTip = attribute.Description
};
passwordBox.PasswordChanged += (sender, _) =>
{
Settings[attribute.Name] = ((PasswordBox)sender).Password;
};
contentControl = passwordBox;
Margin = settingControlMargin,
Password = Settings[attribute.Name] as string ?? string.Empty,
PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
ToolTip = attribute.Description
};
passwordBox.PasswordChanged += (sender, _) =>
{
Settings[attribute.Name] = ((PasswordBox)sender).Password;
};
contentControl = passwordBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
}
case "dropdown":
{
var comboBox = new System.Windows.Controls.ComboBox()
{
var comboBox = new ComboBox()
{
ItemsSource = attribute.Options,
SelectedItem = Settings[attribute.Name],
Margin = settingControlMargin,
ToolTip = attribute.Description
};
comboBox.SelectionChanged += (sender, _) =>
{
Settings[attribute.Name] = (string)((ComboBox)sender).SelectedItem;
};
contentControl = comboBox;
ItemsSource = attribute.Options,
SelectedItem = Settings[attribute.Name],
Margin = settingControlMargin,
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
ToolTip = attribute.Description
};
comboBox.SelectionChanged += (sender, _) =>
{
Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem;
};
contentControl = comboBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
}
}
case "checkbox":
var checkBox = new CheckBox
{
IsChecked = Settings[attribute.Name] is bool isChecked ? isChecked : bool.Parse(attribute.DefaultValue),
Margin = settingControlMargin,
Margin = settingCheckboxMargin,
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
ToolTip = attribute.Description
};
checkBox.Click += (sender, _) =>
@ -481,18 +581,49 @@ namespace Flow.Launcher.Core.Plugin
Settings[attribute.Name] = ((CheckBox)sender).IsChecked;
};
contentControl = checkBox;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
case "hyperlink":
var hyperlink = new Hyperlink
{
ToolTip = attribute.Description,
NavigateUri = attribute.url
};
var linkbtn = new System.Windows.Controls.Button
{
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
Margin = settingControlMargin
};
linkbtn.Content = attribute.urlLabel;
contentControl = linkbtn;
Grid.SetColumn(contentControl, 1);
Grid.SetRow(contentControl, rowCount);
if (rowCount != 0)
mainPanel.Children.Add(sep);
Grid.SetRow(sep, rowCount);
Grid.SetColumn(sep, 0);
Grid.SetColumnSpan(sep, 2);
break;
default:
continue;
}
if (type != "textBlock")
_settingControls[attribute.Name] = contentControl;
panel.Children.Add(name);
panel.Children.Add(contentControl);
mainPanel.Children.Add(panel);
mainPanel.Children.Add(contentControl);
rowCount++;
}
return settingWindow;
}
public void Save()
{
if (Settings != null)
@ -524,7 +655,7 @@ namespace Flow.Launcher.Core.Plugin
case PasswordBox passwordBox:
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string);
break;
case ComboBox comboBox:
case System.Windows.Controls.ComboBox comboBox:
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
break;
case CheckBox checkBox:
@ -535,4 +666,5 @@ namespace Flow.Launcher.Core.Plugin
}
}
}
}
}

View file

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Core.Plugin
{
/// <summary>
/// Execution of JavaScript & TypeScript plugins
/// </summary>
internal class NodePlugin : JsonRPCPlugin
{
private readonly ProcessStartInfo _startInfo;
public NodePlugin(string filename)
{
_startInfo = new ProcessStartInfo
{
FileName = filename,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
}
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
{
_startInfo.ArgumentList[1] = request.ToString();
return ExecuteAsync(_startInfo, token);
}
protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default)
{
// since this is not static, request strings will build up in ArgumentList if index is not specified
_startInfo.ArgumentList[1] = rpcRequest.ToString();
return Execute(_startInfo);
}
public override async Task InitAsync(PluginInitContext context)
{
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
_startInfo.ArgumentList.Add(string.Empty);
await base.InitAsync(context);
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
}
}
}

View file

@ -1,4 +1,5 @@
using System;
using Flow.Launcher.Core.ExternalPlugins;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
@ -74,7 +75,7 @@ namespace Flow.Launcher.Core.Plugin
}
}
public static async Task ReloadData()
public static async Task ReloadDataAsync()
{
await Task.WhenAll(AllPlugins.Select(plugin => plugin.Plugin switch
{
@ -109,7 +110,7 @@ namespace Flow.Launcher.Core.Plugin
/// Call initialize for all plugins
/// </summary>
/// <returns>return the list of failed to init plugins or null for none</returns>
public static async Task InitializePlugins(IPublicAPI api)
public static async Task InitializePluginsAsync(IPublicAPI api)
{
API = api;
var failedPlugins = new ConcurrentQueue<PluginPair>();
@ -165,30 +166,28 @@ namespace Flow.Launcher.Core.Plugin
public static ICollection<PluginPair> ValidPluginsForQuery(Query query)
{
if (NonGlobalPlugins.ContainsKey(query.ActionKeyword))
{
var plugin = NonGlobalPlugins[query.ActionKeyword];
return new List<PluginPair>
{
plugin
};
}
else
{
if (query is null)
return Array.Empty<PluginPair>();
if (!NonGlobalPlugins.ContainsKey(query.ActionKeyword))
return GlobalPlugins;
}
var plugin = NonGlobalPlugins[query.ActionKeyword];
return new List<PluginPair>
{
plugin
};
}
public static async Task<List<Result>> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token)
{
var results = new List<Result>();
var metadata = pair.Metadata;
try
{
var metadata = pair.Metadata;
long milliseconds = -1L;
milliseconds = await Stopwatch.DebugAsync($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}",
var milliseconds = await Stopwatch.DebugAsync($"|PluginManager.QueryForPlugin|Cost for {metadata.Name}",
async () => results = await pair.Plugin.QueryAsync(query, token).ConfigureAwait(false));
token.ThrowIfCancellationRequested();
@ -206,7 +205,10 @@ namespace Flow.Launcher.Core.Plugin
// null will be fine since the results will only be added into queue if the token hasn't been cancelled
return null;
}
catch (Exception e)
{
throw new FlowPluginException(metadata, e);
}
return results;
}
@ -327,4 +329,4 @@ namespace Flow.Launcher.Core.Plugin
}
}
}
}
}

View file

@ -1,31 +1,38 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Forms;
using Droplex;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Core.ExternalPlugins.Environments;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using System.Diagnostics;
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
namespace Flow.Launcher.Core.Plugin
{
public static class PluginsLoader
{
public const string PythonExecutable = "pythonw.exe";
public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSettings settings)
{
var dotnetPlugins = DotNetPlugins(metadatas);
var pythonPlugins = PythonPlugins(metadatas, settings);
var pythonEnv = new PythonEnvironment(metadatas, settings);
var tsEnv = new TypeScriptEnvironment(metadatas, settings);
var jsEnv = new JavaScriptEnvironment(metadatas, settings);
var pythonPlugins = pythonEnv.Setup();
var tsPlugins = tsEnv.Setup();
var jsPlugins = jsEnv.Setup();
var executablePlugins = ExecutablePlugins(metadatas);
var plugins = dotnetPlugins.Concat(pythonPlugins).Concat(executablePlugins).ToList();
var plugins = dotnetPlugins
.Concat(pythonPlugins)
.Concat(tsPlugins)
.Concat(jsPlugins)
.Concat(executablePlugins)
.ToList();
return plugins;
}
@ -41,14 +48,6 @@ namespace Flow.Launcher.Core.Plugin
var milliseconds = Stopwatch.Debug(
$"|PluginsLoader.DotNetPlugins|Constructor init cost for {metadata.Name}", () =>
{
#if DEBUG
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
var assembly = assemblyLoader.LoadAssemblyAndDependencies();
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
typeof(IAsyncPlugin));
var plugin = Activator.CreateInstance(type) as IAsyncPlugin;
#else
Assembly assembly = null;
IAsyncPlugin plugin = null;
@ -62,6 +61,12 @@ namespace Flow.Launcher.Core.Plugin
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
}
#if DEBUG
catch (Exception e)
{
throw;
}
#else
catch (Exception e) when (assembly == null)
{
Log.Exception($"|PluginsLoader.DotNetPlugins|Couldn't load assembly for the plugin: {metadata.Name}", e);
@ -79,6 +84,7 @@ namespace Flow.Launcher.Core.Plugin
Log.Exception($"|PluginsLoader.DotNetPlugins|The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
}
#endif
if (plugin == null)
{
erroredPlugins.Add(metadata.Name);
@ -98,7 +104,7 @@ namespace Flow.Launcher.Core.Plugin
+ (erroredPlugins.Count > 1 ? "plugins have " : "plugin has ")
+ "errored and cannot be loaded:";
Task.Run(() =>
_ = Task.Run(() =>
{
MessageBox.Show($"{errorMessage}{Environment.NewLine}{Environment.NewLine}" +
$"{errorPluginString}{Environment.NewLine}{Environment.NewLine}" +
@ -110,97 +116,10 @@ namespace Flow.Launcher.Core.Plugin
return plugins;
}
public static IEnumerable<PluginPair> PythonPlugins(List<PluginMetadata> source, PluginsSettings settings)
{
if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python))
return new List<PluginPair>();
if (!string.IsNullOrEmpty(settings.PythonDirectory) && FilesFolders.LocationExists(settings.PythonDirectory))
return SetPythonPathForPluginPairs(source, Path.Combine(settings.PythonDirectory, PythonExecutable));
var pythonPath = string.Empty;
if (MessageBox.Show("Flow detected you have installed Python plugins, which " +
"will need Python to run. Would you like to download Python? " +
Environment.NewLine + Environment.NewLine +
"Click no if it's already installed, " +
"and you will be prompted to select the folder that contains the Python executable",
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No
&& string.IsNullOrEmpty(settings.PythonDirectory))
{
var dlg = new FolderBrowserDialog
{
SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
};
var result = dlg.ShowDialog();
if (result == DialogResult.OK)
{
string pythonDirectory = dlg.SelectedPath;
if (!string.IsNullOrEmpty(pythonDirectory))
{
pythonPath = Path.Combine(pythonDirectory, PythonExecutable);
if (File.Exists(pythonPath))
{
settings.PythonDirectory = pythonDirectory;
Constant.PythonPath = pythonPath;
}
else
{
MessageBox.Show("Can't find python in given directory");
}
}
}
}
else
{
var installedPythonDirectory = Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable");
// Python 3.8.9 is used for Windows 7 compatibility
DroplexPackage.Drop(App.python_3_8_9_embeddable, installedPythonDirectory).Wait();
pythonPath = Path.Combine(installedPythonDirectory, PythonExecutable);
if (FilesFolders.FileExists(pythonPath))
{
settings.PythonDirectory = installedPythonDirectory;
Constant.PythonPath = pythonPath;
}
else
{
Log.Error("PluginsLoader",
$"Failed to set Python path after Droplex install, {pythonPath} does not exist",
"PythonPlugins");
}
}
if (string.IsNullOrEmpty(settings.PythonDirectory) || string.IsNullOrEmpty(pythonPath))
{
MessageBox.Show(
"Unable to set Python executable path, please try from Flow's settings (scroll down to the bottom).");
Log.Error("PluginsLoader",
$"Not able to successfully set Python path, the PythonDirectory variable is still an empty string.",
"PythonPlugins");
return new List<PluginPair>();
}
return SetPythonPathForPluginPairs(source, pythonPath);
}
private static IEnumerable<PluginPair> SetPythonPathForPluginPairs(List<PluginMetadata> source, string pythonPath)
=> source
.Where(o => o.Language.ToUpper() == AllowedLanguage.Python)
.Select(metadata => new PluginPair
{
Plugin = new PythonPlugin(pythonPath),
Metadata = metadata
})
.ToList();
public static IEnumerable<PluginPair> ExecutablePlugins(IEnumerable<PluginMetadata> source)
{
return source
.Where(o => o.Language.ToUpper() == AllowedLanguage.Executable)
.Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase))
.Select(metadata => new PluginPair
{
Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
@ -11,7 +11,6 @@ namespace Flow.Launcher.Core.Plugin
internal class PythonPlugin : JsonRPCPlugin
{
private readonly ProcessStartInfo _startInfo;
public override string SupportedLanguage { get; set; } = AllowedLanguage.Python;
public PythonPlugin(string filename)
{
@ -46,6 +45,7 @@ namespace Flow.Launcher.Core.Plugin
protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default)
{
// since this is not static, request strings will build up in ArgumentList if index is not specified
_startInfo.ArgumentList[2] = rpcRequest.ToString();
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
// TODO: Async Action

View file

@ -1,3 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]

View file

@ -96,9 +96,16 @@ namespace Flow.Launcher.Core.Resource
{
LoadLanguage(language);
}
Settings.Language = language.LanguageCode;
CultureInfo.CurrentCulture = new CultureInfo(language.LanguageCode);
// Culture of this thread
// Use CreateSpecificCulture to preserve possible user-override settings in Windows
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
// App domain
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.DefaultThreadCurrentCulture;
// Raise event after culture is set
Settings.Language = language.LanguageCode;
_ = Task.Run(() =>
{
UpdatePluginMetadataTranslations();
@ -115,7 +122,11 @@ namespace Flow.Launcher.Core.Resource
if (languageToSet != AvailableLanguages.Chinese && languageToSet != AvailableLanguages.Chinese_TW)
return false;
if (MessageBox.Show("Do you want to turn on search with Pinyin?", string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
// No other languages should show the following text so just make it hard-coded
// "Do you want to search with pinyin?"
string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?" ;
if (MessageBox.Show(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
return false;
return true;
@ -182,7 +193,7 @@ namespace Flow.Launcher.Core.Resource
{
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
pluginI18N.OnCultureInfoChanged(CultureInfo.DefaultThreadCurrentCulture);
}
catch (Exception e)
{
@ -221,4 +232,4 @@ namespace Flow.Launcher.Core.Resource
}
}
}
}
}

View file

@ -4,7 +4,7 @@ using System.Globalization;
using System.Reflection;
using System.Windows.Data;
namespace Flow.Launcher.Core
namespace Flow.Launcher.Core.Resource
{
public class LocalizationConverter : IValueConverter
{

View file

@ -1,7 +1,6 @@
using System.ComponentModel;
using Flow.Launcher.Core.Resource;
namespace Flow.Launcher.Core
namespace Flow.Launcher.Core.Resource
{
public class LocalizedDescriptionAttribute : DescriptionAttribute
{

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -17,7 +17,7 @@ namespace Flow.Launcher.Core.Resource
{
public class Theme
{
private const int ShadowExtraMargin = 12;
private const int ShadowExtraMargin = 32;
private readonly List<string> _themeDirectories = new List<string>();
private ResourceDictionary _oldResource;
@ -102,7 +102,7 @@ namespace Flow.Launcher.Core.Resource
SetBlurForWindow();
}
catch (DirectoryNotFoundException e)
catch (DirectoryNotFoundException)
{
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found");
if (theme != defaultTheme)
@ -112,7 +112,7 @@ namespace Flow.Launcher.Core.Resource
}
return false;
}
catch (XamlParseException e)
catch (XamlParseException)
{
Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse");
if (theme != defaultTheme)
@ -238,9 +238,10 @@ namespace Flow.Launcher.Core.Resource
Property = Border.EffectProperty,
Value = new DropShadowEffect
{
Opacity = 0.4,
ShadowDepth = 2,
BlurRadius = 15
Opacity = 0.3,
ShadowDepth = 12,
Direction = 270,
BlurRadius = 30
}
};
@ -250,7 +251,7 @@ namespace Flow.Launcher.Core.Resource
marginSetter = new Setter()
{
Property = Border.MarginProperty,
Value = new Thickness(ShadowExtraMargin),
Value = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin),
};
windowBorderStyle.Setters.Add(marginSetter);
}

View file

@ -0,0 +1,19 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace Flow.Launcher.Core.Resource
{
public class TranslationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var key = value.ToString();
if (String.IsNullOrEmpty(key))
return key;
return InternationalizationManager.Instance.GetTranslation(key);
}
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
@ -40,7 +40,7 @@ namespace Flow.Launcher.Core
api.ShowMsg(api.GetTranslation("pleaseWait"),
api.GetTranslation("update_flowlauncher_update_check"));
using var updateManager = await GitHubUpdateManager(GitHubRepository).ConfigureAwait(false);
using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false);
// UpdateApp CheckForUpdate will return value only if the app is squirrel installed
var newUpdateInfo = await updateManager.CheckForUpdate().NonNull().ConfigureAwait(false);
@ -79,7 +79,7 @@ namespace Flow.Launcher.Core
await updateManager.CreateUninstallerRegistryEntry().ConfigureAwait(false);
}
var newVersionTips = NewVersinoTips(newReleaseVersion.ToString());
var newVersionTips = NewVersionTips(newReleaseVersion.ToString());
Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}");
@ -114,8 +114,8 @@ namespace Flow.Launcher.Core
public string HtmlUrl { get; [UsedImplicitly] set; }
}
/// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
private async Task<UpdateManager> GitHubUpdateManager(string repository)
// https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs
private async Task<UpdateManager> GitHubUpdateManagerAsync(string repository)
{
var uri = new Uri(repository);
var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases";
@ -137,12 +137,12 @@ namespace Flow.Launcher.Core
return manager;
}
public string NewVersinoTips(string version)
public string NewVersionTips(string version)
{
var translater = InternationalizationManager.Instance;
var tips = string.Format(translater.GetTranslation("newVersionTips"), version);
var translator = InternationalizationManager.Instance;
var tips = string.Format(translator.GetTranslation("newVersionTips"), version);
return tips;
}
}
}
}

View file

@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;
using System.IO;
using System.Reflection;
@ -21,6 +21,7 @@ namespace Flow.Launcher.Infrastructure
public static readonly string PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins);
public const string Issue = "https://github.com/Flow-Launcher/Flow.Launcher/issues/new";
public static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location.NonNull()).ProductVersion;
public static readonly string Dev = "Dev";
public const string Documentation = "https://flowlauncher.com/docs/#/usage-tips";
public static readonly int ThumbnailSize = 64;
@ -28,8 +29,10 @@ namespace Flow.Launcher.Infrastructure
public static readonly string DefaultIcon = Path.Combine(ImagesDirectory, "app.png");
public static readonly string ErrorIcon = Path.Combine(ImagesDirectory, "app_error.png");
public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png");
public static string PythonPath;
public static string NodePath;
public static readonly string QueryTextBoxIconImagePath = $"{ProgramDirectory}\\Images\\mainsearch.svg";
@ -44,6 +47,7 @@ namespace Flow.Launcher.Infrastructure
public const string Logs = "Logs";
public const string Website = "https://flowlauncher.com";
public const string SponsorPage = "https://github.com/sponsors/Flow-Launcher";
public const string GitHub = "https://github.com/Flow-Launcher/Flow.Launcher";
public const string Docs = "https://flowlauncher.com/docs";
}

View file

@ -67,6 +67,7 @@ namespace Flow.Launcher.Infrastructure.Exception
sb.AppendLine($"* IntPtr Length: {IntPtr.Size}");
sb.AppendLine($"* x64: {Environment.Is64BitOperatingSystem}");
sb.AppendLine($"* Python Path: {Constant.PythonPath}");
sb.AppendLine($"* Node Path: {Constant.NodePath}");
sb.AppendLine($"* CLR Version: {Environment.Version}");
sb.AppendLine($"* Installed .NET Framework: ");
foreach (var result in GetFrameworkVersionFromRegistry())
@ -78,7 +79,7 @@ namespace Flow.Launcher.Infrastructure.Exception
sb.AppendLine();
sb.AppendLine("## Assemblies - " + AppDomain.CurrentDomain.FriendlyName);
sb.AppendLine();
foreach (var ass in AppDomain.CurrentDomain.GetAssemblies().OrderBy(o => o.GlobalAssemblyCache ? 50 : 0))
foreach (var ass in AppDomain.CurrentDomain.GetAssemblies())
{
sb.Append("* ");
sb.Append(ass.FullName);
@ -166,7 +167,7 @@ namespace Flow.Launcher.Infrastructure.Exception
}
return result;
}
catch (System.Exception e)
catch
{
return new List<string>();
}

View file

@ -53,27 +53,16 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="16.10.56" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.4.27" />
<PackageReference Include="NLog" Version="4.7.10" />
<PackageReference Include="NLog.Schema" Version="4.7.10" />
<PackageReference Include="NLog.Web.AspNetCore" Version="4.13.0" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
<!--ToolGood.Words.Pinyin v3.0.2.6 results in high memory usage when search with pinyin is enabled-->
<!--Bumping to it or higher needs to test and ensure this is no longer a problem-->
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.0.1.4" />
</ItemGroup>
<ItemGroup>
<None Update="pinyindb\pinyin_gwoyeu_mapping.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="pinyindb\pinyin_mapping.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="pinyindb\unicode_to_hanyu_pinyin.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -68,7 +68,7 @@ namespace Flow.Launcher.Infrastructure.Http
var userName when string.IsNullOrEmpty(userName) =>
(new Uri($"http://{Proxy.Server}:{Proxy.Port}"), null),
_ => (new Uri($"http://{Proxy.Server}:{Proxy.Port}"),
new NetworkCredential(Proxy.UserName, Proxy.Password))
new NetworkCredential(Proxy.UserName, Proxy.Password))
},
_ => (null, null)
},
@ -79,7 +79,7 @@ namespace Flow.Launcher.Infrastructure.Http
_ => throw new ArgumentOutOfRangeException()
};
}
catch(UriFormatException e)
catch (UriFormatException e)
{
API.ShowMsg("Please try again", "Unable to parse Http Proxy");
Log.Exception("Flow.Launcher.Infrastructure.Http", "Unable to parse Uri", e);
@ -94,7 +94,7 @@ namespace Flow.Launcher.Infrastructure.Http
if (response.StatusCode == HttpStatusCode.OK)
{
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
await response.Content.CopyToAsync(fileStream);
await response.Content.CopyToAsync(fileStream, token);
}
else
{
@ -117,7 +117,7 @@ namespace Flow.Launcher.Infrastructure.Http
public static Task<string> GetAsync([NotNull] string url, CancellationToken token = default)
{
Log.Debug($"|Http.Get|Url <{url}>");
return GetAsync(new Uri(url.Replace("#", "%23")), token);
return GetAsync(new Uri(url), token);
}
/// <summary>
@ -130,36 +130,57 @@ namespace Flow.Launcher.Infrastructure.Http
{
Log.Debug($"|Http.Get|Url <{url}>");
using var response = await client.GetAsync(url, token);
var content = await response.Content.ReadAsStringAsync();
if (response.StatusCode == HttpStatusCode.OK)
{
return content;
}
else
var content = await response.Content.ReadAsStringAsync(token);
if (response.StatusCode != HttpStatusCode.OK)
{
throw new HttpRequestException(
$"Error code <{response.StatusCode}> with content <{content}> returned from <{url}>");
}
return content;
}
/// <summary>
/// Asynchrously get the result as stream from url.
/// Send a GET request to the specified Uri with an HTTP completion option and a cancellation token as an asynchronous operation.
/// </summary>
/// <param name="url">The Uri the request is sent to.</param>
/// <param name="completionOption">An HTTP completion option value that indicates when the operation should be considered completed.</param>
/// <param name="token">A cancellation token that can be used by other objects or threads to receive notice of cancellation</param>
/// <returns></returns>
public static Task<Stream> GetStreamAsync([NotNull] string url,
CancellationToken token = default) => GetStreamAsync(new Uri(url), token);
/// <summary>
/// Send a GET request to the specified Uri with an HTTP completion option and a cancellation token as an asynchronous operation.
/// </summary>
/// <param name="url"></param>
/// <param name="token"></param>
/// <returns></returns>
public static async Task<Stream> GetStreamAsync([NotNull] string url, CancellationToken token = default)
public static async Task<Stream> GetStreamAsync([NotNull] Uri url,
CancellationToken token = default)
{
Log.Debug($"|Http.Get|Url <{url}>");
var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
return await response.Content.ReadAsStreamAsync();
return await client.GetStreamAsync(url, token);
}
public static async Task<HttpResponseMessage> GetResponseAsync(string url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
CancellationToken token = default)
=> await GetResponseAsync(new Uri(url), completionOption, token);
public static async Task<HttpResponseMessage> GetResponseAsync([NotNull] Uri url, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead,
CancellationToken token = default)
{
Log.Debug($"|Http.Get|Url <{url}>");
return await client.GetAsync(url, completionOption, token);
}
/// <summary>
/// Asynchrously send an HTTP request.
/// </summary>
public static async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token = default)
public static async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken token = default)
{
return await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
return await client.SendAsync(request, completionOption, token);
}
}
}

View file

@ -1,9 +1,8 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;
namespace Flow.Launcher.Infrastructure.Image
@ -25,11 +24,11 @@ namespace Flow.Launcher.Infrastructure.Image
public class ImageCache
{
private const int MaxCached = 50;
public ConcurrentDictionary<string, ImageUsage> Data { get; private set; } = new ConcurrentDictionary<string, ImageUsage>();
public ConcurrentDictionary<(string, bool), ImageUsage> Data { get; } = new();
private const int permissibleFactor = 2;
private SemaphoreSlim semaphore = new(1, 1);
public void Initialization(Dictionary<string, int> usage)
public void Initialization(Dictionary<(string, bool), int> usage)
{
foreach (var key in usage.Keys)
{
@ -37,29 +36,29 @@ namespace Flow.Launcher.Infrastructure.Image
}
}
public ImageSource this[string path]
public ImageSource this[string path, bool isFullImage = false]
{
get
{
if (Data.TryGetValue(path, out var value))
if (!Data.TryGetValue((path, isFullImage), out var value))
{
value.usage++;
return value.imageSource;
return null;
}
value.usage++;
return value.imageSource;
return null;
}
set
{
Data.AddOrUpdate(
path,
new ImageUsage(0, value),
(k, v) =>
{
v.imageSource = value;
v.usage++;
return v;
}
(path, isFullImage),
new ImageUsage(0, value),
(k, v) =>
{
v.imageSource = value;
v.usage++;
return v;
}
);
SliceExtra();
@ -82,9 +81,24 @@ namespace Flow.Launcher.Infrastructure.Image
}
}
public bool ContainsKey(string key)
public bool ContainsKey(string key, bool isFullImage)
{
return key is not null && Data.ContainsKey(key) && Data[key].imageSource != null;
return key is not null && Data.ContainsKey((key, isFullImage)) && Data[(key, isFullImage)].imageSource != null;
}
public bool TryGetValue(string key, bool isFullImage, out ImageSource image)
{
if (key is not null)
{
bool hasKey = Data.TryGetValue((key, isFullImage), out var imageUsage);
image = hasKey ? imageUsage.imageSource : null;
return hasKey;
}
else
{
image = null;
return false;
}
}
public int CacheSize()
@ -100,4 +114,4 @@ namespace Flow.Launcher.Infrastructure.Image
return Data.Values.Select(x => x.imageSource).Distinct().Count();
}
}
}
}

View file

@ -14,30 +14,23 @@ namespace Flow.Launcher.Infrastructure.Image
{
public string GetHashFromImage(ImageSource imageSource)
{
if (!(imageSource is BitmapSource image))
if (imageSource is not BitmapSource image)
{
return null;
}
try
{
using (var outStream = new MemoryStream())
{
// PngBitmapEncoder enc2 = new PngBitmapEncoder();
// enc2.Frames.Add(BitmapFrame.Create(tt));
var enc = new JpegBitmapEncoder();
var bitmapFrame = BitmapFrame.Create(image);
bitmapFrame.Freeze();
enc.Frames.Add(bitmapFrame);
enc.Save(outStream);
var byteArray = outStream.GetBuffer();
using (var sha1 = new SHA1CryptoServiceProvider())
{
var hash = Convert.ToBase64String(sha1.ComputeHash(byteArray));
return hash;
}
}
using var outStream = new MemoryStream();
var enc = new JpegBitmapEncoder();
var bitmapFrame = BitmapFrame.Create(image);
bitmapFrame.Freeze();
enc.Frames.Add(bitmapFrame);
enc.Save(outStream);
var byteArray = outStream.GetBuffer();
using var sha1 = SHA1.Create();
var hash = Convert.ToBase64String(sha1.ComputeHash(byteArray));
return hash;
}
catch
{
@ -46,4 +39,4 @@ namespace Flow.Launcher.Infrastructure.Image
}
}
}
}

View file

@ -1,59 +1,63 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
using static Flow.Launcher.Infrastructure.Http.Http;
namespace Flow.Launcher.Infrastructure.Image
{
public static class ImageLoader
{
private static readonly ImageCache ImageCache = new ImageCache();
private static BinaryStorage<Dictionary<string, int>> _storage;
private static readonly ConcurrentDictionary<string, string> GuidToKey = new ConcurrentDictionary<string, string>();
private static readonly ImageCache ImageCache = new();
private static BinaryStorage<Dictionary<(string, bool), int>> _storage;
private static readonly ConcurrentDictionary<string, string> GuidToKey = new();
private static IImageHashGenerator _hashGenerator;
private static bool EnableImageHash = true;
public static ImageSource DefaultImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon));
private static readonly bool EnableImageHash = true;
public static ImageSource MissingImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon));
public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon));
public const int SmallIconSize = 64;
public const int FullIconSize = 256;
private static readonly string[] ImageExtensions =
{
".png",
".jpg",
".jpeg",
".gif",
".bmp",
".tiff",
".ico"
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico"
};
public static void Initialize()
{
_storage = new BinaryStorage<Dictionary<string, int>>("Image");
_storage = new BinaryStorage<Dictionary<(string, bool), int>>("Image");
_hashGenerator = new ImageHashGenerator();
var usage = LoadStorageToConcurrentDictionary();
foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon })
foreach (var icon in new[]
{
Constant.DefaultIcon, Constant.MissingImgIcon
})
{
ImageSource img = new BitmapImage(new Uri(icon));
img.Freeze();
ImageCache[icon] = img;
ImageCache[icon, false] = img;
}
Task.Run(() =>
_ = Task.Run(async () =>
{
Stopwatch.Normal("|ImageLoader.Initialize|Preload images cost", () =>
await Stopwatch.NormalAsync("|ImageLoader.Initialize|Preload images cost", async () =>
{
ImageCache.Data.AsParallel().ForAll(x =>
foreach (var ((path, isFullImage), _) in ImageCache.Data)
{
Load(x.Key);
});
await LoadAsync(path, isFullImage);
}
});
Log.Info($"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
});
@ -63,17 +67,20 @@ namespace Flow.Launcher.Infrastructure.Image
{
lock (_storage)
{
_storage.Save(ImageCache.Data.Select(x => (x.Key, x.Value.usage)).ToDictionary(x => x.Key, x => x.usage));
_storage.Save(ImageCache.Data
.ToDictionary(
x => x.Key,
x => x.Value.usage));
}
}
private static ConcurrentDictionary<string, int> LoadStorageToConcurrentDictionary()
private static ConcurrentDictionary<(string, bool), int> LoadStorageToConcurrentDictionary()
{
lock (_storage)
{
var loaded = _storage.TryLoad(new Dictionary<string, int>());
var loaded = _storage.TryLoad(new Dictionary<(string, bool), int>());
return new ConcurrentDictionary<string, int>(loaded);
return new ConcurrentDictionary<(string, bool), int>(loaded);
}
}
@ -95,11 +102,12 @@ namespace Flow.Launcher.Infrastructure.Image
Folder,
Data,
ImageFile,
FullImageFile,
Error,
Cache
}
private static ImageResult LoadInternal(string path, bool loadFullImage = false)
private static async ValueTask<ImageResult> LoadInternalAsync(string path, bool loadFullImage = false)
{
ImageResult imageResult;
@ -107,13 +115,21 @@ namespace Flow.Launcher.Infrastructure.Image
{
if (string.IsNullOrEmpty(path))
{
return new ImageResult(ImageCache[Constant.MissingImgIcon], ImageType.Error);
}
if (ImageCache.ContainsKey(path))
{
return new ImageResult(ImageCache[path], ImageType.Cache);
return new ImageResult(MissingImage, ImageType.Error);
}
if (ImageCache.ContainsKey(path, loadFullImage))
{
return new ImageResult(ImageCache[path, loadFullImage], ImageType.Cache);
}
if (Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uriResult)
&& (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps))
{
var image = await LoadRemoteImageAsync(loadFullImage, uriResult);
ImageCache[path, loadFullImage] = image;
return new ImageResult(image, ImageType.ImageFile);
}
if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{
var imageSource = new BitmapImage(new Uri(path));
@ -121,12 +137,7 @@ namespace Flow.Launcher.Infrastructure.Image
return new ImageResult(imageSource, ImageType.Data);
}
if (!Path.IsPathRooted(path))
{
path = Path.Combine(Constant.ProgramDirectory, "Images", Path.GetFileName(path));
}
imageResult = GetThumbnailResult(ref path, loadFullImage);
imageResult = await Task.Run(() => GetThumbnailResult(ref path, loadFullImage));
}
catch (System.Exception e)
{
@ -140,14 +151,35 @@ namespace Flow.Launcher.Infrastructure.Image
Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on first try", e);
Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on second try", e2);
ImageSource image = ImageCache[Constant.MissingImgIcon];
ImageCache[path] = image;
ImageSource image = ImageCache[Constant.MissingImgIcon, false];
ImageCache[path, false] = image;
imageResult = new ImageResult(image, ImageType.Error);
}
}
return imageResult;
}
private static async Task<BitmapImage> LoadRemoteImageAsync(bool loadFullImage, Uri uriResult)
{
// Download image from url
await using var resp = await GetStreamAsync(uriResult);
await using var buffer = new MemoryStream();
await resp.CopyToAsync(buffer);
buffer.Seek(0, SeekOrigin.Begin);
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
if (!loadFullImage)
{
image.DecodePixelHeight = SmallIconSize;
image.DecodePixelWidth = SmallIconSize;
}
image.StreamSource = buffer;
image.EndInit();
image.StreamSource = null;
image.Freeze();
return image;
}
private static ImageResult GetThumbnailResult(ref string path, bool loadFullImage = false)
{
@ -173,6 +205,7 @@ namespace Flow.Launcher.Infrastructure.Image
if (loadFullImage)
{
image = LoadFullImage(path);
type = ImageType.FullImageFile;
}
else
{
@ -187,12 +220,12 @@ namespace Flow.Launcher.Infrastructure.Image
else
{
type = ImageType.File;
image = GetThumbnail(path, ThumbnailOptions.None);
image = GetThumbnail(path, ThumbnailOptions.None, loadFullImage ? FullIconSize : SmallIconSize);
}
}
else
{
image = ImageCache[Constant.MissingImgIcon];
image = ImageCache[Constant.MissingImgIcon, false];
path = Constant.MissingImgIcon;
}
@ -204,23 +237,28 @@ namespace Flow.Launcher.Infrastructure.Image
return new ImageResult(image, type);
}
private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly)
private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly, int size = SmallIconSize)
{
return WindowsThumbnailProvider.GetThumbnail(
path,
Constant.ThumbnailSize,
Constant.ThumbnailSize,
size,
size,
option);
}
public static bool CacheContainImage(string path)
public static bool CacheContainImage(string path, bool loadFullImage = false)
{
return ImageCache.ContainsKey(path) && ImageCache[path] != null;
return ImageCache.ContainsKey(path, loadFullImage);
}
public static ImageSource Load(string path, bool loadFullImage = false)
public static bool TryGetValue(string path, bool loadFullImage, out ImageSource image)
{
var imageResult = LoadInternal(path, loadFullImage);
return ImageCache.TryGetValue(path, loadFullImage, out image);
}
public static async ValueTask<ImageSource> LoadAsync(string path, bool loadFullImage = false)
{
var imageResult = await LoadInternalAsync(path, loadFullImage);
var img = imageResult.ImageSource;
if (imageResult.ImageType != ImageType.Error && imageResult.ImageType != ImageType.Cache)
@ -231,19 +269,19 @@ namespace Flow.Launcher.Infrastructure.Image
if (GuidToKey.TryGetValue(hash, out string key))
{ // image already exists
img = ImageCache[key] ?? img;
img = ImageCache[key, false] ?? img;
}
else
{ // new guid
GuidToKey[hash] = path;
}
}
// update cache
ImageCache[path] = img;
ImageCache[path, loadFullImage] = img;
}
return img;
}
@ -252,8 +290,33 @@ namespace Flow.Launcher.Infrastructure.Image
BitmapImage image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.UriSource = new Uri(path);
image.UriSource = new Uri(path);
image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
image.EndInit();
if (image.PixelWidth > 320)
{
BitmapImage resizedWidth = new BitmapImage();
resizedWidth.BeginInit();
resizedWidth.CacheOption = BitmapCacheOption.OnLoad;
resizedWidth.UriSource = new Uri(path);
resizedWidth.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
resizedWidth.DecodePixelWidth = 320;
resizedWidth.EndInit();
if (resizedWidth.PixelHeight > 320)
{
BitmapImage resizedHeight = new BitmapImage();
resizedHeight.BeginInit();
resizedHeight.CacheOption = BitmapCacheOption.OnLoad;
resizedHeight.UriSource = new Uri(path);
resizedHeight.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
resizedHeight.DecodePixelHeight = 320;
resizedHeight.EndInit();
return resizedHeight;
}
return resizedWidth;
}
return image;
}
}

View file

@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using NLog;

View file

@ -15,7 +15,7 @@ namespace Flow.Launcher.Infrastructure
private List<int> originalIndexs = new List<int>();
private List<int> translatedIndexs = new List<int>();
private int translaedLength = 0;
private int translatedLength = 0;
public string key { get; private set; }
@ -32,13 +32,13 @@ namespace Flow.Launcher.Infrastructure
originalIndexs.Add(originalIndex);
translatedIndexs.Add(translatedIndex);
translatedIndexs.Add(translatedIndex + length);
translaedLength += length - 1;
translatedLength += length - 1;
}
public int MapToOriginalIndex(int translatedIndex)
{
if (translatedIndex > translatedIndexs.Last())
return translatedIndex - translaedLength - 1;
return translatedIndex - translatedLength - 1;
int lowerBound = 0;
int upperBound = originalIndexs.Count - 1;
@ -83,7 +83,7 @@ namespace Flow.Launcher.Infrastructure
translatedIndex < translatedIndexs[upperBound * 2])
{
int indexDef = 0;
for (int j = 0; j < upperBound; j++)
{
indexDef += translatedIndexs[j * 2 + 1] - translatedIndexs[j * 2];
@ -102,9 +102,24 @@ namespace Flow.Launcher.Infrastructure
}
}
/// <summary>
/// Translate a language to English letters using a given rule.
/// </summary>
public interface IAlphabet
{
/// <summary>
/// Translate a string to English letters, using a given rule.
/// </summary>
/// <param name="stringToTranslate">String to translate.</param>
/// <returns></returns>
public (string translation, TranslationMapping map) Translate(string stringToTranslate);
/// <summary>
/// Determine if a string can be translated to English letter with this Alphabet.
/// </summary>
/// <param name="stringToTranslate">String to translate.</param>
/// <returns></returns>
public bool CanBeTranslated(string stringToTranslate);
}
public class PinyinAlphabet : IAlphabet
@ -119,63 +134,70 @@ namespace Flow.Launcher.Infrastructure
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
}
public bool CanBeTranslated(string stringToTranslate)
{
return WordsHelper.HasChinese(stringToTranslate);
}
public (string translation, TranslationMapping map) Translate(string content)
{
if (_settings.ShouldUsePinyin)
{
if (!_pinyinCache.ContainsKey(content))
{
if (WordsHelper.HasChinese(content))
{
var resultList = WordsHelper.GetPinyinList(content);
StringBuilder resultBuilder = new StringBuilder();
TranslationMapping map = new TranslationMapping();
bool pre = false;
for (int i = 0; i < resultList.Length; i++)
{
if (content[i] >= 0x3400 && content[i] <= 0x9FD5)
{
map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1);
resultBuilder.Append(' ');
resultBuilder.Append(resultList[i]);
pre = true;
}
else
{
if (pre)
{
pre = false;
resultBuilder.Append(' ');
}
resultBuilder.Append(resultList[i]);
}
}
map.endConstruct();
var key = resultBuilder.ToString();
map.setKey(key);
return _pinyinCache[content] = (key, map);
}
else
{
return (content, null);
}
return BuildCacheFromContent(content);
}
else
{
return _pinyinCache[content];
}
}
return (content, null);
}
private (string translation, TranslationMapping map) BuildCacheFromContent(string content)
{
if (WordsHelper.HasChinese(content))
{
var resultList = WordsHelper.GetPinyinList(content);
StringBuilder resultBuilder = new StringBuilder();
TranslationMapping map = new TranslationMapping();
bool pre = false;
for (int i = 0; i < resultList.Length; i++)
{
if (content[i] >= 0x3400 && content[i] <= 0x9FD5)
{
map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1);
resultBuilder.Append(' ');
resultBuilder.Append(resultList[i]);
pre = true;
}
else
{
if (pre)
{
pre = false;
resultBuilder.Append(' ');
}
resultBuilder.Append(resultList[i]);
}
}
map.endConstruct();
var key = resultBuilder.ToString();
map.setKey(key);
return _pinyinCache[content] = (key, map);
}
else
{
return (content, null);
}
}
}
}
}

View file

@ -2,4 +2,5 @@
[assembly: InternalsVisibleTo("Flow.Launcher")]
[assembly: InternalsVisibleTo("Flow.Launcher.Core")]
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]
[assembly: System.Runtime.Versioning.SupportedOSPlatform("Windows10.0.19041.0")]

View file

@ -9,6 +9,7 @@ using Flow.Launcher.Infrastructure.UserSettings;
namespace Flow.Launcher.Infrastructure.Storage
{
#pragma warning disable SYSLIB0011 // BinaryFormatter is obsolete.
/// <summary>
/// Stroage object using binary data
/// Normally, it has better performance, but not readable
@ -113,4 +114,5 @@ namespace Flow.Launcher.Infrastructure.Storage
}
}
}
#pragma warning restore SYSLIB0011
}

View file

@ -1,4 +1,4 @@
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Plugin.SharedModels;
using System;
using System.Collections.Generic;
using System.Linq;
@ -60,8 +60,13 @@ namespace Flow.Launcher.Infrastructure
return new MatchResult(false, UserSettingSearchPrecision);
query = query.Trim();
TranslationMapping translationMapping;
(stringToCompare, translationMapping) = _alphabet?.Translate(stringToCompare) ?? (stringToCompare, null);
TranslationMapping translationMapping = null;
if (_alphabet is not null && !_alphabet.CanBeTranslated(query))
{
// We assume that if a query can be translated (containing characters of a language, like Chinese)
// it actually means user doesn't want it to be translated to English letters.
(stringToCompare, translationMapping) = _alphabet.Translate(stringToCompare);
}
var currentAcronymQueryIndex = 0;
var acronymMatchData = new List<int>();
@ -202,7 +207,11 @@ namespace Flow.Launcher.Infrastructure
if (allQuerySubstringsMatched)
{
var nearestSpaceIndex = CalculateClosestSpaceIndex(spaceIndices, firstMatchIndex);
var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1,
// firstMatchIndex - nearestSpaceIndex - 1 is to set the firstIndex as the index of the first matched char
// preceded by a space e.g. 'world' matching 'hello world' firstIndex would be 0 not 6
// giving more weight than 'we or donald' by allowing the distance calculation to treat the starting position at after the space.
var score = CalculateSearchScore(query, stringToCompare, firstMatchIndex - nearestSpaceIndex - 1, spaceIndices,
lastMatchIndex - firstMatchIndex, allSubstringsContainedInCompareString);
var resultList = indexList.Select(x => translationMapping?.MapToOriginalIndex(x) ?? x).Distinct().ToList();
@ -296,7 +305,7 @@ namespace Flow.Launcher.Infrastructure
return currentQuerySubstringIndex >= querySubstringsLength;
}
private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, int matchLen,
private static int CalculateSearchScore(string query, string stringToCompare, int firstIndex, List<int> spaceIndices, int matchLen,
bool allSubstringsContainedInCompareString)
{
// A match found near the beginning of a string is scored more than a match found near the end
@ -304,6 +313,14 @@ namespace Flow.Launcher.Infrastructure
// while the score is lower if they are more spread out
var score = 100 * (query.Length + 1) / ((1 + firstIndex) + (matchLen + 1));
// Give more weight to a match that is closer to the start of the string.
// if the first matched char is immediately before space and all strings are contained in the compare string e.g. 'world' matching 'hello world'
// and 'world hello', because both have 'world' immediately preceded by space, their firstIndex will be 0 when distance is calculated,
// to prevent them scoring the same, we adjust the score by deducting the number of spaces it has from the start of the string, so 'world hello'
// will score slightly higher than 'hello world' because 'hello world' has one additional space.
if (firstIndex == 0 && allSubstringsContainedInCompareString)
score -= spaceIndices.Count;
// A match with less characters assigning more weights
if (stringToCompare.Length - query.Length < 5)
{

View file

@ -0,0 +1,65 @@
using System;
using System.Text.Json.Serialization;
namespace Flow.Launcher.Infrastructure.UserSettings
{
public abstract class ShortcutBaseModel
{
public string Key { get; set; }
[JsonIgnore]
public Func<string> Expand { get; set; } = () => { return ""; };
public override bool Equals(object obj)
{
return obj is ShortcutBaseModel other &&
Key == other.Key;
}
public override int GetHashCode()
{
return Key.GetHashCode();
}
}
public class CustomShortcutModel : ShortcutBaseModel
{
public string Value { get; set; }
[JsonConstructorAttribute]
public CustomShortcutModel(string key, string value)
{
Key = key;
Value = value;
Expand = () => { return Value; };
}
public void Deconstruct(out string key, out string value)
{
key = Key;
value = Value;
}
public static implicit operator (string Key, string Value)(CustomShortcutModel shortcut)
{
return (shortcut.Key, shortcut.Value);
}
public static implicit operator CustomShortcutModel((string Key, string Value) shortcut)
{
return new CustomShortcutModel(shortcut.Key, shortcut.Value);
}
}
public class BuiltinShortcutModel : ShortcutBaseModel
{
public string Description { get; set; }
public BuiltinShortcutModel(string key, string description, Func<string> expand)
{
Key = key;
Description = description;
Expand = expand ?? (() => { return ""; });
}
}
}

View file

@ -1,9 +1,5 @@
using System;
using System.Collections.Generic;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Flow.Launcher.Infrastructure.UserSettings
{
@ -31,5 +27,10 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public static readonly string PluginsDirectory = Path.Combine(DataDirectory(), Constant.Plugins);
public static readonly string PluginSettingsDirectory = Path.Combine(DataDirectory(), "Settings", Constant.Plugins);
public const string PythonEnvironmentName = "Python";
public const string NodeEnvironmentName = "Node.js";
public const string PluginEnvironments = "Environments";
public static readonly string PluginEnvironmentsPath = Path.Combine(DataDirectory(), PluginEnvironments);
}
}

View file

@ -1,11 +1,34 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.Infrastructure.UserSettings
{
public class PluginsSettings : BaseModel
{
private string pythonExecutablePath = string.Empty;
public string PythonExecutablePath {
get { return pythonExecutablePath; }
set
{
pythonExecutablePath = value;
Constant.PythonPath = value;
}
}
private string nodeExecutablePath = string.Empty;
public string NodeExecutablePath
{
get { return nodeExecutablePath; }
set
{
nodeExecutablePath = value;
Constant.NodePath = value;
}
}
// TODO: Remove. This is backwards compatibility for 1.10.0 release.
public string PythonDirectory { get; set; }
public Dictionary<string, Plugin> Plugins { get; set; } = new Dictionary<string, Plugin>();
public void UpdatePluginSettings(List<PluginMetadata> metadatas)

View file

@ -1,11 +1,11 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Text.Json.Serialization;
using System.Windows;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher;
using Flow.Launcher.ViewModel;
namespace Flow.Launcher.Infrastructure.UserSettings
@ -18,6 +18,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public string ColorScheme { get; set; } = "System";
public bool ShowOpenResultHotkey { get; set; } = true;
public double WindowSize { get; set; } = 580;
public string PreviewHotkey { get; set; } = $"F1";
public string Language
{
@ -41,8 +42,18 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public bool UseGlyphIcons { get; set; } = true;
public bool UseAnimation { get; set; } = true;
public bool UseSound { get; set; } = true;
public bool UseClock { get; set; } = true;
public bool UseDate { get; set; } = false;
public string TimeFormat { get; set; } = "hh:mm tt";
public string DateFormat { get; set; } = "MM'/'dd ddd";
public bool FirstLaunch { get; set; } = true;
public double SettingWindowWidth { get; set; } = 1000;
public double SettingWindowHeight { get; set; } = 700;
public double SettingWindowTop { get; set; }
public double SettingWindowLeft { get; set; }
public System.Windows.WindowState SettingWindowState { get; set; } = WindowState.Normal;
public int CustomExplorerIndex { get; set; } = 0;
[JsonIgnore]
@ -120,8 +131,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
PrivateArg = "-private",
EnablePrivate = false,
Editable = false
}
,
},
new()
{
Name = "MS Edge",
@ -137,6 +147,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
/// when false Alphabet static service will always return empty results
/// </summary>
public bool ShouldUsePinyin { get; set; } = false;
public bool AlwaysPreview { get; set; } = false;
public bool AlwaysStartEn { get; set; } = false;
[JsonInclude, JsonConverter(typeof(JsonStringEnumConverter))]
public SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular;
@ -177,6 +189,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public ObservableCollection<CustomPluginHotkey> CustomPluginHotkeys { get; set; } = new ObservableCollection<CustomPluginHotkey>();
public ObservableCollection<CustomShortcutModel> CustomShortcuts { get; set; } = new ObservableCollection<CustomShortcutModel>();
[JsonIgnore]
public ObservableCollection<BuiltinShortcutModel> BuiltinShortcuts { get; set; } = new ObservableCollection<BuiltinShortcutModel>() {
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText)
};
public bool DontPromptUpdateMsg { get; set; }
public bool EnableUpdateLog { get; set; }
@ -194,7 +213,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
public bool LeaveCmdOpen { get; set; }
public bool HideWhenDeactive { get; set; } = true;
public bool RememberLastLaunchLocation { get; set; }
public SearchWindowPositions SearchWindowPosition { get; set; } = SearchWindowPositions.MouseScreenCenter;
public bool IgnoreHotkeysOnFullscreen { get; set; }
public HttpProxy Proxy { get; set; } = new HttpProxy();
@ -220,4 +239,12 @@ namespace Flow.Launcher.Infrastructure.UserSettings
Light,
Dark
}
}
public enum SearchWindowPositions
{
RememberLastLaunchLocation,
MouseScreenCenter,
MouseScreenCenterTop,
MouseScreenLeftTop,
MouseScreenRightTop
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,38 +1,65 @@
namespace Flow.Launcher.Plugin
using System;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Allowed plugin languages
/// </summary>
public static class AllowedLanguage
{
public static string Python
{
get { return "PYTHON"; }
}
/// <summary>
/// Python
/// </summary>
public const string Python = "Python";
public static string CSharp
{
get { return "CSHARP"; }
}
/// <summary>
/// C#
/// </summary>
public const string CSharp = "CSharp";
public static string FSharp
{
get { return "FSHARP"; }
}
/// <summary>
/// F#
/// </summary>
public const string FSharp = "FSharp";
public static string Executable
{
get { return "EXECUTABLE"; }
}
/// <summary>
/// Standard .exe
/// </summary>
public const string Executable = "Executable";
/// <summary>
/// TypeScript
/// </summary>
public const string TypeScript = "TypeScript";
/// <summary>
/// JavaScript
/// </summary>
public const string JavaScript = "JavaScript";
/// <summary>
/// Determines if this language is a .NET language
/// </summary>
/// <param name="language"></param>
/// <returns></returns>
public static bool IsDotNet(string language)
{
return language.ToUpper() == CSharp
|| language.ToUpper() == FSharp;
return language.Equals(CSharp, StringComparison.OrdinalIgnoreCase)
|| language.Equals(FSharp, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines if this language is supported
/// </summary>
/// <param name="language"></param>
/// <returns></returns>
public static bool IsAllowed(string language)
{
return IsDotNet(language)
|| language.ToUpper() == Python.ToUpper()
|| language.ToUpper() == Executable.ToUpper();
|| language.Equals(Python, StringComparison.OrdinalIgnoreCase)
|| language.Equals(Executable, StringComparison.OrdinalIgnoreCase)
|| language.Equals(TypeScript, StringComparison.OrdinalIgnoreCase)
|| language.Equals(JavaScript, StringComparison.OrdinalIgnoreCase);
}
}
}
}

View file

@ -4,14 +4,24 @@ using JetBrains.Annotations;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Base model for plugin classes
/// </summary>
public class BaseModel : INotifyPropertyChanged
{
/// <summary>
/// Property changed event handler
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Invoked when a property changes
/// </summary>
/// <param name="propertyName"></param>
[NotifyPropertyChangedInvocator]
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}

View file

@ -3,9 +3,24 @@ using System.Windows.Input;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Delegate for key down event
/// </summary>
/// <param name="e"></param>
public delegate void FlowLauncherKeyDownEventHandler(FlowLauncherKeyDownEventArgs e);
/// <summary>
/// Delegate for query event
/// </summary>
/// <param name="e"></param>
public delegate void AfterFlowLauncherQueryEventHandler(FlowLauncherQueryEventArgs e);
/// <summary>
/// Delegate for drop events [unused?]
/// </summary>
/// <param name="result"></param>
/// <param name="dropObject"></param>
/// <param name="e"></param>
public delegate void ResultItemDropEventHandler(Result result, IDataObject dropObject, DragEventArgs e);
/// <summary>
@ -17,14 +32,30 @@ namespace Flow.Launcher.Plugin
/// <returns>return true to continue handling, return false to intercept system handling</returns>
public delegate bool FlowLauncherGlobalKeyboardEventHandler(int keyevent, int vkcode, SpecialKeyState state);
/// <summary>
/// Arguments container for the Key Down event
/// </summary>
public class FlowLauncherKeyDownEventArgs
{
/// <summary>
/// The actual query
/// </summary>
public string Query { get; set; }
/// <summary>
/// Relevant key events for this event
/// </summary>
public KeyEventArgs keyEventArgs { get; set; }
}
/// <summary>
/// Arguments container for the Query event
/// </summary>
public class FlowLauncherQueryEventArgs
{
/// <summary>
/// The actual query
/// </summary>
public Query Query { get; set; }
}
}

View file

@ -14,10 +14,10 @@
</PropertyGroup>
<PropertyGroup>
<Version>3.0.0</Version>
<PackageVersion>3.0.0</PackageVersion>
<AssemblyVersion>3.0.0</AssemblyVersion>
<FileVersion>3.0.0</FileVersion>
<Version>3.0.1</Version>
<PackageVersion>3.0.1</PackageVersion>
<AssemblyVersion>3.0.1</AssemblyVersion>
<FileVersion>3.0.1</FileVersion>
<PackageId>Flow.Launcher.Plugin</PackageId>
<Authors>Flow-Launcher</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -66,7 +66,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="JetBrains.Annotations" Version="2021.2.0" />
<PackageReference Include="JetBrains.Annotations" Version="2022.3.1" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
</ItemGroup>

View file

@ -15,6 +15,10 @@ namespace Flow.Launcher.Plugin
/// </summary>
public interface IAsyncReloadable : IFeatures
{
/// <summary>
/// Reload plugin data
/// </summary>
/// <returns></returns>
Task ReloadDataAsync();
}
}
}

View file

@ -1,7 +1,8 @@
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Plugin.SharedModels;
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
@ -41,7 +42,7 @@ namespace Flow.Launcher.Plugin
/// <summary>
/// Copy Text to clipboard
/// </summary>
/// <param name="Text">Text to save on clipboard</param>
/// <param name="text">Text to save on clipboard</param>
public void CopyToClipboard(string text);
/// <summary>
@ -163,6 +164,7 @@ namespace Flow.Launcher.Plugin
/// Download the specific url to a cretain file path
/// </summary>
/// <param name="url">URL to download file</param>
/// <param name="filePath">path to save downloaded file</param>
/// <param name="token">place to store file</param>
/// <returns>Task showing the progress</returns>
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default);
@ -178,9 +180,16 @@ namespace Flow.Launcher.Plugin
/// Remove ActionKeyword for specific plugin
/// </summary>
/// <param name="pluginId">ID for plugin that needs to remove action keyword</param>
/// <param name="newActionKeyword">The actionkeyword that is supposed to be removed</param>
/// <param name="oldActionKeyword">The actionkeyword that is supposed to be removed</param>
void RemoveActionKeyword(string pluginId, string oldActionKeyword);
/// <summary>
/// Check whether specific ActionKeyword is assigned to any of the plugin
/// </summary>
/// <param name="actionKeyword">The actionkeyword for checking</param>
/// <returns>True if the actionkeyword is already assigned, False otherwise</returns>
bool ActionKeywordAssigned(string actionKeyword);
/// <summary>
/// Log debug message
/// Message will only be logged in Debug mode

View file

@ -17,6 +17,9 @@
/// </summary>
public interface IReloadable : IFeatures
{
/// <summary>
/// Synchronously reload plugin data
/// </summary>
void ReloadData();
}
}
}

View file

@ -2,4 +2,4 @@
[assembly: InternalsVisibleTo("Flow.Launcher")]
[assembly: InternalsVisibleTo("Flow.Launcher.Core")]
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]
[assembly: InternalsVisibleTo("Flow.Launcher.Test")]

View file

@ -16,7 +16,9 @@ namespace Flow.Launcher.Plugin
{
Search = search;
RawQuery = rawQuery;
#pragma warning disable CS0618
Terms = terms;
#pragma warning restore CS0618
SearchTerms = searchTerms;
ActionKeyword = actionKeyword;
}
@ -98,4 +100,4 @@ namespace Flow.Launcher.Plugin
public override string ToString() => RawQuery;
}
}
}

View file

@ -1,12 +1,15 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media;
namespace Flow.Launcher.Plugin
{
/// <summary>
/// Describes the result of a plugin
/// </summary>
public class Result
{
@ -35,7 +38,11 @@ namespace Flow.Launcher.Plugin
/// user's clipboard when Ctrl + C is pressed on a result. If the text is a file/directory path
/// flow will copy the actual file/folder instead of just the path text.
/// </summary>
public string CopyText { get; set; } = string.Empty;
public string CopyText
{
get => string.IsNullOrEmpty(_copyText) ? SubTitle : _copyText;
set => _copyText = value;
}
/// <summary>
/// This holds the text which can be provided by plugin to help Flow autocomplete text
@ -54,9 +61,14 @@ namespace Flow.Launcher.Plugin
get { return _icoPath; }
set
{
if (!string.IsNullOrEmpty(PluginDirectory) && !Path.IsPathRooted(value))
// As a standard this property will handle prepping and converting to absolute local path for icon image processing
if (!string.IsNullOrEmpty(value)
&& !string.IsNullOrEmpty(PluginDirectory)
&& !Path.IsPathRooted(value)
&& !value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
&& !value.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
_icoPath = Path.Combine(value, IcoPath);
_icoPath = Path.Combine(PluginDirectory, value);
}
else
{
@ -64,13 +76,22 @@ namespace Flow.Launcher.Plugin
}
}
}
/// <summary>
/// Determines if Icon has a border radius
/// </summary>
public bool RoundedIcon { get; set; } = false;
/// <summary>
/// Delegate function, see <see cref="Icon"/>
/// </summary>
/// <returns></returns>
public delegate ImageSource IconDelegate();
/// <summary>
/// Delegate to Get Image Source
/// </summary>
public IconDelegate Icon;
private string _copyText = string.Empty;
/// <summary>
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)
@ -105,6 +126,9 @@ namespace Flow.Launcher.Plugin
/// </summary>
public IList<int> TitleHighlightData { get; set; }
/// <summary>
/// Deprecated as of Flow Launcher v1.9.1. Subtitle highlighting is no longer offered
/// </summary>
[Obsolete("Deprecated as of Flow Launcher v1.9.1. Subtitle highlighting is no longer offered")]
public IList<int> SubTitleHighlightData { get; set; }
@ -122,10 +146,11 @@ namespace Flow.Launcher.Plugin
set
{
_pluginDirectory = value;
if (!string.IsNullOrEmpty(IcoPath) && !Path.IsPathRooted(IcoPath))
{
IcoPath = Path.Combine(value, IcoPath);
}
// When the Result object is returned from the query call, PluginDirectory is not provided until
// UpdatePluginMetadata call is made at PluginManager.cs L196. Once the PluginDirectory becomes available
// we need to update (only if not Uri path) the IcoPath with the full absolute path so the image can be loaded.
IcoPath = _icoPath;
}
}
@ -178,10 +203,58 @@ namespace Flow.Launcher.Plugin
/// Show message as ToolTip on result SubTitle hover over
/// </summary>
public string SubTitleToolTip { get; set; }
/// <summary>
/// Customized Preview Panel
/// </summary>
public Lazy<UserControl> PreviewPanel { get; set; }
/// <summary>
/// Run this result, asynchronously
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public ValueTask<bool> ExecuteAsync(ActionContext context)
{
return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false);
}
/// <summary>
/// Progress bar display. Providing an int value between 0-100 will trigger the progress bar to be displayed on the result
/// </summary>
public int? ProgressBar { get; set; }
/// <summary>
/// Optionally set the color of the progress bar
/// </summary>
/// <default>#26a0da (blue)</default>
public string ProgressBarColor { get; set; } = "#26a0da";
public PreviewInfo Preview { get; set; } = PreviewInfo.Default;
/// <summary>
/// Info of the preview image.
/// </summary>
public record PreviewInfo
{
/// <summary>
/// Full image used for preview panel
/// </summary>
public string PreviewImagePath { get; set; }
/// <summary>
/// Determines if the preview image should occupy the full width of the preview panel.
/// </summary>
public bool IsMedia { get; set; }
public string Description { get; set; }
public IconDelegate PreviewDelegate { get; set; }
public static PreviewInfo Default { get; } = new()
{
PreviewImagePath = null,
Description = null,
IsMedia = false,
PreviewDelegate = null,
};
}
}
}
}

View file

@ -5,6 +5,9 @@ using System.Windows;
namespace Flow.Launcher.Plugin.SharedCommands
{
/// <summary>
/// Commands that are useful to run on files... and folders!
/// </summary>
public static class FilesFolders
{
private const string FileExplorerProgramName = "explorer";
@ -53,10 +56,10 @@ namespace Flow.Launcher.Plugin.SharedCommands
CopyAll(subdir.FullName, temppath);
}
}
catch (Exception e)
catch (Exception)
{
#if DEBUG
throw e;
throw;
#else
MessageBox.Show(string.Format("Copying path {0} has failed, it will now be deleted for consistency", targetPath));
RemoveFolderIfExists(targetPath);
@ -65,6 +68,13 @@ namespace Flow.Launcher.Plugin.SharedCommands
}
/// <summary>
/// Check if the files and directories are identical between <paramref name="fromPath"/>
/// and <paramref name="toPath"/>
/// </summary>
/// <param name="fromPath"></param>
/// <param name="toPath"></param>
/// <returns></returns>
public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPath)
{
try
@ -80,10 +90,10 @@ namespace Flow.Launcher.Plugin.SharedCommands
return true;
}
catch (Exception e)
catch (Exception)
{
#if DEBUG
throw e;
throw;
#else
MessageBox.Show(string.Format("Unable to verify folders and files between {0} and {1}", fromPath, toPath));
return false;
@ -92,6 +102,10 @@ namespace Flow.Launcher.Plugin.SharedCommands
}
/// <summary>
/// Deletes a folder if it exists
/// </summary>
/// <param name="path"></param>
public static void RemoveFolderIfExists(this string path)
{
try
@ -99,26 +113,40 @@ namespace Flow.Launcher.Plugin.SharedCommands
if (Directory.Exists(path))
Directory.Delete(path, true);
}
catch (Exception e)
catch (Exception)
{
#if DEBUG
throw e;
throw;
#else
MessageBox.Show(string.Format("Not able to delete folder {0}, please go to the location and manually delete it", path));
#endif
}
}
/// <summary>
/// Checks if a directory exists
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static bool LocationExists(this string path)
{
return Directory.Exists(path);
}
/// <summary>
/// Checks if a file exists
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
public static bool FileExists(this string filePath)
{
return File.Exists(filePath);
}
/// <summary>
/// Open a directory window (using the OS's default handler, usually explorer)
/// </summary>
/// <param name="fileOrFolderPath"></param>
public static void OpenPath(string fileOrFolderPath)
{
var psi = new ProcessStartInfo { FileName = FileExplorerProgramName, UseShellExecute = true, Arguments = '"' + fileOrFolderPath + '"' };
@ -127,16 +155,20 @@ namespace Flow.Launcher.Plugin.SharedCommands
if (LocationExists(fileOrFolderPath) || FileExists(fileOrFolderPath))
Process.Start(psi);
}
catch (Exception e)
catch (Exception)
{
#if DEBUG
throw e;
throw;
#else
MessageBox.Show(string.Format("Unable to open the path {0}, please check if it exists", fileOrFolderPath));
#endif
}
}
/// <summary>
/// Open the folder that contains <paramref name="path"/>
/// </summary>
/// <param name="path"></param>
public static void OpenContainingFolder(string path)
{
Process.Start(FileExplorerProgramEXE, $" /select,\"{path}\"");

View file

@ -1,6 +1,8 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
@ -87,7 +89,8 @@ namespace Flow.Launcher.Plugin.SharedCommands
/// <summary>
/// Runs a windows command using the provided ProcessStartInfo using a custom execute command function
/// </summary>
/// <param name="Func startProcess">allows you to pass in a custom command execution function</param>
/// <param name="startProcess">allows you to pass in a custom command execution function</param>
/// <param name="info">allows you to pass in the info that will be passed to startProcess</param>
/// <exception cref="FileNotFoundException">Thrown when unable to find the file specified in the command </exception>
/// <exception cref="Win32Exception">Thrown when error occurs during the execution of the command </exception>
public static void Execute(Func<ProcessStartInfo, Process> startProcess, ProcessStartInfo info)

View file

@ -49,8 +49,8 @@
<ItemGroup>
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0">
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@ -129,14 +129,20 @@ namespace Flow.Launcher.Test
}
}
/// <summary>
/// These are standard match scenarios
/// The intention of this test is provide a bench mark for how much the score has increased from a change.
/// Usually the increase in scoring should not be drastic, increase of less than 10 is acceptable.
/// </summary>
[TestCase(Chrome, Chrome, 157)]
[TestCase(Chrome, LastIsChrome, 147)]
[TestCase(Chrome, LastIsChrome, 145)]
[TestCase("chro", HelpCureHopeRaiseOnMindEntityChrome, 50)]
[TestCase("chr", HelpCureHopeRaiseOnMindEntityChrome, 30)]
[TestCase(Chrome, UninstallOrChangeProgramsOnYourComputer, 21)]
[TestCase(Chrome, CandyCrushSagaFromKing, 0)]
[TestCase("sql", MicrosoftSqlServerManagementStudio, 110)]
[TestCase("sql manag", MicrosoftSqlServerManagementStudio, 121)] //double spacing intended
[TestCase("sql", MicrosoftSqlServerManagementStudio, 109)]
[TestCase("sql manag", MicrosoftSqlServerManagementStudio, 120)] //double spacing intended
public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring(
string queryString, string compareString, int expectedScore)
{
@ -275,7 +281,40 @@ namespace Flow.Launcher.Test
$"Query: \"{queryString}\"{Environment.NewLine} " +
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
$"Should be greater than{Environment.NewLine}" +
$"CompareString2: \"{compareString2}\", Score: {compareString1Result.Score}{Environment.NewLine}");
$"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
}
[TestCase("red", "red colour", "metro red")]
[TestCase("red", "this red colour", "this colour red")]
[TestCase("red", "this red colour", "this colour is very red")]
[TestCase("red", "this red colour", "this colour is surprisingly super awesome red and cool")]
[TestCase("red", "this colour is surprisingly super red very and cool", "this colour is surprisingly super very red and cool")]
public void WhenGivenTwoStrings_Scoring_ShouldGiveMoreWeightToTheStringCloserToIndexZero(
string queryString, string compareString1, string compareString2)
{
// When
var matcher = new StringMatcher { UserSettingSearchPrecision = SearchPrecisionScore.Regular };
// Given
var compareString1Result = matcher.FuzzyMatch(queryString, compareString1);
var compareString2Result = matcher.FuzzyMatch(queryString, compareString2);
Debug.WriteLine("");
Debug.WriteLine("###############################################");
Debug.WriteLine($"QueryString: \"{queryString}\"{Environment.NewLine}");
Debug.WriteLine(
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}");
Debug.WriteLine(
$"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
Debug.WriteLine("###############################################");
Debug.WriteLine("");
// Should
Assert.True(compareString1Result.Score > compareString2Result.Score,
$"Query: \"{queryString}\"{Environment.NewLine} " +
$"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" +
$"Should be greater than{Environment.NewLine}" +
$"CompareString2: \"{compareString2}\", Score: {compareString2Result.Score}{Environment.NewLine}");
}
[TestCase("vim", "Vim", "ignoreDescription", "ignore.exe", "Vim Diff", "ignoreDescription", "ignore.exe")]

View file

@ -1,4 +1,4 @@
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.Explorer;
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo;
@ -7,6 +7,7 @@ using Flow.Launcher.Plugin.SharedCommands;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
@ -19,10 +20,12 @@ namespace Flow.Launcher.Test.Plugins
[TestFixture]
public class ExplorerTest
{
#pragma warning disable CS1998 // async method with no await (more readable to leave it async to match the tested signature)
private async Task<List<Result>> MethodWindowsIndexSearchReturnsZeroResultsAsync(Query dummyQuery, string dummyString, CancellationToken dummyToken)
{
return new List<Result>();
}
#pragma warning restore CS1998
private List<Result> MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString, CancellationToken token)
{
@ -30,12 +33,11 @@ namespace Flow.Launcher.Test.Plugins
{
new Result
{
Title="Result 1"
Title = "Result 1"
},
new Result
{
Title="Result 2"
Title = "Result 2"
}
};
}
@ -44,15 +46,13 @@ namespace Flow.Launcher.Test.Plugins
private bool PreviousLocationNotExistReturnsFalse(string dummyString) => false;
[SupportedOSPlatform("windows7.0")]
[TestCase("C:\\SomeFolder\\", "directory='file:C:\\SomeFolder\\'")]
public void GivenWindowsIndexSearch_WhenProvidedFolderPath_ThenQueryWhereRestrictionsShouldUseDirectoryString(string path, string expectedString)
{
// Given
var queryConstructor = new QueryConstructor(new Settings());
// When
var folderPath = path;
var result = queryConstructor.QueryWhereRestrictionsForTopLevelDirectorySearch(folderPath);
var result = QueryConstructor.TopLevelDirectoryConstraint(folderPath);
// Then
Assert.IsTrue(result == expectedString,
@ -60,6 +60,7 @@ namespace Flow.Launcher.Test.Plugins
$"Actual: {result}{Environment.NewLine}");
}
[SupportedOSPlatform("windows7.0")]
[TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY System.FileName")]
[TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY System.FileName")]
public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString)
@ -68,130 +69,68 @@ namespace Flow.Launcher.Test.Plugins
var queryConstructor = new QueryConstructor(new Settings());
//When
var queryString = queryConstructor.QueryForTopLevelDirectorySearch(folderPath);
var queryString = queryConstructor.Directory(folderPath);
// Then
Assert.IsTrue(queryString == expectedString,
Assert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "),
$"Expected string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {queryString}{Environment.NewLine}");
}
[TestCase("C:\\SomeFolder\\flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " +
"FROM SystemIndex WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033))" +
" AND directory='file:C:\\SomeFolder' ORDER BY System.FileName")]
[SupportedOSPlatform("windows7.0")]
[TestCase("C:\\SomeFolder", "flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType" +
" FROM SystemIndex WHERE directory='file:C:\\SomeFolder'" +
" AND (System.FileName LIKE 'flow.launcher.sln%' OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"'))" +
" ORDER BY System.FileName")]
public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
string folderPath, string userSearchString, string expectedString)
{
// Given
var queryConstructor = new QueryConstructor(new Settings());
//When
var queryString = queryConstructor.QueryForTopLevelDirectorySearch(userSearchString);
var queryString = queryConstructor.Directory(folderPath, userSearchString);
// Then
Assert.IsTrue(queryString == expectedString,
$"Expected string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {queryString}{Environment.NewLine}");
}
[TestCase("C:\\SomeFolder\\SomeApp", "(System.FileName LIKE 'SomeApp%' " +
"OR CONTAINS(System.FileName,'\"SomeApp*\"',1033))" +
" AND directory='file:C:\\SomeFolder'")]
public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryWhereRestrictionsShouldUseDirectoryString(
string userSearchString, string expectedString)
{
// Given
var queryConstructor = new QueryConstructor(new Settings());
//When
var queryString = queryConstructor.QueryWhereRestrictionsForTopLevelDirectorySearch(userSearchString);
// Then
Assert.IsTrue(queryString == expectedString,
$"Expected string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {queryString}{Environment.NewLine}");
Assert.AreEqual(expectedString, queryString);
}
[SupportedOSPlatform("windows7.0")]
[TestCase("scope='file:'")]
public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereRestrictionsShouldUseScopeString(string expectedString)
{
//When
var resultString = QueryConstructor.QueryWhereRestrictionsForAllFilesAndFoldersSearch;
const string resultString = QueryConstructor.RestrictionsForAllFilesAndFoldersSearch;
// Then
Assert.IsTrue(resultString == expectedString,
$"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {resultString}{Environment.NewLine}");
Assert.AreEqual(expectedString, resultString);
}
[SupportedOSPlatform("windows7.0")]
[TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " +
"FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")]
"FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
"OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")]
[TestCase("", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY System.FileName")]
public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
{
// Given
var queryConstructor = new QueryConstructor(new Settings());
var baseQuery = queryConstructor.CreateBaseQuery();
var baseQuery = queryConstructor.CreateBaseQuery();
// system running this test could have different locale than the hard-coded 1033 LCID en-US.
var queryKeywordLocale = baseQuery.QueryKeywordLocale;
expectedString = expectedString.Replace("1033", queryKeywordLocale.ToString());
//When
var resultString = queryConstructor.QueryForAllFilesAndFolders(userSearchString);
var resultString = queryConstructor.FilesAndFolders(userSearchString);
// Then
Assert.IsTrue(resultString == expectedString,
$"Expected query string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {resultString}{Environment.NewLine}");
Assert.AreEqual(expectedString, resultString);
}
[TestCase]
public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldContinueDirectoryInfoClassSearch()
{
// Given
var searchManager = new SearchManager(new Settings(), new PluginInitContext());
// When
var results = await searchManager.TopLevelDirectorySearchBehaviourAsync(
MethodWindowsIndexSearchReturnsZeroResultsAsync,
MethodDirectoryInfoClassSearchReturnsTwoResults,
false,
new Query(),
"string not used",
default);
// Then
Assert.IsTrue(results.Count == 2,
$"Expected to have 2 results from DirectoryInfoClassSearch {Environment.NewLine} " +
$"Actual number of results is {results.Count} {Environment.NewLine}");
}
[TestCase]
public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldNotContinueDirectoryInfoClassSearch()
{
// Given
var searchManager = new SearchManager(new Settings(), new PluginInitContext());
// When
var results = await searchManager.TopLevelDirectorySearchBehaviourAsync(
MethodWindowsIndexSearchReturnsZeroResultsAsync,
MethodDirectoryInfoClassSearchReturnsTwoResults,
true,
new Query(),
"string not used",
default);
// Then
Assert.IsTrue(results.Count == 0,
$"Expected to have 0 results because location is indexed {Environment.NewLine} " +
$"Actual number of results is {results.Count} {Environment.NewLine}");
}
[SupportedOSPlatform("windows7.0")]
[TestCase(@"some words", @"FREETEXT('some words')")]
public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSearch_ThenShouldReturnFreeTextString(
string querySearchString, string expectedString)
@ -200,7 +139,7 @@ namespace Flow.Launcher.Test.Plugins
var queryConstructor = new QueryConstructor(new Settings());
//When
var resultString = queryConstructor.QueryWhereRestrictionsForFileContentSearch(querySearchString);
var resultString = QueryConstructor.RestrictionsForFileContentSearch(querySearchString);
// Then
Assert.IsTrue(resultString == expectedString,
@ -208,8 +147,9 @@ namespace Flow.Launcher.Test.Plugins
$"Actual string was: {resultString}{Environment.NewLine}");
}
[SupportedOSPlatform("windows7.0")]
[TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " +
"FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")]
"FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")]
public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
{
@ -217,7 +157,7 @@ namespace Flow.Launcher.Test.Plugins
var queryConstructor = new QueryConstructor(new Settings());
//When
var resultString = queryConstructor.QueryForFileContentSearch(userSearchString);
var resultString = queryConstructor.FileContent(userSearchString);
// Then
Assert.IsTrue(resultString == expectedString,
@ -228,12 +168,15 @@ namespace Flow.Launcher.Test.Plugins
public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue()
{
// Given
var query = new Query { ActionKeyword = "doc:", Search = "search term" };
var query = new Query
{
ActionKeyword = "doc:", Search = "search term"
};
var searchManager = new SearchManager(new Settings(), new PluginInitContext());
// When
var result = searchManager.IsFileContentSearch(query.ActionKeyword);
var result = SearchManager.IsFileContentSearch(query.ActionKeyword);
// Then
Assert.IsTrue(result,
@ -301,24 +244,19 @@ namespace Flow.Launcher.Test.Plugins
$"Actual path string is {returnedPath} {Environment.NewLine}");
}
[TestCase("c:\\SomeFolder\\>", "scope='file:c:\\SomeFolder'")]
[TestCase("c:\\SomeFolder\\>SomeName", "(System.FileName LIKE 'SomeName%' "
+ "OR CONTAINS(System.FileName,'\"SomeName*\"',1033)) AND "
+ "scope='file:c:\\SomeFolder'")]
public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString)
[SupportedOSPlatform("windows7.0")]
[TestCase("c:\\SomeFolder", "scope='file:c:\\SomeFolder'")]
[TestCase("c:\\OtherFolder", "scope='file:c:\\OtherFolder'")]
public void GivenFilePath_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString)
{
// Given
var queryConstructor = new QueryConstructor(new Settings());
//When
var resultString = queryConstructor.QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path);
var resultString = QueryConstructor.RecursiveDirectoryConstraint(path);
// Then
Assert.IsTrue(resultString == expectedString,
$"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {resultString}{Environment.NewLine}");
Assert.AreEqual(expectedString, resultString);
}
[SupportedOSPlatform("windows7.0")]
[TestCase("c:\\somefolder\\>somefile", "*somefile*")]
[TestCase("c:\\somefolder\\somefile", "somefile*")]
[TestCase("c:\\somefolder\\", "*")]
@ -329,9 +267,131 @@ namespace Flow.Launcher.Test.Plugins
var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path);
// Then
Assert.IsTrue(resultString == expectedString,
$"Expected criteria string: {expectedString}{Environment.NewLine} " +
$"Actual criteria string was: {resultString}{Environment.NewLine}");
Assert.AreEqual(expectedString, resultString);
}
[TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "irrelevant", false, true, "c:\\somefolder\\someotherfolder\\")]
[TestCase("c:\\somefolder\\someotherfolder\\", ResultType.Folder, "irrelevant", true, true, "c:\\somefolder\\someotherfolder\\")]
[TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "irrelevant", true, false, "p c:\\somefolder\\someotherfolder\\")]
[TestCase("c:\\somefolder\\someotherfolder\\", ResultType.Folder, "irrelevant", false, false, "c:\\somefolder\\someotherfolder\\")]
[TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "p", true, false, "p c:\\somefolder\\someotherfolder\\")]
[TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "", true, true, "c:\\somefolder\\someotherfolder\\")]
public void GivenFolderResult_WhenGetPath_ThenPathShouldBeExpectedString(
string path,
ResultType type,
string actionKeyword,
bool pathSearchKeywordEnabled,
bool searchActionKeywordEnabled,
string expectedResult)
{
// Given
var settings = new Settings()
{
PathSearchKeywordEnabled = pathSearchKeywordEnabled,
PathSearchActionKeyword = "p",
SearchActionKeywordEnabled = searchActionKeywordEnabled,
SearchActionKeyword = Query.GlobalPluginWildcardSign
};
ResultManager.Init(new PluginInitContext(), settings);
// When
var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword);
// Then
Assert.AreEqual(result, expectedResult);
}
[TestCase("c:\\somefolder\\somefile", ResultType.File, "irrelevant", false, true, "e c:\\somefolder\\somefile")]
[TestCase("c:\\somefolder\\somefile", ResultType.File, "p", true, false, "p c:\\somefolder\\somefile")]
[TestCase("c:\\somefolder\\somefile", ResultType.File, "e", true, true, "e c:\\somefolder\\somefile")]
[TestCase("c:\\somefolder\\somefile", ResultType.File, "irrelevant", false, false, "e c:\\somefolder\\somefile")]
public void GivenFileResult_WhenGetPath_ThenPathShouldBeExpectedString(
string path,
ResultType type,
string actionKeyword,
bool pathSearchKeywordEnabled,
bool searchActionKeywordEnabled,
string expectedResult)
{
// Given
var settings = new Settings()
{
PathSearchKeywordEnabled = pathSearchKeywordEnabled,
PathSearchActionKeyword = "p",
SearchActionKeywordEnabled = searchActionKeywordEnabled,
SearchActionKeyword = "e"
};
ResultManager.Init(new PluginInitContext(), settings);
// When
var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword);
// Then
Assert.AreEqual(result, expectedResult);
}
[TestCase("somefolder", "c:\\somefolder\\", ResultType.Folder, "q", false, false, "q somefolder")]
[TestCase("somefolder", "c:\\somefolder\\", ResultType.Folder, "i", true, false, "p c:\\somefolder\\")]
[TestCase("somefolder", "c:\\somefolder\\", ResultType.Folder, "irrelevant", true, true, "c:\\somefolder\\")]
public void GivenQueryWithFolderTypeResult_WhenGetAutoComplete_ThenResultShouldBeExpectedString(
string title,
string path,
ResultType resultType,
string actionKeyword,
bool pathSearchKeywordEnabled,
bool searchActionKeywordEnabled,
string expectedResult)
{
// Given
var query = new Query() { ActionKeyword = actionKeyword };
var settings = new Settings()
{
PathSearchKeywordEnabled = pathSearchKeywordEnabled,
PathSearchActionKeyword = "p",
SearchActionKeywordEnabled = searchActionKeywordEnabled,
SearchActionKeyword = Query.GlobalPluginWildcardSign,
QuickAccessActionKeyword = "q",
IndexSearchActionKeyword = "i"
};
ResultManager.Init(new PluginInitContext(), settings);
// When
var result = ResultManager.GetAutoCompleteText(title, query, path, resultType);
// Then
Assert.AreEqual(result, expectedResult);
}
[TestCase("somefile", "c:\\somefolder\\somefile", ResultType.File, "q", false, false, "q somefile")]
[TestCase("somefile", "c:\\somefolder\\somefile", ResultType.File, "i", true, false, "p c:\\somefolder\\somefile")]
[TestCase("somefile", "c:\\somefolder\\somefile", ResultType.File, "irrelevant", true, true, "c:\\somefolder\\somefile")]
public void GivenQueryWithFileTypeResult_WhenGetAutoComplete_ThenResultShouldBeExpectedString(
string title,
string path,
ResultType resultType,
string actionKeyword,
bool pathSearchKeywordEnabled,
bool searchActionKeywordEnabled,
string expectedResult)
{
// Given
var query = new Query() { ActionKeyword = actionKeyword };
var settings = new Settings()
{
QuickAccessActionKeyword = "q",
IndexSearchActionKeyword = "i",
PathSearchActionKeyword = "p",
PathSearchKeywordEnabled = pathSearchKeywordEnabled,
SearchActionKeywordEnabled = searchActionKeywordEnabled,
SearchActionKeyword = Query.GlobalPluginWildcardSign
};
ResultManager.Init(new PluginInitContext(), settings);
// When
var result = ResultManager.GetAutoCompleteText(title, query, path, resultType);
// Then
Assert.AreEqual(result, expectedResult);
}
}
}

View file

@ -1,4 +1,4 @@
using NUnit;
using NUnit;
using NUnit.Framework;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Plugin;
@ -16,8 +16,6 @@ namespace Flow.Launcher.Test.Plugins
// ReSharper disable once InconsistentNaming
internal class JsonRPCPluginTest : JsonRPCPlugin
{
public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable;
protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default)
{
throw new System.NotImplementedException();

View file

@ -1,30 +0,0 @@
using Flow.Launcher.Plugin.Program.Programs;
using NUnit.Framework;
using System;
using Windows.ApplicationModel;
namespace Flow.Launcher.Test.Plugins
{
[TestFixture]
public class ProgramTest
{
[TestCase("Microsoft.WindowsCamera", "ms-resource:LensSDK/Resources/AppTitle", "ms-resource://Microsoft.WindowsCamera/LensSDK/Resources/AppTitle")]
[TestCase("microsoft.windowscommunicationsapps", "ms-resource://microsoft.windowscommunicationsapps/hxoutlookintl/AppManifest_MailDesktop_DisplayName",
"ms-resource://microsoft.windowscommunicationsapps/hxoutlookintl/AppManifest_MailDesktop_DisplayName")]
[TestCase("windows.immersivecontrolpanel", "ms-resource:DisplayName", "ms-resource://windows.immersivecontrolpanel/Resources/DisplayName")]
[TestCase("Microsoft.MSPaint", "ms-resource:AppName", "ms-resource://Microsoft.MSPaint/Resources/AppName")]
public void WhenGivenPriReferenceValueShouldReturnCorrectFormat(string packageName, string rawPriReferenceValue, string expectedFormat)
{
// Arrange
var app = new UWP.Application();
// Act
var result = app.FormattedPriReferenceValue(packageName, rawPriReferenceValue);
// Assert
Assert.IsTrue(result == expectedFormat,
$"Expected Pri reference format: {expectedFormat}{Environment.NewLine} " +
$"Actual: {result}{Environment.NewLine}");
}
}
}

View file

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29806.167
# Visual Studio Version 17
VisualStudioVersion = 17.3.32901.215
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Test", "Flow.Launcher.Test\Flow.Launcher.Test.csproj", "{FF742965-9A80-41A5-B042-D6C7D3A21708}"
ProjectSection(ProjectDependencies) = postProject
@ -43,6 +43,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Url",
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FFD651C7-0546-441F-BC8C-D4EE8FD01EA7}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitattributes = .gitattributes
.gitignore = .gitignore
appveyor.yml = appveyor.yml
@ -79,7 +80,7 @@ Global
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.ActiveCfg = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.Build.0 = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x86.ActiveCfg = Debug|Any CPU

View file

@ -58,7 +58,6 @@
<TextBlock
Grid.Column="0"
Margin="0,0,0,0"
FontFamily="Segoe UI"
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource actionKeywordsTitle}"

View file

@ -8,24 +8,17 @@ using Flow.Launcher.ViewModel;
namespace Flow.Launcher
{
public partial class ActionKeywords : Window
public partial class ActionKeywords
{
private readonly PluginPair plugin;
private Settings settings;
private readonly Internationalization translater = InternationalizationManager.Instance;
private readonly PluginViewModel pluginViewModel;
public ActionKeywords(string pluginId, Settings settings, PluginViewModel pluginViewModel)
public ActionKeywords(PluginViewModel pluginViewModel)
{
InitializeComponent();
plugin = PluginManager.GetPluginForId(pluginId);
this.settings = settings;
plugin = pluginViewModel.PluginPair;
this.pluginViewModel = pluginViewModel;
if (plugin == null)
{
MessageBox.Show(translater.GetTranslation("cannotFindSpecifiedPlugin"));
Close();
}
}
private void ActionKeyword_OnLoaded(object sender, RoutedEventArgs e)

View file

@ -6,6 +6,7 @@ using System.Timers;
using System.Windows;
using Flow.Launcher.Core;
using Flow.Launcher.Core.Configuration;
using Flow.Launcher.Core.ExternalPlugins.Environments;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper;
@ -61,6 +62,8 @@ namespace Flow.Launcher
_settingsVM = new SettingWindowViewModel(_updater, _portable);
_settings = _settingsVM.Settings;
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
_alphabet.Initialize(_settings);
_stringMatcher = new StringMatcher(_alphabet);
StringMatcher.Instance = _stringMatcher;
@ -74,7 +77,7 @@ namespace Flow.Launcher
Http.API = API;
Http.Proxy = _settings.Proxy;
await PluginManager.InitializePlugins(API);
await PluginManager.InitializePluginsAsync(API);
var window = new MainWindow(_settings, _mainVM);
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
@ -183,7 +186,7 @@ namespace Flow.Launcher
public void OnSecondAppStarted()
{
Current.MainWindow.Show();
_mainVM.Show();
}
}
}

View file

@ -0,0 +1,55 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Input;
namespace Flow.Launcher.Converters
{
internal class BoolToIMEConversionModeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool v)
{
if (v)
{
return ImeConversionModeValues.Alphanumeric;
}
else
{
return ImeConversionModeValues.DoNotCare;
}
}
return ImeConversionModeValues.DoNotCare;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
internal class BoolToIMEStateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool v)
{
if (v)
{
return InputMethodState.Off;
}
else
{
return InputMethodState.DoNotCare;
}
}
return InputMethodState.DoNotCare;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace Flow.Launcher.Converters
{
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, System.Type targetType, object parameter, CultureInfo culture)
{
if (parameter != null)
{
if (value is true)
{
return Visibility.Collapsed;
}
else
{
return Visibility.Visible;
}
}
else {
if (value is true)
{
return Visibility.Visible;
}
else {
return Visibility.Collapsed;
}
}
}
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
}
}

View file

@ -0,0 +1,19 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace Flow.Launcher.Converters
{
public class DateTimeFormatToNowConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is not string format ? null : DateTime.Now.ToString(format);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View file

@ -11,17 +11,17 @@ namespace Flow.Launcher.Converters
[ValueConversion(typeof(bool), typeof(Visibility))]
public class OpenResultHotkeyVisibilityConverter : IValueConverter
{
private const int MaxVisibleHotkeys = 9;
private const int MaxVisibleHotkeys = 10;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var hotkeyNumber = int.MaxValue;
var number = int.MaxValue;
if (value is ListBoxItem listBoxItem
&& ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox)
hotkeyNumber = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
number = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
return hotkeyNumber <= MaxVisibleHotkeys ? Visibility.Visible : Visibility.Collapsed;
return number <= MaxVisibleHotkeys ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();

View file

@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using System.Windows.Controls;
using System.Windows.Data;
@ -10,7 +10,10 @@ namespace Flow.Launcher.Converters
{
if (value is ListBoxItem listBoxItem
&& ItemsControl.ItemsControlFromItemContainer(listBoxItem) is ListBox listBox)
return listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
{
var res = listBox.ItemContainerGenerator.IndexFromContainer(listBoxItem) + 1;
return res == 10 ? 0 : res; // 10th item => HOTKEY+0
}
return 0;
}

View file

@ -52,7 +52,8 @@ namespace Flow.Launcher.Converters
// Check if Text will be larger then our QueryTextBox
System.Windows.Media.Typeface typeface = new Typeface(QueryTextBox.FontFamily, QueryTextBox.FontStyle, QueryTextBox.FontWeight, QueryTextBox.FontStretch);
System.Windows.Media.FormattedText ft = new FormattedText(QueryTextBox.Text, System.Globalization.CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, QueryTextBox.FontSize, Brushes.Black);
// TODO: Obsolete warning?
System.Windows.Media.FormattedText ft = new FormattedText(QueryTextBox.Text, System.Globalization.CultureInfo.DefaultThreadCurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, QueryTextBox.FontSize, Brushes.Black);
var offset = QueryTextBox.Padding.Right;
@ -75,4 +76,4 @@ namespace Flow.Launcher.Converters
throw new NotImplementedException();
}
}
}
}

View file

@ -0,0 +1,32 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Input;
namespace Flow.Launcher.Converters
{
class StringToKeyBindingConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var mode = parameter as string;
var hotkeyStr = value as string;
var converter = new KeyGestureConverter();
var key = (KeyGesture)converter.ConvertFromString(hotkeyStr);
if (mode == "key")
{
return key.Key;
}
else if (mode == "modifiers")
{
return key.Modifiers;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,32 @@
using System;
using System.Globalization;
using System.Windows.Data;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.ViewModel;
namespace Flow.Launcher.Converters
{
public class TextConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var ID = value.ToString();
switch(ID)
{
case PluginStoreItemViewModel.NewRelease:
return InternationalizationManager.Instance.GetTranslation("pluginStore_NewRelease");
case PluginStoreItemViewModel.RecentlyUpdated:
return InternationalizationManager.Instance.GetTranslation("pluginStore_RecentlyUpdated");
case PluginStoreItemViewModel.None:
return InternationalizationManager.Instance.GetTranslation("pluginStore_None");
case PluginStoreItemViewModel.Installed:
return InternationalizationManager.Instance.GetTranslation("pluginStore_Installed");
default:
return ID;
}
}
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
}
}

View file

@ -64,7 +64,6 @@
<TextBlock
Grid.Column="0"
Margin="0,0,0,0"
FontFamily="Segoe UI"
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource customeQueryHotkeyTitle}"
@ -76,10 +75,14 @@
Text="{DynamicResource customeQueryHotkeyTips}"
TextAlignment="Left"
TextWrapping="WrapWithOverflow" />
<Image
Width="478"
Margin="0,20,0,0"
Source="/Images/illustration_01.png" />
</StackPanel>
<StackPanel Margin="0,20,0,0" Orientation="Horizontal">
<Grid Width="470">
<Grid Width="478">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
@ -132,6 +135,7 @@
LastChildFill="True">
<Button
x:Name="btnTestActionKeyword"
Margin="0,0,10,0"
Padding="10,5,10,5"
Click="BtnTestActionKeyword_OnClick"
Content="{DynamicResource preview}"

View file

@ -81,7 +81,7 @@ namespace Flow.Launcher
}
tbAction.Text = updateCustomHotkey.ActionKeyword;
ctlHotkey.SetHotkey(updateCustomHotkey.Hotkey, false);
_ = ctlHotkey.SetHotkeyAsync(updateCustomHotkey.Hotkey, false);
update = true;
lblAdd.Text = InternationalizationManager.Instance.GetTranslation("update");
}

View file

@ -0,0 +1,164 @@
<Window
x:Class="Flow.Launcher.CustomShortcutSetting"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:flowlauncher="clr-namespace:Flow.Launcher"
Title="{DynamicResource customeQueryShortcutTitle}"
Width="530"
Background="{DynamicResource PopuBGColor}"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Foreground="{DynamicResource PopupTextColor}"
Icon="Images\app.png"
ResizeMode="NoResize"
SizeToContent="Height"
WindowStartupLocation="CenterScreen">
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
<Window.InputBindings>
<KeyBinding Key="Escape" Command="Close" />
</Window.InputBindings>
<Window.CommandBindings>
<CommandBinding Command="Close" Executed="cmdEsc_OnPress" />
</Window.CommandBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="80" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
Grid.Column="4"
Click="BtnCancel_OnClick"
Style="{StaticResource TitleBarCloseButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18,11 27,20 M 18,20 27,11"
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1">
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
</Grid>
</StackPanel>
<StackPanel Margin="26,0,26,0">
<StackPanel Grid.Row="0" Margin="0,0,0,12">
<TextBlock
Grid.Column="0"
Margin="0,0,0,0"
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource customQueryShortcut}"
TextAlignment="Left" />
</StackPanel>
<StackPanel>
<TextBlock
FontSize="14"
Text="{DynamicResource customeQueryShortcutTips}"
TextAlignment="Left"
TextWrapping="WrapWithOverflow" />
<Image
Width="478"
Margin="0,20,0,0"
Source="/Images/illustration_02.png" />
</StackPanel>
<StackPanel Margin="0,10,0,10" Orientation="Horizontal">
<Grid Width="478">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource customShortcut}" />
<TextBox
Grid.Row="0"
Grid.Column="1"
Width="180"
Margin="10"
HorizontalAlignment="Left"
Text="{Binding Key}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource customShortcutExpansion}" />
<DockPanel
Grid.Row="1"
Grid.Column="1"
LastChildFill="True">
<Button
x:Name="btnTestShortcut"
Margin="0,0,10,0"
Padding="10,5,10,5"
Click="BtnTestShortcut_OnClick"
Content="{DynamicResource preview}"
DockPanel.Dock="Right" />
<TextBox
x:Name="tbExpand"
Margin="10,0,10,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Value}" />
</DockPanel>
</Grid>
</StackPanel>
</StackPanel>
</StackPanel>
<Border
Grid.Row="1"
Margin="0,10,0,0"
Background="{DynamicResource PopupButtonAreaBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0,1,0,0">
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button
x:Name="btnCancel"
MinWidth="140"
Margin="10,0,5,0"
Click="BtnCancel_OnClick"
Content="{DynamicResource cancel}" />
<Button
x:Name="btnAdd"
MinWidth="140"
Margin="5,0,10,0"
Click="BtnAdd_OnClick"
Style="{StaticResource AccentButtonStyle}">
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />
</Button>
</StackPanel>
</Border>
</Grid>
</Window>

View file

@ -0,0 +1,73 @@
using Flow.Launcher.Core.Resource;
using Flow.Launcher.ViewModel;
using System;
using System.Windows;
using System.Windows.Input;
namespace Flow.Launcher
{
public partial class CustomShortcutSetting : Window
{
private SettingWindowViewModel viewModel;
public string Key { get; set; } = String.Empty;
public string Value { get; set; } = String.Empty;
private string originalKey { get; init; } = null;
private string originalValue { get; init; } = null;
private bool update { get; init; } = false;
public CustomShortcutSetting(SettingWindowViewModel vm)
{
viewModel = vm;
InitializeComponent();
}
public CustomShortcutSetting(string key, string value, SettingWindowViewModel vm)
{
viewModel = vm;
Key = key;
Value = value;
originalKey = key;
originalValue = value;
update = true;
InitializeComponent();
}
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void BtnAdd_OnClick(object sender, RoutedEventArgs e)
{
if (String.IsNullOrEmpty(Key) || String.IsNullOrEmpty(Value))
{
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("emptyShortcut"));
return;
}
// Check if key is modified or adding a new one
if (((update && originalKey != Key) || !update)
&& viewModel.ShortcutExists(Key))
{
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("duplicateShortcut"));
return;
}
DialogResult = !update || originalKey != Key || originalValue != Value;
Close();
}
private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void BtnTestShortcut_OnClick(object sender, RoutedEventArgs e)
{
App.API.ChangeQuery(tbExpand.Text);
Application.Current.MainWindow.Show();
Application.Current.MainWindow.Opacity = 1;
Application.Current.MainWindow.Focus();
}
}
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
@ -83,20 +83,22 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.0.0" />
<PackageReference Include="Fody" Version="6.5.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="InputSimulator" Version="1.0.4" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="ModernWpfUI" Version="0.9.4" />
<PackageReference Include="NHotkey.Wpf" Version="2.1.0" />
<PackageReference Include="NuGet.CommandLine" Version="5.7.2">
<PackageReference Include="NuGet.CommandLine" Version="6.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
<PackageReference Include="SharpVectors" Version="1.7.6" />
<PackageReference Include="SharpVectors" Version="1.8.1" />
<PackageReference Include="VirtualizingWrapPanel" Version="1.5.7" />
</ItemGroup>
<ItemGroup>
@ -114,4 +116,14 @@
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="taskkill /f /fi &quot;IMAGENAME eq Flow.Launcher.exe&quot;" />
</Target>
</Project>
<Target Name="RemoveDuplicateAnalyzers" BeforeTargets="CoreCompile">
<!-- Work around https://github.com/dotnet/wpf/issues/6792 -->
<ItemGroup>
<FilteredAnalyzer Include="@(Analyzer-&gt;Distinct())" />
<Analyzer Remove="@(Analyzer)" />
<Analyzer Include="@(FilteredAnalyzer)" />
</ItemGroup>
</Target>
</Project>

View file

@ -43,8 +43,8 @@ namespace Flow.Launcher.Helper
public static string DependenciesInfo()
{
var info = $"\nPython Path: {Constant.PythonPath}";
var info = $"\nPython Path: {Constant.PythonPath}\nNode Path: {Constant.NodePath}";
return info;
}
}
}
}

View file

@ -1,4 +1,4 @@
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Infrastructure.UserSettings;
using System;
using NHotkey;
@ -17,7 +17,7 @@ namespace Flow.Launcher.Helper
internal static void Initialize(MainViewModel mainVM)
{
mainViewModel = mainVM;
settings = mainViewModel._settings;
settings = mainViewModel.Settings;
SetHotkey(settings.Hotkey, OnToggleHotkey);
LoadCustomPluginHotkey();
@ -25,7 +25,7 @@ namespace Flow.Launcher.Helper
internal static void OnToggleHotkey(object sender, HotkeyEventArgs args)
{
if (!mainViewModel.ShouldIgnoreHotkeys() && !mainViewModel.GameModeStatus)
if (!mainViewModel.ShouldIgnoreHotkeys())
mainViewModel.ToggleFlowLauncher();
}
@ -74,7 +74,7 @@ namespace Flow.Launcher.Helper
{
SetHotkey(hotkey.Hotkey, (s, e) =>
{
if (mainViewModel.ShouldIgnoreHotkeys() || mainViewModel.GameModeStatus)
if (mainViewModel.ShouldIgnoreHotkeys())
return;
mainViewModel.Show();

View file

@ -385,27 +385,5 @@ namespace Flow.Launcher.Helper
}
#endregion
#region Private Classes
/// <summary>
/// Remoting service class which is exposed by the server i.e the first instance and called by the second instance
/// to pass on the command line arguments to the first instance and cause it to activate itself.
/// </summary>
private class IPCRemoteService : MarshalByRefObject
{
/// <summary>
/// Remoting Object's ease expires after every 5 minutes by default. We need to override the InitializeLifetimeService class
/// to ensure that lease never expires.
/// </summary>
/// <returns>Always null.</returns>
public override object InitializeLifetimeService()
{
return null;
}
}
#endregion
}
}

View file

@ -69,7 +69,7 @@ namespace Flow.Launcher.Helper
//get current active window
IntPtr hWnd = GetForegroundWindow();
if (hWnd != null && !hWnd.Equals(IntPtr.Zero))
if (!hWnd.Equals(IntPtr.Zero))
{
//if current active window is NOT desktop or shell
if (!(hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL)))
@ -98,7 +98,7 @@ namespace Flow.Launcher.Helper
{
IntPtr hWndDesktop = FindWindowEx(hWnd, IntPtr.Zero, "SHELLDLL_DefView", null);
hWndDesktop = FindWindowEx(hWndDesktop, IntPtr.Zero, "SysListView32", "FolderView");
if (hWndDesktop != null && !hWndDesktop.Equals(IntPtr.Zero))
if (!hWndDesktop.Equals(IntPtr.Zero))
{
return false;
}
@ -160,4 +160,4 @@ namespace Flow.Launcher.Helper
public int Bottom;
}
}
}
}

View file

@ -38,9 +38,8 @@
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource Color05B}"
Visibility="Visible">
Press key
</TextBlock>
Text="{DynamicResource flowlauncherPressHotkey}"
Visibility="Visible" />
</Border>
</Popup>
@ -49,8 +48,8 @@
Margin="0,0,18,0"
VerticalContentAlignment="Center"
input:InputMethod.IsInputMethodEnabled="False"
LostFocus="tbHotkey_LostFocus"
PreviewKeyDown="TbHotkey_OnPreviewKeyDown"
TabIndex="100"
LostFocus="tbHotkey_LostFocus"/>
TabIndex="100" />
</Grid>
</UserControl>

View file

@ -9,6 +9,7 @@ using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Plugin;
using System.Threading;
using System.Windows.Interop;
namespace Flow.Launcher
{
@ -65,11 +66,11 @@ namespace Flow.Launcher
{
await Task.Delay(500, token);
if (!token.IsCancellationRequested)
await SetHotkey(hotkeyModel);
await SetHotkeyAsync(hotkeyModel);
});
}
public async Task SetHotkey(HotkeyModel keyModel, bool triggerValidate = true)
public async Task SetHotkeyAsync(HotkeyModel keyModel, bool triggerValidate = true)
{
CurrentHotkey = keyModel;
@ -101,9 +102,9 @@ namespace Flow.Launcher
}
}
public void SetHotkey(string keyStr, bool triggerValidate = true)
public Task SetHotkeyAsync(string keyStr, bool triggerValidate = true)
{
SetHotkey(new HotkeyModel(keyStr), triggerValidate);
return SetHotkeyAsync(new HotkeyModel(keyStr), triggerValidate);
}
private bool CheckHotkeyAvailability() => HotKeyMapper.CheckAvailability(CurrentHotkey);
@ -113,7 +114,7 @@ namespace Flow.Launcher
private void tbHotkey_LostFocus(object sender, RoutedEventArgs e)
{
tbMsg.Text = tbMsgTextOriginal;
tbMsg.Foreground = tbMsgForegroundColorOriginal;
tbMsg.SetResourceReference(TextBox.ForegroundProperty, "Color05B");
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Some files were not shown because too many files have changed in this diff Show more