Compare commits
34 Commits
0.13.2
...
macosintel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9a54c7e68 | ||
|
|
292362135d | ||
|
|
aae5434451 | ||
|
|
2390530d92 | ||
|
|
9ab3ff75b0 | ||
|
|
d25c4e8c81 | ||
|
|
4a7efa27c8 | ||
|
|
cec0b4b4f6 | ||
|
|
a1922c50c1 | ||
|
|
870120bc13 | ||
|
|
f38cf73485 | ||
|
|
9fd193e676 | ||
|
|
52a4730073 | ||
|
|
bc4879cfe9 | ||
|
|
dd71536316 | ||
|
|
a3343205b1 | ||
|
|
1d7cafc831 | ||
|
|
c75df1c8c1 | ||
|
|
ab25e0a066 | ||
|
|
813d820b85 | ||
|
|
66a56888a4 | ||
|
|
4ef16cea56 | ||
|
|
8c17396285 | ||
|
|
694bf1a74d | ||
|
|
0fc2ffc1ea | ||
|
|
d19dde603d | ||
|
|
23bac0fc76 | ||
|
|
e18907ca91 | ||
|
|
4e21f05031 | ||
|
|
8b224eefe7 | ||
|
|
f261ab4cd9 | ||
|
|
f584fabb50 | ||
|
|
2eed6f4afb | ||
|
|
1c9dc93246 |
57
.github/ISSUE_TEMPLATE/1-bug-report-scripts.md
vendored
@@ -1,57 +0,0 @@
|
||||
---
|
||||
name: Bug report (script bug or unexpected script behavior)
|
||||
about: Create a bug report for generated scripts to help privacy.sexy improve
|
||||
labels: bug
|
||||
title: '[BUG]: '
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for reporting an issue with generated script(s).
|
||||
Please fill in as much of the template below as you're able.
|
||||
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
||||
-->
|
||||
|
||||
### Description
|
||||
|
||||
<!--
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
### OS
|
||||
|
||||
<!--
|
||||
Which OS are you using? What version of OS you were using?
|
||||
On Windows: Open "Start button" > "Settings" > "System" > "About".
|
||||
On macOS: Open "Apple menu (top left corner)" > "About This Mac".
|
||||
On Linux: Open terminal > type: lsb_release -a > copy paste the result.
|
||||
-->
|
||||
|
||||
### Reproduction steps
|
||||
|
||||
<!--
|
||||
How can the bug be recreated?
|
||||
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
|
||||
E.g.
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
-->
|
||||
|
||||
### Scripts
|
||||
|
||||
<!--
|
||||
If applicable, please attach the generated privacy.sexy file instead of copy pasting which becomes too long.
|
||||
-->
|
||||
|
||||
### Screenshots
|
||||
|
||||
<!--
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
-->
|
||||
|
||||
### Additional information
|
||||
|
||||
<!--
|
||||
If applicable, add any other context about the problem here.
|
||||
-->
|
||||
114
.github/ISSUE_TEMPLATE/1-bug-report-scripts.yaml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
name: "Bug Report: Script Issues"
|
||||
description: 🐛 Report issues with generated scripts to enhance privacy.sexy
|
||||
labels: [ 'bug' ]
|
||||
title: '[Bug]: '
|
||||
body:
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||
Please complete as much of the form below as possible.
|
||||
Your feedback is valuable, even if you can't provide all details.
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: >-
|
||||
For example: "After running the cleanup script, music playback stopped functioning."
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: How can the bug be recreated?
|
||||
description: |-
|
||||
This is the most important information in the bug report.
|
||||
Bugs that cannot be reproduced cannot be fixed or verified.
|
||||
placeholder: |-
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Operating system
|
||||
description: |-
|
||||
Please specify your operating system and its version.
|
||||
|
||||
- On Windows: Open "Start button" > "Settings" > "System" > "About".
|
||||
- On macOS: Open "Apple menu (top left corner)" > "About This Mac".
|
||||
- On Linux: Open terminal > type: lsb_release -a > copy paste the result.
|
||||
placeholder: >-
|
||||
For example: "Windows 11 Pro 22H3"
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Script file
|
||||
description: |-
|
||||
If applicable, share the generated privacy.sexy file.
|
||||
|
||||
GitHub may restrict script file attachments.
|
||||
Upload your script file to a service like [GitHub Gist](https://gist.github.com/) and share the link here.
|
||||
|
||||
If you used the desktop version to run the script, it is already stored on your system.
|
||||
See the [documentation to locate it](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#secure-script-executionstorage).
|
||||
|
||||
> **💡 Tip:** You can attach script files by dragging them into this area.
|
||||
placeholder: |-
|
||||
Attach the script, or post GitHub Gist link.
|
||||
For example: https://gist.github.com/privacysexy-forks/6d85ad8ca27acc8c6a5417d4af28c9b6.
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: |-
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
> **💡 Tip:** You can attach screenshots by clicking this area to highlight it and then pasting them or dragging files in.
|
||||
placeholder: Attach screenshots here or link to image hosting.
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: |-
|
||||
If applicable, add any other context about the problem here.
|
||||
|
||||
Helpful information includes:
|
||||
|
||||
- Application logs (desktop version only), see: [how to find application logs](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#logging).
|
||||
- Terminal output
|
||||
- Proposed solutions
|
||||
- Other related context such as related issues, software behavior, etc.
|
||||
|
||||
> **💡 Tip:** You can attach log files by dragging them into this area.
|
||||
placeholder: >-
|
||||
For example: "Here are the logs I get from the privacy.sexy 0.13.2 desktop application: ..."
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
---
|
||||
|
||||
**✉️ A friendly note from the maintainer:**
|
||||
|
||||
> [!NOTE]
|
||||
> We are a small open-source project with a small community.
|
||||
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||
> But your issue will eventually get attention regardless.
|
||||
> <p align="right">@undergroundwires</p>
|
||||
|
||||
---
|
||||
104
.github/ISSUE_TEMPLATE/2-bug-report-general.yaml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: "Bug Report: General"
|
||||
description: 🐛 Report general issues to enhance privacy.sexy
|
||||
labels: [ 'bug' ]
|
||||
title: '[Bug]: '
|
||||
body:
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||
Please complete as much of the form below as possible.
|
||||
Your feedback is valuable, even if you can't provide all details.
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Provide a clear and concise description of the issue.
|
||||
placeholder: >-
|
||||
For example: "I cannot select any scripts."
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: |-
|
||||
This is the most important information in the bug report.
|
||||
Bugs that cannot be reproduced cannot be fixed or verified.
|
||||
placeholder: |-
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: Describe what you expected to happen when the error occurred.
|
||||
placeholder: >-
|
||||
For example: "I expected the settings menu to open smoothly without crashing.".
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: |-
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
> **💡 Tip:** You can attach screenshots by clicking this area to highlight it and then pasting them or dragging files in.
|
||||
placeholder: >-
|
||||
Attach screenshots here or link to image hosting.
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: privacy.sexy environment details
|
||||
description: |-
|
||||
If applicable, mention how you were using privacy.sexy when the bug occurred:
|
||||
|
||||
- Web (on which operating system and browser?)
|
||||
- Or desktop (Windows, macOS, or Linux?)
|
||||
placeholder: >-
|
||||
For example: "The web version on Edge browser on Windows 11 23H2."
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: |-
|
||||
If applicable, add any other context about the problem here.
|
||||
|
||||
Helpful information includes:
|
||||
|
||||
- Application logs (desktop version only), see: [how to find application logs](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#logging).
|
||||
- Terminal output
|
||||
- Proposed solutions
|
||||
- Other related context such as related issues, software behavior, etc.
|
||||
|
||||
> **💡 Tip:** You can attach log files by dragging them into this area.
|
||||
placeholder: >-
|
||||
For example: "Here are the logs I get from the privacy.sexy 0.13.2 desktop application: ..."
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
---
|
||||
|
||||
**✉️ A friendly note from the maintainer:**
|
||||
|
||||
> [!NOTE]
|
||||
> We are a small open-source project with a small community.
|
||||
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||
> But your issue will eventually get attention regardless.
|
||||
> <p align="right">@undergroundwires</p>
|
||||
|
||||
---
|
||||
55
.github/ISSUE_TEMPLATE/2-bug-report-generic.md
vendored
@@ -1,55 +0,0 @@
|
||||
---
|
||||
name: Bug report (unrelated to generated scripts)
|
||||
about: Create a bug report that's not related to generated scripts to help privacy.sexy improve
|
||||
labels: bug
|
||||
title: '[BUG]: '
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for reporting an issue.
|
||||
Please fill in as much of the template below as you're able.
|
||||
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
||||
-->
|
||||
|
||||
### Description
|
||||
|
||||
<!--
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
### Reproduction steps
|
||||
|
||||
<!--
|
||||
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
-->
|
||||
|
||||
### Expected behavior
|
||||
|
||||
<!--
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
### Screenshots
|
||||
|
||||
<!--
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
-->
|
||||
|
||||
### Distribution
|
||||
|
||||
<!--
|
||||
If applicable, mention how you were using privacy.sexy when the bug was encountered:
|
||||
- Web (on Desktop or mobile?)
|
||||
- Or desktop (Windows, macOS or Linux?)
|
||||
-->
|
||||
|
||||
### Additional context
|
||||
|
||||
<!--
|
||||
If applicable, add any other context about the problem here.
|
||||
-->
|
||||
36
.github/ISSUE_TEMPLATE/3-feature-request.md
vendored
@@ -1,36 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for privacy.sexy
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for suggesting an idea to improve privacy better 🤗.
|
||||
Please fill in as much of the template below as you're able.
|
||||
-->
|
||||
|
||||
### Problem description
|
||||
|
||||
<!--
|
||||
What are we trying to solve?
|
||||
Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
||||
E.g. I'm always frustrated when [...]
|
||||
-->
|
||||
|
||||
### Proposed solution
|
||||
|
||||
<!--
|
||||
Describe the solution you'd like in a clear and concise manner.
|
||||
-->
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
<!--
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
-->
|
||||
|
||||
### Additional information
|
||||
|
||||
<!--
|
||||
If applicable, add any other context or screenshots about the feature request here.
|
||||
-->
|
||||
73
.github/ISSUE_TEMPLATE/3-suggestion-feature.yaml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: "Suggestion: Feature"
|
||||
description: 💡 Suggest new ideas to enhance privacy.sexy
|
||||
labels: [ 'enhancement' ]
|
||||
title: '[Feature]: '
|
||||
body:
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||
Please complete as much of the form below as possible.
|
||||
Your feedback is valuable, even if you can't provide all details.
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Problem statement
|
||||
description: |-
|
||||
What are we trying to solve?
|
||||
|
||||
Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
||||
placeholder: >-
|
||||
For example: "Every time I use the app, I struggle with..."
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: |-
|
||||
Describe the solution you'd like in a clear and concise manner.
|
||||
placeholder: >-
|
||||
For example: "It would be great if the app could..."
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: |-
|
||||
Have you considered any alternative solutions or features?
|
||||
Different perspectives can inspire new ideas.
|
||||
placeholder: >-
|
||||
For example: "We could also solve it by...".
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: |-
|
||||
If applicable, add any other context or screenshots about the feature request here.
|
||||
|
||||
> **💡 Tip:** You can attach files or screenshots by dragging them into this area.
|
||||
placeholder: >-
|
||||
For example: "Challenges can be ..., but I'm unsure about ..., here is some documentation about it: ..."
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
---
|
||||
|
||||
**✉️ A friendly note from the maintainer:**
|
||||
|
||||
> [!NOTE]
|
||||
> We are a small open-source project with a small community.
|
||||
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||
> But your issue will eventually get attention regardless.
|
||||
> <p align="right">@undergroundwires</p>
|
||||
|
||||
---
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
name: New script suggestion
|
||||
about: Suggest a new script for privacy.sexy
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for contributing to privacy.sexy! 🌟
|
||||
For guidance, see our script guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md.
|
||||
Consider submitting a PR for faster implementation: https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md#extend-scripts.
|
||||
-->
|
||||
|
||||
### Operating system
|
||||
|
||||
<!--
|
||||
Specify the OS: Windows, macOS, or Linux.
|
||||
-->
|
||||
|
||||
### Name
|
||||
|
||||
<!--
|
||||
Suggest a name for the script.
|
||||
Naming conventions: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#name.
|
||||
-->
|
||||
|
||||
### Code
|
||||
|
||||
<!--
|
||||
Provide or explain the code to execute when the script runs.
|
||||
Code guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#code.
|
||||
-->
|
||||
|
||||
### Revert code
|
||||
|
||||
<!--
|
||||
Include code to revert changes to the default state.
|
||||
Leave blank for non-reversible scripts.
|
||||
-->
|
||||
|
||||
### Category
|
||||
|
||||
<!--
|
||||
Suggest a category for the script.
|
||||
If unsure, leave blank for maintainers to decide.
|
||||
-->
|
||||
|
||||
### Recommendation level
|
||||
|
||||
<!--
|
||||
Suggest a recommendation level: STANDARD (non-breaking), STRICT (limits functionality), or NONE (for advanced users).
|
||||
If unsure, leave blank for maintainers to decide.
|
||||
-->
|
||||
|
||||
### Documentation/References
|
||||
|
||||
<!--
|
||||
Provide any relevant documentation or references.
|
||||
Prefer high-quality sources such as vendor documentation.
|
||||
Documentation guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#documentation.
|
||||
-->
|
||||
133
.github/ISSUE_TEMPLATE/4-suggestion-new-script.yaml
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
name: "Suggestion: New Script"
|
||||
description: 💡 Suggest new scripts to enhance privacy.sexy
|
||||
labels: [ 'enhancement' ]
|
||||
title: '[New script]: '
|
||||
body:
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||
Please complete as much of the form below as possible.
|
||||
Your feedback is valuable, even if you can't provide all details.
|
||||
|
||||
For guidance, see our [script guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md).
|
||||
Consider submitting a PR to get your script added more quickly: (see [CONTRIBUTING.md](https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md#extend-scripts))
|
||||
-
|
||||
type: dropdown
|
||||
attributes:
|
||||
label: Operating system
|
||||
description: Which operating system will the new script configure?
|
||||
options:
|
||||
- macOS
|
||||
- Windows
|
||||
- Linux
|
||||
- All of them
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Name of the script
|
||||
description: |-
|
||||
Suggest a name for the script that clearly describes its function.
|
||||
|
||||
See [script naming conventions](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#name) for best practices.
|
||||
placeholder: E.g, "Disable error data submission"
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Documentation/References
|
||||
description: |-
|
||||
Provide any relevant documentation or references.
|
||||
Prefer high-quality sources such as vendor documentation.
|
||||
|
||||
See [documentation guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#documentation) for best practices.
|
||||
placeholder: >-
|
||||
For example: "This script will disable the error data submission, see https://microsoft.com/...".
|
||||
validations:
|
||||
required: true
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Code
|
||||
description: |-
|
||||
If possible, provide or explain the code that the script should execute.
|
||||
|
||||
See [script code guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#code).
|
||||
placeholder: |-
|
||||
For example: "Set registry key like this `reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" /v "AllowTelemetry" /t "REG_DWORD" /d "1"`".
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Revert code
|
||||
description: |-
|
||||
If applicable, provide revert code to restore the changes made by the script.
|
||||
|
||||
The revert code restores changes to their default state before script execution.
|
||||
|
||||
Leave blank for non-reversible scripts.
|
||||
placeholder: |-
|
||||
For example: "Revert to operating system default like this `reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" /v "AllowTelemetry" /t "REG_DWORD" /d "0"`".
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Suggested category
|
||||
description: |-
|
||||
Suggest a category for the script.
|
||||
|
||||
If unsure, leave blank for maintainers to decide.
|
||||
placeholder: >-
|
||||
For example: "Privacy Cleanup > Clear system logs"
|
||||
-
|
||||
type: dropdown
|
||||
attributes:
|
||||
label: Recommendation level
|
||||
description: |-
|
||||
Suggest a recommendation level for the script:
|
||||
|
||||
- **Standard**: Recommended for most users without side-effects.
|
||||
- **Strict**: Provides improved privacy at the cost of some functionality.
|
||||
- **None**: For advanced users or specific needs.
|
||||
|
||||
If unsure, leave blank for maintainers to decide.
|
||||
options:
|
||||
- Standard
|
||||
- Strict
|
||||
- None (do not recommend)
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: |-
|
||||
If applicable, add any other context or screenshots about the script request here.
|
||||
|
||||
> **💡 Tip:** You can attach additional documents or screenshots by dragging them into this area or pasting directly.
|
||||
placeholder: >-
|
||||
For example: "Challenges can be ..., but I am unsure about ..."
|
||||
validations:
|
||||
required: false
|
||||
-
|
||||
type: markdown
|
||||
attributes:
|
||||
value: |-
|
||||
---
|
||||
|
||||
**✉️ A friendly note from the maintainer:**
|
||||
|
||||
> [!NOTE]
|
||||
> We are a small open-source project with a small community.
|
||||
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||
> But your issue will eventually get attention regardless.
|
||||
> <p align="right">@undergroundwires</p>
|
||||
|
||||
---
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +1,7 @@
|
||||
blank_issues_enabled: true
|
||||
# This file must be named `config.yml`. GitHub does not recognize the file if it is named `config.yaml`.
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Donate
|
||||
url: https://undergroundwires.dev/donate/
|
||||
about: ❤️ Donate to support the free software you love to keep it alive.
|
||||
# A separate link for reporting vulnerabilities is not included here because GitHub generates it automatically.
|
||||
|
||||
2
.github/actions/setup-node/action.yml
vendored
@@ -6,4 +6,4 @@ runs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
check-latest: true
|
||||
# check-latest: true # Newest versions can potentially have undiscovered bugs or regressions
|
||||
|
||||
23
.github/workflows/checks.build.yaml
vendored
@@ -72,20 +72,35 @@ jobs:
|
||||
build-docker:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu ] # Windows runners do not support Linux containers
|
||||
os:
|
||||
- macos-13 # Downgraded due to lack of nested virtualization support in ARM-based runners (See: actions/runner-images#9460, actions/runner-images#9741, abiosoft/colima#1023)
|
||||
- ubuntu-latest
|
||||
# - windows-latest # Windows runners do not support Linux containers
|
||||
fail-fast: false # Allows to see results from other combinations
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Install Docker on macOS
|
||||
if: matrix.os == 'macos' # macOS runner is missing Docker
|
||||
if: contains(matrix.os, 'macos') # macOS runner is missing Docker
|
||||
run: |-
|
||||
# Verify Intel-based macOS
|
||||
arch=$(uname -m)
|
||||
case "$arch" in
|
||||
i386|x86_64)
|
||||
echo "Supported architecture: $arch"
|
||||
;;
|
||||
*)
|
||||
>&2 echo 'The macOS is not running on a supported Intel architecture. Virtualization is not supported.'
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
# Install Docker
|
||||
brew install docker
|
||||
# Docker on macOS misses daemon due to licensing, so install colima as runtime
|
||||
# Docker on macOS does not include the Docker daemon due to licensing issues.
|
||||
# Install Colima to use as the Docker runtime.
|
||||
brew install colima
|
||||
# Start the daemon
|
||||
colima start
|
||||
|
||||
@@ -9,9 +9,13 @@ jobs:
|
||||
run-check:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
os:
|
||||
- macos-latest # Latest Apple silicon (ARM64)
|
||||
- macos-12 # Latest Intel-based (x86-64)
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
fail-fast: false # Allows to see results from other combinations
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
@@ -24,7 +28,7 @@ jobs:
|
||||
uses: ./.github/actions/npm-install-dependencies
|
||||
-
|
||||
name: Configure Ubuntu
|
||||
if: matrix.os == 'ubuntu'
|
||||
if: contains(matrix.os, 'ubuntu') # macOS runner is missing Docker
|
||||
shell: bash
|
||||
run: |-
|
||||
sudo apt update
|
||||
|
||||
50
.github/workflows/checks.quality.yaml
vendored
@@ -1,10 +1,10 @@
|
||||
name: quality-checks
|
||||
name: checks.quality
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
strategy:
|
||||
matrix:
|
||||
lint-command:
|
||||
@@ -28,3 +28,49 @@ jobs:
|
||||
-
|
||||
name: Lint
|
||||
run: ${{ matrix.lint-command }}
|
||||
|
||||
todo-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Scan latest commit for TODO comments
|
||||
shell: bash
|
||||
run: |-
|
||||
readonly todo_comment_search_pattern='TODO'':' # Define search pattern in parts to prevent IDE from flagging this script line as a TODO item
|
||||
if git grep "$todo_comment_search_pattern" HEAD; then
|
||||
echo 'TODO comments found in the latest commit.'
|
||||
exit 1
|
||||
else
|
||||
echo 'No TODO comments found in the latest commit.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pylint:
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos, ubuntu, windows ]
|
||||
fail-fast: false # Still interested to see results from other combinations
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
-
|
||||
name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pylint
|
||||
-
|
||||
name: Analyzing the code with pylint
|
||||
run: npm run lint:pylint
|
||||
|
||||
32
.github/workflows/checks.scripts.yaml
vendored
@@ -15,6 +15,10 @@ jobs:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Install ImageMagick on macOS
|
||||
if: matrix.os == 'macos'
|
||||
run: brew install imagemagick
|
||||
-
|
||||
name: Setup node
|
||||
uses: ./.github/actions/setup-node
|
||||
@@ -53,3 +57,31 @@ jobs:
|
||||
-
|
||||
name: Run install-deps
|
||||
run: ${{ matrix.install-command }}
|
||||
|
||||
configure-vscode:
|
||||
runs-on: ${{ matrix.os.name }}-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- name: macos
|
||||
install-vscode-command: brew install --cask visual-studio-code
|
||||
- name: ubuntu
|
||||
install-vscode-command: sudo snap install code --classic
|
||||
- name: windows
|
||||
install-vscode-command: choco install vscode
|
||||
fail-fast: false # Still interested to see results from other combinations
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
-
|
||||
name: Install VSCode
|
||||
run: ${{ matrix.os.install-vscode-command }}
|
||||
-
|
||||
name: Configure VSCode
|
||||
run: python3 ./scripts/configure_vscode.py
|
||||
|
||||
53
CHANGELOG.md
@@ -1,5 +1,58 @@
|
||||
# Changelog
|
||||
|
||||
## 0.13.3 (2024-05-11)
|
||||
|
||||
* win: organize and document network disablement | [2eed6f4](https://github.com/undergroundwires/privacy.sexy/commit/2eed6f4afb6cf85fdc1d6acb808f82405a35cafd)
|
||||
* win: improve disabling SMBv1 protocol | [f584fab](https://github.com/undergroundwires/privacy.sexy/commit/f584fabb50c7de70ba43751d721af94d8fa2fa8a)
|
||||
* win: improve disabling insecure renegotiations | [f261ab4](https://github.com/undergroundwires/privacy.sexy/commit/f261ab4cd9a53e31325e5c6da9129542971fe84b)
|
||||
* win: doc, improve, encourage cipher disabling | [8b224ee](https://github.com/undergroundwires/privacy.sexy/commit/8b224eefe71be6a556a1085d8fe20dbd4b889430)
|
||||
* ci/cd: add check for TODO comments | [4e21f05](https://github.com/undergroundwires/privacy.sexy/commit/4e21f05031d6cc90cda684bd598bec4735f8103b)
|
||||
* win: improve 'Snipping Tool' removal #343 | [e18907c](https://github.com/undergroundwires/privacy.sexy/commit/e18907ca91e483255b44d14d7d923d7eef92afbd)
|
||||
* ci/cd: lint Python scripts using `pylint` | [23bac0f](https://github.com/undergroundwires/privacy.sexy/commit/23bac0fc76ad697abb34f3fb327df5cdeb40286a)
|
||||
* win: improve disabling insecure hashes #131 | [d19dde6](https://github.com/undergroundwires/privacy.sexy/commit/d19dde603ddac47022ee2e0ea865d53857560c26)
|
||||
* Add system requirements documentation #134 | [0fc2ffc](https://github.com/undergroundwires/privacy.sexy/commit/0fc2ffc1ea36a9248c6a92da85a29f7b04b33796)
|
||||
* win, linux, mac: fix various typos #349 | [694bf1a](https://github.com/undergroundwires/privacy.sexy/commit/694bf1a74d935531d7cd46891823af1fa58c3c8c)
|
||||
* Fix script cancellation with new dialog on Linux | [8c17396](https://github.com/undergroundwires/privacy.sexy/commit/8c173962857a39dc0c9e5886cb2af4937e6618e7)
|
||||
* win: improve disabling protocols | [4ef16ce](https://github.com/undergroundwires/privacy.sexy/commit/4ef16cea56789120cd041412d86b5577cccf0725)
|
||||
* win: fix Copilot by excluding `r.bing.com` #329 | [66a5688](https://github.com/undergroundwires/privacy.sexy/commit/66a56888a4b3ead1a6bfef0feffa0218535701fe)
|
||||
* Fix blank window on load on desktop version #348 | [813d820](https://github.com/undergroundwires/privacy.sexy/commit/813d820b85e1b623c50f8e0325ad372bf2f344f9)
|
||||
* Improve desktop icon quality and generation | [ab25e0a](https://github.com/undergroundwires/privacy.sexy/commit/ab25e0a066be14ea979dafd0f80e1091bd5d33f8)
|
||||
* win: improve enabling secure connections #175 | [c75df1c](https://github.com/undergroundwires/privacy.sexy/commit/c75df1c8c1151b64cbf014383dea0b748a8c78b3)
|
||||
* Fix VSCode script issues with added CI/CD tests | [1d7cafc](https://github.com/undergroundwires/privacy.sexy/commit/1d7cafc831dcc339a10646794410dad7096bfe60)
|
||||
* Fix win execution with whitespace in username #351 | [a334320](https://github.com/undergroundwires/privacy.sexy/commit/a3343205b1196d5a81fd3cee2ae661ce871a7bef)
|
||||
* Fix misaligned tooltip positions in modal dialogs | [dd71536](https://github.com/undergroundwires/privacy.sexy/commit/dd71536316ec819caeb418b8635d544ac80e58ad)
|
||||
* Fix Chromium scrollbar-induced layout shifts | [bc4879c](https://github.com/undergroundwires/privacy.sexy/commit/bc4879cfe97becac3c54f6b40780a89464d3b772)
|
||||
* ci/cd: remove `check-latest` from `setup-node` | [52a4730](https://github.com/undergroundwires/privacy.sexy/commit/52a4730073b8ebfb2ce9d530b44e4a179f5849fe)
|
||||
* win: categorize and rename network security #131 | [9fd193e](https://github.com/undergroundwires/privacy.sexy/commit/9fd193e676f1f0646898f5130fbfaaf25050b2e3)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.2...0.13.3)
|
||||
|
||||
## 0.13.2 (2024-04-15)
|
||||
|
||||
* Update documentation for `logo-update.js` script | [4a9b430](https://github.com/undergroundwires/privacy.sexy/commit/4a9b430702bc6082426b50ecc3a06362b5720796)
|
||||
* win: improve and document removing Phone apps #279 | [8924337](https://github.com/undergroundwires/privacy.sexy/commit/89243371faa5d6aef5fce52b0d54a442143cdd39)
|
||||
* Fix bottom gap in card expansion panel | [79183d6](https://github.com/undergroundwires/privacy.sexy/commit/79183d64173e588d88bf074d5b50a52a71c2d885)
|
||||
* ci/cd: Fix macOS Docker build reliability issues | [8a5592f](https://github.com/undergroundwires/privacy.sexy/commit/8a5592f92be4366a806afc9eee9135696a1dd993)
|
||||
* ci/cd: fix IPv6 timeouts with `force-ipv4` action | [52fadcd](https://github.com/undergroundwires/privacy.sexy/commit/52fadcd6177ed06216be9c67dad57192ae02a4f9)
|
||||
* ci/cd: bump Node.js environment to 20.x | [59decd1](https://github.com/undergroundwires/privacy.sexy/commit/59decd17e273bada1493eaa855c43cbabf90308f)
|
||||
* ci/cd: trigger URL checks more, and limit amount | [4fb6302](https://github.com/undergroundwires/privacy.sexy/commit/4fb6302c67f2a3fedff419e8c22872593cf800ef)
|
||||
* Fix overflow in tree node content on small screens | [557cea3](https://github.com/undergroundwires/privacy.sexy/commit/557cea3f4866dc33236874f5fe4d2d69ee963dae)
|
||||
* Fix horizontal layout shift after script selection | [bc7e1fa](https://github.com/undergroundwires/privacy.sexy/commit/bc7e1faa1c3f2b61bf2046fdd6d6a4141b484662)
|
||||
* Fix card header expansion glitch on card collapse | [5d940b5](https://github.com/undergroundwires/privacy.sexy/commit/5d940b57ef2a4c219932cd15201401f8550cfb41)
|
||||
* Ignore `ResizeObserver` errors in Cypress tests | [4472c28](https://github.com/undergroundwires/privacy.sexy/commit/4472c2852e4b87083bda7979471ab9f377d17a01)
|
||||
* win: improve and document secret key scripts | [49f22f0](https://github.com/undergroundwires/privacy.sexy/commit/49f22f048f39e7388633c488b5fe59101b831984)
|
||||
* Fix card arrow not being animated in sync | [7b546c5](https://github.com/undergroundwires/privacy.sexy/commit/7b546c567c4683a37fe94595362f4c2bf92ffd59)
|
||||
* win: improve Windows feature disablement scripts | [b68711e](https://github.com/undergroundwires/privacy.sexy/commit/b68711ef88982c0ee2b1d41b4452e899821adc64)
|
||||
* Fix top script menu overflow on small screens | [b7a20d9](https://github.com/undergroundwires/privacy.sexy/commit/b7a20d9d41ea8bcefdd553b87641f3c22b4cde97)
|
||||
* win: fix Visual Studio remote analysis script #327 | [4142d08](https://github.com/undergroundwires/privacy.sexy/commit/4142d084f64a3b540487ff68b28032977d12006d)
|
||||
* win: improve firewall docs /w `winget` impact #142 | [ffd647d](https://github.com/undergroundwires/privacy.sexy/commit/ffd647d1529375474b81900cc7bee4c32fbf861f)
|
||||
* Centralize and use global spacing variables | [ae17200](https://github.com/undergroundwires/privacy.sexy/commit/ae172000a64416e5a3e2b2e32b7846f039f445f0)
|
||||
* win: improve service revert and docs | [b87b7aa](https://github.com/undergroundwires/privacy.sexy/commit/b87b7aac7d118a23a0d1bfb881e385347de4adb7)
|
||||
* Bump dependencies to latest, hold ESLint | [f3571ab](https://github.com/undergroundwires/privacy.sexy/commit/f3571abeafdbe1e6d152958fab26de91a9c08bc3)
|
||||
* Fix inability to tap outside modal on mobile | [cb144ae](https://github.com/undergroundwires/privacy.sexy/commit/cb144ae47273deeb7058d4b1380e480ebccdaf81)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.1...0.13.2)
|
||||
|
||||
## 0.13.1 (2024-03-22)
|
||||
|
||||
* ci/cd: Fix cross-platform git command compability | [255c51c](https://github.com/undergroundwires/privacy.sexy/commit/255c51c8a0524d3ea8a3b16ffc1b178650525010)
|
||||
|
||||
11
README.md
@@ -60,8 +60,8 @@
|
||||
<br />
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
alt="Quality checks status"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg"
|
||||
alt="Status of quality checks"
|
||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.quality/badge.svg"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml" target="_blank" rel="noopener noreferrer">
|
||||
@@ -122,9 +122,12 @@
|
||||
## Get started
|
||||
|
||||
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.1/privacy.sexy-Setup-0.13.1.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.1/privacy.sexy-0.13.1.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.1/privacy.sexy-0.13.1.AppImage). For more options, see [here](#additional-install-options).
|
||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.3/privacy.sexy-Setup-0.13.3.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.3/privacy.sexy-0.13.3.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.3/privacy.sexy-0.13.3.AppImage). For more options, see [here](#additional-install-options).
|
||||
|
||||
For a detailed comparison of features between the desktop and web versions of privacy.sexy, see [Desktop vs. Web Features](./docs/desktop-vs-web-features.md).
|
||||
See also:
|
||||
|
||||
- [Desktop vs. Web Features](./docs/desktop/desktop-vs-web-features.md): Differences and unique aspects of desktop and web versions.
|
||||
- [System Requirements](./docs/desktop/system-requirements.md): Hardware and software requirements for the desktop version.
|
||||
|
||||
💡 Regularly applying your configuration with privacy.sexy is recommended, especially after each new release and major operating system updates. Each version updates scripts to enhance stability, privacy, and security.
|
||||
|
||||
|
||||
15
SECURITY.md
@@ -43,10 +43,17 @@ privacy.sexy adopts a defense in depth strategy to protect users on multiple lay
|
||||
elevation of privileges for system modifications with explicit user consent and logs every action taken with high privileges. This
|
||||
approach actively minimizes potential security risks by limiting privileged operations and aligning with the principle of least privilege.
|
||||
- **Secure Script Execution/Storage:**
|
||||
Before executing any script, the desktop application stores a copy to allow antivirus software to perform scans. This safeguards against
|
||||
any unwanted modifications. Furthermore, the application incorporates integrity checks for tamper protection. If the script file differs from
|
||||
the user's selected script, the application will not execute or save the script, ensuring the processing of authentic scripts.
|
||||
Recognizing that some users prefer not to keep these records, privacy.sexy provides specialized scripts for deletion of these scripts.
|
||||
- **Antivirus scans:**
|
||||
Before executing any script, the desktop application stores a copy to allow antivirus software to perform scans.
|
||||
This step allows confirming that the scripts are secure and safe to use.
|
||||
- **Tamper protection:**
|
||||
The application incorporates integrity checks for tamper protection.
|
||||
If the script file differs from the user's selected script, the application will not execute or save the script, ensuring the processing
|
||||
of authentic scripts.
|
||||
This safeguards against any unwanted modifications.
|
||||
- **Clean-up:**
|
||||
Recognizing that some users prefer not to keep these records, privacy.sexy provides specialized scripts for deletion of these scripts.
|
||||
This allows users to maintain their privacy by removing traces of their usage patterns or script preferences.
|
||||
|
||||
### Update Security and Integrity
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# build
|
||||
|
||||
This folder contains files that are used by Electron to serve the desktop version.
|
||||
|
||||
Icons are created from the main logo file and should not be changed manually, see [related documentation](./../img/README.md).
|
||||
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 963 B |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1,6 +1,6 @@
|
||||
# Desktop vs. Web Features
|
||||
|
||||
This table highlights differences between the desktop and web versions of `privacy.sexy`.
|
||||
This table outlines the differences between the desktop and web versions of `privacy.sexy`.
|
||||
|
||||
| Feature | Desktop | Web |
|
||||
| ------- | ------- | --- |
|
||||
@@ -8,10 +8,8 @@ This table highlights differences between the desktop and web versions of `priva
|
||||
| [Offline usage](#offline-usage) | 🟢 Available | 🟡 Partially available |
|
||||
| [Auto-updates](#auto-updates) | 🟢 Available | 🟢 Available |
|
||||
| [Logging](#logging) | 🟢 Available | 🔴 Not available |
|
||||
| [Script execution](#script-execution) | 🟢 Available | 🔴 Not available |
|
||||
| [Error handling](#error-handling) | 🟢 Advanced | 🟡 Limited |
|
||||
| [Native dialogs](#native-dialogs) | 🟢 Available | 🔴 Not available |
|
||||
| [Secure script execution/storage](#secure-script-executionstorage) | 🟢 Available | 🔴 Not available |
|
||||
| [Native dialogs](#native-dialogs) | 🟢 Available | 🔴 Not available |
|
||||
|
||||
## Feature descriptions
|
||||
|
||||
@@ -30,11 +28,11 @@ Desktop version inherently allows offline usage.
|
||||
|
||||
### Auto-updates
|
||||
|
||||
Both the desktop and web versions of privacy.sexy provide timely access to the latest features and security improvements. The updates are automatically deployed from source code, reflecting the latest changes for enhanced security and reliability. For more details, see [CI/CD documentation](./ci-cd.md).
|
||||
Both the desktop and web versions of privacy.sexy provide timely access to the latest features and security improvements. The updates are automatically deployed from source code, reflecting the latest changes for enhanced security and reliability. For more details, see [CI/CD documentation](./../ci-cd.md).
|
||||
|
||||
The desktop version ensures secure delivery through cryptographic signatures and version checks.
|
||||
|
||||
[Security is a top priority](./../SECURITY.md#update-security-and-integrity) at privacy.sexy.
|
||||
[Security is a top priority](./../../SECURITY.md#update-security-and-integrity) at privacy.sexy.
|
||||
|
||||
> **Note for macOS users:** On macOS, the desktop version's auto-update process involves manual steps due to Apple's code signing costs.
|
||||
> Users get notified about updates but might need to complete the installation manually.
|
||||
@@ -53,7 +51,7 @@ Log file locations vary by operating system:
|
||||
|
||||
> 💡 privacy.sexy provides scripts to securely erase these logs.
|
||||
|
||||
### Script execution
|
||||
### Secure script execution/storage
|
||||
|
||||
The desktop version of privacy.sexy enables direct script execution, providing a seamless and integrated experience.
|
||||
This direct execution capability isn't available in the web version due to inherent browser restrictions.
|
||||
@@ -69,31 +67,27 @@ These locations vary based on the operating system:
|
||||
|
||||
> 💡 privacy.sexy provides scripts to securely erase your script execution history.
|
||||
|
||||
### Error handling
|
||||
**Script antivirus scans:**
|
||||
|
||||
The desktop version of privacy.sexy features advanced error handling capabilities.
|
||||
It employs robust and reliable execution strategies, including self-healing mechanisms, and provides guidance and troubleshooting information to resolve issues effectively.
|
||||
In contrast, the web version has more basic error handling due to browser limitations and the nature of web applications.
|
||||
To enhance system protection, the desktop version of privacy.sexy automatically verifies the security of script
|
||||
execution files by reading them back.
|
||||
This process triggers antivirus scans to verify that scripts are safe before the execution.
|
||||
|
||||
### Native dialogs
|
||||
|
||||
The desktop version uses native dialogs, offering more features and reliability compared to the browser's file system dialogs.
|
||||
These native dialogs provide a more integrated and user-friendly experience, aligning with the operating system's standard interface and functionalities.
|
||||
|
||||
### Secure script execution/storage
|
||||
|
||||
**Integrity checks:**
|
||||
**Script integrity checks:**
|
||||
|
||||
The desktop version of privacy.sexy implements robust integrity checks for both script execution and storage.
|
||||
Featuring tamper protection, the application actively verifies the integrity of script files before executing or saving them.
|
||||
If the actual contents of a script file do not align with the expected contents, the application refuses to execute or save the script.
|
||||
This proactive approach ensures only unaltered and verified scripts undergo processing, thereby enhancing both security and reliability.
|
||||
Due to browser constraints, this feature is absent in the web version.
|
||||
|
||||
**Error handling:**
|
||||
|
||||
The desktop version of privacy.sexy features advanced error handling capabilities.
|
||||
In scenarios where script execution or storage encounters failure, the desktop application initiates automated troubleshooting and self-healing processes.
|
||||
It also guides users through potential issues with filesystem or third-party software, such as antivirus interventions.
|
||||
Specifically, the application is capable of identifying when antivirus software blocks or removes a script, providing users with tailored error messages
|
||||
and detailed resolution steps. This level of proactive error handling and user guidance enhances the application's security and reliability,
|
||||
offering a feature not achievable in the web version due to browser limitations.
|
||||
It employs robust and reliable execution strategies, including self-healing mechanisms, and provides guidance and troubleshooting information to resolve issues effectively.
|
||||
This proactive error handling and user guidance enhances the application's security and reliability.
|
||||
|
||||
### Native dialogs
|
||||
|
||||
The desktop version uses native dialogs, offering more features and reliability compared to the browser's file system dialogs.
|
||||
These native dialogs provide a more integrated and user-friendly experience, aligning with the operating system's standard interface and functionalities.
|
||||
36
docs/desktop/system-requirements.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# System Requirements for the Desktop Version
|
||||
|
||||
The following system requirements are the official ones for the desktop version.
|
||||
While we have tested and confirmed these requirements, the application might also work on other
|
||||
systems or configurations that haven't undergone official testing.
|
||||
|
||||
## Windows
|
||||
|
||||
- **Version:** Windows 10 and later.
|
||||
- **Processor:** Intel Pentium 4 or later.
|
||||
- **Architecture:** 64-bit (x86-64), ARM (ARM64).
|
||||
|
||||
> **⚠️ Compatibility Note:**
|
||||
> ARM version is only compatible with Windows 11 and later.
|
||||
> It runs non-natively, leading to slower performance due to emulation [1].
|
||||
|
||||
## macOS
|
||||
|
||||
- **Version:** macOS Catalina (10.15) and later.
|
||||
- **Architecture:** Intel-based (x86-64), Apple silicon (ARM64).
|
||||
|
||||
## Linux
|
||||
|
||||
- **Version:** Ubuntu 18.04 and later, Fedora 32 and later, and Debian 10 and later.
|
||||
- **Processor:** Intel Pentium 4 or later.
|
||||
- **Architecture:** 64-bit (x86-64).
|
||||
|
||||
## References
|
||||
|
||||
System requirements reflect Electron's platform capabilities [2] and Chromium's recommended configurations [3].
|
||||
|
||||
For details on the build process, see [electron-builder configuration file](./../../electron-builder.cjs).
|
||||
|
||||
[1]: https://web.archive.org/web/20240428082726/https://learn.microsoft.com/en-us/windows/arm/add-arm-support#emulation-on-arm-based-devices-for-x86-or-x64-windows-apps "Add support Arm devices to your Windows app | Microsoft Learn | learn.microsoft.com"
|
||||
[2]: https://archive.ph/2024.04.28-082958/https://github.com/electron/electron/blob/main/README.md#platform-support "Platform Support | electron/README.md at main · electron/electron · GitHub | github.com"
|
||||
[3]: https://web.archive.org/web/20240428082945/https://support.google.com/chrome/a/answer/7100626?hl=en "Chrome browser system requirements - Chrome Enterprise and Education Help | support.google.com"
|
||||
@@ -23,8 +23,10 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
||||
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint..
|
||||
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
||||
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
||||
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
||||
- [`/main/` **`index.ts`**](./../src/presentation/electron/main/index.ts): Main entry for Electron, managing application windows and lifecycle events.
|
||||
- [`/preload/` **`index.ts`**](./../src/presentation/electron/preload/index.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
||||
- [**`/shared/`**](./../src/presentation/electron/shared/): Shared logic between different Electron processes.
|
||||
- [**`/build/`**](./../src/presentation/electron/build/): `electron-builder` build resources directory, [README.md](./../src/presentation/electron/build/README.md).
|
||||
- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application.
|
||||
- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications.
|
||||
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.
|
||||
|
||||
@@ -27,6 +27,7 @@ Key attributes of a good script:
|
||||
- `Minimize` over `Limit`, `Reduce`
|
||||
- `Maximize` over `Extend`, `Delay`, `Postpone`, `Prolong`
|
||||
- `Remove` over `Uninstall`
|
||||
- `Improve` over `Increase`
|
||||
- Structure your phrases for clarity, examples:
|
||||
- Prefer `Disable XX telemetry` over `Disable telemetry in XX`
|
||||
- Prefer `Clear XX data` over `Clear data from XX`, or `Clear data of XX`.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
|
||||
const { join } = require('node:path');
|
||||
const { readdirSync } = require('fs');
|
||||
const { join, resolve } = require('node:path');
|
||||
const { readdirSync, existsSync } = require('node:fs');
|
||||
const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
|
||||
|
||||
/**
|
||||
@@ -17,6 +17,7 @@ module.exports = {
|
||||
},
|
||||
directories: {
|
||||
output: electronBundled,
|
||||
buildResources: resolvePathFromProjectRoot('src/presentation/electron/build'),
|
||||
},
|
||||
extraMetadata: {
|
||||
main: findMainEntryFile(
|
||||
@@ -42,7 +43,10 @@ module.exports = {
|
||||
|
||||
// macOS
|
||||
mac: {
|
||||
target: 'dmg',
|
||||
target: {
|
||||
target: 'dmg',
|
||||
arch: 'universal',
|
||||
},
|
||||
},
|
||||
dmg: {
|
||||
artifactName: '${name}-${version}.${ext}',
|
||||
@@ -53,10 +57,18 @@ module.exports = {
|
||||
* Finds by accommodating different JS file extensions and module formats.
|
||||
*/
|
||||
function findMainEntryFile(parentDirectory) {
|
||||
const files = readdirSync(parentDirectory);
|
||||
const absoluteParentDirectory = resolvePathFromProjectRoot(parentDirectory);
|
||||
if (!existsSync(absoluteParentDirectory)) {
|
||||
return null; // Avoid disrupting other processes such `npm install`.
|
||||
}
|
||||
const files = readdirSync(absoluteParentDirectory);
|
||||
const entryFile = files.find((file) => /^index\.(cjs|mjs|js)$/.test(file));
|
||||
if (!entryFile) {
|
||||
throw new Error(`Main entry file not found in ${parentDirectory}.`);
|
||||
throw new Error(`Main entry file not found in ${absoluteParentDirectory}.`);
|
||||
}
|
||||
return join(parentDirectory, entryFile);
|
||||
}
|
||||
|
||||
function resolvePathFromProjectRoot(pathSegment) {
|
||||
return resolve(__dirname, pathSegment);
|
||||
}
|
||||
|
||||
4181
package-lock.json
generated
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.3",
|
||||
"private": true,
|
||||
"slogan": "Privacy is sexy",
|
||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy.",
|
||||
@@ -14,7 +14,7 @@
|
||||
"test:integration": "vitest run --dir tests/integration",
|
||||
"test:cy:run": "start-server-and-test \"vite build && vite preview --port 7070\" http://localhost:7070 \"cypress run --config baseUrl=http://localhost:7070\"",
|
||||
"test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"",
|
||||
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
||||
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml && npm run lint:pylint",
|
||||
"install-deps": "node scripts/npm-install.js",
|
||||
"icons:build": "node scripts/logo-update.js",
|
||||
"check:desktop": "vitest run --dir tests/checks/desktop-runtime-errors --environment node",
|
||||
@@ -29,6 +29,7 @@
|
||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||
"lint:pylint": "pylint **/*.py",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"postuninstall": "electron-builder install-app-deps"
|
||||
},
|
||||
@@ -41,7 +42,7 @@
|
||||
"electron-updater": "^6.1.9",
|
||||
"file-saver": "^2.0.5",
|
||||
"markdown-it": "^14.1.0",
|
||||
"vue": "^3.4.21"
|
||||
"vue": "^3.4.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||
@@ -61,13 +62,11 @@
|
||||
"electron": "^29.3.0",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-icon-builder": "^2.0.1",
|
||||
"electron-vite": "^2.1.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"eslint-plugin-vue": "^9.25.0",
|
||||
"eslint-plugin-vuejs-accessibility": "^2.2.1",
|
||||
"icon-gen": "^4.0.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"markdownlint-cli": "^0.39.0",
|
||||
"postcss": "^8.4.38",
|
||||
@@ -77,7 +76,6 @@
|
||||
"remark-validate-links": "^13.0.1",
|
||||
"sass": "^1.75.0",
|
||||
"start-server-and-test": "^2.0.3",
|
||||
"svgexport": "^0.4.2",
|
||||
"terser": "^5.30.3",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.4.5",
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
"""
|
||||
This script configures project-level VSCode settings in '.vscode/settings.json' for
|
||||
development and installs recommended extensions from '.vscode/extensions.json'.
|
||||
Description:
|
||||
This script configures project-level VSCode settings in '.vscode/settings.json' for
|
||||
development and installs recommended extensions from '.vscode/extensions.json'.
|
||||
|
||||
Usage:
|
||||
python3 ./scripts/configure_vscode.py
|
||||
"""
|
||||
# pylint: disable=missing-function-docstring
|
||||
|
||||
@@ -40,7 +44,7 @@ def ensure_setting_file_exists() -> None:
|
||||
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
||||
except IOError as error:
|
||||
print_error(f"Error creating file {VSCODE_SETTINGS_JSON_FILE}: {error}")
|
||||
print(f"📄 Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
||||
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
||||
|
||||
def add_or_update_settings() -> None:
|
||||
configure_setting_key('eslint.validate', ['vue', 'javascript', 'typescript'])
|
||||
@@ -98,7 +102,8 @@ def locate_vscode_cli() -> Optional[str]:
|
||||
if vscode_alias:
|
||||
return vscode_alias
|
||||
potential_vscode_cli_paths = [
|
||||
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code' # macOS VS Code may not register 'code' command in PATH
|
||||
# VS Code on macOS may not register 'code' command in PATH
|
||||
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code'
|
||||
]
|
||||
for vscode_cli_candidate_path in potential_vscode_cli_paths:
|
||||
if Path(vscode_cli_candidate_path).is_file():
|
||||
@@ -109,7 +114,7 @@ def remove_json_comments(json_like: str) -> str:
|
||||
pattern: str = r'(?:"(?:\\.|[^"\\])*"|/\*[\s\S]*?\*/|//.*)|([^:]//.*$)'
|
||||
return re.sub(
|
||||
pattern,
|
||||
lambda m: '' if m.group(1) else m.agroup(0), json_like, flags=re.MULTILINE,
|
||||
lambda m: '' if m.group(1) else m.group(0), json_like, flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
def install_vscode_extensions(vscode_cli_path: str, extensions: list[str]) -> None:
|
||||
@@ -166,16 +171,16 @@ def print_installation_results(successful_installations: int, total_extensions:
|
||||
print_error("Failed to install any of the recommended extensions.")
|
||||
|
||||
def print_error(message: str) -> None:
|
||||
print(f"💀 Error: {message}", file=sys.stderr)
|
||||
print(f"[ERROR] {message}", file=sys.stderr)
|
||||
|
||||
def print_success(message: str) -> None:
|
||||
print(f"✅ Success: {message}")
|
||||
print(f"[SUCCESS] {message}")
|
||||
|
||||
def print_skip(message: str) -> None:
|
||||
print(f"⏩ Skipped: {message}")
|
||||
print(f"[SKIPPED] {message}")
|
||||
|
||||
def print_warning(message: str) -> None:
|
||||
print(f"⚠️ Warning: {message}", file=sys.stderr)
|
||||
print(f"[WARNING] {message}", file=sys.stderr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -2,94 +2,119 @@
|
||||
* Description:
|
||||
* This script updates the logo images across the project based on the primary
|
||||
* logo file ('img/logo.svg' file).
|
||||
*
|
||||
* It handles the creation and update of various icon sizes for different purposes,
|
||||
* including desktop launcher icons, tray icons, and web favicons from a single source
|
||||
* SVG logo file.
|
||||
*
|
||||
* Usage:
|
||||
* node ./scripts/logo-update.js
|
||||
* node ./scripts/logo-update.js
|
||||
*
|
||||
* Notes:
|
||||
* ImageMagick must be installed and accessible in the system's PATH
|
||||
*/
|
||||
|
||||
import { resolve, join } from 'node:path';
|
||||
import { rm, mkdtemp, stat } from 'node:fs/promises';
|
||||
import { resolve, join, dirname } from 'node:path';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { URL, fileURLToPath } from 'node:url';
|
||||
import electronBuilderConfig from '../electron-builder.cjs';
|
||||
|
||||
class Paths {
|
||||
constructor(selfDirectory) {
|
||||
const projectRoot = resolve(selfDirectory, '../');
|
||||
class ImageAssetPaths {
|
||||
constructor(currentScriptDirectory) {
|
||||
const projectRoot = resolve(currentScriptDirectory, '../');
|
||||
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
||||
this.publicDirectory = join(projectRoot, 'src/presentation/public');
|
||||
this.electronBuildDirectory = join(projectRoot, 'build');
|
||||
this.electronBuildResourcesDirectory = electronBuilderConfig.directories.buildResources;
|
||||
}
|
||||
|
||||
get electronTrayIconFile() {
|
||||
return join(this.publicDirectory, 'icon.png');
|
||||
}
|
||||
|
||||
get webFaviconFile() {
|
||||
return join(this.publicDirectory, 'favicon.ico');
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Source image: ${this.sourceImage}\n`
|
||||
+ `Public directory: ${this.publicDirectory}\n`
|
||||
+ `Electron build directory: ${this.electronBuildDirectory}`;
|
||||
return `Source image: ${this.sourceImage}`
|
||||
+ `\nPublic directory: ${this.publicDirectory}`
|
||||
+ `\n\t Electron tray icon file: ${this.electronTrayIconFile}`
|
||||
+ `\n\t Web favicon file: ${this.webFaviconFile}`
|
||||
+ `\nElectron build directory: ${this.electronBuildResourcesDirectory}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const paths = new Paths(getCurrentScriptDirectory());
|
||||
const paths = new ImageAssetPaths(getCurrentScriptDirectory());
|
||||
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
|
||||
await updateDesktopLauncherAndTrayIcon(paths.sourceImage, paths.publicDirectory);
|
||||
await updateWebFavicon(paths.sourceImage, paths.publicDirectory);
|
||||
await updateDesktopIcons(paths.sourceImage, paths.electronBuildDirectory);
|
||||
const convertCommand = await findAvailableImageMagickCommand();
|
||||
await generateDesktopAndTrayIcons(
|
||||
paths.sourceImage,
|
||||
paths.electronTrayIconFile,
|
||||
convertCommand,
|
||||
);
|
||||
await generateWebFavicon(
|
||||
paths.sourceImage,
|
||||
paths.webFaviconFile,
|
||||
convertCommand,
|
||||
);
|
||||
await generateDesktopIcons(
|
||||
paths.sourceImage,
|
||||
paths.electronBuildResourcesDirectory,
|
||||
convertCommand,
|
||||
);
|
||||
console.log('🎉 (Re)created icons successfully.');
|
||||
}
|
||||
|
||||
async function updateDesktopLauncherAndTrayIcon(sourceImage, publicFolder) {
|
||||
async function generateDesktopAndTrayIcons(sourceImage, targetFile, convertCommand) {
|
||||
// Reference: https://web.archive.org/web/20240502124306/https://www.electronjs.org/docs/latest/api/tray
|
||||
console.log(`Updating desktop launcher and tray icon at ${targetFile}.`);
|
||||
await ensureFileExists(sourceImage);
|
||||
await ensureFolderExists(publicFolder);
|
||||
const electronTrayIconFile = join(publicFolder, 'icon.png');
|
||||
console.log(`Updating desktop launcher and tray icon at ${electronTrayIconFile}.`);
|
||||
await runCommand(
|
||||
'npx',
|
||||
'svgexport',
|
||||
await ensureParentFolderExists(targetFile);
|
||||
await convertFromSvgToPng(
|
||||
convertCommand,
|
||||
sourceImage,
|
||||
electronTrayIconFile,
|
||||
targetFile,
|
||||
'512x512',
|
||||
);
|
||||
}
|
||||
|
||||
async function updateWebFavicon(sourceImage, faviconFolder) {
|
||||
console.log('Updating favicon');
|
||||
async function generateWebFavicon(sourceImage, faviconFilePath, convertCommand) {
|
||||
console.log(`Updating favicon at ${faviconFilePath}.`);
|
||||
await ensureFileExists(sourceImage);
|
||||
await ensureFolderExists(faviconFolder);
|
||||
await runCommand(
|
||||
'npx',
|
||||
'icon-gen',
|
||||
`--input ${sourceImage}`,
|
||||
`--output ${faviconFolder}`,
|
||||
'--ico',
|
||||
'--ico-name \'favicon\'',
|
||||
'--report',
|
||||
await ensureParentFolderExists(faviconFilePath);
|
||||
await convertFromSvgToIco(
|
||||
convertCommand,
|
||||
sourceImage,
|
||||
faviconFilePath,
|
||||
[16, 24, 32, 48, 64, 128, 256],
|
||||
);
|
||||
}
|
||||
|
||||
async function updateDesktopIcons(sourceImage, electronIconsDir) {
|
||||
async function generateDesktopIcons(sourceImage, electronBuildResourcesDirectory, convertCommand) {
|
||||
console.log(`Creating Electron icon files to ${electronBuildResourcesDirectory}.`);
|
||||
// Reference: https://web.archive.org/web/20240501103645/https://www.electron.build/icons.html
|
||||
await ensureFolderExists(electronBuildResourcesDirectory);
|
||||
await ensureFileExists(sourceImage);
|
||||
await ensureFolderExists(electronIconsDir);
|
||||
const temporaryDir = await mkdtemp('icon-');
|
||||
const temporaryPngFile = join(temporaryDir, 'icon.png');
|
||||
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by `icon-builder`
|
||||
await runCommand(
|
||||
'npx',
|
||||
'svgexport',
|
||||
const electronMainIconFile = join(electronBuildResourcesDirectory, 'icon.png');
|
||||
await convertFromSvgToPng(
|
||||
convertCommand,
|
||||
sourceImage,
|
||||
temporaryPngFile,
|
||||
'1024:1024',
|
||||
electronMainIconFile,
|
||||
'1024x1024', // Should be at least 512x512
|
||||
);
|
||||
console.log(`Creating electron icons to ${electronIconsDir}.`);
|
||||
await runCommand(
|
||||
'npx',
|
||||
'electron-icon-builder',
|
||||
`--input="${temporaryPngFile}"`,
|
||||
`--output="${electronIconsDir}"`,
|
||||
'--flatten',
|
||||
// Relying on `electron-builder`s conversion from png to ico results in pixelated look on Windows
|
||||
// 10 and 11 according to tests, see:
|
||||
// - https://web.archive.org/web/20240502114650/https://github.com/electron-userland/electron-builder/issues/7328
|
||||
// - https://web.archive.org/web/20240502115448/https://github.com/electron-userland/electron-builder/issues/3867
|
||||
const electronWindowsIconFile = join(electronBuildResourcesDirectory, 'icon.ico');
|
||||
await convertFromSvgToIco(
|
||||
convertCommand,
|
||||
sourceImage,
|
||||
electronWindowsIconFile,
|
||||
[16, 24, 32, 48, 64, 128, 256],
|
||||
);
|
||||
console.log('Cleaning up temporary directory.');
|
||||
await rm(temporaryDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function ensureFileExists(filePath) {
|
||||
@@ -100,12 +125,60 @@ async function ensureFileExists(filePath) {
|
||||
}
|
||||
|
||||
async function ensureFolderExists(folderPath) {
|
||||
if (!folderPath) {
|
||||
throw new Error('Path is missing');
|
||||
}
|
||||
const path = await stat(folderPath);
|
||||
if (!path.isDirectory()) {
|
||||
throw new Error(`Not a directory: ${folderPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureParentFolderExists(filePath) {
|
||||
return ensureFolderExists(dirname(filePath));
|
||||
}
|
||||
|
||||
const BaseImageMagickConvertArguments = Object.freeze([
|
||||
'-background none', // Transparent, so they do not get filled with white.
|
||||
'-strip', // Strip metadata.
|
||||
'-gravity Center', // Center the image when there's empty space
|
||||
]);
|
||||
|
||||
async function convertFromSvgToIco(
|
||||
convertCommand,
|
||||
inputFile,
|
||||
outputFile,
|
||||
sizes,
|
||||
) {
|
||||
await runCommand(
|
||||
convertCommand,
|
||||
...BaseImageMagickConvertArguments,
|
||||
`-density ${Math.max(...sizes).toString()}`, // High enough for sharpness
|
||||
`-define icon:auto-resize=${sizes.map((s) => s.toString()).join(',')}`, // Automatically store multiple sizes in an ico image
|
||||
'-compress None',
|
||||
inputFile,
|
||||
outputFile,
|
||||
);
|
||||
}
|
||||
|
||||
async function convertFromSvgToPng(
|
||||
convertCommand,
|
||||
inputFile,
|
||||
outputFile,
|
||||
size = undefined,
|
||||
) {
|
||||
await runCommand(
|
||||
convertCommand,
|
||||
...BaseImageMagickConvertArguments,
|
||||
...(size === undefined ? [] : [
|
||||
`-resize ${size}`,
|
||||
`-density ${size}`, // High enough for sharpness
|
||||
]),
|
||||
inputFile,
|
||||
outputFile,
|
||||
);
|
||||
}
|
||||
|
||||
async function runCommand(...args) {
|
||||
const command = args.join(' ');
|
||||
console.log(`Running command: ${command}`);
|
||||
@@ -135,4 +208,27 @@ function getCurrentScriptDirectory() {
|
||||
return fileURLToPath(new URL('.', import.meta.url));
|
||||
}
|
||||
|
||||
async function findAvailableImageMagickCommand() {
|
||||
// Reference: https://web.archive.org/web/20240502120041/https://imagemagick.org/script/convert.php
|
||||
const potentialBaseCommands = [
|
||||
'convert', // Legacy command, usually available on Linux/macOS installations
|
||||
'magick convert', // Newer command, available on Windows installations
|
||||
];
|
||||
for (const baseCommand of potentialBaseCommands) {
|
||||
const testCommand = `${baseCommand} -version`;
|
||||
try {
|
||||
await runCommand(testCommand); // eslint-disable-line no-await-in-loop
|
||||
console.log(`Confirmed: ImageMagick command '${baseCommand}' is available and operational.`);
|
||||
return baseCommand;
|
||||
} catch (err) {
|
||||
console.log(`Error: The command '${baseCommand}' is not found or failed to execute. Detailed error: ${err.message}"`);
|
||||
}
|
||||
}
|
||||
throw new Error([
|
||||
'Unable to locate any operational ImageMagick command.',
|
||||
`Attempted commands were: ${potentialBaseCommands.join(', ')}.`,
|
||||
'Please ensure ImageMagick is correctly installed and accessible.',
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
await main();
|
||||
|
||||
@@ -11,10 +11,11 @@ export type CodeRunErrorType =
|
||||
| 'FileWriteError'
|
||||
| 'FileReadbackVerificationError'
|
||||
| 'FilePathGenerationError'
|
||||
| 'UnsupportedOperatingSystem'
|
||||
| 'FileExecutionError'
|
||||
| 'UnsupportedPlatform'
|
||||
| 'DirectoryCreationError'
|
||||
| 'UnexpectedError';
|
||||
| 'FilePermissionChangeError'
|
||||
| 'FileExecutionError'
|
||||
| 'ExternalProcessTermination';
|
||||
|
||||
interface CodeRunStatus {
|
||||
readonly success: boolean;
|
||||
|
||||
@@ -1,44 +1,164 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { PlatformTimer } from './PlatformTimer';
|
||||
import type { Timer, TimeoutType } from './Timer';
|
||||
|
||||
export type CallbackType = (..._: readonly unknown[]) => void;
|
||||
|
||||
export function throttle(
|
||||
callback: CallbackType,
|
||||
waitInMs: number,
|
||||
timer: Timer = PlatformTimer,
|
||||
): CallbackType {
|
||||
const throttler = new Throttler(timer, waitInMs, callback);
|
||||
return (...args: unknown[]) => throttler.invoke(...args);
|
||||
export interface ThrottleOptions {
|
||||
/** Skip the immediate execution of the callback on the first invoke */
|
||||
readonly excludeLeadingCall: boolean;
|
||||
readonly timer: Timer;
|
||||
}
|
||||
|
||||
class Throttler {
|
||||
private queuedExecutionId: TimeoutType | undefined;
|
||||
const DefaultOptions: ThrottleOptions = {
|
||||
excludeLeadingCall: false,
|
||||
timer: PlatformTimer,
|
||||
};
|
||||
|
||||
private previouslyRun: number;
|
||||
export interface ThrottleFunction {
|
||||
(
|
||||
callback: CallbackType,
|
||||
waitInMs: number,
|
||||
options?: Partial<ThrottleOptions>,
|
||||
): CallbackType;
|
||||
}
|
||||
|
||||
export const throttle: ThrottleFunction = (
|
||||
callback: CallbackType,
|
||||
waitInMs: number,
|
||||
options: Partial<ThrottleOptions> = DefaultOptions,
|
||||
): CallbackType => {
|
||||
const defaultedOptions: ThrottleOptions = {
|
||||
...DefaultOptions,
|
||||
...options,
|
||||
};
|
||||
const throttler = new Throttler(waitInMs, callback, defaultedOptions);
|
||||
return (...args: unknown[]) => throttler.invoke(...args);
|
||||
};
|
||||
|
||||
class Throttler {
|
||||
private lastExecutionTime: number | null = null;
|
||||
|
||||
private executionScheduler: DelayedCallbackScheduler;
|
||||
|
||||
constructor(
|
||||
private readonly timer: Timer,
|
||||
private readonly waitInMs: number,
|
||||
private readonly callback: CallbackType,
|
||||
private readonly options: ThrottleOptions,
|
||||
) {
|
||||
if (!waitInMs) { throw new Error('missing delay'); }
|
||||
if (waitInMs < 0) { throw new Error('negative delay'); }
|
||||
this.executionScheduler = new DelayedCallbackScheduler(options.timer);
|
||||
}
|
||||
|
||||
public invoke(...args: unknown[]): void {
|
||||
const now = this.timer.dateNow();
|
||||
if (this.queuedExecutionId !== undefined) {
|
||||
this.timer.clearTimeout(this.queuedExecutionId);
|
||||
this.queuedExecutionId = undefined;
|
||||
}
|
||||
if (!this.previouslyRun || (now - this.previouslyRun >= this.waitInMs)) {
|
||||
this.callback(...args);
|
||||
this.previouslyRun = now;
|
||||
} else {
|
||||
const nextCall = () => this.invoke(...args);
|
||||
const nextCallDelayInMs = this.waitInMs - (now - this.previouslyRun);
|
||||
this.queuedExecutionId = this.timer.setTimeout(nextCall, nextCallDelayInMs);
|
||||
switch (true) {
|
||||
case this.isLeadingCallWithinThrottlePeriod(): {
|
||||
if (this.options.excludeLeadingCall) {
|
||||
this.scheduleNext(args);
|
||||
return;
|
||||
}
|
||||
this.executeNow(args);
|
||||
return;
|
||||
}
|
||||
case this.isAlreadyScheduled(): {
|
||||
this.updateNextScheduled(args);
|
||||
return;
|
||||
}
|
||||
case !this.isThrottlePeriodPassed(): {
|
||||
this.scheduleNext(args);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new Error('Throttle logical error: no conditions for execution or scheduling were met.');
|
||||
}
|
||||
}
|
||||
|
||||
private isLeadingCallWithinThrottlePeriod(): boolean {
|
||||
return this.isThrottlePeriodPassed()
|
||||
&& !this.isAlreadyScheduled();
|
||||
}
|
||||
|
||||
private isThrottlePeriodPassed(): boolean {
|
||||
if (this.lastExecutionTime === null) {
|
||||
return true;
|
||||
}
|
||||
const timeSinceLastExecution = this.options.timer.dateNow() - this.lastExecutionTime;
|
||||
const isThrottleTimePassed = timeSinceLastExecution >= this.waitInMs;
|
||||
return isThrottleTimePassed;
|
||||
}
|
||||
|
||||
private isAlreadyScheduled(): boolean {
|
||||
return this.executionScheduler.getNext() !== null;
|
||||
}
|
||||
|
||||
private scheduleNext(args: unknown[]): void {
|
||||
if (this.executionScheduler.getNext()) {
|
||||
throw new Error('An execution is already scheduled.');
|
||||
}
|
||||
this.executionScheduler.resetNext(
|
||||
() => this.executeNow(args),
|
||||
this.waitInMs,
|
||||
);
|
||||
}
|
||||
|
||||
private updateNextScheduled(args: unknown[]): void {
|
||||
const nextScheduled = this.executionScheduler.getNext();
|
||||
if (!nextScheduled) {
|
||||
throw new Error('A non-existent scheduled execution cannot be updated.');
|
||||
}
|
||||
const nextDelay = nextScheduled.scheduledTime - this.dateNow();
|
||||
this.executionScheduler.resetNext(
|
||||
() => this.executeNow(args),
|
||||
nextDelay,
|
||||
);
|
||||
}
|
||||
|
||||
private executeNow(args: unknown[]): void {
|
||||
this.callback(...args);
|
||||
this.lastExecutionTime = this.dateNow();
|
||||
}
|
||||
|
||||
private dateNow(): number {
|
||||
return this.options.timer.dateNow();
|
||||
}
|
||||
}
|
||||
|
||||
interface ScheduledCallback {
|
||||
readonly scheduleTimeoutId: TimeoutType;
|
||||
readonly scheduledTime: number;
|
||||
}
|
||||
|
||||
class DelayedCallbackScheduler {
|
||||
private scheduledCallback: ScheduledCallback | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly timer: Timer,
|
||||
) { }
|
||||
|
||||
public getNext(): ScheduledCallback | null {
|
||||
return this.scheduledCallback;
|
||||
}
|
||||
|
||||
public resetNext(
|
||||
callback: () => void,
|
||||
delayInMs: number,
|
||||
) {
|
||||
this.clear();
|
||||
this.scheduledCallback = {
|
||||
scheduledTime: this.timer.dateNow() + delayInMs,
|
||||
scheduleTimeoutId: this.timer.setTimeout(() => {
|
||||
this.clear();
|
||||
callback();
|
||||
}, delayInMs),
|
||||
};
|
||||
}
|
||||
|
||||
private clear() {
|
||||
if (this.scheduledCallback === null) {
|
||||
return;
|
||||
}
|
||||
this.timer.clearTimeout(this.scheduledCallback.scheduleTimeoutId);
|
||||
this.scheduledCallback = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ function hasCall(data: FunctionData): data is CallFunctionData {
|
||||
}
|
||||
|
||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||
ensureNoUnnamedFunctions(functions);
|
||||
ensureNoDuplicatesInFunctionNames(functions);
|
||||
ensureEitherCallOrCodeIsDefined(functions);
|
||||
ensureNoDuplicateCode(functions);
|
||||
@@ -108,6 +109,16 @@ function printList(list: readonly string[]): string {
|
||||
return `"${list.join('","')}"`;
|
||||
}
|
||||
|
||||
function ensureNoUnnamedFunctions(functions: readonly FunctionData[]) {
|
||||
const functionsWithoutNames = functions.filter(
|
||||
(func) => !func.name || func.name.trim().length === 0,
|
||||
);
|
||||
if (functionsWithoutNames.length) {
|
||||
const invalidFunctions = functionsWithoutNames.map((f) => JSON.stringify(f));
|
||||
throw new Error(`Some function(s) have no names:\n${invalidFunctions.join('\n')}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) {
|
||||
// Ensure functions do not define both call and code
|
||||
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
||||
|
||||
@@ -184,7 +184,7 @@ actions:
|
||||
> - Logs are valuable for diagnosing issues and understanding past actions [1].
|
||||
> - Script files can help review changes made to the system and aid in reverting those changes if needed.
|
||||
|
||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||
children:
|
||||
-
|
||||
@@ -202,7 +202,7 @@ actions:
|
||||
> - This action is irreversible. Deleted script files cannot be retrieved.
|
||||
> - These files might be necessary for troubleshooting if you experience issues after using privacy.sexy scripts.
|
||||
|
||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||
call:
|
||||
function: ClearDirectoryContents
|
||||
@@ -223,7 +223,7 @@ actions:
|
||||
> - Removing logs will prevent you from reviewing the application's activities, which could be helpful in diagnosing issues.
|
||||
> - Logs can contain valuable information for technical support should you need assistance.
|
||||
|
||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||
call:
|
||||
function: ClearDirectoryContents
|
||||
@@ -2761,7 +2761,7 @@ actions:
|
||||
docs: |-
|
||||
Firefox provides an option for Enhanced Tracking Protection [1], which blocks trackers that
|
||||
gather information about your browsing behavior without disrupting site functionality [1].
|
||||
This feature also includes protections against harmful scripts such as malware that drains
|
||||
This feature also includes protections against harmful scripts such as malware that drain
|
||||
your battery [1].
|
||||
|
||||
This script enables the `privacy.resistFingerprinting` preference,
|
||||
@@ -2791,7 +2791,7 @@ actions:
|
||||
This script enables the `privacy.resistFingerprinting` preference, activating
|
||||
anti-fingerprinting [1][2].
|
||||
|
||||
As an experimental feature, it might cause some website breakage [2], such as impacting web
|
||||
As an experimental feature, it might cause some website breakages [2], such as impacting web
|
||||
speech functionality [3] and favicons [4].
|
||||
|
||||
[1]: https://web.archive.org/web/20221025201025/https://support.mozilla.org/en-US/kb/firefox-protection-against-fingerprinting "Firefox's protection against fingerprinting | Firefox Help | support.mozilla.org"
|
||||
@@ -2876,7 +2876,7 @@ actions:
|
||||
|
||||
It's configured to be enabled in nightly, aurora, beta, or default (developer) builds.
|
||||
In release builds, however, it's set to false [1]. This setting is hard-coded into the C++
|
||||
code to prevent easy disabling [2]. Developers have been approached about this issue but
|
||||
code to prevent easy disabling [2]. Developers have been approached about this issue, but
|
||||
have rejected proposals to unlock it [3].
|
||||
|
||||
Mozilla's plan is to deprecate this setting eventually, followed by removal [1].
|
||||
@@ -3012,7 +3012,7 @@ actions:
|
||||
recommend: standard
|
||||
docs: |-
|
||||
This script sets `toolkit.telemetry.server` to be empty.
|
||||
This preference defines the server to which Telemetry pings are sent [1].
|
||||
This preference defines the server to which telemetry pings are sent [1].
|
||||
|
||||
[1]: https://web.archive.org/web/20221015102124/https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/internals/preferences.html "Preferences and Defines — Firefox Source Docs documentation | firefox-source-docs.mozilla.org"
|
||||
call:
|
||||
@@ -3133,7 +3133,7 @@ actions:
|
||||
name: Disable Firefox Pioneer study monitoring
|
||||
recommend: standard
|
||||
docs: |-
|
||||
This script configures `toolkit.telemetry.pioneer-new-studies-available` to be disabled to opt out.
|
||||
This script configures `toolkit.telemetry.pioneer-new-studies-available` to be disabled to opt out
|
||||
Firefox Pioneer program.
|
||||
|
||||
This setting disables availability check for Firefox Pioneer studies [1].
|
||||
@@ -3711,7 +3711,7 @@ functions:
|
||||
# User-specific:
|
||||
# [~/.profile]
|
||||
# User-specific shell initialization scripts.
|
||||
# ✅ Recomended by Debian to edit for user-specific environment variables.
|
||||
# ✅ Recommended by Debian to edit for user-specific environment variables.
|
||||
# [~/.bashrc]
|
||||
# User-based configuration file to set environment variables for Bash shell.
|
||||
# ❌ Bash-specific.
|
||||
@@ -3783,7 +3783,7 @@ functions:
|
||||
if [[ -f "$cronjob_path" ]]; then
|
||||
if [[ -x "$cronjob_path" ]]; then
|
||||
sudo chmod -x "$cronjob_path"
|
||||
echo "Succesfully disabled cronjob \"$job_name\"."
|
||||
echo "Successfully disabled cronjob \"$job_name\"."
|
||||
else
|
||||
echo "Skipping, cronjob \"$job_name\" is already disabled."
|
||||
fi
|
||||
@@ -3797,7 +3797,7 @@ functions:
|
||||
echo "Skipping, cronjob \"$job_name\" is already enabled."
|
||||
else
|
||||
sudo chmod +x "$cronjob_path"
|
||||
echo "Succesfully enabled cronjob \"$job_name\"."
|
||||
echo "Successfully enabled cronjob \"$job_name\"."
|
||||
fi
|
||||
else
|
||||
>&2 echo "Failed to enable cronjob \"$job_name\" because it's missing."
|
||||
@@ -3939,7 +3939,7 @@ functions:
|
||||
echo "Backup file exists: $file."
|
||||
sudo mv "$backup_file" "$file"
|
||||
echo "Moved to: $file."
|
||||
echo "Succesfully restored."
|
||||
echo "Successfully restored."
|
||||
else
|
||||
>&2 echo "Failed to restore, backup file could not be found at $backup_file."
|
||||
>&2 echo "Was the change initially applied by privacy.sexy?"
|
||||
|
||||
@@ -300,7 +300,7 @@ actions:
|
||||
> - Logs are valuable for diagnosing issues and understanding past actions [1].
|
||||
> - Script files can help review changes made to the system and aid in reverting those changes if needed.
|
||||
|
||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||
children:
|
||||
-
|
||||
@@ -318,7 +318,7 @@ actions:
|
||||
> - This action is irreversible. Deleted script files cannot be retrieved.
|
||||
> - These files might be necessary for troubleshooting if you experience issues after using privacy.sexy scripts.
|
||||
|
||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||
call:
|
||||
function: ClearDirectoryContents
|
||||
@@ -339,7 +339,7 @@ actions:
|
||||
> - Removing logs will prevent you from reviewing the application's activities, which could be helpful in diagnosing issues.
|
||||
> - Logs can contain valuable information for technical support should you need assistance.
|
||||
|
||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||
call:
|
||||
function: ClearDirectoryContents
|
||||
@@ -1468,7 +1468,7 @@ actions:
|
||||
|
||||
> **Caution**:
|
||||
> Disabling automatic updates can leave your system vulnerable to unpatched exploits.
|
||||
> Manually check and and apply updates to stay protected.
|
||||
> Manually check and apply updates to stay protected.
|
||||
children:
|
||||
-
|
||||
name: Disable automatic checks for updates
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface CommandDefinition {
|
||||
buildShellCommand(filePath: string): string;
|
||||
isExecutionTerminatedExternally(exitCode: number): boolean;
|
||||
isExecutablePermissionsRequiredOnFile(): boolean;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { PosixShellArgumentEscaper } from './ShellArgument/PosixShellArgumentEscaper';
|
||||
import type { CommandDefinition } from '../CommandDefinition';
|
||||
import type { ShellArgumentEscaper } from './ShellArgument/ShellArgumentEscaper';
|
||||
|
||||
export const LinuxTerminalEmulator = 'x-terminal-emulator';
|
||||
|
||||
export class LinuxVisibleTerminalCommand implements CommandDefinition {
|
||||
constructor(
|
||||
private readonly escaper: ShellArgumentEscaper = new PosixShellArgumentEscaper(),
|
||||
) { }
|
||||
|
||||
public buildShellCommand(filePath: string): string {
|
||||
return `${LinuxTerminalEmulator} -e ${this.escaper.escapePathArgument(filePath)}`;
|
||||
/*
|
||||
🤔 Potential improvements:
|
||||
Use user-friendly GUI sudo prompt (not terminal-based).
|
||||
If `pkexec` exists, we could do `x-terminal-emulator -e pkexec 'path'`, which always
|
||||
prompts with user-friendly GUI sudo prompt.
|
||||
📝 Options:
|
||||
`x-terminal-emulator -e 'path'`:
|
||||
✅ Visible terminal window
|
||||
❌ Terminal-based (not GUI) sudo prompt.
|
||||
`x-terminal-emulator -e pkexec 'path'
|
||||
✅ Visible terminal window
|
||||
✅ Always prompts with user-friendly GUI sudo prompt.
|
||||
🤔 Not using `pkexec` as it is not in all Linux distributions. It should have smarter
|
||||
logic to handle if it does not exist.
|
||||
`electron.shell.openPath`:
|
||||
❌ Opens the script in the default text editor, verified on
|
||||
Debian/Ubuntu-based distributions.
|
||||
`child_process.execFile()`:
|
||||
❌ Script execution in the background without a visible terminal.
|
||||
*/
|
||||
}
|
||||
|
||||
public isExecutionTerminatedExternally(exitCode: number): boolean {
|
||||
return exitCode === 137;
|
||||
/*
|
||||
`x-terminal-emulator` may return exit code `137` under specific circumstances like when the
|
||||
user closes the terminal (observed with `gnome-terminal` on Pop!_OS). This exit code (128 +
|
||||
Unix signal 9) indicates the process was terminated by a SIGKILL signal, which can occur due
|
||||
to user action (cancelling the progress) or the system (e.g., due to memory shortages).
|
||||
|
||||
Additional exit codes noted for future consideration (currently not handled as they have not
|
||||
been reproduced):
|
||||
|
||||
- 130 (130 = 128 + Unix signal 2): Indicates the script was terminated by the user
|
||||
(Control-C), corresponding to a SIGINT signal.
|
||||
- 143 (128 + Unix signal 15): Indicates termination by a SIGTERM signal, suggesting a request
|
||||
to gracefully terminate the process.
|
||||
*/
|
||||
}
|
||||
|
||||
public isExecutablePermissionsRequiredOnFile(): boolean {
|
||||
/*
|
||||
On Linux, a script file without executable permissions cannot be run directly by its path
|
||||
without specifying a shell explicitly.
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { PosixShellArgumentEscaper } from './ShellArgument/PosixShellArgumentEscaper';
|
||||
import type { CommandDefinition } from '../CommandDefinition';
|
||||
import type { ShellArgumentEscaper } from './ShellArgument/ShellArgumentEscaper';
|
||||
|
||||
export class MacOsVisibleTerminalCommand implements CommandDefinition {
|
||||
constructor(
|
||||
private readonly escaper: ShellArgumentEscaper = new PosixShellArgumentEscaper(),
|
||||
) { }
|
||||
|
||||
public buildShellCommand(filePath: string): string {
|
||||
return `open -a Terminal.app ${this.escaper.escapePathArgument(filePath)}`;
|
||||
/*
|
||||
📝 Options:
|
||||
`child_process.execFile()`
|
||||
"path", `cmd.exe /c "path"`
|
||||
❌ Script execution in the background without a visible terminal.
|
||||
This occurs only when the user runs the application as administrator, as seen
|
||||
in Windows Pro VMs on Azure.
|
||||
`PowerShell Start -Verb RunAs "path"`
|
||||
✅ Visible terminal window
|
||||
✅ GUI sudo prompt (through `RunAs` option)
|
||||
`PowerShell Start "path"`
|
||||
`explorer.exe "path"`
|
||||
`electron.shell.openPath`
|
||||
`start cmd.exe /c "$path"`
|
||||
✅ Visible terminal window
|
||||
✅ GUI sudo prompt (through `RunAs` option)
|
||||
👍 Among all options `start` command is the most explicit one, being the most resilient
|
||||
against the potential changes in Windows or Electron framework (e.g. https://github.com/electron/electron/issues/36765).
|
||||
`%COMSPEC%` environment variable should be checked before defaulting to `cmd.exe.
|
||||
Related docs: https://web.archive.org/web/20240106002357/https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
|
||||
*/
|
||||
}
|
||||
|
||||
public isExecutionTerminatedExternally(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public isExecutablePermissionsRequiredOnFile(): boolean {
|
||||
/*
|
||||
On macOS, a script file without executable permissions cannot be run directly by its path
|
||||
without specifying a shell explicitly.
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { PowerShellInvokeShellCommandCreator } from './PowerShellInvokeShellCommandCreator';
|
||||
|
||||
/**
|
||||
Encoding PowerShell commands resolve issues with quote handling.
|
||||
|
||||
There are known problems with PowerShell's handling of double quotes in command line arguments:
|
||||
- Quote stripping in PowerShell command line arguments: https://web.archive.org/web/20240507102706/https://stackoverflow.com/questions/6714165/powershell-stripping-double-quotes-from-command-line-arguments
|
||||
- privacy.sexy double quotes issue when calling PowerShell from command line: https://web.archive.org/web/20240507102841/https://github.com/undergroundwires/privacy.sexy/issues/351
|
||||
- Challenges with single quotes in PowerShell command line: https://web.archive.org/web/20240507102047/https://stackoverflow.com/questions/20958388/command-line-escaping-single-quote-for-powershell
|
||||
|
||||
Using the `EncodedCommand` parameter is recommended by Microsoft for handling
|
||||
complex quoting scenarios. This approach helps avoid issues by encoding the entire
|
||||
command as a Base64 string:
|
||||
- Microsoft's documentation on using the `EncodedCommand` parameter: https://web.archive.org/web/20240507102733/https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe?view=powershell-5.1#-encodedcommand-base64encodedcommand
|
||||
*/
|
||||
export class EncodedPowerShellInvokeCmdCommandCreator
|
||||
implements PowerShellInvokeShellCommandCreator {
|
||||
public createCommandToInvokePowerShell(powerShellScript: string): string {
|
||||
return generateEncodedPowershellCommand(powerShellScript);
|
||||
}
|
||||
}
|
||||
|
||||
function generateEncodedPowershellCommand(powerShellScript: string): string {
|
||||
const encodedCommand = encodeForPowershellExecution(powerShellScript);
|
||||
return `PowerShell -EncodedCommand ${encodedCommand}`;
|
||||
}
|
||||
|
||||
function encodeForPowershellExecution(script: string): string {
|
||||
// The string must be formatted using UTF-16LE character encoding, see: https://web.archive.org/web/20240507102733/https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe?view=powershell-5.1#-encodedcommand-base64encodedcommand
|
||||
const buffer = Buffer.from(script, 'utf16le');
|
||||
return buffer.toString('base64');
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface PowerShellInvokeShellCommandCreator {
|
||||
createCommandToInvokePowerShell(powerShellCommand: string): string;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { ShellArgumentEscaper } from './ShellArgumentEscaper';
|
||||
|
||||
export class PosixShellArgumentEscaper implements ShellArgumentEscaper {
|
||||
public escapePathArgument(pathArgument: string): string {
|
||||
return posixShellPathArgumentEscape(pathArgument);
|
||||
}
|
||||
}
|
||||
|
||||
function posixShellPathArgumentEscape(pathArgument: string): string {
|
||||
/*
|
||||
- Wraps the path in single quotes, which is a standard practice in POSIX shells
|
||||
(like bash and zsh) found on macOS/Linux to ensure that characters like spaces, '*', and
|
||||
'?' are treated as literals, not as special characters.
|
||||
- Escapes any single quotes within the path itself. This allows paths containing single
|
||||
quotes to be correctly interpreted in POSIX-compliant systems, such as Linux and macOS.
|
||||
*/
|
||||
return `'${pathArgument.replaceAll('\'', '\'\\\'\'')}'`;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { ShellArgumentEscaper } from './ShellArgumentEscaper';
|
||||
|
||||
export class PowerShellArgumentEscaper implements ShellArgumentEscaper {
|
||||
public escapePathArgument(pathArgument: string): string {
|
||||
return powerShellPathArgumentEscape(pathArgument);
|
||||
}
|
||||
}
|
||||
|
||||
function powerShellPathArgumentEscape(pathArgument: string): string {
|
||||
// - Encloses the path in single quotes to handle spaces and most special characters.
|
||||
// - Single quotes are used in PowerShell to ensure the string is treated as a literal string.
|
||||
// - Paths in Windows can include single quotes ('), so any internal single quotes are escaped
|
||||
// using double quotes.
|
||||
return `'${pathArgument.replace(/'/g, "''")}'`;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface ShellArgumentEscaper {
|
||||
escapePathArgument(pathArgument: string): string;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import { PowerShellArgumentEscaper } from './ShellArgument/PowerShellArgumentEscaper';
|
||||
import { EncodedPowerShellInvokeCmdCommandCreator } from './PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator';
|
||||
import type { ShellArgumentEscaper } from './ShellArgument/ShellArgumentEscaper';
|
||||
import type { CommandDefinition } from '../CommandDefinition';
|
||||
import type { PowerShellInvokeShellCommandCreator } from './PowerShellInvoke/PowerShellInvokeShellCommandCreator';
|
||||
|
||||
export class WindowsVisibleTerminalCommand implements CommandDefinition {
|
||||
constructor(
|
||||
private readonly escaper: ShellArgumentEscaper = new PowerShellArgumentEscaper(),
|
||||
private readonly powershellCommandCreator: PowerShellInvokeShellCommandCreator
|
||||
= new EncodedPowerShellInvokeCmdCommandCreator(),
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
) { }
|
||||
|
||||
public buildShellCommand(filePath: string): string {
|
||||
const powershellCommand = [
|
||||
'Start-Process',
|
||||
'-Verb RunAs', // Run as administrator with GUI sudo prompt
|
||||
`-FilePath ${this.escaper.escapePathArgument(filePath)}`,
|
||||
].join(' ');
|
||||
/*
|
||||
Running PowerShell command is preferred due to its flexibility and the way it provides
|
||||
GUI sudo prompt through `RunAs` argument.
|
||||
Other options considered:
|
||||
`child_process.execFile()`
|
||||
"path", `cmd.exe /c "path"`
|
||||
❌ Script execution in the background without a visible terminal.
|
||||
This occurs only when the user runs the application as administrator, as seen
|
||||
in Windows Pro VMs on Azure.
|
||||
`PowerShell Start -Verb RunAs "path"`
|
||||
✅ Visible terminal window
|
||||
✅ GUI sudo prompt (through `RunAs` option)
|
||||
`PowerShell Start "path"`
|
||||
`explorer.exe "path"`
|
||||
`electron.shell.openPath`
|
||||
`start cmd.exe /c "$path"`
|
||||
✅ Visible terminal window
|
||||
✅ GUI sudo prompt (through `RunAs` option)
|
||||
👍 Among all options `start` command is the most explicit one, being the most resilient
|
||||
against the potential changes in Windows or Electron framework (e.g. https://github.com/electron/electron/issues/36765).
|
||||
`%COMSPEC%` environment variable should be checked before defaulting to `cmd.exe.
|
||||
Related docs: https://web.archive.org/web/20240106002357/https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
|
||||
*/
|
||||
this.logger.info(`Building command for PowerShell execution:\n\tCommand: ${powershellCommand}`);
|
||||
return this.powershellCommandCreator.createCommandToInvokePowerShell(powershellCommand);
|
||||
}
|
||||
|
||||
public isExecutionTerminatedExternally(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public isExecutablePermissionsRequiredOnFile(): boolean {
|
||||
/*
|
||||
In Windows, whether a file can be executed is determined by its file extension
|
||||
(.exe, .bat, .cmd, etc.) rather than executable permissions set on the file.
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { CommandDefinition } from '../CommandDefinition';
|
||||
|
||||
export interface CommandDefinitionFactory {
|
||||
provideCommandDefinition(): CommandDefinition;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import type { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { WindowsVisibleTerminalCommand } from '../Commands/WindowsVisibleTerminalCommand';
|
||||
import { LinuxVisibleTerminalCommand } from '../Commands/LinuxVisibleTerminalCommand';
|
||||
import { MacOsVisibleTerminalCommand } from '../Commands/MacOsVisibleTerminalCommand';
|
||||
import type { CommandDefinitionFactory } from './CommandDefinitionFactory';
|
||||
import type { CommandDefinition } from '../CommandDefinition';
|
||||
|
||||
export class OsSpecificTerminalLaunchCommandFactory implements CommandDefinitionFactory {
|
||||
constructor(
|
||||
private readonly environment: RuntimeEnvironment = CurrentEnvironment,
|
||||
) { }
|
||||
|
||||
public provideCommandDefinition(): CommandDefinition {
|
||||
const { os } = this.environment;
|
||||
if (os === undefined) {
|
||||
throw new Error('Operating system could not be identified from environment.');
|
||||
}
|
||||
return getOperatingSystemCommandDefinition(os);
|
||||
}
|
||||
}
|
||||
|
||||
function getOperatingSystemCommandDefinition(
|
||||
operatingSystem: OperatingSystem,
|
||||
): CommandDefinition {
|
||||
const definition = SupportedDesktopCommandDefinitions[operatingSystem];
|
||||
if (!definition) {
|
||||
throw new Error(`Unsupported operating system: ${OperatingSystem[operatingSystem]}`);
|
||||
}
|
||||
return definition;
|
||||
}
|
||||
|
||||
const SupportedDesktopCommandDefinitions: Readonly<Partial<Record<
|
||||
OperatingSystem,
|
||||
CommandDefinition>>> = {
|
||||
[OperatingSystem.Windows]: new WindowsVisibleTerminalCommand(),
|
||||
[OperatingSystem.Linux]: new LinuxVisibleTerminalCommand(),
|
||||
[OperatingSystem.macOS]: new MacOsVisibleTerminalCommand(),
|
||||
} as const;
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { ScriptFileExecutionOutcome } from '../../ScriptFileExecutor';
|
||||
import type { CommandDefinition } from '../CommandDefinition';
|
||||
|
||||
export interface CommandDefinitionRunner {
|
||||
runCommandDefinition(
|
||||
commandDefinition: CommandDefinition,
|
||||
filePath: string,
|
||||
): Promise<ScriptFileExecutionOutcome>;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
|
||||
import { FileSystemExecutablePermissionSetter } from './PermissionSetter/FileSystemExecutablePermissionSetter';
|
||||
import { LoggingNodeShellCommandRunner } from './ShellRunner/LoggingNodeShellCommandRunner';
|
||||
import type { FailedScriptFileExecution, ScriptFileExecutionOutcome } from '../../ScriptFileExecutor';
|
||||
import type { CommandDefinition } from '../CommandDefinition';
|
||||
import type { CommandDefinitionRunner } from './CommandDefinitionRunner';
|
||||
import type { ExecutablePermissionSetter } from './PermissionSetter/ExecutablePermissionSetter';
|
||||
import type { ShellCommandOutcome, ShellCommandRunner } from './ShellRunner/ShellCommandRunner';
|
||||
|
||||
export class ExecutableFileShellCommandDefinitionRunner implements CommandDefinitionRunner {
|
||||
constructor(
|
||||
private readonly executablePermissionSetter: ExecutablePermissionSetter
|
||||
= new FileSystemExecutablePermissionSetter(),
|
||||
private readonly shellCommandRunner: ShellCommandRunner
|
||||
= new LoggingNodeShellCommandRunner(),
|
||||
) { }
|
||||
|
||||
public async runCommandDefinition(
|
||||
commandDefinition: CommandDefinition,
|
||||
filePath: string,
|
||||
): Promise<ScriptFileExecutionOutcome> {
|
||||
if (commandDefinition.isExecutablePermissionsRequiredOnFile()) {
|
||||
const filePermissionsResult = await this.executablePermissionSetter
|
||||
.makeFileExecutable(filePath);
|
||||
if (!filePermissionsResult.success) {
|
||||
return filePermissionsResult;
|
||||
}
|
||||
}
|
||||
const command = commandDefinition.buildShellCommand(filePath);
|
||||
const shellOutcome = await this.shellCommandRunner.runShellCommand(command);
|
||||
return interpretShellOutcome(shellOutcome, commandDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
function interpretShellOutcome(
|
||||
outcome: ShellCommandOutcome,
|
||||
commandDefinition: CommandDefinition,
|
||||
): ScriptFileExecutionOutcome {
|
||||
switch (outcome.type) {
|
||||
case 'RegularProcessExit':
|
||||
if (outcome.exitCode === 0) {
|
||||
return { success: true };
|
||||
}
|
||||
if (commandDefinition.isExecutionTerminatedExternally(outcome.exitCode)) {
|
||||
return createFailureOutcome(
|
||||
'ExternalProcessTermination',
|
||||
`Process terminated externally: Exit code ${outcome.exitCode}.`,
|
||||
);
|
||||
}
|
||||
return createFailureOutcome(
|
||||
'FileExecutionError',
|
||||
`Unexpected exit code: ${outcome.exitCode}.`,
|
||||
);
|
||||
case 'ExternallyTerminated':
|
||||
return createFailureOutcome(
|
||||
'ExternalProcessTermination',
|
||||
`Process terminated by signal ${outcome.terminationSignal}.`,
|
||||
);
|
||||
case 'ExecutionError':
|
||||
return createFailureOutcome(
|
||||
'FileExecutionError',
|
||||
`Execution error: ${outcome.error.message}.`,
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unknown outcome type: ${outcome}`);
|
||||
}
|
||||
}
|
||||
|
||||
function createFailureOutcome(
|
||||
type: CodeRunErrorType,
|
||||
errorMessage: string,
|
||||
): FailedScriptFileExecution {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type,
|
||||
message: `Error during command execution: ${errorMessage}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { ScriptFileExecutionOutcome } from '@/infrastructure/CodeRunner/Execution/ScriptFileExecutor';
|
||||
|
||||
export interface ExecutablePermissionSetter {
|
||||
makeFileExecutable(filePath: string): Promise<ScriptFileExecutionOutcome>;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NodeElectronSystemOperations } from '@/infrastructure/CodeRunner/System/NodeElectronSystemOperations';
|
||||
import type { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import type { SystemOperations } from '@/infrastructure/CodeRunner/System/SystemOperations';
|
||||
import type { ScriptFileExecutionOutcome } from '@/infrastructure/CodeRunner/Execution/ScriptFileExecutor';
|
||||
import type { ExecutablePermissionSetter } from './ExecutablePermissionSetter';
|
||||
|
||||
export class FileSystemExecutablePermissionSetter implements ExecutablePermissionSetter {
|
||||
constructor(
|
||||
private readonly system: SystemOperations = new NodeElectronSystemOperations(),
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
) { }
|
||||
|
||||
public async makeFileExecutable(filePath: string): Promise<ScriptFileExecutionOutcome> {
|
||||
/*
|
||||
This is required on macOS and Linux otherwise the terminal emulators will refuse to
|
||||
execute the script. It's not needed on Windows.
|
||||
*/
|
||||
try {
|
||||
this.logger.info(`Setting execution permissions for file at ${filePath}`);
|
||||
await this.system.fileSystem.setFilePermissions(filePath, '755');
|
||||
this.logger.info(`Execution permissions set successfully for ${filePath}`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type: 'FilePermissionChangeError',
|
||||
message: `Error setting script file permission: ${error.message}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import type { SystemOperations } from '@/infrastructure/CodeRunner/System/SystemOperations';
|
||||
import { NodeElectronSystemOperations } from '@/infrastructure/CodeRunner/System/NodeElectronSystemOperations';
|
||||
import type { ShellCommandOutcome, ShellCommandRunner } from './ShellCommandRunner';
|
||||
|
||||
export class LoggingNodeShellCommandRunner implements ShellCommandRunner {
|
||||
constructor(
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
private readonly systemOps: SystemOperations = new NodeElectronSystemOperations(),
|
||||
) {
|
||||
}
|
||||
|
||||
public runShellCommand(command: string): Promise<ShellCommandOutcome> {
|
||||
this.logger.info(`Executing command: ${command}`);
|
||||
return new Promise((resolve) => {
|
||||
this.systemOps.command.exec(command)
|
||||
// https://archive.today/2024.01.19-004011/https://nodejs.org/api/child_process.html#child_process_event_exit
|
||||
.on('exit', (
|
||||
code, // The exit code if the child exited on its own.
|
||||
signal, // The signal by which the child process was terminated.
|
||||
) => {
|
||||
// One of `code` or `signal` will always be non-null.
|
||||
// If the process exited, code is the final exit code of the process, otherwise null.
|
||||
if (code !== null) {
|
||||
this.logger.info(`Command completed with exit code ${code}.`);
|
||||
resolve({ type: 'RegularProcessExit', exitCode: code });
|
||||
return; // Prevent further execution to avoid multiple promise resolutions and logs.
|
||||
}
|
||||
// If the process terminated due to receipt of a signal, signal is the string name of
|
||||
// the signal, otherwise null.
|
||||
resolve({ type: 'ExternallyTerminated', terminationSignal: signal as NodeJS.Signals });
|
||||
this.logger.warn(`Command terminated by signal: ${signal}`);
|
||||
})
|
||||
.on('error', (error) => {
|
||||
// https://archive.ph/20200912193803/https://nodejs.org/api/child_process.html#child_process_event_error
|
||||
// The 'error' event is emitted whenever:
|
||||
// - The process could not be spawned, or
|
||||
// - The process could not be killed, or
|
||||
// - Sending a message to the child process failed.
|
||||
// The 'exit' event may or may not fire after an error has occurred.
|
||||
this.logger.error('Command execution failed:', error);
|
||||
resolve({ type: 'ExecutionError', error });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export interface ShellCommandRunner {
|
||||
runShellCommand(command: string): Promise<ShellCommandOutcome>;
|
||||
}
|
||||
|
||||
export type ShellCommandOutcome = ProcessStatus & ({
|
||||
readonly type: 'RegularProcessExit',
|
||||
readonly exitCode: number;
|
||||
} | {
|
||||
readonly type: 'ExternallyTerminated';
|
||||
readonly terminationSignal: NodeJS.Signals;
|
||||
} | {
|
||||
readonly type: 'ExecutionError';
|
||||
readonly error: Error;
|
||||
});
|
||||
|
||||
type ProcessOutcomeType = 'RegularProcessExit' | 'ExternallyTerminated' | 'ExecutionError';
|
||||
|
||||
interface ProcessStatus {
|
||||
readonly type: ProcessOutcomeType;
|
||||
readonly error?: Error;
|
||||
readonly terminationSignal?: NodeJS.Signals;
|
||||
readonly exitCode?: number;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import { OsSpecificTerminalLaunchCommandFactory } from './CommandDefinition/Factory/OsSpecificTerminalLaunchCommandFactory';
|
||||
import { ExecutableFileShellCommandDefinitionRunner } from './CommandDefinition/Runner/ExecutableFileShellCommandDefinitionRunner';
|
||||
import type { ScriptFileExecutionOutcome, ScriptFileExecutor } from './ScriptFileExecutor';
|
||||
import type { CommandDefinitionFactory } from './CommandDefinition/Factory/CommandDefinitionFactory';
|
||||
import type { CommandDefinitionRunner } from './CommandDefinition/Runner/CommandDefinitionRunner';
|
||||
import type { CommandDefinition } from './CommandDefinition/CommandDefinition';
|
||||
|
||||
export class VisibleTerminalFileRunner implements ScriptFileExecutor {
|
||||
constructor(
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
private readonly commandFactory: CommandDefinitionFactory
|
||||
= new OsSpecificTerminalLaunchCommandFactory(),
|
||||
private readonly commandRunner: CommandDefinitionRunner
|
||||
= new ExecutableFileShellCommandDefinitionRunner(),
|
||||
) { }
|
||||
|
||||
public async executeScriptFile(
|
||||
filePath: string,
|
||||
): Promise<ScriptFileExecutionOutcome> {
|
||||
this.logger.info(`Executing script file: ${filePath}.`);
|
||||
const outcome = await this.findAndExecuteCommand(filePath);
|
||||
this.logOutcome(outcome);
|
||||
return outcome;
|
||||
}
|
||||
|
||||
private async findAndExecuteCommand(
|
||||
filePath: string,
|
||||
): Promise<ScriptFileExecutionOutcome> {
|
||||
try {
|
||||
let commandDefinition: CommandDefinition;
|
||||
try {
|
||||
commandDefinition = this.commandFactory.provideCommandDefinition();
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type: 'UnsupportedPlatform',
|
||||
message: `Error finding command: ${error.message}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
const runOutcome = await this.commandRunner.runCommandDefinition(
|
||||
commandDefinition,
|
||||
filePath,
|
||||
);
|
||||
return runOutcome;
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type: 'FileExecutionError',
|
||||
message: `Unexpected error: ${error.message}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private logOutcome(outcome: ScriptFileExecutionOutcome) {
|
||||
if (outcome.success) {
|
||||
this.logger.info('Executed script file in terminal successfully.');
|
||||
return;
|
||||
}
|
||||
this.logger.error(
|
||||
'Failed to execute the script file in terminal.',
|
||||
outcome.error.type,
|
||||
outcome.error.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import type { CommandOps, SystemOperations } from '@/infrastructure/CodeRunner/System/SystemOperations';
|
||||
import type { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import type { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { NodeElectronSystemOperations } from '@/infrastructure/CodeRunner/System/NodeElectronSystemOperations';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import type { CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
|
||||
import { isString } from '@/TypeHelpers';
|
||||
import type { FailedScriptFileExecution, ScriptFileExecutionOutcome, ScriptFileExecutor } from './ScriptFileExecutor';
|
||||
|
||||
export class VisibleTerminalScriptExecutor implements ScriptFileExecutor {
|
||||
constructor(
|
||||
private readonly system: SystemOperations = new NodeElectronSystemOperations(),
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
private readonly environment: RuntimeEnvironment = CurrentEnvironment,
|
||||
) { }
|
||||
|
||||
public async executeScriptFile(filePath: string): Promise<ScriptFileExecutionOutcome> {
|
||||
const { os } = this.environment;
|
||||
if (os === undefined) {
|
||||
return this.handleError('UnsupportedOperatingSystem', 'Operating system could not be identified from environment.');
|
||||
}
|
||||
const filePermissionsResult = await this.setFileExecutablePermissions(filePath);
|
||||
if (!filePermissionsResult.success) {
|
||||
return filePermissionsResult;
|
||||
}
|
||||
const scriptExecutionResult = await this.runFileWithRunner(filePath, os);
|
||||
if (!scriptExecutionResult.success) {
|
||||
return scriptExecutionResult;
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
private async setFileExecutablePermissions(
|
||||
filePath: string,
|
||||
): Promise<ScriptFileExecutionOutcome> {
|
||||
/*
|
||||
This is required on macOS and Linux otherwise the terminal emulators will refuse to
|
||||
execute the script. It's not needed on Windows.
|
||||
*/
|
||||
try {
|
||||
this.logger.info(`Setting execution permissions for file at ${filePath}`);
|
||||
await this.system.fileSystem.setFilePermissions(filePath, '755');
|
||||
this.logger.info(`Execution permissions set successfully for ${filePath}`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return this.handleError('FileExecutionError', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async runFileWithRunner(
|
||||
filePath: string,
|
||||
os: OperatingSystem,
|
||||
): Promise<ScriptFileExecutionOutcome> {
|
||||
this.logger.info(`Executing script file: ${filePath} on ${OperatingSystem[os]}.`);
|
||||
const runner = TerminalRunners[os];
|
||||
if (!runner) {
|
||||
return this.handleError('UnsupportedOperatingSystem', `Unsupported operating system: ${OperatingSystem[os]}`);
|
||||
}
|
||||
const context: TerminalExecutionContext = {
|
||||
scriptFilePath: filePath,
|
||||
commandOps: this.system.command,
|
||||
logger: this.logger,
|
||||
};
|
||||
try {
|
||||
await runner(context);
|
||||
this.logger.info('Command script file successfully.');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return this.handleError('FileExecutionError', error);
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(
|
||||
type: CodeRunErrorType,
|
||||
error: Error | string,
|
||||
): FailedScriptFileExecution {
|
||||
const errorMessage = 'Error during script file execution';
|
||||
this.logger.error([type, errorMessage, ...(error ? [error] : [])]);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type,
|
||||
message: `${errorMessage}: ${isString(error) ? error : errorMessage}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface TerminalExecutionContext {
|
||||
readonly scriptFilePath: string;
|
||||
readonly commandOps: CommandOps;
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
type TerminalRunner = (context: TerminalExecutionContext) => Promise<void>;
|
||||
|
||||
export const LinuxTerminalEmulator = 'x-terminal-emulator';
|
||||
|
||||
const TerminalRunners: Partial<Record<OperatingSystem, TerminalRunner>> = {
|
||||
[OperatingSystem.Windows]: async (context) => {
|
||||
const command = [
|
||||
'PowerShell',
|
||||
'Start-Process',
|
||||
'-Verb RunAs', // Run as administrator with GUI sudo prompt
|
||||
`-FilePath ${cmdShellPathArgumentEscape(context.scriptFilePath)}`,
|
||||
].join(' ');
|
||||
/*
|
||||
📝 Options:
|
||||
`child_process.execFile()`
|
||||
"path", `cmd.exe /c "path"`
|
||||
❌ Script execution in the background without a visible terminal.
|
||||
This occurs only when the user runs the application as administrator, as seen
|
||||
in Windows Pro VMs on Azure.
|
||||
`PowerShell Start -Verb RunAs "path"`
|
||||
✅ Visible terminal window
|
||||
✅ GUI sudo prompt (through `RunAs` option)
|
||||
`PowerShell Start "path"`
|
||||
`explorer.exe "path"`
|
||||
`electron.shell.openPath`
|
||||
`start cmd.exe /c "$path"`
|
||||
✅ Visible terminal window
|
||||
✅ GUI sudo prompt (through `RunAs` option)
|
||||
👍 Among all options `start` command is the most explicit one, being the most resilient
|
||||
against the potential changes in Windows or Electron framework (e.g. https://github.com/electron/electron/issues/36765).
|
||||
`%COMSPEC%` environment variable should be checked before defaulting to `cmd.exe.
|
||||
Related docs: https://web.archive.org/web/20240106002357/https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
|
||||
*/
|
||||
await runCommand(command, context);
|
||||
},
|
||||
[OperatingSystem.Linux]: async (context) => {
|
||||
const command = `${LinuxTerminalEmulator} -e ${posixShellPathArgumentEscape(context.scriptFilePath)}`;
|
||||
/*
|
||||
🤔 Potential improvements:
|
||||
Use user-friendly GUI sudo prompt (not terminal-based).
|
||||
If `pkexec` exists, we could do `x-terminal-emulator -e pkexec 'path'`, which always
|
||||
prompts with user-friendly GUI sudo prompt.
|
||||
📝 Options:
|
||||
`x-terminal-emulator -e 'path'`:
|
||||
✅ Visible terminal window
|
||||
❌ Terminal-based (not GUI) sudo prompt.
|
||||
`x-terminal-emulator -e pkexec 'path'
|
||||
✅ Visible terminal window
|
||||
✅ Always prompts with user-friendly GUI sudo prompt.
|
||||
🤔 Not using `pkexec` as it is not in all Linux distributions. It should have smarter
|
||||
logic to handle if it does not exist.
|
||||
`electron.shell.openPath`:
|
||||
❌ Opens the script in the default text editor, verified on
|
||||
Debian/Ubuntu-based distributions.
|
||||
`child_process.execFile()`:
|
||||
❌ Script execution in the background without a visible terminal.
|
||||
*/
|
||||
await runCommand(command, context);
|
||||
},
|
||||
[OperatingSystem.macOS]: async (context) => {
|
||||
const command = `open -a Terminal.app ${posixShellPathArgumentEscape(context.scriptFilePath)}`;
|
||||
// -a Specifies the application to use for opening the file
|
||||
/* eslint-disable vue/max-len */
|
||||
/*
|
||||
🤔 Potential improvements:
|
||||
Use user-friendly GUI sudo prompt for running the script.
|
||||
📝 Options:
|
||||
`open -a Terminal.app 'path'`
|
||||
✅ Visible terminal window
|
||||
❌ Terminal-based (not GUI) sudo prompt.
|
||||
❌ Terminal app requires many privileges to execute the script, this prompts user
|
||||
to grant privileges to the Terminal app.
|
||||
`osascript -e 'do shell script "'/tmp/test.sh'" with administrator privileges'`
|
||||
✅ Script as root
|
||||
✅ GUI sudo prompt.
|
||||
❌ Script execution in the background without a visible terminal.
|
||||
`osascript -e 'do shell script "open -a 'Terminal.app' '/tmp/test.sh'" with administrator privileges'`
|
||||
❌ Script as user, not root
|
||||
✅ GUI sudo prompt.
|
||||
✅ Visible terminal window
|
||||
`osascript -e 'do shell script "/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal '/tmp/test.sh'" with administrator privileges'`
|
||||
✅ Script as root
|
||||
✅ GUI sudo prompt.
|
||||
✅ Visible terminal window
|
||||
Useful resources about `do shell script .. with administrator privileges`:
|
||||
- Change "osascript wants to make changes" prompt: https://web.archive.org/web/20240109191128/https://apple.stackexchange.com/questions/283353/how-to-rename-osascript-in-the-administrator-privileges-dialog
|
||||
- More about `do shell script`: https://web.archive.org/web/20100906222226/http://developer.apple.com/mac/library/technotes/tn2002/tn2065.html
|
||||
*/
|
||||
/* eslint-enable vue/max-len */
|
||||
await runCommand(command, context);
|
||||
},
|
||||
} as const;
|
||||
|
||||
async function runCommand(command: string, context: TerminalExecutionContext): Promise<void> {
|
||||
context.logger.info(`Executing command:\n${command}`);
|
||||
await context.commandOps.exec(command);
|
||||
context.logger.info('Executed command successfully.');
|
||||
}
|
||||
|
||||
function posixShellPathArgumentEscape(pathArgument: string): string {
|
||||
/*
|
||||
- Wraps the path in single quotes, which is a standard practice in POSIX shells
|
||||
(like bash and zsh) found on macOS/Linux to ensure that characters like spaces, '*', and
|
||||
'?' are treated as literals, not as special characters.
|
||||
- Escapes any single quotes within the path itself. This allows paths containing single
|
||||
quotes to be correctly interpreted in POSIX-compliant systems, such as Linux and macOS.
|
||||
*/
|
||||
return `'${pathArgument.replaceAll('\'', '\'\\\'\'')}'`;
|
||||
}
|
||||
|
||||
function cmdShellPathArgumentEscape(pathArgument: string): string {
|
||||
// - Encloses the path in double quotes, which is necessary for Windows command line (cmd.exe)
|
||||
// to correctly handle paths containing spaces.
|
||||
// - Paths in Windows cannot include double quotes `"` themselves, so these are not escaped.
|
||||
return `"${pathArgument}"`;
|
||||
}
|
||||
@@ -4,15 +4,15 @@ import type {
|
||||
CodeRunError, CodeRunOutcome, CodeRunner, FailedCodeRun,
|
||||
} from '@/application/CodeRunner/CodeRunner';
|
||||
import { ElectronLogger } from '../Log/ElectronLogger';
|
||||
import { VisibleTerminalScriptExecutor } from './Execution/VisibleTerminalScriptFileExecutor';
|
||||
import { ScriptFileCreationOrchestrator } from './Creation/ScriptFileCreationOrchestrator';
|
||||
import { VisibleTerminalFileRunner } from './Execution/VisibleTerminalFileRunner';
|
||||
import type { ScriptFileExecutor } from './Execution/ScriptFileExecutor';
|
||||
import type { ScriptFileCreator } from './Creation/ScriptFileCreator';
|
||||
|
||||
export class ScriptFileCodeRunner implements CodeRunner {
|
||||
constructor(
|
||||
private readonly scriptFileExecutor
|
||||
: ScriptFileExecutor = new VisibleTerminalScriptExecutor(),
|
||||
: ScriptFileExecutor = new VisibleTerminalFileRunner(),
|
||||
private readonly scriptFileCreator: ScriptFileCreator = new ScriptFileCreationOrchestrator(),
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
) { }
|
||||
|
||||
@@ -6,6 +6,9 @@ import type {
|
||||
CommandOps, FileSystemOps, LocationOps, OperatingSystemOps, SystemOperations,
|
||||
} from './SystemOperations';
|
||||
|
||||
/**
|
||||
* Thin wrapper for Node and Electron APIs.
|
||||
*/
|
||||
export class NodeElectronSystemOperations implements SystemOperations {
|
||||
public readonly operatingSystem: OperatingSystemOps = {
|
||||
/*
|
||||
@@ -49,13 +52,6 @@ export class NodeElectronSystemOperations implements SystemOperations {
|
||||
};
|
||||
|
||||
public readonly command: CommandOps = {
|
||||
exec: (command) => new Promise((resolve, reject) => {
|
||||
exec(command, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
exec,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { exec } from 'node:child_process';
|
||||
|
||||
export interface SystemOperations {
|
||||
readonly operatingSystem: OperatingSystemOps;
|
||||
readonly location: LocationOps;
|
||||
@@ -14,7 +16,7 @@ export interface LocationOps {
|
||||
}
|
||||
|
||||
export interface CommandOps {
|
||||
exec(command: string): Promise<void>;
|
||||
exec(command: string): ReturnType<typeof exec>;
|
||||
}
|
||||
|
||||
export interface FileSystemOps {
|
||||
|
||||
@@ -13,11 +13,16 @@
|
||||
@use "_code-styling" as *;
|
||||
@use "_margin-padding" as *;
|
||||
@use "_link-styling" as *;
|
||||
@use "_prevent-scrollbar-layout-shift" as *;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
@include prevent-scrollbar-layout-shift;
|
||||
}
|
||||
|
||||
body {
|
||||
background: $color-background;
|
||||
@include base-font-style;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// This mixin prevents layout shifts caused by the appearance of a vertical scrollbar
|
||||
// in Chromium-based browsers on Linux and Windows.
|
||||
// It creates a reserved space for the scrollbar, ensuring content remains stable and does
|
||||
// not shift horizontally when the scrollbar appears.
|
||||
@mixin prevent-scrollbar-layout-shift {
|
||||
scrollbar-gutter: stable;
|
||||
|
||||
@supports not (scrollbar-gutter: stable) { // https://caniuse.com/mdn-css_properties_scrollbar-gutter
|
||||
// Safari workaround: Shift content to accommodate non-overlay scrollbar.
|
||||
// An issue: On small screens, the appearance of the scrollbar can shift content, due to limited space for
|
||||
// both content and scrollbar.
|
||||
$full-width-including-scrollbar: 100vw;
|
||||
$full-width-excluding-scrollbar: 100%;
|
||||
$scrollbar-width: calc($full-width-including-scrollbar - $full-width-excluding-scrollbar);
|
||||
padding-inline-start: $scrollbar-width; // Allows both right-to-left (RTL) and left-to-right (LTR) text direction support
|
||||
}
|
||||
|
||||
// More details: https://web.archive.org/web/20240509122237/https://stackoverflow.com/questions/1417934/how-to-prevent-scrollbar-from-repositioning-web-page
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { useCodeRunner } from '@/presentation/components/Shared/Hooks/UseCodeRun
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { useDialog } from '@/presentation/components/Shared/Hooks/Dialog/UseDialog';
|
||||
import { useScriptDiagnosticsCollector } from '@/presentation/components/Shared/Hooks/UseScriptDiagnosticsCollector';
|
||||
import { useAutoUnsubscribedEventListener } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener';
|
||||
|
||||
export function provideDependencies(
|
||||
context: IApplicationContext,
|
||||
@@ -77,6 +78,10 @@ export function provideDependencies(
|
||||
InjectionKeys.useScriptDiagnosticsCollector,
|
||||
useScriptDiagnosticsCollector,
|
||||
),
|
||||
useAutoUnsubscribedEventListener: (di) => di.provide(
|
||||
InjectionKeys.useAutoUnsubscribedEventListener,
|
||||
useAutoUnsubscribedEventListener,
|
||||
),
|
||||
};
|
||||
registerAll(Object.values(resolvers), api);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import type { CodeRunError } from '@/application/CodeRunner/CodeRunner';
|
||||
import IconButton from './IconButton.vue';
|
||||
import { createScriptErrorDialog } from './ScriptErrorDialog';
|
||||
|
||||
@@ -38,15 +39,19 @@ export default defineComponent({
|
||||
currentContext.state.collection.scripting.fileExtension,
|
||||
);
|
||||
if (!success) {
|
||||
dialog.showError(...(await createScriptErrorDialog({
|
||||
errorContext: 'run',
|
||||
errorType: error.type,
|
||||
errorMessage: error.message,
|
||||
isFileReadbackError: error.type === 'FileReadbackVerificationError',
|
||||
}, scriptDiagnosticsCollector)));
|
||||
await handleCodeRunFailure(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCodeRunFailure(error: CodeRunError) {
|
||||
dialog.showError(...(await createScriptErrorDialog({
|
||||
errorContext: 'run',
|
||||
errorType: error.type,
|
||||
errorMessage: error.message,
|
||||
isFileReadbackError: error.type === 'FileReadbackVerificationError',
|
||||
}, scriptDiagnosticsCollector)));
|
||||
}
|
||||
|
||||
return {
|
||||
canRun,
|
||||
runCode,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<span class="info-container">
|
||||
<span
|
||||
class="info-container"
|
||||
>
|
||||
<TooltipWrapper>
|
||||
<AppIcon icon="circle-info" />
|
||||
<template #tooltip>
|
||||
@@ -19,27 +21,17 @@ export default defineComponent({
|
||||
TooltipWrapper,
|
||||
AppIcon,
|
||||
},
|
||||
props: {
|
||||
hasLeftMargin: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "@/presentation/assets/styles/main" as *;
|
||||
|
||||
@mixin apply-style-when-placed-after-non-text {
|
||||
* + & {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
<style lang="scss">
|
||||
.info-container {
|
||||
vertical-align: text-top;
|
||||
|
||||
* + & { // If it's followed by any other element
|
||||
vertical-align: middle;
|
||||
@include set-property-ch-value-with-fallback(
|
||||
$property: margin-left,
|
||||
$value-in-ch: 0.5,
|
||||
)
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<span class="info-tooltip-wrapper">
|
||||
<span>
|
||||
<slot />
|
||||
</span>
|
||||
<span>
|
||||
<InfoTooltipInline>
|
||||
<slot name="info" />
|
||||
</InfoTooltipInline>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import InfoTooltipInline from './InfoTooltipInline.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
InfoTooltipInline,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/presentation/assets/styles/main" as *;
|
||||
|
||||
.info-tooltip-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include set-property-ch-value-with-fallback(
|
||||
$property: gap,
|
||||
$value-in-ch: 0.5,
|
||||
)
|
||||
}
|
||||
</style>
|
||||
@@ -17,7 +17,7 @@
|
||||
<p>
|
||||
This requires you to do additional manual
|
||||
steps. If you are unsure how to follow the instructions, tap or hover on information
|
||||
<InfoTooltip>Engage with icons like this for extra wisdom!</InfoTooltip>
|
||||
<InfoTooltipInline>Engage with icons like this for extra wisdom!</InfoTooltipInline>
|
||||
icons near the steps, or follow the easy alternative described above.
|
||||
</p>
|
||||
<p>
|
||||
@@ -32,12 +32,12 @@ import { defineComponent, computed } from 'vue';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { getOperatingSystemDisplayName } from '@/presentation/components/Shared/OperatingSystemNames';
|
||||
import InfoTooltip from './InfoTooltip.vue';
|
||||
import InfoTooltipInline from './Help/InfoTooltipInline.vue';
|
||||
import PlatformInstructionSteps from './Steps/PlatformInstructionSteps.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
InfoTooltip,
|
||||
InfoTooltipInline,
|
||||
PlatformInstructionSteps,
|
||||
},
|
||||
props: {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<InstructionSteps>
|
||||
<InstructionStep>
|
||||
Download the file.
|
||||
<InfoTooltip>
|
||||
<InfoTooltipInline>
|
||||
<p>
|
||||
You should have already been prompted to save the script file.
|
||||
</p>
|
||||
@@ -10,11 +10,11 @@
|
||||
If this was not the case or you did not save the script when prompted,
|
||||
please try to download your script file again.
|
||||
</p>
|
||||
</InfoTooltip>
|
||||
</InfoTooltipInline>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
Open terminal.
|
||||
<InfoTooltip>
|
||||
<InfoTooltipInline>
|
||||
<p>
|
||||
Opening terminal changes based on the distro you run.
|
||||
</p>
|
||||
@@ -39,30 +39,32 @@
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
</InfoTooltip>
|
||||
</InfoTooltipInline>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
<p>
|
||||
Navigate to the folder where you downloaded the file e.g.:
|
||||
</p>
|
||||
<p>
|
||||
<CopyableCommand>cd ~/Downloads</CopyableCommand>
|
||||
<InfoTooltip>
|
||||
<p>
|
||||
Press on <code>enter/return</code> key after running the command.
|
||||
</p>
|
||||
<p>
|
||||
If the file is not downloaded on Downloads folder,
|
||||
change <code>Downloads</code> to path where the file is downloaded.
|
||||
</p>
|
||||
<p>
|
||||
This command means:
|
||||
<ul>
|
||||
<li><code>cd</code> will change the current folder.</li>
|
||||
<li><code>~</code> is the user home directory.</li>
|
||||
</ul>
|
||||
</p>
|
||||
</InfoTooltip>
|
||||
<InfoTooltipWrapper>
|
||||
<CopyableCommand>cd ~/Downloads</CopyableCommand>
|
||||
<template #info>
|
||||
<p>
|
||||
Press on <code>enter/return</code> key after running the command.
|
||||
</p>
|
||||
<p>
|
||||
If the file is not downloaded on Downloads folder,
|
||||
change <code>Downloads</code> to path where the file is downloaded.
|
||||
</p>
|
||||
<p>
|
||||
This command means:
|
||||
<ul>
|
||||
<li><code>cd</code> will change the current folder.</li>
|
||||
<li><code>~</code> is the user home directory.</li>
|
||||
</ul>
|
||||
</p>
|
||||
</template>
|
||||
</InfoTooltipWrapper>
|
||||
</p>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
@@ -70,26 +72,28 @@
|
||||
Give the file execute permissions:
|
||||
</p>
|
||||
<p>
|
||||
<CopyableCommand>chmod +x {{ filename }}</CopyableCommand>
|
||||
<InfoTooltip>
|
||||
<p>
|
||||
Press on <code>enter/return</code> key after running the command.
|
||||
</p>
|
||||
<p>
|
||||
It will make the file executable.
|
||||
</p>
|
||||
<p>
|
||||
If you use desktop environment you can alternatively (instead of running the command):
|
||||
<ol>
|
||||
<li>Locate the file using your file manager.</li>
|
||||
<li>Right click on the file, select "Properties".</li>
|
||||
<li>Go to "Permissions" and check "Allow executing file as program".</li>
|
||||
</ol>
|
||||
</p>
|
||||
<p>
|
||||
These GUI steps and name of options may change depending on your file manager.'
|
||||
</p>
|
||||
</InfoTooltip>
|
||||
<InfoTooltipWrapper>
|
||||
<CopyableCommand>chmod +x {{ filename }}</CopyableCommand>
|
||||
<template #info>
|
||||
<p>
|
||||
Press on <code>enter/return</code> key after running the command.
|
||||
</p>
|
||||
<p>
|
||||
It will make the file executable.
|
||||
</p>
|
||||
<p>
|
||||
If you use desktop environment you can alternatively (instead of running the command):
|
||||
<ol>
|
||||
<li>Locate the file using your file manager.</li>
|
||||
<li>Right click on the file, select "Properties".</li>
|
||||
<li>Go to "Permissions" and check "Allow executing file as program".</li>
|
||||
</ol>
|
||||
</p>
|
||||
<p>
|
||||
These GUI steps and name of options may change depending on your file manager.'
|
||||
</p>
|
||||
</template>
|
||||
</InfoTooltipWrapper>
|
||||
</p>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
@@ -97,21 +101,24 @@
|
||||
Execute the file:
|
||||
</p>
|
||||
<p>
|
||||
<CopyableCommand>./{{ filename }}</CopyableCommand>
|
||||
<InfoTooltip>
|
||||
<p>
|
||||
If you have desktop environment, instead of running this command you can alternatively:
|
||||
</p>
|
||||
<ol>
|
||||
<li>Locate the file using your file manager.</li>
|
||||
<li>Right click on the file, select "Run as program".</li>
|
||||
</ol>
|
||||
</InfoTooltip>
|
||||
<InfoTooltipWrapper>
|
||||
<CopyableCommand>./{{ filename }}</CopyableCommand>
|
||||
<template #info>
|
||||
<p>
|
||||
If you have desktop environment, instead of running this command
|
||||
you can alternatively:
|
||||
</p>
|
||||
<ol>
|
||||
<li>Locate the file using your file manager.</li>
|
||||
<li>Right click on the file, select "Run as program".</li>
|
||||
</ol>
|
||||
</template>
|
||||
</InfoTooltipWrapper>
|
||||
</p>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
If asked, enter your administrator password.
|
||||
<InfoTooltip>
|
||||
<InfoTooltipInline>
|
||||
<p>
|
||||
As you type, your password will be hidden but the keys are still
|
||||
registered, so keep typing.
|
||||
@@ -122,7 +129,7 @@
|
||||
<p>
|
||||
Administrator privileges are required to configure OS.
|
||||
</p>
|
||||
</InfoTooltip>
|
||||
</InfoTooltipInline>
|
||||
</InstructionStep>
|
||||
</InstructionSteps>
|
||||
</template>
|
||||
@@ -131,13 +138,15 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import InstructionSteps from '../InstructionSteps.vue';
|
||||
import InstructionStep from '../InstructionStep.vue';
|
||||
import InfoTooltip from '../../InfoTooltip.vue';
|
||||
import InfoTooltipInline from '../../Help/InfoTooltipInline.vue';
|
||||
import InfoTooltipWrapper from '../../Help/InfoTooltipWrapper.vue';
|
||||
import CopyableCommand from '../CopyableCommand.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
CopyableCommand,
|
||||
InfoTooltip,
|
||||
InfoTooltipInline,
|
||||
InfoTooltipWrapper,
|
||||
InstructionSteps,
|
||||
InstructionStep,
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<InstructionSteps>
|
||||
<InstructionStep>
|
||||
Download the file.
|
||||
<InfoTooltip>
|
||||
<InfoTooltipInline>
|
||||
<p>
|
||||
You should have already been prompted to save the script file.
|
||||
</p>
|
||||
@@ -10,38 +10,38 @@
|
||||
If this was not the case or you did not save the script when prompted,
|
||||
please try to download your script file again.
|
||||
</p>
|
||||
</InfoTooltip>
|
||||
</InfoTooltipInline>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
Open terminal.
|
||||
<InfoTooltip>
|
||||
<InfoTooltipInline>
|
||||
Type Terminal into Spotlight or open it from the Applications -> Utilities folder.
|
||||
</InfoTooltip>
|
||||
</InfoTooltipInline>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
<p>
|
||||
Navigate to the folder where you downloaded the file e.g.:
|
||||
</p>
|
||||
<p>
|
||||
<CopyableCommand>
|
||||
cd ~/Downloads
|
||||
</CopyableCommand>
|
||||
<InfoTooltip>
|
||||
<p>
|
||||
Press on <code>enter/return</code> key after running the command.
|
||||
</p>
|
||||
<p>
|
||||
If the file is not downloaded on Downloads folder,
|
||||
change <code>Downloads</code> to path where the file is downloaded.
|
||||
</p>
|
||||
<p>
|
||||
This command means:
|
||||
<ul>
|
||||
<li><code>cd</code> will change the current folder.</li>
|
||||
<li><code>~</code> is the user home directory.</li>
|
||||
</ul>
|
||||
</p>
|
||||
</InfoTooltip>
|
||||
<InfoTooltipWrapper>
|
||||
<CopyableCommand>cd ~/Downloads</CopyableCommand>
|
||||
<template #info>
|
||||
<p>
|
||||
Press on <code>enter/return</code> key after running the command.
|
||||
</p>
|
||||
<p>
|
||||
If the file is not downloaded on Downloads folder,
|
||||
change <code>Downloads</code> to path where the file is downloaded.
|
||||
</p>
|
||||
<p>
|
||||
This command means:
|
||||
<ul>
|
||||
<li><code>cd</code> will change the current folder.</li>
|
||||
<li><code>~</code> is the user home directory.</li>
|
||||
</ul>
|
||||
</p>
|
||||
</template>
|
||||
</InfoTooltipWrapper>
|
||||
</p>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
@@ -49,15 +49,17 @@
|
||||
Give the file execute permissions:
|
||||
</p>
|
||||
<p>
|
||||
<CopyableCommand>chmod +x {{ filename }}</CopyableCommand>
|
||||
<InfoTooltip>
|
||||
<p>
|
||||
Press on <code>enter/return</code> key after running the command.
|
||||
</p>
|
||||
<p>
|
||||
It will make the file executable.
|
||||
</p>
|
||||
</InfoTooltip>
|
||||
<InfoTooltipWrapper>
|
||||
<CopyableCommand>chmod +x {{ filename }}</CopyableCommand>
|
||||
<template #info>
|
||||
<p>
|
||||
Press on <code>enter/return</code> key after running the command.
|
||||
</p>
|
||||
<p>
|
||||
It will make the file executable.
|
||||
</p>
|
||||
</template>
|
||||
</InfoTooltipWrapper>
|
||||
</p>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
@@ -65,15 +67,17 @@
|
||||
Execute the file:
|
||||
</p>
|
||||
<p>
|
||||
<CopyableCommand>./{{ filename }}</CopyableCommand>
|
||||
<InfoTooltip>
|
||||
Alternatively you can locate the file in <strong>Finder</strong> and double click on it.
|
||||
</InfoTooltip>
|
||||
<InfoTooltipWrapper>
|
||||
<CopyableCommand>./{{ filename }}</CopyableCommand>
|
||||
<template #info>
|
||||
Alternatively you can locate the file in <strong>Finder</strong> and double click on it.
|
||||
</template>
|
||||
</InfoTooltipWrapper>
|
||||
</p>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
If asked, enter your administrator password.
|
||||
<InfoTooltip>
|
||||
<InfoTooltipInline>
|
||||
<p>
|
||||
As you type, your password will be hidden but the keys are
|
||||
still registered, so keep typing.
|
||||
@@ -84,7 +88,7 @@
|
||||
<p>
|
||||
Administrator privileges are required to configure OS.
|
||||
</p>
|
||||
</InfoTooltip>
|
||||
</InfoTooltipInline>
|
||||
</InstructionStep>
|
||||
</InstructionSteps>
|
||||
</template>
|
||||
@@ -93,13 +97,15 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import InstructionSteps from '../InstructionSteps.vue';
|
||||
import InstructionStep from '../InstructionStep.vue';
|
||||
import InfoTooltip from '../../InfoTooltip.vue';
|
||||
import InfoTooltipInline from '../../Help/InfoTooltipInline.vue';
|
||||
import InfoTooltipWrapper from '../../Help/InfoTooltipWrapper.vue';
|
||||
import CopyableCommand from '../CopyableCommand.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
CopyableCommand,
|
||||
InfoTooltip,
|
||||
InfoTooltipInline,
|
||||
InfoTooltipWrapper,
|
||||
InstructionSteps,
|
||||
InstructionStep,
|
||||
},
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<InstructionSteps>
|
||||
<InstructionStep>
|
||||
Download the file.
|
||||
<InfoTooltip>
|
||||
<InfoTooltipInline>
|
||||
<p>If a save prompt doesn't appear, try downloading the script again.</p>
|
||||
</InfoTooltip>
|
||||
</InfoTooltipInline>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
If warned by your browser, keep the file.
|
||||
<InfoTooltip>
|
||||
<InfoTooltipInline>
|
||||
<!--
|
||||
Tests (15/01/2023):
|
||||
- Edge (Defender activated): "filename isn't commonly downloaded..."
|
||||
@@ -33,11 +33,11 @@
|
||||
For <strong>Firefox</strong> and <strong>Chrome</strong>, typically no additional
|
||||
action is needed.
|
||||
</p>
|
||||
</InfoTooltip>
|
||||
</InfoTooltipInline>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
If your antivirus (e.g., Defender) alerts you, address the warning.
|
||||
<InfoTooltip>
|
||||
<InfoTooltipInline>
|
||||
<!--
|
||||
Tests (15/01/2023):
|
||||
- Edge (Defender activated): "Couldn't download - Virus detected"
|
||||
@@ -75,7 +75,7 @@
|
||||
<li>and keep real-time protection enabled whenever possible.</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
</InfoTooltip>
|
||||
</InfoTooltipInline>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
<!--
|
||||
@@ -85,7 +85,7 @@
|
||||
- Firefox: "filename is executable file. Executable files may contain..?" OK/Cancel
|
||||
-->
|
||||
Open the downloaded file.
|
||||
<InfoTooltip>
|
||||
<InfoTooltipInline>
|
||||
<p>
|
||||
Confirm any browser prompts to open the file.
|
||||
</p>
|
||||
@@ -100,11 +100,11 @@
|
||||
<strong>Edge</strong> and <strong>Chrome</strong> users usually will not
|
||||
encounter additional prompts.
|
||||
</p>
|
||||
</InfoTooltip>
|
||||
</InfoTooltipInline>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
If prompted, confirm SmartScreen warnings.
|
||||
<InfoTooltip>
|
||||
<InfoTooltipInline>
|
||||
<p>
|
||||
Windows SmartScreen might display a cautionary message.
|
||||
</p>
|
||||
@@ -118,11 +118,11 @@
|
||||
<li>Select <strong>Run anyway</strong>.</li>
|
||||
</ol>
|
||||
</p>
|
||||
</InfoTooltip>
|
||||
</InfoTooltipInline>
|
||||
</InstructionStep>
|
||||
<InstructionStep>
|
||||
If administrative permissions are requested, grant them.
|
||||
<InfoTooltip>
|
||||
<InfoTooltipInline>
|
||||
<p>
|
||||
The script may request administrative rights to apply changes.
|
||||
</p>
|
||||
@@ -132,7 +132,7 @@
|
||||
<p>
|
||||
Click <strong>Yes</strong> to authorize and run the script.
|
||||
</p>
|
||||
</InfoTooltip>
|
||||
</InfoTooltipInline>
|
||||
</InstructionStep>
|
||||
</InstructionSteps>
|
||||
</template>
|
||||
@@ -141,11 +141,11 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import InstructionSteps from '../InstructionSteps.vue';
|
||||
import InstructionStep from '../InstructionStep.vue';
|
||||
import InfoTooltip from '../../InfoTooltip.vue';
|
||||
import InfoTooltipInline from '../../Help/InfoTooltipInline.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
InfoTooltip,
|
||||
InfoTooltipInline,
|
||||
InstructionSteps,
|
||||
InstructionStep,
|
||||
},
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import type { CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
|
||||
import type { ScriptDiagnosticData, ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import type { Dialog } from '@/presentation/common/Dialog';
|
||||
import type { Dialog, SaveFileErrorType } from '@/presentation/common/Dialog';
|
||||
|
||||
type ErrorDialogParameters = Parameters<Dialog['showError']>;
|
||||
|
||||
export async function createScriptErrorDialog(
|
||||
information: ScriptErrorDetails,
|
||||
scriptDiagnosticsCollector: ScriptDiagnosticsCollector | undefined,
|
||||
): Promise<Parameters<Dialog['showError']>> {
|
||||
): Promise<ErrorDialogParameters> {
|
||||
const diagnostics = await scriptDiagnosticsCollector?.collectDiagnosticInformation();
|
||||
if (information.isFileReadbackError) {
|
||||
return createAntivirusErrorDialog(information, diagnostics);
|
||||
}
|
||||
if (information.errorContext === 'run'
|
||||
&& information.errorType === 'ExternalProcessTermination') {
|
||||
return createScriptInterruptedDialog(information);
|
||||
}
|
||||
return createGenericErrorDialog(information, diagnostics);
|
||||
}
|
||||
|
||||
export interface ScriptErrorDetails {
|
||||
readonly errorContext: 'run' | 'save';
|
||||
readonly errorType: string;
|
||||
readonly errorType: CodeRunErrorType | SaveFileErrorType;
|
||||
readonly errorMessage: string;
|
||||
readonly isFileReadbackError: boolean;
|
||||
}
|
||||
@@ -23,7 +30,7 @@ export interface ScriptErrorDetails {
|
||||
function createGenericErrorDialog(
|
||||
information: ScriptErrorDetails,
|
||||
diagnostics: ScriptDiagnosticData | undefined,
|
||||
): Parameters<Dialog['showError']> {
|
||||
): ErrorDialogParameters {
|
||||
return [
|
||||
selectBasedOnErrorContext({
|
||||
runningScript: 'Error Running Script',
|
||||
@@ -66,7 +73,7 @@ function createGenericErrorDialog(
|
||||
function createAntivirusErrorDialog(
|
||||
information: ScriptErrorDetails,
|
||||
diagnostics: ScriptDiagnosticData | undefined,
|
||||
): Parameters<Dialog['showError']> {
|
||||
): ErrorDialogParameters {
|
||||
const defenderSteps = generateDefenderSteps(information, diagnostics);
|
||||
return [
|
||||
'Possible Antivirus Script Block',
|
||||
@@ -117,6 +124,33 @@ function createAntivirusErrorDialog(
|
||||
];
|
||||
}
|
||||
|
||||
function createScriptInterruptedDialog(
|
||||
information: ScriptErrorDetails,
|
||||
): ErrorDialogParameters {
|
||||
return [
|
||||
'Script Stopped',
|
||||
[
|
||||
'The script stopped before it could finish.',
|
||||
'This happens if the script is cancelled manually or if the system terminates the process.',
|
||||
'\n',
|
||||
generateUnorderedSolutionList({
|
||||
title: 'To ensure successful script completion:',
|
||||
solutions: [
|
||||
'Keep the terminal window open during script execution.',
|
||||
'If the script closed unexpectedly, try running it again.',
|
||||
'Check for sufficient memory (RAM) and system resources.',
|
||||
'Avoid running tasks that might disrupt the script.',
|
||||
],
|
||||
}),
|
||||
'\n',
|
||||
'If you intentionally stopped the script, ignore this message.',
|
||||
'Reach out to the community for further assistance.',
|
||||
'\n',
|
||||
generateTechnicalDetails(information),
|
||||
].join('\n'),
|
||||
];
|
||||
}
|
||||
|
||||
interface SolutionListOptions {
|
||||
readonly solutions: readonly string[];
|
||||
readonly title: string;
|
||||
|
||||
@@ -31,6 +31,7 @@ import { defineComponent, ref } from 'vue';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
|
||||
import { dumpNames } from './DumpNames';
|
||||
import { useScrollbarGutterWidth } from './UseScrollbarGutterWidth';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -39,6 +40,7 @@ export default defineComponent({
|
||||
setup() {
|
||||
const { log } = injectKey((keys) => keys.useLogger);
|
||||
const isOpen = ref(true);
|
||||
const scrollbarGutterWidth = useScrollbarGutterWidth();
|
||||
|
||||
const devActions: readonly DevAction[] = [
|
||||
{
|
||||
@@ -58,6 +60,7 @@ export default defineComponent({
|
||||
devActions,
|
||||
isOpen,
|
||||
close,
|
||||
scrollbarGutterWidth,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -71,10 +74,14 @@ interface DevAction {
|
||||
<style scoped lang="scss">
|
||||
@use "@/presentation/assets/styles/main" as *;
|
||||
|
||||
$viewport-edge-offset: $spacing-absolute-large; // close to Chromium gutter width (15px)
|
||||
|
||||
.dev-toolkit-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
top: $viewport-edge-offset;
|
||||
right: max(v-bind(scrollbarGutterWidth), $viewport-edge-offset);
|
||||
|
||||
background-color: rgba($color-on-surface, 0.5);
|
||||
color: $color-on-primary;
|
||||
padding: $spacing-absolute-medium;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
computed, readonly, ref, shallowRef, watch,
|
||||
} from 'vue';
|
||||
import { throttle } from '@/application/Common/Timing/Throttle';
|
||||
import { useAutoUnsubscribedEventListener } from '../Shared/Hooks/UseAutoUnsubscribedEventListener';
|
||||
import { useResizeObserver } from '../Shared/Hooks/Resize/UseResizeObserver';
|
||||
|
||||
const RESIZE_EVENT_THROTTLE_MS = 200;
|
||||
|
||||
export function useScrollbarGutterWidth() {
|
||||
const scrollbarWidthInPx = ref(getScrollbarGutterWidth());
|
||||
|
||||
const { startListening } = useAutoUnsubscribedEventListener();
|
||||
startListening(window, 'resize', throttle(() => {
|
||||
scrollbarWidthInPx.value = getScrollbarGutterWidth();
|
||||
}, RESIZE_EVENT_THROTTLE_MS));
|
||||
|
||||
const bodyWidth = useBodyWidth();
|
||||
watch(() => bodyWidth.value, () => {
|
||||
scrollbarWidthInPx.value = getScrollbarGutterWidth();
|
||||
}, { immediate: false });
|
||||
|
||||
const scrollbarWidthStyle = computed(() => `${scrollbarWidthInPx.value}px`);
|
||||
return readonly(scrollbarWidthStyle);
|
||||
}
|
||||
|
||||
function getScrollbarGutterWidth(): number {
|
||||
return document.documentElement.clientWidth - document.documentElement.offsetWidth;
|
||||
}
|
||||
|
||||
function useBodyWidth() {
|
||||
const width = ref(document.body.offsetWidth);
|
||||
useResizeObserver(
|
||||
{
|
||||
observedElementRef: shallowRef(document.body),
|
||||
throttleInMs: RESIZE_EVENT_THROTTLE_MS,
|
||||
observeCallback: (entries) => {
|
||||
for (const entry of entries) {
|
||||
width.value = entry.borderBoxSize[0].inlineSize;
|
||||
}
|
||||
},
|
||||
observeOptions: { box: 'border-box' },
|
||||
},
|
||||
);
|
||||
return readonly(width);
|
||||
}
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent, ref, onMounted, onUnmounted, computed,
|
||||
defineComponent, ref, computed,
|
||||
} from 'vue';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
|
||||
@@ -54,6 +54,7 @@ export default defineComponent({
|
||||
},
|
||||
setup() {
|
||||
const { currentState, onStateChange } = injectKey((keys) => keys.useCollectionState);
|
||||
const { startListening } = injectKey((keys) => keys.useAutoUnsubscribedEventListener);
|
||||
|
||||
const width = ref<number | undefined>();
|
||||
|
||||
@@ -70,7 +71,7 @@ export default defineComponent({
|
||||
collapseAllCards();
|
||||
}, { immediate: true });
|
||||
|
||||
const outsideClickListener = (event: PointerEvent): void => {
|
||||
startListening(document, 'click', (event) => {
|
||||
if (areAllCardsCollapsed()) {
|
||||
return;
|
||||
}
|
||||
@@ -79,14 +80,6 @@ export default defineComponent({
|
||||
if (element && !element.contains(target)) {
|
||||
onOutsideOfActiveCardClicked(target);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', outsideClickListener);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', outsideClickListener);
|
||||
});
|
||||
|
||||
function onOutsideOfActiveCardClicked(clickedElement: Element): void {
|
||||
|
||||
@@ -68,9 +68,7 @@ export default defineComponent({
|
||||
flex-direction: column;
|
||||
flex: 1; // Expands the container to fill available horizontal space, enabling alignment of child items.
|
||||
max-width: 100%; // Prevents horizontal expansion of inner content (e.g., when a code block is shown)
|
||||
*:not(:first-child) {
|
||||
margin-left: $spacing-absolute-small;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -80,6 +78,7 @@ export default defineComponent({
|
||||
}
|
||||
.docs {
|
||||
background: $color-primary-darkest;
|
||||
margin-left: $spacing-absolute-small;
|
||||
margin-top: $spacing-relative-x-small;
|
||||
color: $color-on-primary;
|
||||
text-transform: none;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { useAutoUnsubscribedEventListener, type UseEventListener } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener';
|
||||
|
||||
export function useKeyboardInteractionState(window: WindowWithEventListeners = globalThis.window) {
|
||||
export function useKeyboardInteractionState(
|
||||
eventTarget: EventTarget = DefaultEventSource,
|
||||
useEventListener: UseEventListener = useAutoUnsubscribedEventListener,
|
||||
) {
|
||||
const { startListening } = useEventListener();
|
||||
const isKeyboardBeingUsed = ref(false);
|
||||
|
||||
const enableKeyboardFocus = () => {
|
||||
@@ -17,20 +22,10 @@ export function useKeyboardInteractionState(window: WindowWithEventListeners = g
|
||||
isKeyboardBeingUsed.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', enableKeyboardFocus, true);
|
||||
window.addEventListener('click', disableKeyboardFocus, true);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', enableKeyboardFocus);
|
||||
window.removeEventListener('click', disableKeyboardFocus);
|
||||
});
|
||||
startListening(eventTarget, 'keydown', enableKeyboardFocus);
|
||||
startListening(eventTarget, 'click', disableKeyboardFocus);
|
||||
|
||||
return { isKeyboardBeingUsed };
|
||||
}
|
||||
|
||||
export interface WindowWithEventListeners {
|
||||
addEventListener: typeof global.window.addEventListener;
|
||||
removeEventListener: typeof global.window.removeEventListener;
|
||||
}
|
||||
export const DefaultEventSource = document;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { onMounted, onUnmounted, type Ref } from 'vue';
|
||||
import { type Ref } from 'vue';
|
||||
import { useAutoUnsubscribedEventListener, type UseEventListener } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener';
|
||||
import { TreeNodeCheckState } from './Node/State/CheckState';
|
||||
import type { TreeNode } from './Node/TreeNode';
|
||||
import type { TreeRoot } from './TreeRoot/TreeRoot';
|
||||
@@ -10,8 +11,10 @@ type TreeNavigationKeyCodes = 'ArrowLeft' | 'ArrowUp' | 'ArrowRight' | 'ArrowDow
|
||||
export function useTreeKeyboardNavigation(
|
||||
treeRootRef: Readonly<Ref<TreeRoot>>,
|
||||
treeElementRef: Readonly<Ref<HTMLElement | undefined>>,
|
||||
useEventListener: UseEventListener = useAutoUnsubscribedEventListener,
|
||||
) {
|
||||
useKeyboardListener(treeElementRef, (event) => {
|
||||
const { startListening } = useEventListener();
|
||||
startListening(treeElementRef, 'keydown', (event) => {
|
||||
if (!treeElementRef.value) {
|
||||
return; // Not yet initialized?
|
||||
}
|
||||
@@ -40,19 +43,6 @@ export function useTreeKeyboardNavigation(
|
||||
});
|
||||
}
|
||||
|
||||
function useKeyboardListener(
|
||||
elementRef: Readonly<Ref<HTMLElement | undefined>>,
|
||||
handleKeyboardEvent: (event: KeyboardEvent) => void,
|
||||
) {
|
||||
onMounted(() => {
|
||||
elementRef.value?.addEventListener('keydown', handleKeyboardEvent, true);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
elementRef.value?.removeEventListener('keydown', handleKeyboardEvent);
|
||||
});
|
||||
}
|
||||
|
||||
interface TreeNavigationContext {
|
||||
readonly focus: SingleNodeFocusManager;
|
||||
readonly nodes: QueryableNodes;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { onBeforeUnmount } from 'vue';
|
||||
|
||||
export function useAnimationFrameLimiter(
|
||||
cancelAnimationFrame: CancelAnimationFrameFunction = window.cancelAnimationFrame,
|
||||
requestAnimationFrame: RequestAnimationFrameFunction = window.requestAnimationFrame,
|
||||
onTeardown: RegisterTeardownCallbackFunction = onBeforeUnmount,
|
||||
): AnimationFrameLimiter {
|
||||
let requestId: AnimationFrameId | null = null;
|
||||
const cancelNextFrame = () => {
|
||||
if (requestId === null) {
|
||||
return;
|
||||
}
|
||||
cancelAnimationFrame(requestId);
|
||||
};
|
||||
const resetNextFrame = (callback: AnimationFrameRequestCallback) => {
|
||||
cancelNextFrame();
|
||||
requestId = requestAnimationFrame(callback);
|
||||
};
|
||||
onTeardown(() => {
|
||||
cancelNextFrame();
|
||||
});
|
||||
return {
|
||||
cancelNextFrame,
|
||||
resetNextFrame,
|
||||
};
|
||||
}
|
||||
|
||||
export type CancelAnimationFrameFunction = typeof window.cancelAnimationFrame;
|
||||
|
||||
export type RequestAnimationFrameFunction = (callback: AnimationFrameRequestCallback) => number;
|
||||
|
||||
export type RegisterTeardownCallbackFunction = (callback: () => void) => void;
|
||||
|
||||
export type AnimationFrameId = ReturnType<typeof requestAnimationFrame>;
|
||||
|
||||
export type AnimationFrameRequestCallback = () => void;
|
||||
|
||||
export interface AnimationFrameLimiter {
|
||||
cancelNextFrame(): void;
|
||||
resetNextFrame(callback: AnimationFrameRequestCallback): void;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
onBeforeMount, onBeforeUnmount,
|
||||
watch, type Ref,
|
||||
} from 'vue';
|
||||
import { throttle, type ThrottleFunction } from '@/application/Common/Timing/Throttle';
|
||||
import { useResizeObserverPolyfill } from './UseResizeObserverPolyfill';
|
||||
import { useAnimationFrameLimiter } from './UseAnimationFrameLimiter';
|
||||
|
||||
export function useResizeObserver(
|
||||
config: ResizeObserverConfig,
|
||||
usePolyfill = useResizeObserverPolyfill,
|
||||
useFrameLimiter = useAnimationFrameLimiter,
|
||||
throttler: ThrottleFunction = throttle,
|
||||
onSetup: LifecycleHookRegistration = onBeforeMount,
|
||||
onTeardown: LifecycleHookRegistration = onBeforeUnmount,
|
||||
) {
|
||||
const { resetNextFrame, cancelNextFrame } = useFrameLimiter();
|
||||
// This prevents the 'ResizeObserver loop completed with undelivered notifications' error when
|
||||
// the browser can't process all observations within one animation frame.
|
||||
// Reference: https://github.com/WICG/resize-observer/issues/38
|
||||
|
||||
const { resizeObserverReady } = usePolyfill();
|
||||
// This ensures compatibility with ancient browsers. All modern browsers support ResizeObserver.
|
||||
// Compatibility info: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#browser_compatibility
|
||||
|
||||
const throttledCallback = throttler(config.observeCallback, config.throttleInMs);
|
||||
// Throttling enhances performance during rapid changes such as window resizing.
|
||||
|
||||
let observer: ResizeObserver | null;
|
||||
|
||||
const disposeObserver = () => {
|
||||
cancelNextFrame();
|
||||
observer?.disconnect();
|
||||
observer = null;
|
||||
};
|
||||
|
||||
onSetup(() => {
|
||||
watch(() => config.observedElementRef.value, (element) => {
|
||||
if (!element) {
|
||||
disposeObserver();
|
||||
return;
|
||||
}
|
||||
resizeObserverReady.then((createObserver) => {
|
||||
disposeObserver();
|
||||
observer = createObserver((...args) => {
|
||||
resetNextFrame(() => throttledCallback(...args));
|
||||
});
|
||||
observer.observe(element, config?.observeOptions);
|
||||
});
|
||||
}, { immediate: true });
|
||||
});
|
||||
|
||||
onTeardown(() => {
|
||||
disposeObserver();
|
||||
});
|
||||
}
|
||||
|
||||
export interface ResizeObserverConfig {
|
||||
readonly observedElementRef: ObservedElementReference;
|
||||
readonly throttleInMs: number;
|
||||
readonly observeCallback: ResizeObserverCallback;
|
||||
readonly observeOptions?: ResizeObserverOptions;
|
||||
}
|
||||
|
||||
export type ObservedElementReference = Readonly<Ref<HTMLElement | undefined>>;
|
||||
|
||||
export type LifecycleHookRegistration = (callback: () => void) => void;
|
||||
@@ -16,11 +16,17 @@ async function polyfillResizeObserver(): Promise<typeof ResizeObserver> {
|
||||
return polyfillLoader.getValue();
|
||||
}
|
||||
|
||||
interface ResizeObserverCreator {
|
||||
(
|
||||
...args: ConstructorParameters<typeof ResizeObserver>
|
||||
): ResizeObserver;
|
||||
}
|
||||
|
||||
export function useResizeObserverPolyfill() {
|
||||
const resizeObserverReady = new Promise<void>((resolve) => {
|
||||
const resizeObserverReady = new Promise<ResizeObserverCreator>((resolve) => {
|
||||
onMounted(async () => {
|
||||
await polyfillResizeObserver();
|
||||
resolve();
|
||||
resolve((args) => new ResizeObserver(args));
|
||||
});
|
||||
});
|
||||
return { resizeObserverReady };
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
onBeforeUnmount,
|
||||
shallowRef,
|
||||
watch,
|
||||
type Ref,
|
||||
} from 'vue';
|
||||
|
||||
export interface UseEventListener {
|
||||
(): TargetEventListener;
|
||||
}
|
||||
|
||||
export const useAutoUnsubscribedEventListener: UseEventListener = () => ({
|
||||
startListening: (eventTargetSource, eventType, eventHandler) => {
|
||||
const eventTargetRef = isEventTarget(eventTargetSource)
|
||||
? shallowRef(eventTargetSource)
|
||||
: eventTargetSource;
|
||||
return startListeningRef(
|
||||
eventTargetRef,
|
||||
eventType,
|
||||
eventHandler,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
type EventTargetRef = Readonly<Ref<EventTarget | undefined>>;
|
||||
|
||||
type EventTargetOrRef = EventTargetRef | EventTarget;
|
||||
|
||||
function isEventTarget(obj: EventTargetOrRef): obj is EventTarget {
|
||||
return obj instanceof EventTarget;
|
||||
}
|
||||
|
||||
export interface TargetEventListener {
|
||||
startListening<TEvent extends keyof HTMLElementEventMap>(
|
||||
eventTargetSource: EventTargetOrRef,
|
||||
eventType: TEvent,
|
||||
eventHandler: (event: HTMLElementEventMap[TEvent]) => void,
|
||||
): void;
|
||||
}
|
||||
|
||||
function startListeningRef<TEvent extends keyof HTMLElementEventMap>(
|
||||
eventTargetRef: Readonly<Ref<EventTarget | undefined>>,
|
||||
eventType: TEvent,
|
||||
eventHandler: (event: HTMLElementEventMap[TEvent]) => void,
|
||||
): void {
|
||||
const eventListenerManager = new EventListenerManager();
|
||||
watch(() => eventTargetRef.value, (element) => {
|
||||
eventListenerManager.removeListenerIfExists();
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
eventListenerManager.addListener(element, eventType, eventHandler);
|
||||
}, { immediate: true });
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
eventListenerManager.removeListenerIfExists();
|
||||
});
|
||||
}
|
||||
|
||||
class EventListenerManager {
|
||||
private removeListener: (() => void) | null = null;
|
||||
|
||||
public removeListenerIfExists() {
|
||||
if (this.removeListener === null) {
|
||||
return;
|
||||
}
|
||||
this.removeListener();
|
||||
this.removeListener = null;
|
||||
}
|
||||
|
||||
public addListener<TEvent extends keyof HTMLElementEventMap>(
|
||||
eventTarget: EventTarget,
|
||||
eventType: TEvent,
|
||||
eventHandler: (event: HTMLElementEventMap[TEvent]) => void,
|
||||
) {
|
||||
eventTarget.addEventListener(eventType, eventHandler);
|
||||
this.removeListener = () => eventTarget.removeEventListener(eventType, eventHandler);
|
||||
}
|
||||
}
|
||||
@@ -16,4 +16,6 @@ export interface ScrollDomStateAccessor {
|
||||
readonly htmlScrollHeight: number;
|
||||
readonly htmlClientWidth: number;
|
||||
readonly htmlClientHeight: number;
|
||||
readonly htmlOffsetWidth: number;
|
||||
readonly htmlOffsetHeight: number;
|
||||
}
|
||||
|
||||
@@ -103,7 +103,6 @@ const BodyStyleLeft: DomPropertyMutator<{
|
||||
storeInitialState: (dom) => ({
|
||||
htmlScrollLeft: dom.htmlScrollLeft,
|
||||
bodyStyleLeft: dom.bodyStyleLeft,
|
||||
bodyMarginLeft: dom.bodyComputedMarginLeft,
|
||||
}),
|
||||
onBlock: (initialState, dom) => {
|
||||
if (initialState.htmlScrollLeft === 0) {
|
||||
@@ -206,13 +205,21 @@ const BodyPositionFixed: DomPropertyMutator<{
|
||||
|
||||
const BodyWidth100Percent: DomPropertyMutator<{
|
||||
readonly bodyStyleWidth: string;
|
||||
readonly htmlOffsetWidth: number;
|
||||
readonly htmlClientWidth: number;
|
||||
}> = {
|
||||
storeInitialState: (dom) => ({
|
||||
bodyStyleWidth: dom.bodyStyleWidth,
|
||||
htmlOffsetWidth: dom.htmlOffsetWidth,
|
||||
htmlClientWidth: dom.htmlClientWidth,
|
||||
}),
|
||||
onBlock: (_, dom) => {
|
||||
dom.bodyStyleWidth = calculateBodyViewportStyleWithMargins(
|
||||
[dom.bodyComputedMarginLeft, dom.bodyComputedMarginRight],
|
||||
onBlock: (initialState, dom) => {
|
||||
dom.bodyStyleWidth = calculateAdjustedStyle(
|
||||
[
|
||||
dom.bodyComputedMarginLeft,
|
||||
dom.bodyComputedMarginRight,
|
||||
calculateScrollbarGutterStyle(initialState.htmlClientWidth, initialState.htmlOffsetWidth),
|
||||
],
|
||||
);
|
||||
return ScrollRevertAction.RestoreRequired;
|
||||
},
|
||||
@@ -223,13 +230,21 @@ const BodyWidth100Percent: DomPropertyMutator<{
|
||||
|
||||
const BodyHeight100Percent: DomPropertyMutator<{
|
||||
readonly bodyStyleHeight: string;
|
||||
readonly htmlOffsetHeight: number;
|
||||
readonly htmlClientHeight: number;
|
||||
}> = {
|
||||
storeInitialState: (dom) => ({
|
||||
bodyStyleHeight: dom.bodyStyleHeight,
|
||||
htmlOffsetHeight: dom.htmlOffsetHeight,
|
||||
htmlClientHeight: dom.htmlClientHeight,
|
||||
}),
|
||||
onBlock: (_, dom) => {
|
||||
dom.bodyStyleHeight = calculateBodyViewportStyleWithMargins(
|
||||
[dom.bodyComputedMarginTop, dom.bodyComputedMarginBottom],
|
||||
onBlock: (initialState, dom) => {
|
||||
dom.bodyStyleHeight = calculateAdjustedStyle(
|
||||
[
|
||||
dom.bodyComputedMarginTop,
|
||||
dom.bodyComputedMarginBottom,
|
||||
calculateScrollbarGutterStyle(initialState.htmlClientHeight, initialState.htmlOffsetHeight),
|
||||
],
|
||||
);
|
||||
return ScrollRevertAction.RestoreRequired;
|
||||
},
|
||||
@@ -280,11 +295,20 @@ interface DomPropertyMutator<TInitialStateValue> {
|
||||
restoreStateOnUnblock(storedState: TInitialStateValue, dom: ScrollDomStateAccessor): void;
|
||||
}
|
||||
|
||||
function calculateBodyViewportStyleWithMargins(
|
||||
margins: readonly string[],
|
||||
/** Calculates allocated scrollbar gutter, adjusting for `scrollbar-gutter: stable` */
|
||||
function calculateScrollbarGutterStyle(
|
||||
clientSize: number,
|
||||
offsetSize: number,
|
||||
): string {
|
||||
const scrollbarGutterSize = clientSize - offsetSize;
|
||||
return scrollbarGutterSize !== 0 ? `${scrollbarGutterSize}px` : '';
|
||||
}
|
||||
|
||||
function calculateAdjustedStyle(
|
||||
spaceOffsets: readonly string[],
|
||||
): string {
|
||||
let value = '100%';
|
||||
const calculatedMargin = margins
|
||||
const calculatedMargin = spaceOffsets
|
||||
.filter((marginText) => marginText.length > 0)
|
||||
.join(' + '); // without setting margins, it leads to layout shift if body has margin
|
||||
if (calculatedMargin) {
|
||||
|
||||
@@ -61,4 +61,8 @@ class WindowScrollDomState implements ScrollDomStateAccessor {
|
||||
get htmlClientWidth(): number { return HtmlElement.clientWidth; }
|
||||
|
||||
get htmlClientHeight(): number { return HtmlElement.clientHeight; }
|
||||
|
||||
get htmlOffsetWidth(): number { return HtmlElement.offsetWidth; }
|
||||
|
||||
get htmlOffsetHeight(): number { return HtmlElement.offsetHeight; }
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { onBeforeMount, onBeforeUnmount } from 'vue';
|
||||
import { useAutoUnsubscribedEventListener, type UseEventListener } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener';
|
||||
|
||||
export function useEscapeKeyListener(callback: () => void) {
|
||||
const onKeyUp = (event: KeyboardEvent) => {
|
||||
export function useEscapeKeyListener(
|
||||
callback: () => void,
|
||||
eventTarget: EventTarget = document,
|
||||
useEventListener: UseEventListener = useAutoUnsubscribedEventListener,
|
||||
): void {
|
||||
const { startListening } = useEventListener();
|
||||
startListening(eventTarget, 'keyup', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
window.addEventListener('keyup', onKeyUp);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keyup', onKeyUp);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent, ref, watchEffect, nextTick,
|
||||
defineComponent, ref, watchEffect,
|
||||
nextTick, inject,
|
||||
} from 'vue';
|
||||
import ModalOverlay from './ModalOverlay.vue';
|
||||
import ModalContent from './ModalContent.vue';
|
||||
@@ -28,6 +29,8 @@ import { useCurrentFocusToggle } from './Hooks/UseCurrentFocusToggle';
|
||||
import { useEscapeKeyListener } from './Hooks/UseEscapeKeyListener';
|
||||
import { useAllTrueWatcher } from './Hooks/UseAllTrueWatcher';
|
||||
|
||||
export const INJECTION_KEY_ESCAPE_LISTENER = Symbol('useEscapeKeyListener');
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
ModalOverlay,
|
||||
@@ -49,6 +52,11 @@ export default defineComponent({
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const listenEscapeKey: typeof useEscapeKeyListener = inject(
|
||||
INJECTION_KEY_ESCAPE_LISTENER,
|
||||
useEscapeKeyListener,
|
||||
);
|
||||
|
||||
const isRendered = ref(false);
|
||||
const isOpen = ref(false);
|
||||
const overlayTransitionedOut = ref(false);
|
||||
@@ -56,7 +64,7 @@ export default defineComponent({
|
||||
|
||||
useLockBodyBackgroundScroll(isOpen);
|
||||
useCurrentFocusToggle(isOpen);
|
||||
useEscapeKeyListener(() => handleEscapeKeyUp());
|
||||
listenEscapeKey(() => handleEscapeKeyUp());
|
||||
|
||||
const {
|
||||
onAllConditionsMet: onModalFullyTransitionedOut,
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent, shallowRef, onMounted, onBeforeUnmount, watch,
|
||||
defineComponent, shallowRef, onMounted, watch,
|
||||
} from 'vue';
|
||||
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
|
||||
import { throttle } from '@/application/Common/Timing/Throttle';
|
||||
import { useResizeObserver } from './Hooks/Resize/UseResizeObserver';
|
||||
|
||||
export default defineComponent({
|
||||
emits: {
|
||||
@@ -20,31 +19,21 @@ export default defineComponent({
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
},
|
||||
setup(_, { emit }) {
|
||||
const { resizeObserverReady } = useResizeObserverPolyfill();
|
||||
|
||||
const containerElement = shallowRef<HTMLElement>();
|
||||
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let observer: ResizeObserver | undefined;
|
||||
|
||||
onMounted(() => {
|
||||
watch(() => containerElement.value, async (element) => {
|
||||
if (!element) {
|
||||
disposeObserver();
|
||||
return;
|
||||
}
|
||||
resizeObserverReady.then(() => {
|
||||
disposeObserver();
|
||||
observer = new ResizeObserver(throttle(updateSize, 200));
|
||||
observer.observe(element);
|
||||
});
|
||||
updateSize(); // Do not throttle, immediately inform new width
|
||||
}, { immediate: true });
|
||||
useResizeObserver({
|
||||
observedElementRef: containerElement,
|
||||
observeCallback: updateSize,
|
||||
throttleInMs: 200,
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
disposeObserver();
|
||||
onMounted(() => {
|
||||
watch(() => containerElement.value, async () => {
|
||||
updateSize();
|
||||
}, { immediate: true });
|
||||
});
|
||||
|
||||
function updateSize() {
|
||||
@@ -81,11 +70,6 @@ export default defineComponent({
|
||||
return { isChanged: true };
|
||||
}
|
||||
|
||||
function disposeObserver() {
|
||||
observer?.disconnect();
|
||||
observer = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
containerElement,
|
||||
};
|
||||
|
||||
@@ -34,7 +34,10 @@ import {
|
||||
useFloating, arrow, shift, flip, type Placement, offset, type Side, type Coords, autoUpdate,
|
||||
} from '@floating-ui/vue';
|
||||
import { defineComponent, shallowRef, computed } from 'vue';
|
||||
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/UseResizeObserverPolyfill';
|
||||
import { useResizeObserverPolyfill } from '@/presentation/components/Shared/Hooks/Resize/UseResizeObserverPolyfill';
|
||||
import { throttle } from '@/application/Common/Timing/Throttle';
|
||||
import { type TargetEventListener } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
const GAP_BETWEEN_TOOLTIP_AND_TRIGGER_IN_PX = 2;
|
||||
@@ -48,9 +51,12 @@ export default defineComponent({
|
||||
const triggeringElement = shallowRef<HTMLElement | undefined>();
|
||||
const arrowElement = shallowRef<HTMLElement | undefined>();
|
||||
|
||||
const eventListener = injectKey((keys) => keys.useAutoUnsubscribedEventListener);
|
||||
useResizeObserverPolyfill();
|
||||
|
||||
const { floatingStyles, middlewareData, placement } = useFloating(
|
||||
const {
|
||||
floatingStyles, middlewareData, placement, update,
|
||||
} = useFloating(
|
||||
triggeringElement,
|
||||
tooltipDisplayElement,
|
||||
{
|
||||
@@ -68,6 +74,17 @@ export default defineComponent({
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
Not using `float-ui`'s `autoUpdate` with `animationFrame: true` because it updates tooltips on
|
||||
every frame through `requestAnimationFrame`. This behavior is analogous to a continuous loop
|
||||
(often 60 updates per second and more depending on the refresh rate), which can be excessively
|
||||
performance-intensive. It's overkill for the application needs and a monkey solution due to
|
||||
its brute-force nature.
|
||||
*/
|
||||
setupTransitionEndEvents(throttle(() => {
|
||||
update();
|
||||
}, 400, { excludeLeadingCall: true }), eventListener);
|
||||
|
||||
const arrowStyles = computed<CSSProperties>(() => {
|
||||
if (!middlewareData.value.arrow) {
|
||||
return {
|
||||
@@ -126,6 +143,19 @@ function getCounterpartBoxOffsetProperty(placement: Placement): keyof CSSPropert
|
||||
const currentSide = placement.split('-')[0] as Side;
|
||||
return sideCounterparts[currentSide];
|
||||
}
|
||||
|
||||
function setupTransitionEndEvents(
|
||||
handler: () => void,
|
||||
listener: TargetEventListener,
|
||||
) {
|
||||
const transitionEndEvents: readonly (keyof HTMLElementEventMap)[] = [
|
||||
'transitionend',
|
||||
'transitioncancel',
|
||||
];
|
||||
transitionEndEvents.forEach((eventName) => {
|
||||
listener.startListening(document.body, eventName, handler);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
13
src/presentation/electron/build/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# build
|
||||
|
||||
This folder is the *build resources directory* [1] and contains files used by electron-build to
|
||||
create the desktop version of the application.
|
||||
|
||||
Generate icons from the main logo file.
|
||||
Do not modify these icons manually.
|
||||
For more details, see the [related documentation](./../../../../img/README.md).
|
||||
|
||||
The electron-builder [1] uses these files, as specified in the [configuration file](./../../../../electron-builder.cjs).
|
||||
|
||||
For more information on icons in electron-builder,
|
||||
visit [Icons - electron-builder | www.electron.build](https://web.archive.org/web/20240501103645/https://www.electron.build/icons.html).
|
||||
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 364 KiB |
BIN
src/presentation/electron/build/icon.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
@@ -14,6 +14,8 @@ import {
|
||||
} from './ElectronConfig';
|
||||
import { registerAllIpcChannels } from './IpcRegistration';
|
||||
|
||||
const hideWindowUntilLoaded = true;
|
||||
const openDevToolsOnDevelopment = true;
|
||||
const isDevelopment = !app.isPackaged;
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
@@ -48,12 +50,12 @@ function createWindow() {
|
||||
preload: PRELOADER_SCRIPT_PATH,
|
||||
},
|
||||
icon: APP_ICON_PATH,
|
||||
show: !hideWindowUntilLoaded,
|
||||
});
|
||||
|
||||
focusAndShowOnceLoaded(win);
|
||||
win.setMenuBarVisibility(false);
|
||||
configureExternalsUrlsOpenBrowser(win);
|
||||
loadApplication(win);
|
||||
|
||||
win.on('closed', () => {
|
||||
win = null;
|
||||
});
|
||||
@@ -101,9 +103,8 @@ function loadApplication(window: BrowserWindow) {
|
||||
} else {
|
||||
loadUrlWithNodeWorkaround(window, RENDERER_HTML_PATH);
|
||||
}
|
||||
if (isDevelopment) {
|
||||
window.webContents.openDevTools();
|
||||
} else {
|
||||
openDevTools(window);
|
||||
if (!isDevelopment) {
|
||||
const updater = setupAutoUpdater();
|
||||
updater.checkForUpdates();
|
||||
}
|
||||
@@ -165,3 +166,29 @@ function configureAppQuitBehavior() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function focusAndShowOnceLoaded(window: BrowserWindow) {
|
||||
window.once('ready-to-show', () => {
|
||||
window.show(); // Shows and focuses
|
||||
bringToFront(window);
|
||||
});
|
||||
}
|
||||
|
||||
function bringToFront(window: BrowserWindow) {
|
||||
// Only needed for Windows, tested on GNOME 42.5, Windows 11 23H2 Pro and macOS Sonoma 14.4.1.
|
||||
// Some report it's also needed for some versions of GNOME.
|
||||
// - https://github.com/electron/electron/issues/2867#issuecomment-409858459
|
||||
// - https://github.com/signalapp/Signal-Desktop/blob/0999df2d6e93da805b2135f788ffc739ba69832d/app/SystemTrayService.ts#L277-L284
|
||||
window.setAlwaysOnTop(true);
|
||||
window.setAlwaysOnTop(false);
|
||||
}
|
||||
|
||||
function openDevTools(window: BrowserWindow) {
|
||||
if (!isDevelopment) {
|
||||
return;
|
||||
}
|
||||
if (!openDevToolsOnDevelopment) {
|
||||
return;
|
||||
}
|
||||
window.webContents.openDevTools();
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ import type { useCurrentCode } from '@/presentation/components/Shared/Hooks/UseC
|
||||
import type { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents';
|
||||
import type { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
|
||||
import type { useLogger } from '@/presentation/components/Shared/Hooks/Log/UseLogger';
|
||||
import type { useCodeRunner } from './components/Shared/Hooks/UseCodeRunner';
|
||||
import type { useDialog } from './components/Shared/Hooks/Dialog/UseDialog';
|
||||
import type { useScriptDiagnosticsCollector } from './components/Shared/Hooks/UseScriptDiagnosticsCollector';
|
||||
import type { useCodeRunner } from '@/presentation/components/Shared/Hooks/UseCodeRunner';
|
||||
import type { useDialog } from '@/presentation/components/Shared/Hooks/Dialog/UseDialog';
|
||||
import type { useScriptDiagnosticsCollector } from '@/presentation/components/Shared/Hooks/UseScriptDiagnosticsCollector';
|
||||
import type { useAutoUnsubscribedEventListener } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener';
|
||||
|
||||
export const InjectionKeys = {
|
||||
useCollectionState: defineTransientKey<ReturnType<typeof useCollectionState>>('useCollectionState'),
|
||||
@@ -23,6 +24,7 @@ export const InjectionKeys = {
|
||||
useCodeRunner: defineTransientKey<ReturnType<typeof useCodeRunner>>('useCodeRunner'),
|
||||
useDialog: defineTransientKey<ReturnType<typeof useDialog>>('useDialog'),
|
||||
useScriptDiagnosticsCollector: defineTransientKey<ReturnType<typeof useScriptDiagnosticsCollector>>('useScriptDiagnostics'),
|
||||
useAutoUnsubscribedEventListener: defineTransientKey<ReturnType<typeof useAutoUnsubscribedEventListener>>('useAutoUnsubscribedEventListener'),
|
||||
};
|
||||
|
||||
export interface InjectionKeyWithLifetime<T> {
|
||||
|
||||
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 24 KiB |