Merge remote-tracking branch 'upstream/dev' into ResetWindowWhenShowUp

This commit is contained in:
Vic 2023-04-03 13:15:08 +08:00
commit 12da405387
631 changed files with 90533 additions and 39627 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.
-->

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

@ -0,0 +1,80 @@
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.
- label: >
I am using the latest version of Flow Launcher.
- 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". If you are using a prerelease version please append the build number.
placeholder: "Example: 1.11.0"
validations:
required: true
- type: input
attributes:
label: Windows Build Number
description: Run "ver" at CMD (command prompt) or go to Windows Settings -> Systems -> About.
placeholder: "Example: 10.0.19043.1288"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Error Log
description: >
From flow type 'open log location' and find log file with the corresponding date containing the error info.
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>

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

@ -0,0 +1,5 @@
github
https
ssh
ubuntu
runcount

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$

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

@ -0,0 +1,90 @@
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
bak
tmp
directx
mvvm
dlg
ddd
dddd
clearlogfolder
ACCENT_ENABLE_TRANSPARENTGRADIENT
ACCENT_ENABLE_BLURBEHIND
WCA_ACCENT_POLICY
HGlobal
dopusrt
firefox
msedge
svgc
ime
zindex
txb
btn
otf
searchplugin
Noresult
wpftk
mkv
flac

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')
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 }}"

26
.github/workflows/stale.yml vendored Normal file
View file

@ -0,0 +1,26 @@
# For more information, see:
# https://github.com/actions/stale
name: Mark stale issues and pull requests
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v8
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
days-before-close: 7
days-before-pr-close: -1
exempt-all-milestones: true
close-issue-message: 'This issue was closed because it has been stale for 7 days with no activity. If you feel this issue still needs attention please feel free to reopen.'
stale-pr-label: 'no-pr-activity'
exempt-issue-labels: 'keep-fresh'
exempt-pr-labels: 'keep-fresh,awaiting-approval,work-in-progress'

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@v2
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

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0-windows</TargetFramework>
<TargetFramework>net7.0-windows</TargetFramework>
<UseWpf>true</UseWpf>
<UseWindowsForms>true</UseWindowsForms>
<OutputType>Library</OutputType>
@ -53,10 +53,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Droplex" Version="1.4.0" />
<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="7.0.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.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

@ -93,4 +93,4 @@ namespace Flow.Launcher.Core.Plugin
public Dictionary<string, object> SettingsChange { get; set; }
}
}
}

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)
@ -115,7 +118,7 @@ namespace Flow.Launcher.Core.Plugin
foreach (var result in queryResponseModel.Result)
{
result.Action = c =>
result.AsyncAction = async c =>
{
UpdateSettings(result.SettingsChange);
@ -133,15 +136,15 @@ namespace Flow.Launcher.Core.Plugin
}
else
{
var actionResponse = Request(result.JsonRPCAction);
await using var actionResponse = await RequestAsync(result.JsonRPCAction);
if (string.IsNullOrEmpty(actionResponse))
if (actionResponse.Length == 0)
{
return !result.JsonRPCAction.DontHideAfterAction;
}
var jsonRpcRequestModel =
JsonSerializer.Deserialize<JsonRPCRequestModel>(actionResponse, options);
var jsonRpcRequestModel = await
JsonSerializer.DeserializeAsync<JsonRPCRequestModel>(actionResponse, options);
if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false)
{
@ -166,19 +169,20 @@ namespace Flow.Launcher.Core.Plugin
private void ExecuteFlowLauncherAPI(string method, object[] parameters)
{
var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray();
MethodInfo methodInfo = PluginManager.API.GetType().GetMethod(method, parametersTypeArray);
if (methodInfo != null)
var methodInfo = typeof(IPublicAPI).GetMethod(method, parametersTypeArray);
if (methodInfo == null)
{
return;
}
try
{
methodInfo.Invoke(PluginManager.API, parameters);
}
catch (Exception)
{
try
{
methodInfo.Invoke(PluginManager.API, parameters);
}
catch (Exception)
{
#if (DEBUG)
throw;
throw;
#endif
}
}
}
@ -240,73 +244,62 @@ namespace Flow.Launcher.Core.Plugin
protected async Task<Stream> ExecuteAsync(ProcessStartInfo startInfo, CancellationToken token = default)
{
Process process = null;
bool disposed = false;
try
using var process = Process.Start(startInfo);
if (process == null)
{
process = Process.Start(startInfo);
if (process == null)
{
Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process");
return Stream.Null;
}
Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process");
return Stream.Null;
}
await using var source = process.StandardOutput.BaseStream;
var sourceBuffer = BufferManager.GetStream();
using var errorBuffer = BufferManager.GetStream();
var buffer = BufferManager.GetStream();
token.Register(() =>
{
// ReSharper disable once AccessToModifiedClosure
// Manually Check whether disposed
if (!disposed && !process.HasExited)
process.Kill();
});
var sourceCopyTask = process.StandardOutput.BaseStream.CopyToAsync(sourceBuffer, token);
var errorCopyTask = process.StandardError.BaseStream.CopyToAsync(errorBuffer, token);
await using var registeredEvent = token.Register(() =>
{
try
{
// token expire won't instantly trigger the exception,
// manually kill process at before
await source.CopyToAsync(buffer, token);
if (!process.HasExited)
process.Kill();
sourceBuffer.Dispose();
}
catch (OperationCanceledException)
catch (Exception e)
{
await buffer.DisposeAsync();
return Stream.Null;
Log.Exception("|JsonRPCPlugin.ExecuteAsync|Exception when kill process", e);
}
});
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;
}
finally
try
{
process?.Dispose();
disposed = true;
// token expire won't instantly trigger the exception,
// manually kill process at before
await process.WaitForExitAsync(token);
await Task.WhenAll(sourceCopyTask, errorCopyTask);
}
catch (OperationCanceledException)
{
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
@ -354,126 +347,242 @@ 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
};
break;
}
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()
{
var textBox = new TextBox()
{
Width = 300,
Text = Settings[attribute.Name] as string ?? string.Empty,
Margin = settingControlMargin,
ToolTip = attribute.Description
};
textBox.TextChanged += (_, _) =>
{
Settings[attribute.Name] = textBox.Text;
};
contentControl = textBox;
break;
}
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()
{
Margin = new Thickness(10, 0, 0, 0),
Text = Settings[attribute.Name] as string ?? string.Empty,
HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch,
ToolTip = attribute.Description
};
textBox.TextChanged += (_, _) =>
{
Settings[attribute.Name] = textBox.Text;
};
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;
break;
}
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;
break;
}
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;
break;
}
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 +590,47 @@ 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 +662,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 +673,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

@ -15,20 +15,6 @@ namespace Flow.Launcher.Core.Plugin
private readonly AssemblyName assemblyName;
private static readonly ConcurrentDictionary<string, byte> loadedAssembly;
static PluginAssemblyLoader()
{
var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies();
loadedAssembly = new ConcurrentDictionary<string, byte>(
currentAssemblies.Select(x => new KeyValuePair<string, byte>(x.FullName, default)));
AppDomain.CurrentDomain.AssemblyLoad += (sender, args) =>
{
loadedAssembly[args.LoadedAssembly.FullName] = default;
};
}
internal PluginAssemblyLoader(string assemblyFilePath)
{
dependencyResolver = new AssemblyDependencyResolver(assemblyFilePath);
@ -47,10 +33,9 @@ namespace Flow.Launcher.Core.Plugin
// When resolving dependencies, ignore assembly depenedencies that already exits with Flow.Launcher
// Otherwise duplicate assembly will be loaded and some weird behavior will occur, such as WinRT.Runtime.dll
// will fail due to loading multiple versions in process, each with their own static instance of registration state
if (assemblyPath == null || ExistsInReferencedPackage(assemblyName))
return null;
var existAssembly = Default.Assemblies.FirstOrDefault(x => x.FullName == assemblyName.FullName);
return LoadFromAssemblyPath(assemblyPath);
return existAssembly ?? (assemblyPath == null ? null : LoadFromAssemblyPath(assemblyPath));
}
internal Type FromAssemblyGetTypeOfInterface(Assembly assembly, Type type)
@ -58,10 +43,5 @@ namespace Flow.Launcher.Core.Plugin
var allTypes = assembly.ExportedTypes;
return allTypes.First(o => o.IsClass && !o.IsAbstract && o.GetInterfaces().Any(t => t == type));
}
internal bool ExistsInReferencedPackage(AssemblyName assemblyName)
{
return loadedAssembly.ContainsKey(assemblyName.FullName);
}
}
}

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

@ -16,7 +16,7 @@ namespace Flow.Launcher.Core.Plugin
return null;
}
var rawQuery = string.Join(Query.TermSeparator, terms);
var rawQuery = text;
string actionKeyword, search;
string possibleActionKeyword = terms[0];
string[] searchTerms;
@ -24,13 +24,13 @@ namespace Flow.Launcher.Core.Plugin
if (nonGlobalPlugins.TryGetValue(possibleActionKeyword, out var pluginPair) && !pluginPair.Metadata.Disabled)
{ // use non global plugin for query
actionKeyword = possibleActionKeyword;
search = terms.Length > 1 ? rawQuery[(actionKeyword.Length + 1)..] : string.Empty;
search = terms.Length > 1 ? rawQuery[(actionKeyword.Length + 1)..].TrimStart() : string.Empty;
searchTerms = terms[1..];
}
else
{ // non action keyword
actionKeyword = string.Empty;
search = rawQuery;
search = rawQuery.TrimStart();
searchTerms = terms;
}

View file

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

View file

@ -19,6 +19,8 @@ namespace Flow.Launcher.Core.Resource
public static Language Serbian = new Language("sr", "Srpski");
public static Language Portuguese_Portugal = new Language("pt-pt", "Português");
public static Language Portuguese_Brazil = new Language("pt-br", "Português (Brasil)");
public static Language Spanish = new Language("es", "Spanish");
public static Language Spanish_LatinAmerica = new Language("es-419", "Spanish (Latin America)");
public static Language Italian = new Language("it", "Italiano");
public static Language Norwegian_Bokmal = new Language("nb-NO", "Norsk Bokmål");
public static Language Slovak = new Language("sk", "Slovenský");
@ -43,6 +45,8 @@ namespace Flow.Launcher.Core.Resource
Serbian,
Portuguese_Portugal,
Portuguese_Brazil,
Spanish,
Spanish_LatinAmerica,
Italian,
Norwegian_Bokmal,
Slovak,

View file

@ -96,10 +96,17 @@ 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;
Task.Run(() =>
// 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,6 +193,7 @@ namespace Flow.Launcher.Core.Resource
{
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
pluginI18N.OnCultureInfoChanged(CultureInfo.DefaultThreadCurrentCulture);
}
catch (Exception e)
{
@ -220,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;
@ -36,7 +36,7 @@ namespace Flow.Launcher.Core.Resource
{
_themeDirectories.Add(DirectoryPath);
_themeDirectories.Add(UserDirectoryPath);
MakesureThemeDirectoriesExist();
MakeSureThemeDirectoriesExist();
var dicts = Application.Current.Resources.MergedDictionaries;
_oldResource = dicts.First(d =>
@ -55,20 +55,17 @@ namespace Flow.Launcher.Core.Resource
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
}
private void MakesureThemeDirectoriesExist()
private void MakeSureThemeDirectoriesExist()
{
foreach (string dir in _themeDirectories)
foreach (var dir in _themeDirectories.Where(dir => !Directory.Exists(dir)))
{
if (!Directory.Exists(dir))
try
{
try
{
Directory.CreateDirectory(dir);
}
catch (Exception e)
{
Log.Exception($"|Theme.MakesureThemeDirectoriesExist|Exception when create directory <{dir}>", e);
}
Directory.CreateDirectory(dir);
}
catch (Exception e)
{
Log.Exception($"|Theme.MakesureThemeDirectoriesExist|Exception when create directory <{dir}>", e);
}
}
}
@ -82,13 +79,17 @@ namespace Flow.Launcher.Core.Resource
{
if (string.IsNullOrEmpty(path))
throw new DirectoryNotFoundException("Theme path can't be found <{path}>");
// reload all resources even if the theme itself hasn't changed in order to pickup changes
// to things like fonts
UpdateResourceDictionary(GetResourceDictionary(theme));
Settings.Theme = theme;
//always allow re-loading default theme, in case of failure of switching to a new theme from default theme
if (_oldTheme != theme || theme == defaultTheme)
{
UpdateResourceDictionary(GetResourceDictionary());
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
}
@ -99,7 +100,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)
@ -109,7 +110,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)
@ -131,9 +132,9 @@ namespace Flow.Launcher.Core.Resource
_oldResource = dictionaryToUpdate;
}
private ResourceDictionary CurrentThemeResourceDictionary()
private ResourceDictionary GetThemeResourceDictionary(string theme)
{
var uri = GetThemePath(Settings.Theme);
var uri = GetThemePath(theme);
var dict = new ResourceDictionary
{
Source = new Uri(uri, UriKind.Absolute)
@ -142,10 +143,12 @@ namespace Flow.Launcher.Core.Resource
return dict;
}
public ResourceDictionary GetResourceDictionary()
private ResourceDictionary CurrentThemeResourceDictionary() => GetThemeResourceDictionary(Settings.Theme);
public ResourceDictionary GetResourceDictionary(string theme)
{
var dict = CurrentThemeResourceDictionary();
var dict = GetThemeResourceDictionary(theme);
if (dict["QueryBoxStyle"] is Style queryBoxStyle &&
dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle)
{
@ -197,6 +200,11 @@ namespace Flow.Launcher.Core.Resource
return dict;
}
private ResourceDictionary GetCurrentResourceDictionary( )
{
return GetResourceDictionary(Settings.Theme);
}
public List<string> LoadAvailableThemes()
{
List<string> themes = new List<string>();
@ -226,7 +234,7 @@ namespace Flow.Launcher.Core.Resource
public void AddDropShadowEffectToCurrentTheme()
{
var dict = GetResourceDictionary();
var dict = GetCurrentResourceDictionary();
var windowBorderStyle = dict["WindowBorderStyle"] as Style;
@ -235,9 +243,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
}
};
@ -247,7 +256,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);
}
@ -269,7 +278,7 @@ namespace Flow.Launcher.Core.Resource
public void RemoveDropShadowEffectFromCurrentTheme()
{
var dict = CurrentThemeResourceDictionary();
var dict = GetCurrentResourceDictionary();
var windowBorderStyle = dict["WindowBorderStyle"] as Style;
var effectSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) as Setter;

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;
@ -33,14 +33,14 @@ namespace Flow.Launcher.Core
public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true)
{
await UpdateLock.WaitAsync();
await UpdateLock.WaitAsync().ConfigureAwait(false);
try
{
if (!silentUpdate)
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}");
@ -88,9 +88,13 @@ namespace Flow.Launcher.Core
UpdateManager.RestartApp(Constant.ApplicationFileName);
}
}
catch (Exception e) when (e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)
catch (Exception e)
{
Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
if ((e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException))
Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e);
else
Log.Exception($"|Updater.UpdateApp|Error Occurred", e);
if (!silentUpdate)
api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"),
api.GetTranslation("update_flowlauncher_check_connection"));
@ -114,8 +118,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 +141,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;
@ -19,8 +19,9 @@ namespace Flow.Launcher.Infrastructure
public static readonly string RootDirectory = Directory.GetParent(ApplicationDirectory).ToString();
public static readonly string PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins);
public const string Issue = "https://github.com/Flow-Launcher/Flow.Launcher/issues/new";
public const string Issue = "https://github.com/Flow-Launcher/Flow.Launcher/issues/new/choose";
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

@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
namespace Flow.Launcher.Infrastructure
{
public static class FileExplorerHelper
{
/// <summary>
/// Gets the path of the file explorer that is currently in the foreground
/// </summary>
public static string GetActiveExplorerPath()
{
var explorerWindow = GetActiveExplorer();
string locationUrl = explorerWindow?.LocationURL;
return !string.IsNullOrEmpty(locationUrl) ? new Uri(locationUrl).LocalPath + "\\" : null;
}
/// <summary>
/// Gets the file explorer that is currently in the foreground
/// </summary>
private static dynamic GetActiveExplorer()
{
Type type = Type.GetTypeFromProgID("Shell.Application");
if (type == null) return null;
dynamic shell = Activator.CreateInstance(type);
if (shell == null)
{
return null;
}
var explorerWindows = new List<dynamic>();
var openWindows = shell.Windows();
for (int i = 0; i < openWindows.Count; i++)
{
var window = openWindows.Item(i);
if (window == null) continue;
// find the desired window and make sure that it is indeed a file explorer
// we don't want the Internet Explorer or the classic control panel
// ToLower() is needed, because Windows can report the path as "C:\\Windows\\Explorer.EXE"
if (Path.GetFileName((string)window.FullName)?.ToLower() == "explorer.exe")
{
explorerWindows.Add(window);
}
}
if (explorerWindows.Count == 0) return null;
var zOrders = GetZOrder(explorerWindows);
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
/// <summary>
/// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1.
/// </summary>
private static IEnumerable<int> GetZOrder(List<dynamic> hWnds)
{
var z = new int[hWnds.Count];
for (var i = 0; i < hWnds.Count; i++) z[i] = -1;
var index = 0;
var numRemaining = hWnds.Count;
EnumWindows((wnd, _) =>
{
var searchIndex = hWnds.FindIndex(x => x.HWND == wnd.ToInt32());
if (searchIndex != -1)
{
z[searchIndex] = index;
numRemaining--;
if (numRemaining == 0) return false;
}
index++;
return true;
}, IntPtr.Zero);
return z;
}
}
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0-windows</TargetFramework>
<TargetFramework>net7.0-windows</TargetFramework>
<ProjectGuid>{4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}</ProjectGuid>
<OutputType>Library</OutputType>
<UseWpf>true</UseWpf>
@ -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

@ -1,4 +1,6 @@
using System;
#nullable enable
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text.Json;
@ -16,7 +18,7 @@ namespace Flow.Launcher.Infrastructure
/// <summary>
/// http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy
/// </summary>
public static T NonNull<T>(this T obj)
public static T NonNull<T>(this T? obj)
{
if (obj == null)
{

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows.Input;
@ -11,10 +12,10 @@ namespace Flow.Launcher.Infrastructure.Hotkey
public bool Shift { get; set; }
public bool Win { get; set; }
public bool Ctrl { get; set; }
public Key CharKey { get; set; }
public Key CharKey { get; set; } = Key.None;
Dictionary<Key, string> specialSymbolDictionary = new Dictionary<Key, string>
private static readonly Dictionary<Key, string> specialSymbolDictionary = new Dictionary<Key, string>
{
{Key.Space, "Space"},
{Key.Oem3, "~"}
@ -27,19 +28,19 @@ namespace Flow.Launcher.Infrastructure.Hotkey
ModifierKeys modifierKeys = ModifierKeys.None;
if (Alt)
{
modifierKeys = ModifierKeys.Alt;
modifierKeys |= ModifierKeys.Alt;
}
if (Shift)
{
modifierKeys = modifierKeys | ModifierKeys.Shift;
modifierKeys |= ModifierKeys.Shift;
}
if (Win)
{
modifierKeys = modifierKeys | ModifierKeys.Windows;
modifierKeys |= ModifierKeys.Windows;
}
if (Ctrl)
{
modifierKeys = modifierKeys | ModifierKeys.Control;
modifierKeys |= ModifierKeys.Control;
}
return modifierKeys;
}
@ -86,7 +87,7 @@ namespace Flow.Launcher.Infrastructure.Hotkey
Ctrl = true;
keys.Remove("Ctrl");
}
if (keys.Count > 0)
if (keys.Count == 1)
{
string charKey = keys[0];
KeyValuePair<Key, string>? specialSymbolPair = specialSymbolDictionary.FirstOrDefault(pair => pair.Value == charKey);
@ -98,7 +99,7 @@ namespace Flow.Launcher.Infrastructure.Hotkey
{
try
{
CharKey = (Key) Enum.Parse(typeof (Key), charKey);
CharKey = (Key)Enum.Parse(typeof(Key), charKey);
}
catch (ArgumentException)
{
@ -110,36 +111,116 @@ namespace Flow.Launcher.Infrastructure.Hotkey
public override string ToString()
{
string text = string.Empty;
List<string> keys = new List<string>();
if (Ctrl)
{
text += "Ctrl + ";
keys.Add("Ctrl");
}
if (Alt)
{
text += "Alt + ";
keys.Add("Alt");
}
if (Shift)
{
text += "Shift + ";
keys.Add("Shift");
}
if (Win)
{
text += "Win + ";
keys.Add("Win");
}
if (CharKey != Key.None)
{
text += specialSymbolDictionary.ContainsKey(CharKey)
keys.Add(specialSymbolDictionary.ContainsKey(CharKey)
? specialSymbolDictionary[CharKey]
: CharKey.ToString();
: CharKey.ToString());
}
else if (!string.IsNullOrEmpty(text))
return string.Join(" + ", keys);
}
/// <summary>
/// Validate hotkey
/// </summary>
/// <param name="validateKeyGestrue">Try to validate hotkey as a KeyGesture.</param>
/// <returns></returns>
public bool Validate(bool validateKeyGestrue = false)
{
switch (CharKey)
{
text = text.Remove(text.Length - 3);
case Key.LeftAlt:
case Key.RightAlt:
case Key.LeftCtrl:
case Key.RightCtrl:
case Key.LeftShift:
case Key.RightShift:
case Key.LWin:
case Key.RWin:
case Key.None:
return false;
default:
if (validateKeyGestrue)
{
try
{
KeyGesture keyGesture = new KeyGesture(CharKey, ModifierKeys);
}
catch (System.Exception e) when (e is NotSupportedException || e is InvalidEnumArgumentException)
{
return false;
}
}
if (ModifierKeys == ModifierKeys.None)
{
return !IsPrintableCharacter(CharKey);
}
else
{
return true;
}
}
}
return text;
private static bool IsPrintableCharacter(Key key)
{
// https://stackoverflow.com/questions/11881199/identify-if-a-event-key-is-text-not-only-alphanumeric
return (key >= Key.A && key <= Key.Z) ||
(key >= Key.D0 && key <= Key.D9) ||
(key >= Key.NumPad0 && key <= Key.NumPad9) ||
key == Key.OemQuestion ||
key == Key.OemQuotes ||
key == Key.OemPlus ||
key == Key.OemOpenBrackets ||
key == Key.OemCloseBrackets ||
key == Key.OemMinus ||
key == Key.DeadCharProcessed ||
key == Key.Oem1 ||
key == Key.Oem7 ||
key == Key.OemPeriod ||
key == Key.OemComma ||
key == Key.OemMinus ||
key == Key.Add ||
key == Key.Divide ||
key == Key.Multiply ||
key == Key.Subtract ||
key == Key.Oem102 ||
key == Key.Decimal;
}
public override bool Equals(object obj)
{
if (obj is HotkeyModel other)
{
return ModifierKeys == other.ModifierKeys && CharKey == other.CharKey;
}
else
{
return false;
}
}
public override int GetHashCode()
{
return HashCode.Combine(ModifierKeys, CharKey);
}
}
}

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();
@ -74,7 +73,7 @@ namespace Flow.Launcher.Infrastructure.Image
// To delete the images from the data dictionary based on the resizing of the Usage Dictionary
// Double Check to avoid concurrent remove
if (Data.Count > permissibleFactor * MaxCached)
foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key).ToArray())
foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key))
Data.TryRemove(key, out _);
semaphore.Release();
}
@ -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,5 @@
using System;
#nullable enable
using System;
using System.Globalization;
using System.IO;
using System.Text.Json;
@ -11,62 +12,86 @@ namespace Flow.Launcher.Infrastructure.Storage
/// </summary>
public class JsonStorage<T> where T : new()
{
protected T _data;
protected T? Data;
// need a new directory name
public const string DirectoryName = "Settings";
public const string FileSuffix = ".json";
public string FilePath { get; set; }
public string DirectoryPath { get; set; }
protected string FilePath { get; init; } = null!;
private string TempFilePath => $"{FilePath}.tmp";
private string BackupFilePath => $"{FilePath}.bak";
protected string DirectoryPath { get; init; } = null!;
public T Load()
{
string? serialized = null;
if (File.Exists(FilePath))
{
var serialized = File.ReadAllText(FilePath);
if (!string.IsNullOrWhiteSpace(serialized))
serialized = File.ReadAllText(FilePath);
}
if (!string.IsNullOrEmpty(serialized))
{
try
{
Deserialize(serialized);
Data = JsonSerializer.Deserialize<T>(serialized) ?? TryLoadBackup() ?? LoadDefault();
}
else
catch (JsonException)
{
LoadDefault();
Data = TryLoadBackup() ?? LoadDefault();
}
}
else
{
LoadDefault();
Data = TryLoadBackup() ?? LoadDefault();
}
return _data.NonNull();
return Data.NonNull();
}
private void Deserialize(string serialized)
{
try
{
_data = JsonSerializer.Deserialize<T>(serialized);
}
catch (JsonException e)
{
LoadDefault();
Log.Exception($"|JsonStorage.Deserialize|Deserialize error for json <{FilePath}>", e);
}
if (_data == null)
{
LoadDefault();
}
}
private void LoadDefault()
private T LoadDefault()
{
if (File.Exists(FilePath))
{
BackupOriginFile();
}
_data = new T();
Save();
return new T();
}
private T? TryLoadBackup()
{
if (!File.Exists(BackupFilePath))
return default;
try
{
var data = JsonSerializer.Deserialize<T>(File.ReadAllText(BackupFilePath));
if (data != null)
{
Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully");
if(File.Exists(FilePath))
File.Replace(BackupFilePath, FilePath, null);
else
File.Move(BackupFilePath, FilePath);
return data;
}
return default;
}
catch (JsonException)
{
return default;
}
}
private void BackupOriginFile()
@ -82,13 +107,22 @@ namespace Flow.Launcher.Infrastructure.Storage
public void Save()
{
string serialized = JsonSerializer.Serialize(_data, new JsonSerializerOptions() { WriteIndented = true });
string serialized = JsonSerializer.Serialize(Data,
new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(FilePath, serialized);
File.WriteAllText(TempFilePath, serialized);
if (!File.Exists(FilePath))
{
File.Move(TempFilePath, FilePath);
}
else
{
File.Replace(TempFilePath, FilePath, BackupFilePath);
}
}
}
[Obsolete("Deprecated as of Flow Launcher v1.8.0, on 2021.06.21. " +
"This is used only for Everything plugin v1.4.9 or below backwards compatibility")]
public class JsonStrorage<T> : JsonStorage<T> where T : new() { }
}

View file

@ -18,7 +18,7 @@ namespace Flow.Launcher.Infrastructure.Storage
public PluginJsonStorage(T data) : this()
{
_data = data;
Data = data;
}
}
}

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
@ -13,11 +13,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public class Settings : BaseModel
{
private string language = "en";
private string _theme = Constant.DefaultTheme;
public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}";
public string OpenResultModifiers { get; set; } = KeyConstant.Alt;
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
{
@ -28,7 +30,18 @@ namespace Flow.Launcher.Infrastructure.UserSettings
OnPropertyChanged();
}
}
public string Theme { get; set; } = Constant.DefaultTheme;
public string Theme
{
get => _theme;
set
{
if (value == _theme)
return;
_theme = value;
OnPropertyChanged();
OnPropertyChanged(nameof(MaxResultsToShow));
}
}
public bool UseDropShadowEffect { get; set; } = false;
public string QueryBoxFont { get; set; } = FontFamily.GenericSansSerif.Name;
public string QueryBoxFontStyle { get; set; }
@ -41,8 +54,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 +143,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
PrivateArg = "-private",
EnablePrivate = false,
Editable = false
}
,
},
new()
{
Name = "MS Edge",
@ -137,6 +159,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;
@ -171,12 +195,32 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public double WindowLeft { get; set; }
public double WindowTop { get; set; }
/// <summary>
/// Custom left position on selected monitor
/// </summary>
public double CustomWindowLeft { get; set; } = 0;
/// <summary>
/// Custom top position on selected monitor
/// </summary>
public double CustomWindowTop { get; set; } = 0;
public int MaxResultsToShow { get; set; } = 5;
public int ActivateTimes { get; set; }
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()
{
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText),
new BuiltinShortcutModel("{active_explorer_path}", "shortcut_active_explorer_path", FileExplorerHelper.GetActiveExplorerPath)
};
public bool DontPromptUpdateMsg { get; set; }
public bool EnableUpdateLog { get; set; }
@ -193,8 +237,16 @@ namespace Flow.Launcher.Infrastructure.UserSettings
}
}
public bool LeaveCmdOpen { get; set; }
public bool HideWhenDeactive { get; set; } = true;
public bool RememberLastLaunchLocation { get; set; }
public bool HideWhenDeactivated { get; set; } = true;
[JsonConverter(typeof(JsonStringEnumConverter))]
public SearchWindowScreens SearchWindowScreen { get; set; } = SearchWindowScreens.RememberLastLaunchLocation;
[JsonConverter(typeof(JsonStringEnumConverter))]
public SearchWindowAligns SearchWindowAlign { get; set; } = SearchWindowAligns.Center;
public int CustomScreenNumber { get; set; } = 1;
public bool IgnoreHotkeysOnFullscreen { get; set; }
public HttpProxy Proxy { get; set; } = new HttpProxy();
@ -220,4 +272,22 @@ namespace Flow.Launcher.Infrastructure.UserSettings
Light,
Dark
}
}
public enum SearchWindowScreens
{
RememberLastLaunchLocation,
Cursor,
Focus,
Primary,
Custom
}
public enum SearchWindowAligns
{
Center,
CenterTop,
LeftTop,
RightTop,
Custom
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,6 @@
namespace Flow.Launcher.Plugin
using System.Windows.Input;
namespace Flow.Launcher.Plugin
{
public class ActionContext
{
@ -11,5 +13,13 @@
public bool ShiftPressed { get; set; }
public bool AltPressed { get; set; }
public bool WinPressed { get; set; }
public ModifierKeys ToModifierKeys()
{
return (CtrlPressed ? ModifierKeys.Control : ModifierKeys.None) |
(ShiftPressed ? ModifierKeys.Shift : ModifierKeys.None) |
(AltPressed ? ModifierKeys.Alt : ModifierKeys.None) |
(WinPressed ? ModifierKeys.Windows : ModifierKeys.None);
}
}
}
}

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

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0-windows</TargetFramework>
<TargetFramework>net7.0-windows</TargetFramework>
<ProjectGuid>{8451ECDD-2EA4-4966-BB0A-7BBC40138E80}</ProjectGuid>
<UseWPF>true</UseWPF>
<OutputType>Library</OutputType>
@ -14,10 +14,10 @@
</PropertyGroup>
<PropertyGroup>
<Version>2.1.1</Version>
<PackageVersion>2.1.1</PackageVersion>
<AssemblyVersion>2.1.1</AssemblyVersion>
<FileVersion>2.1.1</FileVersion>
<Version>4.0.0</Version>
<PackageVersion>4.0.0</PackageVersion>
<AssemblyVersion>4.0.0</AssemblyVersion>
<FileVersion>4.0.0</FileVersion>
<PackageId>Flow.Launcher.Plugin</PackageId>
<Authors>Flow-Launcher</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@ -65,8 +65,8 @@
<PrivateAssets>all</PrivateAssets>
<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="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<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
@ -224,16 +233,30 @@ namespace Flow.Launcher.Plugin
/// Open directory in an explorer configured by user via Flow's Settings. The default is Windows Explorer
/// </summary>
/// <param name="DirectoryPath">Directory Path to open</param>
/// <param name="FileName">Extra FileName Info</param>
public void OpenDirectory(string DirectoryPath, string FileName = null);
/// <param name="FileNameOrFilePath">Extra FileName Info</param>
public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null);
/// <summary>
/// Opens the url. The browser and mode used is based on what's configured in Flow's default browser settings.
/// Opens the URL with the given Uri object.
/// The browser and mode used is based on what's configured in Flow's default browser settings.
/// </summary>
public void OpenUrl(Uri url, bool? inPrivate = null);
/// <summary>
/// Opens the URL with the given string.
/// The browser and mode used is based on what's configured in Flow's default browser settings.
/// Non-C# plugins should use this method.
/// </summary>
public void OpenUrl(string url, bool? inPrivate = null);
/// <summary>
/// Opens the application URI.
/// Opens the application URI with the given Uri object, e.g. obsidian://search-query-example
/// </summary>
public void OpenAppUri(Uri appUri);
/// <summary>
/// Opens the application URI with the given string, e.g. obsidian://search-query-example
/// Non-C# plugins should use this method
/// </summary>
public void OpenAppUri(string appUri);
}

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,11 +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
{
@ -34,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
@ -53,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
{
@ -63,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)
@ -85,6 +107,14 @@ namespace Flow.Launcher.Plugin
/// </summary>
public Func<ActionContext, bool> Action { get; set; }
/// <summary>
/// Delegate. An Async action to take in the form of a function call when the result has been selected
/// <returns>
/// true to hide flowlauncher after select result
/// </returns>
/// </summary>
public Func<ActionContext, ValueTask<bool>> AsyncAction { get; set; }
/// <summary>
/// Priority of the current result
/// <value>default: 0</value>
@ -96,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; }
@ -113,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;
}
}
@ -144,7 +178,7 @@ namespace Flow.Launcher.Plugin
/// <inheritdoc />
public override string ToString()
{
return Title + SubTitle;
return Title + SubTitle + Score;
}
/// <summary>
@ -169,5 +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

@ -1,16 +1,19 @@
using System;
using System.Diagnostics;
using System.IO;
#pragma warning disable IDE0005
using System.Windows;
#pragma warning restore IDE0005
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";
private const string FileExplorerProgramEXE = "explorer.exe";
/// <summary>
/// Copies the folder and all of its files and folders
/// including subfolders to the target location
@ -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,47 +113,91 @@ 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 + '"' };
var psi = new ProcessStartInfo
{
FileName = FileExplorerProgramName,
UseShellExecute = true,
Arguments = '"' + fileOrFolderPath + '"'
};
try
{
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
}
}
public static void OpenContainingFolder(string path)
/// <summary>
/// Open a file with associated application
/// </summary>
/// <param name="filePath">File path</param>
/// <param name="workingDir">Working directory</param>
/// <param name="asAdmin">Open as Administrator</param>
public static void OpenFile(string filePath, string workingDir = "", bool asAdmin = false)
{
Process.Start(FileExplorerProgramEXE, $" /select,\"{path}\"");
var psi = new ProcessStartInfo
{
FileName = filePath,
UseShellExecute = true,
WorkingDirectory = workingDir,
Verb = asAdmin ? "runas" : string.Empty
};
try
{
if (FileExists(filePath))
Process.Start(psi);
}
catch (Exception)
{
#if DEBUG
throw;
#else
MessageBox.Show(string.Format("Unable to open the path {0}, please check if it exists", filePath));
#endif
}
}
///<summary>
@ -174,22 +232,16 @@ namespace Flow.Launcher.Plugin.SharedCommands
///</summary>
public static string GetPreviousExistingDirectory(Func<string, bool> locationExists, string path)
{
var previousDirectoryPath = "";
var index = path.LastIndexOf('\\');
if (index > 0 && index < (path.Length - 1))
{
previousDirectoryPath = path.Substring(0, index + 1);
if (!locationExists(previousDirectoryPath))
{
return "";
}
string previousDirectoryPath = path.Substring(0, index + 1);
return locationExists(previousDirectoryPath) ? previousDirectoryPath : "";
}
else
{
return "";
}
return previousDirectoryPath;
}
///<summary>
@ -209,5 +261,33 @@ namespace Flow.Launcher.Plugin.SharedCommands
return path;
}
/// <summary>
/// Returns if <paramref name="parentPath"/> contains <paramref name="subPath"/>.
/// From https://stackoverflow.com/a/66877016
/// </summary>
/// <param name="parentPath">Parent path</param>
/// <param name="subPath">Sub path</param>
/// <param name="allowEqual">If <see langword="true"/>, when <paramref name="parentPath"/> and <paramref name="subPath"/> are equal, returns <see langword="true"/></param>
/// <returns></returns>
public static bool PathContains(string parentPath, string subPath, bool allowEqual = false)
{
var rel = Path.GetRelativePath(parentPath.EnsureTrailingSlash(), subPath);
return (rel != "." || allowEqual)
&& rel != ".."
&& !rel.StartsWith("../")
&& !rel.StartsWith(@"..\")
&& !Path.IsPathRooted(rel);
}
/// <summary>
/// Returns path ended with "\"
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static string EnsureTrailingSlash(this string path)
{
return path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
}
}
}

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

@ -0,0 +1,53 @@
using Flow.Launcher.Plugin.SharedCommands;
using NUnit.Framework;
namespace Flow.Launcher.Test
{
[TestFixture]
public class FilesFoldersTest
{
// Testcases from https://stackoverflow.com/a/31941905/20703207
// Disk
[TestCase(@"c:", @"c:\foo", true)]
[TestCase(@"c:\", @"c:\foo", true)]
// Slash
[TestCase(@"c:\foo\bar\", @"c:\foo\", false)]
[TestCase(@"c:\foo\bar", @"c:\foo\", false)]
[TestCase(@"c:\foo", @"c:\foo\bar", true)]
[TestCase(@"c:\foo\", @"c:\foo\bar", true)]
// File
[TestCase(@"c:\foo", @"c:\foo\a.txt", true)]
[TestCase(@"c:\foo", @"c:/foo/a.txt", true)]
[TestCase(@"c:\FOO\a.txt", @"c:\foo", false)]
[TestCase(@"c:\foo\a.txt", @"c:\foo\", false)]
[TestCase(@"c:\foobar\a.txt", @"c:\foo", false)]
[TestCase(@"c:\foobar\a.txt", @"c:\foo\", false)]
[TestCase(@"c:\foo\", @"c:\foo.txt", false)]
// Prefix
[TestCase(@"c:\foo", @"c:\foobar", false)]
[TestCase(@"C:\Program", @"C:\Program Files\", false)]
[TestCase(@"c:\foobar", @"c:\foo\a.txt", false)]
[TestCase(@"c:\foobar\", @"c:\foo\a.txt", false)]
// Edge case
[TestCase(@"c:\foo", @"c:\foo\..\bar\baz", false)]
[TestCase(@"c:\bar", @"c:\foo\..\bar\baz", true)]
[TestCase(@"c:\barr", @"c:\foo\..\bar\baz", false)]
// Equality
[TestCase(@"c:\foo", @"c:\foo", false)]
[TestCase(@"c:\foo\", @"c:\foo", false)]
[TestCase(@"c:\foo", @"c:\foo\", false)]
public void GivenTwoPaths_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult)
{
Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path));
}
[TestCase(@"c:\foo", @"c:\foo", true)]
[TestCase(@"c:\foo\", @"c:\foo", true)]
[TestCase(@"c:\foo", @"c:\foo\", true)]
public void GivenTwoPathsAreTheSame_WhenCheckPathContains_ThenShouldBeTrue(string parentPath, string path, bool expectedResult)
{
Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, true));
}
}
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
<ProjectGuid>{FF742965-9A80-41A5-B042-D6C7D3A21708}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
@ -48,13 +48,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0">
<PackageReference Include="Moq" Version="4.18.4" />
<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>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
</ItemGroup>
</Project>

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,8 +7,11 @@ using Flow.Launcher.Plugin.SharedCommands;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using static Flow.Launcher.Plugin.Explorer.Search.SearchManager;
namespace Flow.Launcher.Test.Plugins
{
@ -19,10 +22,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 +35,11 @@ namespace Flow.Launcher.Test.Plugins
{
new Result
{
Title="Result 1"
Title = "Result 1"
},
new Result
{
Title="Result 2"
Title = "Result 2"
}
};
}
@ -44,15 +48,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 +62,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 +71,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 +141,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 +149,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 +159,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,7 +170,10 @@ 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());
@ -250,6 +195,7 @@ namespace Flow.Launcher.Test.Plugins
[TestCase(@"c:\>*", true)]
[TestCase(@"c:\>", true)]
[TestCase(@"c:\SomeLocation\SomeOtherLocation\>", true)]
[TestCase(@"c:\SomeLocation\SomeOtherLocation", true)]
public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult)
{
// When, Given
@ -301,24 +247,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 +270,194 @@ 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);
}
[TestCase(@"c:\foo", @"c:\foo", true)]
[TestCase(@"C:\Foo\", @"c:\foo\", true)]
[TestCase(@"c:\foo", @"c:\foo\", false)]
public void GivenTwoPaths_WhenCompared_ThenShouldBeExpectedSameOrDifferent(string path1, string path2, bool expectedResult)
{
// Given
var comparator = PathEqualityComparator.Instance;
var result1 = new Result
{
Title = Path.GetFileName(path1),
SubTitle = path1
};
var result2 = new Result
{
Title = Path.GetFileName(path2),
SubTitle = path2
};
// When, Then
Assert.AreEqual(expectedResult, comparator.Equals(result1, result2));
}
[TestCase(@"c:\foo\", @"c:\foo\")]
[TestCase(@"C:\Foo\", @"c:\foo\")]
public void GivenTwoPaths_WhenComparedHasCode_ThenShouldBeSame(string path1, string path2)
{
// Given
var comparator = PathEqualityComparator.Instance;
var result1 = new Result
{
Title = Path.GetFileName(path1),
SubTitle = path1
};
var result2 = new Result
{
Title = Path.GetFileName(path2),
SubTitle = path2
};
var hash1 = comparator.GetHashCode(result1);
var hash2 = comparator.GetHashCode(result2);
// When, Then
Assert.IsTrue(hash1 == hash2);
}
[TestCase(@"%appdata%", true)]
[TestCase(@"%appdata%\123", true)]
[TestCase(@"c:\foo %appdata%\", false)]
[TestCase(@"c:\users\%USERNAME%\downloads", true)]
[TestCase(@"c:\downloads", false)]
[TestCase(@"%", false)]
[TestCase(@"%%", false)]
[TestCase(@"%bla%blabla%", false)]
public void GivenPath_WhenHavingEnvironmentVariableOrNot_ThenShouldBeExpected(string path, bool expectedResult)
{
// When
var result = EnvironmentVariables.HasEnvironmentVar(path);
// 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();
@ -48,7 +46,7 @@ namespace Flow.Launcher.Test.Plugins
foreach (var result in results)
{
Assert.IsNotNull(result);
Assert.IsNotNull(result.Action);
Assert.IsNotNull(result.AsyncAction);
Assert.IsNotNull(result.Title);
}
@ -76,7 +74,7 @@ namespace Flow.Launcher.Test.Plugins
[TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))]
public async Task GivenModel_WhenSerializeWithDifferentNamingPolicy_ThenExpectSameResult_Async(JsonRPCQueryResponseModel reference)
{
var camelText = JsonSerializer.Serialize(reference, new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
var camelText = JsonSerializer.Serialize(reference, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
var pascalText = JsonSerializer.Serialize(reference);
@ -92,7 +90,7 @@ namespace Flow.Launcher.Test.Plugins
Assert.AreEqual(result1, referenceResult);
Assert.IsNotNull(result1);
Assert.IsNotNull(result1.Action);
Assert.IsNotNull(result1.AsyncAction);
}
}

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

@ -17,7 +17,7 @@ namespace Flow.Launcher.Test
Query q = QueryBuilder.Build("> file.txt file2 file3", nonGlobalPlugins);
Assert.AreEqual("file.txt file2 file3", q.Search);
Assert.AreEqual("file.txt file2 file3", q.Search);
Assert.AreEqual(">", q.ActionKeyword);
}
@ -31,7 +31,7 @@ namespace Flow.Launcher.Test
Query q = QueryBuilder.Build("> file.txt file2 file3", nonGlobalPlugins);
Assert.AreEqual("> file.txt file2 file3", q.Search);
Assert.AreEqual("> file.txt file2 file3", q.Search);
}
[Test]

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)
@ -44,6 +37,7 @@ namespace Flow.Launcher
var oldActionKeyword = plugin.Metadata.ActionKeywords[0];
var newActionKeyword = tbAction.Text.Trim();
newActionKeyword = newActionKeyword.Length > 0 ? newActionKeyword : "*";
if (!PluginViewModel.IsActionKeywordRegistered(newActionKeyword))
{
pluginViewModel.ChangeActionKeyword(newActionKeyword, oldActionKeyword);

View file

@ -1,11 +1,13 @@
using System;
using System;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
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 +63,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,21 +78,21 @@ 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()}");
Current.MainWindow = window;
Current.MainWindow.Title = Constant.FlowLauncher;
HotKeyMapper.Initialize(_mainVM);
// happlebao todo temp fix for instance code logic
// todo temp fix for instance code logic
// load plugin before change language, because plugin language also needs be changed
InternationalizationManager.Instance.Settings = _settings;
InternationalizationManager.Instance.ChangeLanguage(_settings.Language);
// main windows needs initialized before theme change because of blur settigns
// main windows needs initialized before theme change because of blur settings
ThemeManager.Instance.Settings = _settings;
ThemeManager.Instance.ChangeTheme(_settings.Theme);
@ -104,14 +108,22 @@ namespace Flow.Launcher
});
}
private void AutoStartup()
{
if (_settings.StartFlowLauncherOnSystemStartup)
// we try to enable auto-startup on first launch, or reenable if it was removed
// but the user still has the setting set
if (_settings.StartFlowLauncherOnSystemStartup && !Helper.AutoStartup.IsEnabled)
{
if (!SettingWindow.StartupSet())
try
{
SettingWindow.SetStartup();
Helper.AutoStartup.Enable();
}
catch (Exception e)
{
// but if it fails (permissions, etc) then don't keep retrying
// this also gives the user a visual indication in the Settings widget
_settings.StartFlowLauncherOnSystemStartup = false;
Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"), e.Message);
}
}
}
@ -119,20 +131,17 @@ namespace Flow.Launcher
//[Conditional("RELEASE")]
private void AutoUpdates()
{
Task.Run(async () =>
_ = Task.Run(async () =>
{
if (_settings.AutoUpdates)
{
// check udpate every 5 hours
var timer = new Timer(1000 * 60 * 60 * 5);
timer.Elapsed += async (s, e) =>
{
await _updater.UpdateAppAsync(API);
};
timer.Start();
// check updates on startup
// check update every 5 hours
var timer = new PeriodicTimer(TimeSpan.FromHours(5));
await _updater.UpdateAppAsync(API);
while (await timer.WaitForNextTickAsync())
// check updates on startup
await _updater.UpdateAppAsync(API);
}
});
}
@ -175,7 +184,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

@ -0,0 +1,25 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Flow.Launcher.Converters
{
public class DiameterToCenterPointConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is double d)
{
return new Point(d / 2, d / 2);
}
return new Point(0, 0);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View file

@ -0,0 +1,27 @@
using System;
using System.Globalization;
using System.Windows.Data;
using Windows.Devices.PointOfService;
namespace Flow.Launcher.Converters
{
public class IconRadiusConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length != 2)
throw new ArgumentException("IconRadiusConverter must have 2 parameters");
return values[1] switch
{
true => (double)values[0] / 2,
false => (double)values[0],
_ => throw new ArgumentException("The second argument should be boolean", nameof(values))
};
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

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();
}
}
}

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