Compare commits

...

170 commits
15.1 ... master

Author SHA1 Message Date
Aymeric Cucherousset
a49d1baf87
Fix french translations (#538) 2025-04-15 07:16:06 +02:00
Piotr (Peter) Mardziel
fe8d8fe383
translation fixes and disambiguations (#528)
Co-authored-by: Piotr Mardziel <piotrm@Piotrs-Laptop.local>
2025-04-15 07:14:31 +02:00
crschnick
ab71d178f3 Show script icons in browser 2025-03-25 14:59:11 +00:00
crschnick
9318a24120 Fix script hierarchy containing duplicates 2025-03-25 14:56:46 +00:00
crschnick
a69ecdc94c Select icon if it is an exact match 2025-03-25 14:03:23 +00:00
crschnick
937d59a27a Fix potential OOB 2025-03-24 15:06:36 +00:00
crschnick
f648d63f4b [release] 2025-03-23 08:10:15 +00:00
crschnick
881452ccd0 Don't focus window after load 2025-03-22 18:52:36 +00:00
crschnick
c7bde460af [stage] 2025-03-22 18:39:01 +00:00
crschnick
12fe24a695 Make desktop open more robust 2025-03-22 18:37:59 +00:00
crschnick
3c13cf5e2a Improve tab init fail handling 2025-03-22 14:53:26 +00:00
crschnick
bc0e14a332 Don't check nested event loop return value 2025-03-22 13:30:06 +00:00
crschnick
10dab33ed4 Rework list box sync again 2025-03-22 13:24:18 +00:00
crschnick
c67411815e Check for event loop enter issues 2025-03-22 13:00:32 +00:00
crschnick
74bedf630c Fix list box sync 2025-03-22 11:23:59 +00:00
crschnick
93413c3da1 Improve icon choice keyboard workflow 2025-03-22 10:15:30 +00:00
crschnick
b39c74b4ff Improve list box performance by synchronizing less 2025-03-21 16:53:10 +00:00
crschnick
474d201708 Improve download progress display 2025-03-21 16:52:45 +00:00
crschnick
5c6acc50e4 [stage] 2025-03-21 14:32:04 +00:00
crschnick
0c0d91b67d Add ability to launch custom terminal editors 2025-03-21 14:28:33 +00:00
crschnick
894b93e3c2 [stage] 2025-03-21 09:33:10 +00:00
crschnick
8c516c043c Improve ssh key chmod 2025-03-21 09:25:46 +00:00
crschnick
9dc368ae7b Delay list box order updates 2025-03-21 09:09:51 +00:00
crschnick
f5543beb71 [stage] 2025-03-20 19:42:10 +00:00
crschnick
8c51eac399 Guard against nested event loop exceptions 2025-03-20 12:15:12 +00:00
crschnick
d4a713f29b [stage] 2025-03-20 10:19:20 +00:00
crschnick
c738331acd Disable terminal logging when not supported 2025-03-20 10:18:49 +00:00
crschnick
25fc3ca0c1 Fix int field issues 2025-03-20 10:18:39 +00:00
crschnick
e1ebd24c04 Ssh identity fixes [stage] 2025-03-20 09:31:30 +00:00
crschnick
96fb6b579b Synchronize list box to prevent concurrent modifications 2025-03-19 19:47:26 +00:00
crschnick
a01756562d Improve some error messages 2025-03-19 19:46:31 +00:00
crschnick
ad0e628b60 Add missing git message for icons 2025-03-19 19:09:57 +00:00
crschnick
a7f33dd0d2 [stage] 2025-03-19 19:03:28 +00:00
crschnick
5b6dadedfe Handle LXD scan errors 2025-03-19 18:29:13 +00:00
loong
45ef2eda8c
Update translations_zh.properties (#498) 2025-03-19 19:27:29 +01:00
crschnick
b4d1a9e68b Hide download box buttons while downloading 2025-03-19 17:54:18 +00:00
crschnick
086237f965 Make install detection more robust 2025-03-19 17:53:45 +00:00
crschnick
0417ce635e Bump jsvg dependency to fix reported svg rendering issues 2025-03-19 11:57:56 +00:00
crschnick
fa02ee1bc2 Bump vnclib to fix color depth issues 2025-03-19 11:57:30 +00:00
crschnick
7e2663c6ea [release] 2025-03-18 11:13:22 +00:00
crschnick
39e6e66b16 Fix box refresh on scene change [release] 2025-03-18 10:39:27 +00:00
crschnick
7b9afaef08 List box reliability fixes [release] 2025-03-18 10:02:51 +00:00
crschnick
a130bcfa91 Ignore late list changes 2025-03-18 08:47:06 +00:00
crschnick
6084fb8d4d Revert to previous list box implementation 2025-03-18 08:46:43 +00:00
crschnick
72a5c67aab Fix double struc creation 2025-03-18 08:39:32 +00:00
crschnick
e0bdf3f52d Improve terminal order on Linux 2025-03-18 08:38:57 +00:00
crschnick
e4b01ccf0b List box memory usage improvements [stage] 2025-03-18 07:30:36 +00:00
crschnick
516cfced4a Warp terminal adjustments [stage] 2025-03-17 19:28:31 +00:00
crschnick
40e2015780 Fix right side browser being blocked 2025-03-17 19:28:21 +00:00
crschnick
fe632cd474 [stage] 2025-03-17 18:36:27 +00:00
crschnick
8ee9c25f5f GC adjustments 2025-03-17 18:34:22 +00:00
crschnick
e1d0642557 Warp linux fixes 2025-03-17 18:33:30 +00:00
crschnick
6f29cfd637 Add support for warp on Windows and Linux 2025-03-17 18:16:49 +00:00
crschnick
4f15a39280 More translation fixes 2025-03-17 15:12:35 +00:00
Ikko Eltociear Ashimine
10c8ba62e7
docs: update contact_ja.md (#493) 2025-03-17 16:08:03 +01:00
crschnick
28dd386752 [release] 2025-03-17 08:30:59 +00:00
crschnick
ca1559937b Update changelog 2025-03-17 08:15:08 +00:00
crschnick
62ef05f707 [release] 2025-03-17 08:09:54 +00:00
crschnick
e78a791c06 Fix stage theme change not triggering 2025-03-17 08:07:01 +00:00
crschnick
72f2decd75 Use more aggressive gc ratio [stage] 2025-03-17 06:50:28 +00:00
crschnick
d93eacdb9b Tweak some gc settings [stage] 2025-03-17 06:45:17 +00:00
crschnick
795a3dde7c Remove platform pause from list box 2025-03-17 06:43:44 +00:00
crschnick
4ecc55443a Fix stackoverflow for some elevation requests 2025-03-17 06:43:34 +00:00
crschnick
51c8fff9bd Fix potential NPE 2025-03-17 06:42:57 +00:00
crschnick
25e2ddd2a3 Fix various NullPointers 2025-03-16 16:17:50 +00:00
crschnick
5473474334 Fix NPE when doing undo in filter 2025-03-16 16:17:33 +00:00
crschnick
c94e270eee Fix some platform out of bounds errors 2025-03-16 14:01:06 +00:00
crschnick
8850a78a36 Update ListBoxViewComp.java 2025-03-16 13:58:02 +00:00
crschnick
bffe75a540 Add more logging for layout content loading [stage] 2025-03-16 10:13:36 +00:00
crschnick
484028f22f Fix incus/lxd launches for busybox [stage] 2025-03-16 09:15:31 +00:00
crschnick
b2ac3c1fba Only refresh frame on Windows [stage] 2025-03-16 08:39:53 +00:00
crschnick
12c414eecc Add more logging for app initialization [stage] 2025-03-16 06:59:38 +00:00
crschnick
c42b6d8439 Fix user feedback size limit 2025-03-16 06:58:10 +00:00
crschnick
05d93c68ee Fix fish shell session scripts being skipped [stage] 2025-03-15 14:13:58 +00:00
crschnick
ec3b95c11b Fix outdated security links 2025-03-15 14:13:35 +00:00
crschnick
e69da5d5b6 Don't animate transition in performance mode 2025-03-15 08:43:54 +00:00
crschnick
c265b6b87d [release] 2025-03-15 07:10:20 +00:00
crschnick
9844cadbdd Link sync docs instead of showing built-in dialog 2025-03-14 11:28:02 +00:00
crschnick
1c42605650 [stage] 2025-03-13 12:57:36 +00:00
crschnick
5b454cd8cd Fix NPE for FreeRDP 2025-03-13 12:55:25 +00:00
crschnick
d7cb5967c6 Some shell fixes 2025-03-13 12:44:29 +00:00
crschnick
a98425f100 Some shell fixes 2025-03-13 12:36:12 +00:00
crschnick
793ca373aa Properly delegate initialized call [stage] 2025-03-13 08:39:14 +00:00
crschnick
b0a7f9d17e [stage] 2025-03-13 08:33:07 +00:00
crschnick
55e7c65462 Rework connections category on macos to focus on fallback shell 2025-03-13 08:26:29 +00:00
crschnick
c57000acee Rework fallback shell 2025-03-13 08:26:04 +00:00
crschnick
ff5941249d Fix vscode remote detection on macos 2025-03-13 04:28:04 +00:00
crschnick
48450490c3 Fix observable binding being gced 2025-03-13 04:13:27 +00:00
crschnick
6cc46ed08f Support ctrl+backspace for password fields 2025-03-12 11:35:13 +00:00
crschnick
03d222e5f5 Improve os logo mapping with spaces 2025-03-11 09:05:04 +00:00
crschnick
b2efb8ddfa Add clear functionality for choice comps 2025-03-11 04:28:51 +00:00
crschnick
f5f14a441f [release] 2025-03-11 02:23:20 +00:00
crschnick
3b84e5e480 Various small fixes [stage] 2025-03-11 01:51:53 +00:00
crschnick
0af114ca8b Adjust icon size [stage] 2025-03-10 09:18:28 +00:00
crschnick
ca5a3a63c6 Various small fixes 2025-03-10 09:12:49 +00:00
crschnick
09a0323069 Update sidebar size [stage] 2025-03-10 04:30:29 +00:00
crschnick
69524fb140 Style fixes 2025-03-10 03:52:34 +00:00
crschnick
8dd23fde64 More list box visibility adjustments [stage] 2025-03-10 03:25:34 +00:00
crschnick
9ce9c31674 List box visibility fixes 2025-03-10 03:23:05 +00:00
crschnick
907b6b8033 Various fixes 2025-03-10 01:53:09 +00:00
crschnick
35eb46df84 [stage] 2025-03-09 12:07:58 +00:00
crschnick
287e0031a0 Small section fixes 2025-03-09 12:07:51 +00:00
crschnick
1c933cd34f [stage] 2025-03-09 10:52:31 +00:00
crschnick
5f0732f5ae Fix PTB update alert showing too early 2025-03-09 10:52:14 +00:00
crschnick
2643a47116 [stage] 2025-03-09 10:35:35 +00:00
crschnick
50565d28ed Fix user token check [stage] 2025-03-09 09:58:39 +00:00
crschnick
d66571b799 [stage] 2025-03-09 09:15:52 +00:00
crschnick
b77921549b Section rework 2025-03-09 09:13:55 +00:00
crschnick
39ee41e7b4 More performance adjustments 2025-03-09 05:40:30 +00:00
crschnick
ee418ed0ff Section performance fixes 2025-03-09 05:00:41 +00:00
crschnick
a898341011 Various performance fixes 2025-03-09 03:45:20 +00:00
crschnick
e4d29c441f Trim atlantafx css 2025-03-09 02:09:17 +00:00
crschnick
afaf206a42 More performance fixes 2025-03-09 01:17:19 +00:00
crschnick
572a0f0341 Various performance fixes 2025-03-09 00:56:56 +00:00
crschnick
ab5fa40828 Improved password manager test field 2025-03-09 00:24:38 +00:00
crschnick
78f6278e2b Improve store icon dialog 2025-03-09 00:18:34 +00:00
crschnick
98a7a9ae0e Style fixes 2025-03-09 00:07:56 +00:00
crschnick
04f17c6978 More svg rasterizer fixes 2025-03-08 23:48:53 +00:00
crschnick
9870a42744 Improve rasterizer output 2025-03-08 23:28:40 +00:00
crschnick
64069c7085 Improve macos frame border color 2025-03-08 23:20:22 +00:00
crschnick
95e5816e1b Style fixes 2025-03-07 15:40:00 +00:00
crschnick
da30fbd302 Make terminal launches more resistent [stage] 2025-03-06 17:32:36 +00:00
crschnick
97356b45d5 [stage] 2025-03-04 23:22:46 +00:00
crschnick
df8f3d1f8a [stage] 2025-03-04 16:23:49 +00:00
crschnick
be04a8f700 [release] 2025-03-04 11:53:22 +00:00
crschnick
2f7a2e1b19 Various fixes [stage] 2025-03-03 21:43:11 +00:00
crschnick
cefedcd0c8 Improve identity creation defaults 2025-03-03 20:44:44 +00:00
crschnick
0329a87ffd Fix new item show condition 2025-03-03 20:44:28 +00:00
crschnick
565f2f4cdd Fix provider refresh when no updates are made 2025-03-03 20:44:13 +00:00
crschnick
70c9152dd6 Fix browser modal overlay location 2025-03-03 20:44:01 +00:00
crschnick
4f30d5fd14 Fix children refresh merge not working [stage] 2025-03-03 16:08:17 +00:00
crschnick
43f4bfeb0a [stage] 2025-03-03 14:48:51 +00:00
crschnick
ee270b6cdc [stage] 2025-03-03 13:44:39 +00:00
crschnick
0bd1279399 Make max widths larger for several components 2025-03-03 13:39:03 +00:00
crschnick
a01dac767a Improve icon cache handling of blank images 2025-03-03 12:45:56 +00:00
crschnick
bbca5bafb3 SSH chmod fixes 2025-03-01 15:54:35 +00:00
crschnick
635a37c7ea Improve scale factor detection [stage] 2025-03-01 12:33:44 +00:00
crschnick
a38a8ed8a8 [stage] 2025-03-01 11:40:57 +00:00
crschnick
ee82a912fa Various small fixes 2025-03-01 11:40:51 +00:00
crschnick
6958a4979c Improve transfer stop resiliency 2025-02-28 22:01:14 +00:00
crschnick
3a90a2a67a Improve dark mode detection [stage] 2025-02-28 19:45:23 +00:00
crschnick
8cdeb54115 Theme improvements [stage] 2025-02-28 19:34:32 +00:00
crschnick
574399bf15 Improve theme logging 2025-02-28 18:40:21 +00:00
crschnick
671216a04e Fix logging statement [stage] 2025-02-28 17:32:59 +00:00
crschnick
6e541006c3 [stage] 2025-02-28 16:56:37 +00:00
crschnick
c53ac3eba5 Merge branch 'icons-constrast' 2025-02-28 16:55:30 +00:00
crschnick
34b576e9bb [stage] 2025-02-28 15:09:01 +00:00
crschnick
3df09a7eef Trace platform preferences changes [stage] 2025-02-28 14:38:24 +00:00
crschnick
8005d4b225 Fix NPE [release] 2025-02-27 08:50:25 +00:00
crschnick
c7a147883a Improve script compat description 2025-02-26 13:01:53 +00:00
crschnick
241954bdd9 Various fixes [stage] 2025-02-26 09:58:18 +00:00
crschnick
29ba973217 Small fixes [stage] 2025-02-25 07:16:10 +00:00
crschnick
3babf16a5b Small fixes [stage] 2025-02-25 05:44:50 +00:00
crschnick
d84e2fe0ce macos category display fixes 2025-02-25 04:59:33 +00:00
crschnick
996423c74e Add more macos editors 2025-02-25 04:55:50 +00:00
crschnick
09da9c59cb Add windsurf linux 2025-02-24 18:08:31 +00:00
crschnick
da6c0600e5 Add windows ai editors 2025-02-24 16:54:11 +00:00
crschnick
98c1090917 Tailscale translation fixes 2025-02-24 16:13:44 +00:00
crschnick
73a2beceac Translation fixes 2025-02-24 16:10:19 +00:00
Cesaryuan
69e959408e
Update Chinese translations for Tailscale terms (#449) 2025-02-24 17:08:27 +01:00
crschnick
04aab314da Launch feature polish [stage] 2025-02-24 14:58:21 +00:00
crschnick
d6002fa5fc Launch improvements 2025-02-24 14:25:18 +00:00
crschnick
8f653880ba Properly implement launch command 2025-02-24 13:43:49 +00:00
crschnick
cdfd7dc67d Update README.md 2025-02-24 06:51:00 +00:00
crschnick
28c039d359 Fix terminal restart secret progress being wrong 2025-02-23 16:35:02 +00:00
crschnick
af18da6ead Fix entry state not showing when first added 2025-02-23 14:16:55 +00:00
crschnick
28391e5847 Fix identity updates for empty values 2025-02-23 14:16:39 +00:00
crschnick
41ecefd090 Fix podman state display 2025-02-23 14:16:30 +00:00
crschnick
f2ea4cc4d7 Add support for cosmic-term 2025-02-23 07:58:39 +00:00
crschnick
dea1b0ff47 Recognize hetzner box 2025-02-23 07:58:22 +00:00
182 changed files with 3512 additions and 1612 deletions

View file

@ -13,14 +13,15 @@ XPipe is a new type of shell connection hub and remote file manager that allows
XPipe fully integrates with your tools such as your favourite text/code editors, terminals, shells, command-line tools and more. The platform is designed to be extensible, allowing anyone to add easily support for more tools or to implement custom functionality through a modular extension system.
It currently supports:
- [SSH](https://www.ssh.com/academy/ssh/protocol) connections, config files, and tunnels
- [Docker](https://www.docker.com/), [Podman](https://podman.io/), [LXD](https://linuxcontainers.org/lxd/introduction/), and [incus](https://linuxcontainers.org/incus/) container instances located on any host
- [Proxmox PVE](https://www.proxmox.com/en/proxmox-virtual-environment/overview) virtual machines and containers
- [Hyper-V](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/about/), [KVM/QEMU](https://linux-kvm.org/page/Main_Page), [VMware Player/Workstation/Fusion](https://www.vmware.com/products/desktop-hypervisor/workstation-and-fusion) virtual machines
- [Kubernetes](https://kubernetes.io/) clusters, pods, and containers
- [Windows Subsystem for Linux](https://ubuntu.com/wsl), [Cygwin](https://www.cygwin.com/), and [MSYS2](https://www.msys2.org/) instances
- [Powershell Remote Sessions](https://learn.microsoft.com/en-us/powershell/scripting/learn/remoting/running-remote-commands?view=powershell-7.3)
- [Tailscale SSH](https://tailscale.com/kb/1193/tailscale-ssh) and [Teleport](https://goteleport.com/) connections
- [SSH](https://docs.xpipe.io/guide/ssh) connections, config files, and tunnels
- [Docker](https://docs.xpipe.io/guide/docker), [Podman](https://docs.xpipe.io/guide/podman), [LXD](https://docs.xpipe.io/guide/lxc), and [incus](https://docs.xpipe.io/guide/lxc) containers
- [Proxmox PVE](https://docs.xpipe.io/guide/proxmox) virtual machines and containers
- [Hyper-V](https://docs.xpipe.io/guide/hyperv), [KVM](https://docs.xpipe.io/guide/kvm), [VMware Player/Workstation/Fusion](https://docs.xpipe.io/guide/vmware) virtual machines
- [Kubernetes](https://docs.xpipe.io/guide/kubernetes) clusters, pods, and containers
- [Tailscale](https://docs.xpipe.io/guide/tailscale) and [Teleport](https://docs.xpipe.io/guide/teleport) connections
- Windows Subsystem for Linux, Cygwin, and MSYS2 environments
- Powershell Remote Sessions
- RDP and VNC connections
## Connection hub
@ -76,13 +77,6 @@ It currently supports:
- There are no servers involved, all your information stays on your systems. The XPipe application does not send any personal or sensitive information to outside services.
- Vault changes can be pushed and pulled from your own remote git repository by multiple team members across many systems
## Programmatic connection control via the API
- The XPipe API provides programmatic access to XPipes features via an HTTP interface
- Manage all your remote systems and access their file systems in your own favorite programming language
- Either call the API directly or with the help of the [python library](https://github.com/xpipe-io/xpipe-python-api)
- Full documentation can be either found in the application itself under the API tab or in the [OpenAPI](/openapi.yaml) spec file
# Downloads
Note that this is a desktop application that should be run on your local desktop workstation, not on any server or containers. It will be able to connect to your server infrastructure from there.
@ -93,12 +87,6 @@ Installers are the easiest way to get started and come with an optional automati
- [Windows .msi Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-windows-x86_64.msi)
You can also install XPipe by pasting the installation command into your terminal. This will perform the .msi setup for the current user automatically:
```
powershell -ExecutionPolicy Bypass -Command iwr "https://github.com/xpipe-io/xpipe/raw/master/get-xpipe.ps1" -OutFile "$env:TEMP\get-xpipe.ps1" ";" "&" "$env:TEMP\get-xpipe.ps1"
```
If you don't like installers, you can also use a portable version that is packaged as an archive:
- [Windows .zip Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-windows-x86_64.zip)
@ -107,6 +95,20 @@ Alternatively, you can also use the following package managers:
- [choco](https://community.chocolatey.org/packages/xpipe) to install it with `choco install xpipe`.
- [winget](https://github.com/microsoft/winget-cli) to install it with `winget install xpipe-io.xpipe --source winget`.
## macOS
Installers are the easiest way to get started and come with an optional automatic update functionality:
- [MacOS .pkg Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-macos-x86_64.pkg)
- [MacOS .pkg Installer (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-macos-arm64.pkg)
If you don't like installers, you can also use a portable version that is packaged as an archive:
- [MacOS .dmg Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-macos-x86_64.dmg)
- [MacOS .dmg Portable (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-macos-arm64.dmg)
Alternatively, you can also use [Homebrew](https://github.com/xpipe-io/homebrew-tap) to install XPipe with `brew install --cask xpipe-io/tap/xpipe`.
## Linux
You can install XPipe the fastest by pasting the installation command into your terminal. This will perform the setup automatically.
@ -166,35 +168,11 @@ Alternatively, there are also AppImages available:
Note that the portable version assumes that you have some basic packages for graphical systems already installed
as it is not a perfect standalone version. It should however run on most systems.
## macOS
## Docker container
Installers are the easiest way to get started and come with an optional automatic update functionality:
XPipe is a desktop application first and foremost. It requires a full desktop environment to function with various installed applications such as terminals, editors, shells, CLI tools, and more. So there is no true web-based interface for XPipe.
- [MacOS .pkg Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-macos-x86_64.pkg)
- [MacOS .pkg Installer (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-macos-arm64.pkg)
You also can install XPipe by pasting the installation command into your terminal. This will perform the `.pkg` installation automatically:
```
bash <(curl -sL https://github.com/xpipe-io/xpipe/raw/master/get-xpipe.sh)
```
If you don't like installers, you can also use a portable version that is packaged as an archive:
- [MacOS .dmg Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-macos-x86_64.dmg)
- [MacOS .dmg Portable (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-macos-arm64.dmg)
Alternatively, you can also use [Homebrew](https://github.com/xpipe-io/homebrew-tap) to install XPipe with `brew install --cask xpipe-io/tap/xpipe`.
## Early access releases
Prior to major releases, there will be several Public Test Build (PTB) releases published at https://github.com/xpipe-io/xpipe-ptb to see whether everything is production ready and contain the latest new features.
In case you're interested in trying out the PTB versions, you can easily do so without any limitations. The regular releases and PTB releases are designed to not interfere with each other and can therefore be installed and used side by side.
## XPipe Webtop
XPipe is a desktop application first and foremost. It requires a full desktop environment to function with various installed applications such as terminals, editors, shells, CLI tools, and more. So there is no true web-based interface for XPipe. Since it might make sense however to access your XPipe environment from the web, there is also a so-called webtop docker container image for XPipe. [XPipe Webtop](https://github.com/xpipe-io/xpipe-webtop) is a web-based desktop environment that can be run in a container and accessed from a browser via KasmVNC. The desktop environment comes with XPipe and various terminals and editors preinstalled and configured.
Since it might make sense however to access your XPipe environment from the web, there is also a so-called webtop docker container image for XPipe. [XPipe Webtop](https://github.com/xpipe-io/xpipe-webtop) is a web-based desktop environment that can be run in a container and accessed from a browser via KasmVNC. The desktop environment comes with XPipe and various terminals and editors preinstalled and configured.
# Further information

View file

@ -2,6 +2,6 @@
Due to its nature, XPipe has to handle a lot of sensitive information. Therefore, the security, integrity, and privacy of your data has topmost priority.
General information about the security approach of the XPipe application can be found on the website at https://xpipe.io/features#security. If you're interested in security implementation details, you can find them at https://docs.xpipe.io/security.
More information about the security approach of the XPipe application can be found on the documentation website at https://docs.xpipe.io/reference/security.
You can report security vulnerabilities in this GitHub repository in a confidential manner. We will get back to you as soon as possible if you do.

View file

@ -48,7 +48,7 @@ dependencies {
api 'com.vladsch.flexmark:flexmark-ext-yaml-front-matter:0.64.8'
api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8'
api("com.github.weisj:jsvg:1.7.0")
api("com.github.weisj:jsvg:1.7.1")
api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar")
api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar")
api 'org.bouncycastle:bcprov-jdk18on:1.80'

View file

@ -20,7 +20,9 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
public class AppBeaconServer {
@ -34,6 +36,7 @@ public class AppBeaconServer {
private final boolean propertyPort;
private boolean running;
private ExecutorService executor;
private HttpServer server;
@Getter
@ -105,7 +108,11 @@ public class AppBeaconServer {
}
running = false;
server.stop(1);
server.stop(0);
executor.shutdown();
try {
executor.awaitTermination(30, TimeUnit.SECONDS);
} catch (InterruptedException ignored) {}
}
private void initAuthSecret() throws IOException {
@ -127,12 +134,7 @@ public class AppBeaconServer {
}
private void start() throws IOException {
server = HttpServer.create(
new InetSocketAddress(Inet4Address.getByAddress(new byte[] {0x7f, 0x00, 0x00, 0x01}), port), 10);
BeaconInterface.getAll().forEach(beaconInterface -> {
server.createContext(beaconInterface.getPath(), new BeaconRequestHandler<>(beaconInterface));
});
server.setExecutor(Executors.newFixedThreadPool(5, r -> {
executor = Executors.newFixedThreadPool(5, r -> {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setDaemon(true);
t.setName("http handler");
@ -140,7 +142,13 @@ public class AppBeaconServer {
ErrorEvent.fromThrowable(e).handle();
});
return t;
}));
});
server = HttpServer.create(
new InetSocketAddress(Inet4Address.getByAddress(new byte[] {0x7f, 0x00, 0x00, 0x01}), port), 10);
BeaconInterface.getAll().forEach(beaconInterface -> {
server.createContext(beaconInterface.getPath(), new BeaconRequestHandler<>(beaconInterface));
});
server.setExecutor(executor);
var resourceMap = Map.of(
"openapi.yaml", "misc/openapi.yaml",

View file

@ -1,6 +1,7 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStorageQuery;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.api.ConnectionQueryExchange;
@ -14,43 +15,7 @@ public class ConnectionQueryExchangeImpl extends ConnectionQueryExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) {
var catMatcher = Pattern.compile(
toRegex("all connections/" + msg.getCategoryFilter().toLowerCase()));
var conMatcher = Pattern.compile(toRegex(msg.getConnectionFilter().toLowerCase()));
var typeMatcher = Pattern.compile(toRegex(msg.getTypeFilter().toLowerCase()));
List<DataStoreEntry> found = new ArrayList<>();
for (DataStoreEntry storeEntry : DataStorage.get().getStoreEntries()) {
if (!storeEntry.getValidity().isUsable()) {
continue;
}
var name = DataStorage.get().getStorePath(storeEntry).toString();
if (!conMatcher.matcher(name).matches()) {
continue;
}
var cat = DataStorage.get()
.getStoreCategoryIfPresent(storeEntry.getCategoryUuid())
.orElse(null);
if (cat == null) {
continue;
}
var c = DataStorage.get().getStorePath(cat).toString();
if (!catMatcher.matcher(c).matches()) {
continue;
}
if (!typeMatcher
.matcher(storeEntry.getProvider().getId().toLowerCase())
.matches()) {
continue;
}
found.add(storeEntry);
}
var found = DataStorageQuery.query(msg.getCategoryFilter(), msg.getConnectionFilter(), msg.getTypeFilter());
return Response.builder()
.found(found.stream().map(entry -> entry.getUuid()).toList())
.build();
@ -60,85 +25,4 @@ public class ConnectionQueryExchangeImpl extends ConnectionQueryExchange {
public Object getSynchronizationObject() {
return DataStorage.get();
}
private String toRegex(String pattern) {
// https://stackoverflow.com/a/17369948/6477761
StringBuilder sb = new StringBuilder(pattern.length());
int inGroup = 0;
int inClass = 0;
int firstIndexInClass = -1;
char[] arr = pattern.toCharArray();
for (int i = 0; i < arr.length; i++) {
char ch = arr[i];
switch (ch) {
case '\\':
if (++i >= arr.length) {
sb.append('\\');
} else {
char next = arr[i];
switch (next) {
case ',':
// escape not needed
break;
case 'Q':
case 'E':
// extra escape needed
sb.append('\\');
default:
sb.append('\\');
}
sb.append(next);
}
break;
case '*':
if (inClass == 0) sb.append(".*");
else sb.append('*');
break;
case '?':
if (inClass == 0) sb.append('.');
else sb.append('?');
break;
case '[':
inClass++;
firstIndexInClass = i + 1;
sb.append('[');
break;
case ']':
inClass--;
sb.append(']');
break;
case '.':
case '(':
case ')':
case '+':
case '|':
case '^':
case '$':
case '@':
case '%':
if (inClass == 0 || (firstIndexInClass == i && ch == '^')) sb.append('\\');
sb.append(ch);
break;
case '!':
if (firstIndexInClass == i) sb.append('^');
else sb.append('!');
break;
case '{':
inGroup++;
sb.append('(');
break;
case '}':
inGroup--;
sb.append(')');
break;
case ',':
if (inGroup > 0) sb.append('|');
else sb.append(',');
break;
default:
sb.append(ch);
}
}
return sb.toString();
}
}

View file

@ -1,6 +1,7 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.beacon.api.DaemonFocusExchange;
import com.sun.net.httpserver.HttpExchange;
@ -9,7 +10,11 @@ public class DaemonFocusExchangeImpl extends DaemonFocusExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) {
OperationMode.switchUp(OperationMode.map(msg.getMode()));
OperationMode.switchUp(OperationMode.GUI);
var w = AppMainWindow.getInstance();
if (w != null) {
w.focus();
}
return Response.builder().build();
}

View file

@ -0,0 +1,68 @@
package io.xpipe.app.beacon.impl;
import atlantafx.base.layout.ModalBox;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStorageQuery;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.beacon.api.TerminalExternalLaunchExchange;
import java.util.List;
public class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException {
var found = DataStorageQuery.queryUserInput(msg.getConnection());
if (found.isEmpty()) {
throw new BeaconClientException("No connection found for input " + msg.getConnection());
}
if (found.size() > 1) {
throw new BeaconServerException("Multiple connections found: " + found.stream().map(DataStoreEntry::getName).toList());
}
var e = found.getFirst();
var isShell = e.getStore() instanceof ShellStore;
if (!isShell) {
throw new BeaconClientException("Connection " + DataStorage.get().getStorePath(e).toString() + " is not a shell connection");
}
if (!checkPermission()) {
return Response.builder().command(List.of()).build();
}
var r = TerminalLauncherManager.externalExchange(e.ref(), msg.getArguments());
return Response.builder().command(r).build();
}
private boolean checkPermission() {
var cache = AppCache.getBoolean("externalLaunchPermitted", false);
if (cache) {
return true;
}
var r = AppDialog.confirm("externalLaunch");
if (r) {
AppCache.update("externalLaunchPermitted", true);
}
return r;
}
@Override
public boolean requiresEnabledApi() {
return false;
}
@Override
public Object getSynchronizationObject() {
return DataStorage.get();
}
}

View file

@ -2,13 +2,14 @@ package io.xpipe.app.beacon.impl;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.beacon.api.TerminalLaunchExchange;
import com.sun.net.httpserver.HttpExchange;
public class TerminalLaunchExchangeImpl extends TerminalLaunchExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException {
var r = TerminalLauncherManager.launchExchange(msg.getRequest());
return Response.builder().targetFile(r).build();
}

View file

@ -17,11 +17,9 @@ public class TerminalPrepareExchangeImpl extends TerminalPrepareExchange {
var term = AppPrefs.get().terminalType().getValue();
var unicode = term.supportsUnicode();
var escapes = term.supportsEscapes();
var finished = TerminalLauncherManager.isCompletedSuccessfully(msg.getRequest());
return Response.builder()
.supportsUnicode(unicode)
.supportsEscapeSequences(escapes)
.alreadyFinished(finished)
.build();
}

View file

@ -24,6 +24,9 @@ public class BrowserAbstractSessionModel<T extends BrowserSessionTab> {
public void closeAsync(BrowserSessionTab e) {
ThreadHelper.runAsync(() -> {
// This is a bit ugly
// If we die on tab init, wait a bit with closing to avoid removal while it is still being inited/added
ThreadHelper.sleep(100);
closeSync(e);
});
}

View file

@ -73,6 +73,7 @@ public class BrowserFullSessionComp extends SimpleComp {
var pinnedStack = createSplitStack(rightSplit, tabs);
var loadingStack = new AnchorComp(List.of(tabs, pinnedStack, loadingIndicator));
loadingStack.apply(struc -> struc.get().setPickOnBounds(false));
var splitPane = new LeftSplitPaneComp(vertical, loadingStack)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(d -> {
@ -147,8 +148,8 @@ public class BrowserFullSessionComp extends SimpleComp {
var rec = new Rectangle();
rec.widthProperty().bind(struc.get().widthProperty());
rec.heightProperty().bind(struc.get().heightProperty());
rec.setArcHeight(7);
rec.setArcWidth(7);
rec.setArcHeight(11);
rec.setArcWidth(11);
struc.get().getChildren().getFirst().setClip(rec);
})
.vgrow();
@ -173,6 +174,7 @@ public class BrowserFullSessionComp extends SimpleComp {
private StackComp createSplitStack(SimpleDoubleProperty rightSplit, BrowserSessionTabsComp tabs) {
var cache = new HashMap<BrowserSessionTab, Region>();
var splitStack = new StackComp(List.of());
splitStack.apply(struc -> struc.get().setPickOnBounds(false));
splitStack.apply(struc -> {
model.getEffectiveRightTab().subscribe((newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {

View file

@ -156,8 +156,8 @@ public final class BrowserFileListComp extends SimpleComp {
var os = fileList.getFileSystemModel()
.getFileSystem()
.getShell()
.orElseThrow()
.getOsType();
.map(shellControl -> shellControl.getOsType())
.orElse(null);
table.widthProperty().subscribe((newValue) -> {
if (os != OsType.WINDOWS && os != OsType.MACOS) {
ownerCol.setVisible(newValue.doubleValue() > 1000);

View file

@ -53,7 +53,7 @@ public class BrowserFileSystemSavedState {
public BrowserFileSystemSavedState() {
lastDirectory = null;
recentDirectories = FXCollections.observableList(new ArrayList<>(STORED));
recentDirectories = FXCollections.synchronizedObservableList(FXCollections.observableList(new ArrayList<>(STORED)));
}
static BrowserFileSystemSavedState loadForStore(BrowserFileSystemTabModel model) {
@ -164,7 +164,7 @@ public class BrowserFileSystemSavedState {
.map(recentEntry -> new RecentEntry(FileNames.toDirectory(recentEntry.directory), recentEntry.time))
.filter(distinctBy(recentEntry -> recentEntry.getDirectory()))
.collect(Collectors.toCollection(ArrayList::new));
return new BrowserFileSystemSavedState(null, FXCollections.observableList(cleaned));
return new BrowserFileSystemSavedState(null, FXCollections.synchronizedObservableList(FXCollections.observableList(cleaned)));
}
}

View file

@ -207,7 +207,7 @@ public class BrowserFileSystemTabComp extends SimpleComp {
home,
model.getCurrentPath().isNull(),
fileList,
model.getCurrentPath().isNull().not()));
model.getCurrentPath().isNull().not()), false);
var r = stack.styleClass("browser-content-container").createRegion();
r.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {

View file

@ -1,6 +1,7 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.*;
import javafx.beans.property.BooleanProperty;
@ -11,7 +12,10 @@ import java.nio.file.Path;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Timer;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.regex.Pattern;
@ -413,23 +417,60 @@ public class BrowserFileTransferOperation {
// Initialize progress immediately prior to reading anything
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.getSize());
byte[] buffer = new byte[bs];
int read;
while ((read = inputStream.read(buffer, 0, bs)) > 0) {
if (cancelled()) {
var killStreams = new AtomicBoolean(false);
var exception = new AtomicReference<Exception>();
var thread = ThreadHelper.createPlatformThread("transfer", true, () -> {
try {
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.getSize());
byte[] buffer = new byte[bs];
int read;
while ((read = inputStream.read(buffer, 0, bs)) > 0) {
if (cancelled()) {
killStreams.set(true);
break;
}
if (!checkTransferValidity()) {
killStreams.set(true);
break;
}
outputStream.write(buffer, 0, read);
transferred.addAndGet(read);
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
}
} catch (Exception ex) {
exception.set(ex);
}
});
thread.start();
while (true) {
var alive = thread.isAlive();
var cancelled = cancelled();
if (cancelled) {
// Assume that the transfer has stalled if it doesn't finish until then
thread.join(1000);
killStreams();
break;
}
if (!checkTransferValidity()) {
killStreams();
break;
if (alive) {
Thread.sleep(100);
continue;
}
outputStream.write(buffer, 0, read);
transferred.addAndGet(read);
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
if (killStreams.get()) {
killStreams();
}
var ex = exception.get();
if (ex != null) {
throw ex;
} else {
break;
}
}
}

View file

@ -26,7 +26,7 @@ public class BrowserHistorySavedStateImpl implements BrowserHistorySavedState {
ObservableList<Entry> lastSystems;
public BrowserHistorySavedStateImpl(List<Entry> lastSystems) {
this.lastSystems = FXCollections.observableArrayList(lastSystems);
this.lastSystems = FXCollections.synchronizedObservableList(FXCollections.observableArrayList(lastSystems));
}
private static BrowserHistorySavedStateImpl INSTANCE;

View file

@ -60,7 +60,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
map.put(emptyDisplay, empty);
map.put(contentDisplay, empty.not());
var stack = new MultiContentComp(map);
var stack = new MultiContentComp(map, false);
return stack.createRegion();
}
@ -165,7 +165,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
.accessibleText(e.getPath())
.disable(disable)
.styleClass("directory-button")
.apply(struc -> struc.get().setMaxWidth(2000))
.apply(struc -> struc.get().setMaxWidth(20000))
.styleClass(Styles.RIGHT_PILL)
.hgrow()
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));

View file

@ -38,7 +38,7 @@ public class BrowserOverviewComp extends SimpleComp {
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
var commonPlatform = FXCollections.<FileEntry>observableArrayList();
var commonPlatform = FXCollections.<FileEntry>synchronizedObservableList(FXCollections.observableArrayList());
ThreadHelper.runFailableAsync(() -> {
var common = sc.getOsType().determineInterestingPaths(sc).stream()
.filter(s -> !s.isBlank())

View file

@ -4,8 +4,11 @@ import io.xpipe.app.browser.BrowserAbstractSessionModel;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.BrowserSessionTab;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.AppMainWindowContentComp;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.terminal.TerminalDockComp;
@ -18,6 +21,7 @@ import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.Optional;
@ -134,6 +138,13 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
dockModel.toggleView(aBoolean);
});
});
AppDialog.getModalOverlay().addListener((ListChangeListener<? super ModalOverlay>) c -> {
if (c.getList().size() > 0) {
dockModel.toggleView(false);
} else {
dockModel.toggleView(viewActive.get());
}
});
}
private void refreshShowingState() {

View file

@ -65,13 +65,14 @@ public class BrowserTransferComp extends SimpleComp {
return Bindings.createStringBinding(
() -> {
var p = sourceItem.get().getProgress().getValue();
var progressSuffix = p == null
|| sourceItem
.get()
.downloadFinished()
.get()
var hideProgress = sourceItem
.get()
.downloadFinished()
.get();
var share = p != null ? (p.getTransferred() * 100 / p.getTotal()) : 0;
var progressSuffix = hideProgress
? ""
: " " + (p.getTransferred() * 100 / p.getTotal()) + "%";
: " " + share + "%";
return entry.getFileName() + progressSuffix;
},
sourceItem.get().getProgress());
@ -81,14 +82,14 @@ public class BrowserTransferComp extends SimpleComp {
var dragNotice = new LabelComp(AppI18n.observable("dragLocalFiles"))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left")))
.apply(struc -> struc.get().setWrapText(true))
.hide(model.getEmpty());
.hide(Bindings.or(model.getEmpty(), model.getTransferring()));
var clearButton = new IconButtonComp("mdi2c-close", () -> {
ThreadHelper.runAsync(() -> {
model.clear(true);
});
})
.hide(model.getEmpty())
.hide(Bindings.or(model.getEmpty(), model.getTransferring()))
.tooltipKey("clearTransferDescription");
var downloadButton = new IconButtonComp("mdi2f-folder-move-outline", () -> {
@ -96,7 +97,7 @@ public class BrowserTransferComp extends SimpleComp {
model.transferToDownloads();
});
})
.hide(model.getEmpty())
.hide(Bindings.or(model.getEmpty(), model.getTransferring()))
.tooltipKey("downloadStageDescription");
var bottom = new HorizontalComp(

View file

@ -8,7 +8,9 @@ import io.xpipe.app.util.ShellTemp;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.FXCollections;
@ -34,6 +36,7 @@ public class BrowserTransferModel {
BrowserFullSessionModel browserSessionModel;
ObservableList<Item> items = FXCollections.observableArrayList();
ObservableBooleanValue empty = Bindings.createBooleanBinding(() -> items.isEmpty(), items);
BooleanProperty transferring = new SimpleBooleanProperty();
public BrowserTransferModel(BrowserFullSessionModel browserSessionModel) {
this.browserSessionModel = browserSessionModel;
@ -47,8 +50,9 @@ public class BrowserTransferModel {
}
if (toDownload.isPresent()) {
downloadSingle(toDownload.get());
} else {
ThreadHelper.sleep(20);
}
ThreadHelper.sleep(20);
}
});
thread.start();
@ -126,6 +130,7 @@ public class BrowserTransferModel {
}
try {
transferring.setValue(true);
var op = new BrowserFileTransferOperation(
BrowserLocalFileSystem.getLocalFileEntry(TEMP),
List.of(item.getBrowserEntry().getRawFileEntry()),
@ -150,6 +155,8 @@ public class BrowserTransferModel {
synchronized (items) {
items.remove(item);
}
} finally {
transferring.setValue(false);
}
}

View file

@ -154,7 +154,15 @@ public abstract class Comp<S extends CompStructure<?>> {
}
public Comp<S> disable(ObservableValue<Boolean> o) {
return apply(struc -> struc.get().disableProperty().bind(o));
return apply(struc -> {
var region = struc.get();
BindingsHelper.preserve(region, o);
o.subscribe(n -> {
PlatformThread.runLaterIfNeeded(() -> {
region.setDisable(n);
});
});
});
}
public Comp<S> padding(Insets insets) {

View file

@ -22,7 +22,6 @@ public class AnchorComp extends Comp<CompStructure<AnchorPane>> {
for (var c : comps) {
pane.getChildren().add(c.createRegion());
}
pane.setPickOnBounds(false);
return new SimpleCompStructure<>(pane);
}
}

View file

@ -39,7 +39,7 @@ public class AppLayoutComp extends Comp<AppLayoutComp.Structure> {
return model.getSelected().getValue().equals(entry);
},
model.getSelected())));
var multi = new MultiContentComp(map);
var multi = new MultiContentComp(map, true);
multi.styleClass("background");
var pane = new BorderPane();

View file

@ -6,6 +6,7 @@ import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.resources.AppImages;
import io.xpipe.app.resources.AppResources;
import io.xpipe.app.util.PlatformThread;
@ -82,6 +83,7 @@ public class AppMainWindowContentComp extends SimpleComp {
loaded.subscribe(struc -> {
if (struc != null) {
TrackEvent.info("Window content node set");
PlatformThread.runNestedLoopIteration();
anim.stop();
struc.prepareAddition();
@ -90,6 +92,7 @@ public class AppMainWindowContentComp extends SimpleComp {
pane.getStyleClass().remove("background");
pane.getChildren().remove(vbox);
struc.show();
TrackEvent.info("Window content node shown");
}
});
@ -107,14 +110,6 @@ public class AppMainWindowContentComp extends SimpleComp {
}
});
loaded.addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
Platform.runLater(() -> {
stage.requestFocus();
});
}
});
return pane;
});
var modal = new ModalOverlayStackComp(bg, overlay);

View file

@ -42,7 +42,7 @@ public class ComboTextFieldComp extends Comp<CompStructure<ComboBox<String>>> {
});
});
text.setEditable(true);
text.setMaxWidth(2000);
text.setMaxWidth(20000);
text.setValue(value.getValue() != null ? value.getValue() : null);
text.valueProperty().addListener((c, o, n) -> {
value.setValue(n != null && n.length() > 0 ? n : null);

View file

@ -41,7 +41,7 @@ public class FilterComp extends Comp<CompStructure<CustomTextField>> {
});
var filter = new CustomTextField();
filter.setMinHeight(0);
filter.setMaxHeight(2000);
filter.setMaxHeight(20000);
filter.getStyleClass().add("filter-comp");
filter.promptTextProperty().bind(AppI18n.observable("searchFilter"));
filter.rightProperty()
@ -67,7 +67,7 @@ public class FilterComp extends Comp<CompStructure<CustomTextField>> {
filterText.subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> {
clear.setVisible(val != null);
if (!Objects.equals(filter.getText(), val)) {
if (!Objects.equals(filter.getText(), val) && !(val == null && "".equals(filter.getText()))) {
filter.setText(val);
}
});

View file

@ -1,29 +0,0 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import java.util.List;
public class GrowPaneComp extends Comp<CompStructure<Pane>> {
private final List<Comp<?>> comps;
public GrowPaneComp(List<Comp<?>> comps) {
this.comps = List.copyOf(comps);
}
@Override
public CompStructure<Pane> createBase() {
var pane = new BorderPane();
for (var c : comps) {
pane.setCenter(c.createRegion());
}
pane.setPickOnBounds(false);
return new SimpleCompStructure<>(pane);
}
}

View file

@ -37,7 +37,7 @@ public class IntComboFieldComp extends Comp<CompStructure<ComboBox<String>>> {
text.setValue(value.getValue() != null ? value.getValue().toString() : null);
text.setItems(FXCollections.observableList(
predefined.stream().map(integer -> "" + integer).toList()));
text.setMaxWidth(2000);
text.setMaxWidth(20000);
text.getStyleClass().add("int-combo-field-comp");
text.setSkin(new ComboBoxListViewSkin<>(text));
text.setVisibleRowCount(Math.min(10, predefined.size()));

View file

@ -5,6 +5,7 @@ import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.scene.control.TextField;
@ -13,6 +14,8 @@ import javafx.scene.input.KeyEvent;
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
import java.util.Objects;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class IntFieldComp extends Comp<CompStructure<TextField>> {
@ -34,29 +37,38 @@ public class IntFieldComp extends Comp<CompStructure<TextField>> {
@Override
public CompStructure<TextField> createBase() {
var text = new TextField(value.getValue() != null ? value.getValue().toString() : null);
var field = new TextField(value.getValue() != null ? value.getValue().toString() : null);
value.addListener((ChangeListener<Number>) (observableValue, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
if (newValue == null) {
text.setText("");
} else {
if (newValue.intValue() < minValue) {
value.setValue(minValue);
return;
}
if (newValue.intValue() > maxValue) {
value.setValue(maxValue);
return;
}
text.setText(newValue.toString());
// Check if control value is the same. Then don't set it as that might cause bugs
if ((newValue == null && field.getText().isEmpty())
|| Objects.equals(field.getText(), newValue != null ? newValue.toString() : null)) {
return;
}
if (newValue == null) {
Platform.runLater(() -> {
field.setText(null);
});
return;
}
if (newValue.intValue() < minValue) {
value.setValue(minValue);
return;
}
if (newValue.intValue() > maxValue) {
value.setValue(maxValue);
return;
}
field.setText(newValue.toString());
});
});
text.addEventFilter(KeyEvent.KEY_TYPED, keyEvent -> {
field.addEventFilter(KeyEvent.KEY_TYPED, keyEvent -> {
if (minValue < 0) {
if (!"-0123456789".contains(keyEvent.getCharacter())) {
keyEvent.consume();
@ -68,7 +80,7 @@ public class IntFieldComp extends Comp<CompStructure<TextField>> {
}
});
text.textProperty().addListener((observableValue, oldValue, newValue) -> {
field.textProperty().addListener((observableValue, oldValue, newValue) -> {
if (newValue == null
|| newValue.isEmpty()
|| (minValue < 0 && "-".equals(newValue))
@ -79,12 +91,12 @@ public class IntFieldComp extends Comp<CompStructure<TextField>> {
int intValue = Integer.parseInt(newValue);
if (minValue > intValue || intValue > maxValue) {
text.textProperty().setValue(oldValue);
field.textProperty().setValue(oldValue);
}
value.setValue(Integer.parseInt(text.textProperty().get()));
value.setValue(Integer.parseInt(field.textProperty().get()));
});
return new SimpleCompStructure<>(text);
return new SimpleCompStructure<>(field);
}
}

View file

@ -4,12 +4,17 @@ import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.PlatformThread;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
@ -22,6 +27,7 @@ import javafx.scene.layout.VBox;
import lombok.Setter;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
@ -37,7 +43,7 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
private final boolean scrollBar;
@Setter
private int platformPauseInterval = -1;
private boolean visibilityControl = false;
public ListBoxViewComp(
ObservableList<T> shown, ObservableList<T> all, Function<T, Comp<?>> compFunction, boolean scrollBar) {
@ -56,16 +62,24 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
vbox.setFocusTraversable(false);
var scroll = new ScrollPane(vbox);
refresh(scroll, vbox, shown, all, cache, false, false);
refresh(scroll, vbox, shown, all, cache, false);
shown.addListener((ListChangeListener<? super T>) (c) -> {
refresh(scroll, vbox, c.getList(), all, cache, true, true);
var hadScene = new AtomicBoolean(false);
scroll.sceneProperty().subscribe(scene -> {
if (scene != null) {
hadScene.set(true);
refresh(scroll, vbox, shown, all, cache, true);
}
});
all.addListener((ListChangeListener<? super T>) c -> {
synchronized (cache) {
cache.keySet().retainAll(c.getList());
}
shown.addListener((ListChangeListener<? super T>) (c) -> {
Platform.runLater(() -> {
if (scroll.getScene() == null && hadScene.get()) {
return;
}
refresh(scroll, vbox, c.getList(), all, cache, true);
});
});
if (scrollBar) {
@ -92,50 +106,84 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
scroll.setFitToWidth(true);
scroll.getStyleClass().add("list-box-view-comp");
registerVisibilityListeners(scroll, vbox);
return new SimpleCompStructure<>(scroll);
}
private void registerVisibilityListeners(ScrollPane scroll, VBox vbox) {
if (!visibilityControl) {
return;
}
var dirty = new SimpleBooleanProperty();
var animationTimer = new AnimationTimer() {
@Override
public void handle(long now) {
if (!dirty.get()) {
return;
}
updateVisibilities(scroll, vbox);
dirty.set(false);
}
};
scroll.vvalueProperty().addListener((observable, oldValue, newValue) -> {
updateVisibilities(scroll, vbox);
dirty.set(true);
});
scroll.heightProperty().addListener((observable, oldValue, newValue) -> {
updateVisibilities(scroll, vbox);
dirty.set(true);
});
vbox.heightProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
updateVisibilities(scroll, vbox);
});
dirty.set(true);
});
// We can't directly listen to any parent element changing visibility, so this is a compromise
if (AppLayoutModel.get() != null) {
AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
updateVisibilities(scroll, vbox);
});
dirty.set(true);
});
}
BrowserFullSessionModel.DEFAULT.getSelectedEntry().addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
updateVisibilities(scroll, vbox);
});
dirty.set(true);
});
if (StoreViewState.get() != null) {
StoreViewState.get().getSortMode().addListener((observable, oldValue, newValue) -> {
// This is very ugly, but it just takes multiple iterations for the order to apply
Platform.runLater(() -> {
Platform.runLater(() -> {
Platform.runLater(() -> {
dirty.set(true);
});
});
});
});
}
vbox.sceneProperty().addListener((observable, oldValue, newValue) -> {
Node c = vbox;
while ((c = c.getParent()) != null) {
c.boundsInParentProperty().addListener((observable1, oldValue1, newValue1) -> {
updateVisibilities(scroll, vbox);
});
dirty.set(true);
if (newValue != null) {
animationTimer.start();
} else {
animationTimer.stop();
}
Platform.runLater(() -> {
updateVisibilities(scroll, vbox);
});
Node c = vbox;
do {
c.boundsInParentProperty().addListener((change, oldBounds,newBounds) -> {
dirty.set(true);
});
// Don't listen to root node changes, that seemingly can cause exceptions
} while ((c = c.getParent()) != null && c.getParent() != null);
if (newValue != null) {
newValue.heightProperty().addListener((observable1, oldValue1, newValue1) -> {
updateVisibilities(scroll, vbox);
dirty.set(true);
});
}
});
return new SimpleCompStructure<>(scroll);
}
private boolean isVisible(ScrollPane pane, VBox box, Node node) {
@ -178,9 +226,20 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
}
private void updateVisibilities(ScrollPane scroll, VBox vbox) {
if (!visibilityControl) {
return;
}
int count = 0;
for (Node child : vbox.getChildren()) {
var v = isVisible(scroll, vbox, child);
child.setVisible(v);
if (v) {
count++;
}
}
if (count > 10) {
// System.out.println("Visible: " + count);
}
}
@ -190,44 +249,42 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
List<? extends T> shown,
List<? extends T> all,
Map<T, Region> cache,
boolean asynchronous,
boolean refreshVisibilities) {
Runnable update = () -> {
synchronized (cache) {
var set = new HashSet<T>();
// These lists might diverge on updates
set.addAll(shown);
set.addAll(all);
// These lists might diverge on updates, so add both
synchronized (shown) {
set.addAll(shown);
}
synchronized (all) {
set.addAll(all);
}
// Clear cache of unused values
cache.keySet().removeIf(t -> !set.contains(t));
}
final long[] lastPause = {System.currentTimeMillis()};
// Create copy to reduce chances of concurrent modification
var shownCopy = new ArrayList<>(shown);
var newShown = shownCopy.stream()
.map(v -> {
var elapsed = System.currentTimeMillis() - lastPause[0];
if (platformPauseInterval != -1 && elapsed > platformPauseInterval) {
PlatformThread.runNestedLoopIteration();
lastPause[0] = System.currentTimeMillis();
// Use copy to prevent concurrent modifications and to not synchronize to long
List<T> shownCopy;
synchronized (shown) {
shownCopy = new ArrayList<>(shown);
}
List<Region> newShown = shownCopy.stream().map(v -> {
if (!cache.containsKey(v)) {
var comp = compFunction.apply(v);
if (comp != null) {
var r = comp.createRegion();
if (visibilityControl) {
r.setVisible(false);
}
cache.put(v, r);
} else {
cache.put(v, null);
}
}
if (!cache.containsKey(v)) {
var comp = compFunction.apply(v);
if (comp != null) {
var r = comp.createRegion();
r.setVisible(false);
cache.put(v, r);
} else {
cache.put(v, null);
}
}
return cache.get(v);
})
.filter(region -> region != null)
.toList();
return cache.get(v);
}).filter(region -> region != null).toList();
if (listView.getChildren().equals(newShown)) {
return;
@ -247,11 +304,6 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
updateVisibilities(scroll, listView);
}
};
if (asynchronous) {
Platform.runLater(update);
} else {
PlatformThread.runLaterIfNeeded(update);
}
update.run();
}
}

View file

@ -2,6 +2,7 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.value.ObservableValue;
@ -15,9 +16,11 @@ import java.util.Map;
public class MultiContentComp extends SimpleComp {
private final boolean log;
private final Map<Comp<?>, ObservableValue<Boolean>> content;
public MultiContentComp(Map<Comp<?>, ObservableValue<Boolean>> content) {
public MultiContentComp(Map<Comp<?>, ObservableValue<Boolean>> content, boolean log) {
this.log = log;
this.content = FXCollections.observableMap(content);
}
@ -34,7 +37,14 @@ public class MultiContentComp extends SimpleComp {
});
for (Map.Entry<Comp<?>, ObservableValue<Boolean>> e : content.entrySet()) {
var name = e.getKey().getClass().getSimpleName();
if (log) {
TrackEvent.trace("Creating content tab region for element " + name);
}
var r = e.getKey().createRegion();
if (log) {
TrackEvent.trace("Created content tab region for element " + name);
}
e.getValue().subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> {
r.setManaged(val);
@ -42,6 +52,9 @@ public class MultiContentComp extends SimpleComp {
});
});
m.put(e.getKey(), r);
if (log) {
TrackEvent.trace("Added content tab region for element " + name);
}
}
return stack;

View file

@ -10,6 +10,8 @@ import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
@ -68,23 +70,32 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
@Override
public Structure createBase() {
var text = new PasswordField();
text.setText(value.getValue() != null ? value.getValue().getSecretValue() : null);
text.textProperty().addListener((c, o, n) -> {
var field = new PasswordField();
field.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
if (e.isControlDown() && e.getCode() == KeyCode.BACK_SPACE) {
var sel = field.getSelection();
if (sel.getEnd() > 0) {
field.setText(field.getText().substring(sel.getEnd()));
e.consume();
}
}
});
field.setText(value.getValue() != null ? value.getValue().getSecretValue() : null);
field.textProperty().addListener((c, o, n) -> {
value.setValue(n != null && n.length() > 0 ? encrypt(n.toCharArray()) : null);
});
value.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> {
// Check if control value is the same. Then don't set it as that might cause bugs
if ((n == null && text.getText().isEmpty())
|| Objects.equals(text.getText(), n != null ? n.getSecretValue() : null)) {
if ((n == null && field.getText().isEmpty())
|| Objects.equals(field.getText(), n != null ? n.getSecretValue() : null)) {
return;
}
text.setText(n != null ? n.getSecretValue() : null);
field.setText(n != null ? n.getSecretValue() : null);
});
});
HBox.setHgrow(text, Priority.ALWAYS);
HBox.setHgrow(field, Priority.ALWAYS);
var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> {
ClipboardHelper.copyPassword(value.getValue());
@ -93,7 +104,7 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
.tooltipKey("copyPassword")
.createRegion();
var ig = new InputGroup(text);
var ig = new InputGroup(field);
ig.setFillHeight(true);
ig.getStyleClass().add("secret-field-comp");
if (allowCopy) {
@ -103,10 +114,10 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
ig.focusedProperty().addListener((c, o, n) -> {
if (n) {
text.requestFocus();
field.requestFocus();
}
});
return new Structure(ig, text);
return new Structure(ig, field);
}
}

View file

@ -76,7 +76,9 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
var shortcut = e.combination();
b.apply(new TooltipAugment<>(e.name(), shortcut));
b.apply(struc -> {
AppFontSizes.xl(struc.get());
AppFontSizes.lg(struc.get());
struc.get().setAlignment(Pos.CENTER);
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
value.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> {
@ -123,7 +125,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
.tooltipKey("updateAvailableTooltip")
.accessibleTextKey("updateAvailableTooltip");
b.apply(struc -> {
AppFontSizes.xl(struc.get());
AppFontSizes.lg(struc.get());
});
b.hide(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {

View file

@ -24,7 +24,6 @@ public class StackComp extends Comp<CompStructure<StackPane>> {
pane.getChildren().add(c.createRegion());
}
pane.setAlignment(Pos.CENTER);
pane.setPickOnBounds(false);
return new SimpleCompStructure<>(pane);
}
}

View file

@ -4,6 +4,7 @@ import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.process.OsType;
import javafx.beans.binding.Bindings;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
@ -57,6 +58,11 @@ public class DenseStoreEntryComp extends StoreEntryComp {
return false;
}
@Override
public int getHeight() {
return OsType.getLocal() == OsType.WINDOWS ? 38 : 37;
}
protected Region createContent() {
var grid = new GridPane();
grid.setHgap(8);

View file

@ -82,7 +82,8 @@ public class OsLogoComp extends SimpleComp {
}
return ICONS.entrySet().stream()
.filter(e -> name.toLowerCase().contains(e.getKey()))
.filter(e -> name.toLowerCase().contains(e.getKey()) ||
name.toLowerCase().replaceAll("\\s+", "").contains(e.getKey()))
.findAny()
.map(e -> e.getValue())
.orElse("os/linux.svg");

View file

@ -21,6 +21,11 @@ public class StandardStoreEntryComp extends StoreEntryComp {
return true;
}
@Override
public int getHeight() {
return 57;
}
private Label createSummary() {
var summary = new Label();
summary.textProperty().bind(getWrapper().getShownSummary());

View file

@ -80,8 +80,10 @@ public class StoreCategoryComp extends SimpleComp {
.apply(struc -> {
struc.get().setAlignment(Pos.CENTER);
struc.get().setFocusTraversable(false);
if (OsType.getLocal() != OsType.LINUX) {
HBox.setMargin(struc.get(), new Insets(0, 0, 2.6, 0));
if (OsType.getLocal() == OsType.WINDOWS) {
HBox.setMargin(struc.get(), new Insets(0, 0, 2.3, 0));
} else if (OsType.getLocal() == OsType.MACOS) {
HBox.setMargin(struc.get(), new Insets(0, 0, 1.8, 0));
}
})
.disable(Bindings.isEmpty(category.getChildren().getList()))
@ -168,6 +170,7 @@ public class StoreCategoryComp extends SimpleComp {
new ListBoxViewComp<>(l, l, storeCategoryWrapper -> new StoreCategoryComp(storeCategoryWrapper), false);
children.styleClass("children");
children.minHeight(0);
children.setVisibilityControl(true);
var hide = Bindings.createBooleanBinding(
() -> {

View file

@ -24,6 +24,7 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.MenuButton;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@ -199,7 +200,7 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
selected),
() -> {});
button.apply(struc -> {
struc.get().setMaxWidth(2000);
struc.get().setMaxWidth(20000);
struc.get().setAlignment(Pos.CENTER_LEFT);
Comp<?> graphic = PrettyImageHelper.ofFixedSize(
Bindings.createStringBinding(
@ -224,6 +225,14 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
}
event.consume();
});
struc.get().setOnMouseClicked(event -> {
if (event.getButton() != MouseButton.SECONDARY) {
return;
}
selected.setValue(mode == Mode.PROXY ? DataStorage.get().local().ref() : null);
event.consume();
});
})
.styleClass("choice-comp");
@ -241,7 +250,7 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
StackPane.setMargin(icon, new Insets(10));
pane.setPickOnBounds(false);
StackPane.setAlignment(icon, Pos.CENTER_RIGHT);
pane.setMaxWidth(2000);
pane.setMaxWidth(20000);
r.prefWidthProperty().bind(pane.widthProperty());
r.maxWidthProperty().bind(pane.widthProperty());
return pane;

View file

@ -183,6 +183,10 @@ public class StoreCreationComp extends DialogComp {
}
public static void showEdit(DataStoreEntry e) {
showEdit(e, dataStoreEntry -> {});
}
public static void showEdit(DataStoreEntry e, Consumer<DataStoreEntry> consumer) {
show(
e.getName(),
e.getProvider(),
@ -204,6 +208,7 @@ public class StoreCreationComp extends DialogComp {
}
}
}
consumer.accept(e);
});
},
true,

View file

@ -68,10 +68,10 @@ public abstract class StoreEntryComp extends SimpleComp {
}
}
public static StoreEntryComp customSection(StoreSection e, boolean topLevel) {
public static StoreEntryComp customSection(StoreSection e) {
var prov = e.getWrapper().getEntry().getProvider();
if (prov != null) {
return prov.customEntryComp(e, topLevel);
return prov.customEntryComp(e, e.getDepth() == 1);
} else {
var forceCondensed = AppPrefs.get() != null
&& AppPrefs.get().condenseConnectionDisplay().get();
@ -81,6 +81,8 @@ public abstract class StoreEntryComp extends SimpleComp {
public abstract boolean isFullSize();
public abstract int getHeight();
@Override
protected final Region createSimple() {
var r = createContent();
@ -357,9 +359,7 @@ public abstract class StoreEntryComp extends SimpleComp {
getWrapper().moveTo(storeCategoryWrapper.getCategory());
event.consume();
});
if (storeCategoryWrapper.getParent() == null
|| storeCategoryWrapper.equals(
getWrapper().getCategory().getValue())) {
if (storeCategoryWrapper.getParent() == null) {
m.setDisable(true);
}

View file

@ -27,11 +27,11 @@ public class StoreEntryListComp extends SimpleComp {
.getAllChildren()
.getList(),
(StoreSection e) -> {
var custom = StoreSection.customSection(e, true).hgrow();
var custom = StoreSection.customSection(e).hgrow();
return custom;
},
true);
content.setPlatformPauseInterval(50);
content.setVisibilityControl(true);
content.apply(struc -> {
// Reset scroll
StoreViewState.get().getActiveCategory().addListener((observable, oldValue, newValue) -> {
@ -142,6 +142,6 @@ public class StoreEntryListComp extends SimpleComp {
map.put(new StoreScriptsIntroComp(scriptsIntroShowing), showScriptsIntro);
map.put(new StoreIdentitiesIntroComp(), showIdentitiesIntro);
return new MultiContentComp(map).createRegion();
return new MultiContentComp(map, false).createRegion();
}
}

View file

@ -32,23 +32,6 @@ import java.util.function.Function;
public class StoreEntryListOverviewComp extends SimpleComp {
private final Property<StoreSortMode> sortMode;
public StoreEntryListOverviewComp() {
this.sortMode = new SimpleObjectProperty<>();
StoreViewState.get().getActiveCategory().subscribe(val -> {
sortMode.setValue(val.getSortMode().getValue());
});
sortMode.addListener((observable, oldValue, newValue) -> {
var cat = StoreViewState.get().getActiveCategory().getValue();
if (cat == null) {
return;
}
cat.getSortMode().setValue(newValue);
});
}
private Region createGroupListHeader() {
var label = new Label();
var name = BindingsHelper.flatMap(
@ -142,6 +125,7 @@ public class StoreEntryListOverviewComp extends SimpleComp {
}
private Comp<?> createAlphabeticalSortButton() {
var sortMode = StoreViewState.get().getSortMode();
var icon = Bindings.createObjectBinding(
() -> {
if (sortMode.getValue() == StoreSortMode.ALPHABETICAL_ASC) {
@ -182,6 +166,7 @@ public class StoreEntryListOverviewComp extends SimpleComp {
}
private Comp<?> createDateSortButton() {
var sortMode = StoreViewState.get().getSortMode();
var icon = Bindings.createObjectBinding(
() -> {
if (sortMode.getValue() == StoreSortMode.DATE_ASC) {

View file

@ -32,7 +32,6 @@ public class StoreEntryWrapper {
private final Property<String> name;
private final DataStoreEntry entry;
private final Property<Instant> lastAccess;
private final Property<Instant> lastAccessApplied = new SimpleObjectProperty<>();
private final BooleanProperty disabled = new SimpleBooleanProperty();
private final BooleanProperty busy = new SimpleBooleanProperty();
private final Property<DataStoreEntry.Validity> validity = new SimpleObjectProperty<>();
@ -104,10 +103,6 @@ public class StoreEntryWrapper {
setupListeners();
}
public void applyLastAccess() {
this.lastAccessApplied.setValue(lastAccess.getValue());
}
public void moveTo(DataStoreCategory category) {
ThreadHelper.runAsync(() -> {
DataStorage.get().moveEntryToCategory(entry, category);
@ -130,8 +125,7 @@ public class StoreEntryWrapper {
public void delete() {
ThreadHelper.runAsync(() -> {
DataStorage.get().deleteChildren(this.entry);
DataStorage.get().deleteStoreEntry(this.entry);
DataStorage.get().deleteWithChildren(this.entry);
});
}

View file

@ -6,10 +6,12 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.icon.SystemIcon;
import io.xpipe.app.icon.SystemIconCache;
import io.xpipe.app.icon.SystemIconManager;
import io.xpipe.app.resources.AppImages;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.control.*;
@ -106,19 +108,29 @@ public class StoreIconChoiceComp extends SimpleComp {
}
private void updateData(TableView<List<SystemIcon>> table, String filterString) {
var displayedIcons = filterString == null || filterString.isBlank() || filterString.length() < 2
? icons.stream()
.sorted(Comparator.comparing(systemIcon -> systemIcon.getId()))
.toList()
: icons.stream()
.filter(icon -> containsString(icon.getId(), filterString))
.toList();
var data = partitionList(displayedIcons, columns);
var available = icons.stream()
.filter(systemIcon -> AppImages.hasNormalImage("icons/" + systemIcon.getSource().getId() + "/" + systemIcon.getId() + "-40.png"))
.sorted(Comparator.comparing(systemIcon -> systemIcon.getId()))
.toList();
table.getPlaceholder().setVisible(available.size() == 0);
var filtered = available;
if (filterString != null && !filterString.isBlank() && filterString.length() >= 2) {
filtered = available.stream().filter(icon -> containsString(icon.getId(), filterString)).toList();
}
var data = partitionList(filtered, columns);
table.getItems().setAll(data);
var selectMatch = filtered.size() == 1 || filtered.stream().anyMatch(systemIcon -> systemIcon.getId().equals(filterString));
// Table updates seem to not always be instant, sometimes the column is not there yet
if (selectMatch && table.getColumns().size() > 0) {
table.getSelectionModel().select(0, table.getColumns().getFirst());
selected.setValue(filtered.getFirst());
} else {
selected.setValue(null);
}
}
private <T> Collection<List<T>> partitionList(List<T> list, int size) {
private <T> List<List<T>> partitionList(List<T> list, int size) {
List<List<T>> partitions = new ArrayList<>();
if (list.size() == 0) {
return partitions;

View file

@ -53,12 +53,12 @@ public class StoreSection {
}
}
public static Comp<?> customSection(StoreSection e, boolean topLevel) {
public static Comp<?> customSection(StoreSection e) {
var prov = e.getWrapper().getEntry().getProvider();
if (prov != null) {
return prov.customSectionComp(e, topLevel);
return prov.customSectionComp(e);
} else {
return new StoreSectionComp(e, topLevel);
return new StoreSectionComp(e);
}
}
@ -96,7 +96,7 @@ public class StoreSection {
var current = mappedSortMode.getValue();
if (current != null) {
return current.comparator().compare(current.representative(o1), current.representative(o2));
return current.comparator().compare(o1, o2);
} else {
return 0;
}

View file

@ -0,0 +1,165 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
public abstract class StoreSectionBaseComp extends Comp<CompStructure<VBox>> {
private static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
private static final PseudoClass ROOT = PseudoClass.getPseudoClass("root");
private static final PseudoClass TOP = PseudoClass.getPseudoClass("top");
private static final PseudoClass SUB = PseudoClass.getPseudoClass("sub");
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth");
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth");
protected final StoreSection section;
public StoreSectionBaseComp(StoreSection section) {
this.section = section;
}
protected ObservableBooleanValue effectiveExpanded(ObservableBooleanValue expanded) {
return section.getWrapper() != null ? Bindings.createBooleanBinding(
() -> {
return expanded.get()
&& section.getShownChildren().getList().size() > 0;
},
expanded,
section.getShownChildren().getList()) : new SimpleBooleanProperty(true);
}
protected void addPseudoClassListeners(VBox vbox, ObservableBooleanValue expanded) {
var observable = effectiveExpanded(expanded);
BindingsHelper.preserve(this, observable);
observable.subscribe(val -> {
vbox.pseudoClassStateChanged(EXPANDED, val);
});
vbox.pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
vbox.pseudoClassStateChanged(ODD, section.getDepth() % 2 != 0);
vbox.pseudoClassStateChanged(ROOT, section.getDepth() == 0);
vbox.pseudoClassStateChanged(SUB, section.getDepth() > 1);
vbox.pseudoClassStateChanged(TOP, section.getDepth() == 1);
if (section.getWrapper() != null) {
if (section.getDepth() == 1) {
section.getWrapper().getColor().subscribe(val -> {
var newList = new ArrayList<>(vbox.getStyleClass());
newList.removeIf(s -> Arrays.stream(DataColor.values()).anyMatch(dataStoreColor -> dataStoreColor.getId().equals(s)));
newList.remove("gray");
newList.add("color-box");
if (val != null) {
newList.add(val.getId());
} else {
newList.add("gray");
}
vbox.getStyleClass().setAll(newList);
});
}
section.getWrapper().getPerUser().subscribe(val -> {
vbox.pseudoClassStateChanged(PseudoClass.getPseudoClass("per-user"), val);
});
}
}
protected void addVisibilityListeners(VBox root, HBox hbox) {
var children = new ArrayList<>(hbox.getChildren());
hbox.getChildren().clear();
root.visibleProperty().subscribe((newValue) -> {
if (newValue) {
hbox.getChildren().addAll(children);
} else {
hbox.getChildren().removeAll(children);
}
});
}
protected ListBoxViewComp<StoreSection> createChildrenList(Function<StoreSection, Comp<?>> function, ObservableBooleanValue hide) {
var content = new ListBoxViewComp<>(
section.getShownChildren().getList(),
section.getAllChildren().getList(),
(StoreSection e) -> {
return function.apply(e).grow(true, false);
},
section.getWrapper() == null);
content.setVisibilityControl(true);
content.minHeight(0);
content.hgrow();
content.styleClass("children-content");
content.hide(hide);
return content;
}
protected Comp<CompStructure<Button>> createExpandButton(Runnable action, int width, ObservableBooleanValue expanded) {
var icon = Bindings.createObjectBinding(() -> new LabelGraphic.IconGraphic(
expanded.get() && section.getShownChildren().getList().size() > 0 ?
"mdal-keyboard_arrow_down" :
"mdal-keyboard_arrow_right"), expanded, section.getShownChildren().getList());
var expandButton = new IconButtonComp(icon,
action);
expandButton
.minWidth(width)
.prefWidth(width)
.accessibleText(Bindings.createStringBinding(
() -> {
return "Expand " + section.getWrapper().getName().getValue();
},
section.getWrapper().getName()))
.disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0))
.styleClass("expand-button")
.maxHeight(100);
return expandButton;
}
protected Comp<CompStructure<Button>> createQuickAccessButton(int width, Consumer<StoreSection> r) {
var quickAccessDisabled = Bindings.createBooleanBinding(
() -> {
return section.getShownChildren().getList().isEmpty();
},
section.getShownChildren().getList());
var quickAccessButton = new StoreQuickAccessButtonComp(section, r)
.styleClass("quick-access-button")
.minWidth(width)
.prefWidth(width)
.maxHeight(100)
.accessibleText(Bindings.createStringBinding(
() -> {
return "Access " + section.getWrapper().getName().getValue();
},
section.getWrapper().getName()))
.disable(quickAccessDisabled);
return quickAccessButton;
}
}

View file

@ -12,11 +12,14 @@ import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import java.util.ArrayList;
@ -24,103 +27,24 @@ import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class StoreSectionComp extends Comp<CompStructure<VBox>> {
public class StoreSectionComp extends StoreSectionBaseComp {
public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
private static final PseudoClass ROOT = PseudoClass.getPseudoClass("root");
private static final PseudoClass SUB = PseudoClass.getPseudoClass("sub");
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth");
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth");
private final StoreSection section;
private final boolean topLevel;
public StoreSectionComp(StoreSection section, boolean topLevel) {
this.section = section;
this.topLevel = topLevel;
}
private Comp<CompStructure<Button>> createQuickAccessButton() {
var quickAccessDisabled = Bindings.createBooleanBinding(
() -> {
return section.getShownChildren().getList().isEmpty();
},
section.getShownChildren().getList());
Consumer<StoreSection> quickAccessAction = w -> {
ThreadHelper.runFailableAsync(() -> {
w.getWrapper().executeDefaultAction();
});
};
var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction)
.vgrow()
.styleClass("quick-access-button")
.apply(struc -> struc.get().setMinWidth(30))
.apply(struc -> struc.get().setPrefWidth(30))
.maxHeight(100)
.accessibleText(Bindings.createStringBinding(
() -> {
return "Access " + section.getWrapper().getName().getValue();
},
section.getWrapper().getName()))
.disable(quickAccessDisabled)
.focusTraversableForAccessibility()
.tooltipKey("accessSubConnections", new KeyCodeCombination(KeyCode.RIGHT));
return quickAccessButton;
}
private Comp<CompStructure<Button>> createExpandButton() {
var expandButton = new IconButtonComp(
Bindings.createObjectBinding(
() -> new LabelGraphic.IconGraphic(
section.getWrapper().getExpanded().get()
&& section.getShownChildren()
.getList()
.size()
> 0
? "mdal-keyboard_arrow_down"
: "mdal-keyboard_arrow_right"),
section.getWrapper().getExpanded(),
section.getShownChildren().getList()),
() -> {
section.getWrapper().toggleExpanded();
});
expandButton
.apply(struc -> struc.get().setMinWidth(30))
.apply(struc -> struc.get().setPrefWidth(30))
.focusTraversableForAccessibility()
.tooltipKey("expand", new KeyCodeCombination(KeyCode.SPACE))
.accessibleText(Bindings.createStringBinding(
() -> {
return "Expand " + section.getWrapper().getName().getValue();
},
section.getWrapper().getName()))
.disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0))
.styleClass("expand-button")
.maxHeight(100)
.vgrow();
return expandButton;
public StoreSectionComp(StoreSection section) {
super(section);
}
@Override
public CompStructure<VBox> createBase() {
var entryButton = StoreEntryComp.customSection(section, topLevel);
var quickAccessButton = createQuickAccessButton();
var expandButton = createExpandButton();
var buttonList = new ArrayList<Comp<?>>();
if (entryButton.isFullSize()) {
buttonList.add(quickAccessButton);
}
buttonList.add(expandButton);
var buttons = new VerticalComp(buttonList);
var topEntryList = new HorizontalComp(List.of(buttons, entryButton.hgrow()));
topEntryList.apply(struc -> {
var mainButton = struc.get().getChildren().get(1);
mainButton.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
var entryButton = StoreEntryComp.customSection(section);
entryButton.hgrow();
entryButton.apply(struc -> {
struc.get().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.SPACE) {
section.getWrapper().toggleExpanded();
event.consume();
}
if (event.getCode() == KeyCode.RIGHT) {
var ref = (VBox) struc.get().getChildren().getFirst();
var ref = (VBox) ((HBox) struc.get().getParent()).getChildren().getFirst();
if (entryButton.isFullSize()) {
var btn = (Button) ref.getChildren().getFirst();
btn.fire();
@ -130,72 +54,45 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
});
});
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
// section is actually expanded
var listSections = section.getShownChildren()
.filtered(
storeSection -> section.getAllChildren().getList().size() <= 20
|| section.getWrapper().getExpanded().get(),
section.getWrapper().getExpanded(),
section.getAllChildren().getList());
var content = new ListBoxViewComp<>(
listSections.getList(),
section.getAllChildren().getList(),
(StoreSection e) -> {
return StoreSection.customSection(e, false).apply(GrowAugment.create(true, false));
},
false);
content.minHeight(0).hgrow();
var quickAccessButton = createQuickAccessButton(30, c -> {
ThreadHelper.runFailableAsync(() -> {
c.getWrapper().executeDefaultAction();
});
});
quickAccessButton.vgrow();
quickAccessButton.focusTraversableForAccessibility();
quickAccessButton.tooltipKey("accessSubConnections", new KeyCodeCombination(KeyCode.RIGHT));
var expandButton = createExpandButton(() -> section.getWrapper().toggleExpanded(), 30, section.getWrapper().getExpanded());
expandButton.vgrow();
expandButton.focusTraversableForAccessibility();
expandButton.tooltipKey("expand", new KeyCodeCombination(KeyCode.SPACE));
var buttonList = new ArrayList<Comp<?>>();
if (entryButton.isFullSize()) {
buttonList.add(quickAccessButton);
}
buttonList.add(expandButton);
var buttons = new VerticalComp(buttonList);
var topEntryList = new HorizontalComp(List.of(buttons, entryButton));
topEntryList.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));
topEntryList.minHeight(entryButton.getHeight());
topEntryList.maxHeight(entryButton.getHeight());
topEntryList.prefHeight(entryButton.getHeight());
var effectiveExpanded = effectiveExpanded(section.getWrapper().getExpanded());
var content = createChildrenList(c -> StoreSection.customSection(c), Bindings.not(effectiveExpanded));
var expanded = Bindings.createBooleanBinding(
() -> {
return section.getWrapper().getExpanded().get()
&& section.getShownChildren().getList().size() > 0;
},
section.getWrapper().getExpanded(),
section.getShownChildren().getList());
var full = new VerticalComp(List.of(
topEntryList,
Comp.separator().hide(expanded.not()),
content.styleClass("children-content")
.hide(Bindings.or(
Bindings.not(section.getWrapper().getExpanded()),
Bindings.size(section.getShownChildren().getList())
.isEqualTo(0)))));
return full.styleClass("store-entry-section-comp")
.apply(struc -> {
Comp.separator().hide(Bindings.not(effectiveExpanded)),
content));
full.styleClass("store-entry-section-comp");
full.apply(struc -> {
struc.get().setFillWidth(true);
expanded.subscribe(val -> {
struc.get().pseudoClassStateChanged(EXPANDED, val);
});
struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
struc.get().pseudoClassStateChanged(ODD, section.getDepth() % 2 != 0);
struc.get().pseudoClassStateChanged(ROOT, topLevel);
struc.get().pseudoClassStateChanged(SUB, !topLevel);
section.getWrapper().getColor().subscribe(val -> {
if (!topLevel) {
return;
}
var newList = new ArrayList<>(struc.get().getStyleClass());
newList.removeIf(s -> Arrays.stream(DataColor.values())
.anyMatch(
dataStoreColor -> dataStoreColor.getId().equals(s)));
newList.remove("gray");
newList.add("color-box");
if (val != null) {
newList.add(val.getId());
} else {
newList.add("gray");
}
struc.get().getStyleClass().setAll(newList);
});
section.getWrapper().getPerUser().subscribe(val -> {
struc.get().pseudoClassStateChanged(PseudoClass.getPseudoClass("per-user"), val);
});
})
.createStructure();
var hbox = ((HBox) struc.get().getChildren().getFirst());
addPseudoClassListeners(struc.get(), section.getWrapper().getExpanded());
addVisibilityListeners(struc.get(), hbox);
});
return full.createStructure();
}
}

View file

@ -12,23 +12,18 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
public class StoreSectionMiniComp extends StoreSectionBaseComp {
public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth");
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth");
private static final PseudoClass ROOT = PseudoClass.getPseudoClass("root");
private static final PseudoClass TOP = PseudoClass.getPseudoClass("top");
private static final PseudoClass SUB = PseudoClass.getPseudoClass("sub");
private final StoreSection section;
private final BooleanProperty expanded;
private final BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment;
private final Consumer<StoreSection> action;
@ -36,142 +31,61 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
StoreSection section,
BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment,
Consumer<StoreSection> action) {
this.section = section;
super(section);
this.augment = augment;
this.action = action;
this.expanded = new SimpleBooleanProperty(section.getWrapper() == null || section.getWrapper().getExpanded().getValue());
}
@Override
public CompStructure<VBox> createBase() {
var list = new ArrayList<Comp<?>>();
BooleanProperty expanded;
if (section.getWrapper() != null) {
var root = new ButtonComp(section.getWrapper().getShownName(), () -> {})
.apply(struc -> {
struc.get()
.setGraphic(PrettyImageHelper.ofFixedSize(
var root = new ButtonComp(section.getWrapper().getShownName(), () -> {
action.accept(section);
});
root.hgrow();
root.maxWidth(2000);
root.styleClass("item");
root.apply(struc -> {
struc.get().setAlignment(Pos.CENTER_LEFT);
struc.get().setGraphic(PrettyImageHelper.ofFixedSize(
section.getWrapper().getIconFile(), 16, 16)
.createRegion());
})
.apply(struc -> {
struc.get().setAlignment(Pos.CENTER_LEFT);
})
.apply(struc -> {
struc.get().setOnAction(event -> {
action.accept(section);
event.consume();
});
})
.grow(true, false)
.apply(struc -> struc.get().setMnemonicParsing(false))
.styleClass("item");
struc.get().setMnemonicParsing(false);
});
augment.accept(section, root);
expanded =
new SimpleBooleanProperty(section.getWrapper().getExpanded().get()
&& section.getShownChildren().getList().size() > 0);
var button = new IconButtonComp(
Bindings.createObjectBinding(
() -> new LabelGraphic.IconGraphic(
expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right"),
expanded),
() -> {
expanded.set(!expanded.get());
})
.apply(struc -> struc.get().setMinWidth(20))
.apply(struc -> struc.get().setPrefWidth(20))
.focusTraversable()
.accessibleText(Bindings.createStringBinding(
() -> {
return "Expand "
+ section.getWrapper().getName().getValue();
},
section.getWrapper().getName()))
.disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0))
.grow(false, true)
.styleClass("expand-button");
var expandButton = createExpandButton(() -> expanded.set(!expanded.get()), 20, expanded);
expandButton.focusTraversable();
var quickAccessDisabled = Bindings.createBooleanBinding(
() -> {
return section.getShownChildren().getList().isEmpty();
},
section.getShownChildren().getList());
Consumer<StoreSection> quickAccessAction = action;
var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction)
.vgrow()
.styleClass("quick-access-button")
.maxHeight(100)
.disable(quickAccessDisabled);
var quickAccessButton = createQuickAccessButton(20, action);
var buttonList = new ArrayList<Comp<?>>();
buttonList.add(button);
buttonList.add(expandButton);
buttonList.add(root);
if (section.getDepth() == 1) {
buttonList.add(quickAccessButton);
}
list.add(new HorizontalComp(buttonList).apply(struc -> struc.get().setFillHeight(true)));
} else {
expanded = new SimpleBooleanProperty(true);
var h = new HorizontalComp(buttonList);
h.apply(struc -> struc.get().setFillHeight(true));
h.prefHeight(28);
list.add(h);
}
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
// section is actually expanded
var listSections = section.getWrapper() != null
? section.getShownChildren()
.filtered(
storeSection ->
section.getAllChildren().getList().size() <= 20 || expanded.get(),
expanded,
section.getAllChildren().getList())
: section.getShownChildren();
var content = new ListBoxViewComp<>(
listSections.getList(),
section.getAllChildren().getList(),
(StoreSection e) -> {
return new StoreSectionMiniComp(e, this.augment, this.action);
},
section.getWrapper() == null)
.minHeight(0)
.hgrow();
var content = createChildrenList(c -> new StoreSectionMiniComp(c, this.augment, this.action), Bindings.not(expanded));
list.add(content);
list.add(content.styleClass("children-content")
.hide(Bindings.or(
Bindings.not(expanded),
Bindings.size(section.getAllChildren().getList()).isEqualTo(0))));
var vert = new VerticalComp(list);
return vert.styleClass("store-section-mini-comp")
.apply(struc -> {
var full = new VerticalComp(list);
full.styleClass("store-section-mini-comp");
full.apply(struc -> {
struc.get().setFillWidth(true);
expanded.subscribe(val -> {
struc.get().pseudoClassStateChanged(EXPANDED, val);
});
struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
struc.get().pseudoClassStateChanged(ODD, section.getDepth() % 2 != 0);
struc.get().pseudoClassStateChanged(ROOT, section.getDepth() == 0);
struc.get().pseudoClassStateChanged(TOP, section.getDepth() == 1);
struc.get().pseudoClassStateChanged(SUB, section.getDepth() > 1);
})
.apply(struc -> {
addPseudoClassListeners(struc.get(), expanded);
if (section.getWrapper() != null) {
section.getWrapper().getColor().subscribe(val -> {
if (section.getDepth() != 1) {
return;
}
struc.get().getStyleClass().removeIf(s -> Arrays.stream(DataColor.values())
.anyMatch(dataStoreColor ->
dataStoreColor.getId().equals(s)));
struc.get().getStyleClass().remove("gray");
struc.get().getStyleClass().add("color-box");
if (val != null) {
struc.get().getStyleClass().add(val.getId());
} else {
struc.get().getStyleClass().add("gray");
}
});
var hbox = ((HBox) struc.get().getChildren().getFirst());
addVisibilityListeners(struc.get(), hbox);
}
})
.createStructure();
});
return full.createStructure();
}
}

View file

@ -1,19 +1,12 @@
package io.xpipe.app.comp.store;
import java.time.Instant;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.*;
import java.util.stream.Stream;
public interface StoreSortMode {
StoreSortMode ALPHABETICAL_DESC = new StoreSortMode() {
@Override
public StoreSection representative(StoreSection s) {
return s;
}
@Override
public String getId() {
@ -27,11 +20,6 @@ public interface StoreSortMode {
}
};
StoreSortMode ALPHABETICAL_ASC = new StoreSortMode() {
@Override
public StoreSection representative(StoreSection s) {
return s;
}
@Override
public String getId() {
return "alphabetical-asc";
@ -44,10 +32,10 @@ public interface StoreSortMode {
.reversed();
}
};
StoreSortMode DATE_DESC = new StoreSortMode() {
StoreSortMode DATE_DESC = new StoreSortMode.DateSortMode() {
private Instant date(StoreSection s) {
var la = s.getWrapper().getLastAccessApplied().getValue();
protected Instant date(StoreSection s) {
var la = s.getWrapper().getLastAccess().getValue();
if (la == null) {
return Instant.MAX;
}
@ -56,35 +44,19 @@ public interface StoreSortMode {
}
@Override
public StoreSection representative(StoreSection s) {
return Stream.concat(
s.getShownChildren().getList().stream()
.filter(section -> section.getWrapper()
.getEntry()
.getValidity()
.isUsable())
.map(this::representative),
Stream.of(s))
.max(Comparator.comparing(section -> date(section)))
.orElseThrow();
protected int compare(Instant s1, Instant s2) {
return s1.compareTo(s2);
}
@Override
public String getId() {
return "date-desc";
}
@Override
public Comparator<StoreSection> comparator() {
return Comparator.comparing(e -> {
return date(e);
});
}
};
StoreSortMode DATE_ASC = new StoreSortMode() {
StoreSortMode DATE_ASC = new StoreSortMode.DateSortMode() {
private Instant date(StoreSection s) {
var la = s.getWrapper().getLastAccessApplied().getValue();
protected Instant date(StoreSection s) {
var la = s.getWrapper().getLastAccess().getValue();
if (la == null) {
return Instant.MIN;
}
@ -93,32 +65,16 @@ public interface StoreSortMode {
}
@Override
public StoreSection representative(StoreSection s) {
return Stream.concat(
s.getShownChildren().getList().stream()
.filter(section -> section.getWrapper()
.getEntry()
.getValidity()
.isUsable())
.map(this::representative),
Stream.of(s))
.max(Comparator.comparing(section -> date(section)))
.orElseThrow();
protected int compare(Instant s1, Instant s2) {
return s2.compareTo(s1);
}
@Override
public String getId() {
return "date-asc";
}
@Override
public Comparator<StoreSection> comparator() {
return Comparator.<StoreSection, Instant>comparing(e -> {
return date(e);
})
.reversed();
}
};
List<StoreSortMode> ALL = List.of(ALPHABETICAL_DESC, ALPHABETICAL_ASC, DATE_DESC, DATE_ASC);
static Optional<StoreSortMode> fromId(String id) {
@ -131,9 +87,54 @@ public interface StoreSortMode {
return DATE_ASC;
}
StoreSection representative(StoreSection s);
String getId();
Comparator<StoreSection> comparator();
abstract class DateSortMode implements StoreSortMode {
private int entriesListOberservableIndex = -1;
private final Map<StoreSection, StoreSection> cachedRepresentatives = new IdentityHashMap<>();
private StoreSection computeRepresentative(StoreSection s) {
return Stream.concat(
s.getShownChildren().getList().stream()
.filter(section -> section.getWrapper()
.getEntry()
.getValidity()
.isUsable())
.map(this::getRepresentative),
Stream.of(s))
.max(Comparator.comparing(section -> date(section)))
.orElseThrow();
}
private StoreSection getRepresentative(StoreSection s) {
if (StoreViewState.get().getEntriesListUpdateObservable().get() != entriesListOberservableIndex) {
cachedRepresentatives.clear();
entriesListOberservableIndex = StoreViewState.get().getEntriesListUpdateObservable().get();
}
if (cachedRepresentatives.containsKey(s)) {
return cachedRepresentatives.get(s);
}
var r = computeRepresentative(s);
cachedRepresentatives.put(s, r);
return r;
}
protected abstract Instant date(StoreSection s);
protected abstract int compare(Instant s1, Instant s2);
@Override
public Comparator<StoreSection> comparator() {
return (o1, o2) -> {
var r1 = getRepresentative(o1);
var r2 = getRepresentative(o2);
return DateSortMode.this.compare(date(r1), date(r2));
};
}
}
}

View file

@ -39,6 +39,9 @@ public class StoreViewState {
@Getter
private final Property<StoreCategoryWrapper> activeCategory = new SimpleObjectProperty<>();
@Getter
private final Property<StoreSortMode> sortMode = new SimpleObjectProperty<>();
@Getter
private StoreSection currentTopLevelSection;
@ -118,15 +121,23 @@ public class StoreViewState {
.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreEntries().stream()
.map(StoreEntryWrapper::new)
.toList()));
allEntries.getList().forEach(e -> e.applyLastAccess());
categories
.getList()
.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreCategories().stream()
.map(StoreCategoryWrapper::new)
.toList()));
sortMode.addListener((observable, oldValue, newValue) -> {
var cat = getActiveCategory().getValue();
if (cat == null) {
return;
}
cat.getSortMode().setValue(newValue);
});
activeCategory.addListener((observable, oldValue, newValue) -> {
DataStorage.get().setSelectedCategory(newValue.getCategory());
sortMode.setValue(newValue.getSortMode().getValue());
});
var selected = AppCache.getNonNull("selectedCategory", UUID.class, () -> DataStorage.DEFAULT_CATEGORY_UUID);
activeCategory.setValue(categories.getList().stream()
@ -141,7 +152,6 @@ public class StoreViewState {
}
public void updateDisplay() {
allEntries.getList().forEach(e -> e.applyLastAccess());
toggleStoreListUpdate();
}
@ -180,7 +190,6 @@ public class StoreViewState {
var l = Arrays.stream(entry)
.map(StoreEntryWrapper::new)
.peek(storeEntryWrapper -> storeEntryWrapper.update())
.peek(wrapper -> wrapper.applyLastAccess())
.toList();
// Don't update anything if we have already reset
@ -202,6 +211,7 @@ public class StoreViewState {
.getUuid())))
.forEach(storeCategoryWrapper -> storeCategoryWrapper.update());
}
l.forEach(storeEntryWrapper -> storeEntryWrapper.update());
});
}

View file

@ -20,10 +20,6 @@ public class AppFont {
// Load ikonli fonts
TrackEvent.info("Loading ikonli fonts ...");
new FontIcon("mdi2s-stop");
new FontIcon("mdi2m-magnify");
new FontIcon("mdi2d-database-plus");
new FontIcon("mdi2p-professional-hexagon");
new FontIcon("mdi2c-chevron-double-right");
TrackEvent.info("Loading bundled fonts ...");
AppResources.with(

View file

@ -56,10 +56,7 @@ public class AppInstance {
try {
var inputs = AppProperties.get().getArguments().getOpenArgs();
// Assume that we want to open the GUI if we launched again
client.get()
.performRequest(DaemonFocusExchange.Request.builder()
.mode(XPipeDaemonMode.GUI)
.build());
client.get().performRequest(DaemonFocusExchange.Request.builder().build());
if (!inputs.isEmpty()) {
client.get()
.performRequest(DaemonOpenExchange.Request.builder()

View file

@ -47,6 +47,7 @@ public class AppProperties {
boolean autoAcceptEula;
UUID uuid;
boolean initialLaunch;
boolean restarted;
/**
* Unique identifier that resets on every XPipe restart.
*/
@ -129,6 +130,9 @@ public class AppProperties {
autoAcceptEula = Optional.ofNullable(System.getProperty("io.xpipe.app.acceptEula"))
.map(Boolean::parseBoolean)
.orElse(false);
restarted = Optional.ofNullable(System.getProperty("io.xpipe.app.restarted"))
.map(Boolean::parseBoolean)
.orElse(false);
// We require the user dir from here
AppUserDirectoryCheck.check(dataDir);

View file

@ -19,6 +19,7 @@ import javafx.application.ColorScheme;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.MapChangeListener;
import javafx.css.PseudoClass;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
@ -83,33 +84,40 @@ public class AppTheme {
public static void init() {
if (init) {
TrackEvent.trace("Theme init requested again");
return;
}
if (AppPrefs.get() == null) {
TrackEvent.trace("Theme init prior to prefs init, setting theme to default");
Theme.getDefaultLightTheme().apply();
return;
}
try {
var lastSystemDark = AppCache.getBoolean("lastDarkTheme", false);
var nowDark = Platform.getPreferences().getColorScheme() == ColorScheme.DARK;
var nowDark = isDarkMode();
AppCache.update("lastDarkTheme", nowDark);
if (AppPrefs.get().theme().getValue() == null || lastSystemDark != nowDark) {
TrackEvent.trace("Updating theme to system theme");
setDefault();
}
Platform.getPreferences().addListener((MapChangeListener<? super String, ? super Object>) change -> {
TrackEvent.withTrace("Platform preference changed").tag("change", change.toString()).handle();
});
Platform.getPreferences().addListener((MapChangeListener<? super String, ? super Object>) change -> {
if (change.getKey().equals("GTK.theme_name")) {
Platform.runLater(() -> {
updateThemeToThemeName(change.getValueRemoved(), change.getValueAdded());
});
}
});
Platform.getPreferences().colorSchemeProperty().addListener((observableValue, colorScheme, t1) -> {
Platform.runLater(() -> {
if (t1 == ColorScheme.DARK
&& !AppPrefs.get().theme().getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
}
if (t1 != ColorScheme.DARK
&& AppPrefs.get().theme().getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
}
updateThemeToColorScheme(t1);
});
});
} catch (IllegalStateException ex) {
@ -130,12 +138,52 @@ public class AppTheme {
init = true;
}
private static void updateThemeToThemeName(Object oldName, Object newName) {
if (OsType.getLocal() == OsType.LINUX && newName != null) {
var toDark = (oldName == null || !oldName.toString().contains("-dark")) &&
newName.toString().contains("-dark");
var toLight = (oldName == null || oldName.toString().contains("-dark")) &&
!newName.toString().contains("-dark");
if (toDark) {
updateThemeToColorScheme(ColorScheme.DARK);
} else if (toLight) {
updateThemeToColorScheme(ColorScheme.LIGHT);
}
}
}
private static boolean isDarkMode() {
var nowDark = Platform.getPreferences().getColorScheme() == ColorScheme.DARK;
if (nowDark) {
return true;
}
var gtkTheme = Platform.getPreferences().get("GTK.theme_name");
return gtkTheme != null && gtkTheme.toString().contains("-dark");
}
private static void updateThemeToColorScheme(ColorScheme colorScheme) {
if (colorScheme == null) {
return;
}
if (colorScheme == ColorScheme.DARK
&& !AppPrefs.get().theme().getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
}
if (colorScheme != ColorScheme.DARK
&& AppPrefs.get().theme().getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
}
}
public static void reset() {
if (!init) {
return;
}
var nowDark = Platform.getPreferences().getColorScheme() == ColorScheme.DARK;
var nowDark = isDarkMode();
AppCache.update("lastDarkTheme", nowDark);
}
@ -162,19 +210,26 @@ public class AppTheme {
}
PlatformThread.runLaterIfNeeded(() -> {
if (AppMainWindow.getInstance() == null) {
var window = AppMainWindow.getInstance();
if (window == null) {
return;
}
var window = AppMainWindow.getInstance().getStage();
var scene = window.getScene();
TrackEvent.debug("Setting theme " + newTheme.getId() + " for scene");
// Don't animate transition in performance mode
if (AppPrefs.get() == null || AppPrefs.get().performanceMode().get()) {
newTheme.apply();
return;
}
var stage = window.getStage();
var scene = stage.getScene();
Pane root = (Pane) scene.getRoot();
Image snapshot = scene.snapshot(null);
ImageView imageView = new ImageView(snapshot);
root.getChildren().add(imageView);
newTheme.apply();
TrackEvent.debug("Set theme " + newTheme.getId() + " for scene");
Platform.runLater(() -> {
// Animate!
@ -300,7 +355,7 @@ public class AppTheme {
AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10, AppFontSizes.BASE_11),
() -> ColorHelper.withOpacity(
Platform.getPreferences().getAccentColor().desaturate().desaturate(), 0.2),
115);
91);
// Adjust this to create your own theme
public static final Theme CUSTOM = new DerivedTheme(

View file

@ -1,36 +0,0 @@
package io.xpipe.app.core.check;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.OsType;
import java.util.concurrent.TimeUnit;
public class AppBundledToolsCheck {
private static boolean getResult() {
var fc = new ProcessBuilder("where", "ssh")
.redirectErrorStream(true)
.redirectOutput(ProcessBuilder.Redirect.DISCARD);
try {
var proc = fc.start();
proc.waitFor(2, TimeUnit.SECONDS);
return proc.exitValue() == 0;
} catch (Exception e) {
return false;
}
}
public static void check() {
if (AppPrefs.get().useBundledTools().get()) {
return;
}
if (!OsType.getLocal().equals(OsType.WINDOWS)) {
return;
}
if (!getResult()) {
AppPrefs.get().useBundledTools.set(true);
}
}
}

View file

@ -56,9 +56,6 @@ public class BaseMode extends OperationMode {
TrackEvent.info("Initializing base mode components ...");
AppMainWindow.loadingText("initializingApp");
LicenseProvider.get().init();
// We no longer need this
// AppCertutilCheck.check();
AppBundledToolsCheck.check();
AppHomebrewCoreutilsCheck.check();
AppAvCheck.check();
AppJavaOptionsCheck.check();
@ -94,6 +91,7 @@ public class BaseMode extends OperationMode {
AppPrefs.setLocalDefaultsIfNeeded();
PlatformInit.init(true);
AppMainWindow.addUpdateTitleListener();
TrackEvent.info("Shell initialization thread completed");
},
() -> {
shellLoaded.await();
@ -106,15 +104,16 @@ public class BaseMode extends OperationMode {
DataStorage.init();
storageLoaded.countDown();
StoreViewState.init();
AppMainWindow.loadingText("loadingUserInterface");
AppLayoutModel.init();
PlatformInit.init(true);
PlatformThread.runLaterIfNeededBlocking(() -> {
AppGreetingsDialog.showIfNeeded();
AppMainWindow.loadingText("initializingApp");
});
imagesLoaded.await();
browserLoaded.await();
iconsLoaded.await();
TrackEvent.info("Waiting for startup dialogs to close");
AppDialog.waitForAllDialogsClose();
PlatformThread.runLaterIfNeededBlocking(() -> {
try {
@ -124,6 +123,7 @@ public class BaseMode extends OperationMode {
}
});
UpdateChangelogAlert.showIfNeeded();
TrackEvent.info("Connection storage initialization thread completed");
},
() -> {
AppFileWatcher.init();
@ -131,6 +131,7 @@ public class BaseMode extends OperationMode {
BlobManager.init();
TerminalView.init();
TerminalLauncherManager.init();
TrackEvent.info("File/Watcher initialization thread completed");
},
() -> {
PlatformInit.init(true);
@ -139,13 +140,16 @@ public class BaseMode extends OperationMode {
storageLoaded.await();
SystemIconManager.init();
iconsLoaded.countDown();
TrackEvent.info("Platform initialization thread completed");
},
() -> {
BrowserIconManager.loadIfNecessary();
shellLoaded.await();
BrowserLocalFileSystem.init();
storageLoaded.await();
BrowserFullSessionModel.init();
browserLoaded.countDown();
TrackEvent.info("Browser initialization thread completed");
});
ActionProvider.initProviders();
DataStoreProviders.init();

View file

@ -180,6 +180,8 @@ public abstract class OperationMode {
var startupMode = getStartupMode();
switchToSyncOrThrow(map(startupMode));
// If it doesn't find time, the JVM will not gc the startup workload
System.gc();
inStartup = false;
AppOpenArguments.init();
}
@ -256,7 +258,7 @@ public abstract class OperationMode {
var exec = XPipeInstallation.createExternalAsyncLaunchCommand(
loc,
XPipeDaemonMode.GUI,
"\"-Dio.xpipe.app.acceptEula=true\" \"-Dio.xpipe.app.dataDir=" + dataDir + "\"",
"\"-Dio.xpipe.app.acceptEula=true\" \"-Dio.xpipe.app.dataDir=" + dataDir + "\" \"-Dio.xpipe.app.restarted=true\"",
true);
LocalShell.getShell().executeSimpleCommand(exec);
}

View file

@ -84,7 +84,7 @@ public class AppDialog {
var transition = new PauseTransition(Duration.millis(200));
transition.setOnFinished(e -> {
if (wait) {
Platform.exitNestedEventLoop(key, null);
PlatformThread.exitNestedEventLoop(key);
}
});
transition.play();
@ -95,7 +95,7 @@ public class AppDialog {
}
});
if (wait) {
Platform.enterNestedEventLoop(key);
PlatformThread.enterNestedEventLoop(key);
waitForDialogClose(o);
}
}
@ -108,6 +108,7 @@ public class AppDialog {
public static Comp<?> dialogText(String s) {
return Comp.of(() -> {
var text = new Text(s);
text.getStyleClass().add("dialog-text");
text.setWrappingWidth(450);
var sp = new StackPane(text);
return sp;
@ -118,6 +119,7 @@ public class AppDialog {
public static Comp<?> dialogText(ObservableValue<String> s) {
return Comp.of(() -> {
var text = new Text();
text.getStyleClass().add("dialog-text");
text.textProperty().bind(s);
text.setWrappingWidth(450);
var sp = new StackPane(text);

View file

@ -138,8 +138,10 @@ public class AppMainWindow {
}
public static synchronized void initContent() {
TrackEvent.info("Window content node creation started");
var content = new AppLayoutComp();
var s = content.createStructure();
TrackEvent.info("Window content node structure created");
loadedContent.setValue(s);
}
@ -150,6 +152,13 @@ public class AppMainWindow {
}
}
public void focus() {
PlatformThread.runLaterIfNeeded(() -> {
stage.setIconified(false);
stage.requestFocus();
});
}
private static String createTitle() {
var t = LicenseProvider.get().licenseTitle();
var base =

View file

@ -124,10 +124,13 @@ public class ModifiedStage extends Stage {
var transition = new PauseTransition(Duration.millis(300));
transition.setOnFinished(e -> {
applyModes(stage);
stage.setWidth(stage.getWidth() - 1);
Platform.runLater(() -> {
stage.setWidth(stage.getWidth() + 1);
});
// We only need to update the frame by resizing on Windows
if (OsType.getLocal() == OsType.WINDOWS) {
stage.setWidth(stage.getWidth() - 1);
Platform.runLater(() -> {
stage.setWidth(stage.getWidth() + 1);
});
}
});
transition.play();
});

View file

@ -2,6 +2,7 @@ package io.xpipe.app.ext;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.FailableConsumer;
@ -21,6 +22,7 @@ public interface ActionProvider {
List<ActionProvider> ALL_STANDALONE = new ArrayList<>();
static void initProviders() {
TrackEvent.trace("Starting action provider initialization");
for (ActionProvider actionProvider : ALL) {
try {
actionProvider.init();
@ -28,6 +30,7 @@ public interface ActionProvider {
ErrorEvent.fromThrowable(t).handle();
}
}
TrackEvent.trace("Finished action provider initialization");
}
default void init() throws Exception {}

View file

@ -93,8 +93,8 @@ public interface DataStoreProvider {
return StoreEntryComp.create(s, null, preferLarge);
}
default StoreSectionComp customSectionComp(StoreSection section, boolean topLevel) {
return new StoreSectionComp(section, topLevel);
default StoreSectionComp customSectionComp(StoreSection section) {
return new StoreSectionComp(section);
}
default boolean shouldShowScan() {

View file

@ -7,6 +7,7 @@ import com.github.weisj.jsvg.SVGDocument;
import com.github.weisj.jsvg.SVGRenderingHints;
import com.github.weisj.jsvg.attributes.ViewBox;
import com.github.weisj.jsvg.parser.SVGLoader;
import io.xpipe.app.issue.TrackEvent;
import lombok.Getter;
import java.awt.*;
@ -21,6 +22,14 @@ import javax.imageio.ImageIO;
public class SystemIconCache {
private static enum ImageColorScheme {
TRANSPARENT,
MIXED,
LIGHT,
DARK
}
private static final Path DIRECTORY =
AppProperties.get().getDataDir().resolve("cache").resolve("icons").resolve("raster");
private static final int[] sizes = new int[] {16, 24, 40, 80};
@ -50,11 +59,31 @@ public class SystemIconCache {
Files.createDirectories(target);
for (var icon : e.getValue().getIcons()) {
if (refreshChecksum(icon.getFile(), target, icon.getName(), icon.isDark())) {
var dark = icon.getColorSchemeData() == SystemIconSourceFile.ColorSchemeData.DARK;
if (refreshChecksum(icon.getFile(), target, icon.getName(), dark)) {
continue;
}
rasterizeSizes(icon.getFile(), target, icon.getName(), icon.isDark());
var scheme = rasterizeSizes(icon.getFile(), target, icon.getName(), dark);
if (scheme == ImageColorScheme.TRANSPARENT) {
var message = "Failed to rasterize icon icon " + icon.getFile().getFileName().toString() + ": Rasterized image is transparent";
ErrorEvent.fromMessage(message).omit().expected().handle();
continue;
}
if (scheme != ImageColorScheme.DARK || icon.getColorSchemeData() != SystemIconSourceFile.ColorSchemeData.DEFAULT) {
continue;
}
var hasExplicitDark = e.getValue().getIcons().stream().anyMatch(
systemIconSourceFile -> systemIconSourceFile.getSource().equals(icon.getSource()) &&
systemIconSourceFile.getName().equals(icon.getName()) &&
systemIconSourceFile.getColorSchemeData() == SystemIconSourceFile.ColorSchemeData.DARK);
if (hasExplicitDark) {
continue;
}
rasterizeSizesInverted(icon.getFile(), target, icon.getName(), true);
}
}
} catch (Exception e) {
@ -77,28 +106,60 @@ public class SystemIconCache {
}
}
private static boolean rasterizeSizes(Path path, Path dir, String name, boolean dark) throws IOException {
private static ImageColorScheme rasterizeSizes(Path path, Path dir, String name, boolean dark) throws IOException {
TrackEvent.trace("Rasterizing image " + path.getFileName().toString());
try {
ImageColorScheme c = null;
for (var size : sizes) {
rasterize(path, dir, name, dark, size);
var image = rasterize(path, size);
if (image == null) {
continue;
}
if (c == null) {
c = determineColorScheme(image);
if (c == ImageColorScheme.TRANSPARENT) {
return ImageColorScheme.TRANSPARENT;
}
}
write(dir, name, dark, size, image);
}
return true;
return c;
} catch (Exception ex) {
var message = "Failed to rasterize icon icon " + path.getFileName().toString() + ": " + ex.getMessage();
ErrorEvent.fromThrowable(ex).description(message).omit().expected().handle();
return null;
}
}
private static ImageColorScheme rasterizeSizesInverted(Path path, Path dir, String name, boolean dark) throws IOException {
try {
ImageColorScheme c = null;
for (var size : sizes) {
var image = rasterize(path, size);
if (image == null) {
continue;
}
var invert = invert(image);
write(dir, name, dark, size, invert);
}
return c;
} catch (Exception ex) {
if (ex instanceof IOException) {
throw ex;
}
ErrorEvent.fromThrowable(ex).omit().expected().handle();
return false;
return null;
}
}
private static void rasterize(Path path, Path dir, String name, boolean dark, int px) throws IOException {
private static BufferedImage rasterize(Path path, int px) throws IOException {
SVGLoader loader = new SVGLoader();
URL svgUrl = path.toUri().toURL();
SVGDocument svgDocument = loader.load(svgUrl);
if (svgDocument == null) {
return;
return null;
}
BufferedImage image = new BufferedImage(px, px, BufferedImage.TYPE_INT_ARGB);
@ -109,8 +170,67 @@ public class SystemIconCache {
g.setRenderingHint(SVGRenderingHints.KEY_SOFT_CLIPPING, SVGRenderingHints.VALUE_SOFT_CLIPPING_ON);
svgDocument.render((Component) null, g, new ViewBox(0, 0, px, px));
g.dispose();
return image;
}
private static BufferedImage write(Path dir, String name, boolean dark, int px, BufferedImage image) throws IOException {
var out = dir.resolve(name + "-" + px + (dark ? "-dark" : "") + ".png");
ImageIO.write(image, "png", out.toFile());
return image;
}
private static BufferedImage invert(BufferedImage image) {
var buffer = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
int clr = image.getRGB(x, y);
int alpha = (clr >> 24) & 0xff;
int red = (clr & 0x00ff0000) >> 16;
int green = (clr & 0x0000ff00) >> 8;
int blue = clr & 0x000000ff;
buffer.setRGB(x, y, new Color(255- red, 255- green, 255- blue, alpha).getRGB());
}
}
return buffer;
}
private static ImageColorScheme determineColorScheme(BufferedImage image) {
var transparent = true;
var counter = 0;
var mean = 0.0;
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
int clr = image.getRGB(x, y);
int alpha = (clr >> 24) & 0xff;
int red = (clr & 0x00ff0000) >> 16;
int green = (clr & 0x0000ff00) >> 8;
int blue = clr & 0x000000ff;
if (alpha > 0) {
transparent = false;
}
if (alpha < 200) {
continue;
}
mean += red + green + blue;
counter++;
}
}
if (transparent) {
return ImageColorScheme.TRANSPARENT;
}
mean /= counter * 3;
if (mean < 50) {
return ImageColorScheme.DARK;
} else if (mean > 205) {
return ImageColorScheme.LIGHT;
} else {
return ImageColorScheme.MIXED;
}
}
}

View file

@ -80,7 +80,18 @@ public class SystemIconManager {
});
}
public static void reloadImages() {
private static void reloadImages() {
AppImages.remove(s -> s.startsWith("icons/"));
try {
for (var source : getEffectiveSources()) {
AppImages.loadRasterImages(SystemIconCache.getDirectory(source), "icons/" + source.getId());
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
}
private static void clearInvalidImages() {
AppImages.remove(s -> s.startsWith("icons/"));
try {
for (var source : getEffectiveSources()) {

View file

@ -1,11 +1,13 @@
package io.xpipe.app.icon;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.Validators;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.util.ValidationException;
import com.fasterxml.jackson.annotation.JsonSubTypes;
@ -90,7 +92,14 @@ public interface SystemIconSource {
@Override
public void refresh() throws Exception {
try (var sc =
ProcessControlProvider.get().createLocalProcessControl(true).start()) {
ProcessControlProvider.get().createLocalProcessControl(true).start()) {
var present = sc.view().findProgram("git").isPresent();
if (!present) {
var msg = "Git command-line tools are not available in the PATH but are required to use icons from a git repository. For more details, see https://git-scm.com/downloads.";
ErrorEvent.fromMessage(msg).expected().handle();
return;
}
var dir = SystemIconManager.getPoolPath().resolve(id);
if (!Files.exists(dir)) {
sc.command(CommandBuilder.of()

View file

@ -31,25 +31,53 @@ public class SystemIconSourceData {
}
var files = Files.walk(dir).toList();
for (var file : files) {
if (file.getFileName().toString().endsWith(".svg")) {
var name = FilenameUtils.getBaseName(file.getFileName().toString());
var cleanedName = name.replaceFirst("-light$", "").replaceFirst("-dark$", "");
var hasLightVariant = Files.exists(file.getParent().resolve(cleanedName + "-light.svg"));
var hasDarkVariant = Files.exists(file.getParent().resolve(cleanedName + "-dark.svg"));
if (hasLightVariant && !hasDarkVariant && name.endsWith("-light")) {
var s = new SystemIconSourceFile(source, cleanedName.toLowerCase(Locale.ROOT), file, true);
sourceFiles.add(s);
continue;
}
List<Path> flatFiles = files.stream()
.filter(path -> Files.isRegularFile(path))
.filter(path -> path.toString().endsWith(".svg"))
.map(path -> {
var name = FilenameUtils.getBaseName(path.getFileName().toString());
var cleanedName = name.replaceFirst("-light$", "").replaceFirst("-dark$", "");
var cleanedPath = path.getParent().resolve(cleanedName + ".svg");
return cleanedPath;
}).toList();
for (var file : flatFiles) {
var name = FilenameUtils.getBaseName(file.getFileName().toString());
var displayName = name.toLowerCase(Locale.ROOT);
var baseFile = file.getParent().resolve(name + ".svg");
var hasBaseVariant = Files.exists(baseFile);
var darkModeFile = file.getParent().resolve(name + "-light.svg");
var hasDarkModeVariant = Files.exists(darkModeFile);
var lightModeFile = file.getParent().resolve(name + "-dark.svg");
var hasLightModeVariant = Files.exists(lightModeFile);
if (hasLightVariant && hasDarkVariant && (name.endsWith("-dark") || name.endsWith("-light"))) {
continue;
}
var s = new SystemIconSourceFile(source, cleanedName.toLowerCase(Locale.ROOT), file, false);
sourceFiles.add(s);
if (hasBaseVariant && hasDarkModeVariant) {
sourceFiles.add(new SystemIconSourceFile(source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
sourceFiles.add(new SystemIconSourceFile(source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DARK));
continue;
}
if (hasBaseVariant && hasLightModeVariant) {
sourceFiles.add(new SystemIconSourceFile(source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DARK));
sourceFiles.add(new SystemIconSourceFile(source, displayName, lightModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
continue;
}
if (!hasBaseVariant) {
if (hasLightModeVariant) {
sourceFiles.add(new SystemIconSourceFile(source, displayName, lightModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
if (hasDarkModeVariant) {
sourceFiles.add(new SystemIconSourceFile(source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DARK));
}
} else {
if (hasDarkModeVariant) {
sourceFiles.add(
new SystemIconSourceFile(source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
}
}
continue;
}
sourceFiles.add(new SystemIconSourceFile(source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();

View file

@ -7,8 +7,13 @@ import java.nio.file.Path;
@Value
public class SystemIconSourceFile {
public static enum ColorSchemeData {
DARK,
DEFAULT;
}
SystemIconSource source;
String name;
Path file;
boolean dark;
ColorSchemeData colorSchemeData;
}

View file

@ -39,6 +39,14 @@ public class SentryErrorHandler implements ErrorHandler {
return hasEmail || hasText;
}
private static boolean doesExceedCommentSize(String text) {
if (text == null || text.isEmpty()) {
return false;
}
return text.length() > 5000;
}
private static Throwable adjustCopy(Throwable throwable, boolean clear) {
if (throwable == null) {
return null;
@ -139,6 +147,16 @@ public class SentryErrorHandler implements ErrorHandler {
atts.forEach(attachment -> s.addAttachment(attachment));
}
if (doesExceedCommentSize(ee.getUserReport())) {
try {
var report = Files.createTempFile("report", ".txt");
Files.writeString(report, ee.getUserReport());
s.addAttachment(new Attachment(report.toString()));
} catch (Exception ex) {
AppLogs.get().logException("Unable to create report file", ex);
}
}
s.setTag(
"hasLicense",
String.valueOf(
@ -176,7 +194,7 @@ public class SentryErrorHandler implements ErrorHandler {
AppPrefs.get() != null
? String.valueOf(AppPrefs.get().useLocalFallbackShell().get())
: "unknown");
s.setTag("initial", AppProperties.get() != null ? AppProperties.get().isInitialLaunch() + "" : null);
s.setTag("initial", AppProperties.get() != null ? AppProperties.get().isInitialLaunch() + "" : "false");
var exMessage = ee.getThrowable() != null ? ee.getThrowable().getMessage() : null;
if (ee.getDescription() != null
@ -231,7 +249,11 @@ public class SentryErrorHandler implements ErrorHandler {
if (hasEmail) {
fb.setEmail(email);
}
fb.setComments(text);
if (doesExceedCommentSize(text)) {
fb.setComments("<Attachment>");
} else {
fb.setComments(text);
}
Sentry.captureUserFeedback(fb);
}
Sentry.flush(3000);

View file

@ -32,8 +32,8 @@ public class AboutCategory extends AppPrefsCategory {
.grow(true, false),
null)
.addComp(
new TileButtonComp("slack", "slackDescription", "mdi2s-slack", e -> {
Hyperlinks.open(Hyperlinks.SLACK);
new TileButtonComp("documentation", "documentationDescription", "mdi2b-book-open-variant", e -> {
Hyperlinks.open(Hyperlinks.DOCS);
e.consume();
})
.grow(true, false),
@ -45,13 +45,6 @@ public class AboutCategory extends AppPrefsCategory {
})
.grow(true, false),
null)
.addComp(
new TileButtonComp("securityPolicy", "securityPolicyDescription", "mdrmz-security", e -> {
Hyperlinks.open(Hyperlinks.DOCS_SECURITY);
e.consume();
})
.grow(true, false),
null)
.addComp(
new TileButtonComp("privacy", "privacyDescription", "mdomz-privacy_tip", e -> {
Hyperlinks.open(Hyperlinks.DOCS_PRIVACY);
@ -66,7 +59,8 @@ public class AboutCategory extends AppPrefsCategory {
.styleClass("open-source-notices");
var modal = ModalOverlay.of("openSourceNotices", comp);
modal.show();
}))
})
.grow(true, false))
.addComp(
new TileButtonComp("eula", "eulaDescription", "mdi2c-card-text-outline", e -> {
Hyperlinks.open(Hyperlinks.DOCS_EULA);

View file

@ -9,6 +9,7 @@ import io.xpipe.app.icon.SystemIconSource;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.util.ModuleHelper;
@ -57,8 +58,6 @@ public class AppPrefs {
mapVaultShared(new SimpleBooleanProperty(false), "dontAcceptNewHostKeys", Boolean.class, false);
public final BooleanProperty performanceMode =
mapLocal(new SimpleBooleanProperty(), "performanceMode", Boolean.class, false);
public final BooleanProperty useBundledTools =
mapLocal(new SimpleBooleanProperty(false), "useBundledTools", Boolean.class, true);
public final ObjectProperty<AppTheme.Theme> theme =
mapLocal(new SimpleObjectProperty<>(), "theme", AppTheme.Theme.class, false);
final BooleanProperty useSystemFont =
@ -118,6 +117,8 @@ public class AppPrefs {
mapLocal(new SimpleObjectProperty<>(), "externalEditor", ExternalEditorType.class, false);
final StringProperty customEditorCommand =
mapLocal(new SimpleStringProperty(""), "customEditorCommand", String.class, false);
final BooleanProperty customEditorCommandInTerminal =
mapLocal(new SimpleBooleanProperty(false), "customEditorCommandInTerminal", Boolean.class, false);
final BooleanProperty automaticallyCheckForUpdates =
mapLocal(new SimpleBooleanProperty(true), "automaticallyCheckForUpdates", Boolean.class, false);
final BooleanProperty encryptAllVaultData =
@ -272,7 +273,7 @@ public class AppPrefs {
INSTANCE = new AppPrefs();
PrefsProvider.getAll().forEach(prov -> prov.addPrefs(INSTANCE.extensionHandler));
INSTANCE.loadLocal();
INSTANCE.fixInvalidLocalValues();
INSTANCE.adjustLocalValues();
INSTANCE.vaultStorageHandler = new AppPrefsStorageHandler(
INSTANCE.storageDirectory().getValue().resolve("preferences.json"));
}
@ -326,10 +327,6 @@ public class AppPrefs {
return performanceMode;
}
public ObservableBooleanValue useBundledTools() {
return useBundledTools;
}
public ObservableValue<Boolean> useSystemFont() {
return useSystemFont;
}
@ -410,6 +407,10 @@ public class AppPrefs {
return customEditorCommand;
}
public ObservableBooleanValue customEditorCommandInTerminal() {
return customEditorCommandInTerminal;
}
public final ReadOnlyIntegerProperty editorReloadTimeout() {
return editorReloadTimeout;
}
@ -531,7 +532,7 @@ public class AppPrefs {
}
}
private void fixInvalidLocalValues() {
private void adjustLocalValues() {
// You can set the directory to empty in the settings
if (storageDirectory.get() == null || storageDirectory.get().toString().isBlank()) {
storageDirectory.setValue(DEFAULT_STORAGE_DIR);
@ -543,6 +544,11 @@ public class AppPrefs {
ErrorEvent.fromThrowable(e).expected().build().handle();
storageDirectory.setValue(DEFAULT_STORAGE_DIR);
}
if (AppProperties.get().isInitialLaunch()) {
var f = PlatformState.determineDefaultScalingFactor();
uiScale.setValue(f.isPresent() ? f.getAsInt() : null);
}
}
private void loadSharedRemote() {

View file

@ -52,7 +52,6 @@ public class AppPrefsComp extends SimpleComp {
split.setFillHeight(true);
split.getStyleClass().add("prefs");
var stack = new StackPane(split);
stack.setPickOnBounds(false);
return stack;
}
}

View file

@ -14,24 +14,25 @@ public class ConnectionsCategory extends AppPrefsCategory {
@Override
protected Comp<?> create() {
var prefs = AppPrefs.get();
var options = new OptionsBuilder()
.addTitle("connections")
.sub(new OptionsBuilder()
.pref(prefs.condenseConnectionDisplay)
.addToggle(prefs.condenseConnectionDisplay)
.pref(prefs.showChildCategoriesInParentCategory)
.addToggle(prefs.showChildCategoriesInParentCategory)
.pref(prefs.openConnectionSearchWindowOnConnectionCreation)
.addToggle(prefs.openConnectionSearchWindowOnConnectionCreation)
.pref(prefs.requireDoubleClickForConnections)
.addToggle(prefs.requireDoubleClickForConnections))
var connectionsBuilder = new OptionsBuilder().pref(prefs.condenseConnectionDisplay).addToggle(prefs.condenseConnectionDisplay).pref(
prefs.showChildCategoriesInParentCategory).addToggle(prefs.showChildCategoriesInParentCategory).pref(
prefs.openConnectionSearchWindowOnConnectionCreation).addToggle(prefs.openConnectionSearchWindowOnConnectionCreation).pref(
prefs.requireDoubleClickForConnections).addToggle(prefs.requireDoubleClickForConnections);
var localShellBuilder = new OptionsBuilder().pref(prefs.useLocalFallbackShell).addToggle(prefs.useLocalFallbackShell);
// Change order to prioritize fallback shell on macOS
var options = OsType.getLocal() == OsType.MACOS ? new OptionsBuilder()
.addTitle("localShell")
.sub(new OptionsBuilder().pref(prefs.useLocalFallbackShell).addToggle(prefs.useLocalFallbackShell));
.sub(localShellBuilder)
.addTitle("connections")
.sub(connectionsBuilder) :
new OptionsBuilder()
.addTitle("connections")
.sub(connectionsBuilder)
.addTitle("localShell")
.sub(localShellBuilder);
if (OsType.getLocal() == OsType.WINDOWS) {
options.addTitle("sshConfiguration")
.sub(new OptionsBuilder()
.pref(prefs.useBundledTools)
.addToggle(prefs.useBundledTools)
.addComp(prefs.getCustomComp("x11WslInstance")));
}
return options.buildComp();

View file

@ -47,9 +47,13 @@ public class EditorCategory extends AppPrefsCategory {
prefs.externalEditor, PrefsChoiceValue.getSupported(ExternalEditorType.class), false))
.nameAndDescription("customEditorCommand")
.addComp(new TextFieldComp(prefs.customEditorCommand, true)
.apply(struc -> struc.get().setPromptText("myeditor $FILE"))
.hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM)))
.addComp(terminalTest))
.apply(struc -> struc.get().setPromptText("myeditor $FILE")))
.hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM))
.addComp(terminalTest)
.nameAndDescription("customEditorCommandInTerminal")
.addToggle(prefs.customEditorCommandInTerminal)
.hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM))
)
.buildComp();
}
}

View file

@ -2,6 +2,7 @@ package io.xpipe.app.prefs;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.WindowsRegistry;
import io.xpipe.core.process.CommandBuilder;
@ -36,6 +37,53 @@ public interface ExternalEditorType extends PrefsChoiceValue {
}
};
WindowsType CURSOR_WINDOWS = new WindowsType("app.cursor", "Cursor", true) {
@Override
protected Optional<Path> determineInstallation() {
return Optional.of(Path.of(System.getenv("LOCALAPPDATA"))
.resolve("Programs")
.resolve("cursor")
.resolve("Cursor.exe"));
}
};
WindowsType WINDSURF_WINDOWS = new WindowsType("app.windsurf", "windsurf.cmd", false) {
@Override
protected Optional<Path> determineInstallation() {
return Optional.of(Path.of(System.getenv("LOCALAPPDATA"))
.resolve("Programs")
.resolve("Windsurf")
.resolve("bin")
.resolve("windsurf.cmd"));
}
};
// Cli is broken, keep inactive
WindowsType THEIAIDE_WINDOWS = new WindowsType("app.theiaide", "Theiaide", true) {
@Override
protected Optional<Path> determineInstallation() {
return Optional.of(Path.of(System.getenv("LOCALAPPDATA"))
.resolve("Programs")
.resolve("TheiaIDE")
.resolve("TheiaIDE.exe"));
}
};
WindowsType TRAE_WINDOWS = new WindowsType("app.trae", "trae.cmd", false) {
@Override
protected Optional<Path> determineInstallation() {
return Optional.of(Path.of(System.getenv("LOCALAPPDATA"))
.resolve("Programs")
.resolve("Trae")
.resolve("bin")
.resolve("trae.cmd"));
}
};
WindowsType VSCODE_WINDOWS = new WindowsType("app.vscode", "code.cmd", false) {
@Override
@ -88,6 +136,8 @@ public interface ExternalEditorType extends PrefsChoiceValue {
}
};
LinuxPathType WINDSURF_LINUX = new LinuxPathType("app.windsurf", "windsurf");
LinuxPathType ZED_LINUX = new LinuxPathType("app.zed", "zed");
ExternalEditorType ZED_MACOS = new MacOsEditor("app.zed", "Zed");
@ -110,6 +160,9 @@ public interface ExternalEditorType extends PrefsChoiceValue {
ExternalEditorType SUBLIME_MACOS = new MacOsEditor("app.sublime", "Sublime Text");
ExternalEditorType VSCODE_MACOS = new MacOsEditor("app.vscode", "Visual Studio Code");
ExternalEditorType VSCODIUM_MACOS = new MacOsEditor("app.vscodium", "VSCodium");
ExternalEditorType CURSOR_MACOS = new MacOsEditor("app.cursor", "Cursor");
ExternalEditorType WINDSURF_MACOS = new MacOsEditor("app.windsurf", "Windsurf");
ExternalEditorType TRAE_MACOS = new MacOsEditor("app.trae", "Trae");
ExternalEditorType CUSTOM = new ExternalEditorType() {
@Override
@ -119,10 +172,13 @@ public interface ExternalEditorType extends PrefsChoiceValue {
throw ErrorEvent.expected(new IllegalStateException("No custom editor command specified"));
}
var format =
customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE";
ExternalApplicationHelper.startAsync(CommandBuilder.of()
.add(ExternalApplicationHelper.replaceFileArgument(format, "FILE", file.toString())));
var format = customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE";
var command = CommandBuilder.of().add(ExternalApplicationHelper.replaceFileArgument(format, "FILE", file.toString()));
if (AppPrefs.get().customEditorCommandInTerminal().get()) {
TerminalLauncher.openDirect(file.toString(), sc -> command.buildFull(sc), AppPrefs.get().terminalType.get());
} else {
ExternalApplicationHelper.startAsync(command);
}
}
@Override
@ -136,11 +192,11 @@ public interface ExternalEditorType extends PrefsChoiceValue {
ExternalEditorType WEBSTORM = new GenericPathType("app.webstorm", "webstorm", false);
ExternalEditorType CLION = new GenericPathType("app.clion", "clion", false);
List<ExternalEditorType> WINDOWS_EDITORS =
List.of(VSCODIUM_WINDOWS, VSCODE_INSIDERS_WINDOWS, VSCODE_WINDOWS, NOTEPADPLUSPLUS, NOTEPAD);
List.of(CURSOR_WINDOWS, WINDSURF_WINDOWS, TRAE_WINDOWS, VSCODIUM_WINDOWS, VSCODE_INSIDERS_WINDOWS, VSCODE_WINDOWS, NOTEPADPLUSPLUS, NOTEPAD);
List<LinuxPathType> LINUX_EDITORS =
List.of(VSCODIUM_LINUX, VSCODE_LINUX, ZED_LINUX, KATE, GEDIT, PLUMA, LEAFPAD, MOUSEPAD, GNOME);
List.of(ExternalEditorType.WINDSURF_LINUX, VSCODIUM_LINUX, VSCODE_LINUX, ZED_LINUX, KATE, GEDIT, PLUMA, LEAFPAD, MOUSEPAD, GNOME);
List<ExternalEditorType> MACOS_EDITORS =
List.of(BBEDIT, VSCODIUM_MACOS, VSCODE_MACOS, SUBLIME_MACOS, ZED_MACOS, TEXT_EDIT);
List.of(CURSOR_MACOS, WINDSURF_MACOS, TRAE_MACOS, BBEDIT, VSCODIUM_MACOS, VSCODE_MACOS, SUBLIME_MACOS, ZED_MACOS, TEXT_EDIT);
List<ExternalEditorType> CROSS_PLATFORM_EDITORS = List.of(FLEET, INTELLIJ, PYCHARM, WEBSTORM, CLION);
@SuppressWarnings("TrivialFunctionalExpressionUsage")

View file

@ -108,7 +108,7 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
ThreadHelper.runFailableAsync(() -> {
// Startup is slow
ThreadHelper.sleep(10000);
Files.delete(config);
FileUtils.deleteQuietly(config.toFile());
});
}
@ -125,8 +125,12 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
@Override
public void launch(LaunchConfiguration configuration) throws Exception {
var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig());
var escapedPw = configuration.getPassword().getSecretValue().replaceAll("'", "\\\\'");
launch(configuration.getTitle(), CommandBuilder.of().addFile(file.toString()).add("/cert-ignore").add("/p:'" + escapedPw + "'"));
var b = CommandBuilder.of().addFile(file.toString()).add("/cert-ignore");
if (configuration.getPassword() != null) {
var escapedPw = configuration.getPassword().getSecretValue().replaceAll("'", "\\\\'");
b.add("/p:'" + escapedPw + "'");
}
launch(configuration.getTitle(), b);
}
@Override

View file

@ -15,6 +15,7 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.scene.control.TextField;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
@ -111,8 +112,13 @@ public class IconsCategory extends AppPrefsCategory {
return;
}
var path = Path.of(dir.get());
if (Files.isRegularFile(path)) {
throw new IllegalArgumentException("A custom icon directory requires to be a directory of .svg files, not a single file");
}
var source = SystemIconSource.Directory.builder()
.path(Path.of(dir.get()))
.path(path)
.id(UUID.randomUUID().toString())
.build();
if (!sources.contains(source)) {

View file

@ -7,6 +7,7 @@ import io.xpipe.app.comp.base.IntegratedTextAreaComp;
import io.xpipe.app.comp.base.LabelComp;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.util.BindingsHelper;
@ -115,6 +116,7 @@ public class PasswordManagerCategory extends AppPrefsCategory {
.minHeight(120);
var templates = Comp.of(() -> {
var cb = new MenuButton();
AppFontSizes.base(cb);
cb.textProperty().bind(BindingsHelper.flatMap(prefs.passwordManager, externalPasswordManager -> {
return externalPasswordManager != null
? AppI18n.observable(externalPasswordManager.getId())
@ -145,6 +147,7 @@ public class PasswordManagerCategory extends AppPrefsCategory {
new TextFieldComp(testPasswordManagerValue)
.apply(struc -> struc.get().setPromptText("Enter password key"))
.styleClass(Styles.LEFT_PILL)
.prefWidth(400)
.apply(struc -> struc.get().setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ENTER) {
test.run();
@ -153,14 +156,17 @@ public class PasswordManagerCategory extends AppPrefsCategory {
})),
new ButtonComp(null, new FontIcon("mdi2p-play"), test).styleClass(Styles.RIGHT_PILL)));
testInput.apply(struc -> {
struc.get().setFillHeight(true);
var first = ((Region) struc.get().getChildren().get(0));
var second = ((Region) struc.get().getChildren().get(1));
second.minHeightProperty().bind(first.heightProperty());
second.maxHeightProperty().bind(first.heightProperty());
second.prefHeightProperty().bind(first.heightProperty());
});
var testPasswordManager = new HorizontalComp(List.of(
testInput, Comp.hspacer(25), new LabelComp(testPasswordManagerResult).apply(struc -> struc.get()
.setOpacity(0.5))))
.setOpacity(0.8))))
.padding(new Insets(10, 0, 0, 0))
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
.apply(struc -> struc.get().setFillHeight(true));

View file

@ -5,6 +5,7 @@ import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.storage.DataStorageSyncHandler;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.app.util.ThreadHelper;
@ -27,14 +28,6 @@ public class SyncCategory extends AppPrefsCategory {
return "vaultSync";
}
private static void showHelpAlert() {
var md = AppI18n.get().getMarkdownDocumentation("vault");
var markdown = new MarkdownComp(md, s -> s, true).prefWidth(600);
var modal = ModalOverlay.of(markdown);
modal.addButton(ModalButton.ok());
AppDialog.show(modal);
}
public Comp<?> create() {
var prefs = AppPrefs.get();
AtomicReference<Region> button = new AtomicReference<>();
@ -61,7 +54,7 @@ public class SyncCategory extends AppPrefsCategory {
var remoteRepo = new TextFieldComp(prefs.storageGitRemote).hgrow();
var helpButton = new ButtonComp(AppI18n.observable("help"), new FontIcon("mdi2h-help-circle-outline"), () -> {
showHelpAlert();
Hyperlinks.open(Hyperlinks.DOCS_SYNC);
});
var remoteRow = new HorizontalComp(List.of(remoteRepo, helpButton)).spacing(10);
remoteRow.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));

View file

@ -15,6 +15,7 @@ import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.app.util.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@ -58,6 +59,10 @@ public class TerminalCategory extends AppPrefsCategory {
var feature = LicenseProvider.get().getFeature("logging");
if (newValue && !feature.isSupported()) {
try {
// Disable it again so people don't forget that they left it on
Platform.runLater(() -> {
prefs.enableTerminalLogging.set(false);
});
feature.throwIfUnsupported();
} catch (LicenseRequiredException ex) {
ErrorEvent.fromThrowable(ex).handle();

View file

@ -54,7 +54,7 @@ public class UpdateCheckComp extends SimpleComp {
}
if (updateReady.getValue()) {
var prefix = AppDistributionType.get() == AppDistributionType.PORTABLE
var prefix = !AppDistributionType.get().getUpdateHandler().supportsDirectInstallation()
? AppI18n.get("updateReadyPortable")
: AppI18n.get("updateReady");
var version = "Version "

View file

@ -451,6 +451,7 @@ public abstract class DataStorage {
newChildren = l.stream()
.filter(dataStoreEntryRef -> dataStoreEntryRef != null && dataStoreEntryRef.get() != null)
.toList();
e.getProvider().onChildrenRefresh(e);
} else {
newChildren = null;
}
@ -518,14 +519,35 @@ public abstract class DataStorage {
.toList());
toUpdate.removeIf(pair -> {
if (pair.getKey().getStorePersistentState() != null
&& pair.getValue().get().getStorePersistentState() != null) {
return pair.getKey()
.getStorePersistentState()
.equals(pair.getValue().get().getStorePersistentState());
} else {
// Children classes might not be the same, the same goes for state classes
// This can happen when there are multiple child classes and the ids got switched around
var storeClassMatch = pair.getKey()
.getStore()
.getClass()
.equals(pair.getValue().get().getStore().getClass());
if (!storeClassMatch) {
return true;
}
DataStore merged = ((FixedChildStore) pair.getKey().getStore())
.merge(pair.getValue().getStore().asNeeded());
var mergedStoreChanged = pair.getKey().getStore() != merged;
if (pair.getKey().getStorePersistentState() == null || pair.getValue().get().getStorePersistentState() == null) {
return !mergedStoreChanged;
}
var stateClassMatch = pair.getKey()
.getStorePersistentState()
.getClass()
.equals(pair.getValue().get().getStorePersistentState().getClass());
if (!stateClassMatch) {
return true;
}
var stateChange = !pair.getKey()
.getStorePersistentState()
.equals(pair.getValue().get().getStorePersistentState());
return !mergedStoreChanged && !stateChange;
});
if (toRemove.isEmpty() && toAdd.isEmpty() && toUpdate.isEmpty()) {
@ -547,31 +569,18 @@ public abstract class DataStorage {
}
addStoreEntriesIfNotPresent(toAdd.stream().map(DataStoreEntryRef::get).toArray(DataStoreEntry[]::new));
toUpdate.forEach(pair -> {
// Update state by merging
if (pair.getKey().getStorePersistentState() != null
&& pair.getValue().get().getStorePersistentState() != null) {
var classMatch = pair.getKey()
.getStorePersistentState()
.getClass()
.equals(pair.getValue().get().getStorePersistentState().getClass());
// Children classes might not be the same, the same goes for state classes
// This can happen when there are multiple child classes and the ids got switched around
if (classMatch) {
DataStore merged = ((FixedChildStore) pair.getKey().getStore())
.merge(pair.getValue().getStore().asNeeded());
if (merged != pair.getKey().getStore()) {
pair.getKey().setStoreInternal(merged, false);
}
var s = pair.getKey().getStorePersistentState();
var mergedState = s.mergeCopy(pair.getValue().get().getStorePersistentState());
pair.getKey().setStorePersistentState(mergedState);
}
DataStore merged = ((FixedChildStore) pair.getKey().getStore())
.merge(pair.getValue().getStore().asNeeded());
if (merged != pair.getKey().getStore()) {
pair.getKey().setStoreInternal(merged, false);
}
var s = pair.getKey().getStorePersistentState();
var mergedState = s.mergeCopy(pair.getValue().get().getStorePersistentState());
pair.getKey().setStorePersistentState(mergedState);
});
refreshEntries();
saveAsync();
e.getProvider().onChildrenRefresh(e);
toAdd.forEach(
dataStoreEntryRef -> dataStoreEntryRef.get().getProvider().onParentRefresh(dataStoreEntryRef.get()));
toUpdate.forEach(dataStoreEntryRef ->
@ -591,25 +600,6 @@ public abstract class DataStorage {
}
}
public void deleteChildren(DataStoreEntry e) {
var c = getDeepStoreChildren(e);
if (c.isEmpty()) {
return;
}
c.forEach(entry -> entry.finalizeEntry());
this.storeEntriesSet.removeAll(c);
synchronized (identityStoreEntryMapCache) {
identityStoreEntryMapCache.remove(e.getStore());
}
synchronized (storeEntryMapCache) {
storeEntryMapCache.remove(e.getStore());
}
this.listeners.forEach(l -> l.onStoreRemove(c.toArray(DataStoreEntry[]::new)));
refreshEntries();
saveAsync();
}
public void deleteWithChildren(DataStoreEntry... entries) {
List<DataStoreEntry> toDelete = Arrays.stream(entries)
.flatMap(entry -> {

View file

@ -0,0 +1,152 @@
package io.xpipe.app.storage;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.beacon.api.ConnectionQueryExchange;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class DataStorageQuery {
public static List<DataStoreEntry> queryUserInput(String connection) {
var found = query("**", "**" + connection + "*", "*");
if (found.size() > 1) {
var narrow = found.stream().filter(dataStoreEntry -> dataStoreEntry.getName().equalsIgnoreCase(connection)).toList();
if (narrow.size() == 1) {
return narrow;
}
}
return found;
}
public static List<DataStoreEntry> query(String categoryFilter, String connectionFilter, String typeFilter) {
if (DataStorage.get() == null) {
return List.of();
}
var catMatcher = Pattern.compile(
toRegex("all connections/" + categoryFilter.toLowerCase()));
var conMatcher = Pattern.compile(toRegex(connectionFilter.toLowerCase()));
var typeMatcher = Pattern.compile(toRegex(typeFilter.toLowerCase()));
List<DataStoreEntry> found = new ArrayList<>();
for (DataStoreEntry storeEntry : DataStorage.get().getStoreEntries()) {
if (!storeEntry.getValidity().isUsable()) {
continue;
}
var name = DataStorage.get().getStorePath(storeEntry).toString();
if (!conMatcher.matcher(name).matches()) {
continue;
}
var cat = DataStorage.get()
.getStoreCategoryIfPresent(storeEntry.getCategoryUuid())
.orElse(null);
if (cat == null) {
continue;
}
var c = DataStorage.get().getStorePath(cat).toString();
if (!catMatcher.matcher(c).matches()) {
continue;
}
if (!typeMatcher
.matcher(storeEntry.getProvider().getId().toLowerCase())
.matches()) {
continue;
}
found.add(storeEntry);
}
return found;
}
private static String toRegex(String pattern) {
pattern = pattern.replaceAll("\\*\\*", "#");
// https://stackoverflow.com/a/17369948/6477761
StringBuilder sb = new StringBuilder(pattern.length());
int inGroup = 0;
int inClass = 0;
int firstIndexInClass = -1;
char[] arr = pattern.toCharArray();
for (int i = 0; i < arr.length; i++) {
char ch = arr[i];
switch (ch) {
case '\\':
if (++i >= arr.length) {
sb.append('\\');
} else {
char next = arr[i];
switch (next) {
case ',':
// escape not needed
break;
case 'Q':
case 'E':
// extra escape needed
sb.append('\\');
default:
sb.append('\\');
}
sb.append(next);
}
break;
case '*':
if (inClass == 0) sb.append("[^/]*");
else sb.append('*');
break;
case '#':
if (inClass == 0) sb.append(".*");
else sb.append('*');
break;
case '?':
if (inClass == 0) sb.append('.');
else sb.append('?');
break;
case '[':
inClass++;
firstIndexInClass = i + 1;
sb.append('[');
break;
case ']':
inClass--;
sb.append(']');
break;
case '.':
case '(':
case ')':
case '+':
case '|':
case '^':
case '$':
case '@':
case '%':
if (inClass == 0 || (firstIndexInClass == i && ch == '^')) sb.append('\\');
sb.append(ch);
break;
case '!':
if (firstIndexInClass == i) sb.append('^');
else sb.append('!');
break;
case '{':
inGroup++;
sb.append('(');
break;
case '}':
inGroup--;
sb.append(')');
break;
case ',':
if (inGroup > 0) sb.append('|');
else sb.append(',');
break;
default:
sb.append(ch);
}
}
return sb.toString();
}
}

View file

@ -34,7 +34,10 @@ public class DataStoreEntryRef<T extends DataStore> {
}
public void checkComplete() throws Throwable {
getStore().checkComplete();
var store = getStore();
if (store != null) {
getStore().checkComplete();
}
}
public DataStoreEntry get() {
@ -42,7 +45,7 @@ public class DataStoreEntryRef<T extends DataStore> {
}
public T getStore() {
return entry.getStore().asNeeded();
return entry.getStore() != null ? entry.getStore().asNeeded() : null;
}
@SuppressWarnings("unchecked")

View file

@ -59,14 +59,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
// };
static ExternalTerminalType determineFallbackTerminalToOpen(ExternalTerminalType type) {
if (type == XSHELL || type == MOBAXTERM || type == SECURECRT) {
return ProcessControlProvider.get().getEffectiveLocalDialect() == ShellDialects.CMD ? CMD : POWERSHELL;
}
if (type != TERMIUS && type instanceof WaveTerminalType) {
if (type != XSHELL && type != MOBAXTERM && type != SECURECRT && type != TERMIUS && !(type instanceof WaveTerminalType)) {
return type;
}
// Fallback to an available default
switch (OsType.getLocal()) {
case OsType.Linux linux -> {
// This should not be termius or wave as all others take precedence
@ -414,6 +411,34 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
return CommandBuilder.of().add("-c").addFile(configuration.getScriptFile());
}
};
ExternalTerminalType COSMIC_TERM = new SimplePathType("app.cosmicTerm", "cosmic-term", true) {
@Override
public String getWebsite() {
return "https://github.com/pop-os/cosmic-term";
}
@Override
public TerminalOpenFormat getOpenFormat() {
return TerminalOpenFormat.NEW_WINDOW;
}
@Override
public boolean isRecommended() {
return false;
}
@Override
public boolean useColoredTitle() {
return true;
}
@Override
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
return CommandBuilder.of()
.add("-e")
.addFile(configuration.getScriptFile());
}
};
ExternalTerminalType UXTERM = new SimplePathType("app.uxterm", "uxterm", true) {
@Override
public String getWebsite() {
@ -615,7 +640,6 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
.addFile(configuration.getScriptFile()));
}
};
ExternalTerminalType WARP = new WarpTerminalType();
ExternalTerminalType CUSTOM = new CustomTerminalType();
List<ExternalTerminalType> WINDOWS_TERMINALS = List.of(
WindowsTerminalType.WINDOWS_TERMINAL_CANARY,
@ -623,6 +647,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
WindowsTerminalType.WINDOWS_TERMINAL,
AlacrittyTerminalType.ALACRITTY_WINDOWS,
WezTerminalType.WEZTERM_WINDOWS,
WarpTerminalType.WINDOWS,
CMD,
PWSH,
POWERSHELL,
@ -648,15 +673,17 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
TILIX,
GUAKE,
TILDA,
COSMIC_TERM,
UXTERM,
XTERM,
DEEPIN_TERMINAL,
FOOT,
Q_TERMINAL,
WarpTerminalType.LINUX,
TERMIUS,
WaveTerminalType.WAVE_LINUX);
List<ExternalTerminalType> MACOS_TERMINALS = List.of(
WARP,
WarpTerminalType.MACOS,
ITERM2,
KittyTerminalType.KITTY_MACOS,
TabbyTerminalType.TABBY_MAC_OS,

View file

@ -1,14 +1,19 @@
package io.xpipe.app.terminal;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.SecretManager;
import io.xpipe.app.util.SecretQueryProgress;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.core.process.ProcessControl;
import io.xpipe.core.process.TerminalInitScriptConfig;
import io.xpipe.core.process.*;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.SequencedMap;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.CountDownLatch;
public class TerminalLauncherManager {
@ -73,6 +78,9 @@ public class TerminalLauncherManager {
synchronized (entries) {
req = entries.get(request);
}
if (req == null) {
return;
}
var byPid = ProcessHandle.of(pid);
if (byPid.isEmpty()) {
throw new BeaconClientException("Unable to find terminal child process " + pid);
@ -84,33 +92,44 @@ public class TerminalLauncherManager {
req.setPid(shell.pid());
}
public static Path waitExchange(UUID request) throws BeaconClientException, BeaconServerException {
public static void waitExchange(UUID request) throws BeaconClientException, BeaconServerException {
TerminalLaunchRequest req;
synchronized (entries) {
req = entries.get(request);
}
if (req == null) {
throw new BeaconClientException("Unknown launch request " + request);
return;
}
if (req.isSetupCompleted()) {
submitAsync(req.getRequest(), req.getProcessControl(), req.getConfig(), req.getWorkingDirectory());
}
try {
return req.waitForCompletion();
req.waitForCompletion();
} finally {
req.setSetupCompleted(true);
}
}
public static Path launchExchange(UUID request) throws BeaconClientException {
public static Path launchExchange(UUID request) throws BeaconClientException, BeaconServerException {
synchronized (entries) {
var e = entries.values().stream()
.filter(entry -> entry.getRequest().equals(request))
.findFirst()
.orElse(null);
if (e == null) {
throw new BeaconClientException("Unknown launch request " + request);
// It seems like that some terminals might enter a restart loop to try to start an older process again
// This would spam XPipe continuously with launch requests if we returned an error here
// Therefore, we just return a new local shell session
TrackEvent.withTrace("Unknown launch request").tag("request", request.toString()).handle();
try (var sc = LocalShell.getShell().start()) {
var defaultShell = ProcessControlProvider.get().getEffectiveLocalDialect();
var shellExec = defaultShell.getExecutableName();
var script = ScriptHelper.createExecScript(sc, shellExec);
return Path.of(script.toString());
} catch (Exception ex) {
throw new BeaconServerException(ex);
}
}
if (!(e.getResult() instanceof TerminalLaunchResult.ResultSuccess)) {
@ -120,4 +139,41 @@ public class TerminalLauncherManager {
return ((TerminalLaunchResult.ResultSuccess) e.getResult()).getTargetScript();
}
}
public static List<String> externalExchange(DataStoreEntryRef<ShellStore> ref, List<String> arguments) throws BeaconClientException, BeaconServerException {
var request = UUID.randomUUID();
ShellControl session;
try {
session = ref.getStore().getOrStartSession();
} catch (Exception e) {
throw new BeaconServerException(e);
}
ProcessControl control;
if (arguments.size() > 0) {
control = session.command(CommandBuilder.of().addAll(arguments));
} else {
control = session;
}
var config = new TerminalInitScriptConfig(ref.get().getName(), false, TerminalInitFunction.none());
submitAsync(request, control, config, null);
waitExchange(request);
var script = launchExchange(request);
try (var sc = LocalShell.getShell().start()) {
var runCommand = ProcessControlProvider.get().getEffectiveLocalDialect().getOpenScriptCommand(script.toString()).buildBaseParts(sc);
var cleaned = runCommand.stream().map(s -> {
if (s.startsWith("\"") && s.endsWith("\"")) {
s = s.substring(1, s.length() - 1);
} else if (s.startsWith("'") && s.endsWith("'")) {
s = s.substring(1, s.length() - 1);
}
return s;
}).toList();
return cleaned;
} catch (Exception e) {
throw new BeaconServerException(e);
}
}
}

View file

@ -1,57 +1,140 @@
package io.xpipe.app.terminal;
import io.xpipe.app.prefs.ExternalApplicationHelper;
import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.WindowsRegistry;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.process.TerminalInitFunction;
public class WarpTerminalType extends ExternalTerminalType.MacOsType {
import java.nio.file.Files;
import java.nio.file.Path;
public WarpTerminalType() {
super("app.warp", "Warp");
public interface WarpTerminalType extends ExternalTerminalType, TrackableTerminalType {
static WarpTerminalType WINDOWS = new Windows();
static WarpTerminalType LINUX = new Linux();
static WarpTerminalType MACOS = new MacOs();
class Windows implements WarpTerminalType {
@Override
public int getProcessHierarchyOffset() {
return 1;
}
@Override
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
if (!configuration.isPreferTabs()) {
DesktopHelper.openUrl("warp://action/new_window?path=" + configuration.getScriptFile());
} else {
DesktopHelper.openUrl("warp://action/new_tab?path=" + configuration.getScriptFile());
}
}
@Override
public boolean isAvailable() {
return WindowsRegistry.local().keyExists(WindowsRegistry.HKEY_CURRENT_USER, "Software\\Classes\\warp");
}
@Override
public String getId() {
return "app.warp";
}
@Override
public TerminalOpenFormat getOpenFormat() {
// Warp always opens the new separate window, so we don't want to use it in the file browser for docking
// Just say that we don't support new windows, that way it doesn't dock
return TerminalOpenFormat.TABBED;
}
}
class Linux implements WarpTerminalType {
@Override
public int getProcessHierarchyOffset() {
return 2;
}
@Override
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
if (!configuration.isPreferTabs()) {
DesktopHelper.openUrl("warp://action/new_window?path=" + configuration.getScriptFile());
} else {
DesktopHelper.openUrl("warp://action/new_tab?path=" + configuration.getScriptFile());
}
}
@Override
public boolean isAvailable() {
return Files.exists(Path.of("/opt/warpdotdev"));
}
@Override
public String getId() {
return "app.warp";
}
@Override
public TerminalOpenFormat getOpenFormat() {
// Warp always opens the new separate window, so we don't want to use it in the file browser for docking
// Just say that we don't support new windows, that way it doesn't dock
return TerminalOpenFormat.TABBED;
}
}
class MacOs extends MacOsType implements WarpTerminalType {
public MacOs() {
super("app.warp", "Warp");
}
@Override
public int getProcessHierarchyOffset() {
return 2;
}
@Override
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
LocalShell.getShell()
.executeSimpleCommand(CommandBuilder.of()
.add("open", "-a")
.addQuoted("Warp.app")
.addFile(configuration.getScriptFile()));
}
@Override
public TerminalOpenFormat getOpenFormat() {
return TerminalOpenFormat.TABBED;
}
}
@Override
public TerminalOpenFormat getOpenFormat() {
return TerminalOpenFormat.TABBED;
}
@Override
public int getProcessHierarchyOffset() {
return 2;
}
@Override
public String getWebsite() {
default String getWebsite() {
return "https://www.warp.dev/";
}
@Override
public boolean isRecommended() {
default boolean isRecommended() {
return true;
}
@Override
public boolean useColoredTitle() {
default boolean useColoredTitle() {
return true;
}
@Override
public boolean shouldClear() {
default boolean shouldClear() {
return false;
}
@Override
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
LocalShell.getShell()
.executeSimpleCommand(CommandBuilder.of()
.add("open", "-a")
.addQuoted("Warp.app")
.addFile(configuration.getScriptFile()));
}
@Override
public TerminalInitFunction additionalInitCommands() {
default TerminalInitFunction additionalInitCommands() {
return TerminalInitFunction.of(sc -> {
if (sc.getShellDialect() == ShellDialects.ZSH) {
return "printf '\\eP$f{\"hook\": \"SourcedRcFileForWarp\", \"value\": { \"shell\": \"zsh\"}}\\x9c'";

View file

@ -20,6 +20,11 @@ public class GitHubUpdater extends UpdateHandler {
super(startBackgroundThread);
}
@Override
public boolean supportsDirectInstallation() {
return true;
}
@Override
public List<ModalButton> createActions() {
var list = new ArrayList<ModalButton>();

View file

@ -17,6 +17,11 @@ public class PortableUpdater extends UpdateHandler {
super(thread);
}
@Override
public boolean supportsDirectInstallation() {
return false;
}
@Override
public List<ModalButton> createActions() {
var list = new ArrayList<ModalButton>();

View file

@ -4,6 +4,7 @@ import io.xpipe.app.comp.base.ModalButton;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppDistributionType;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
@ -104,9 +105,10 @@ public abstract class UpdateHandler {
var checked = false;
ThreadHelper.sleep(Duration.ofMinutes(1).toMillis());
event("Starting background updater thread");
var run = !AppProperties.get().isRestarted();
while (true) {
if (AppPrefs.get().automaticallyUpdate().get()
|| AppPrefs.get().checkForSecurityUpdates().get()) {
if (run && (AppPrefs.get().automaticallyUpdate().get()
|| AppPrefs.get().checkForSecurityUpdates().get())) {
event("Performing background update");
refreshUpdateCheckSilent(
!checked,
@ -116,6 +118,7 @@ public abstract class UpdateHandler {
}
ThreadHelper.sleep(Duration.ofHours(1).toMillis());
run = true;
}
})
.start();
@ -141,6 +144,8 @@ public abstract class UpdateHandler {
return false;
}
public abstract boolean supportsDirectInstallation();
public final AvailableRelease refreshUpdateCheckSilent(boolean first, boolean securityOnly) {
try {
return refreshUpdateCheck(first, securityOnly);
@ -178,7 +183,7 @@ public abstract class UpdateHandler {
prepareUpdateImpl();
// Show available update in PTB more aggressively
if (AppProperties.get().isStaging() && preparedUpdate.getValue() != null) {
if (AppProperties.get().isStaging() && preparedUpdate.getValue() != null && !OperationMode.isInStartup()) {
UpdateAvailableDialog.showIfNeeded();
}
}

View file

@ -3,7 +3,6 @@ package io.xpipe.app.util;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.process.CountDown;
import io.xpipe.core.process.ElevationHandler;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.SecretReference;
@ -21,7 +20,7 @@ public class BaseElevationHandler implements ElevationHandler {
}
@Override
public boolean handleRequest(ShellControl parent, UUID requestId, CountDown countDown, boolean confirmIfNeeded) {
public boolean handleRequest(UUID requestId, CountDown countDown, boolean confirmIfNeeded, boolean interactive) {
var ref = getSecretRef();
if (ref == null) {
return false;
@ -35,7 +34,7 @@ public class BaseElevationHandler implements ElevationHandler {
List.of(),
List.of(),
countDown,
parent.isInteractive());
interactive);
return true;
}

View file

@ -8,6 +8,7 @@ import lombok.Value;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
@ -15,12 +16,14 @@ import java.util.function.Function;
@SuppressWarnings("InfiniteLoopStatement")
public class BindingsHelper {
private static final Set<ReferenceEntry> REFERENCES = Collections.newSetFromMap(new ConcurrentHashMap<>());
private static final Set<ReferenceEntry> REFERENCES = new HashSet<>();
static {
ThreadHelper.createPlatformThread("referenceGC", true, () -> {
while (true) {
REFERENCES.removeIf(ReferenceEntry::canGc);
synchronized (REFERENCES) {
REFERENCES.removeIf(ReferenceEntry::canGc);
}
ThreadHelper.sleep(1000);
// Use for testing
@ -31,7 +34,9 @@ public class BindingsHelper {
}
public static void preserve(Object source, Object target) {
REFERENCES.add(new ReferenceEntry(new WeakReference<>(source), target));
synchronized (REFERENCES) {
REFERENCES.add(new ReferenceEntry(new WeakReference<>(source), target));
}
}
public static <T, U> ObservableValue<U> map(

View file

@ -30,7 +30,10 @@ public class ContextMenuHelper {
Platform.runLater(() -> {
var first = contextMenu.getItems().getFirst();
if (first != null) {
first.getStyleableNode().requestFocus();
var s = first.getStyleableNode();
if (s != null) {
s.requestFocus();
}
}
});
});

View file

@ -85,9 +85,19 @@ public class DerivedObservableList<T> {
target.setAll(newList);
}
private int indexOfFromStart(List<? extends T> list, T value, int start) {
for (int i = start; i < list.size(); i++) {
if (Objects.equals(list.get(i), value)) {
return i;
}
}
return -1;
}
private void setContentUnique(List<? extends T> newList) {
var listSet = new HashSet<>(list);
var newSet = new HashSet<>(newList);
// Addition
if (newSet.containsAll(list)) {
var l = new ArrayList<>(newList);
@ -100,7 +110,7 @@ public class DerivedObservableList<T> {
var start = 0;
for (int end = 0; end <= list.size(); end++) {
var index = end < list.size() ? newList.indexOf(list.get(end)) : newList.size();
var index = end < list.size() ? indexOfFromStart(newList, list.get(end), end) : newList.size();
for (; start < index; start++) {
list.add(start, newList.get(start));
}
@ -133,7 +143,8 @@ public class DerivedObservableList<T> {
var cache = new HashMap<T, V>();
var l1 = this.<V>createNewDerived();
Runnable runnable = () -> {
cache.keySet().removeIf(t -> !getList().contains(t));
var listSet = new HashSet<>(list);
cache.keySet().removeIf(t -> !listSet.contains(t));
l1.setContent(list.stream()
.map(v -> {
if (!cache.containsKey(v)) {

View file

@ -1,6 +1,8 @@
package io.xpipe.app.util;
import io.xpipe.app.core.AppDistributionType;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
@ -8,11 +10,48 @@ import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FilePath;
import java.awt.*;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
public class DesktopHelper {
private static final String[] browsers = {
"xdg-open", "google-chrome", "firefox", "opera", "konqueror", "mozilla", "gnome-open", "open"
};
public static void openUrl(String uri) {
try {
if (OsType.getLocal() == OsType.WINDOWS) {
var pb = new ProcessBuilder("rundll32", "url.dll,FileProtocolHandler", uri);
pb.directory(new File(System.getProperty("user.home")));
pb.redirectErrorStream(true);
pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
pb.start();
} else if (OsType.getLocal() == OsType.LINUX) {
String browser = null;
for (String b : browsers) {
if (browser == null
&& Runtime.getRuntime()
.exec(new String[] {"which", b})
.getInputStream()
.read()
!= -1) {
Runtime.getRuntime().exec(new String[] {browser = b, uri});
}
}
} else {
var pb = new ProcessBuilder("open", uri);
pb.directory(new File(System.getProperty("user.home")));
pb.redirectErrorStream(true);
pb.redirectOutput(ProcessBuilder.Redirect.DISCARD);
pb.start();
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
}
public static Path getDesktopDirectory() throws Exception {
if (OsType.getLocal() == OsType.WINDOWS) {
return Path.of(LocalShell.getLocalPowershell()
@ -90,47 +129,46 @@ public class DesktopHelper {
return;
}
if (!Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
return;
}
if (!Files.exists(file)) {
return;
}
ThreadHelper.runAsync(() -> {
try {
Desktop.getDesktop().open(file.toFile());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).expected().handle();
var xdg = OsType.getLocal() == OsType.LINUX;
if (Desktop.getDesktop().isSupported(Desktop.Action.OPEN) && AppDistributionType.get() != AppDistributionType.WEBTOP) {
try {
Desktop.getDesktop().open(file.toFile());
return;
} catch (Exception e) {
ErrorEvent.fromThrowable(e).expected().omitted(xdg).handle();
}
}
if (xdg) {
LocalExec.readStdoutIfPossible("xdg-open", file.toString());
}
});
}
public static void browseFileInDirectory(Path file) {
if (!Desktop.getDesktop().isSupported(Desktop.Action.BROWSE_FILE_DIR)) {
if (!Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
ErrorEvent.fromMessage("Desktop integration unable to open file " + file)
.expected()
.handle();
return;
}
ThreadHelper.runAsync(() -> {
try {
Desktop.getDesktop().open(file.getParent().toFile());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).expected().handle();
}
});
browsePathLocal(file.getParent());
return;
}
ThreadHelper.runAsync(() -> {
try {
Desktop.getDesktop().browseFileDirectory(file.toFile());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).expected().handle();
var xdg = OsType.getLocal() == OsType.LINUX;
if (AppDistributionType.get() != AppDistributionType.WEBTOP) {
try {
Desktop.getDesktop().browseFileDirectory(file.toFile());
return;
} catch (Exception e) {
ErrorEvent.fromThrowable(e).expected().omitted(xdg).handle();
}
}
if (xdg) {
LocalExec.readStdoutIfPossible("xdg-open", file.getParent().toString());
}
});
}

View file

@ -18,6 +18,11 @@ import javax.crypto.SecretKey;
public class EncryptionToken {
private static EncryptionToken vaultToken;
private static EncryptionToken userToken;
public static void invalidateUserToken() {
userToken = null;
}
private static EncryptionToken createUserToken() {
var userHandler = DataStorageUserHandler.getInstance();
@ -39,12 +44,15 @@ public class EncryptionToken {
}
public static EncryptionToken ofUser() {
var userHandler = DataStorageUserHandler.getInstance();
if (userHandler.getActiveUser() == null) {
throw new IllegalStateException("No active user available");
}
if (userToken == null) {
var userHandler = DataStorageUserHandler.getInstance();
if (userHandler.getActiveUser() == null) {
throw new IllegalStateException("No active user available");
}
return createUserToken();
userToken = createUserToken();
}
return userToken;
}
public static EncryptionToken ofVaultKey() {
@ -59,6 +67,12 @@ public class EncryptionToken {
@JsonIgnore
private Boolean isVault;
@JsonIgnore
private Boolean isUser;
@JsonIgnore
private EncryptionToken usedUserToken;
public boolean canDecrypt() {
return isVault() || isUser();
}
@ -79,7 +93,13 @@ public class EncryptionToken {
return false;
}
return userHandler.getActiveUser().equals(decode(userHandler.getEncryptionKey()));
if (userToken == EncryptionToken.ofUser() && isUser != null) {
return isUser;
}
usedUserToken = ofUser();
isUser = userHandler.getActiveUser().equals(decode(userHandler.getEncryptionKey()));
return isUser;
}
public boolean isVault() {

View file

@ -1,6 +1,9 @@
package io.xpipe.app.util;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.process.OsType;
import java.io.File;
public class Hyperlinks {
@ -13,6 +16,7 @@ public class Hyperlinks {
public static final String DOCS_EULA = "https://docs.xpipe.io/legal/end-user-license-agreement";
public static final String DOCS_SECURITY = "https://docs.xpipe.io/reference/security";
public static final String DOCS_WEBTOP_UPDATE = "https://docs.xpipe.io/guide/webtop#updating";
public static final String DOCS_SYNC = "https://docs.xpipe.io/guide/sync";
public static final String GITHUB = "https://github.com/xpipe-io/xpipe";
public static final String GITHUB_PTB = "https://github.com/xpipe-io/xpipe-ptb";
@ -24,38 +28,7 @@ public class Hyperlinks {
public static final String SLACK =
"https://join.slack.com/t/XPipe/shared_invite/zt-1awjq0t5j-5i4UjNJfNe1VN4b_auu6Cg";
static final String[] browsers = {
"xdg-open", "google-chrome", "firefox", "opera", "konqueror", "mozilla", "gnome-open", "open"
};
@SuppressWarnings("deprecation")
public static void open(String uri) {
String osName = System.getProperty("os.name");
try {
if (osName.startsWith("Mac OS")) {
Runtime.getRuntime().exec("open " + uri);
} else if (osName.startsWith("Windows")) {
Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + uri);
} else { // assume Unix or Linux
String browser = null;
for (String b : browsers) {
if (browser == null
&& Runtime.getRuntime()
.exec(new String[] {"which", b})
.getInputStream()
.read()
!= -1) {
Runtime.getRuntime().exec(new String[] {browser = b, uri});
}
}
if (browser == null) {
throw new Exception("No web browser or URL opener found to open " + uri);
}
}
} catch (Exception e) {
// should not happen
// dump stack for debug purpose
ErrorEvent.fromThrowable(e).handle();
}
DesktopHelper.openUrl(uri);
}
}

View file

@ -20,7 +20,7 @@ public class LocalExec {
if (process.exitValue() != 0) {
return Optional.empty();
} else {
var s = new String(out, StandardCharsets.UTF_8);
var s = new String(out, StandardCharsets.UTF_8).trim();
TrackEvent.withTrace("Local command finished")
.tag("command", String.join(" ", command))
.tag("stdout", s)

View file

@ -1,7 +1,6 @@
package io.xpipe.app.util;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.ExternalApplicationType;
import io.xpipe.app.prefs.ExternalEditorType;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
@ -15,7 +14,7 @@ public class LocalShellCache extends ShellControlCache {
super(shellControl);
}
public Optional<Path> getVsCodePath() {
public Optional<Path> getVsCodeCliPath() {
if (!has("codePath")) {
try {
var app =
@ -25,8 +24,8 @@ public class LocalShellCache extends ShellControlCache {
.map(s -> Path.of(s));
}
case OsType.MacOs macOs -> {
yield new ExternalApplicationType.MacApplication(
"app.vscode", "Visual Studio Code") {}.findApp();
yield CommandSupport.findProgram(getShellControl(), "code")
.map(s -> Path.of(s));
}
case OsType.Windows windows -> {
yield ExternalEditorType.VSCODE_WINDOWS.findExecutable();

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