Compare commits

...

45 Commits

Author SHA1 Message Date
undergroundwires
9fd193e676 win: categorize and rename network security #131
This commit restructures the categorization of network security-related
scripts to improve clarity and align with user expectations. It involves
renaming and reorganizing categories to more accurately reflect their
functions beyond just IIS configurations.

This reorganization helps users find and utilize network security
scripts more efficiently and ensures that the categorization accurately
reflects the broader application of the scripts beyond server
configurations.

Changes:

- Merge network security enhancements under a single category.
- Rename categories for simplicity and increased technical accuracy.
- Flatten nested categories to streamline navigation and enhance
  clarity.
- Update documentation to match the new category structures.
- Revise script recommendations to encourage broader use.
- Fix revert codes of some related scripts to reflect default OS
  settings.
2024-05-11 11:39:28 +02:00
undergroundwires
52a4730073 ci/cd: remove check-latest from setup-node
This commit addresses the issue where Windows GitHub runners experience
failures due to unstable Node.js releases, particularly version 20.13.0,
as detailed in nodejs/node#52884 and nodejs/node#52682.

The 'check-latest' input in the 'setup-node' GitHub Action forces
every job to verify and potentially install the latest Node.js version.
This input was originally introduced to reduce maintenance efforts to
keep CI/CD setup up-to-date with the latest Node version.

However, the necessity to always run the latest Node.js version is not
critical for the CI/CD setup. Additionally, it causes increased network
requests and may inadvertently introduce unstable Node.js versions.

This commit removes the 'check-latest' option to prevent the immediate
adoption of new, potentially unstable Node.js releases, thus simplifying
the CI/CD pipeline. This keeps  CI/CD process is robust and predictable,
reducing the chances of unexpected disruptions in service deployment.
2024-05-10 12:18:16 +02:00
undergroundwires
bc4879cfe9 Fix Chromium scrollbar-induced layout shifts
This commit addresses an issue in Chromium on Linux and Windows where
the appearance of a vertical scrollbar causes unexpected horizontal
layout shifts. This behavior typically occurs when the window is
resized, a card is opened or a script is selected, resulting in content
being pushed to the left.

The solution implemented involves using `scrollbar-gutter: stable` to
ensure space is always allocated for the scrollbar, thus preventing any
shift in the page layout. This fix primarily affects Chromium-based
browsers on Linux and Windows. It has no impact on Firefox on any
platform, or any browser on macOS (including Chromium). Because these
render the scrollbar as an overlay, and do not suffer from this issue.

Steps to reproduce the issue using Chromium browser on Linux/Windows:

1. Open the app with a height large enough where a vertical scrollbar is
   not visible.
2. Resize the window to a height that triggers a vertical scrollbar.
3. Notice the layout shift as the body content moves to the right.

Changes:

- Add a CSS mixin to handle scrollbar gutter allocation with a fallback.
- Add support for modal dialog background lock to handle
  `scrollbar-gutter: stable;` in calculations to avoid layout shift when
  a modal is open.
- Add E2E test to avoid regression.
- Update DevToolkit to accommodate new scrollbar spacing.
2024-05-09 18:35:02 +02:00
undergroundwires
dd71536316 Fix misaligned tooltip positions in modal dialogs
This commit fixes a bug that causes tooltips to be slightly misaligned.

Tooltip positioning was incorrect during modal transitions due to their
initial movement, causing tooltips to align incorrectly at the start of
the animation rather than the end.

One way to solve this would be using `autoUpdate` from `floating-ui`
with `animationFrame: true`. However, this recalculates positions tens
of times per second, impacting performance. This is a monkey solution.

This commit adopts a more efficient approach by updating tooltip
positions only at the end of the transitions, which reduces calculations
and conserves resources.

Key changes:

- Addd transition end event listener for updating tooltip positions.
- Use throttling to eliminate excessive position recalculations.

Other supporting changes:

- Improve throttle function to support efficient recalculations of
  positions:
  - Add ability to optionally exclude the first execution (leading
    call).
  - Refactor to simplify it make it easier to follow and read.
  - Fix a bug where initial calls were incorrectly throttled if
    `dateNow()` returned `0`.
- Introduce and use a global hook for efficient DOM event management.
  This greatily introduce safety, reuse and testability of event
  listening.
2024-05-08 15:24:12 +02:00
undergroundwires
a3343205b1 Fix win execution with whitespace in username #351
This commit addresses the issue where scripts fail to execute on Windows
environments with usernames containing spaces. The problem stemmed from
PowerShell and cmd shell's handling of spaces in quoted arguments.

The solution involves encoding PowerShell commands before execution,
which mitigates the quoting issues previously causing script failures.
This approach is now integrated into the execution flow, ensuring that
commands are correctly handled irrespective of user names or other
variables that may include spaces.

Changes:

- Implement encoding for PowerShell commands to handle spaces in usernames
  and other similar scenarios.
- Update script documentation URLs to reflect changes in directory
  structure.

Fixes #351
2024-05-07 13:57:19 +02:00
undergroundwires
1d7cafc831 Fix VSCode script issues with added CI/CD tests
- Correct incorrect attribute in `configure_vscode.py`.
- Introduce CI tests for early error detection in the script.
- Replace emojis with ASCII in CI logs to avoid Windows encoding issues.
2024-05-06 17:18:19 +02:00
undergroundwires
c75df1c8c1 win: improve enabling secure connections #175
This commit refines the configuration of TLS and DTLS protocols on
Windows to enhance compatibility and stability across different
Windows versions.

Changes:

- Enable TLS 1.3 exclusively on Windows 11 and newer, addressing
  stability concerns with previous Windows versions, and resolving
  issue #175.
- Enable DTLS 1.2, replacing DTLS 1.3 due to lack of support in
  Windows. DTLS is enabled only on Windows 10 version 16007 and later
  for compatibility.
- Reorganize script categories for better clarity and manageability.
- Update revert codes for registry deletions to prevent false negative
  error outputs.
- Adjust recommendation levels to encourage more scripts due to system
  stability and documentation improvements introduced in this commit.
- Remove incorrect registry keys previously set for .NET apps.
- Add missing 64-bit registry keys for .NET apps.
- Rename scripts for improved simplicity and consistency.
- Improve documentation for affected scripts, correcting the
  misleading information about DTLS 1.2 vulnerability.
- Convert hexadecimal values to decimal in scripts to improve
  clarity.
- Introduce shared functions to reduce redundancy and improve
  script maintainability.
- Add more comments in generated code and simplify existing comments.
2024-05-05 10:57:45 +02:00
undergroundwires
ab25e0a066 Improve desktop icon quality and generation
This commit refactors the icon and logo generation process by replacing
multiple dependencies with ImageMagick. This simplifies the build
process and enhances maintainability.

Key changes:

- Remove unnecessary icon files for macOS (.icns) and Linux
  (size-specific PNGs). Electron-builder can now auto-generate these
  from a single `logo.png` starting from version 19.54.0, see:
  - electron-userland/electron-builder#1682
  - electron-userland/electron-builder#2533
- Retain `ico` generation with multiple sizes to fix pixelated/bad
  looking icons on Windows, see:
  - electron-userland/electron-builder#7328
  - electron-userland/electron-builder#3867
- Replaced `svgexport`, `icon-gen`, and `electron-icon-builder`
  dependencies with ImageMagick, addressing issues with outdated
  dependencies and unreliable CI/CD builds.
- Move electron-builder build resources to
  `src/presentation/electron/build` for better project structure.
- Improve `electron-builder` configuration file by making it
  importable/reusable without prebuilding the Electron application.
2024-05-04 12:09:42 +02:00
undergroundwires
813d820b85 Fix blank window on load on desktop version #348
This commit updates the application startup behavior to prevent showing
a blank window until it's fully loaded on all platforms. This enhancement
improves the user experience by ensuring the UI only becomes visible
when it is ready to interact with.

This fix contributes to a smoother user experience by aligning the
window display timing with content readiness, thus avoiding the brief
display of an empty screen.

Changes:

- Set window to initially hide until fully loaded using the
  `ready-to-show` event.
- Show the window, focus on it and bring it front once it is loaded.
  Windows requires additional logic to put Window to front, see
  electron/electron#2867.
- Parametrize the behavior of opening developer tools for easier
  configuration during testing.
2024-05-03 12:03:36 +02:00
undergroundwires
66a56888a4 win: fix Copilot by excluding r.bing.com #329
This commit modifies the blocking behavior of `r.bing.com` due to its
extensive use across multiple Windows features, including Copilot and
Maps. Previously, included in the 'Cortana and Live Tiles' block list,
this host was causing issues for Copilot functionalites, as noted in
issue #329. By excluding `r.bing.com` from block list, this update aims
to prevent unintended disruptions without compromising the privacy gains
of other scripts.

Changes include:

- Exclude `r.bing.com` from the "Cortana and Live Tiles" block list.
- Improve documentation to clarify the role and exclusions.
- Improve documentation with consistent header for blocked hosts.
2024-05-02 13:52:09 +02:00
undergroundwires
4ef16cea56 win: improve disabling protocols
This commit groups scripts related to disabling protocols under same
category, streamlining the process for disabling protocols like NetBios,
SMBv1, and various TLS/SSL versions. It improves the documentation and
scripts of the related scripts.

Key changes:

- Introduce new category for disabling insecure protocols and move
  related scripts under it.
- Remove .NET configuration from TLS 1.0 disabling to prevent unwanted
  side effects on .NET applications, maintaining system integrity.
- Remove the script disabling DTLS 1.1 as this protocol does not exist.
- Recommend previously not recommended scripts:
  - SSL 2.0 in 'Standard' because it's already removed from Windows.
  - SSL 3.0 in 'Standard' because it's already disabled by default.
  - TLS 1.0 in 'Strict' as it's deprecated on Windows.
  - TLS 1.1 in 'Strict' as it's deprecated on Windows.
- Rename and reorder scripts for consistency and enhanced readability.
- Fix revert codes to accurately reflect successful operations, by
  adding `2>nul` on `reg delete` commands.
- Expand documentation to include detailed precautions and references,
  aiding users in understanding the implications of their actions
  (addressing user feedback from #57, #131, #183, #185).

Other supporting changes:

- Convert hexadecimal values to decimal to enhance script readability.
- Refactor scripts to utilize shared functions, improving maintainability.
- Add detailed comments within the scripts to aid in comprehension.
- Minor updates to other crypto scripts for consistency.
- Reorganize protocol listing by age for a logical script flow.
- Standardize comments across various TLS configuration scripts for
  clarity.
- Fix enabling DTLS 1.3 being categorized as disabling insecure
  connection.
2024-05-01 12:18:55 +02:00
undergroundwires
8c17396285 Fix script cancellation with new dialog on Linux
This commit improves the management of script execution process by
enhancing the way terminal commands are handled, paving the way for
easier future modifications and providing clearer feedback to users when
scripts are cancelled.

Previously, the UI displayed a generic error message which could lead to
confusion if the user intentionally cancelled the script execution. Now,
a specific error dialog will appear, improving the user experience by
accurately reflecting the action taken by the user.

This change affects code execution on Linux where closing GNOME terminal
returns exit code `137` which is then treated by script cancellation by
privacy.sexy to show the accurate error dialog. It does not affect macOS
and Windows as curret commands result in success (`0`) exit code on
cancellation.

Additionally, this update encapsulates OS-specific logic into dedicated
classes, promoting better separation of concerns and increasing the
modularity of the codebase. This makes it simpler to maintain and extend
the application.

Key changes:

- Display a specific error message for script cancellations.
- Refactor command execution into dedicated classes.
- Improve file permission setting flexibility and avoid setting file
  permissions on Windows as it's not required to execute files.
- Introduce more granular error types for script execution.
- Increase logging for shell commands to aid in debugging.
- Expand test coverage to ensure reliability.
- Fix error dialogs not showing the error messages due to incorrect
  propagation of errors.

Other supported changes:

- Update `SECURITY.md` with details on script readback and verification.
- Fix a typo in `IpcRegistration.spec.ts`.
- Document antivirus scans in `desktop-vs-web-features.md`.
2024-04-30 15:04:59 +02:00
undergroundwires
694bf1a74d win, linux, mac: fix various typos #349
This commit fixes various typos in documentation and code.

Co-authored-by: RainRat <rainrat78@yahoo.ca>
2024-04-29 13:04:33 +02:00
undergroundwires
0fc2ffc1ea Add system requirements documentation #134
- Create system requirements documentation for desktop versions,
  addressing issue #134.
- Reorganize related documents into `docs/desktop` for improved
  structure and accessibility.
- Update references to address ARM chip emulation issues noted in user
  feedback, issue #348.
2024-04-28 16:57:55 +02:00
undergroundwires
d19dde603d win: improve disabling insecure hashes #131
This commit addresses reports in issue #131 about third-party cloud
services like MEGA and Dropbox being affected by hash disabling. It
updates the documentation to guide users on the potential impact,
adjusts the recommendation levels along with other minor improvements.

- Recommend hash disabling scripts in 'Strict'.
- Expand and refine documentation, adding warnings to inform user
  decisions (addressing issues #57, #131, #175, #183).
- Add a new shared function to standardize hash disabling, increasing
  code maintainability.
- Change from hexadecimal to decimal in scripts for clarity.
- Improve code comments for better understanding.
- Add comments in generated to code to make it easier to follow.
- Fix revert codes showing errors by using `2>nul` in `reg delete`
  commands.
- Rename scripts for consistent naming conventions.

Supporting changes in other SSL/TLS handshake scripts:

- Update documentation for consistency.
- Rename shared functions for consistency and clarity.
- Improve generated code comments for clarity.
2024-04-27 11:27:26 +02:00
undergroundwires
23bac0fc76 ci/cd: lint Python scripts using pylint
This commit integrates `pylint` into the CI/CD pipeline to improve the
quality of Python scripts within the project. By enforcing stricter
linting standards, the aim is to identify and correct potential issues
more efficiently, ultimately contributing to more reliable and
maintainable code.

Changes:

- Introduce `npm run lint:pylint` command to facilitate unified way to
  run linting on different environments.
- Include `npm run lint:pylint` in the CI/CD workflow to ensure all
  commits adhere to established Python coding standards.
- Fix an issue identified by `pylint` in `configure_vscode.py`.
- Rename the workflow to match the latest naming convention.
2024-04-26 17:03:38 +02:00
undergroundwires
e18907ca91 win: improve 'Snipping Tool' removal #343
Due to changes in how Windows handles the Snipping Tool, this commit
reclassifies the tool's disablement into its own distinct category.
This update introduces alternative methods to disable the tool,
enhances documentation, and improves script functionality.

Changes include:

- Move Snipping Tool removal to a standalone category for clearer
  navigation.
- Expand documentation to better describe the tool's impact on privacy.
- Add methods to disable the tool without removing the app.
- Implement a shared function to disable specific Windows hotkeys.
- Rename Cortana shortcut disablement script for consistency.
2024-04-25 10:36:33 +02:00
undergroundwires
4e21f05031 ci/cd: add check for TODO comments
This commit introduces a new GitHub Actions job within the quality
checks workflow that scans the latest commit for TODO comments. The
intention is to prevent such comments from being merged into the main
branch, promoting cleaner and more maintainable code.

The script uses a specific pattern to avoid IDE detection and
misclassification of the script line as a TODO item itself. If any TODO
comments are found, the script exists with a non-zero status,
indicating an issue that must be addressed before proceeding.
2024-04-24 23:59:55 +02:00
undergroundwires
8b224eefe7 win: doc, improve, encourage cipher disabling
- Introduce 'Disable insecure ciphers' category to organize and group
  cipher disabling scripts.
- Expand documentation, adding cautionary notes to help users make
  informed decisions, addressing issues #57, #131, #175, and #183.
- Implement `DisableCipherAlgorithm` function to standardize the
  approach to disabling cipher algorithms, enhancing maintainability
  and promoting code reuse.
- Replace hexadecimal numbers with decimals in scripts to improve
  readability.
- Add comments to generated code for better understandability.
- Update revert codes to avoid incorrect error messages when
  operations are successful, using `2>nul` in `reg delete` commands.
- Rename scripts for consistency, incorporating 'insecure' in titles.
- Adjust recommendations to disable all insecure ciphers in 'Strict'
  mode due to security risks, and recommend disabling `NULL` in
  'Standard' mode as it removes encryption.
- Remove disabling of `DES 56`, correcting a redundancy as this cipher
  configuration does not exist.
2024-04-21 14:31:00 +02:00
undergroundwires
f261ab4cd9 win: improve disabling insecure renegotiations
This commit improves script clarity and user guidance on disabling
insecure renegotiations.

- Update script name for clarity.
- Improve documentation for better understanding.
- Recommend the script as 'Strict' to align with its security focus.
- Modify revert codes to suppress misleading error messages upon
  successful reversion by including `2>nul` in `reg delete` commands.
- Convert hexadecimal to decimal in registry commands to improve
  readability.
2024-04-20 19:18:52 +02:00
undergroundwires
f584fabb50 win: improve disabling SMBv1 protocol
- Improve documentation.
- Add disabling `mrxsmb10` service (enabled with SMB1 feature).
- Configure Windows Server service for server side.
2024-04-19 16:16:00 +02:00
undergroundwires
2eed6f4afb win: organize and document network disablement
Reorganize and document scripts for disabling network features,
enhancing their discoverability and manageability. This commit
categorizes scripts related to disabling insecure network connections,
improves documentation, and makes these scripts more accessible.

- Group scripts under `Disable insecure connections` category.
- Move SMBv1 and NetBios disablement scripts to this new category.
- Improve documentation, highlighting the security improvements
  and potential compatibility issues with older systems.

Addresses issues #57, #115, #183, #175, and #185 by simplifying the
process of troubleshooting and reversing changes if necessary.
2024-04-17 21:35:56 +02:00
undergroundwires-bot
1c9dc93246 ⬆️ bump everywhere to 0.13.2 2024-04-16 07:45:17 +00:00
undergroundwires
cb144ae472 Fix inability to tap outside modal on mobile
This commit addresses touch target size issues on mobile devices by
adjusting modal margins. The larger margin allows for easier interaction
for modal dialogs by tapping outside the modal area on smaller screens.

Key changes:

- Introduce 30px margin on larger screens and 20px on smaller devices
  around modals, adhering to accessibility guidelines.
- Remove `max-height: 90vh;` in favor of consistent vertical margins,
  centralizing the spacing control via the `margin` property.
- Remove `max-height: 90v;` used to display scroll-bars as the vertical
  margin is now handled by `margin` property in single place.
2024-04-15 09:21:31 +02:00
undergroundwires
f3571abeaf Bump dependencies to latest, hold ESLint
This commit updates the project's npm dependencies to their
latest versions.

Updates to the following dependencies are on hold due to compatibility
issues:

- `@typescript-eslint/eslint-plugin`:
  - Blocked by `@vue/eslint-config-airbnb-with-typescript`
    (vuejs/eslint-config-airbnb#63).
- `@typescript-eslint/parser`:
  - Blocked by `@vue/eslint-config-airbnb-with-typescript`
    (vuejs/eslint-config-airbnb#63).
- `@vue/eslint-config-typescript`:
  - Blocked by `@vue/eslint-config-airbnb-with-typescript`
    (vuejs/eslint-config-airbnb#63).
- `eslint`:
  - Blocked by `@vue/eslint-config-airbnb-with-typescript`
    (vuejs/eslint-config-airbnb#65).
  - Blocked by `@typescript-eslint/eslint-plugin` and
    `@typescript-eslint/parser`
    (typescript-eslint/typescript-eslint#8211).

These dependencies remain at their current major versions, and
their status is documented in the `package.json` to inform future
updates.

Other supporting changes:

- Moves `@types/markdown-it` to `devDependencies` which was incorrectly
  included in `dependencies`.
- Fix error in `TreeView.spec` tests, revealed by the version bump.
- Update `markdown-it` import to match the new file.
2024-04-14 22:38:47 +02:00
undergroundwires
b87b7aac7d win: improve service revert and docs
This commit refines the reversion process for disabled services,
including handling cases where a service is missing, and enhances
documentation related to default service states. It corrects the
startup mode for the `gupdatem` service from 'Automatic' to 'Manual'.

Key changes:

- Add documentation on default service states and startup types.
- Introduce `ignoreMissingOnRevert` to skip errors when reverting
  missing services, improving the user experience.
- Standardize script titles for consistency across service
  disablement scripts.
- Correct the startup type for `gupdatem` to 'Manual', aligning
  it with its actual default state.

Supporting changes:

- Update `DisableService` function to support `ignoreMissingOnRevert`,
  allowing more flexibility in handling missing services on revert.
- Change `treatMissingStateAsOk` to `ignoreMissingOnRevert` for
  clarity and consistency.
2024-04-13 13:36:12 +02:00
undergroundwires
ae172000a6 Centralize and use global spacing variables
This commit improves UI consistency. It also improves maintainability by
removing "magic values" in favor of standardized spacing throughout the
application.

- Adjust spacing variables to match the convention.
- Add `_spacing.scss` to define a centralized set of spacing variables, both
  absolute and relative, to standardize the spacing throughout the application.
  This new approach ensures a consistent spacing logic across all components and
  layouts, facilitating easier maintenance and scalability of the styling codebase.
- Update various SCSS styles to utilize the new spacing variables. This change
  harmonizes the spacing across different parts of the application, aligning with
  the new design system's principles.
- Slightly adjust existing padding/margin/gaps for better consistency.

Other supporting changes per component:

- RatingCircle: Update style names to match convention and simplify
  hacky way to inject circle width value through CSS variables. Add
  tests for the new behavior and refactor existing tests for easier
  extensibility.
- TheFooter: Add small gap when footer items wrap.
- HiearchicalTreeNode: Refactor variables to separate caret size clearly
  from padding applied.
- App: Make padding responsive as initial behavior of v0.13.0 before
  5d940b57ef.
- ModalDialog: Use responsive absolute values instead of percentage.
- HorizontalResizeSlider:
  - Use `v-bind` instead of hacky way to inject SCSS values through variables.
  - Remove `verticalMargin` property to simplify its styling.
- Move `src/presentation/assets/styles/components/_card.scss` closer to
  components that it styles. Update structure documentation.

The centralization of spacing definitions will aid in future design
adjustments, ensuring that updates to spacing can be made swiftly and
uniformly across the application. It's a step towards a more maintainable
and scalable frontend architecture.
2024-04-12 18:38:12 +02:00
undergroundwires
ffd647d152 win: improve firewall docs /w winget impact #142
This commit enhances the documentation related to disabling the firewall
services in Windows, with a focus on the `winget` CLI's functionality,
resolving #142.

Changes:

- Expand documentation to include implications on `winget` CLI,
  addressing the issue #142.
- Add documentation for disabling `mpsdrv` service.
- Align documentation for disabling `mpssvc` service to match updates
  made for `mpsrv` to maintain consistency across documentation.
- Introduce documentation for parent categories affected by scripts
  that disable these services.
- Add documentation for parent categories for disabling these firewall
  services.

The documentation aims to provide users with a comprehensive
understanding of how these changes affect both system performance and
security posture.
2024-04-10 10:11:59 +02:00
undergroundwires
4142d084f6 win: fix Visual Studio remote analysis script #327
This commit improves the IntelliCode privacy settings for Visual Studio
by adjusting registry entries to prevent data collection without
impacting IntelliCode's functionality.

- Fix registry value setting for `DisableRemoteAnalysis` to prevent
  unexpected hangs in Visual Studio.
  This resolves issues reported in #267 and #268.
- Change the script recommentation level to 'Standard', and remove
  previous warnings about potential hangups, based on the successful
  mitigation of these issues.
  This reverts 7f7a84e3ba.
- Incorporate feedback from an official Microsoft statement
  (MicrosoftDocs/intellicode#510), acknowledging the discontinuation of
  certain IntelliCode backend services. This renders the remote analysis
  feature obsolete.
- Revise the documentation to make it more accessible and easier to
  understand.
2024-04-09 13:47:37 +02:00
undergroundwires
b7a20d9d41 Fix top script menu overflow on small screens
On very small screens (that can be tested with iPhone SE size), the
`All` button overflows. This makes E2E tests fail with width like
`320px`.

This commit fixes the issue by removing `whitespace: no-break` but
employing simpler and self-documenting layout.

Key changes:

- Simplify scripts menu layout instead of relying on
  `white-space: nowrap`.
- Increase gap when script menu items starts wrapping to avoid
  "squeezed" look.

Other supporting changes:

- Simplify gaps by using `column-gap` and `row-gap` properties rather
  than calculating margins.
- Use class-based styling instead of using `id`.
- Use more clear, consistent CSS class naming with prefixes in
  `TheScriptsMenu` to improve maintainability.
- Introduce `center-middle-flex-item` mixin for better documenting the
  code.
2024-04-08 12:28:38 +02:00
undergroundwires
b68711ef88 win: improve Windows feature disablement scripts
- Migrate feature disablement to PowerShell for clarity and robustness.
- Improve log outputs and error handling for missing or default-disabled
  features. This fixes false-positive errors by treating the absence of
  a targeted feature as a success condition, and treats features
  disabled by the OS as non-issues.
- Fix revert logic to align with OS defaults, correcting previous
  behavior that indiscriminately enabled features without considering
  their default state.
- Fix usage of incorrect feature name for `LDPPrintService`, correcting
  attempts to disable a non-existing feature.
- Standardize script recommendations for outdated or missing features
  on modern Windows versions by recommending them on 'Standard'
  selection, providing clearer guidance for users.
- Rename feature-related scripts for consistency with Windows display
  names, improving consistency and script discoverability.
- Expand documentation for all feature-disabling scripts, adding
  details such as display names, descriptions, and default states,
  thereby informing users about the specifics and rationale of each
  script.
- Rename `DisableFeature` function to `DisableWindowsFeature` for
  increased descriptiveness and alignment with PowerShell conventions.
- Harmonize the use of the `DisableWindowsFeature` function across
  scripts targeting various features, including SMBv1 and PowerShell
  2.0 downgrade attacks, enhancing consistency and maintainability.
- Add code comments in the generated disable/enable feature scripts,
  improving understandability for users.
- Add the ability to revert to default OS behavior for feature
  enablement/disablement to align with OS defaults.
2024-04-07 10:18:55 +02:00
undergroundwires
7b546c567c Fix card arrow not being animated in sync
This commit fixes an UI inconsitency where the arrow did not animate in
sync with with the card's expansion panel during the expansion process.
The solution implemented involves the use of actual DOM element for the
arrow, rather than a pseude-element, allowing for unified animation with
the expansion panel.

Changes:

- Extraction of the expansion arrow into its own Vue component,
  `CardExpansionArrow`, improving maintainability and separation of
  concerns.
- Transition to using a real DOM element for the expansion arrow, moving
  away from the `&:before` pseudo-class. This leads to simpler codebase,
  better separation of concerns and closer alignment with HTML
  semantics.
2024-04-06 14:11:30 +02:00
undergroundwires
49f22f048f win: improve and document secret key scripts
- Consolidate secret key improvement scripts into a single category.
- Simplify script names to improve user understanding.
- Expand and refine documentation, adding cautionary notes for clarity
  and helping users make informed decisions (addresses issues #57, #131,
  #175, #183).
- Adjust recommendation levels for scripts to 'Standard' to reflect
  their adoption in modern Windows and align with security standards:
  - Set Diffie-Hellman key exchange minimum to 2048 bits, matching
    modern Windows defaults
  - Align RSA key size with Microsoft's upcoming deprecation of 1024-bit
    keys.
- Improve the revert process by suppressing false error messages using
  `2>nul` in `reg delete` commands.
- Introduce a unified approach to adjust key sizes in key exchange
  algorithms with `RequireMinimumKeySize` function.
- Modify the Diffie-Hellman key exchange to a 2048-bit minimum instead
  of 4096 bits to balance security with broader software compatibility.
  This attempts to reduce side-effects on third-party software as
  reported in #57, #131, #183).
- Replace hexadecimal values with decimal equivalents in registry edits
  to facilitate better maintainability and readability.
2024-04-05 15:01:05 +02:00
undergroundwires
4472c2852e Ignore ResizeObserver errors in Cypress tests
This commit addresses false negative failures in Cypress due to a known
Chrome issue.

The included change prevents Cypress tests from failing because of the
non-critical `ResizeObserver loop limit exceeded` error, which occurs
inconsistently during CI/CD runs with GitHub runners. This error has
been documented in CHrome and does not affect actual browser usage or
local test runs. This commit implements a widely recommended workaround
that ignores this specific error during test execution.

Error from Cypress:

```
Error: The following error originated from your application code, not from Cypress.
> ResizeObserver loop limit exceeded
```

The solution follows community-driven advice and past discussions on
handling this benign exception within test scenarios. It contributes to
more reliable CI/CD results by filtering out irrelevant error noise.

For detailed background and discussion on this error, see:

- Cypress issues: cypress-io/cypress#8418, cypress-io/cypress#20341
- Cypress PRs: cypress-io/cypress#20257, cypress-io/cypress#20284
- Discussion in Quasar: quasarframework/quasar#2233
- Discussion in specification repository: WICG/resize-observer#38
2024-04-04 10:02:37 +02:00
undergroundwires
5d940b57ef Fix card header expansion glitch on card collapse
This commit fixes an issue where the card's header would improperly
expand to full height during card collapse, leading to a less smooth
user experience. Previously, this was caused by the indiscriminate use
of `transition: all` in the `.card__expander`, which included unwanted
properties in the transition during collapse, such as height. This is
solved by using Vue transitions to apply transition only during
expansion.

Changes:

- Introduce a new Vue component, `CardExpandAnimation`:
  - Centralizes the animation process, applying the same animation to
    both the card and its arrow for consistency.
  - Resolves the glitch by adjusting classes exclusively during the
    enter animation phase, avoiding unintended side effects during leave
    animation phase.
  - Adopts a Vue-idiomatic approach for transition management, improving
    code readability and maintainability.
  - Improves separation of concerns by isolating animation logic from
    the component's core functionality, facilitating easier updates or
    replacements.
- Remove unnecessary transitions to enhance code simplicity and
  performance:
  - Remove `transition: all` on `.card__expander`, which was identified
    as the cause of the issue.
  - Remove unnecessary `transition: all` on `.card`.
  - Adjust transitions to specifically target and affect the transform
    property (instead of `all`) to optimize animation behavior and
    eliminate potential side-effects.

These changes not only fix the issue at hand but also contribute to a
more maintainable and performant codebase by clarifying animation logic
and reducing unnecessary CSS transitions.
2024-04-03 09:51:09 +02:00
undergroundwires
bc7e1faa1c Fix horizontal layout shift after script selection
This commit resolves an issue causing horizontal UI layout shift when a
script is selected for the first time, and when all selected scripts are
deselected. This issue was only observed on Chromium-based browsers on
Linux environment when using macOS and Windows script collections.

The underlying cause was identified as the use of percentage-based
values for CSS margin and padding. To resolve this issue, these values
were updated to absolute measurements. This adjustment maintains layout
consistency across user interactions without compromising the
responsiveness.

The underlying cause was identified as the use of percentage-based values
for CSS margin and padding within certain elements. To resolve this issue,
these values were updated to absolute measurements. This adjustment
maintains layout consistency across user interactions without compromising
the responsiveness of the application.

Additionally, an end-to-end (E2E) test has been introduced to monitor
for future regressions of this layout shift bug, ensuring that the fix
remains effective over subsequent updates.
2024-04-02 12:17:20 +02:00
undergroundwires
557cea3f48 Fix overflow in tree node content on small screens
This commit addresses a UI issue observed on small screens, particularly
during text searches involving nested nodes.

Implementing word-breaking for the improved display of script/category
titles and their documentation prevents content overflow. This change
ensures that both the header (including the node title and documentation
icon) and the documentation text stay fully visible without overflowing.

Additionally, this fix replaces ID-based styling (`#node`) with
class-based styling, using clear, descriptive names. This enhances CSS
and JavaScript reusability and maintainability.
2024-04-01 12:34:21 +02:00
undergroundwires
4fb6302c67 ci/cd: trigger URL checks more, and limit amount
Key changes:

- Run URL checks more frequently on every change.
- Introduce environment variable to randomly select and limit URLs
  tested, this way the tests will provide quicker feedback on code
  changes.

Other supporting changes:

- Log more information about test before running the test to enable
  easier troubleshooting.
- Move shuffle function for arrays for reusability and missing tests.
2024-03-31 13:39:01 +02:00
undergroundwires
59decd17e2 ci/cd: bump Node.js environment to 20.x
This commit upgrades Node.js version to v20.x in CI/CD environment.

Previously used Node 18.x is moving towards end-of-life, with a planned
date of 2025-04-30. In contrast, Node 20.x has been offering long-term
support (LTS) since 2023-10-24. This makes Node 20.x a stable and
recommended version for production environments.

This commit also configures `actions/setup-node` with the
`check-latest` flag to always use the latest Node 20.x version, keeping
CI/CD setup up-to-date with minimal maintenance.
Details:
- actions/setup-node#165
- actions/setup-node#160

Using Node 20.x in CI/CD environments provides better compatibility with
Electron v29.0 which moves to Node 20.x.
Details:
- electron/electron#40343

This upgrade improves network connection handling in CI/CD pipelines
(where issues occur due to GitHub runners not supporting IPv6).
Details:
- actions/runner#3138
- actions/runner-images#668
- actions/runner#3213
- actions/runner-images#9540

Node 20.x adopts the Happy Eyeballs algorithm for improved IPv6
connectivity.
- nodejs/node#40702
- nodejs/node#41625
- nodejs/node#44731

This mitigates issues like `UND_ERR_CONNECT_TIMEOUT` and localhost DNS
resolution in CI/CD environments:
Details:
- nodejs/node#40537
- actions/runner#3213
- actions/runner-images#9540

Node 20 introduces `setDefaultAutoSelectFamily`, a global function from
Node 19.4.0, enabling better IPv4 support, especially in environments
with limited or problematic IPv6 support.
Details:
- nodejs/node#45777

Node 20.x defaults to the new `autoSelectFamily`, improving network
connection reliability in GitHub runners lacking full IPv6 support.
Details:
- nodejs/node#46790
2024-03-30 13:54:45 +01:00
undergroundwires
52fadcd617 ci/cd: fix IPv6 timeouts with force-ipv4 action
This commit introduces the `force-ipv4` GitHub action to address
connectivity issues caused by the lack of IPv6 support in GitHub
runners. Details:
- actions/runner#3138
- actions/runner-images#668

This change solves connection problems when Node's `fetch` API fails due
to `UND_ERR_CONNECT_TIMEOUT` errors. Details:
- actions/runner-images#9540
- actions/runner#3213

This action disables IPv6 at the system level, ensuring all outging
requests use IPv4. Resolving connectivity issues when running external
URL checks and Docker build checks.

This solution is a temporary workaround until GitHub runners support
IPv6 or Node `fetch` API has a working solution such as Happy Eyeball.
Detais:
- nodejs/node#41625
- nodejs/undici#1531
2024-03-29 13:11:27 +01:00
undergroundwires
8a5592f92b ci/cd: Fix macOS Docker build reliability issues
This commit addresses intermittent failures in macOS Docker builds
within the GitHub Actions environment, attributed to slow agent
responses. By adjusting the retry logic, it aims to reduce build
failures caused by delayed Docker service readiness.

The enhancements increase the robustness and clarity of the build
process, especially for macOS, while maintaining functionality across
other operating systems.

Key changes:

- Increase max retries for the server check script from 30 to 90 for
  macOS, accommodating slower startup times.
- Refine retry logic to prevent unnecessary retries after receiving a
  definitive HTTP status code, enabling faster feedback and efficient
  failure handling.

Other supporting changes:

- Introduce a `--max-retries` parameter in the server status check
  script for dynamic adjustment based on the operating system.
- Add emojis to log messages to enhance the visibility of request
  attempts in logs.
- Shift from `http.get` to the `fetch` API for server status checks,
  utilizing its modern syntax, standardization, enriched feature set,
  and better error handling.
- Standardize error output to `stderr`.
- Add a Node.js shebang in the server check script to improve usability.
2024-03-27 11:56:58 +01:00
undergroundwires
79183d6417 Fix bottom gap in card expansion panel
This commit fixes an unintended bottom gap in the card expansional
panel, observed in the Windows script collection's "Advanced settings".

This issue arrives from a `min-height` CSS property that no longer
aligns with the current method for achieving balanced padding. It's only
visible when an action (a card) contains too few scripts (nodes).
2024-03-26 09:56:50 +01:00
undergroundwires
89243371fa win: improve and document removing Phone apps #279
This commit improves Windows scripts related to phone apps, extending
documentation, renaming scripts for clarity, removing unnecessary
scripts and adjusting recommendation levels.

Changes:

- Add script to disable the 'Call' system app, identified as missing in
  issue #279.
- Update documentation for each phone-related app to include
  descriptions and cautionary advice, focusing on privacy and
  system performance benefits.
- Rename scripts for better alignment with actual app names and to
  correct misconceptions:
  - 'Communications - Phone' to 'Microsoft Phone'
  - 'Your Phone Companion' to 'Your Phone'
- Remove the script for deleting `Microsoft.Windows.Phone` package,
  correcting a community misreport.
- Adjust recommendations to remove Phone-related apps, considering their
  limited necessity for OS functionality and common software use.
2024-03-25 12:07:26 +01:00
undergroundwires
4a9b430702 Update documentation for logo-update.js script
- Remove bash shebang from the JavaScript file.
- Add documentation on top of the script file.
2024-03-24 18:35:47 +01:00
undergroundwires-bot
ac176935f5 ⬆️ bump everywhere to 0.13.1 2024-03-23 09:39:01 +00:00
180 changed files with 11917 additions and 10925 deletions

32
.github/actions/force-ipv4/README.md vendored Normal file
View File

@@ -0,0 +1,32 @@
# force-ipv4
## Overview
This GitHub action enforces IPv4 for all outgoing network requests. It addresses connectivity issues encountered in GitHub runners, where IPv6 requests may lead to timeouts due to the lack of IPv6 support [1] [2].
## Background
Some applications attempt network connections over IPv6.
Such as requests made by Node's `fetch` API causes `UND_ERR_CONNECT_TIMEOUT` [3] [4] and similar issues [5].
This happens when the software cannot handle this such as by using Happy Eyeballs [6] [7].
## Usage
To use this action in your GitHub workflow, add the following step before any job that requires network access:
```yaml
- name: Enforce IPv4 Connectivity
uses: ./.github/actions/force-ipv4
```
## Note
This action is a workaround addressing specific IPv6-related connectivity issues on GitHub runners and may not be necessary if GitHub's infrastructure evolves to fully support IPv6 in the future.
[1]: https://archive.ph/2024.03.28-185829/https://github.com/actions/runner/issues/3138 "Actions Runner fails on IPv6 only host · Issue #3138 · actions/runner · GitHub | github.com"
[2]: https://archive.ph/2024.03.28-185838/https://github.com/actions/runner-images/issues/668 "IPv6 on GitHub-hosted runners · Issue #668 · actions/runner-images · GitHub | github.com"
[3]: https://archive.ph/2024.03.28-185847/https://github.com/actions/runner/issues/3213 "GitHub runner cannot send `fetch` with `node`, failing with IPv6 DNS error `UND_ERR_CONNECT_TIMEOUT` · Issue #3213 · actions/runner · GitHub | github.com"
[4]: https://archive.ph/2024.03.28-185853/https://github.com/actions/runner-images/issues/9540 "Cannot send outbound requests using node fetch, failing with IPv6 DNS error UND_ERR_CONNECT_TIMEOUT · Issue #9540 · actions/runner-images · GitHub | github.com"
[5]: https://archive.today/2024.03.30-113315/https://github.com/nodejs/node/issues/40537 "\"localhost\" favours IPv6 in node v17, used to favour IPv4 · Issue #40537 · nodejs/node · GitHub"
[6]: https://archive.ph/2024.03.28-185900/https://github.com/nodejs/node/issues/41625 "Happy Eyeballs support (address IPv6 issues in Node 17) · Issue #41625 · nodejs/node · GitHub | github.com"
[7]: https://archive.ph/2024.03.28-185910/https://github.com/nodejs/undici/issues/1531 "fetch times out in under 5 seconds · Issue #1531 · nodejs/undici · GitHub | github.com"

12
.github/actions/force-ipv4/action.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
inputs:
project-root:
required: false
default: '.'
runs:
using: composite
steps:
-
name: Run prefer IPv4 script
shell: bash
run: ./.github/actions/force-ipv4/force-ipv4.sh
working-directory: ${{ inputs.project-root }}

80
.github/actions/force-ipv4/force-ipv4.sh vendored Executable file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env bash
main() {
if is_linux; then
echo 'Configuring Linux...'
configure_warp_with_doh_and_ipv6_exclusion_on_linux # [WORKS] Resolves the issue when run independently on GitHub runners lacking IPv6 support.
prefer_ipv4_on_linux # [DOES NOT WORK] It does not resolve the issue when run independently on GitHub runners without IPv6 support.
# Considered alternatives:
# - `sysctl` commands, and direct changes to `/proc/sys/net/` and `/etc/sysctl.conf` led to silent
# Node 18 exits (code: 13) when using `fetch`.
elif is_macos; then
echo 'Configuring macOS...'
configure_warp_with_doh_and_ipv6_exclusion_on_macos # [WORKS] Resolves the issue when run independently on GitHub runners lacking IPv6 support.
disable_ipv6_on_macos # [WORKS INCONSISTENTLY] Resolves the issue inconsistently when run independently on GitHub runners without IPv6 support.
fi
echo "IPv4: $(curl --ipv4 --silent --max-time 15 --retry 3 --user-agent Mozilla https://api.ip.sb/geoip)"
echo "IPv6: $(curl --ipv6 --silent --max-time 15 --retry 3 --user-agent Mozilla https://api.ip.sb/geoip)"
}
is_linux() {
[[ "$(uname -s)" == "Linux" ]]
}
is_macos() {
[[ "$(uname -s)" == "Darwin" ]]
}
configure_warp_with_doh_and_ipv6_exclusion_on_linux() {
install_warp_on_debian
configure_warp_doh_and_exclude_ipv6
}
configure_warp_with_doh_and_ipv6_exclusion_on_macos() {
brew install cloudflare-warp
configure_warp_doh_and_exclude_ipv6
}
configure_warp_doh_and_exclude_ipv6() {
echo 'Beginning configuration of the Cloudflare WARP client with DNS-over-HTTPS and IPv6 exclusion...'
echo 'Initiating client registration with Cloudflare...'
warp-cli --accept-tos registration new
echo 'Configuring WARP to operate in DNS-over-HTTPS mode (warp+doh)...'
warp-cli --accept-tos mode warp+doh
echo 'Excluding IPv6 traffic from WARP by configuring it as a split tunnel...'
warp-cli --accept-tos add-excluded-route '::/0' # Exclude IPv6, forcing IPv4 resolution
# `tunnel ip add` does not work with IP ranges, see https://community.cloudflare.com/t/cant-cidr-for-split-tunnling/630834
echo 'Establishing WARP connection...'
warp-cli --accept-tos connect
}
install_warp_on_debian() {
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | sudo gpg --yes --dearmor --output /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflare-client.list
sudo apt-get update
sudo apt-get install -y cloudflare-warp
}
disable_ipv6_on_macos() {
networksetup -listallnetworkservices \
| tail -n +2 \
| while IFS= read -r interface; do
echo "Disabling IPv6 on: $interface..."
networksetup -setv6off "$interface"
done
}
prefer_ipv4_on_linux() {
local -r gai_config_file_path='/etc/gai.conf'
if [ ! -f "$gai_config_file_path" ]; then
echo "Creating $gai_config_file_path since it doesn't exist..."
touch "$gai_config_file_path"
fi
echo "precedence ::ffff:0:0/96 100" | sudo tee -a "$gai_config_file_path" > /dev/null
echo "Configuration complete."
}
main

View File

@@ -5,4 +5,5 @@ runs:
name: Setup node
uses: actions/setup-node@v4
with:
node-version: 18.x
node-version: 20.x
# check-latest: true # Newest versions can potentially have undiscovered bugs or regressions

View File

@@ -95,6 +95,12 @@ jobs:
-
name: Run Docker image on port 8080
run: docker run -d -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest
-
name: Enforce IPv4 Connectivity # Used due to GitHub runners' lack of IPv6 support, preventing request timeouts.
uses: ./.github/actions/force-ipv4
-
name: Check server is up and returns HTTP 200
run: node ./scripts/verify-web-server-status.js --url http://localhost:8080
run: >-
node ./scripts/verify-web-server-status.js \
--url http://localhost:8080 \
--max-retries ${{ matrix.os == 'macos' && '90' || '30' }}

View File

@@ -1,6 +1,7 @@
name: checks.external-urls
on:
push:
schedule:
- cron: '0 0 * * 0' # at 00:00 on every Sunday
@@ -17,6 +18,13 @@ jobs:
-
name: Install dependencies
uses: ./.github/actions/npm-install-dependencies
-
name: Enforce IPv4 Connectivity # Used due to GitHub runners' lack of IPv6 support, preventing request timeouts.
uses: ./.github/actions/force-ipv4
-
name: Test
run: npm run check:external-urls
env:
RANDOMIZED_URL_CHECK_LIMIT: "${{ github.event_name == 'push' && '100' || '3000' }}"
# - Scheduled checks has high limit for thorough testing.
# - For push events, triggered by code changes, the amount of URLs are limited to provide quick feedback.

View File

@@ -1,4 +1,4 @@
name: quality-checks
name: checks.quality
on: [ push, pull_request ]
@@ -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

View File

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

View File

@@ -1,5 +1,49 @@
# Changelog
## 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)
* Fix tooltip falling behind elements on fade out | [1964524](https://github.com/undergroundwires/privacy.sexy/commit/19645248ab7bc78dc872fa176c1a3650d7d6d644)
* Improve VSCode detection in `configure_vscode.py` | [98845e6](https://github.com/undergroundwires/privacy.sexy/commit/98845e6caee168db131aaf0736533e450827a52c)
* Bump TypeScript to 5.3 with `verbatimModuleSyntax` | [a721e82](https://github.com/undergroundwires/privacy.sexy/commit/a721e82a4fb603c0732ccfdffc87396c2a01363e)
* Migrate to Vite 5 and adjust configurations | [4ac1425](https://github.com/undergroundwires/privacy.sexy/commit/4ac1425f76079352268c488f3ff607d1fdc1beb2)
* win: improve and unify service start/stop logic | [adc2089](https://github.com/undergroundwires/privacy.sexy/commit/adc20898873d50a8873ffc74c48257e69a45d367)
* Upgrade vitest to v1 and fix test definitions | [e721885](https://github.com/undergroundwires/privacy.sexy/commit/e7218850ba62a7bebaf4768b13e46cba0dedd906)
* Improve URL checks to reduce false-negatives | [5abf8ff](https://github.com/undergroundwires/privacy.sexy/commit/5abf8ff216a1da737fd489864eeee880f78d6601)
* win: improve OneDrive data deletion safety | [5eff3a0](https://github.com/undergroundwires/privacy.sexy/commit/5eff3a04886d0d23a6e4c13a0178bb247105c5cb)
* Bump Electron to latest and use native ESM | [840adf9](https://github.com/undergroundwires/privacy.sexy/commit/840adf9429ed47f9e88c05e90f1d3ab930c2dfc4)
* Fix tooltip styling inconsistency | [ec34ac1](https://github.com/undergroundwires/privacy.sexy/commit/ec34ac1124e8b8ae53bf31a4dbdc88bb078b3d4e)
* win: fix VSCode manual update switch script #312 | [b71ad79](https://github.com/undergroundwires/privacy.sexy/commit/b71ad797a3af0db45143249903cb5e178692de7c)
* mac, linux, win: fix dead URLs and improve docs | [abec9de](https://github.com/undergroundwires/privacy.sexy/commit/abec9def075d82fdaee9663ef8fe1a488911f45b)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.0...0.13.1)
## 0.13.0 (2024-02-11)
* win: add disabling clipboard features #251, #247 | [c6ebba8](https://github.com/undergroundwires/privacy.sexy/commit/c6ebba85fb1b362be0d81d3078f19db71e0528b2)

View File

@@ -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.0/privacy.sexy-Setup-0.13.0.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.0/privacy.sexy-0.13.0.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.0/privacy.sexy-0.13.0.AppImage). For more options, see [here](#additional-install-options).
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.2/privacy.sexy-Setup-0.13.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.2/privacy.sexy-0.13.2.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.2/privacy.sexy-0.13.2.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.

View File

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

View File

@@ -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).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

View File

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

View File

@@ -0,0 +1,40 @@
# 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 (x64), ARM.
> **⚠️ 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 (64-bit), Apple Silicon (ARM).
> **⚠️ Compatibility Note:**
> Apple Silicon version runs non-natively, leading to slower performance due to emulation [2].
## 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 (x64).
## References
System requirements reflect Electron's platform capabilities [3] and Chromium's recommended configurations [4].
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.today/2024.04.28-082901/https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary%23overview "Building a universal macOS binary | Apple Developer Documentation | developer.apple.com"
[3]: 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"
[4]: 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"

View File

@@ -14,18 +14,19 @@ The presentation layer uses an event-driven architecture for bidirectional react
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
- [**`index.html`**](./../src/presentation/index.html): The `index.html` entry file, located at the root of the project as required by Vite
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
- [**`components/`**](./../src/presentation/components/): Contains Vue components, helpers and styles coupled to Vue components.
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
- [**`Hooks`**](../src/presentation/components/Shared/Hooks): Hooks used by components through [dependency injection](#dependency-injections).
- [**`/public/`**](../src/presentation/public/): Contains static assets.
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite.
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
- [**`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.
@@ -38,6 +39,13 @@ The presentation layer uses an event-driven architecture for bidirectional react
They should also have different visual state when hovering/touching on them that indicates that they are being clicked, which helps with accessibility.
- **Borders**:
privacy.sexy prefers sharper edges in its design language.
- **Fonts**:
- Use the primary font for regular text and monospace font for code or specific data.
- Use cursive and logo fonts solely for branding.
- Refer to [standardized font size variables](../src/presentation/assets/styles/_typography.scss) for font sizing, avoiding arbitrary `px`, `em`, `rem`, or percentage values.
- **Spacing**:
Use [global spacing variables](../src/presentation/assets/styles/_spacing.scss) for consistent margin, padding, and gap definitions.
This provides uniform spatial distribution and alignment of elements, enhancing visual harmony and making the UI more scalable and maintainable.
## Application data

View File

@@ -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`.

View File

@@ -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(
@@ -53,10 +54,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);
}

11629
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "privacy.sexy",
"version": "0.13.0",
"version": "0.13.2",
"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,66 +29,68 @@
"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"
},
"dependencies": {
"@floating-ui/vue": "^1.0.2",
"@floating-ui/vue": "^1.0.6",
"@juggle/resize-observer": "^3.4.0",
"@types/markdown-it": "^13.0.7",
"ace-builds": "^1.30.0",
"ace-builds": "^1.33.0",
"electron-log": "^5.1.2",
"electron-progressbar": "^2.1.0",
"electron-updater": "^6.1.4",
"electron-progressbar": "^2.2.1",
"electron-updater": "^6.1.9",
"file-saver": "^2.0.5",
"markdown-it": "^13.0.2",
"vue": "^3.3.7"
"markdown-it": "^14.1.0",
"vue": "^3.4.21"
},
"devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.1.0",
"@rushstack/eslint-patch": "^1.6.1",
"@types/ace": "^0.0.49",
"@types/file-saver": "^2.0.5",
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"@rushstack/eslint-patch": "^1.10.2",
"@types/ace": "^0.0.52",
"@types/file-saver": "^2.0.7",
"@types/markdown-it": "^14.0.1",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"@vitejs/plugin-legacy": "^5.3.2",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-airbnb-with-typescript": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/test-utils": "^2.4.1",
"autoprefixer": "^10.4.16",
"cypress": "^13.3.1",
"electron": "^29.1.4",
"@vue/eslint-config-typescript": "12.0.0",
"@vue/test-utils": "^2.4.5",
"autoprefixer": "^10.4.19",
"cypress": "^13.7.3",
"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.56.0",
"eslint": "8.57.0",
"eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-vue": "^9.19.2",
"eslint-plugin-vuejs-accessibility": "^2.2.0",
"icon-gen": "^4.0.0",
"jsdom": "^22.1.0",
"markdownlint-cli": "^0.37.0",
"postcss": "^8.4.31",
"eslint-plugin-vue": "^9.25.0",
"eslint-plugin-vuejs-accessibility": "^2.2.1",
"jsdom": "^24.0.0",
"markdownlint-cli": "^0.39.0",
"postcss": "^8.4.38",
"remark-cli": "^12.0.0",
"remark-lint-no-dead-urls": "^1.1.0",
"remark-preset-lint-consistent": "^5.1.2",
"remark-validate-links": "^13.0.0",
"sass": "^1.69.3",
"start-server-and-test": "^2.0.1",
"svgexport": "^0.4.2",
"terser": "^5.21.0",
"remark-preset-lint-consistent": "^6.0.0",
"remark-validate-links": "^13.0.1",
"sass": "^1.75.0",
"start-server-and-test": "^2.0.3",
"terser": "^5.30.3",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"vite": "^5.1.6",
"vitest": "^1.3.1",
"vue-tsc": "^1.8.19",
"typescript": "^5.4.5",
"vite": "^5.2.8",
"vitest": "^1.5.0",
"vue-tsc": "^2.0.13",
"yaml-lint": "^1.7.0"
},
"//devDependencies": {
"terser": "Used by `@vitejs/plugin-legacy` for minification",
"@rushstack/eslint-patch": "Needed by `@vue/eslint-config-typescript` and `@vue/eslint-config-airbnb-with-typescript`"
"@rushstack/eslint-patch": "Needed by `@vue/eslint-config-typescript` and `@vue/eslint-config-airbnb-with-typescript`",
"@typescript-eslint/eslint-plugin": "Cannot migrate to v7 because of `@vue/eslint-config-airbnb-with-typescript`, see https://github.com/vuejs/eslint-config-airbnb/issues/63",
"@typescript-eslint/parser": "Cannot migrate to v7 because of `@vue/eslint-config-airbnb-with-typescript`, see https://github.com/vuejs/eslint-config-airbnb/issues/63",
"@vue/eslint-config-typescript": "Cannot migrate to v13 because of `@vue/eslint-config-airbnb-with-typescript`, see https://github.com/vuejs/eslint-config-airbnb/issues/63",
"eslint": "Cannot migrate to v9 `@typescript-eslint/eslint-plugin` (≤ v7), `@typescript-eslint/parser` (≤ v7), `@vue/eslint-config-airbnb-with-typescript@` (≤ v8) requires `eslint` ≤ v8, see https://github.com/vuejs/eslint-config-airbnb/issues/65, https://github.com/typescript-eslint/typescript-eslint/issues/8211"
},
"homepage": "https://privacy.sexy",
"repository": {

View File

@@ -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()

View File

@@ -1,84 +1,120 @@
#!/usr/bin/env bash
import { resolve, join } from 'node:path';
import { rm, mkdtemp, stat } from 'node:fs/promises';
/**
* 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
*
* Notes:
* ImageMagick must be installed and accessible in the system's PATH
*/
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) {
@@ -89,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}`);
@@ -124,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();

View File

@@ -1,62 +1,87 @@
#!/usr/bin/env node
/**
* Description:
* This script checks if a server, provided as a CLI argument, is up
* and returns an HTTP 200 status code.
* It is designed to provide easy verification of server availability
* and will retry a specified number of times.
* This script checks if a server, provided as a CLI argument, is up
* and returns an HTTP 200 status code.
* It is designed to provide easy verification of server availability
* and will retry a specified number of times.
*
* Usage:
* node ./scripts/verify-web-server-status.js --url [URL]
* node ./scripts/verify-web-server-status.js --url [URL] [--max-retries NUMBER]
*
* Options:
* --url URL of the server to check
* --url URL of the server to check
* --max-retries Maximum number of retry attempts (default: 30)
*/
import { get } from 'http';
const MAX_RETRIES = 30;
const DEFAULT_MAX_RETRIES = 30;
const RETRY_DELAY_IN_SECONDS = 3;
const URL_PARAMETER_NAME = '--url';
const PARAMETER_NAME_URL = '--url';
const PARAMETER_NAME_MAX_RETRIES = '--max-retries';
function checkServer(currentRetryCount = 1) {
const serverUrl = getServerUrl();
console.log(`Requesting ${serverUrl}...`);
get(serverUrl, (res) => {
if (res.statusCode === 200) {
async function checkServer(currentRetryCount = 1) {
const serverUrl = readRequiredParameterValue(PARAMETER_NAME_URL);
const maxRetries = parseNumber(
readOptionalParameterValue(PARAMETER_NAME_MAX_RETRIES, DEFAULT_MAX_RETRIES),
);
console.log(`🌐 Requesting ${serverUrl}...`);
try {
const response = await fetch(serverUrl);
if (response.status === 200) {
console.log('🎊 Success: The server is up and returned HTTP 200.');
process.exit(0);
} else {
console.log(`Server returned HTTP status code ${res.statusCode}.`);
retry(currentRetryCount);
exitWithError(`Server returned unexpected HTTP status code ${response.statusCode}.`);
}
}).on('error', (err) => {
console.error('Error making the request:', err);
retry(currentRetryCount);
});
} catch (error) {
console.error('Error making the request:', error);
scheduleNextRetry(maxRetries, currentRetryCount);
}
}
function retry(currentRetryCount) {
console.log(`Attempt ${currentRetryCount}/${MAX_RETRIES}:`);
function scheduleNextRetry(maxRetries, currentRetryCount) {
console.log(`Attempt ${currentRetryCount}/${maxRetries}:`);
console.log(`Retrying in ${RETRY_DELAY_IN_SECONDS} seconds.`);
const remainingTime = (MAX_RETRIES - currentRetryCount) * RETRY_DELAY_IN_SECONDS;
const remainingTime = (maxRetries - currentRetryCount) * RETRY_DELAY_IN_SECONDS;
console.log(`Time remaining before timeout: ${remainingTime}s`);
if (currentRetryCount < MAX_RETRIES) {
if (currentRetryCount < maxRetries) {
setTimeout(() => checkServer(currentRetryCount + 1), RETRY_DELAY_IN_SECONDS * 1000);
} else {
console.log('Failure: The server at did not return HTTP 200 within the allocated time. Exiting.');
process.exit(1);
exitWithError('The server at did not return HTTP 200 within the allocated time.');
}
}
function getServerUrl() {
const urlIndex = process.argv.indexOf(URL_PARAMETER_NAME);
if (urlIndex === -1 || urlIndex === process.argv.length - 1) {
console.error(`Parameter "${URL_PARAMETER_NAME}" is not provided.`);
process.exit(1);
function readRequiredParameterValue(parameterName) {
const parameterValue = readOptionalParameterValue(parameterName);
if (parameterValue === undefined) {
exitWithError(`Parameter "${parameterName}" is required but not provided.`);
}
return process.argv[urlIndex + 1];
return parameterValue;
}
checkServer();
function readOptionalParameterValue(parameterName, defaultValue) {
const index = process.argv.indexOf(parameterName);
if (index === -1 || index === process.argv.length - 1) {
return defaultValue;
}
return process.argv[index + 1];
}
function parseNumber(numberLike) {
const number = parseInt(numberLike, 10);
if (Number.isNaN(number)) {
exitWithError(`Invalid number: ${numberLike}`);
}
return number;
}
function exitWithError(message) {
console.error(`Failure: ${message}`);
console.log('Exiting');
process.exit(1);
}
await checkServer();

View File

@@ -11,10 +11,11 @@ export type CodeRunErrorType =
| 'FileWriteError'
| 'FileReadbackVerificationError'
| 'FilePathGenerationError'
| 'UnsupportedOperatingSystem'
| 'FileExecutionError'
| 'UnsupportedPlatform'
| 'DirectoryCreationError'
| 'UnexpectedError';
| 'FilePermissionChangeError'
| 'FileExecutionError'
| 'ExternalProcessTermination';
interface CodeRunStatus {
readonly success: boolean;

View File

@@ -0,0 +1,12 @@
/*
Shuffle an array of strings, returning a new array with elements in random order.
Uses the Fisher-Yates (or Durstenfeld) algorithm.
*/
export function shuffle<T>(array: readonly T[]): T[] {
const shuffledArray = [...array];
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
}
return shuffledArray;
}

View File

@@ -1,44 +1,156 @@
/* eslint-disable max-classes-per-file */
import { PlatformTimer } from './PlatformTimer';
import type { Timer, TimeoutType } from './Timer';
export type CallbackType = (..._: readonly unknown[]) => void;
export interface ThrottleOptions {
/** Skip the immediate execution of the callback on the first invoke */
readonly excludeLeadingCall: boolean;
readonly timer: Timer;
}
const DefaultOptions: ThrottleOptions = {
excludeLeadingCall: false,
timer: PlatformTimer,
};
export function throttle(
callback: CallbackType,
waitInMs: number,
timer: Timer = PlatformTimer,
options: Partial<ThrottleOptions> = DefaultOptions,
): CallbackType {
const throttler = new Throttler(timer, waitInMs, callback);
const defaultedOptions: ThrottleOptions = {
...DefaultOptions,
...options,
};
const throttler = new Throttler(waitInMs, callback, defaultedOptions);
return (...args: unknown[]) => throttler.invoke(...args);
}
class Throttler {
private queuedExecutionId: TimeoutType | undefined;
private lastExecutionTime: number | null = null;
private previouslyRun: number;
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;
}
}

View File

@@ -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?"

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
export interface CommandDefinition {
buildShellCommand(filePath: string): string;
isExecutionTerminatedExternally(exitCode: number): boolean;
isExecutablePermissionsRequiredOnFile(): boolean;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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');
}

View File

@@ -0,0 +1,3 @@
export interface PowerShellInvokeShellCommandCreator {
createCommandToInvokePowerShell(powerShellCommand: string): string;
}

View File

@@ -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('\'', '\'\\\'\'')}'`;
}

View File

@@ -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, "''")}'`;
}

View File

@@ -0,0 +1,3 @@
export interface ShellArgumentEscaper {
escapePathArgument(pathArgument: string): string;
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,5 @@
import type { CommandDefinition } from '../CommandDefinition';
export interface CommandDefinitionFactory {
provideCommandDefinition(): CommandDefinition;
}

View File

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

View File

@@ -0,0 +1,9 @@
import type { ScriptFileExecutionOutcome } from '../../ScriptFileExecutor';
import type { CommandDefinition } from '../CommandDefinition';
export interface CommandDefinitionRunner {
runCommandDefinition(
commandDefinition: CommandDefinition,
filePath: string,
): Promise<ScriptFileExecutionOutcome>;
}

View File

@@ -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}`,
},
};
}

View File

@@ -0,0 +1,5 @@
import type { ScriptFileExecutionOutcome } from '@/infrastructure/CodeRunner/Execution/ScriptFileExecutor';
export interface ExecutablePermissionSetter {
makeFileExecutable(filePath: string): Promise<ScriptFileExecutionOutcome>;
}

View File

@@ -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}`,
},
};
}
}
}

View File

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

View File

@@ -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;
}

View File

@@ -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,
);
}
}

View File

@@ -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}"`;
}

View File

@@ -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,
) { }

View File

@@ -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,
};
}

View File

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

View File

@@ -0,0 +1,16 @@
// Use for fixed-size elements where consistent spacing is important
// regardless of context.
$spacing-absolute-xx-small: 3px;
$spacing-absolute-x-small : 4px;
$spacing-absolute-small : 6px;
$spacing-absolute-medium : 10px;
$spacing-absolute-large : 15px;
$spacing-absolute-x-large : 20px;
$spacing-absolute-xx-large: 30px;
// Use for elements with text content where spacing should
// scale with text size.
$spacing-relative-x-small : 0.25em;
$spacing-relative-small : 0.5em;
$spacing-relative-medium : 1em;
$spacing-relative-large : 2em;

View File

@@ -5,24 +5,28 @@
CSS Base applies a style foundation for HTML elements that is consistent for baseline browsers
*/
@use "@/presentation/assets/styles/colors" as *;
@use "@/presentation/assets/styles/mixins" as *;
@use "@/presentation/assets/styles/vite-path" as *;
@use "@/presentation/assets/styles/typography" as *;
@use "../colors" as *;
@use "../mixins" as *;
@use "../vite-path" as *;
@use "../typography" as *;
@use "../spacing" as *;
@use "_code-styling" as *;
@use "_margin-padding" as *;
@use "_link-styling" as *;
$base-spacing: 1em;
@use "_prevent-scrollbar-layout-shift" as *;
* {
box-sizing: border-box;
}
html {
@include prevent-scrollbar-layout-shift;
}
body {
background: $color-background;
@include base-font-style;
@include apply-uniform-spacing($base-spacing: $base-spacing)
@include apply-uniform-spacing;
}
input {
@@ -30,12 +34,12 @@ input {
}
blockquote {
padding: 0 $base-spacing;
border-left: .25em solid $color-primary;
padding: 0 $spacing-relative-medium;
border-left: $spacing-absolute-x-small solid $color-primary;
}
@include style-code-elements(
$code-block-padding: $base-spacing,
$code-block-padding: $spacing-relative-medium,
$color-background: $color-primary-darker,
);

View File

@@ -1,4 +1,5 @@
@use 'sass:math';
@use "../spacing" as *;
@mixin no-margin($selectors) {
#{$selectors} {
@@ -26,7 +27,7 @@
}
}
@mixin apply-uniform-vertical-spacing($base-vertical-spacing) {
@mixin apply-uniform-vertical-spacing {
/* Reset default top/bottom margins added by browser. */
@include no-margin('p');
@include no-margin('h1, h2, h3, h4, h5, h6');
@@ -36,29 +37,27 @@
@include no-margin('ul, ol');
/* Add spacing between elements using `margin-bottom` only (bottom-up instead of top-down strategy). */
$small-vertical-spacing: math.div($base-vertical-spacing, 2);
@include bottom-margin('p', $base-vertical-spacing);
@include bottom-margin('li > p', $small-vertical-spacing); // Reduce margin for paragraphs directly within list items to visually group related content.
@include bottom-margin('h1, h2, h3, h4, h5, h6', $small-vertical-spacing);
@include bottom-margin('ul, ol', $base-vertical-spacing);
@include bottom-margin('li', $small-vertical-spacing);
@include bottom-margin('table', $base-vertical-spacing);
@include bottom-margin('blockquote', $base-vertical-spacing);
@include bottom-margin('pre', $base-vertical-spacing);
@include bottom-margin('article', $base-vertical-spacing);
@include bottom-margin('hr', $base-vertical-spacing);
@include bottom-margin('p', $spacing-relative-medium);
@include bottom-margin('li > p', $spacing-relative-small); // Reduce margin for paragraphs directly within list items to visually group related content.
@include bottom-margin('h1, h2, h3, h4, h5, h6', $spacing-relative-small);
@include bottom-margin('ul, ol', $spacing-relative-medium);
@include bottom-margin('li', $spacing-relative-small);
@include bottom-margin('table', $spacing-relative-medium);
@include bottom-margin('blockquote', $spacing-relative-medium);
@include bottom-margin('pre', $spacing-relative-medium);
@include bottom-margin('article', $spacing-relative-medium);
@include bottom-margin('hr', $spacing-relative-medium);
}
@mixin apply-uniform-horizontal-spacing($base-horizontal-spacing) {
@mixin apply-uniform-horizontal-spacing {
/* Reset default left/right paddings added by browser. */
@include no-padding('ul, ol');
/* Add spacing for list items. */
$large-horizontal-spacing: $base-horizontal-spacing * 2;
@include left-padding('ul, ol', $large-horizontal-spacing);
@include left-padding('ul, ol', $spacing-relative-large);
}
@mixin apply-uniform-spacing($base-spacing) {
@include apply-uniform-vertical-spacing($base-spacing);
@include apply-uniform-horizontal-spacing($base-spacing);
@mixin apply-uniform-spacing {
@include apply-uniform-vertical-spacing;
@include apply-uniform-horizontal-spacing;
}

View File

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

View File

@@ -1 +0,0 @@
$card-gap: 15px;

View File

@@ -5,6 +5,5 @@
@forward "./media";
@forward "./colors";
@forward "./base";
@forward "./spacing";
@forward "./mixins";
@forward "./components/card";

View File

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

View File

@@ -50,24 +50,48 @@ function getOptionalDevToolkitComponent(): Component | undefined {
<style lang="scss">
@use "@/presentation/assets/styles/main" as *;
@use 'sass:math';
@mixin responsive-spacing {
// Avoid using percentage-based values for spacing the avoid unintended layout shifts.
margin-left: $spacing-absolute-medium;
margin-right: $spacing-absolute-medium;
padding: $spacing-absolute-xx-large;
@media screen and (max-width: $media-screen-big-width) {
margin-left: $spacing-absolute-small;
margin-right: $spacing-absolute-small;
padding: $spacing-absolute-x-large;
}
@media screen and (max-width: $media-screen-medium-width) {
margin-left: $spacing-absolute-x-small;
margin-right: $spacing-absolute-x-small;
padding: $spacing-absolute-medium;
}
@media screen and (max-width: $media-screen-small-width) {
margin-left: 0;
margin-right: 0;
padding: $spacing-absolute-small;
}
}
#app {
margin-right: auto;
margin-left: auto;
max-width: 1600px;
.app__wrapper {
margin: 0% 2% 0% 2%;
background-color: $color-surface;
color: $color-on-surface;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.06);
padding: 2%;
@include responsive-spacing;
display:flex;
flex-direction: column;
.app__row {
margin-bottom: 10px;
margin-bottom: $spacing-absolute-large;
}
.app__code-buttons {
padding-bottom: 10px;
padding-bottom: $spacing-absolute-medium;
}
}
}

View File

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

View File

@@ -74,7 +74,6 @@ export default defineComponent({
color: $color-on-secondary;
border: none;
padding: 20px;
transition-duration: 0.4s;
overflow: hidden;
box-shadow: 0 3px 9px $color-primary-darkest;

View File

@@ -54,14 +54,14 @@ export default defineComponent({
.copyable-command {
display: inline-flex;
padding: 0.25em;
padding: $spacing-relative-x-small;
font-size: $font-size-absolute-small;
.dollar {
margin-right: 0.5rem;
margin-right: $spacing-relative-small;
user-select: none;
}
.copy-action-container {
margin-left: 1rem;
margin-left: $spacing-relative-medium;
}
}
</style>

View File

@@ -8,7 +8,7 @@
import { defineComponent } from 'vue';
export default defineComponent({
name: 'InstructionSteps', // Define component name for empty component for Vue build and ESLint compatibility.
// Empty component for ESLint compatibility, workaround for https://github.com/vuejs/vue-eslint-parser/issues/125.
});
</script>

View File

@@ -8,6 +8,6 @@
import { defineComponent } from 'vue';
export default defineComponent({
name: 'InstructionSteps', // Define component name for empty component for Vue build and ESLint compatibility.
// Empty component for ESLint compatibility, workaround for https://github.com/vuejs/vue-eslint-parser/issues/125.
});
</script>

View File

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

View File

@@ -34,12 +34,15 @@ export default defineComponent({
</script>
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
.container {
display: flex;
flex-direction: row;
justify-content: center;
gap: 30px;
gap: $spacing-absolute-xx-large;
}
.code-button {
width: 10%;
min-width: 90px;

View File

@@ -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,13 +74,17 @@ 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: 10px;
padding: $spacing-absolute-medium;
z-index: 10000;
display:flex;
@@ -113,14 +120,14 @@ interface DevAction {
.action-buttons {
display: flex;
flex-direction: column;
gap: 10px;
gap: $spacing-absolute-medium;
@include reset-ul;
.action-button {
@include reset-button;
display: block;
padding: 5px 10px;
padding: $spacing-absolute-x-small $spacing-absolute-medium;
background-color: $color-primary;
color: $color-on-primary;
border: none;

View File

@@ -0,0 +1,39 @@
import {
computed, readonly, ref, watch,
} from 'vue';
import { throttle } from '@/application/Common/Timing/Throttle';
import { useAutoUnsubscribedEventListener } from '../Shared/Hooks/UseAutoUnsubscribedEventListener';
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);
const observer = new ResizeObserver((entries) => throttle(() => {
for (const entry of entries) {
width.value = entry.borderBoxSize[0].inlineSize;
}
}, RESIZE_EVENT_THROTTLE_MS));
observer.observe(document.body, { box: 'border-box' });
return readonly(width);
}

View File

@@ -25,7 +25,7 @@ export default defineComponent({
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
$gap: 0.25rem;
$gap: $spacing-relative-x-small;
.list {
display: flex;
:deep(.items) {

View File

@@ -37,8 +37,10 @@ export default defineComponent({
</script>
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
.circle-rating {
display: inline-flex;
gap: 0.2em;
gap: $spacing-relative-x-small;
}
</style>

View File

@@ -1,19 +1,17 @@
<template>
<svg
:style="{
'--circle-stroke-width': `${circleStrokeWidthInPx}px`,
}"
:viewBox="viewBox"
>
<circle
:cx="circleRadiusInPx"
:cy="circleRadiusInPx"
:r="circleRadiusWithoutStrokeInPx"
:class="{
filled,
}"
/>
</svg>
<span class="circle-container">
<svg :viewBox="viewBox">
<circle
:cx="circleRadiusInPx"
:cy="circleRadiusInPx"
:r="circleRadiusWithoutStrokeInPx"
:stroke-width="circleStrokeWidthStyleValue"
:class="{
filled,
}"
/>
</svg>
</span>
</template>
<script lang="ts">
@@ -43,10 +41,13 @@ export default defineComponent({
const height = circleDiameterInPx + circleStrokeWidthInPx;
return `${minX} ${minY} ${width} ${height}`;
});
const circleStrokeWidthStyleValue = computed(() => {
return `${circleStrokeWidthInPx}px`;
});
return {
circleRadiusInPx,
circleDiameterInPx,
circleStrokeWidthInPx,
circleStrokeWidthStyleValue,
circleRadiusWithoutStrokeInPx,
viewBox,
};
@@ -55,17 +56,18 @@ export default defineComponent({
</script>
<style scoped lang="scss">
$circleColor: currentColor;
$circleHeight: 0.8em;
$circleStrokeWidth: var(--circle-stroke-width);
@use "@/presentation/assets/styles/main" as *;
$circle-color: currentColor;
$circle-height: $font-size-relative-smaller;
svg {
height: $circleHeight;
font-size: $circle-height;
height: 1em;
circle {
stroke: $circleColor;
stroke-width: $circleStrokeWidth;
stroke: $circle-color;
&.filled {
fill: $circleColor;
fill: $circle-color;
}
}
}

View File

@@ -88,7 +88,7 @@ export default defineComponent({
@mixin horizontal-stack {
display: flex;
gap: 0.5em;
gap: $spacing-relative-small;
}
@mixin apply-icon-color($color) {

View File

@@ -55,7 +55,7 @@ export default defineComponent({
@mixin horizontal-stack {
display: flex;
gap: 0.5em;
gap: $spacing-relative-small;
}
@mixin apply-icon-color($color) {

View File

@@ -1,13 +1,13 @@
<template>
<div id="container">
<div class="item rows">
<TheRecommendationSelector class="item" />
<TheRevertSelector class="item" />
<div class="scripts-menu">
<div class="scripts-menu-item scripts-menu-rows">
<TheRecommendationSelector class="scripts-menu-item" />
<TheRevertSelector class="scripts-menu-item" />
</div>
<TheOsChanger class="item" />
<TheOsChanger class="scripts-menu-item" />
<TheViewChanger
v-if="!isSearching"
class="item"
class="scripts-menu-item"
@view-changed="$emit('viewChanged', $event)"
/>
</div>
@@ -67,29 +67,44 @@ export default defineComponent({
</script>
<style scoped lang="scss">
$margin-between-lines: 7px;
#container {
@use "@/presentation/assets/styles/main" as *;
@use 'sass:math';
@mixin center-middle-flex-item {
&:first-child, &:last-child {
flex-grow: 1;
flex-basis: 0;
}
&:last-child {
justify-content: flex-end;
}
}
$responsive-alignment-breakpoint: $media-screen-medium-width;
.scripts-menu {
display: flex;
flex-wrap: wrap;
margin-top: -$margin-between-lines;
.item {
flex: 1;
white-space: nowrap;
column-gap: $spacing-relative-medium;
row-gap: $spacing-relative-small;
flex-wrap: wrap;
align-items: center;
margin-left: $spacing-absolute-small;
margin-right: $spacing-absolute-small;
@media screen and (max-width: $responsive-alignment-breakpoint) {
justify-content: space-around;
}
.scripts-menu-item {
display: flex;
justify-content: center;
align-items: center;
margin: $margin-between-lines 5px 0 5px;
&:first-child {
justify-content: flex-start;
}
&:last-child {
justify-content: flex-end;
@media screen and (min-width: $responsive-alignment-breakpoint) {
@include center-middle-flex-item;
}
}
.rows {
.scripts-menu-rows {
display: flex;
flex-direction: column;
align-items: flex-start;
row-gap: $spacing-relative-x-small;
}
}
</style>

View File

@@ -1,13 +1,5 @@
<template>
<div
class="slider"
:style="{
'--vertical-margin': verticalMargin,
'--first-min-width': firstMinWidth,
'--first-initial-width': firstInitialWidth,
'--second-min-width': secondMinWidth,
}"
>
<div class="slider">
<div ref="firstElement" class="first">
<slot name="first" />
</div>
@@ -27,10 +19,6 @@ export default defineComponent({
SliderHandle,
},
props: {
verticalMargin: {
type: String,
required: true,
},
firstMinWidth: {
type: String,
required: true,
@@ -71,21 +59,18 @@ export default defineComponent({
display: flex;
flex-direction: row;
.first {
min-width: var(--first-min-width);
width: var(--first-initial-width);
min-width: v-bind(firstMinWidth);
width: v-bind(firstInitialWidth);
}
.second {
flex: 1;
min-width: var(--second-min-width);
min-width: v-bind(secondMinWidth);
}
@media screen and (max-width: $media-vertical-view-breakpoint) {
flex-direction: column;
.first {
width: auto !important;
}
.second {
margin-top: var(--vertical-margin);
}
.handle {
display: none;
}

View File

@@ -81,7 +81,7 @@ $cursor : v-bind(cursorCssValue);
.icon {
color: $color;
}
margin-right: 5px;
margin-left: 5px;
margin-right: $spacing-absolute-small;
margin-left: $spacing-absolute-small;
}
</style>

View File

@@ -2,8 +2,7 @@
<div class="script-area">
<TheScriptsMenu @view-changed="currentView = $event" />
<HorizontalResizeSlider
class="row"
vertical-margin="15px"
class="horizontal-slider"
first-initial-width="55%"
first-min-width="20%"
second-min-width="20%"
@@ -42,9 +41,17 @@ export default defineComponent({
</script>
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
.script-area {
display: flex;
flex-direction: column;
gap: 6px;
gap: $spacing-absolute-medium;
}
.horizontal-slider {
// Add row gap between lines on mobile (smaller screens)
// when the slider turns into rows.
row-gap: $spacing-absolute-large;
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<transition name="card-expand-collapse-transition">
<slot />
</transition>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
// Empty component for ESLint compatibility, workaround for https://github.com/vuejs/vue-eslint-parser/issues/125.
});
</script>
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
.card-expand-collapse-transition-enter-active {
transition:
opacity 0.3s ease-in-out,
max-height 0.3s ease-in-out;
}
.card-expand-collapse-transition-enter-from {
opacity: 0; // Fade-in
max-height: 0; // Expand
}
</style>

View File

@@ -0,0 +1,32 @@
<template>
<div class="arrow-container">
<div class="arrow" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
// Empty component for ESLint compatibility, workaround for https://github.com/vuejs/vue-eslint-parser/issues/125.
});
</script>
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
$arrow-size: $font-size-absolute-normal;
.arrow-container {
position: relative;
.arrow {
position: absolute;
left: calc(50% - $arrow-size * 1.5);
top: calc(-0.35 * $arrow-size);
border: solid $color-primary-darker;
border-width: 0 $arrow-size $arrow-size 0;
padding: $arrow-size;
transform: rotate(-135deg);
}
}
</style>

View File

@@ -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 {
@@ -125,6 +118,7 @@ function isClickable(element: Element) {
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
@use "./card-gap" as *;
.cards {
display: flex;
@@ -135,7 +129,7 @@ function isClickable(element: Element) {
It ensures that there's room to grow, so the animation is shown without overflowing
with scrollbars.
*/
padding: 10px;
padding: $spacing-absolute-medium;
}
.error {

View File

@@ -3,7 +3,6 @@
ref="cardElement"
class="card"
:class="{
'is-collapsed': !isExpanded,
'is-inactive': activeCategoryId && activeCategoryId !== categoryId,
'is-expanded': isExpanded,
}"
@@ -29,20 +28,28 @@
:category-id="categoryId"
/>
</div>
<div class="card__expander" @click.stop>
<div class="card__expander__close-button">
<FlatButton
icon="xmark"
@click="collapse()"
/>
<CardExpandTransition>
<div v-show="isExpanded">
<CardExpansionArrow />
<div
class="card__expander"
@click.stop
>
<div class="card__expander__close-button">
<FlatButton
icon="xmark"
@click="collapse()"
/>
</div>
<div class="card__expander__content">
<ScriptsTree
:category-id="categoryId"
:has-top-padding="false"
/>
</div>
</div>
</div>
<div class="card__expander__content">
<ScriptsTree
:category-id="categoryId"
:has-top-padding="false"
/>
</div>
</div>
</CardExpandTransition>
</div>
</template>
@@ -56,6 +63,8 @@ import { injectKey } from '@/presentation/injectionSymbols';
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
import CardSelectionIndicator from './CardSelectionIndicator.vue';
import CardExpandTransition from './CardExpandTransition.vue';
import CardExpansionArrow from './CardExpansionArrow.vue';
export default defineComponent({
components: {
@@ -63,6 +72,8 @@ export default defineComponent({
AppIcon,
CardSelectionIndicator,
FlatButton,
CardExpandTransition,
CardExpansionArrow,
},
props: {
categoryId: {
@@ -127,16 +138,14 @@ export default defineComponent({
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
@use "./card-gap" as *;
$card-inner-padding : 30px;
$arrow-size : 15px;
$expanded-margin-top : 30px;
$card-inner-padding : $spacing-absolute-xx-large;
$expanded-margin-top : $spacing-absolute-xx-large;
$card-horizontal-gap : $card-gap;
.card {
transition: all 0.2s ease-in-out;
&__inner {
.card__inner {
padding-top: $card-inner-padding;
padding-right: $card-inner-padding;
padding-bottom: 0;
@@ -149,7 +158,7 @@ $card-horizontal-gap : $card-gap;
width: 100%;
text-transform: uppercase;
text-align: center;
transition: all 0.2s ease-in-out;
transition: transform 0.2s ease-in-out;
display:flex;
flex-direction: column;
@@ -160,9 +169,6 @@ $card-horizontal-gap : $card-gap;
color: $color-on-secondary;
transform: scale(1.05);
}
&:after {
transition: all 0.3s ease-in-out;
}
.card__inner__title {
display: flex;
flex-direction: column;
@@ -173,19 +179,18 @@ $card-horizontal-gap : $card-gap;
.card__inner__selection_indicator {
height: $card-inner-padding;
margin-right: -$card-inner-padding;
padding-right: 10px;
padding-right: $spacing-absolute-medium;
display: flex;
justify-content: flex-end;
}
.card__inner__expand-icon {
width: 100%;
margin-top: .25em;
margin-top: $spacing-relative-x-small;
vertical-align: middle;
font-size: $font-size-absolute-normal;
}
}
.card__expander {
transition: all 0.2s ease-in-out;
position: relative;
background-color: $color-primary-darker;
color: $color-on-primary;
@@ -205,7 +210,7 @@ $card-horizontal-gap : $card-gap;
.card__expander__close-button {
font-size: $font-size-absolute-large;
align-self: flex-end;
margin-right: 0.25em;
margin-right: $spacing-absolute-small;
@include clickable;
color: $color-primary-light;
@include hover-or-touch {
@@ -214,43 +219,15 @@ $card-horizontal-gap : $card-gap;
}
}
&.is-collapsed {
.card__inner {
&:after {
content: "";
opacity: 0;
}
}
.card__expander {
max-height: 0;
min-height: 0;
overflow: hidden;
opacity: 0;
}
}
&.is-expanded {
.card__inner {
height: auto;
background-color: $color-secondary;
color: $color-on-secondary;
&:after { // arrow
content: "";
display: block;
position: absolute;
bottom: calc(-1 * #{$expanded-margin-top});
left: calc(50% - #{$arrow-size});
border-left: #{$arrow-size} solid transparent;
border-right: #{$arrow-size} solid transparent;
border-bottom: #{$arrow-size} solid $color-primary-darker;
}
}
.card__expander {
min-height: 200px;
margin-top: $expanded-margin-top;
opacity: 1;
}
@include hover-or-touch {

View File

@@ -0,0 +1,3 @@
@use "@/presentation/assets/styles/main" as *;
$card-gap: $spacing-absolute-large;

View File

@@ -128,10 +128,7 @@ export default defineComponent({
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
$margin-inner: 4px;
.scripts-view {
margin-top: $margin-inner;
@media screen and (min-width: $media-vertical-view-breakpoint) {
// so the current code is always visible
overflow: auto;
@@ -143,16 +140,17 @@ $margin-inner: 4px;
display: flex;
flex-direction: column;
background-color: $color-scripts-bg;
padding-top: $spacing-absolute-large;
padding-bottom:$spacing-absolute-large;
.search__query {
display: flex;
justify-content: center;
flex-direction: row;
align-items: center;
margin-top: 1em;
color: $color-primary-light;
.search__query__close-button {
font-size: $font-size-absolute-large;
margin-left: 0.25rem;
margin-left: $spacing-relative-x-small;
}
}
.search-no-matches {
@@ -161,11 +159,9 @@ $margin-inner: 4px;
word-break:break-word;
color: $color-on-primary;
font-size: $font-size-absolute-large;
padding:10px;
padding: $spacing-absolute-medium;
text-align:center;
> div {
padding-bottom:13px;
}
gap: $spacing-relative-small;
a {
color: $color-primary;
}

View File

@@ -69,7 +69,7 @@ export default defineComponent({
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: 5px;
margin-left: $spacing-absolute-small;
}
.header {
display: flex;
@@ -80,10 +80,10 @@ export default defineComponent({
}
.docs {
background: $color-primary-darkest;
margin-top: 0.25em;
margin-top: $spacing-relative-x-small;
color: $color-on-primary;
text-transform: none;
padding: 0.5em;
padding: $spacing-absolute-medium;
&-collapsed {
display: none;
}

View File

@@ -1,6 +1,6 @@
import MarkdownIt from 'markdown-it';
import type { MarkdownRenderer } from '../MarkdownRenderer';
import type { RenderRule } from 'markdown-it/lib/renderer';
import type { RenderRule } from 'markdown-it/lib/renderer.mjs'; // eslint-disable-line import/extensions
export class MarkdownItHtmlRenderer implements MarkdownRenderer {
public render(markdownContent: string): string {

View File

@@ -1,12 +1,15 @@
<template>
<DocumentableNode :docs="nodeMetadata.docs">
<div id="node">
<div class="item">
<DocumentableNode
class="node-content-wrapper"
:docs="nodeMetadata.docs"
>
<div class="node-content">
<div class="node-content-item">
<NodeTitle :title="nodeMetadata.text" />
</div>
<RevertToggle
v-if="nodeMetadata.isReversible"
class="item"
class="node-content-item"
:node="nodeMetadata"
/>
</div>
@@ -38,13 +41,22 @@ export default defineComponent({
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
#node {
display: flex;
flex-direction: row;
flex-wrap: wrap;
.node-content-wrapper {
/*
Compensate word breaking when it causes overflows of the node content,
This issue happens on small devices when nodes are being rendered during search where the node header or
documentation grows to cause to overflow.
*/
overflow-wrap: anywhere;
.item:not(:first-child) {
margin-left: 5px;
.node-content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
.node-content-item:not(:first-child) {
margin-left: $spacing-relative-small;
}
}
}
</style>

View File

@@ -2,7 +2,7 @@
<ToggleSwitch
v-model="isReverted"
:stop-click-propagation="true"
:label="'Revert'"
label="Revert"
/>
</template>

View File

@@ -83,12 +83,12 @@ $color-text-unchecked : $color-on-primary;
$color-text-checked : $color-on-secondary;
$color-bg-unchecked : $color-primary;
$color-bg-checked : $color-secondary;
$padding-horizontal : calc($font-size * 0.4);
$padding-vertical : calc($font-size * 0.3);
$padding-horizontal : $spacing-relative-small;
$padding-vertical : $spacing-absolute-small;
$size-height : calc($font-size + ($padding-vertical * 2));
$size-circle : math.div($size-height * 2, 3);
$size-circle : calc($size-height * 2/3);
$gap-between-circle-and-text : 0.25em;
$gap-between-circle-and-text : $spacing-relative-x-small;
@mixin locateNearCircle($direction: 'left') {
$circle-width: calc(#{$size-circle} + #{$padding-horizontal});

View File

@@ -73,7 +73,8 @@ export default defineComponent({
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
$padding: 20px;
$padding-horizontal : $spacing-absolute-large;
$padding-vertical : $spacing-absolute-x-large;
.scripts-tree-container {
display: flex; // We could provide `block`, but `flex` is more versatile.
@@ -84,11 +85,11 @@ $padding: 20px;
flex: 1; // Expands the container to fill available horizontal space, enabling alignment of child items.
padding-bottom: $padding;
padding-left: $padding;
padding-right: $padding;
padding-bottom: $padding-vertical;
padding-left: $padding-horizontal;
padding-right: $padding-horizontal;
&.top-padding {
padding-top: $padding;
padding-top: $padding-vertical;
}
}
</style>

View File

@@ -9,7 +9,7 @@
:tree-root="treeRoot"
>
<div
class="expand-collapse-arrow"
class="expand-collapse-caret"
:class="{
expanded: isExpanded,
'has-children': hasChildren,
@@ -141,10 +141,13 @@ export default defineComponent({
flex-direction: row;
align-items: center;
.expand-collapse-arrow {
.expand-collapse-caret {
$caret-size: 24px;
$padding-right: $spacing-absolute-small;
flex-shrink: 0;
height: 30px;
margin-left: 30px;
height: $caret-size;
margin-left: $caret-size + $padding-right;
width: 0;
@include clickable;
@@ -157,25 +160,32 @@ export default defineComponent({
&.has-children {
margin-left: 0;
width: 30px;
width: $caret-size + $padding-right;
position: relative;
$caret-dimension: $caret-size * 0.375;
$caret-stroke-width: 1.5px;
&:after {
border: 1.5px solid $color-node-arrow;
border: $caret-stroke-width solid $color-node-arrow;
position: absolute;
border-left: 0;
border-top: 0;
left: 9px;
left: $caret-dimension;
top: 50%;
height: 9px;
width: 9px;
transform: rotate(-45deg) translateY(-50%) translateX(0);
height: $caret-dimension;
width: $caret-dimension;
transform:
rotate(-45deg)
translateY(-50%)
translateX($caret-dimension * 0.2);
transition: transform .25s;
transform-origin: center;
}
&.expanded:after {
transform: rotate(45deg) translateY(-50%) translateX(-5px);
transform:
rotate(45deg)
translateY(-50%)
translateX($caret-dimension * -0.5);
}
}
}

View File

@@ -76,18 +76,18 @@ export default defineComponent({
}
}
.node {
margin-bottom: 3px;
margin-top: 3px;
padding-bottom: 3px;
padding-top: 3px;
padding-right: 6px;
margin-bottom: $spacing-absolute-xx-small;
margin-top: $spacing-absolute-xx-small;
padding-bottom: $spacing-absolute-xx-small;
padding-top: $spacing-absolute-xx-small;
padding-right: $spacing-absolute-small;
box-sizing: border-box;
.content {
display: flex; // We could provide `block`, but `flex` is more versatile.
color: $color-node-fg;
padding-left: 9px;
padding-right: 6px;
padding-left: $spacing-relative-small;
padding-right: $spacing-absolute-x-small;
text-decoration: none;
user-select: none;
}

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